MIT6.828 LAB1_Part1-2 启动 PC


Part1:PC Bootstrap

简介

实验分为三个部分:

  • 熟悉汇编语言、QEMU x86模拟器、PC上电启动过程
  • 检查我们的6.828内核的boot loader程序,它位于labboot目录下。
  • 深入研究6.828内核本身的初始模板,位于kernel目录下。

【注】 此处已经默认你已经搭建好基本的实验环境。

正式开始!

MIT6.828 官网 Lab 1: Booting a PC

GDB调试中常用指令:

下载源码并且编译执行:


mkdir ~/6.828
cd ~/6.828
git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab

cd lab
# 编译源码
make 
# qemu 模拟x86环境,运行minimal kernel
make qemu
# 如果你是在服务器等其他无图形界面的环境下实验可以执行下面的命令启动
# make qemu-nox 

运行成功的话终端就会打印出以下字符:

6828 decimal is 015254 octal!
Physical memory: 66556K available, base = 640K, extended = 65532K
check_page_alloc() succeeded!
check_page() succeeded!
check_kern_pgdir() succeeded!
check_page_installed_pgdir() succeeded!
[00000000] new env 00001000
Incoming TRAP frame at 0xefffffbc
Incoming TRAP frame at 0xefffffbc
hello, world
Incoming TRAP frame at 0xefffffbc
i am environment 00001000
Incoming TRAP frame at 0xefffffbc
[00001000] exiting gracefully
[00001000] free env 00001000
Destroyed the only environment - nothing more to do!
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.
K> 

键入kerninfo,值得注意的是,此内核监视器“直接”在模拟PC的“原始(虚拟)硬件”上运行。

细节记录

  • PC中BIOS大小为64k, 物理地址范围0x000f0000-0x000fffff
    PC 开机首先0xfffff0处执行 jmp [0xf000,0xe05b] 指令。在gdb中使用si(Step Instruction)进行跟踪。
(gdb) si
[f000:e05b]    0xfe05b:    cmpw   $0xffc8,%cs:(%esi)   # 比较大小,改变PSW
0x0000e05b in ?? ()
(gdb) si
[f000:e062]    0xfe062:    jne    0xd241d416           # 不相等则跳转
0x0000e062 in ?? ()
(gdb) si
[f000:e066]    0xfe066:    xor    %edx,%edx            # 清零edx
0x0000e066 in ?? ()
(gdb) si
[f000:e068]    0xfe068:    mov    %edx,%ss
0x0000e068 in ?? ()
(gdb) si
[f000:e06a]    0xfe06a:    mov    $0x7000,%sp
0x0000e06a in ?? ()

BIOS运行过程中,它设定了中断描述符表,对VGA显示器等设备进行了初始化。在初始化完PCI总线和所有BIOS负责的重要设备后,它就开始搜索软盘、硬盘、或是CD-ROM等可启动的设备。最终,当它找到可引导磁盘时,BIOS从磁盘读取引导加载程序并将控制权转移给它1

Part 2: The Boot Loader

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

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

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

  1. test 逻辑运算指令,对两个操作数进行AND操作,并且修改PSW, testAND 指令唯一不同的地方是,TEST 指令不修改目标操作数。

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

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

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

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

加载内核

接下来我们分析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

Exercise 6

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

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

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


  1. 如果你想进一步了解计算机的完整启动过程,可阅读[计算机是如何启动的?从未上电到操作系统启动](https://www.dingmos.com/index.php/archives/31/)

声明:一丁点儿|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - MIT6.828 LAB1_Part1-2 启动 PC


勿在浮沙筑高台,每天进步一丁点儿!