本实验的网站链接:MIT 6.828 Lab 1 Exercise 10。

题目

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?

解答

题目要求我们在test_backtrace函数处设置断点,测试每次进入此函数时发生了什么事情,并观察每次将哪几个words压栈。首先贴上test_backtrace函数对应的C语言代码及汇编代码。

test_backtrace函数对应的C代码

void test_backtrace(int x)
{cprintf("entering test_backtrace %d\n", x);if (x > 0)test_backtrace(x-1);elsemon_backtrace(0, 0, 0);cprintf("leaving test_backtrace %d\n", x);
}

test_backtrace函数对应的汇编代码

f0100040:   55                      push   %ebp
f0100041:   89 e5                   mov    %esp,%ebp
f0100043:   56                      push   %esi
f0100044:   53                      push   %ebx
f0100045:   e8 5b 01 00 00          call   f01001a5 <\_\_x86.get_pc_thunk.bx>
f010004a:   81 c3 be 12 01 00       add    $0x112be,%ebx
f0100050:   8b 75 08                mov    0x8(%ebp),%esi
f0100053:   83 ec 08                sub    $0x8,%esp
f0100056:   56                      push   %esi
f0100057:   8d 83 18 07 ff ff       lea    -0xf8e8(%ebx),%eax
f010005d:   50                      push   %eax
f010005e:   e8 cf 09 00 00          call   f0100a32 <cprintf>
f0100063:   83 c4 10                add    $0x10,%esp
f0100066:   85 f6                   test   %esi,%esi
f0100068:   7f 2b                   jg     f0100095 <test\_backtrace+0x55>
f010006a:   83 ec 04                sub    $0x4,%esp
f010006d:   6a 00                   push   $0x0
f010006f:   6a 00                   push   $0x0
f0100071:   6a 00                   push   $0x0
f0100073:   e8 f4 07 00 00          call   f010086c <mon\_backtrace>
f0100078:   83 c4 10                add    $0x10,%esp
f010007b:   83 ec 08                sub    $0x8,%esp
f010007e:   56                      push   %esi
f010007f:   8d 83 34 07 ff ff       lea    -0xf8cc(%ebx),%eax
f0100085:   50                      push   %eax
f0100086:   e8 a7 09 00 00          call   f0100a32 <cprintf>
}
f010008b:   83 c4 10                add    $0x10,%esp
f010008e:   8d 65 f8                lea    -0x8(%ebp),%esp
f0100091:   5b                      pop    %ebx
f0100092:   5e                      pop    %esi
f0100093:   5d                      pop    %ebp
f0100094:   c3                      ret
f0100095:   83 ec 0c                sub    $0xc,%esp
f0100098:   8d 46 ff                lea    -0x1(%esi),%eax
f010009b:   50                      push   %eax
f010009c:   e8 9f ff ff ff          call   f0100040 <test\_backtrace>
f01000a1:   83 c4 10                add    $0x10,%esp
f01000a4:   eb d5                   jmp    f010007b <test\_backtrace+0x3b>

观察test_backtrace函数调用栈

下面开始观察test_backtrace函数的调用栈。%esp存储栈顶的位置,%ebp存储调用者栈顶的位置,%eax存储x的值,这几个寄存器需要重点关注,因此我使用gdb的display命令设置每次运行完成后自动打印它们的值,此外我也设置了自动打印栈内被用到的那段内存的数据,以便清楚观察栈的变化情况。Let's go.

进入test_backtrace(5)

f01000d1:   c7 04 24 05 00 00 00    movl   $0x5,(%esp)
f01000d8:   e8 63 ff ff ff          call   f0100040 <test\_backtrace>
f01000dd:   83 c4 10                add    $0x10,%esp

test_backtrace函数的调用发生在i386_init函数中,传入的参数x=5.我们将从这里开始跟踪栈内数据的变化情况。各寄存器及栈内的数据如下所示。可见,共有两个4字节的整数被压入栈:

  1. 输入参数的值(也就是5)。
  2. call指令的下一条指令的地址(也就是f01000dd)。
%esp = 0xf010ffdc
%ebp = 0xf010fff8
// stack info
0xf010ffe0: 0x00000005  // 第1次调用时的输入参数:5
0xf010ffdc: 0xf01000dd  // 第1次调用时的返回地址

