第六步 设计我们的LOADER——加载内核


查看系列文章点这里: 操作系统真象还原

前言

  我们在前面利用 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小一点,要么改变内存布局,不过咱们既然找到也确定了之前崩溃的原因,就先暂时采取第一种方法,等之后完成全部内容后再尝试重新设计内存分布(指不定以后还会出现类似的情况)。


  持续更新中~~

  • 33
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值