对于6.828,我们将使用传统的硬盘启动机制,这意味着我们的boot loader
必须满足于512字节。
boot loader由一个汇编语言源文件boot / boot.S
和一个C源文件boot / main.c
组成。
BIOS将boot.S
这段代码从硬盘的第一个扇区load到物理地址为0x7c00
的位置,同时CPU工作在real mode
。
boot.S
需要将CPU的工作模式从实模式转换到32位的保护模式, 并且 jump 到 C 语言程序。
源码阅读,知识点:
1.cli (clear interrupt)
2. cld (clear direction flag)
df: 方向标志位。在串处理指令中,控制每次操作后si,di的增减。(df=0,每次操作后si、di递增;df=1,每次操作后si、di递减)。
为了向前兼容早期的PC机,A20
地址线接地,所以当地址大于1M范围时,会默认回滚到0处。所以在转向32位模式之前,需要使能A20
。
3. test 逻辑运算指令,对两个操作数进行AND
操作,并且修改PSW
, test
与 AND
指令唯一不同的地方是,TEST
指令不修改目标操作数。
test al, 00001001b ;测试位 0 和位 3
lgdt gdtdesc
, 加载全局描述符表,暂时不管全局描述表是如何生成的。cr0
, control register,控制寄存器。CR0中包含了6个预定义标志,0位是保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。
ljmp $PROT_MODE_CSEG, $protcseg
,PROT_MODE_CSEG = 8 ,这个值好像是很有讲究的,在《自己动手写操作系统》这本书里面看到过。因为此时已经进入了32位实模式,此时的8
不再是实模式下简单的cs
了,貌似与GDT有关,当时觉得GDT的初始化贼复杂,此处先不深究。
在一个terminal
中cd到lab
目录下,执行 make qemu-gdb
。再开一个 terminal
执行make gdb
。
因为BIOS会把boot loader加载到0x7c00的位置,因此设置断点b *0x7c00
。再执行c
,会看到QUMU终端上显示Booting from hard disk
。
执行x/30i 0x7c00
就能看到与boot.S
中类似的汇编代码了。
这篇文章里有详细的调试过程MIT 6.828 JOS学习笔记5. Exercise 1.3, 不过我觉得暂时不需要分析得这么细。
接下来我们分析boot loader
的C语言部分。
首先熟悉以下C指针。 编译运行pointer.c
结果。 可以发现 a[]
,b
的地址相差很多,因为两者所存放的段不同。
1: a = 0xbfa8bdbc, b = 0x9e3a160, c = (nil)
2: a[0] = 200, a[1] = 101, a[2] = 102, a[3] = 103
3: a[0] = 200, a[1] = 300, a[2] = 301, a[3] = 302
4: a[0] = 200, a[1] = 400, a[2] = 301, a[3] = 302
5: a[0] = 200, a[1] = 128144, a[2] = 256, a[3] = 302
// b = a + 4
6: a = 0xbfa8bdbc, b = 0xbfa8bdc0, c = 0xbfa8bdbd
ELF格式非常强大和复杂,但大多数复杂的部分都是为了支持共享库的动态加载,在6.828课程中并不会用到。在本课程中,我们可以把ELF可执行文件简单地看为带有加载信息的标头,后跟几个程序部分,每个程序部分都是一个连续的代码块或数据,其将被加载到指定内存中。
我们所需要关心的Program Section是:
Loader
and program
必须自己将.bss段清零。每个程序头的ph-> p_pa
字段包含段的目标物理地址(在这种情况下,它实际上是一个物理地址,尽管ELF规范对该字段的实际含义含糊不清)
BIOS会将引导扇区的内容加载到 0x7c00 的位置,引导程序也就从0x7C00的位置开始执行。我们通过-Ttext 0x7C00
将链接地址传递给boot / Makefrag
中的链接器,因此链接器将在生成的代码中生成正确的内存地址。
除了部分信息之外,ELF头中还有一个对我们很重要的字段,名为e_entry
。该字段保存程序中入口点的链接地址:程序应该开始执行的代码段的存储地址。 在反汇编代码中,可以看到最后call 了 0x10018地址。
((void (*)(void)) (ELFHDR->e_entry))();
7d6b: ff 15 18 00 01 00 call *0x10018
在0x7d6b 打断点后,c
再si
一次,发现实际跳转地址位0x10000c
(gdb) b *0x7d6b
Breakpoint 3 at 0x7d6b
(gdb) c
Continuing.
=> 0x7d6b: call *0x10018
Breakpoint 3, 0x00007d6b in ?? ()
(gdb) si
=> 0x10000c: movw $0x1234,0x472
与实际执行objdump -f kernel
的 结果一致。
../kern/kernel: file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0010000c
在BIOS进入Boot loader时检查内存的8个字在0x00100000处,然后在引导加载程序进入内核时再次检查。 他们为什么不同? 第二个断点有什么? (你真的不需要用QEMU来回答这个问题。试想一下)
答案应该很明显,在BIOS进入Boot loader时,0x100000内存后的8个字都为零,因为此时内核程序还没有加载进入内存。 内核的加载在bootmain
函数中完成。
若需要用gdb调试,可以使用x/8x 0x100000
查看其内存内容。