进入test_backtrace函数后,涉及栈内数据修改的指令可以分为三部分:

  1. 函数开头,将部分寄存器的值压栈,以便函数结束前可以恢复。
  2. 调用cprintf前,将输入参数压入栈。
  3. 在第2次调用test_backtrace前,将输入参数压入栈。
    这里尚未弄明白f0100053对应的命令为啥要将esp寄存器的值减去8,感觉没有必要啊,有待研究。(伍注:大概明白了,应该是默认输入参数有4个4字节的word,不足4个的话也分配4个,这样从栈中清除输入参数很简单,直接将esp的值加上16即可)
// function start
f0100040:   55                      push   %ebp
f0100041:   89 e5                   mov    %esp,%ebp
f0100043:   56                      push   %esi
f0100044:   53                      push   %ebx
// call cprintf
f0100053:   83 ec 08                sub    $0x8,%esp
f0100056:   56                      push   %esi
f0100057:   8d 83 18 07 ff ff       lea    -0xf8e8(%ebx),%eax
f010005d:   50                      push   %eax
f010005e:   e8 cf 09 00 00          call   f0100a32 <cprintf>
f0100063:   83 c4 10                add    $0x10,%esp
// call test_backtrace(x-1)
f0100095:   83 ec 0c                sub    $0xc,%esp
f0100098:   8d 46 ff                lea    -0x1(%esi),%eax
f010009b:   50                      push   %eax
f010009c:   e8 9f ff ff ff          call   f0100040 <test_backtrace>

进入test_backtrace(4)

在即将进入test_backtrace(4)前,栈内数据如下所示。

%esp = 0xf010ffc0
%ebp = 0xf010ffd8
// stack info
0xf010ffe0: 0x00000005  // 第1次调用时的输入参数:5
0xf010ffdc: 0xf01000dd  // 第1次调用时的返回地址
0xf010ffd8: 0xf010fff8  // 第1次调用时寄存器%ebp的值
0xf010ffd4: 0x10094     // 第1次调用时寄存器%esi的值
0xf010ffd0: 0xf0111308  // 第1次调用时寄存器%ebx的值
0xf010ffcc: 0xf010004a  // 残留数据,不需关注
0xf010ffc8: 0x00000000  // 残留数据,不需关注
0xf010ffc4: 0x00000005  // 残留数据,不需关注
0xf010ffc0: 0x00000004  // 第2次调用时的输入参数

进入test_backtrace(3), test_backtrace(2), test_backtrace(1)和test_backtrace(0)的情况与test_backtrace(4)类似,不再赘述。下面直接给出进入mon_backtrace(0, 0, 0)时的栈内数据的情况。

进入mon_backtrace(0, 0, 0)

在即将进入mon_backtrace(0, 0, 0)前,栈内数据如下所示。

