《ucore lab1 练习5》实验报告

本秂侑毒 提交于 2019-12-02 15:21:08

[练习5]实现函数调用堆栈跟踪函数

我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。如果能够正确实现此函数,可在lab1中执行 “make qemu”后,在qemu模拟器中得到类似如下的输出:

在这里插入图片描述

请完成实验,看看输出是否与上述显示大致一致,并解释最后一行各个数值的含义。

提示:可阅读小节“函数堆栈”,了解编译器如何建立函数调用关系的。在完成lab1编译后,查看lab1/obj/bootblock.asm,了解bootloader源码与机器码的语句和地址等的对应关系;查看lab1/obj/kernel.asm,了解 ucore OS源码与机器码的语句和地址等的对应关系。

要求完成函数kern/debug/kdebug.c::print_stackframe的实现,提交改进后源代码包(可以编译执行) ,并在实验报告中简要说明实现过程,并写出对上述问题的回答。

补充材料:
由于显示完整的栈结构需要解析内核文件中的调试符号,较为复杂和繁琐。代码中有一些辅助函数可以使用。例如可以通过调用print_debuginfo函数完成查找对应函数名并打印至屏幕的功能。具体可以参见kdebug.c代码中的注释。

代码实现

1. 编程前,首先了解下当前情况:在Terminal下输入make qemu,发现打印以下信息后就退出了:

along:~/src/ucore/labcodes/lab1$ sudo make qemu
WARNING: Image format was not specified for 'bin/ucore.img' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
(THU.CST) os is loading ...

Special kernel symbols:
  entry  0x00100000 (phys)
  etext  0x001036f3 (phys)
  edata  0x0010e950 (phys)
  end    0x0010fdc0 (phys)
Kernel executable memory footprint: 64KB

2. 分析print_stackframe的函数调用关系

> kern_init ->
    grade_backtrace ->
        grade_backtrace0(0, (int)kern_init, 0xffff0000) ->
                grade_backtrace1(0, 0xffff0000) ->
                    grade_backtrace2(0, (int)&0, 0xffff0000, (int)&(0xffff0000)) ->
                        mon_backtrace(0, NULL, NULL) ->
                            print_stackframe ->

3. 找到print_stackframe函数,发现函数里面的注释已经提供了十分详细的步骤,基本上按照提示来做就行了。

  • 首先定义两个局部变量ebp、esp分别存放ebp、esp寄存器的值。这里将ebp定义为指针,是为了方便后面取ebp寄存器的值。
  • 调用read_ebp函数来获取执行print_stackframe函数时ebp寄存器的值,这里read_ebp必须定义为inline函数,否则获取的是执行read_ebp函数时的ebp寄存器的值。
  • 调用read_eip函数来获取当前指令的位置,也就是此时eip寄存器的值。这里read_eip必须定义为常规函数而不是inline函数,因为这样的话在调用read_eip时会把当前指令的下一条指令的地址(也就是eip寄存器的值)压栈,那么在进入read_eip函数内部后便可以从栈中获取到调用前eip寄存器的值。
  • 由于变量eip存放的是下一条指令的地址,因此将变量eip的值减去1,得到的指令地址就属于当前指令的范围了。由于只要输入的地址属于当前指令的起始和结束位置之间,print_debuginfo都能搜索到当前指令,因此这里减去1即可。
  • 以后变量eip的值就不能再调用read_eip来获取了(每次调用获取的值都是相同的),而应该从ebp寄存器指向栈中的位置再往上一个单位中获取。
  • 由于ebp寄存器指向栈中的位置存放的是调用者的ebp寄存器的值,据此可以继续顺藤摸瓜,不断回溯,直到ebp寄存器的值变为0

1. 根据注释填写代码如下:

void print_stackframe(void) {
     /* LAB1 YOUR CODE : STEP 1 */
     /* (1) call read_ebp() to get the value of ebp. the type is (uint32_t);
      * (2) call read_eip() to get the value of eip. the type is (uint32_t);
      * (3) from 0 .. STACKFRAME_DEPTH
      *    (3.1) printf value of ebp, eip
     *    (3.2) (uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]
      *    (3.3) cprintf("\n");
      *    (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.
      *    (3.5) popup a calling stackframe
      *           NOTICE: the calling funciton's return addr eip  = ss:[ebp+4]
      *                   the calling funciton's ebp = ss:[ebp]
      */
	uint32_t ebp=read_ebp();   //调用read ebp访问当前ebp的值,数据类	型为32位。
	uint32_t eip=read_eip();   //调用read eip访问eip的值,数据类型同。
	int i;   //这里有个细节问题,就是不能for int i,这里面的C标准似乎不允许
	for(i=0;i<STACKFRAME_DEPTH&&ebp!=0;i++)
	{
		//(3) from 0 .. STACKFRAME_DEPTH
		cprintf("ebp:0x%08x eip:0x%08x ",ebp,eip);//(3.1)printf value of ebp, eip
		uint32_t *args=(uint32_t *)ebp+2;//此时args指向了ss:[ebp+8]的位置 此处存放着参数
		cprintf("arg :0x%08x 0x%08x 0x%08x 0x%08x\n",*(args+0),*		(args+1),*(args+2),*(args+3));//依次打印调用函数的参数1 2 3 4
 
//(3.2)(uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]
 
//因为使用的是栈数据结构,因此可以直接根据ebp就能读取到各个栈帧的地址和值,ebp+4处为返回地址,ebp+8处为第一个参数值(最后一个入栈的参数值,对应32位系统),ebp-4处为第一个局部变量,ebp处为上一层 ebp 值。
 
//而这里,*代表指针,指针也是占用4个字节,因此可以直接对于指针加一,地址加4。
 
		print_debuginfo(eip-1);	//打印eip以及ebp相关的信息
		eip=((uint32_t *)ebp+1);//此时eip指向了返回地址
		ebp=((uint32_t *)ebp+0);//ebp指向了原ebp的位置
//最后更新ebp:ebp=ebp[0],更新eip:eip=ebp[1],因为ebp[0]=ebp,ebp[1]=ebp[0]+4=eip。
	}
}

2. 上述代码所调用的函数有read_eipread_ebpprint_debuginfo

  • read_eip函数定义在kdebug.c中
read_eip(void) {
   uint32_t eip;
    /*
    *asm表示后面的代码为汇编代码
    *volatile 表示编译器不要优化代码,后面的指令 保留原样
    *%0表示列表开始的第一个寄存器 
    *“=r”(eip)表示gcc让eip对应一个通用寄存器
    *下面这条语句的作用是将ss:[ebp+4]对应的值保存到eip中,ss:[ebp+4]对应的值是
    *函数的返回地址,也就是说将函数的返回地址保存到eip中,然后返回eip
    */
   asm volatile("movl 4(%%ebp), %0" : "=r" (eip));   
   return eip;
}
  • read_ebp函数函数定义在x86.h中
static inline uint32_t
read_ebp(void) {
    uint32_t ebp;
    /*
    *asm表示后面的代码为汇编代码
    *volatile 表示编译器不要优化代码,后面的指令 保留原样
    *%0表示列表开始的第一个寄存器 
    *“=r”(ebp)表示gcc让ebp对应一个通用寄存器
    *下面这条语句的作用是将ss:[ebp+4]对应的值保存到ebp中,ss:[ebp+4]对应的值是
    *函数的返回地址,也就是说将函数的返回地址保存到eip中,然后返回ebp
    */
    asm volatile ("movl %%ebp, %0" : "=r" (ebp));  //内联汇编,读取edp寄存器的值到变量ebp
    return ebp;  //返回ebp的值
}
  • print_debuginfo函数
