首页 > 编程语言 > 详细

c语言程序运行时的栈与寄存器的变化

时间:2016-02-27 22:09:47      阅读:268      评论:0      收藏:0      [点我收藏+]

原创作品转载请注明出处

参考材料 《Linux内核分析》 MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”

作者:Casualet

 

我们在这里从汇编代码的角度, 给出一段简单的C语言程序运行过程中机器状态的变化情况. 我们的实验环境是Ubuntu 64位, 编译器gcc的版本是4.8.4. 

我们使用的c程序如下:

  1. int g(int x){
  2. return x + 3;
  3. }
  4. int f(int x){
  5. return g(x);
  6. }
  7. int main(void){
  8. return f(8) + 1;
  9. }

这个简单的c程序有一个main函数, 在main函数里调用了f函数, 然后f函数调用了g函数. 我们把其编译成32位的汇编代码, 使用的命令是:  gcc -S -o main.s main.c -m32. 这样,我们获得了汇编代码文件main.s,  打开以后可以看到这种效果:

  1. .file "test.c"
  2. .text
  3. .globl g
  4. .type g, @function
  5. g:
  6. .LFB0:
  7. .cfi_startproc
  8. pushl %ebp
  9. .cfi_def_cfa_offset 8
  10. .cfi_offset 5, -8
  11. movl %esp, %ebp
  12. .cfi_def_cfa_register 5
  13. movl 8(%ebp), %eax
  14. addl $3, %eax
  15. popl %ebp
  16. .cfi_restore 5
  17. .cfi_def_cfa 4, 4
  18. ret
  19. .cfi_endproc
  20. .LFE0:
  21. .size g, .-g
  22. .globl f
  23. .type f, @function
  24. f:
  25. .LFB1:
  26. .cfi_startproc
  27. pushl %ebp
  28. .cfi_def_cfa_offset 8
  29. .cfi_offset 5, -8
  30. movl %esp, %ebp
  31. .cfi_def_cfa_register 5
  32. subl $4, %esp
  33. movl 8(%ebp), %eax
  34. movl %eax, (%esp)
  35. call g
  36. leave
  37. .cfi_restore 5
  38. .cfi_def_cfa 4, 4
  39. ret
  40. .cfi_endproc
  41. .LFE1:
  42. .size f, .-f
  43. .globl main
  44. .type main, @function
  45. main:
  46. .LFB2:
  47. .cfi_startproc
  48. pushl %ebp
  49. .cfi_def_cfa_offset 8
  50. .cfi_offset 5, -8
  51. movl %esp, %ebp
  52. .cfi_def_cfa_register 5
  53. subl $4, %esp
  54. movl $8, (%esp)
  55. call f
  56. addl $1, %eax
  57. leave
  58. .cfi_restore 5
  59. .cfi_def_cfa 4, 4
  60. ret
  61. .cfi_endproc
  62. .LFE2:
  63. .size main, .-main
  64. .ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4"
  65. .section .note.GNU-stack,"",@progbits