%esp = 0xf010ff20
%ebp = 0xf010ff38
// stack info
0xf010ffe0: 0x00000005  // 第1次调用时的输入参数:5
0xf010ffdc: 0xf01000dd  // 第1次调用时的返回地址
0xf010ffd8: 0xf010fff8  // 第1次调用开始时寄存器%ebp的值
0xf010ffd4: 0x10094     // 第1次调用开始时寄存器%esi的值
0xf010ffd0: 0xf0111308  // 第1次调用开始时寄存器%ebx的值
0xf010ffcc: 0xf010004a  // 预留空间,不需关注
0xf010ffc8: 0x00000000  // 预留空间,不需关注
0xf010ffc4: 0x00000005  // 预留空间,不需关注
0xf010ffc0: 0x00000004  // 第2次调用时的输入参数:4
0xf010ffbc: 0xf01000a1  // 第2次调用时的返回地址
0xf010ffb8: 0xf010ffd8  // 第2次调用开始时寄存器%ebp的值
0xf010ffb4: 0x00000005  // 第2次调用开始时寄存器%esi的值
0xf010ffb0: 0xf0111308  // 第2次调用开始时寄存器%ebx的值
0xf010ffac: 0xf010004a  // 预留空间,不需关注
0xf010ffa8: 0x00000000  // 预留空间,不需关注
0xf010ffa4: 0x00000004  // 预留空间,不需关注
0xf010ffa0: 0x00000003  // 第3次调用时的输入参数:3
0xf010ff9c: 0xf01000a1  // 第3次调用时的返回地址
0xf010ff98: 0xf010ffb8  // 第3次调用开始时寄存器%ebp的值
0xf010ff94: 0x00000004  // 第3次调用开始时寄存器%esi的值
0xf010ff90: 0xf0111308  // 第3次调用开始时寄存器%ebx的值
0xf010ff8c: 0xf010004a  // 预留空间,不需关注
0xf010ff88: 0xf010ffb8  // 预留空间,不需关注
0xf010ff84: 0x00000003  // 预留空间,不需关注
0xf010ff80: 0x00000002  // 第4次调用时的输入参数:2
0xf010ff7c: 0xf01000a1  // 第4次调用时的返回地址
0xf010ff78: 0xf010ff98  // 第4次调用开始时寄存器%ebp的值
0xf010ff74: 0x00000003  // 第4次调用开始时寄存器%esi的值
0xf010ff70: 0xf0111308  // 第4次调用开始时寄存器%ebx的值
0xf010ff6c: 0xf010004a  // 预留空间,不需关注
0xf010ff68: 0xf010ff98  // 预留空间,不需关注
0xf010ff64: 0x00000002  // 预留空间,不需关注
0xf010ff60: 0x00000001  // 第5次调用时的输入参数:1
0xf010ff5c: 0xf01000a1  // 第5次调用时的返回地址
0xf010ff58: 0xf010ff78  // 第5次调用开始时寄存器%ebp的值
0xf010ff54: 0x00000002  // 第5次调用开始时寄存器%esi的值
0xf010ff50: 0xf0111308  // 第5次调用开始时寄存器%ebx的值
0xf010ff4c: 0xf010004a  // 预留空间,不需关注
0xf010ff48: 0xf010ff78  // 预留空间,不需关注
0xf010ff44: 0x00000001  // 预留空间,不需关注
0xf010ff40: 0x00000000  // 第6次调用时的输入参数:0
0xf010ff3c: 0xf01000a1  // 第6次调用时的返回地址
0xf010ff38: 0xf010ff58  // 第6次调用开始时寄存器%ebp的值
0xf010ff34: 0x00000001  // 第6次调用开始时寄存器%esi的值
0xf010ff30: 0xf0111308  // 第6次调用开始时寄存器%ebx的值
0xf010ff2c: 0xf010004a  // 预留空间,不需关注
0xf010ff28: 0x00000000  // 第7次调用时的第1个输入参数:0
0xf010ff24: 0x00000000  // 第7次调用时的第2个输入参数:0
0xf010ff20: 0x00000000  // 第7次调用时的第3个输入参数:0

mon_backtrace函数目前内部为空,不需关注。

退出mon_backtrace(0, 0, 0)

通过add $0x10, %esp语句,将输入参数及预留的4字节从栈中清除。此时%esp = 0xf010ff30,%ebp = 0xf010ff38.

退出test_backtrace(0)

连续3个pop语句将ebx, esi和ebp寄存器依次出栈,然后通过ret语句返回。其他1~5的退出过程类似,不再赘述。

回答问题

问题1:每次调用test_backtrace时发生了什么事情?

答:每次调用test_backtrace时,主要做了如下事情:

  1. 将返回地址(call指令的下一条指令的地址)压栈
  2. 将ebp, esi, ebx三个寄存器的值压栈,以便退出函数前恢复它们的值
  3. 调用cprintf函数打印"entering test_backtrace x",其中x为输入参数的值
  4. 将输入参数(x-1)压栈,再在栈中分配3个双字,共16字节,以方便清栈
  5. 调用test_backtrace(x-1)
  6. 调用cprintf函数打印"leaving test_backtrace x",其中x为输入参数的值

最终QEMU窗口的打印信息如下所示。

6828 decimal is 15254 octal!
entering test_backtrace 5
entering test_backtrace 4
entering test_backtrace 3
entering test_backtrace 2
entering test_backtrace 1
entering test_backtrace 0
leaving test_backtrace 0
leaving test_backtrace 1
leaving test_backtrace 2
leaving test_backtrace 3
leaving test_backtrace 4
leaving test_backtrace 5

问题2:每次调用test_backtrace时将多少个双字(32位)压栈?它们分别是什么?

答:结合第一问的回答,不难发现每次调用test_backtrace时共将8个双字压栈:

  1. 返回地址
  2. ebp, esi, ebx三个寄存器的值
  3. 输入参数(x-1)的值
  4. 3个预留双字(与输入参数构成4字节,方便清栈)

