今天突然想分析一下函数在相互调用过程中栈帧的变化,还是想尽量以比较清晰的思路把这一过程描述出来,关于c函数调用原理的理解是很重要的。
1.关于栈
首先必须明确一点也是非常重要的一点,栈是向下生长的,所谓向下生长是指从内存高地址->低地址的路径延伸,那么就很明显了,栈有栈底和栈顶,那么栈顶的地址要比栈底低。对x86体系的CPU而言,其中
---> 寄存器ebp(base pointer )可称为“帧指针”或“基址指针”,其实语意是相同的。
---> 寄存器esp(stack pointer)可称为“ 栈指针”。
要知道的是:
---> ebp 在未受改变之前始终指向栈帧的开始,也就是栈底,所以ebp的用途是在堆栈中寻址用的。
---> esp是会随着数据的入栈和出栈移动的,也就是说,esp始终指向栈顶。
见下图,假设函数A调用函数B,我们称A函数为"调用者",B函数为“被调用者”则函数调用过程可以这么描述:
(1)先将调用者(A)的堆栈的基址(ebp)入栈,以保存之前任务的信息。
(2)然后将调用者(A)的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者B的栈底)。
(3)然后在这个基址(被调用者B的栈底)上开辟(一般用sub指令)相应的空间用作被调用者B的栈空间。
(4)函数B返回后,从当前栈帧的ebp即恢复为调用者A的栈顶(esp),使栈顶恢复函数B被调用前的位置;然后调用者A再从恢复后的栈顶可弹出之前的ebp值(可以这么做是因为这个值在函数调用前一步被压入堆栈)。这样,ebp和esp就都恢复了调用函数B前的位置,也就是栈恢复函数B调用前的状态。
这个过程在AT&T汇编中通过两条指令完成,即:
leave
ret
这两条指令更直白点就相当于:
mov %ebp , %esp
pop %ebp
2.举个简单的实例,从汇编的视角看函数调用
2.1建立一个简单的程序,程序文件名为 main.c
开发测试环境:
Ubuntu 12.04
gcc版本:4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5) (是Ubuntu自带的)
- <spanstyle="font-size:18px;">
- voidswap(int*a,int*b)
- {
- intc;
- c=*a;
- *a=*b;
- *b=c;
- }
- intmain(void)
- {
- inta;
- intb;
- intret;
- a=16;
- b=64;
- ret=0;
- swap(&a,&b);
- ret=a-b;
- returnret;
- }</span>
2.2编译
#gcc -g -o main main.c
#objdump -d main > main.dump
#gcc -Wall -S -o main.s main.c
这样大家可以看main.s也可以看main.dump,这里我们选择使用main.dump。
截取关键的部分,即_start, swap , main,为什么会有_start呢,因为ELF格式的入口其实是_start而不是main()。下面的图展示了main()函数调用swap()前后的栈空间的结构。右边的数字代表相对帧指针的偏移字节数。后面我们使用GDB调试就会发现栈的变化跟下图是一致的。
(!!!请注意,由于栈对齐的缘故,编译器分配栈空间时可能会有没用到的内存地址,而这些没使用到的内存地址就没在下图表示出来,所以下图只能当作示意图来了解函数栈帧结构!!具体的栈内存内容以下文的GDB调试的信息为准!!!)
下面是main.dump中_start的代码注释,比较重要的是对esp的栈对齐操作,esp是16字节对齐的,注意左边行号的右边的0x8048300一类的数字是指令地址。
下面是main.dump中swap()函数和main()函数的汇编代码,代码旁有详细的注释。
下面我们使用GDB调试main.c的代码,使用刚才编译好的main镜像。
# gdb start (启动gdb)
# (gdb) file main (加载镜像文件)
# (gdb) break main (把main()设置为断点,注意gdb并没有把断点设置在main的第一条指令,而是设置在了调整栈指针为局部变量保留空间之后)
# (gdb) run (运行程序)
# (gdb) stepi (单步执行,不熟悉gdb的童鞋要注意了,stepi命令执行之后显示出来的源代码行或者指令地址,都是即将执行的指令,而不是刚刚执行完的指令!)
原文地址:http://blog.csdn.net/zsy2020314/article/details/9429707
v
分享到:
相关推荐
1. x86 Instruction Set Reference 2. x86 Disassembly/Functions and Stack Frames 3
【应聘笔记系列】堆栈、栈帧与函数调用过程分析,C-C++堆栈指引
CSDN 博文,对栈帧的概念以及函数调用过程栈的变化做了详细的分析!
本节通过代码实例分析函数调用过程中栈帧的布局、形成和消亡。示例代码如下:该程序每个函数都嵌入汇编代码,以获取各函数运行时刻EBP和ESP寄存器的值。每个函数都打印出EBP寄存器所指向内存地址处的值,以及位于其...
堆栈由谁清除这个很重要,如果是要写汇编函数给C调用,一定要小心堆栈的清除工作,如果是__cdecl方式的函数,则函数本身(如果不用汇编写)则不需要关心保存参数的堆栈的清除,但是如果是__stdcall的规则,一定要在...
函数调用时栈以及寄存器指针的变化 举了一个具体的例子来说明,配有图示
所以函数调用就是改变这两个值函数调用在对子函数进行调用时,栈帧情况如下:调用者栈帧中,保存了被调用函数的参数以及调用者的返回地址,其流程大致如下:父函数将调用参
这是一篇有关编写函数调用原理的文章,通过大量的示意图从比较基础的概念开始分解函数调用时交替的完整变化过程,同时将通过特定示例来分析在X-64平台上函数调用在汇编级的表示,从而深刻理解函数调用原理。...
在imul 指令中,两个64位的乘法最多会产生128位的结果,需要 %rax 与 %rdx 共同存储乘法结果,在div 指令中被除数是128 位的,同样需要%r
创建一个栈帧的最重要步骤是主调函数如何向栈中传递函数参数。主调函数必须精确存储这些参数,以便被调函数能够访问到它们。函数通过选择特定的调用约定,来表明其希望以特定方式接收参数。此外,当被调函数完成任务...
3.1 工程的创建 3.2.1 函数中无任何局部变量的情况 3.2.2 函数中只有一个局部变量的情况 3.2.3 函数中有多个局部变量的情况
函数调用时怎么做的?函数调用结束后是怎么返回的?我们在学习c语言的时期,经常被这些问题困扰,这些问题都将在本资源中得到讲解,其次,这份资源是一份笔记文件,如有不足之处请多包涵,如果想要其他部分的笔记...
在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。 由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。 实例:...
也是是Pop出来的对象没有Push回去,情况很难复现,所以在Pop里的打印日志,跟踪是谁调用了它,我想在GDB调试里可以追踪调用的栈帧,那也一定有方法实现。首先上网搜索了一下,并没有结果!还好代码量不是很多,只能...
在KEIL MDK中演示链接寄存器在BL指令执行时的变化,以及函数在调用过程中堆栈的压栈和出栈,从堆栈区里查看到没有栈帧里所述的PC、SP、FP等压栈。网上一篇文章说"ARM压栈的顺序很是规矩(也比较容易被黑客攻破么),...
# python解释器 python.exe 会用一个叫做PyEval_EvalFrameEx(c语言函数)去执行foo函数,首先会创建一个栈帧(stack frame), """ python在运行前会编译成字节码对象 当foo调用bar函数进,又会创建一个栈帧, 关键是所有的...
(2)函数调用深度 在很多系统中都对函数调用的深度做了限制,函数调用深度是指函数嵌套的程度 (3)函数调用的参数 栈帧部分已经描述了函数参数的保存位