mit6.828 lab1:Booting a PC

1.为什么要学习这门课程

刚刚经历过秋招,深刻感觉到自己计算机知识还不够扎实。尽管也能拿到好几个大厂offer,但扪心自问,很多os的设计理念和原理,我只能从书本上抽象的概念复读背诵,难以窥探操作系统内核的奥妙。其实早已听说这门大名鼎鼎的课程,其lab做下来应该会是收获颇丰。 课程地址 然后找到lab1的作业,跟着完成12个exercise就是lab1的内容了。

2.environment setup

我的是Mac环境,操作比较简单,需要注意的是,qemu一定是安装课程网址上提供补丁版本的qemu,否则后面的一些lab会出问题。 主要参考了:博客

但在执行第二行命令会卡死,也就是安装6828自己打补丁版本的qemu失败,所以我直接下载了该课程给出的qemu源码编译:

brew install $(brew deps qemu)
PATH=${PATH}:/usr/local/opt/gettext/bin make install
git clone https://github.com/mit-pdos/6.828-qemu.git qemu
./configure --disable-kvm --disable-werror --disable-sdl
make && make install

然后用第一个博客里的方法安装其他几个工具链中的tools

最后把/usr/local/bin下的 i386***-gdb 简化名字为gdb,否则make gdb的时候会找不到可执行文件

2.物理内存地址结构

最早的芯片,只支持1MB的物理内存,也即 0x00000000 - 0x 000fffff地址之间,最低的640kb叫做low mem,作为最早的pc可以使用的ram。0x000A000 至0x0010000之间这384作为视频播放的缓冲区或者固件占用的内存。

随着芯片的发展,可以支持4Gb以上的物理内存,为了能够向前兼容,也保留了前1MB的设计格式不变。所以,物理内存就出现了一个”hole”(0x000A000 - 0x0010000)把ram分为了两部分(前640KB+extended mem)

CS为代码段寄存器,IP为指令指针寄存器,从名称上我们可以看出它们和指令的关系。

在8086PC机中,任意时刻,设CS中的内容为M,IP中的内容为N,8086CPU将从内存M*16+N单元开始,读取一条指令并执行。

开机的过程: 1. PC通电后会设置CS为0xf000,IP为0xfff0,第一条指令会在物理内存0xffff0处,该地址位于BIOS区域的尾部。该条指令为ljmp $0xf000,$0xe05b跳转到BIOS的前半部分:0xfe05b 处,这里开始执行的指令就是bios做一些初始化操作:检测各种底层的设备,比如时钟,GDTR寄存器。以及设置中断向量表,然后找到一个可以boot的磁盘,load进物理地址为:0x7c00 to 0x7dff 的位置,然后设置两个寄存器:CS:IP to 0000:7c00 ,将控制权交给boot loader

  1. boot loader,主要包含了两个文件:/boot/boot.S/boot/main.c ,boot loader 做两件事:

    • 从 real mode 切换到 32-bit protected mode(boot.s)

    • 通过一些io指令,从磁盘上读取kernel(boot/main.c)

  2. 控制权交给了kernel,kernel开始执行一些任务

3.boot loader

boot loader的主要两个作用,上一节一级阐述过,首先,boot loader的代码指令被加载到了0x7c00开始的位置,练习三的任务,就是在gdb中,深入boot loader 两个代码文件中追踪指令的执行,思考每一条指令的意义,所以第一个点断设置在0x7c00。需要注意的是,在boot loader代码指令执行期间,栈的起始地址,是0x7c00,然后向低地址增长,所以在调试boot loader的过程中,可以一直手动模拟这个栈:

总结一下,栈的生长过程是:调用某个函数,会先保存调用返回的下一条指令的地址,然后将参数按照倒序压栈,进入该函数后,先保存ebp的值,然后把调用过程中会使用到的调用者保存寄存器的值压栈,后面就是处理该调用的逻辑。

在读取内核文件到物理地址0x100000的过程中,首先要正确认识到ELF文件的格式:

下面函数就是首先读入第一个扇区,然后找到program header table,遍历这个table,加载每个段至对应的LMA。

