保护模式内核代码赏析
进入保护模式内核后,还有很多和体系结构相关的工作需要准备。 下面我们就利用bochs的调试功能,对这部分代码作一些分析。
具体如何进入到这部分调试界面,可参考bootloader如何加载bzImage
计算当前内核被加载的地址
进入保护模式内核后,有这么一段代码是用来计算保护模式内核被加载到哪里,并把这个地址保存到epb中。 arch/x86/boot/compressed/head_64.S
leal (BP_scratch+4)(%esi), %esp
call 1f
1: popl %ebp
subl $ rva(1b), %ebp原理是call的短调用,会将下一条指令的地址压栈。也就是popl %ebp的地址会被压栈。而rva(1b)是这条指令相对于startup_32的偏移,所以两者相减就得到了本次运行个过程中,实际被加载到的地址。
现在我们就用bochs来验证一下,看看保护模式的内核是不是加载到了0x100000。
(0) Breakpoint 2, 0x0000000000100000 in ?? ()
Next at t=90656425
(0) [0x0000000000100000] 0010:0000000000100000 (unk. ctxt): cld ; fc
<bochs:7> u /10
00100000: ( ): cld ; fc
00100001: ( ): cli ; fa
00100002: ( ): lea esp, dword ptr ds:[esi+488] ; 8da6e8010000
00100008: ( ): call .+0 ; e800000000
0010000d: ( ): pop ebp ; 5d
0010000e: ( ): sub ebp, 0x0000000d ; 83ed0d在0x100000处断点停止后,先看一下反汇编。popl %ebp指令的地址是0x10000d。而这条指令的相对地址是0x0d。接下来我们来确认压栈的地址,和最后计算的结果。
在执行减法前,查看一下寄存器。发现ebp的值已经是0x10000d了。
接下去再执行一条,也就是减去0x0d,ebp的值就是0x100000了。这就是我们这次保护模式内核被加载的地址,也是我们想要的。
我后来想到,这么计算还需要依赖一个条件,就是代码段的基址需要是0。否则计算出的值需要加上基址才是。那就来看看当前的代码段设置。
看来确实是0。
切换GDT
紧接着内核切换了gdt.
而这里的gdt定义是
所以,gdt这块区域的开头几个字节是用作gdtr的,而且16位的界限已经定义好了。但是基址没有定义,因为运行时的地址我们事先不知道。但是还记得我们刚才分析的代码么?对了,就是后把当前加载的地址保存到了ebp。所以这第一句 leal rva(gdt)(%ebp), %eax就是计算了gdt的基址。
分析完了,那就用bochs调试一下看看。我们在leal这句执行完后,查看一下。
说明gdt的地址在0xb2a010,我们用内存查看工具查看一下。
其中0x2f是界限,意思是gdt的大小是0x2f + 0x01 = 0x30 也就是有48字节。而我们gdt中,正好占了48字节,符合! 再来看dump出来的第二块8字节,因为x86是little endian的,所以实际值就是0x00cf9a000000ffff。这个不正好和__KERNEL32_CS所应以的值一样吗?完美!
执行完movl %eax, 2(%eax)后,我们再查看一下内存。因为gdt的最开始两个字节是界限,我们跳过这部分,直接dump后面的四个字节。
显示此时这部分内存的内容已经是我们刚才所得到的gdt基址0x00b2a010了。
切换gdt的前后,我们都查看一下gdt。
这里可以看到,gdt确实发生了变化。大功告成!
计算解压缩内核用的地址
接下来有一段看着很长,实际功能简单的代码。注释上说计算解压缩的内核的之,并保存到ebx中。
我翻译了一下代码,并写道了注释里。如果用c来写,其实就是这么一句。
那这个alignment是多少呢?这个值是从BP_kernel_alignment(%esi)这个地址取出来的。这个esi实际指向了bootparam,而BP_kernel_alignment是其中hdr里kernel_alignemnt的字段。看了下arch/x86/boot/header.S,这个值是在编译的时候定义号的。
而这个CONFIG_PHYSICAL_ALIGN是一个配置项,看了下当前配置的值是0x200000。也就是2M。
分析完了,我们用bochs来验证一下:
kernel_alignment是不是0x200000
计算完后,ebx是不是等于ebp向上2M对齐。ebp是刚才计算出来保护模式内核的加载地址1M,所以ebx预期也是2M
在从kernel_alignment取值前后各查看了eax。可以看出,kernel_alignment确实是0x200000。
然后我们确认一下ebx的值。
我们在cmp这条语句断点,查看rbx确实也是0x200000。
这时候我发现了一个定义,
这也是一个内核配置,默认是16M。所以我们辛辛苦苦算了半天,最后因为ebx小于16M,导致ebx还是强制设置成了16M。 好吧,白看了半天。
直接从startup_64开始debug
32位下的工作确实有限,我们也不能每次都从startup_32开始一步步debug。那怎么直接设置断点到startup_64呢?其实答案就在代码里。
也就是说,startup_64的偏移一定是0x200(又是这512字节)。应为startup_32的偏移是0,所以如果要直接断点在startup_64,可以在0x100200地址设置断点。我们来验证一下:
瞧,正如我们所料。
移动压缩内核
在bzImage的全貌中,我们看到安装的内核是被压缩过,再打包的。而为了解压缩,需要先把已加载的,压缩内核搬移到新的位置,再来做解压缩。
其中rbx是搬移后压缩内核的起始地址,但是为了避免在搬运过程中破坏内存,所以是从高地址到低地址搬运。好了,我们搬运完后来查看一下。
搬运先查看一下寄存器,rbx的值是0x0359a000。也就是一会儿我们预期把压缩的内核搬运到这个地址。并且我们先反汇编这个地址,发现此时地址内的内容都是0。
搬运完后,我们再反汇编一下。发现此时已经有内容了。而且可以对比一下,这段代码和startup_32正好一样!
接下来内核就会跳转到 rbx + Lrelocated继续运行。因为rbx就是刚才新搬运的地址0x0359a000,所以从这里开始内核就在新的地址运行了。差不多是在53M左右的地址空间。
Last updated
Was this helpful?