【施工完成】MIT 6.828 lab 1: C, Assembly, Tools and Bootstrapping

2019年1月24日 0 作者 CrazyKK

花费了30+小时,终于搞定了orz

 

Part 1: PC Bootstrap

The PC’s Physical Address Space

8086/8088时代

由于8086/8088只有20跟地址线,因此物理内存空间就是2^20=1MB.地址空间从0x00000到0xFFFFF.其中从0x00000开始的640k空间被称为”low memory”,是PC真正能使用的RAM。从 0xA0000 到 0xFFFFF 的384k的non-volatile memory被硬件保留,用作video display buffers和BIOS等。

80286/80386时代及以后

为了保持向后兼容,因此0-1MB的空间还是和原来保持一致。因此地址空间似乎存在一个“洞”(为什么我觉得其实是两个“洞”。。。不是空着的才叫“洞”吗),PC能使用的RAM被这个“洞”(也就是0xA0000 到 0xFFFFF)分成了0x00000000到0x000BFFFF的640k和 0x00100000到0xFFFFFFFF两部分。

此外,在地址空间的最上面一部分,通常被BIOS保留用于 32-bit PCI devices的memory mapped. memory mapped是对于memory和I/O设备使用相同的地址空间的一种I/O寻址方式。具体可以参考Memory-mapped I/O。PCI设备具体可以参考PCI_Express深入PCI与PCIe之一:硬件篇

目前处理器已经可以支持超过4GB大小的内存空间。因此为了保持后向兼容性,地址空间又会多一个”洞”。

The ROM BIOS

用qemu模拟启动,观察到进入BIOS执行的第一条命令为

说明PC执行的第一条指令的物理地址为0xffff0。

然后使用si命令执行单步指令,得到的前面几条执行的指令如下:

如果看着觉得似懂非懂…不要慌,问题不大,因为这里不需要弄明白BIOS到底在干什么。不过建议先复习一下x86汇编,可以参考General Registers (AX, BX, CX, and DX)Intel 80386 Reference Programmer’s Manual Table of Contents 等内容。然后强烈推荐去稍微看一下gdb_examining data 部分的教程,尤其是查看memory和register内容的章节,对搞清楚BIOS这里到底在干嘛大有裨益。(x [memory]来查看某个地址的内容,x/i [memory]将该地址的指令以人类可读的方式写出,p/x $[register] 来查看某个寄存器的值。)

那么BIOS大概做了什么呢?主要是建立Interrupt descriptor table(其实就是x86体系架构中断向量表的实现),初始化一些硬件设备,然后寻找一个”bootable”设备。如果找到了这样一个设备,BIOS就将该设备上的boot loader加载到内存,并将控制权交给boot loader.

先明确几个概念。所谓boot loader,就是在加载OS前运行的一段程序。通常在硬盘的第一个sector里,因此这个sector也叫boot sector.至于我们更经常见到的master boot record(主引导记录),其实就是一种对于分区过的媒介的特殊的boot sector.

顺便提一句,确定一个设备是否为”bootable”是通过 0x55和0xAA两个boot signature来决定的。具体来说,如果一个设备中的第0个sector的最后两个byte的值分别为0x55和0xAA,就认为这是一个bootable设备。可以参考bool sequence

Part 2: The Boot Loader

BIOS在初始化完成后需要将boot loader加载到内存,具体的地址为 0x7c00 到0x7dff。

关于0x7c00这个magic number是怎么来的? 其实不重要,不过感兴趣可以参考Why BIOS loads MBR into 0x7C00 in x86 ? 知道这个magic number其实不是x86相关的,而是和IBM的BIOS开发团队有关就可以了。

boot loader包含一个汇编文件boot/boot.S和一个c语言文件boot/main.c

先来看下boot/boot.S文件都在干什么吧

不过在这之前,不妨先复习一下real mode和proteced mode

