写在前面:自制操作系统Gos 第二章第八篇:主要内容是如何加载内核
Gos完整代码:Github
前面我们不是在MBR引导程序工作就是在Loader内核加载器中工作。在开启分页机制之后,其实我们下一步做的工作也就是加载内核了。
这里提前剧透一下加载内核的步骤:
- 加载内核:其实也就是把内核文件从硬盘上加载到内存中
- 初始化内核:在开启分页机制之后,将加载进来的elf内核文件放入相应的虚拟内存地址,然后跳过去执行就可以了
加载内核
第一步就是加载内核了。但是在加载之前,我们需要考虑把内核加载到哪里呢?
首先,我们要明白,实际我们现在还是在实模式下的1M空间,并不是开启分页后拥有1GB那么大的空间。所以其实内核的安身立命的物理地址其实还在这1M空间,我们先来看看这个空间都是怎么分布的吧:
可用看到,相对空间大一点且没有用的空间就是0x7C00~0x9FBFF
这段空间啦,取个整,选0x7000
这段空间就好了。选好空间之后,我们直接调用之前写好的rd_disk_m_32
函数从磁盘读取文件就好了。
注:MBR加载区的内容已经没有了,我们可以直接覆盖啦
KERNEL_BIN_BASE_ADDR equ 0x70000 ;kernel.bin被加载的内存位置
KERNEL_START_SECTOR equ 0x9 ;内核程序在磁盘中的扇区号起始位置
; ------------------------- 加载kernel ----------------------
;这里起始就是从9号扇区开始读取200个扇区,读到KERNEL_BIN_BASE_ADDR
mov eax, KERNEL_START_SECTOR ; kernel.bin所在的扇区号
mov ebx, KERNEL_BIN_BASE_ADDR ; 从磁盘读出后,写入到ebx指定的地址
mov ecx, 200 ; 读入的扇区数
call rd_disk_m_32
初始化内核
内核kernel.bin文件呢,其实本身也就是而执行可执行文件。其包含了内核程序中代码段、数据段、文件头等等信息的集合。还记得我说过守约是操作系统设计的精髓了么?
我们现在要做的事情就是根据ELF约定去吧内核文件中的段复制到内存的相应位置就可以了。
注:ELF文件的详细信息我放到博客目标文件详解了,没有相关知识的同学可以查阅一下
在正式初始化内核之前,我们要确定一个点,那就是我们的内核初始化执行的入口地址在哪里呢?
一开始我们的设置loader.bin的位置是0x900,再加上GDT部分的大小,那么我们把地址设置到0x1500没有问题吧。同时,我们设计低端1M的虚拟内存和物理内存是一 一对应的,所以物理内存的0x1500
其实对应的是0xc0001500
。
所以我们进行如下定义:
KERNEL_ENTRY_POINT equ 0xc0001500
这个点其实就是我们内核的执行位置(内核在0x70000
)了,最终我们需要通过绝对跳转jmp
过去:
enter_kernel:
call kernel_init ;按照elf格式初始化内核bin文件
mov esp, 0xc009f000 ;初始化栈指针
jmp KERNEL_ENTRY_POINT ;跳转过去,执行内核程序
好了,我们现在已经知道了怎么去执行内核程序了。但是还有一个点没有做:
- 函数kernel_init到底做了什么
函数kernel_init
内核kernel.bin是一个可执行程序,对于可执行程序来说,我们只对其中的段感兴趣,因为它们才是程序运行的实质指令和数据所在的地方。所以这个函数的主要作用就是把段加载到内存中该加载的位置。
内核在KERNEL_BIN_BASE_ADDR
这个位置,按照 elf 文件的约定,其实这个地方就是 elf 文件头。从这个位置开始就是各个段了,那我们首先就是要获得这个起始位置:
xor ebx, ebx ;ebx记录程序头表地址
mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; 偏移文件开始部分28字节的地方是e_phoff,表示第1 个program header在文件中的偏移量
add ebx, KERNEL_BIN_BASE_ADDR
而我们要加载其他段的方式是每次增加一个段头的大小,之后分析其段类型是不是PT_NULL,如果不是就将其拷贝到编译的地址中:
//PT_NULL equ 0
mov dx, [KERNEL_BIN_BASE_ADDR + 42] ; 偏移文件42字节处的属性是e_phentsize,表示program header大小
cmp byte [ebx + 0], PT_NULL ; 若p_type等于 PT_NULL,说明此program header未使用。
je .PTNULL
;为函数memcpy压入参数,参数是从右往左依然压入.函数原型类似于 memcpy(dst,src,size)
push dword [ebx + 16] ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size
mov eax, [ebx + 4] ; 距程序头偏移量为4字节的位置是p_offset
add eax, KERNEL_BIN_BASE_ADDR ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址
push eax ; 压入函数memcpy的第二个参数:源地址
push dword [ebx + 8] ; 压入函数memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr,这就是目的地址
call mem_cpy ; 调用mem_cpy完成段复制
add esp,12 ; 清理栈中压入的三个参数
mem_cpy函数做的就是逐字节拷贝,逻辑比较简单这里就不放代码了。以上汇编代码其实就完成了一个段的拷贝,那么我们就可以接着比较下个段了:
add ebx, edx ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header
loop .each_segment
这样咱们就完成了kernel.bin文件的内存拷贝工作,它现在正式的存在在了我们的内存中!
至此,我们的内存中的内容就变成了下图:
注:内核文件kernel.bin和mbr在初始化之后是可以覆盖的
参考文件
[1] 操作系统真相还原