疑问

  1. 在i386_init入口处设置断点并运行,发现执行memset(edata, 0, end - edata);时会异常结束,这是什么原因?暂时注释掉这一句后,运行到monitor函数时又会不断打印乱码以及“unknown command”,这又是什么原因?
    答:见《一个memset导致的血案》.

  2. 这段代码中多次出现"sub 0x8, %esp"或"add 0x10, %esp"等语句,不知道为啥要加减对应的值?
    答:突然想明白了,这里应该是默认输入参数为4个4字节的word,不够4个的话也在栈中分配4个word的空间,这样清栈的时候会很简单,直接将%esp加上16即可。

  3. cprintf中的格式化字符串存储在内存中的哪个位置?这个有什么规则的吗?

备注

  1. call 0xf01001bc <__x86.get_pc_thunk.bx>的作用:

    • 根据What is i686.get_pc_thunk.bx? Why do we need this call?的解释,这个指令是将代码的地址保存到ebx寄存器,这样全局对象可以通过ebx寄存器的值加上偏移量来访问。
    • 使用gdb调试时发现这个get_pc_thunk.bx函数的实现只是将esp寄存器保存的地址对应的值赋给ebx寄存器:
    => 0xf01001bc <__x86.get_pc_thunk.bx>:  mov    (%esp),%ebx
    => 0xf01001bf <__x86.get_pc_thunk.bx+3>:    ret    
  2. call和ret:call指令先将下一条指令的地址压栈,然后跳到指定地址;ret指令设置eip地址为esp寄存器所指向的地址(也就是返回地址),然后将返回地址出栈。

  3. jmp *%eax is AT&T syntax for jmp eax, which is one form of jmp r/m32. It will jump to the address contained in register eax.

  4. 使用gdb设置断点时,需要填映射前的地址,如果填映射后的地址的话不会在对应位置停下来。比如:内核中将0xf01000a6映射到0x1000a6,设置断点时如果填0x1000a6不会生效,必须填0xf01000a6.具体原因有待研究。

  5. MOVZ指令
    • movzwl 0xf00b8000, %edx是指将0xf00b8000这个地址所指向的一个word复制给寄存器%edx,而不是将0xf00b8000这个数字复制给寄存器%edx。
    • MOVZBW: Move Zero-Extended Byte to Word. For MOVZBW, the low 8 bits of the destination are replaced by the source operand. the top 8 bits are set to 0. The source operand is unaffected.
    • MOVZBL: Move Zero-Extended Byte to Long. For MOVZBL, the low 8 bits of the destination are replaced by the source operand. the top 24 bits are set to 0. The source operand is unaffected.
    • MOVZWL: Move Zero-Extended Word to Long. For MOVZBW, the low 16 bits of the destination are replaced by the source operand. the top 16 bits are set to 0. The source operand is unaffected.
  6. mov 0x8(%ebp),%esi这句代码将输入参数赋值到%esi寄存器,为什么输入参数是在%ebp寄存器指向的地址加上8的位置呢?这是因为在调用该函数时,先将输入参数压栈后,然后执行call指令,此时会将下一条指令的地址压栈;在跳到函数入口后,又会将ebp寄存器的值压栈。因此,输入参数在栈中的位置与栈顶相差8:(%ebp)存储旧的%ebp寄存器的值,4(%ebp)存储返回地址的值,8(%ebp)才是输入参数的值。

  7. gdb的print指令
    • print $esp 显示esp寄存器的值
    • print *0xf010ffe0 显示0xf010ffe0这个地址的值
    • print/x *0xf010ffe0@8 以16进制的形式显示0xf010ffe0这个地址往后8个整数的值
  8. gdb的display命令:Print value of expression EXP each time the program stops. Use "undisplay" to cancel display requests previously made.

转载于:https://www.cnblogs.com/wuhualong/p/lab01_exercise10_test_backtrace.html

