MIT6.828-Lab1-Part3-The_Kernel

 

内核相关的一些知识点

内核

详细理解最小的JOS内核

使用虚拟内存来解决位置依赖

位置依赖是指在传统的静态编译和链接模型中,程序通常被编译成机器码,并直接加载到内存中执行。程序在运行时必须要加载到固定的内存地址,否则运行会出错。使用虚拟内存可以解决位置相关性问题,因为虚拟内存允许程序在不同的虚拟内存地址空间中运行,而不会影响其行为。当程序被加载到内存中时,操作系统会将程序的虚拟地址映射到实际的物理地址,从而实现了程序的正确执行。

操作系统内核通常会被链接到非常高的虚拟内存空间(virtual address),例如 .text VMA=0xf0100000,这主要是为了让处理器的虚拟空间的低地址部分预留给用户使用(会在下一个lab解释)。

很多机器并没有 0xf0100000这个地址,所以需要通过处理器的内存管理硬件,借助内存管理技术(分段、分页)将这个地址映射到 .text LMA=0x00100000 = 2^10 = 1M,根据PC引导程序的内存布局图片来看,此地址就在BIOS的正上方。

实验中是采用分页管理的办法来实现地址的映射,但没有使用计算机采用的分页管理硬件。而是通过手写的,一旦CRO_PG被设置,就会将:

  1. 虚拟地址 0xf000 0000 ~ 0xf040 0000 映射到物理地址 0x0000 0000 ~ 0x0040 0000
  2. 虚拟地址 0x0000 0000 ~ 0x0040 0000 映射到物理地址 0x0000 0000 ~ 0x0040 0000

任何不在这两个范围的虚拟地址都会导致硬件异常,由于实验没有设计中断处理,会直接导致QEMU关机或无止尽的重启。

Exercise7:

  1. 使用QEMU和GDB跟踪内核文件。在执行 movl %eax %cr0 前后,内存地址 0x0010|00000xf010|0000 存放着什么?
  2. 如果上面的指令没执行,而是被跳过,那么第一条出错的指令会是什么?可以通过注释 kern/entry.S 文件的这一行来进行验证。

格式化输出到控制台(console)

经常在C语言编程时用到的 printf() 函数也是需要在操作系统的内核中进行实现的。

通读 kern/printf.c, lib/printfmt.c, 和 kern/console.c,理解它们之间的关系。后边的实验会让我们弄清楚为什么 printfmt.c 文件被放在 lib 目录下。

先简单说一下它们之间的关系:

  1. printf.c 内有三个函数调用关系(a->b指b调用a)是:putch -> vcprintf -> cprintf,最终实现的功能是输出单个字符,另外:
    1. cputchar -> putch: 而 cputchar 被定义在 console.c
    2. vprintfmt -> vcprintf: 而 vprintfmt 被定义在 printfmt.c

用图来表示(没带参数的函数只是忽略没写,并不代表没有参数):

image-20230425152653943

  1. putch 借助 cputchar 实现输出单个字符
  2. vcprintf 借助 vprintfmt,并通过 fmt 来格式化需要输出的字符串

有一个疑问就是va_list ap是什么?带有”…“这种可变参数的函数cprintf是如何确定ap输入到vcprintf的。将在Exercise8详细说明

Exercise8:

  1. 缺省了一部分有关于使用 “%o” 来格式化八进制输出的代码,完善它。
  2. 回答几个问题,具体见此作业的博客

增强console的功能,使其能够显示不同的字体颜色,传统的方法是在字符串两端加入转义序列。

堆栈(stack)

在实验的最后一部分,探讨C语言如何在x86上使用堆栈(stack)的,并且还会重新编写一个新的 kernel monitor function,它会打印出堆栈的变化轨迹(backtrace),即一系列被保存到堆栈的IP(Instruction Pointer)寄存器的值。这些值是由于执行了一些列嵌套的call指令得到的。

Exercise9:

  1. 判断一下操作系统内核是从哪条指令开始初始化它的堆栈空间的,以及这个堆栈坐落在内存的哪个地方?
  2. 内核是如何给它的堆栈保留一块内存空间的?
  3. 堆栈指针又是指向这块被保留的区域的哪一端的呢?

x86栈指针寄存器(%esp)指向的是整个栈正在被使用部分的最低地址(因为是向下生长的),更下面的地址都是还未使用的。压栈:减少指针值将值写到指针指向的位置。出栈:读取此时指针指向的地址内的值将指针值增加。在32-bit模式下,每一次对堆栈的操作都是以32bit为单位,所以%esp的值永远可以被4整除。

而ebp寄存器则是记录每一个程序的栈帧的相关信息的一个非常重要的寄存器。每一个程序在运行时都会分配给它一个栈帧,用于实现存放一些临时变量,传递参数给它调用的子函数等等功能。当现在进入某个子程序时,最先要运行的代码就是先把之前调用这个子程序的程序的ebp寄存器的值压入堆栈中保存起来,然后把ebp寄存器的值更新为当前esp寄存器的值。此时就相当于为这个子程序定义了它的ebp寄存器的值,也就是它栈帧的一个边界。只要所有的程序都遵循这样的编程规则,那么当我们运行到程序的任意一点时。我们可以通过在堆栈中保存的一系列ebp寄存器的值来回溯,弄清楚是怎样的一个函数调用序列使我们的程序运行到当前的这个点。

Exercise10:

  1. 为了能够更好的了解在x86上的C程序调用过程的细节,首先找到在 obj/kern/kern.asm中test_backtrace 子程序的地址, 设置断点,并且探讨一下在内核启动后,这个程序被调用时发生了什么。对于这个循环嵌套调用的程序test_backtrace,它一共压入了多少信息到堆栈之中。并且它们都代表什么含义?

Exercise11:

test_backtrace 会调用 mon_backtrace,它已经在 /lab/kern/monitor.c 中有一个原型了,完善它,并使它产生如下输出:

Stack backtrace:
  ebp f0109e58  eip f0100a62  args 00000001 f0109e80 f0109e98 f0100ed2 00000031
  ebp f0109ed8  eip f01000d6  args 00000000 00000000 f0100058 f0109f28 00000061
  ...

在每一行中,ebp后面的值代表的是被这个函数所使用的ebp寄存器的值,这个值也是这个函数的栈帧的最高地址。而eip后面的值代表的是函数的返回地址。最后的五个列在args之后的16进制的值,是传递给这个函数的头五个输入参数,当然,有可能输入参数不到五个。


目前通过backtrace已经知道一直深入到mon_backtrace()函数的所有函数的有关地址。但实际情况中,经常会想弄清楚这些地址对应的到底是哪个函数。为了达到这个目的,lab提供了一个函数 debuginfo_eip(),这个函数会在标识表(symbol table)中查找eip的值,然后显示出来关于这个eip的值相关的调试信息。这个函数定义在 kern/kdebug.c 文件中。

Exercise12:

  1. 修改 backtrace 的代码,达到:针对每个 eip,它的函数名,源文件名和对应于该 eip 的行号。
  2. XXX
  3. XXX