本章内存规划

内核的结构、功能和加载
内核的结构和功能
- 头部:记录各个段的汇编位置,这些统计数据用于告诉初始化代码如何加载内核
;以下常量定义部分。内核的大部分内容都应当固定
core_code_seg_sel equ 0x38 ;内核代码段选择子
core_data_seg_sel equ 0x30 ;内核数据段选择子
sys_routine_seg_sel equ 0x28 ;系统公共例程代码段的选择子
video_ram_seg_sel equ 0x20 ;视频显示缓冲区的段选择子
core_stack_seg_sel equ 0x18 ;内核堆栈段选择子
mem_0_4_gb_seg_sel equ 0x08 ;整个0-4GB内存的段的选择子
;-------------------------------------------------------------------------------
;以下是系统核心的头部,用于加载核心程序
core_length dd core_end ;核心程序总长度#00
sys_routine_seg dd section.sys_routine.start
;系统公用例程段位置#04
core_data_seg dd section.core_data.start
;核心数据段位置#08
core_code_seg dd section.core_code.start
;核心代码段位置#0c
core_entry dd start ;核心代码段入口点#10
dw core_code_seg_sel
-
初始化代码:基本就是 MBR 和 Loader(或者叫 BootLoader)了,从BIOS那里接力,安装最基本的段描述符,初始化最初的执行环境,然后从硬盘上读取和加载内核的剩余部分
-
内核代码段:用于分配内存,读取和加载用户程序,控制用户程序的执行
-
内核数据段:提供一段可读写的内存空间供内核使用
-
公共例程段: 就是一些 例程或者叫子程序,供内核或者用户程序调用
![1]()
内核的加载
这部分的内容其实就是关于 MBR 的,所以以下讨论的是 MBR 的编写
-
声明内核的位置,包括 物理地址和逻辑扇区号
core_base_address equ 0x00040000 ;常数,内核加载的起始内存地址 core_start_sector equ 0x00000001 ;常数,内核的起始逻辑扇区号 -
进入保护模式
这里就简单说一下步骤就好了以下设置了存储GDT信息的结构--- 界限+基地址
pgdt dw 0 dd 0x00007e00 ;GDT的物理地址-
设置堆栈,结合我们的内存规划图,堆栈段和MBR以 0x7c00为界线
-
计算GDT所在的逻辑段地址
-
安装段描述符
- 空描述符
- 0-4GB的数据段描述符,方便我们修改访问数据
- 代码段描述符
- 堆栈段描述符
- 显示缓冲区描述符
![2]()
-
加载 GDTR 寄存器
-
打开A20地址线
-
关中断
-
设置 cr0 寄存器的 PE 位
-
跳转到保护模式下的代码
-
-
加载内核
- 初始化段寄存器以访问相应的内存段
; 代码段,我们进入保护模式的跳转语句就已经设置好了 mov eax,0x0008 ;加载数据段(0..4GB)选择子 mov ds,eax mov eax,0x0018 ;加载堆栈段选择子 mov ss,eax xor esp,esp ;堆栈指针 <- 0 ,回滚- 从硬盘读入内核程序到内存,先读一个扇区,包含内核的头部数据,然后就知道总共有多少个扇区要读了
设置好例程 read_hard_disk_0 需要的参数后,读取内核的第一个扇区数据
;以下加载系统核心程序 mov edi,core_base_address mov eax,core_start_sector mov ebx,edi ;起始地址 call read_hard_disk_0 ;以下读取程序的起始部分(一个扇区)然后是判断程序还需要读多少个扇区,这里就不谈了,直接看代码就好了
最后循环读完整个内核就行
- 安装 内核 的段描述符
之前我们已经创建了几个保护模式下的初始的段描述符,然后我们现在要 为内核的各个段创建描述符,简单来说,就是往 GDT 里添加新的描述符
这里需要注意的是,我们需要访问修改 pgdt,但这个标号是在代码段中,代码段是不可写的,所以我们这里使用的是上一章介绍的别名技术,用0-4GB的数据段来访问修改
mov esi,[0x7c00+pgdt+0x02] ;不可以在代码段内寻址pgdt,但可以 ;通过4GB的段来访问![3]()
我们的思路是给出一个段的 段基址、段界限和段的属性, 通过例程拼凑出段描述符,然后安装进 GDT 中
段基址由 标号的汇编地址+内核的加载地址 得出
段界限由 后一个段的标号-当前段的标号-1 得出
段属性直接给出就行下面给个例子:
;建立公用例程段描述符 mov eax,[edi+0x04] ;公用例程代码段起始汇编地址 mov ebx,[edi+0x08] ;核心数据段汇编地址 sub ebx,eax dec ebx ;公用例程段界限 add eax,edi ;公用例程段基地址 mov ecx,0x00409800 ;字节粒度的代码段描述符 call make_gdt_descriptor ;调用例程拼凑段描述符 mov [esi+0x28],eax ;安装段描述符 低32位 mov [esi+0x2c],edx ;安装段描述符 高32位安装完后的GDT如下所示
![4]()
安装完各个段的描述符后,还需要修改 pgdt 的内容(界限),然后加载 GDTR 寄存器使修改生效,最后将控制权交给内核
mov word [0x7c00+pgdt],63 ;描述符表的界限 lgdt [0x7c00+pgdt] ;加载 GDTR 寄存器 jmp far [edi+0x10] ;跳转到内核的入口点,结合内核的头部组成图来看
用户程序的加载和重定位
内核中执行的代码其实挺短的,就本章节的例子而言
内核加载用户程序的流程
mov esi,50 ;用户程序位于逻辑50扇区
call load_relocate_program
mov [esp_pointer],esp ;临时保存堆栈指针
mov ds,ax
jmp far [0x10] ;控制权交给用户程序(入口点)
;堆栈可能切换
return_point: ;用户程序返回点
mov eax,core_data_seg_sel ;使ds指向核心数据段
mov ds,eax
mov eax,core_stack_seg_sel ;切换回内核自己的堆栈
mov ss,eax
mov esp,[esp_pointer]
;这里可以放置清除用户程序各种描述符的指令
;也可以加载并启动其它程序
hlt
代码段的主要内容就是上面这些。让我们看看内核做了什么
1.首先,我们给出用户程序的逻辑扇区号,然后调用例程加载和重定位用户程序
2.然后,我们将内核的堆栈指针保存到指定位置
3.然后,让ds指向用户程序头部,跳转到用户程序的入口点,移交控制权
4.用户程序返回(结束)后,内核切换ds和ss到自己的数据段和堆栈段
然后内核再重复以上或者做其他事情
用户程序的结构
主要是头部的结构,基本上和之前的差不多,唯一不同的就是,因为我们的用户程序能够调用内核提供的例程,所以我们有了这么一个 “符号地址检索表”
;符号地址检索表
salt_items dd (header_end-salt)/256 ;#0x24
salt: ;#0x28
PrintString db '@PrintString'
times 256-($-PrintString) db 0
TerminateProgram db '@TerminateProgram'
times 256-($-TerminateProgram) db 0
ReadDiskData db '@ReadDiskData'
times 256-($-ReadDiskData) db 0
我们要做的就是将各个标号处的字符串替换成对应例程的地址,让我们能够在用户程序中调用例程,后面会详细介绍,这里就不过多介绍了。暂时只需要知道这一段内容是为了让用户程序能够调用内核的例程
计算用户程序占用的扇区数
预读一个扇区的内容,这个扇区包含用户程序头部的数据,然后在循环读取就行了
已经挺熟练了,就不多讲了
简单的动态内存分配
其实相当简陋,主要由 例程 allocate_memory 实现
内核数据段中 ram_alloc dd 0x00100000 ;下次分配内存时的起始地址,这个ram_alloc标号处就存储着 内核下次分配内存时的起始地址,就是由allocate_memory计算出来的
不多提及了,看例程的具体实现就行
段的重定位和描述符的创建
根据前面读取到的用户程序头部信息,创建段的描述符(已经在保护模式下,段的重定位和创建描述符算是重合了吧),主要由例程 make_seg_descriptor 创建,set_up_gdt_descriptor 安装
重定位用户程序内的符号地址
前面几个步骤虽然讲的是 内核加载用户程序,但其实和 MBR加载内核 还是很相似的,这个步骤是为了用户程序能够使用内核创建的例程
在内核数据段中也有一个“符号地址检索表”
;符号地址检索表
salt:
salt_1 db '@PrintString'
times 256-($-salt_1) db 0
dd put_string
dw sys_routine_seg_sel
salt_2 db '@ReadDiskData'
times 256-($-salt_2) db 0
dd read_hard_disk_0
dw sys_routine_seg_sel
salt_3 db '@PrintDwordAsHexString'
times 256-($-salt_3) db 0
dd put_hex_dword
dw sys_routine_seg_sel
salt_4 db '@TerminateProgram'
times 256-($-salt_4) db 0
dd return_point
dw core_code_seg_sel
salt_item_len equ $-salt_4
salt_items equ ($-salt)/salt_item_len
不过和用户程序中的稍有区别------内核中的每一项都有具体的入口地址,用户程序中的是没有的,我们要做的就是用具体地址替换掉用户程序中的字符串