real mode / protected mode

  • Real_mode 地址空间被限制在2^20(因为地址总线为20),没有虚拟内存的概念,内存都是真实的物理内存。在real mode下,segment位于物理内存中的固定位置上。
  • 16-bit Protected Mode 登场于intel 80286处理器。首次引入了虚拟内存的概念。依赖局部性原理,只将程序运行需要的部分放入内存,暂时用不到的部分则存储在硬盘。segment的位置在其从disk回到memory中,可能和之前的位置不同。由于segment的位置不再固定,引入Global Descriptor Table,GDT来描述segment的信息,诸如是否在内存中,如果在,在内存中的什么位置,以及访问权限。由于寄存器仍然是16bit,所以segment OSTEP
  •  32-bit Protected Mode  登场于intel 80386处理器。比起80286,使用的寄存器是32-bit的,因此segment size 增大到4GB(2^32). 同时,由于segment size不再像64k那么小,以前的一整个segment要么都在memory中,要么都在disk中的策略就变得不太科学了。因此引入paging 机制,将segment分成尺寸更小的page。允许segment中的一部分在memory中。关于paging可以参考OSTEP的18章。

这里值得一提的是,对于支持protected mode的cpu,启动时为了保持向后兼容,仍然会以real mode启动,之后再切换到protected mode.

When a processor that supports x86 protected mode is powered on, it begins executing instructions in real mode, in order to maintain backward compatibility with earlier x86 processors.[4] Protected mode may only be entered after the system software sets up one descriptor table and enables the Protection Enable (PE) bit in the control register 0 (CR0)

boot/boot.S文件在干什么

 

第一次看到这段代码的时候感觉Enable A20这一部分比较喵(ling)喵(ren)喵(fei)喵(jie)

可以参考A20 – a pain from the past。重点是

One sets the output port of the keyboard controller by first writing 0xd1 to port 0x64, and the the desired value of the output port to port 0x60. One usually sees the values 0xdd and 0xdf used to disable/enable A20.

然后比较让人疑惑的可能是”bootstrap GDT”这部分。参考cs421 x86 Assembly Guide尤其是:

知道gdtdesc部分做的事情是,在gdtdesc这个位置定义了一个word类型(2字节)的变量,值为0x17,参考注释也就是gdt定义的那一段的size大小。然后在gdtdsec+2这个位置定义了long类型(4字节)的gdt地址.

这里gdt和gdtdesc都是”label”,label其实就是标记了一个内存地址,方便使用。

具体来说,一个“label”的值,是其之后的第一条instruction的内存地址。

We use the notation <label> to refer to labeled locations in the program text. Labels can be inserted anywhere in x86 assembly code text by entering a label name followed by a colon. For example,

The second instruction in this code fragment is labeled begin. Elsewhere in the code, we can refer to the memory location that this instruction is located at in memory using the more convenient symbolic name begin. This label is just a convenient way of expressing the location instead of its 32-bit value.

然后是关于gdt部分,SEG看起来是个宏,我们看到inc/mmu.h这个文件中相关的部分,豁然开朗。

接下来不太明确的地方可能是cr0部分。

我们看到代码最开始有一个CR0_PE_ON,值为0x1.之后就是在计算cr0 = cr0 | 0x1,按照注释说这样就可以把保护模式打开了。理解到这里其实就ok,不过我还是想多说两句。 Control register是用来控制cpu行为的寄存器。cr0是x86体系架构的Control register中的一个。cr0是32bit的寄存器,其中一些bit上有名称以及固定的作用。比如对于位置bit 0,该位置的名称是”Protected Mode Enable”,简称为PE,当该位置值为1,表示保护模式被打开。

最后一个小细节是”.globl start”。”.globl”是什么含义?为什么要把start这个label定义成global的?可以参考What is global _start in assembly language? 用人话说就是定义成.globl的lable会被导出到生成的.o文件中,不然linker找不到这个符号。由于start是这个boot.S文件的entry point,因此需要linker看到。

最后,从全局来看,boot.S这个文件做了什么呢? 其实上面一个小节中已经提到了。

