MIT 6.828 Lab1笔记
PART 1: PC Bootstrapr
这一部分主要是实验环境的配置,跟随教程操作即可
实模式下的地址转换
物理地址 = 段基址*16 + 偏移量
PART 2: Boot Loader
在6.828课程的系统中,boot loader包含两部分:一份汇编代码 boot/boot.s 和一份c代码 boot/main.c 。它们构成的boot loader主要有两个作用
1. 将处理器从实模式切换道32位保护模式。在实模式中,处理器只能访问1MB物理空间,而在32位保护模式下,处理器可以访问1MB以上的物理空间。切换模式后,逻辑地址向物理地址的映射方式也发生了变化,(段基址:偏移量)转化需要的offset从16变为32。
2. 借由x86的特殊I/O指令,直接从IDE硬盘中读取内核程序。
Exercise 3 的四个问题
Q: 处理器是何时开始执行32位代码的?是什么机制使得处理器从16位转为32位?
A: 如下图所示, 汇编操作将cr0寄存器的最后一位置为1,通过Intel手册我们可以知道,当cr0寄存器的最后一位为1时,开启保护模式。
Q: boot loader 执行的最后一条指令是什么?它加载的kernel执行的第一条指令是什么?
A: 如下图所示,这是boot loader执行的最后一条指令。这条指令转到了kernel程序的入口。
kernel执行的第一条指令如下图所示。
Q: kernel的第一条指令在哪里?
A: 根据上一张图,我们可以看到kernel第一条指令的地址在于 0x10000c
Q: boot loader在从硬盘读取内核时,它是怎样决定读取几个扇区的?它是怎样得知这个信息
A: bootmain函数如下图所示,程序首先读取足够大的数据(程序中是SECTSIZE*8),判断要读取的数据是不是一个ELF数据,如果是,则读取程序头,从表征程序头的结构体里面的e_phnum成员得知要读取的数据数量。
Exercise 5
linker的地址改变,从结果上来看,kernel无法被加载进内存,现象如下图所示
我在这里将链接地址从0x7c00改成了0x7cd0,用gdb跟踪程序,会发现boot loader的起始地址变为了我们改成的0x7cd0,如下图所示
将链接地址修改成不同的值会出现不同的问题,但最终都会导致程序循环,无法正确加载kernel程序
Exercise 6
将断点打在boot loader的起始位置,观察地址0x100000,可以看出这一地址的数据全部为0。当程序进入kernel后,发现指定的地址位置出现了数据。原因可能是boot loader 将kernel加载进了内存,所以相应位置的数据出现了变化;
Exercise 7
在执行指令 movl %eax, %cr0 之前, 两个地址对应的数值如下图所示地址空间
在执行指令过后, 两个地址对应的数值如下图所示地址空间
执行指令前两个地址空间对应的数据不同,而执行指令后两个地址空间对应的数据相同,说明开启分页机制后这两个虚拟地址被映射成了同一个物理地址。
注释掉这条指令重新编译,从结果上来看,硬件启动失败,直接退出
gdb跟踪发现是一条jmp指令出错了,因为跳转过后的地址是一个无效地址。
PART 3: The Kernel
kern/printf.c, lib/printfmt.c 和 kern/console.c三个文件的关系
kern/console.c 最底层,用了大量汇编语言编写,主要是适配于硬件的屏幕输出;
kern/printf.c 使用了console.c中的底层函数;
lib/printfmt.c 的函数最上层,封装了内核中的函数,可以供上层使用。
Exercise 8
需要修改的代码在lib/printfmt.c中,改变后的代码为
case 'o': // Replace this with your code. /* putch('X', putdat); putch('X', putdat); putch('X', putdat); break; */ num = getuint(&ap, lflag); base = 8; goto number;
模仿上方的代码即可写出,下面是问题的回答
1. console.c 导出了 cputchar 函数供 printf.c 使用,cputchar封装在了 putch 函数中。
2. 下面代码处理的情况是:屏幕上已经充满了字符,若现在的操作需要向屏幕再次输入字符,则需要进行的动作是:1.所有的字符向上移动一行 2.最后一行清空。这段代码实现的就是这个功能。
if (crt_pos >= CRT_SIZE) {//*如果目前屏幕上的字符数超过了屏幕的容量 int i; memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));//* 将从第二行开始的内容移动到第一行去 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++) crt_buf[i] = 0x0700 | ' ';//*最后一行的内容清零,由上文的内容可知,对0x700进行或操作与显示的色彩有关 crt_pos -= CRT_COLS; }
3. 我将文中提及到的代码段加在了 kern/init.c 中,因为前方有明显的屏幕打印值,所以可以很方便地找到要追踪地语句地位置。把断点打在 cpirntf 上。
在 cprintf 函数内,fmt指向的是字符串 "x %d, y %x, z %d\n" 的地址。函数原型中没有ap,所以这里ap也打印不出来,如下图所示
在 vcprintf 函数内,fmt指向的是字符串 "x %d, y %x, z %d\n" 的地址。ap指向一个地址,这个地址分别放有 x, y, z 的值,如下图所示
cons_putc 中,变量是一个char型
va_arg 是一个宏定义,gdb无法在之上直接打断点。进入vprintfmt逐步跟踪后,可以看到每次va_arg后,ap都是指向下一个变量的地址
4. 输出变成了 He110 World 。57616化成16进制是e110, 72,6c,64分别是rld的ascii码,如果在大端序的cpu上运行,则 Wo后面不会显示字符。
5. 虽然输入y没有对应的值,但还是会打印栈上的内容,在信心安全领域这种叫做格式化字符串漏洞,用于泄露栈上信息。
6. 函数原型声明为 cprintf(const char *fmt, va_list ap);
Exercise 11 12
要通过寄存器追溯函数调用栈,首先要弄懂一些细节:
1.pop 和 push 指令的操作顺序
push: 先减小esp寄存器里的值,再将要压入的数据写入到esp寄存器指向的地址;
pop: 先读出esp寄存器指向地址的值,再增加esp寄存器的值
读写与寄存器值改变, 这两个顺序不要弄错了
2.即将进行函数调用时,进行的压栈操作
首先按照参数的声明顺序,将参数压入栈中(关于压栈的顺序,可能根据编译器有所不同;32位操作系统下函数传参需要通过压栈,64位cpu寄存器资源丰富,传参直接用寄存器)。
然后压入返回地址eip,传参和压入eip都是在call函数之前实现的
最后将esp地址传递给ebp,并将ebp的值压入栈中。注意,这一部是在被调函数中实现的。
3.栈的生长方向,从高地址向低地址生长
关于在调用时显示符号信息,这个实验中的要求比较简单,已经写好了 stab_binsearch 函数,只需要利用即可,相应的代码中也有提示。
有一点需要注意的是,stab结构体中有很多成员,哪一项显示的是eip地址的行号呢,通过验证,我们可以得知是 n_value 这个变量,如果使用了其他变量,make grade 也能通过,不过返回地址是不正确的。具体看程序吧
kern/kdebug.c中代码如下图所示
kern/monitor.c中代码如下图所示
make qemu 的结果如下图所示
通过查看 obj/kernel.asm ,我们判断 test_backtrace 的返回地址应该是基地址+0x28, 即十进制的40, 与实验结果相符。
make grade, 实验通过!!