https://pdos.csail.mit.edu/6.828/2018/labs/lab1/

手动编译实验提供的patch版qemu

https://pdos.csail.mit.edu/6.828/2018/tools.html

1
2
3
4
git clone https://github.com/mit-pdos/6.828-qemu.git qemu
sudo apt install libsdl1.2-dev libtool-bin libglib2.0-dev libz-dev libpixman-1-dev
./configure --disable-kvm --disable-werror --prefix=/opt/jos-qemu --target-list="i386-softmmu x86_64-softmmu" --python=python2
make -j16

之后在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  # Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg

.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment

# Set up the stack pointer and call into C.
movl $start, %esp
call bootmain

在这条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
2
3
4
lgdt    gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0

通过向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
2
3
4
5
6
7
8
9
10
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULL # null seg
SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg
SEG(STA_W, 0x0, 0xffffffff) # data seg

gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt

2. bootloader执行的最后一条指令是什么,以及它刚加载的内核的第一条指令是什么?

bootloader从磁盘加载内核ELF文件所在的扇区数据到内存中,读取ELF信息并最终跳转到ELF entry addr开始执行ELF。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void readsect(void *dst, uint32_t offset)
{
// 等待磁盘准备好
waitdisk();

// 将要读取的扇区数设置为 1
outb(0x1F2, 1);

// 设置读取的起始扇区偏移地址
outb(0x1F3, offset); // 低字节
outb(0x1F4, offset >> 8); // 次低字节
outb(0x1F5, offset >> 16); // 次高字节
outb(0x1F6, (offset >> 24) | 0xE0); // 高字节,0xE0 用于设置逻辑驱动器号

// 发送命令来读取扇区
outb(0x1F7, 0x20); // cmd 0x20 - 读取扇区

// 等待磁盘准备好
waitdisk();

// 从磁盘读取扇区数据到目标地址
insl(0x1F0, dst, SECTSIZE / 4); // 每次读取 4 字节,SECTSIZE/4 为读取次数
}

static inline void
insl(int port, void *addr, int cnt)
{
asm volatile("cld\n\trepne\n\tinsl"
: "=D" (addr), "=c" (cnt)
: "d" (port), "0" (addr), "1" (cnt)
: "memory", "cc");
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
// read 1st page off disk
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

// is this a valid ELF?
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;

// load each program segment (ignores ph flags)
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph++)
// p_pa is the load address of this segment (as well
// as the physical address)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

首先从磁盘读出ELF Header,解析Program Header信息,然后依次从磁盘读出每个段的数据并加载到指定物理地址。