算法实现也很简单,就是用循环,我们从用户程序的表中取出一项,然后循环对比内核中的表(比较的是字符串是否相等), 相等,将地址填入,不相等就继续找,也就是说外循环是用户程序,内循环是内核

执行用户程序
- 初始化各个寄存器
mov eax,ds
mov fs,eax ;fs指向的是用户程序的头部,后续用fs来调用内核的例程
mov eax,[stack_seg]
mov ss,eax
mov esp,0
mov eax,[data_seg]
mov ds,eax
- 调用内核例程
mov ebx,message_1
call far [fs:PrintString] ;fs指向用户程序头部,标号PrintString处有具体的例程入口地址
- 程序结束,控制权返回给系统
jmp far [fs:TerminateProgram] ;将控制权返回到系统
;TerminateProgram例程是返回内核中的 return_point 的位置
例程
read_hard_disk_0
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
;EAX=逻辑扇区号
;DS:EBX=目标缓冲区地址
;返回:EBX=EBX+512
make_gdt_descriptor
make_gdt_descriptor: ;构造描述符
;输入:EAX=线性基地址
; EBX=段界限
; ECX=属性(各属性位都在原始
; 位置,其它没用到的位置0)
;返回:EDX:EAX=完整的描述符
load_relocate_program
load_relocate_program: ;加载并重定位用户程序
;输入:ESI=起始逻辑扇区号
;返回:AX=指向用户程序头部的选择子
allocate_memory
allocate_memory: ;分配内存
;输入:ECX=希望分配的字节数
;输出:ECX=起始线性地址
make_seg_descriptor
make_seg_descriptor: ;构造存储器和系统的段描述符
;输入:EAX=线性基地址
; EBX=段界限
; ECX=属性。各属性位都在原始
; 位置,无关的位清零
;返回:EDX:EAX=描述符
set_up_gdt_descriptor
set_up_gdt_descriptor: ;在GDT内安装一个新的描述符
;输入:EDX:EAX=描述符
;输出:CX=描述符的选择子




posted on
浙公网安备 33010602011771号