查看系列文章点这里: 操作系统真象还原
前言
我们在前面利用 LOADER 进入了保护模式、开启了分页机制,如今也到了它交出控制权的时候了,接下来我们将完善 LOADER,看看它是如何将控制权交给内核的。
一、如何加载内核?
加载内核嘛,其实就跟前面加载 MBR、LOADER 一样,做好准备工作,然后找到入口地址,跳转过去就行了。要做的准备工作也很简单,设置一下寄存器,将内核代码从磁盘赋值到内存上,就这么简单。
只不过稍微有点点不一样的是,我们的内核是 kernel.bin ,我们要加载的就是它,但它是elf格式的,并不全是我们编写的代码,因此在跳转之前,我们要解析它,将可以执行部分提取出来。
稍微解释一下elf格式吧,我们将来实现的内核虽然很小,但也是有很多个源文件编译、链接得来的,要将这么多源文件整合起来,没点辅助信息是不可能的,带有辅助信息的源文件就是elf格式(当然是Linux环境下)。这个辅助的信息也叫做程序头部,就在文件最开始的部分,格式是固定的。我们用到的辅助信息不多也很简单,只介绍用到的,如果大家感兴趣可以自己搜来学习一下。
LOADER 要做的就是把 kernel.bin 加载到内存中正确的地方,所以说我们需要的信息很简单,通俗一点来说就是,我们需要知道一共有几个程序段,每个程序段从哪里来到哪里去,每个程序段有多长。就这么多,是不是看起来很简单,其实本身也不难,具体这些信息在文件的哪个位置,叫啥名字,我们在代码里在详细说。
接下来我们说一下我们内核在哪里,首先我们在编译、链接完后只能将其放在磁盘里,所以首先需要 LOADER 将其从硬盘中读取到内存中(0x70000,开启分页机制前读取),然后再解析它,将真正的内核代码提取出来放到咱们内核的入口地址(0xc0001500,开启分页机制后解析)。内存位置的选择其实随便,主要不冲突进行,之所以在开启分页机制前读取是因为这样可以直接粘贴 MBR 中的代码,稍微修改一下进行,图个方便嘛,大家肯定能理解!
要做什么我们已经搞清楚了,接下来我们看看怎么实现叭!
二、实践检验!
首先是 boot.inc 中一些新增常量的定义,如下:
;------------------------------------
; ELF 段相关值
;------------------------------------
PT_NULL equ 0
;------------------------------------
; 内核属性
;------------------------------------
;kernel.bin 所在的扇区号
KERNEL_START_SECTOR equ 0x9
;从磁盘读出后要写到内存中的地址
KERNEL_BIN_BASE_ADDR equ 0x70000
;解析完后要写到内存中的地址
KERNEL_ENTRY_POINT equ 0xc0001500
关于我们是怎样解析elf文件的,都在注释中说明,很简单,直接给出loader.S 新增的部分,如下:
......
;============================================================
;进入内核
;============================================================
;强制刷新流水线,更新gdt
jmp SELECTOR_CODE:enter_kernel
enter_kernel:
call kernel_init
mov esp, 0xc009f000 ;进入内核后,设置新的栈顶
mov byte [gs:0x320], '6'
mov byte [gs:0x321], 0xA4
mov byte [gs:0x322], ' '
mov byte [gs:0x323], 0xA4
mov byte [gs:0x324], 'L'
mov byte [gs:0x325], 0xA4
mov byte [gs:0x326], 'O'
mov byte [gs:0x327], 0xA4
mov byte [gs:0x328], 'A'
mov byte [gs:0x329], 0xA4
mov byte [gs:0x32a], 'D'
mov byte [gs:0x32b], 0xA4
mov byte [gs:0x32c], ' '
mov byte [gs:0x32d], 0xA4
mov byte [gs:0x32e], 'K'
mov byte [gs:0x32f], 0xA4
mov byte [gs:0x330], 'E'
mov byte [gs:0x331], 0xA4
mov byte [gs:0x332], 'R'
mov byte [gs:0x333], 0xA4
mov byte [gs:0x334], 'N'
mov byte [gs:0x335], 0xA4
mov byte [gs:0x336], 'E'
mov byte [gs:0x337], 0xA4
mov byte [gs:0x338], 'L'
mov byte [gs:0x339], 0xA4
jmp KERNEL_ENTRY_POINT ;跳转到内核
; ============================================================
; 创建页目录及页表
; ============================================================
......
; ============================================================
; 功能:读取硬盘n(由ecx寄存器决定读几个扇区)个扇区
; ============================================================
rd_disk_m_32:
;第一步:设置要读取的扇区数
mov esi, eax
mov dx, 0x1f2
mov al, cl
out dx, al
mov eax, esi
;第二步,将LBA地址存入0x1f3 ~ 0x1f6
;LBA地址7~0位写入端口0x1f3
mov dx, 0x1f3
out dx, al
;LBA地址15~8位写入端口0x1f4
push cx
mov cl, 8
shr eax, cl
mov dx, 0x1f4
out dx, al
;LBA地址23~16位写入端口0x1f5
shr eax, cl
mov dx, 0x1f5
out dx, al
;设置device
shr eax, cl
and al, 0x0f ;设置LB地址24~27位
or al, 0xe0 ;0xe0 -> 11100000 ,设置7~4位为1110,表示为LBA模式
mov dx, 0x1f6
out dx, al ;将配置信息写入端口0x1f6
;第三步:向0x1f7端口写入读命令(0x20)
mov dx, 0x1f7
mov al, 0x20
out dx, al
;第四步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令,读时表示读入硬盘状态
nop
in al, dx
and al, 0x88 ;第3位为1表示硬盘控制器已经准备好数据传输
;第7位为1表示硬盘忙
cmp al, 0x08
jnz .not_ready ;若为准备好,则跳回not_ready处继续等待
;第五步:从0x1f0端口读入数据
pop ax ; cx -> ax
mov dx, 256 ;一个扇区512字节,每次读入一字,即两字节,共要读256次
mul dx
mov cx, ax
mov dx, 0x1f0
.go_on_read:
in ax, dx
mov [ebx], ax
add ebx, 2
loop .go_on_read
ret
; ============================================================
; 将kernel.bin中的segment拷贝到编译的地址
; ============================================================
kernel_init:
xor eax, eax
xor ebx, ebx ;ebx记录程序头表的地址
xor ecx, ecx ;ecx记录程序头表中的program header数量
xor edx, edx ;edx记录program header的尺寸,即e_phentsize
; ---------------------------------------------
; 提取 ELF 文件头中的程序头信息
; ---------------------------------------------
;e_phentsize(2 字节)-> 程序头条目的大小
mov dx, [KERNEL_BIN_BASE_ADDR + 42]
;e_phoff(4 字节)-> 程序头表的起始位置
mov ebx, [KERNEL_BIN_BASE_ADDR + 28]
add ebx, KERNEL_BIN_BASE_ADDR
;e_phnum(2 字节)-> 程序头表的条目计数
mov cx, [KERNEL_BIN_BASE_ADDR + 44]
; ---------------------------------------------
; 处理ELF文件中的每个段
; ---------------------------------------------
.each_segment:
;p_type 等于 PT_NULL 说明此program header未使用
cmp byte [ebx+0], PT_NULL
je .PT_NULL
;为函数mem_cpy(dst,src,size)压入参数
;第三个参数,p_filesz = size
push dword [ebx+16]
;第二个参数,p_offset + base_addr = src addr
mov eax, [ebx+4]
add eax, KERNEL_BIN_BASE_ADDR
push eax
;第三个参数,p_vaddr = dest addr
push dword [ebx+8]
call mem_cpy
;清理栈中压入的三个参数
add esp, 12
.PT_NULL:
;edx记录program header的尺寸,故ebx指向下一个program header
add ebx, edx
loop .each_segment
ret
; ---------------------------------------------
; 逐字节拷贝函数mem_cpy(dst,src,size)
; ---------------------------------------------
mem_cpy:
; 将方向标志位设置为0,这意味着传输方向是朝高地址的方向
cld
push ebp ;入栈备份
mov ebp, esp
push ecx ;入栈备份
mov edi, [ebp+8] ;第一个参数dst
mov esi, [ebp+12] ;第二个参数src
mov ecx, [ebp+16] ;第三个参数size
;重复执行 movsb 指令,重复次数为 ecx 的值
rep movsb
;恢复环境
pop ecx
pop ebp
ret
到这里 LOADER 已经结束了,但是好像还少个内核,老方法,先伪造一个假的,在code目录下新建文件夹kernel,在目录kernel下新建main.c,内容如下:
int main(void){
while(1);
return 0;
}
编译、连接、拷贝!
nasm -I ./include/ -o mbr.bin mbr.S &&
dd if=./mbr.bin of=../bochs/hd60M.img bs=512 count=1 conv=notrunc &&
nasm -I ./include/ -o loader.bin loader.S &&
dd if=./loader.bin of=../bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc &&
gcc -m32 -c -o ./kernel/main.o ./kernel/main.c &&
ld -m elf_i386 ./kernel/main.o -Ttext 0xc0001500 -e main -o ./kernel/kernel.bin &&
dd if=./kernel/kernel.bin of=../bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
结果如下:
我在测试的时候遇到了一个问题,以前输出的提示信息是很详细的,这次在测试的时候,程序没有正常执行,检查发现,是文件大小超过了我们之前在 MBR 中定义的四个扇区的大小,于是我改成了五个扇区,还是不行,经过不停的测试,确认是 kernel_init 函数中出问题了,程序跳转到未知的地方了。然后又发现只要少输出一点提示信息,就能成功运行,即便刚刚减少到2048字节也不行,必须要减小到1800字节以下才可以。我现在也没有想明白,如果谁知道为什么,希望能告诉我一下,谢谢!
破案了,用断点一点点试,最后确定了是 rep movsb 这条指令执行出错了,然后就开始了单步执行,最后在这个地方出错了,如下图所示:
可以看到,正常执行的时候(蓝色框框),只要ecx寄存器值不为0,那么下一条待执行的指令就与当前指令相同,但是在过程当中出现了错误(红色框框),ecx值不为0,但指令发生了变化。通过查看edi寄存器(目的地址)的值,我们可以发现,是在数据传输的过程中,我们将 LOADER 的代码覆盖掉了,然后导致了错误发生。
知道错误原因了,就来分析一下为什么吧,LOADER 的首地址是0x900,内核映像 的入口地址是0x1500。首地址和入口地址不同,我们在链接的时候,指定了内核的入口地址,那么就只能保证入口地址是我们设置的值,却不能保证入口地址就是首地址,也就是说 内核映像 的首地址在0x1500以前,我们在 rep movsb 指令处设置断电,并查看寄存器的值,如下所示:
可以看到 内核映像 的首地址是0x1000(和书上说的一致),0x1000 - 0x900 = 0x700,换成十进制也就是1792,也就是说 LOADER 不能超过1792字节,不然就会出现覆盖,就会导致程序崩溃,这与我之前测试发现大小小于1800才能成功这一现象也是吻合的。
所以,要想程序不崩溃,要么 LOADER小一点,要么改变内存布局,不过咱们既然找到也确定了之前崩溃的原因,就先暂时采取第一种方法,等之后完成全部内容后再尝试重新设计内存分布(指不定以后还会出现类似的情况)。
持续更新中~~