由于以点开头的都是链接时候用到的信息, 跟实际的代码执行逻辑没有关系, 为了方便分析,我们给出删除了以点开头的行以后的代码版本:

  1. g:
  2. pushl %ebp
  3. movl %esp, %ebp
  4. movl 8(%ebp), %eax
  5. addl $3, %eax  
  6. popl %ebp
  7. ret
  8. f:
  9. pushl %ebp
  10. movl %esp, %ebp
  11. subl $4, %esp
  12. movl 8(%ebp), %eax
  13. movl %eax, (%esp
  14. call g
  15. leave
  16. ret
  17. main:
  18. pushl %ebp
  19. movl %esp, %ebp
  20. subl $4, %esp
  21. movl $8, (%esp)
  22. call f
  23. addl $1, %eax
  24. leave
  25. ret

在这里,我们可以清晰地看到汇编代码和三个3函数之间的对应关系.我们补充两张代码的图例:

技术分享

技术分享

接下来我们从main函数开始分析:

首先是

  1. pushl %ebp
  2. movl %esp, %ebp
  3. subl $4, %esp
这三条指令的作用是保存栈的信息. 我们将栈想象成一段内存空间, 其中ebp寄存器指向栈底位置, esp寄存器指向栈顶位置,栈底位置是高地址,栈顶位置是低地址.  当进入main函数时,需要使用一段新的栈空间, 也就是说, 如果原来栈是如图1的栈空间, 现在,进入main函数后,执行了上面的3条指令,变成了图2到图3的情况.

技术分享技术分享

      图1

技术分享

技术分享

               图2

技术分享

技术分享

                图3

这样, 从ebp开始,到esp 就是属于main函数的栈. main函数执行完, 需要清空这个栈, 返回原来的状态, 但是怎么返回呢? 因为我们保存了100这个信息, 所以我们知道, 在调用main函数以前,ebp的值是100, esp的值是88, 所以我们可以返回. 这也就是为什么要做上面这三个步骤. 然后我们继续执行指令, 把数字8放在esp指向的位置, 得到如下的结果:

技术分享

技术分享

                图4

接下来,调用函数f, 这一步会把eip压栈. eip指向的是call的下一条指令, addl $1, %eax. 进入f函数以后, 又进行以下三步:

  1. pushl %ebp
  2. movl %esp, %ebp
  3. subl $4, %esp

这个的效果和前面讲的是一样的, 结果图如下:

技术分享

技术分享

                图5

然后,movl  8(%ebp), %eax 表示把ebp+8地址所在位置的值放到eax中, 在这里,这个值是正好是8. (对应c语言,我们发现原来要做的事情是int x参数传递.所以说, 在32位的x86情况下, 函数的参数传递是通过栈来实现的, 我们在使用call 指令调用函数前, 先把函数需要的参数压栈, 然后再使用call指令导致eip压栈, 然后进入新的函数后, 旧的ebp压栈, 新的ebp指向的位置存了这个刚压栈的旧的ebp. 所以, 我们通过新的ebp指向的位置, 可以通过计算的方法, 得到函数需要的参数).  接下来, movl %eax, (%esp)  会把eax的值放到esp指向的内存的位置, 然后调用 g函数, 又可以压栈call指令的下一条指令的地址, 得到的结果图是:

技术分享

技术分享

                图6

然后,我们进入了g函数, 执行了前两条指令,得到的结果是:

技术分享

技术分享

                图7

第三条指令, 和前面说过的用法相同, 是把8这个数字放在%eax中.下一个指令把数字+3,所以现在eax中的数字是11.  接下来的popl %ebp, ebp的值变成了72,因为这个时候esp执行的位置存放的值就是72,这个值正好就是之前存放的上一个函数的ebp的值, 所以得到如下的图:

技术分享

技术分享

                图8

然后, ret执行,会把leave的地址弹到eip中, 就可以执行leave 指令了.得到的图是: 

技术分享

技术分享

                图9

leave 指令类似一条宏指令, 等价于

   movl %ebp, %esp

   popl %ebp

我们知道,ebp=72指向的位置存了82这个数,正好是上一次存的旧的ebp的值, 所以经过这步得到如下的图.

技术分享

技术分享

                图10

这样, 又遇到了一次ret, 开始执行main 函数中的addl $1, %eax, 由于eax 的值是11, 所以现在变成了12. 然后又碰到leave 指令, 达到清栈的目的, 效果图如下:

技术分享

技术分享

                图11

于是, 栈恢复了初始的状态. 我们可以看到, 在main函数之后, 有一个ret指令. 由于我们之前进入main函数的时候没有考虑地址压栈, 那部分是操作系统来管理的,  所以这里不考虑这条指令的执行.

总结:

一个函数的执行过程, 会有自己的一段从ebp 到esp的栈空间.  对于一个函数, ebp指向的位置的值是调用这个函数的上一个函数的栈空间的ebp的值. 这种机制使得leave指令可以清空一个函数的栈, 达到调用之前的状态. 由于在这个栈设置之前, 有一个eip压栈的过程, 所以leave 以后的ret正好对应了上一个函数的返回地址, 也就是返回上一个函数时要执行的指令的地址. 另外,由于对于一个函数的栈空间来说, ebp指向的位置存了上一个ebp的值, 再往上是上一个函数的返回地址, 再往上是上一个函数压栈传参数的值, 所以我们知道了自己的当前ebp, 就可以通过栈的机制来获得参数.

 

c语言程序运行时的栈与寄存器的变化

原文:http://www.cnblogs.com/syw-casualet/p/5223595.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!