【连载中|更新于2024.7.31】MIT 6.828/2018 JOS 实验记录 lab1
https://pdos.csail.mit.edu/6.828/2018/labs/lab1/
手动编译实验提供的patch版qemu
https://pdos.csail.mit.edu/6.828/2018/tools.html
1 | git clone https://github.com/mit-pdos/6.828-qemu.git qemu |
之后在x86_64-softmmu中可以找到有编译得到的qemu-system-x86_64程序。
Part 2: The Boot Loader
练习 3:查看实验工具指南,特别是有关 GDB 命令的部分。即使您熟悉 GDB,其中也包含了一些对操作系统工作有用的晦涩 GDB 命令。
在地址 0x7c00 处设置一个断点,这是引导扇区将被加载的位置。继续执行直到该断点。追踪 boot/boot.S 中的代码,使用源代码和反汇编文件 obj/boot/boot.asm 来跟踪您的位置。在 GDB 中使用 x/i 命令反汇编bootloader中的指令序列,并将原始bootloader源代码与 obj/boot/boot.asm 中的反汇编和 GDB 中的内容进行比较。
追踪 boot/main.c 中的 bootmain(),然后进入 readsect()。找出与 readsect() 中每个语句对应的确切汇编指令。继续跟踪 readsect() 的其余部分并返回到 bootmain(),识别从磁盘读取内核剩余扇区的 for 循环的开始和结束。找出循环结束后将运行的代码,在那里设置一个断点,并继续执行到该断点。然后逐步执行bootloader的其余部分。
1. 处理器在何时开始执行32位代码?究竟是什么导致了从16位模式切换到32位模式?
1 | # Jump to next instruction, but in 32-bit code segment. |
在这条ljmp指令后进入32位模式,ljmp 指令(长跳转指令)用于在x86架构中改变代码段选择符和指令指针,从而切换到新的代码段,并且可以在实模式和保护模式下使用。这在模式切换和段切换过程中尤其重要。 ljmp 指令的语法如下
1 | ljmp $segment_selector, $offset |
segment_selector: 指定要跳转到的代码段的段选择符。它在保护模式下通常是一个指向全局描述符表(GDT)中的段描述符的索引。offset: 指定在目标段中的偏移地址。 在x86架构中,GDT(全局描述符表)用于定义内存段的属性,如代码段和数据段。GDT中的每个段描述符都有一个索引,通过段选择符访问。段选择符是一个16位的值,其高13位用于索引GDT中的段描述符,1位标识使用GDT还是LDT,低2位表示权限级别。比如,0x08 是一个段选择符,其索引为1,指向GDT中的代码段描述符,用于在切换到保护模式时初始化段寄存器。
1 | lgdt gdtdesc |
通过向cr0寄存器写入PE(保护模式启动)标志位来打开32位模式。
进入保护模式的必要步骤:
首先,通过 lgdt
指令加载全局描述符表(GDT),以确保有合适的段描述符定义。然后,将控制寄存器 CR0 的 PE 位(保护模式启用位)设置为 1,以准备进入保护模式。接着,执行 ljmp $PROT_MODE_CSEG, $protcseg
指令,完成模式切换。其中,$PROT_MODE_CSEG
是保护模式下的代码段选择符,指向 GDT 中定义了 32 位模式的代码段描述符;$protcseg
是新的代码段中的偏移地址,指示从这里开始执行 32 位指令。在执行 ljmp
指令时,CPU 根据段选择符从 GDT 中获取段描述符,确定新的代码段的基地址、段限和段权限,从而使 CPU 切换到 32 位保护模式。
JOS中给出的GDT表:
1 | # Bootstrap GDT |
2. bootloader执行的最后一条指令是什么,以及它刚加载的内核的第一条指令是什么?
bootloader从磁盘加载内核ELF文件所在的扇区数据到内存中,读取ELF信息并最终跳转到ELF entry addr开始执行ELF。
1 | void readsect(void *dst, uint32_t offset) |
insl 函数使用内联汇编,通过 insl 指令从指定的 I/O 端口读取指定数量的4字节数据到内存地址。它首先清除方向标志以确保内存地址按顺序递增,然后通过 repne 前缀重复执行 insl 指令,直到读取所有指定的数据项。这允许从端口高效地批量读取数据到内存中。
bootloader最后一条指令:call *0x10018 // ((void (*)(void)) (ELFHDR->e_entry))();
内核的第一条指令:movw $0x1234,0x472 // entry(_start) label in entry.S
3. 内核的第一条指令在哪里?
readelf查看kernel文件,其entry addr为 0x10000c
,这个地址就是内核ELF第一个指令所在的内存地址。
1 | 90: 0010000c 0 NOTYPE GLOBAL DEFAULT 1 _start |
4. bootloader如何决定它必须读取多少个扇区以便从磁盘获取整个内核?它从哪里找到此信息?
1 | // read 1st page off disk |
首先从磁盘读出ELF Header,解析Program Header信息,然后依次从磁盘读出每个段的数据并加载到指定物理地址。