# boot/main.c
    struct Proghdr *ph, *eph;

	// read 1st page off disk
	readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
    //ELFHDR = 0x10000 
	// 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); // 计算program header 的地址= 起始地址+ offset
   
	eph = ph + ELFHDR->e_phnum;
    // end of program header ,终点位置,说白了,就是计算出program header table 的起始和终点位置,然后遍历这个表中的每一个表项。
	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);
        // p_pa就是该段的`LMA`,并且是物理地址,因为现在虚拟内存还未开启。


4.kernel

首先回忆一下,boot loader将kernel 加载到了物理地址 0x00100000位置,也就是第一张图中,BIOS 后面的地址位置。但为了方便程序员写代码,os的虚拟内存系统,会将kernel 映射到虚拟内存的高地址上,而用户代码起始执行位置从低地址开始。

目前只需要映射4MB内存: 虚拟地址0xf0000000 - 0xf0400000 到 物理地址 0x00000000 - 0x00400000.这个映射表是手写的,存在:kern/entrypgdir.c 接下来两步: 1. 将entry_pgdir 的地址加载至寄存器:cr3 2. 将 寄存器cr0置1,标志着虚拟内存的开始,以后使用的地址都是虚拟地址

exercise 8 是本lab第一次要写代码,这节的任务主要就是滤清c语言的printf,将数据格式化打印到控制台的原理。

vprintfmt函数中,就是处理格式化输出,以c语言中的printf("hello: %4d,%8d",1001,1010) ,fmt即为hello: %4d,ap中包含了两个参数:1001、1010,首先会原样输出fmt中,第一个%前的所有字符,然后处理格式化输出,计算输出宽度和格式,然后根据不同的格式,例如十进制,八进制等,计算base=?,处理有符号数的符号等等,最后调用printnum(是数字的话)。

exercise 9,观察stack。

进入entry后,一开始是没有对%esp %ebp寄存器进行修改的,知道进入i386_init以前,说明设置系统堆栈的是这两句:

movl    $0x0,%ebp            # nuke frame pointer
movl    $(bootstacktop),%esp

进入gdb进行调试:

可见,初始化stack时,将栈顶指向了:0xf0110000,注意,在数据段中定义栈顶bootstacktop之前,首先分配了KSTKSIZE这么多的存储空间,专门用于堆栈,这个KSTKSIZE = 8 * PGSIZE = 8 * 4096 = 32KB。所以用于堆栈的地址空间为 0xf0108000-0xf0110000,其中栈顶指针指向0xf0110000. 那么这个堆栈实际坐落在内存的 0x00108000-0x00110000物理地址空间中。

exercise 10就是观察递归调用函数时,如何保存返回地址,压入参数,保存调用者保存寄存器。

exercise 11 接下来,要理解指针的意义,和对其进行加减操作,解引用操作等。然后完成monitor.c代码中,输出相应数据。 来看看需要输出哪些数据吧:

Stack backtrace: (这个简单,直接cprintf)
  ebp f0109e58  eip f0100a62  args 00000001 f0109e80 f0109e98 f0100ed2 00000031
  ebp f0109ed8  eip f01000d6  args 00000000 00000000 f0100058 f0109f28 00000061

ebp eip寄存器的值在哪呢?首先,已经写好了一个 read_ebp()函数给我们使用,那么显然可以读到ebp的值,对其解引用会得到ebp寄存器指向内存堆栈的位置,该位置即为上一层调用的esp栈顶的位置,所以对其+1,就是eip的值,接下来就是五个参数的值。

exercise 12 更进一步,打印调试信息。

# kdebug.c
// Your code here.
	stab_binsearch(stabs,&lline,&rline,N_SLINE,addr);
	if(lline<=rline){
		info->eip_line=stabs[lline].n_desc;
	}else{
		info->eip_line = -1;
	}

这里我是根据上下文的代码猜的,多试了几次就对了,首先二分查找行号,第四个参数,根据注释//text segment line number可以猜到,然后如果查找到了,就把该行的某个字段赋予eip_line,这里我猜了几次,我一开始用的是value字段,结果不对,后来网上看了别人的代码,改成了正确的。这里还有个坑人的是,注释中写了一句:If found, set info->eip_line to the right line number. 结果right line 的答案一直通不过,我改了lline 就过了.

所有代码写好后,命令行中: make grade 就是对整个lab1的code进行测试:

全部通过的话就会是以上结果。