//打印eip以及ebp的相关信息
print_debuginfo(uintptr_t eip) {
    struct eipdebuginfo info;
    if (debuginfo_eip(eip, &info) != 0) {
        cprintf("    <unknow>: -- 0x%08x --\n", eip);
    }
    else {
        char fnname[256];
        int j;
        for (j = 0; j < info.eip_fn_namelen; j ++) {
            fnname[j] = info.eip_fn_name[j];
        }
        fnname[j] = '\0';
        cprintf("    %s:%d: %s+%d\n", info.eip_file, info.eip_line,
                fnname, eip - info.eip_fn_addr);
    }
}

编码完成后,执行make qemu,打印结果如下所示,与实验指导书的结果类似。

ebp:0x00007b38 eip:0x00100bf2 args:0x00010094 0x0010e950 0x00007b68 0x001000a2
    kern/debug/kdebug.c:297: print_stackframe+48
ebp:0x00007b48 eip:0x00100f40 args:0x00000000 0x00000000 0x00000000 0x0010008d
    kern/debug/kmonitor.c:125: mon_backtrace+23
ebp:0x00007b68 eip:0x001000a2 args:0x00000000 0x00007b90 0xffff0000 0x00007b94
    kern/init/init.c:48: grade_backtrace2+32
ebp:0x00007b88 eip:0x001000d1 args:0x00000000 0xffff0000 0x00007bb4 0x001000e5
    kern/init/init.c:53: grade_backtrace1+37
ebp:0x00007ba8 eip:0x001000f8 args:0x00000000 0x00100000 0xffff0000 0x00100109
    kern/init/init.c:58: grade_backtrace0+29
ebp:0x00007bc8 eip:0x00100124 args:0x00000000 0x00000000 0x00000000 0x0010379c
    kern/init/init.c:63: grade_backtrace+37
ebp:0x00007be8 eip:0x00100066 args:0x00000000 0x00000000 0x00000000 0x00007c4f
    kern/init/init.c:28: kern_init+101
ebp:0x00007bf8 eip:0x00007d6e args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
    <unknow>: -- 0x00007d6d --

解释最后一行各个参数的含义

最后一行是ebp:0x00007bf8 eip:0x00007d6e args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8,共有ebp,eip和args三类参数,下面分别给出解释。

  • ebp:0x0007bf8
    此时ebp的值是kern_init函数的栈顶地址,从obj/bootblock.asm文件中知道整个栈的栈顶地址为0x00007c00,ebp指向的栈位置存放调用者的ebp寄存器的值,ebp+4指向的栈位置存放返回地址的值,这意味着kern_init函数的调用者(也就是bootmain函数)没有传递任何输入参数给它!因为单是存放旧的ebp、返回地址已经占用8字节了。

  • eip:0x00007d6e
    eip的值是kern_init函数的返回地址,也就是bootmain函数调用kern_init对应的指令的下一条指令的地址。这与obj/bootblock.asm是相符合的。

7d6c: ff d0 call *%eax
7d6e: ba 00 8a ff ff mov $0xffff8a00,%edx

  • args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
    一般来说,args存放的4个dword是对应4个输入参数的值。但这里比较特殊,由于bootmain函数调用kern_init并没传递任何输入参数,并且栈顶的位置恰好在boot loader第一条指令存放的地址的上面,而args恰好是kern_int的ebp寄存器指向的栈顶往上第2~5个单元,因此args存放的就是boot loader指令的前16个字节!可以对比obj/bootblock.asm文件来验证(验证时要注意系统是小端字节序)。

00007c00 <start>:
7c00: fa cli
7c01: fc cld
7c02: 31 c0 xor %eax,%eax
7c04: 8e d8 mov %eax,%ds
7c06: 8e c0 mov %eax,%es
7c08: 8e d0 mov %eax,%ss
7c0a: e4 64 in $0x64,%al
7c0c: a8 02 test $0x2,%al
7c0e: 75 fa jne 7c0a <seta20.1>

其对应的是第一个使用堆栈的函数,bootmain.c中的bootmain。(因为此时ebp对应地址的值为0)
bootloader设置的堆栈从0x7c00开始,使用”call bootmain”转入bootmain函数。
call指令压栈,所以bootmain中ebp为0x7bf8。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!