LAB_1_Part2_The Boot Loader

1. LAB_1_Booting a PC

1.1. Part 2: The Boot Loader

对于6.828,我们将使用传统的硬盘启动机制,这意味着我们的boot loader必须满足于512字节。

boot loader由一个汇编语言源文件boot / boot.S和一个C源文件boot / main.c组成。

1.1.1. boot.S

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, testAND指令唯一不同的地方是,TEST 指令不修改目标操作数。

test al, 00001001b ;测试位 0 和位 3
  1. lgdt gdtdesc, 加载全局描述符表,暂时不管全局描述表是如何生成的。
  2. cr0, control register,控制寄存器。

CR0中包含了6个预定义标志,0位是保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。

  1. ljmp $PROT_MODE_CSEG, $protcseg,

PROT_MODE_CSEG = 8 ,这个值好像是很有讲究的,在《自己动手写操作系统》这本书里面看到过。因为此时已经进入了32位实模式,此时的8不再是实模式下简单的cs了,貌似与GDT有关,当时觉得GDT的初始化贼复杂,此处先不深究。

1.1.2. 调试boot.S

在一个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, 不过我觉得暂时不需要分析得这么细。

1.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是:

  • .text : 可执行指令
  • .rodata: 只读数据段,例如字符串常量。(但是,我们不会费心设置硬件来禁止写入。)
  • .data : 存放已经初始化的数据
  • .bss : 存放未初始化的变量, 但是在ELF中只需要记录.bss的起始地址和长度。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 打断点后,csi一次,发现实际跳转地址位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

1.1.4. Exercise 6

在BIOS进入Boot loader时检查内存的8个字在0x00100000处,然后在引导加载程序进入内核时再次检查。 他们为什么不同? 第二个断点有什么? (你真的不需要用QEMU来回答这个问题。试想一下)

答案应该很明显,在BIOS进入Boot loader时,0x100000内存后的8个字都为零,因为此时内核程序还没有加载进入内存。 内核的加载在bootmain函数中完成。

若需要用gdb调试,可以使用x/8x 0x100000 查看其内存内容。

你可能感兴趣的:(MIT6.828操作系统)