6 调用栈实例分析
本节通过代码实例分析函数调用过程中栈帧的布局、形成和消亡。
6.1 栈帧的布局
示例代码如下:
//StackReg.c
#include
//获取函数运行时寄存器%ebp和%esp的值
#define FETCH_SREG(_ebp, _esp) do{\
asm volatile( \
"movl %%ebp, %0 \n" \
"movl %%esp, %1 \n" \
: "=r" (_ebp), "=r" (_esp) \
); \
}while()
//也可使用gcc扩展register void *pvEbp __asm__ ("%ebp"); register void *pvEsp __asm__ ("%esp");获取,
// pvEbp和pvEsp指针变量的值就是FETCH_SREG(_ebp, _esp)中_ebp和_esp的值
#define PRINT_ADDR(x) printf("[%s]: &"#x" = %p\n", __FUNCTION__, &x)
#define PRINT_SREG(_ebp, _esp) do{\
printf("[%s]: EBP = 0x%08x\n", __FUNCTION__, _ebp); \
printf("[%s]: ESP = 0x%08x\n", __FUNCTION__, _esp); \
printf("[%s]: (EBP) = 0x%08x\n", __FUNCTION__, *(int *)_ebp); \
printf("[%s]: (EIP) = 0x%08x\n", __FUNCTION__, *((int *)_ebp + )); \
printf("[%s]: &"#_esp" = %p\n", __FUNCTION__, &_esp); \
printf("[%s]: &"#_ebp" = %p\n", __FUNCTION__, &_ebp); \
}while()
void tail(int paraTail){
int locTail = ;
int ebpReg, espReg;
FETCH_SREG(ebpReg, espReg);
PRINT_SREG(ebpReg, espReg);
PRINT_ADDR(paraTail);
PRINT_ADDR(locTail);
}
int middle(int paraMid1, int paraMid2, int paraMid3){
int ebpReg, espReg;
tail(paraMid1);
FETCH_SREG(ebpReg, espReg);
PRINT_SREG(ebpReg, espReg);
PRINT_ADDR(paraMid1);
PRINT_ADDR(paraMid2);
PRINT_ADDR(paraMid3);
return ;
}
int main(void){
int ebpReg, espReg;
int locMain = middle(, , );
FETCH_SREG(ebpReg, espReg);
PRINT_SREG(ebpReg, espReg);
PRINT_ADDR(locMain);
return ;
}
StackReg
该程序每个函数都嵌入汇编代码,以获取各函数运行时刻EBP和ESP寄存器的值。每个函数都打印出EBP寄存器所指向内存地址处的值,以及位于其后的函数返回地址。图7给出程序的编译和运行结果。
图7 StackReg运行结果
为便于理解输出结果中数据间的关系,将其转化为图8所示。图左还示出栈的增长方向和栈的内存地址。黑色箭头和寄存器名表示当前栈帧,否则用灰色表示。图中表示tail函数内所看到的栈布局,其中完整示出tail和middle函数的栈帧结构,以及main函数的部分。注意,形参1、2、3(常量)不在栈内。
图8 StackReg栈帧布局
通常每个函数都有自己的栈帧。各栈帧中存放前一个调用函数的栈帧基址,通过该地址域将所有主调函数与被调函数的栈帧以链表形式连在一起。函数调用级数越多,占用的栈空间也越大,因此应小心使用递归函数。
6.2 栈帧的形成
为方便讲解,获取StackReg示例程序所对应的汇编代码片段,如图9所示。在汇编代码中,最左列为指令在内存中的地址,栈帧中的返回地址(return address)即指此类地址。最右列为待执行的汇编指令语句,中间列为该指令在代码段中的16进制表示,可见push %ebp指令仅占一个字节(0x55)。每次CPU执行都要先读取%eip寄存器值,然后定位到%eip指向的汇编指令内存地址,读取该指令并执行。读取指令会使%eip寄存器值增加相应指令的长度(字节数),执行指令后%eip值为下条待执行指令的跳转地址。
图9 StackReg汇编片段
假设程序运行在main刚调用middle函数时,观察栈帧布局如何变化。程序进入middle函数所运行的第一条指令位于内存地址0x804847c处,在运行该指令之前的栈帧结构如图10所示。此时EBP指向main函数栈帧的头部,而ESP所指向的内存中存放程序返回到main函数的指令位置(0x080485c5)。
图10 StackReg运行中栈帧结构-1
被调函数在调用后获得程序的控制权,接着需完成3项工作:建立自己的栈帧,为局部变量分配空间,按需保存寄存器%ebx、%esi和%edi的值。
内存地址0x804847c~0x804847f的指令用于形成middle函数的栈帧。第一条指令(位于地址0x804847c处,简称)将主调函数main的栈帧基址保存到栈上(压栈操作),该地址用于从被调函数堆栈返回到主调函数main中。正是各函数内的这一操作,使得所有栈帧连在一起成为一条链。
将%esp寄存器的值赋值给%ebp寄存器,此时%ebp寄存器中存放当前函数的栈帧基址,以便根据偏移量访问堆栈中的参数或变量。这样便可腾出%esp寄存器以作他用,并在需要时根据%ebp值从当前函数栈顶直接返回栈底。
对%esp进行减操作,即将%esp向低地址处移动40(0x28)个字节,以便在栈上腾出空间来存放局部变量和临时变量。
运行完上述三条指令后,middle函数的栈帧就已形成,如图11所示。图中还示出该函数内的局部变量ebpReg和espReg在栈帧中的位置。
图11 StackReg运行中栈帧结构-2
随后,将执行middle函数体。执行过程中帧基指针EBP保持不变,通过该指针加偏移量即可访问函数实参、局部变量和临时存储内容。即使middle函数内调用其他函数(如tail),甚至递归调用middle自身,只要在这些子调用返回时恢复EBP,就可继续用EBP加偏移量的方式访问实参等信息。
和是middle函数中内嵌的汇编代码,用于获取此时%ebp和%esp寄存器的值。将%ebp寄存器值放入局部变量ebpReg中,则将%esp寄存器值放入局部变量espReg中。其中,0xfffffffc(%ebp)等于(%ebp - 4),表示在帧基指针向低地址偏移四字节的地址处存储的内容(偏移量用补码表示,负值表示向低地址偏移)。
和将main函数中传递来的第一个变量paraMid1值拷贝到%esp寄存器所指向的内存中,为调用tail函数准备实参。此时栈空间如图12所示。
图12 StackReg运行中栈帧结构-3
调用tail函数,该调用将返回地址(EIP指令指针寄存器的内容)压入栈中,调用该指令后的栈空间如图13所示。压栈的返回地址是0x804848d,从图9中可看出该地址指向middle函数内调用tail函数的后一条指令,当tail函数返回时将从该地址处继续运行程序。调用也意味着进入tail函数的栈帧,tail函数采用与middle函数相同方式的建立自己的栈帧。前面图8所示正是tail函数建立栈帧时的内存布局。
图13 StackReg运行中栈帧结构-4
通过以上运行时分析,可看到函数调用过程中堆栈扩展与恢复的动态过程。%esp和%ebp两个寄存器之间的赋值时机,正是主调函数和被调函数职责交替之时。也正是该时机的正确,才能保证堆栈的恢复。
6.3 栈帧的消亡
在把程序控制权返还给主调函数前,被调函数若有返回值,则先将其保存在相应寄存器(通常是%eax)中,然后按需恢复%ebx、%esi和%edi寄存器的值,最后从栈里弹出返回地址。
下面观察tail函数内进行函数返回时栈空间如何变化。为leave指令,将%esp寄存器的值设置为%ebp寄存器值并做一次弹栈操作,将弹栈操作的内容放入%ebp寄存器中。该指令的功能等价于"mov %ebp, %esp"加"pop %ebp",可将tail函数所建立的栈帧清除。该指令执行后的栈布局与图13完全相同。用于将栈上的返回地址弹出到%eip寄存器中,执行该指令后程序返回到middle函数的0x804848d地址处。该指令执行后的栈结构与图12相同。
6.4 返回结构体
分析以下示例程序:
//StackStrt.c
#include
typedef struct{
int member1;
int member2;
int member3;
}T_RET_STRT;
//FETCH_SREG/PRINT_SREG/PRINT_ADDR宏定义,略(详见StackReg.c)
T_RET_STRT func(int paraFunc){
T_RET_STRT locStrtFunc = {.member1=, .member2=, .member3=};
int ebpReg, espReg;
FETCH_SREG(ebpReg, espReg);
PRINT_SREG(ebpReg, espReg);
PRINT_ADDR(paraFunc);
printf("[%s]: (BelowPara) = 0x%08x\n", __FUNCTION__, *((int *)¶Func - ));
PRINT_ADDR(locStrtFunc.member1);
PRINT_ADDR(locStrtFunc.member2);
PRINT_ADDR(locStrtFunc.member3);
return locStrtFunc;
}
int main(void){
int ebpReg, espReg;
T_RET_STRT locStrtMain = func();
FETCH_SREG(ebpReg, espReg);
PRINT_SREG(ebpReg, espReg);
PRINT_ADDR(locStrtMain.member1);
PRINT_ADDR(locStrtMain.member2);
PRINT_ADDR(locStrtMain.member3);
return ;
}
StackStrt
该示例中,main和func函数内均定义类型为T_RET_STRT的局部变量,且func函数的返回值类型也是T_RET_STRT。变量locStrtMain和locStrtFunc的内存将分配在各自函数的栈帧中,那么func函数的locStrtFunc变量值如何通过函数返回值传递到main函数的locStrtMain变量中?编译该程序并运行以观察结果,如图14所示。图15示出func函数内所看到的栈布局。
图14 StackStrt运行结果
图15 StackStrt栈帧布局
从图中可看出,main函数调用func函数时除将后者所需的参数压入栈中外,还将局部变量locStrtMain地址也压入栈中;func函数返回时将locStrtFunc变量的值通过该地址直接拷贝到main函数的locStrtMain变量中,从而省去一次通过栈的中转拷贝。
删除打印等无关语句后,查看StackStrt.c源文件汇编代码如下图所示(略有删减):
图16 StackStrt汇编片段
将局部变量locStrtMain结构体在栈中的地址存入%eax寄存器。将标量参数(100)入栈,因已预留好存储空间,故此处等效于"pushl $0x64"。将%eax中保存的结构体地址(&locStrtMain)入栈,此处等效于"pushl %eax"。
将8(%ebp)处所存储的主调函数locStrtMain结构体地址存入%edx寄存器。至对被调函数栈内的局部变量locStrtFunc结构体赋值。至将locStrtFunc结构体的各个成员变量值依次存入%edx寄存器所指向的内存地址处(&locStrtMain)。将暂存的%edx寄存器内容存入%eax寄存器,此时%eax内存放主调函数结构体locStrtMain的地址。
根据汇编结果,可知func函数被“改编”为以下实现:
void func(T_RET_STRT *pStrtMain, int paraFunc){
T_RET_STRT locStrtFunc = {.member1=, .member2=, .member3=};
pStrtMain->member1 = locStrtFunc.member1;
pStrtMain->member2 = locStrtFunc.member2;
pStrtMain->member3 = locStrtFunc.member3;
return; //此句可有可无
}
modified func
若显式声明结构体指针参数,则可编写更高效的func函数代码:
void func(T_RET_STRT *pStrtMain, int paraFunc){
pStrtMain->member1 = ;
pStrtMain->member2 = ;
pStrtMain->member3 = ;
}
improved func
注意,若T_RET_STRT locStrtMain = func(100)改为func(100),主调函数栈上仍会预留一个结构体变量的空间,然后将该变量地址存入%eax寄存器。和分别变为sub $0x1c, %esp和lea 0xffffffe8(%ebp), %eax。
从以上分析亦知,当函数以结构体或联合体作为返回值时,函数第一个参数存放在栈帧12(%ebp)位置处,而8(%ebp)位置处存放返回值的地址。
C语言函数调用栈(一)
程序的执行过程可看作连续的函数调用.当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行.函数调用过程通常使用堆栈实现,每个用户态进程对应一个调用栈结构(call sta ...
C语言函数调用栈(二)
5 函数调用约定 创建一个栈帧的最重要步骤是主调函数如何向栈中传递函数参数.主调函数必须精确存储这些参数,以便被调函数能够访问到它们.函数通过选择特定的调用约定,来表明其希望以特定方式接收参数.此外, ...
测试c语言函数调用性能因素之测试三
函数调用:即调用函数调用被调用函数,调用函数压栈,被调用函数执行,调用函数出栈,调用函数继续执行的一个看似简单的过程,系统底层却做了大量操作. 操作: 1, 调用函数帧指针 ...
go语言调度器源代码情景分析之四:函数调用栈
本文是系列 第一章 预备知识的第3小节. 什么是栈 栈是一种“后进先出”的数据结构,它相当于一个容器,当需要往容器里面添加元素时只能放在最上面的一个元素之上,需 ...
C语言函数调用时候内存中栈的动态变化详细分析(彩图)
版权声明:本文为博主原创文章,未经博主允许不得转载.欢迎联系我qq2488890051 https://blog.csdn.net/kangkanglhb88008/article/details/8 ...
C语言数据结构----栈与递归
本节主要说程序中的栈函数栈的关系以及栈和递归算法的关系. 一.函数调用时的栈 1.程序调用时的栈是也就是平时所说的函数栈是数据结构的一种应用,函数调用栈一般是从搞地质向低地址增长的,栈顶为内存的低地址 ...
C语言函数调用完整过程
C语言函数调用详细过程 函数调用是步骤如下: 按照调用约定传参 调用约定是调用方(Caller)和被调方(Callee)之间按相关标准 对函数的某些行为做出是商议,其中包括下面内容: 传参顺序:是从左 ...
Linux Debugging(一): 使用反汇编理解C++程序函数调用栈
拿到CoreDump后,如果看到的地址都是????,那么基本上可以确定,程序的栈被破坏掉了.GDB也是使用函数的调用栈去还原"事故现场"的.因此理解函数调用栈,是使用GDB进行现场 ...
在chrome开发者工具中观察函数调用栈、作用域链与闭包
在chrome开发者工具中观察函数调用栈.作用域链与闭包 在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量 ...
随机推荐
HDU--跑道相遇
跑道相遇 Time Limit: 3000/1000 MS (Java/Others) Memory Limit: 65535/32768 K (Java/Others)Total Submis ...
关于codeblock中一些常用的快捷键(搬运)
关于codeblock中一些常用的快捷键(搬运) codeblock作为一个常用的C/C++编译器,是我最常用的一款编译器,但也因为常用,所以有时为了更加快速的操作难免会用到一些快捷键,但是因为我本身 ...
WPF [调用线程无法访问此对象,因为另一个线程拥有该对象。] 解决方案以及如何实现字体颜色的渐变
本文说明WPF [调用线程无法访问此对象,因为另一个线程拥有该对象.] 解决方案以及如何实现字体颜色的渐变 先来看看C#中Timer的简单说明,你想必猜到实现需要用到Timer的相关知识了吧. C# ...
分享记录我的Linux系统入门学习经验
人生很多事情都是巧合,或者说命中注定.就拿Linux于我来说,大学期间也修了Linux基础这门课,但是从来没有想到自己会从事与Linux相关的工作,更没有想过自己可以通过Linux获取更多的东西. 真 ...
12.C#yield return和yield break及实际应用小例(六章6.2-6.4)
晚上好,各位.今天结合书中所讲和MSDN所查,聊下yield关键字,它是我们简化迭代器的关键. 如果你在语句中使用了yield关键字,则意味着它在其中出现的方法.运算符或get访问器是迭代器,通过使用 ...
thinkphp-2
php的跨文件变量? global $g是一个脚本文件中, 函数外部的变量在函数中要使用时的 全局变量 $_GET等是所谓的"超全局变量", 但仍然是只能在一个脚本的范围内使用 要 ...
Bootstrap_CSS全局样式
一.HTML5 文档类型 Bootstrap 使用到的某些 HTML 元素和 CSS 属性需要将页面设置为 HTML5 文档类型.在你项目中的每个页面都要参照下面的格式进行设置.
JMeter Http请求POST提交token失败,取样器结果Response Code 415
Jmeter脚本,http请求以post方式提交token,执行脚本在察看结果中显示失败,取样器结果响应状态Response Code 415,如图:
位置信息类API调用的代码示例合集:中国省市区查询、经纬度地址转换、POI检索等
以下示例代码适用于 www.apishop.net 网站下的API,使用本文提及的接口调用代码示例前,您需要先申请相应的API服务. 中国省市区查询:2017最新中国省市区地址 经纬度地址转换:经纬度 ...
多线程系列之十一:Two-Phase Termination模式
一,Two-Phase Termination模式 翻译过来就是:分两阶段终止 二,示例程序 public class CountupTread extends Thread { private lo ...