官网:https://pdos.csail.mit.edu/6.828/2018/schedule.html
参考资料:
-
https://blog.youkuaiyun.com/bysui/category_6232831.html
-
https://github.com/SmallPond/MIT6.828_OS
-
https://www.cnblogs.com/fatsheep9146/category/769143.html
前期准备
配置环境
-
VMware Workstation虚拟机
-
Ubuntu 16.04
-
安装MIT给的Tool Chain:https://pdos.csail.mit.edu/6.828/2018/tools.html
-
安装MIT打Patch的Qemu(我安装的时候用的./configure --disable-kvm --disable-werror --target-list=“i386-softmmu x86_64-softmmu”)
可能出现的错误
- 上面的链接中gmp-5.0.2可能下载不了,用https://mirrors.sjtug.sjtu.edu.cn/gnu/gmp/gmp-5.0.2.tar.bz2
- mpc-0.9可能需要加–no-check-certificate选项
- 安装gmp时出现No usable m4 in $PATH or /usr/5bin:
apt-get install m4
- 安装gcc时出现cannot compute suffix of object files: cannot compile:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
- 安装gdb时出现no termcap library found:去https://ftp.gnu.org/gnu/termcap/安装termcap后即可
- git clone qemu时出现git-remote-https: symbol lookup error: /usr/lib/x86_64-linux-gnu/libhogweed.so.4: undefined symbol: __gmpz_limbs_read:
rm /usr/local/lib/libgmp.so*
Lab1:Booting a PC
Part 1: PC Bootstrap
mkdir 6.828
cd 6.828
git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab
cd lab
make
make qemu
// 在两个终端中分别输入make qemu-gdb
和make gdb
,观察ljmp那一行。
Exercise2
使用si命令去追踪ROM BIOS几条指令
Part 2: The Boot Loader
观察boot/boot.S, boot/main.c, obj/boot/boot.asm文件。
boot.S,main.c
cli关中断。
cld指定串处理操作的指针的移动方向。
使用异或操作将ax寄存器中设为0,并清空ds、es、ss三个段寄存器的数据。
观察seta20.1代码:
由图看出0x64端口为控制器读取状态寄存器,属于keyboard controller键盘控制器804x
inb指令为读取端口的一字节到al寄存器中;
testb指令将0x2和al寄存器中的数据作与运算;
jnz表示结果不为0则继续到inb指令;
前三行指令表示当bit 1为0时才跳转到下一指令。(bit1的值代表输入缓冲区是否满了,即CPU传送给键盘控制器的数据是否已经取走,如果CPU想向控制器传送新数据的话,必须先保证这一位为0。)
movb与outb表示将0xd1数据写入0x64端口。
d1指令表示下一次写入0x60端口的数据将被写入给804x控制器的输出端口。可以理解为下一个写入0x60端口的数据是一个控制指令。
seta20.2代码类似,df指令表示进入到保护模式。
lgdt gdtdesc,指将gdtdesc标识符的值送入全局映射描述符表寄存器GDTR中,把关于GDT表的一些重要信息存放到CPU的GDTR寄存器中,其中包括GDT表的内存起始地址,以及GDT表的长度。有关gdt和gdtdesc的具体数据在boot.S最后有:
movl、orl、movl指令表示把cr0寄存器的bit0置1,代表保护模式启动。
跳转指令,把当前的运行模式切换成32位地址模式。
加载这些段寄存器,使GDTR的值生效。
设置当前的esp寄存器的值,并正式跳转到main.c文件中的bootmain函数处。然后来看main.c文件:
前面的注释基本和前面所说差不多,bootloader主要由boot.S与main.c来控制。下面来看bootmain函数。
bootmain函数首先调用readseg函数(如下),从注释来看,以距离内核起始地址offset个偏移量的存储单元为起始,将之后count字节的数据送入以pa为起始地址的内存物理地址处。这一行函数把内核的第一个页的内容读取的内存地址ELFHDR处。相当于把操作系统映像文件的elf头部读取出来放入内存中。
if条件语句判断这个输入文件是否是合法的elf可执行文件。
ph被指定为Program Header Table表头,这个表存放着程序中所有段的信息。通过这个表才能找到要执行的代码段,数据段等等。
eph指向该表末尾。
for循环就是在把操作系统内核的各个段从外存读入内存中。
e_entry字段指向的是这个文件的执行入口地址。这里相当于开始运行这个内核文件。 自此把控制权从boot loader转交给了操作系统的内核。
Exercise3.1
在0x7c00处设置断点,观察boot.S代码的运行过程,同时使用boot.S文件和系统反汇编出来的文件obj/boot/boot.asm。也可以使用GDB的x/i指令来获取任意一个机器指令的反汇编指令,把源文件boot.S文件和boot.asm文件以及在GDB反汇编出来的指令进行比较。
首先设置断点并运行到断点处。
使用x/i反汇编。
查看obj/boot/boot.asm文件,基本一致。
Exercise3.2
追踪到boot/main.c
中的bootmain()
,然后追踪到readsect()
。确定与readsect()
中的每个语句相对应的确切汇编指令。跟踪readsect()
的其余部分 并返回bootmain()
,并确定从磁盘读取内核剩余扇区的for
循环的开始和结束。找出循环结束时将运行的代码,在此处设置断点,然后继续执行该断点。然后逐步完成引导加载程序的其余部分。
略,详情可见https://www.cnblogs.com/fatsheep9146/p/5115086.html
questions
1、处理器在什么时候开始执行 32 位代码?究竟是什么导致从 16 位模式切换到 32 位模式?
boot.S中,运行完 " ljmp $PROT_MODE_CSEG, $protcseg " 语句后,正式进入32位工作模式。
2、boot loader中执行的最后一条语句是什么?内核被加载到内存后执行的第一条语句又是什么?
boot loader执行的最后一条语句是bootmain中的最后一条语句 " ((void (*)(void)) (ELFHDR->e_entry))(); "。内核被加载到内存后的第一句为 movw $0x1234, 0x472。
3、内核的第一条指令在哪里?
/kern/entry.S文件中
4、boot loader是如何知道它要读取多少个扇区才能把整个内核都送入内存的呢?在哪里找到这些信息?
在Program Header Table表中有操作系统有多少个段,每个段有多少个扇区的信息;
该表在操作系统内核映像文件的ELF头部信息中。
Loading the Kernel
Exercise4
熟悉C语言指针(略)
ELF文件可以理解为由三大部分组成:一个是带有加载信息的文件头,然后紧跟着程序段表,然后紧跟着几个程序段。其中每一个段都是一块连续的代码或者数据。它们在被运行时要首先被加载到内存中。boot loader的工作就是把它们加载到内存中。
在6.828中对ELF的三个段感兴趣:
- .text段:存放所有程序的可执行代码
- .rodata段:存放所有只读数据的数据段
- .data段:存放所有被初始化过的数据段
当链接器计算程序的内存布局时,它会为未初始化的全局变量,在.data段后的.bss段中保留空间,C 要求未初始化的全局变量以零值开头。因此,无需 在 ELF 二进制文件中存储.bss段的内容;相反,链接器只记录.bss段的地址和大小。加载程序时必须将 .bss段清零。
使用objdump -h obj/kern/kernel
来查看jos中所有段的名称、大小和链接地址。
在每一个段中都有两个比较重要的字段,VMA(链接地址),LMA(加载地址)。其中加载地址代表这个段被加载到内存中后的内存地址,链接地址则指的是这个段希望被存放到的内存地址。通常两者是相等的。
通过输入下述指令来获取kernel的Program Headers Table的信息:
objdump -x obj/kern/kernel
需要被加载到内存的段被标记为LOAD。
BIOS通常会把boot sector加载到内存地址0x7c00处。
Exercise5
追踪boot loader一开始的几句指令,找到第一条满足如下条件的指令处:修改了boot loader的链接地址,这个指令就会出现错误。(在boot/Makefrag文件中修改链接地址,修改完成后运行 make clean, 然后通过make指令重新编译内核)
首先make clean,然后修改boot/Makefrag文件中的链接地址,重新make,观察boot.asm文件:
可以看到入口变为了7e00。
继续按照之前在7c00处打断点,可以看到在切换到保护模式的语句中出现了错误。
记得修改回来。
Exercise6
使用GDB的x命令:x/Nx ADDR(这个指令将打印出从ADDR地址开始之后的N个字的内容)。重启一下Qemu,在Bios进入boot loader之前,内存地址0x00100000处8个字的内容,然后在boot loader运行到内核开始处停止,再看下这个地址处的值。为什么二者不同?第二次这个内存处所存放的值的含义是什么?
首先刚进入gdb观察:
全为0
从boot.asm看到,bootmain中加载内核的地址在0x7d63,设置断点并观察0x100000的值:
可以推测,这里面存放的应该是指令段,即.text段的内容。
Part 3: The Kernel
Using virtual memory to work around position dependence
一个问题:我们应该把操作系统放在高地址处,但是在实际的计算机内存中却没有那么高的地址,这该怎么办?
解决方案:在虚拟地址空间中,把操作系统放在高地址处0xf0100000,但在实际的内存中把操作系统放在一个低的物理地址空间处,如0x00100000。当用户程序想访问一个操作系统内核时,首先给出的高的虚拟地址,然后计算机通过某个机构把这个虚拟地址映射为真实的物理地址。那么这种机构通常通过分段管理,分页管理来实现。
使用 kern/entrypgdir.c
中手写的、静态初始化的页目录和页表来执行操作(不必了解细节)。
Exercise7
使用Qemu和GDB追踪JOS内核文件,在movl %eax, %cr0指令前查看内存地址0x00100000以及0xf0100000处的内存。然后使用stepi命令执行完这条命令,再次检查这两个地址处的内容。
通过注释entry.S中的这条指令,如果该指令并没有执行,而是被跳过,那么第一个会出现问题的指令是什么?
从boot.asm文件可以看到,调用内核文件的地址在0x7d63,打断点后发现内核代码的入口地址为0x10000C,单步执行后发现movl %eax, %cr0指令在地址0x100025处,观察此时的0x100000地址和0xf0100000地址的内容:
执行命令后再查看此时地址的内容:
可以看到将0xf0100000的内容映射到了0x100000中。
接下来在entry.S中注释这一语句,make clean并make:
再进行调试:
gdb窗口中,原本0x10025的语句被注释后,可以看到下面一句是将f010002c地址写入eax并跳转,因为没有映射地址,所以这里出错。
qemu窗口中的错误。
Formatted Printing to the Console
阅读kern/printf.c
, lib/printfmt.c
, 和kern/console.c
大致观察printf.c:
观察注释,主要是printfmt及cputchar函数。可以看到vcprintf和cprintf都调用了vprintfmt函数(在lib/printfmt.c中),而cputchar函数是在console.c中定义的。接下来来看console.c中的cputchar函数:
由注释可以看到,cputchar是高等级的console I/O,调用的cons_putc函数是将字符输出到控制台上。
下面来看lib/printfmt.c:
vprintfmt函数:
总体是一个大的循环,首先输出%之前的所有字符,然后通过switch处理%后面的格式化输出。
Exercise8
我们省略了一小部分代码—即当我们在printf中指定输出"%o"格式的字符串,即八进制格式的代码。尝试去完成这部分程序。
类似case d,将基数base改为8即可。
questions
1、解释一下printf.c和console.c两个之间的关系。console.c输出了哪些子函数?这些子函数是怎么被printf.c所利用的?
printf.c调用console.c的接口cputchar,将这个函数封装在putch中,并将这个封装好的函数作为参数传给vprintfmt函数,用于向屏幕上输出一个字符。
2、解释console.c的如下代码:
1 if (crt_pos >= CRT_SIZE) {
2 int i;
3 memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
4 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
5 crt_buf[i] = 0x0700 | ' ';
6 crt_pos -= CRT_COLS;
7 }
crt_post是当前光标位置,CRT_SIZE是屏幕上总共的可以输出的字符数(其值等于行数乘以每行的列数),这段代码的意思是当屏幕输出满了以后,将屏幕上的内容都向上移一行,即将第一行移出屏幕,同时将最后一行用空格填充,最后将光标移动到屏幕最后一行的开始处。
3、对以下代码
int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);
(1)对cprintf函数来说,fmt和ap指针分别指向哪里?
(2)按照执行的顺序列出对cons_putc, va_arg和vcprintf的调用。对于cons_putc,列出它所有的输入参数。对于va_arg,列出ap在执行这个函数前后的变化。对于vcprintf,列出它的两个输入参数的值。
(1)fmt指向的是显示信息的格式字符串,即"x %d, y %x, z %d\n"。而ap是可变参数,指向所有输入参数的集合。
(2)先调用vcprintf,参数值为fmt和ap的值;然后按照字符串来分别调用cons_putc和va_arg。
tips:可以将该代码添加到kern/monitor.c中,来观察实际输出。
4、运行下列代码:
unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);
可以看到输出为He110 World
因为57616对应八进制为e110,i所表示的内存地址处存储了rld。(小端大端)
5、运行下列代码,y会输出什么?
cprintf("x=%d y=%d", 3);
由于有两个%d但只有一个3,y并没有参数被指定,所以会输出一个不确定的值。
The Stack
Exercise9
判断操作系统内核是从哪条指令开始初始化它的堆栈空间的,以及这个堆栈在内存的哪个地方?内核是如何给它的堆栈保留一块内存空间的?堆栈指针又是指向这块被保留的区域的哪一端的呢?
前面分析过boot.S和main.c,它们并不属于操作系统的内核。main.c文件中bootmain函数运行到最后时,执行的最后一条指令是跳转到entry.S文件中的entry地址处。此时控制权已经被转交给了entry.S。在跳转到entry之前,并没有对%esp,%ebp寄存器的内容进行修改,所以在bootmain中并没有初始化堆栈空间的语句。
下面进入entry.S,最后一条指令是要调用i386_init函数。这个子程序位于init.c文件之中,已经开始对操作系统进行一些初始化工作,所以在这之前,内核的堆栈应该已经设置好了。所以设置内核堆栈的指令就是call i386_init 指令之前的两条语句:
所以栈顶为bootstacktop指针的地址,利用反汇编来查看:
另外也可以通过gdb来打印寄存器esp的值:
可以看到栈的空间在f010f000-f0117000的位置。栈顶指针即esp,为f0117000。
esp指向栈顶位置,栈的生长由高向低,所以当计算机将一个值压入堆栈时,需要先把esp中的值减1(有时候是减4,由机器字长决定),然后把值存入内存单元。从堆栈中弹出一个值反之。
ebp则是记录每一个程序的栈帧的相关信息的一个非常重要的寄存器。
Exercise10
为更好的了解在x86上C调用的细节,找到在obj/kern/kern.asm中test_backtrace子程序的地址,设置断点,并且探讨一下在内核启动后,这个程序被调用时发生了什么。对于这个循环嵌套调用的程序test_backtrace,它一共压入了多少信息到堆栈之中。并且它们都代表什么含义?
kernel.asm中,test_backtrace的地址在f0100040,test_backtrace如下:
void
test_backtrace(int x)
{
cprintf("entering test_backtrace %d\n", x);
if (x > 0)
test_backtrace(x-1);
else
mon_backtrace(0, 0, 0);
cprintf("leaving test_backtrace %d\n", x);
}
设置断点
在test_backtrace执行前,esp为0xf0116fdc,ebp为0xf0116ff8。
执行push %ebp后:
将值压入ebp中,esp变为fd8。
执行mov %esp,%ebp后:
esp与ebp均变为fd8。
执行push %ebx后:
保存ebx的值,esp变为fd4。
执行sub后:
esp减掉0x14,变为fc0。后续以此类推。(以下借鉴https://github.com/clpsz/mit-jos-2014/tree/master/Lab1/Exercise10)
f0100040: 55 push %ebp ;压入调用函数的%ebp
f0100041: 89 e5 mov %esp,%ebp ;将当前%esp存到%ebp中,作为栈帧
f0100043: 53 push %ebx ;保存%ebx当前值,防止寄存器状态被破坏
f0100044: 83 ec 14 sub $0x14,%esp ;开辟20字节栈空间用于本函数内使用
f0100047: 8b 5d 08 mov 0x8(%ebp),%ebx ;取出调用函数传入的第一个参数
f010004a: 89 5c 24 04 mov %ebx,0x4(%esp) ;压入cprintf的最后一个参数,x的值
f010004e: c7 04 24 e0 19 10 f0 movl $0xf01019e0,(%esp) ;压入cprintf的倒数第二个参数,指向格式化字符串"entering test_backtrace %d\n"
f0100055: e8 27 09 00 00 call f0100981 <cprintf> ;调用cprintf函数,打印entering test_backtrace (x)
f010005a: 85 db test %ebx,%ebx ;测试是否小于0
f010005c: 7e 0d jle f010006b <test_backtrace+0x2b> ;如果小于0,则结束递归,跳转到0xf010006b处执行
f010005e: 8d 43 ff lea -0x1(%ebx),%eax ;如果不小于0,则将x的值减1,复制到栈上
f0100061: 89 04 24 mov %eax,(%esp) ;接上一行
f0100064: e8 d7 ff ff ff call f0100040 <test_backtrace> ;递归调用test_backtrace
f0100069: eb 1c jmp f0100087 <test_backtrace+0x47> ;跳转到f0100087执行
f010006b: c7 44 24 08 00 00 00 movl $0x0,0x8(%esp) ;如果x小于等于0,则跳到这里执行,压入mon_backtrace的最后一个参数
f0100072: 00
f0100073: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp) ;压入mon_backtrace的倒数第二个参数
f010007a: 00
f010007b: c7 04 24 00 00 00 00 movl $0x0,(%esp) ;压入mon_backtrace的倒数第三个参数
f0100082: e8 68 07 00 00 call f01007ef <mon_backtrace> ;调用mon_backtrace,这是这个练习需要实现的函数
f0100087: 89 5c 24 04 mov %ebx,0x4(%esp) ;压入cprintf的最后一个参数,x的值
f010008b: c7 04 24 fc 19 10 f0 movl $0xf01019fc,(%esp) ;压入cprintf的倒数第二个参数,指向格式化字符串"leaving test_backtrace %d\n"
f0100092: e8 ea 08 00 00 call f0100981 <cprintf> ;调用cprintf函数,打印leaving test_backtrace (x)
f0100097: 83 c4 14 add $0x14,%esp ;回收开辟的栈空间
f010009a: 5b pop %ebx ;恢复寄存器%ebx的值
f010009b: 5d pop %ebp ;恢复寄存器%ebp的值
f010009c: c3 ret ;函数返回
Exercise11
略(参考https://www.cnblogs.com/fatsheep9146/p/5070145.html)
Exercise12
略(有点复杂)
可能出现的错误
- git clone时出现fatal: unable to access ‘https://pdos.csail.mit.edu/6.828/2018/jos.git/’: server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none:
git config --global http.sslverify false