When a processor that supports x86 protected mode is powered on, it begins executing instructions in real mode, in order to maintain backward compatibility with earlier x86 processors.[4] Protected mode may only be entered after the system software sets up one descriptor table and enables the Protection Enable (PE) bit in the control register 0 (CR0)

 

boot/main.c这个文件在干什么

先注意到一些看起来像是汇编指令的东西…比如outb之类。查看inc/x86.h文件,找到他们的定义。

发现就是用c将汇编封装了一层。这个东西应该叫“inline assembly”,具体可以参考Brennan’s Guide to Inline Assembly 其中volatile关键字表示禁止gcc优化这段代码。

If your assembly statement must execute where you put it, (i.e. must not be moved out of a loop as an optimization), put the keyword volatile after asm and before the ()’s. To be ultra-careful, use

__asm__ __volatile__ (…whatever…);

However, I would like to point out that if your assembly’s only purpose is to calculate the output registers, with no other side effects, you should leave off the volatile keyword so your statement will be processed into GCC’s common subexpression elimination optimization.

注释上写的要”boot  an ELF kernel image from the first IDE hard disk”,那么,首先要知道什么是ELF. ELF其实就是一种文件格式,全称为“Executable and Linkable Format”可以参考Executable_and_Linkable_Format#File_layout,建议通读这一部分,内容不多,不过对之后很有用。

参考一下inc/elf.h文件,以及main.c中的注释,就可以整体上知道这段代码是在干什么了:将ELF格式的kernel image从硬盘读到内存中,并将控制权交给kernel image.

下面说几个细节。我们知道readsect是在读一个扇区,但是我怎么知道扇区是这样读的?可以参考ATA_PIO_Mode的x86 Directions部分

第二个细节是“((void (*)(void)) (ELFHDR->e_entry))()”,乍一看有点不明觉厉,其实就是一个函数指针,e_entry是入口函数的地址。通知调用该函数,将控制权交给elf格式的kernel image.

接下来我们看一下根据编译boot.s和main.c得到的反汇编文件

 

可以看到,上面的代码是从0x7c00开始执行的,而用gdb调试发现BIOS执行的第一条指令的位置其实是在0xf000:0xfff0  那么问题来了…CS段是什么时候从0xf000到0的呢? 在0x7c00之前,BIOS是在做什么呢?

我们用gdb看一下这一部分的代码:

其中的lidtw是加载向量描述表(load interrupt descriptor table), lgdtw是加载全局描述表(global descriptor table,GDT) 可以参考 LGDT/LIDT — Load Global/Interrupt Descriptor Table Register

第16,17行的0x70,0x71可以参考CMOS#Accessing_CMOS_Registers,虽然我觉得这太细节了,不看也罢。

18-20行的内容,是快速enbale A20的方法,可以参考A20_Line

然后第21-26行…似曾相识啊..这不就是启动protected mode的步骤吗…

可是这还没有加载boot loader啊..怎么就进入protected mode了呢。。参考bootloader – switching processor to protected mode,发现有些BIOS在实现的时候,会在加载boot loader之前,先短暂进入保护模式,目的可能是为了使用在保护模式下的一些特性(比如32-bit的register),然后在进入bootloader之前,再切换回实模式。 以及据某6.828学习群大佬说…在进入boot loader之前进入保护模式的方法和boot loader中进入保护模式的方法是不一样的…进入保护模式的方法一共有四种… 感觉太过细节,暂且不去关心了。

第26行之后的代码…抱歉我也不是很懂…看起来无关紧要,如果之后发现这段是重要的再说。

来回答一下几个问题吧。

  • At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?

开始执行32-bit code是从位置0x7c32,执行的命令为mov    $0x10,%ax
从16-bit mode转化到32-bit mode是将control register 0 的 第1位(PE)设置为1导致的。

  • What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?

boot loader执行的最后一条指令是0x7d61:      call   *0x10018  ,对应的c语言代码是 ((void (*)(void)) (ELFHDR->e_entry))();   kernel加载后执行的第一条指令为 movw   $0x1234,0x472

  • Where is the first instruction of the kernel?

