概述
原创作品转载请注明出处
参考材料 《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, @function
g:
.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, @function
f:
.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, @function
main:
.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
ret
f:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call g
leave
ret
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $8, (%esp)
call f
addl $1, %eax
leave
ret
在这里,我们可以清晰地看到汇编代码和三个3函数之间的对应关系.我们补充两张代码的图例:
接下来我们从main函数开始分析:
首先是
pushl %ebp
movl %esp, %ebp
subl $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 %ebp
movl %esp, %ebp
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, 就可以通过栈的机制来获得参数.
转载于:https://www.cnblogs.com/syw-casualet/p/5223595.html
最后
以上就是机智冰棍为你收集整理的c语言程序运行时的栈与寄存器的变化的全部内容,希望文章能够帮你解决c语言程序运行时的栈与寄存器的变化所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复