《MIT 6.828 Lab 1 Exercise 10》实验报告相关推荐

  1. python程序设计上机实践答案_清华大学出版社-图书详情-《C++程序设计上机实践与学习辅导(第三版)》...

    前言 C++是一种高效.实用的程序设计语言,它既可以进行过程化程序设计,也可以进行面向对象程序设计,是编程人员使用最广泛的工具之一. 对程序设计语言而言,编写出简洁.高效.实用的程序才是最终的学习目的 ...

  2. c语言实训作业总结,c语言程序设计上机实践心得报告

    c语言程序设计上机实践心得报告 班级:11 电信 2 姓名:莫金波 学号:1107032242012.12.28 惠州学院 HUIZHOU UNIVERSITY 我们专业的学生在专业老师的带领下进行了 ...

  3. c语言上机总结报告,C语言程序设计上机实践心得报告

    <C语言程序设计上机实践心得报告>由会员分享,可在线阅读,更多相关<C语言程序设计上机实践心得报告(12页珍藏版)>请在人人文库网上搜索. 1.C语言程序设计上机实践心得报告班 ...

  4. c语言程序设计上机总结,C语言程序设计上机实践心得报告.doc

    C语言程序设计上机实践心得报告C语言程序设计上机实践心得报告 班级:11电信2 姓名:莫金波 学号:110703224 2012.12.28 我们专业的学生在专业老师的带领下进行了c语言设计基础教程的 ...

  5. c语言程序设计上机实践,C语言程序设计上机实践心得报告

    C语言程序设计上机实践心得报告 C语言程序设计上机实践心得报告 班级:11电信2 姓名:莫金波 学号:110703224 2021.12.28 惠州学院 HUIZHOU UNIVERSITY 我们专业 ...

  6. 沈航C语言上机实验题答案,大学大一c语言程序设计实验室上机题全部代码答案(实验报告).doc...

    大学大一c语言程序设计实验室上机题全部代码答案(实验报告) C语言实验报告 实验1-1: hello world程序: 源代码: #include main() { printf("hell ...

  7. 北京电大c语言实验作业二,大学大一c语言程序设计实验室上机题全部代码答案(实验报告).doc...

    大学大一c语言程序设计实验室上机题全部代码答案(实验报告).doc C语言实验报告实验1-1:helloworld程序:源代码:#includemain(){printf("hellowor ...

  8. 看懂《C程序设计(第五版)学习辅导》第16章中介绍的用Visual Studio 2010对C程序进行编辑、编译、连接和运行的方法,并进行以下操作

    看懂<C程序设计(第五版)学习辅导>第16章中介绍的用Visual Studio 2010对C程序进行编辑.编译.连接和运行的方法,并进行以下操作: 点我看视频讲解+可运行代码 记得收藏视 ...

  9. 五猴分桃c语言课程设计,c语言程序设计五猴分桃问题实验报告.doc

    c语言程序设计五猴分桃问题实验报告.doc 课程设计报告学院.系:吉林大学珠海学院计算机科学与技术系专业名称:软件工程课程设计科目C语言程序课程设计所在班级:10班学生学号:04121010学生姓名: ...

  10. c语言五猴分桃案例分析,C语言程序设计五猴分桃问题实验报告.doc

    C语言程序设计五猴分桃问题实验报告 课 程 设 计 报 告 学院.系:吉林大学珠海学院计算机科学与技术系专业名称:软件工程课程设计科目C语言程序课程设计所在班级:10班学生学号:学生姓名:赵学文指导教 ...

最新文章

  1. Hadoop学习笔记五
  2. tomcat装死原因汇总
  3. 在Spring容器外部连接对象依赖项
  4. 《疯狂Java讲义》(二十九)---- JDBC
  5. 检测单击鼠标左键并拖动的消息_计算机视觉OpenCV学习笔记(四):关于鼠标的相关事件函数...
  6. 专用集装箱行业调研报告 - 市场现状分析与发展前景预测(2021-2027年)
  7. 开发中git的常用命令
  8. Redisbook学习笔记(3)数据类型之列表
  9. java 二分_java二分查找算法
  10. 力扣-409 最长回文串
  11. Python 基本功能应用与简单数据分析
  12. Thinking in Java 13.7扫描输入
  13. linux gnu ld,GNU LD用法
  14. 通过split命令分割大文件
  15. python调用dm.dll
  16. premiere pro 2020 pr 快捷键
  17. 01-Chrome架构:仅仅打开了1个页面,为什么有4个进程
  18. DSP之Controlsuit和C2000Ware下载安装
  19. 33个高能CSS选择器让我受益匪浅
  20. win10和ubuntu双系统,使用AOMEI分区后启动项损坏,只显示grub>

热门文章

  1. Linux入门(13)——Ubuntu16.04下将图片和pdf互转
  2. 【ESP32S2】把只有4M flash的开发板制作成一个2G的U盘(一)
  3. 对啊网初级会计6所有者权益
  4. 【算法与数据结构】3 知行合一,线性查找的自定义类测试
  5. hanning 窗 c++实现
  6. H3C(UIS R390X G2)安装ESXI
  7. 基于TCPIP在线更新文件客户端
  8. 使用宝利通视频会议时声音断断续续的怎么处理
  9. 网上流传的知名企业工资一览
  10. 网易是怎样成长起来的呢?