kernel的第一条指令的地址为0x10000c

  • How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?

boot loader先读一小部分kernel,具体来说是8个sector,也就是1 page,对应的代码为 readseg((uint32_t) ELFHDR, SECTSIZE*8, 0); 然后读进来的这部分里面包含了整个kernel有多大的信息,这些信息存储在inc/elf.h文件中。

Loading the Kernel

练习4提到了要熟悉c语言的指针..去看了下推荐的”The C Programming Language “..发现真是一本非常棒的入门书…之前还以为是像《算法导论》一样只可远观的大部头…可惜已经不适初学者了… 练习4中给出了一段使用c语言指针的代码,第5个输出要注意一下大小端…

 

在继续之前,需要仔细看一下elf文件的内容ELF

ELF文件

elf文件分成了很多个section,通常.data section存放初始化的global/static variable,.text 存放代码,.rodata section 用来存放字符串常量,.bss section用来存放未初始化的global/static variabel.  .bss section没有对应的变量内容,原因是未初始化的变量按照规定会默认为0,因此没必要再存一次。“Thus there is no need to store contents for .bss in the ELF binary; instead, the linker records just the address and size of the .bss section. The loader or the program itself must arrange to zero the.bss section.”

我们比较关心的是.data section, .text section, .rodata section

我们可以用 objdump -h 命令查看一个ELF文件的 section header,

 

其中size是这个section的大小,VMA (Virtual Memory Address,6.828中叫link address) 是section开始执行时所在的memory address,LMA (Load Memory Address)是这个section被加载到memory中所处的位置。通常这两个地址是一样的。

boot loader使用elf文件中的program header来决定如何记载section, program header指明了ELF文件的哪一部分需要记载到memory中,以及加载到memory的什么位置。我们可以用bjdump -x obj/kern/kernel查看ELF的全部header文件

练习5 Trace through the first few instructions of the boot loader again and identify the first instruction that would “break” or otherwise do the wrong thing if you were to get the boot loader’s link address wrong. Then change the link address in boot/Makefrag to something wrong, run make clean, recompile the lab with make, and trace into the boot loader again to see what happens. Don’t forget to change the link address back and make clean again afterward!

把boot loader的link address从0x7c00改成了0x9c00… 然后进入gdb单步调试。

发现lgdtw的参数出现了负数 [ 0:7c1e] => 0x7c1e: lgdtw -0x639c  ,然后继续执行,到[ 0:7c2d] => 0x7c2d: ljmp $0x8,$0x9c32  ,发生了crash.

我们观察到生成的boot.asm文件,地址确实是从0x9c00开始了。

但是实际上。。BIOS仍然把boot loader记载到了0x7c00….这是约定俗成吗? BIOS无视Boot loader的link address,直接加载到0x7c00?   没有找到相关资料,有待进一步探寻。

练习6 Reset the machine (exit QEMU/GDB and start them again). Examine the 8 words of memory at 0x00100000 at the point the BIOS enters the boot loader, and then again at the point the boot loader enters the kernel. Why are they different? What is there at the second breakpoint? (You do not really need to use QEMU to answer this question. Just think.)

这个问题是问,BIOS进入boot loader时(也就是在0x7c00时)和boot loader进入kernel时(0x10000c),地址0x00100000开始的8个word单位的值,为什么不同。

0x7c00时,0x00100000处的8个word的值都为0…

在0x10000c时,0x00100000处的值翻译成指令之后是:

不一样的原因是,在刚刚进入boot loader时,kernel还没有加载进内存,因此是空的.

Part 3: The Kernel

Using virtual memory to work around position dependence

OS的kernel通常喜欢运行再较高地址的虚拟内存中,比如0xf0100000,为的是低地址留给用户程序。但是有的机器可能没有那么大的memory,因此不存在0xf0100000这个物理地址。因此这里需要做一个虚拟内存到物理内存的映射。在这个部分实验中,我们不需要至少地址映射是如何work的,只需要知道效果就好。

