原创作品转载请注明出处
参考材料 《Linux内核分析》 MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
作者:Casualet
我们在这里从汇编代码的角度, 给出一段简单的C语言程序运行过程中机器状态的变化情况. 我们的实验环境是Ubuntu 64位, 编译器gcc的版本是4.8.4.
我们使用的c程序如下:
int g(int x){ return x + 3;}int f(int x){ return g(x);}int main(void){ return f(8) + 1;}这个简单的c程序有一个main函数, 在main函数里调用了f函数, 然后f函数调用了g函数. 我们把其编译成32位的汇编代码, 使用的命令是: gcc -S -o main.s main.c -m32. 这样,我们获得了汇编代码文件main.s, 打开以后可以看到这种效果:
 .file "test.c" .text .globl g .type g, @functiong:.LFB0: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 movl 8(%ebp), %eax addl $3, %eax popl %ebp .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc.LFE0: .size g, .-g .globl f .type f, @functionf:.LFB1: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp) call g leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc.LFE1: .size f, .-f .globl main .type main, @functionmain:.LFB2: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 subl $4, %esp movl $8, (%esp) call f addl $1, %eax leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc.LFE2: .size main, .-main .ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4" .section .note.GNU-stack,"",@progbits由于以点开头的都是链接时候用到的信息, 跟实际的代码执行逻辑没有关系, 为了方便分析,我们给出删除了以点开头的行以后的代码版本:
g: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax addl $3, %eax   popl %ebp retf: pushl %ebp movl %esp, %ebp subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp)  call g leave retmain: pushl %ebp movl %esp, %ebp subl $4, %esp movl $8, (%esp) call f addl $1, %eax leave ret在这里,我们可以清晰地看到汇编代码和三个3函数之间的对应关系.我们补充两张代码的图例:


接下来我们从main函数开始分析:
首先是
pushl %ebpmovl %esp, %ebpsubl $4, %esp

图1


图2


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


图4
接下来,调用函数f, 这一步会把eip压栈. eip指向的是call的下一条指令, addl $1, %eax. 进入f函数以后, 又进行以下三步:
pushl %ebpmovl %esp, %ebpsubl $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, 就可以通过栈的机制来获得参数.
原文:http://www.cnblogs.com/syw-casualet/p/5223595.html