具体来说,当CR0_PG被置为1之前,内存地址为物理内存地址(严格地说,其实是线性地址,不过在boot/boot.S中做了线性地址到物理地址的等价映射),当CRO_PG flag被置为1之后,地址就变成了虚拟内存地址。我们可以用gdb调试看一下发生了什么。

Exercise 7.  Use QEMU and GDB to trace into the JOS kernel and stop at the  movl %eax, %cr0. Examine memory at 0x00100000 and at 0xf0100000. Now, single step over that instruction using the stepi GDB command. Again, examine memory at 0x00100000 and at 0xf0100000. Make sure you understand what just happened.

What is the first instruction after the new mapping is established that would fail to work properly if the mapping weren’t in place? Comment out the  movl %eax, %cr0 in kern/entry.S, trace into it, and see if you were right.

先用b *0x10000c处设置断点,这个是JOS kernel开始运行的地址。然后单步几步,在movl %eax , %cr0处停留,也就是cr0_PG flag恰好也被制为1之前。观察一下0x00100000和0xf0100000的内容:

 

然后接着单步一次,再次用x/8i观察8条0x00100000和0xf0100000处的内容

可以观察到,在cx0_PG flag被置为1之前,地址0xf0100000处是一片虚无。

置为1之后,地址0xf0100000处的内容和0x00100000处的内容一致。需要注意,此时这两个地址都是虚拟内存地址了。具体来说

Once  CR0_PG is set, memory references are virtual addresses that get translated by the virtual memory hardware to physical addresses.  entry_pgdir translates virtual addresses in the range 0xf0000000 through 0xf0400000 to physical addresses 0x00000000 through 0x00400000, as well as virtual addresses 0x00000000 through 0x00400000 to physical addresses 0x00000000 through 0x00400000

然后我们注释掉 movl %eax, %cr0 in kern/entry.S

再次用gdb调试,发现0x10002a: jmp *%eax  crash了。 原因显然是由于没有开启保护模式,eax的地址值不合法。

Formatted Printing to the Console

printf的格式化输出并不是天生就有的,首先阅读一下相关的几个代码。kern/printf.c, kern/console.c和lib/printfmt.c

Exercise 8. We have omitted a small fragment of code – the code necessary to print octal numbers using patterns of the form “%o”. Find and fill in this code fragment.

很简单,修改之后代码为

接下来来回答几个问题

  1. Explain the interface between printf.c and console.c. Specifically, what function does console.c export? How is this function used by printf.c?

printf.c与console.c的接口是console.c中的cputchar(),作用是向console中打印一个字符。printf.c在patch()函数中使用了cputchar()

2.Explain the following from console.c:

这段代码很显然,含义是屏幕的字符数超过了屏幕能显示的最大数目的情况下,将第二行到最后一行的字符整体上移一行(这样原先的第一行就被覆盖了),然后将最后一行的内容清空(因为已经上移到倒数第二行了)   应该是类似屏幕滚动的效果

3. For the following questions you might wish to consult the notes for Lecture 2. These notes cover GCC’s calling convention on the x86.

Trace the execution of the following code step-by-step:

  • In the call to  cprintf(), to what does  fmt point? To what does  ap point?
  • List (in order of execution) each call to  cons_putcva_arg, and  vcprintf. For  cons_putc, list its argument as well. For  va_arg, list what  ap points to before and after the call. For  vcprintf list the values of its two arguments.

这个问题的解答可以先参考一下c语言变长参数x86 calling conventions

我们先看一下print.c的代码:

从int cprintf(const char *fmt, …)开始看,参数*fmt应该就是 我们熟悉的c语言的printf的格式化部分,也就是第一个参数。

然后整体就是c语言变长参数的routine,但是没有使用va_arg, 而是用cnt = cvprintf(fmt,ap),返回了一个不知道什么的个数。

接下来看int vcprintf(const char *fmt, va_list ap),好像没什么好看的…. 然后是vprintfmt,代码如下:

大致扫一眼可以发现这段代码是处理输出的格式化参数的,包括输出类型,精度,场宽之类。

我们注意到putch函数的作用是向console输出一个字符,并统计当前累计的输出字符个数。

接下来我们来回答问题:

  • 在cprintf的调用中,fmt指向的是”x %d, y %x, z %d\n”, ap指向的是第一个变长参数,也就是变量x在调用栈中的地址。
  • cons_putc调用的过程按先后顺序为:
    • cons_putc(‘x’)
    • cons_putc(‘ ‘)
    • cons_putc(‘1’)
    • cons_putc(‘,’)
    • cons_putc(‘ ‘)
    • cons_putc(‘y’)
    • cons_putc(‘ ‘)
    • cons_putc(‘3’)
    • cons_putc(‘,’)
    • cons_putc(‘ ‘)
    • cons_putc(‘z’)
    • cons_putc(‘ ‘)
    • cons_putc(‘4’)
    • cons_putc(‘\n’)
  • va_arg一共调用了三次
    • 第一次调用前,ap指向参数x在栈中的地址,调用之后,ap指向参数y在栈中的地址。
    • 第二次调用前,ap指向参数y在栈中的地址,调用之后,ap指向参数z在栈中的地址。
    • 第三次调用前,ap指向参数z在栈中的地址,调用之后,ap指向参数z之后4字节的地址。
  • vcprintf的参数值为”x %d, y %x, z %d\n” 和 参数x在调用栈中的地址。

4.Run the following code.

What is the output? Explain how this output is arrived at in the step-by-step manner of the previous exercise. Here’s an ASCII table that maps bytes to characters.

The output depends on that fact that the x86 is little-endian. If the x86 were instead big-endian what would you set  i to in order to yield the same output? Would you need to change  57616 to a different value?

输出结果为  “He110 World” 前半部分的e110就是57616的十六进制表示。后半部分将unsiged int i 当成unsigned char类型输出,十六进制64,6c,72对应的字符分别为‘d’,‘l’,’r’.

然后先复习一下字节序。整数类型static_cast不会有字节序问题,指针++和–操作不涉及cast和字节序问题。把指针类型reinterpret_cast才会有字节序问题,例如:

由于x86体系架构字节序为little-endian,因此实际输出为’r’,’l’,’d’.

如果x86体系架构为large-endian,那么i的值应该改为0x00726c64,以实现相同的输出结果。

57616不需要做修改,因为整数类型staic_cast不存在字节序问题。

5.In the following code, what is going to be printed after  'y='? (note: the answer is not a specific value.) Why does this happen?

x的结果就是3,y的输出是没意义的一个整数。原因是,这句话会发生当va_list中没有下一个变量时,仍然使用va_arg去取下一个变量。而根据va_arg,此时的行为是undefined behaviour.

6.Let’s say that GCC changed its calling convention so that it pushed arguments on the stack in declaration order, so that the last argument is pushed last. How would you have to change  cprintf or its interface so that it would still be possible to pass it a variable number of arguments?

感觉如果知识修改cprintf来达到目的有点难? 因为压栈顺序和之前相反了,那么va_arg这个宏需要修改一下…或者,添加一个buffer,不是一次处理一个参数,而是先将参数全部读取,然后调换顺序,之后再进行处理。

 

The Stack

Exercise 9. Determine where the kernel initializes its stack, and exactly where in memory its stack is located. How does the kernel reserve space for its stack? And at which “end” of this reserved area is the stack pointer initialized to point to?

参考obj/kernel.asm

得知kernel初始化stack是在地址0xf010002c和0xf0100031完成的。stack被加载到了地址0xf01100000. 至于kernel如何为stack保留空间这个问题,我的理解是,stack现在有了初始位置,但是它如何知道自己有多大空间呢? 换句话说,这个问题问的是kernel如何决定stack的大小。这一部分其实定义在inc/memlayout.h中,

最后一个问题,由于x86体系架构下栈是向下增长的。因此stack pointer初始指向这段保留区域的大地址端(也就是上面)

 

Exercise 10. To become familiar with the C calling conventions on the x86, find the address of the  test_backtrace function in obj/kern/kernel.asm, set a breakpoint there, and examine what happens each time it gets called after the kernel starts. How many 32-bit words does each recursive nesting level of  test_backtrace push on the stack, and what are those words?

Note that, for this exercise to work properly, you should be using the patched version of QEMU available on the tools page or on Athena. Otherwise, you’ll have to manually translate all breakpoint and memory addresses to linear addresses.

test_backtrace的入口地址在0xf0100040,在这里设置断点,然后最后的输出结果如下:

对于每次调用函数test_backtrace,有三个32-bit的变量被压栈,可以参考

分别是参数x,ebp和ebx. 参数x和ebp的压栈是常规操作,就不解释了。ebx的压栈可能有些疑问,可以参考Why are these registers pushed to stack?

下一个练习:

Exercise 11. Implement the backtrace function as specified above. Use the same format as in the example, since otherwise the grading script will be confused. When you think you have it working right, run make grade to see if its output conforms to what our grading script expects, and fix it if it doesn’t. After you have handed in your Lab 1 code, you are welcome to change the output format of the backtrace function any way you like.

If you use  read_ebp(), note that GCC may generate “optimized” code that calls  read_ebp() before mon_backtrace()’s function prologue, which results in an incomplete stack trace (the stack frame of the most recent function call is missing). While we have tried to disable optimizations that cause this reordering, you may want to examine the assembly of  mon_backtrace() and make sure the call to read_ebp() is happening after the function prologue.

这个练习主要参考x86-calling-conventions, 主要是需要知道ebp的内容是上一个stack frame中的ebp,以及ebp+4是返回地址,ebp+8是第一个参数,还有ebp的初始值是0.

最后的实现为:

 

然后是最后一个练习:

Exercise 12. Modify your stack backtrace function to display, for each eip, the function name, source file name, and line number corresponding to that eip.

In  debuginfo_eip, where do __STAB_* come from? This question has a long answer; to help you to discover the answer, here are some things you might want to do:

  • look in the file kern/kernel.ld for __STAB_*
  • run objdump -h obj/kern/kernel
  • run objdump -G obj/kern/kernel
  • run gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at init.s.
  • see if the bootloader loads the symbol table in memory as part of loading the kernel binary

Complete the implementation of  debuginfo_eip by inserting the call to  stab_binsearch to find the line number for an address.

Add a backtrace command to the kernel monitor, and extend your implementation of  mon_backtrace to call  debuginfo_eip and print a line for each stack frame of the form:

Each line gives the file name and line within that file of the stack frame’s eip, followed by the name of the function and the offset of the eip from the first instruction of the function (e.g., monitor+106 means the return eip is 106 bytes past the beginning of monitor).

Be sure to print the file and function names on a separate line, to avoid confusing the grading script.

Tip: printf format strings provide an easy, albeit obscure, way to print non-null-terminated strings like those in STABS tables. printf("%.*s", length, string) prints at most  length characters of  string. Take a look at the printf man page to find out why this works.

You may find that some functions are missing from the backtrace. For example, you will probably see a call to  monitor() but not to  runcmd(). This is because the compiler in-lines some function calls. Other optimizations may cause you to see unexpected line numbers. If you get rid of the -O2 fromGNUMakefile, the backtraces may make more sense (but your kernel will run more slowly).

需要先了解一下stab,简单来说是一种调试数据格式。具体可以参考stabs 和 调试 DWARF 和 STAB 格式 。

objdump -h obj/kern/kernel的输出为

我们可以看到stabstr段的link address(VMA)为f01059c5.

然后用gdb调试,先断点到0x10000c,也就是bootloader记载kernel的位置。然后再单步执行几步,直到开启保护模式。此时查看 地址f01059c5,结果如下,说明boot loader在加载kernel的同时也将符号表加载到了内存中

 

接下来先看一下我们要补全的kern/kdebug.c文件