6 调用栈实例分析

本节通过代码实例分析函数调用过程中栈帧的布局、形成和消亡。

6.1 栈帧的布局

示例代码如下:

 //StackReg.c
#include <stdio.h> //获取函数运行时寄存器%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处,简称<指令804847c>)将主调函数main的栈帧基址保存到栈上(压栈操作),该地址用于从被调函数堆栈返回到主调函数main中。正是各函数内的这一操作,使得所有栈帧连在一起成为一条链。

<指令804847d>将%esp寄存器的值赋值给%ebp寄存器,此时%ebp寄存器中存放当前函数的栈帧基址,以便根据偏移量访问堆栈中的参数或变量。这样便可腾出%esp寄存器以作他用,并在需要时根据%ebp值从当前函数栈顶直接返回栈底。

<指令804847f>对%esp进行减操作,即将%esp向低地址处移动40(0x28)个字节,以便在栈上腾出空间来存放局部变量和临时变量。

运行完上述三条指令后,middle函数的栈帧就已形成,如图11所示。图中还示出该函数内的局部变量ebpReg和espReg在栈帧中的位置。

图11 StackReg运行中栈帧结构-2

随后,将执行middle函数体。执行过程中帧基指针EBP保持不变,通过该指针加偏移量即可访问函数实参、局部变量和临时存储内容。即使middle函数内调用其他函数(如tail),甚至递归调用middle自身,只要在这些子调用返回时恢复EBP,就可继续用EBP加偏移量的方式访问实参等信息。

<指令804848d>和<指令804848f>是middle函数中内嵌的汇编代码,用于获取此时%ebp和%esp寄存器的值。<指令8048491>将%ebp寄存器值放入局部变量ebpReg中,<指令8048494>则将%esp寄存器值放入局部变量espReg中。其中,0xfffffffc(%ebp)等于(%ebp - 4),表示在帧基指针向低地址偏移四字节的地址处存储的内容(偏移量用补码表示,负值表示向低地址偏移)。

<指令8048482>和<指令8048485>将main函数中传递来的第一个变量paraMid1值拷贝到%esp寄存器所指向的内存中,为调用tail函数准备实参。此时栈空间如图12所示。

图12 StackReg运行中栈帧结构-3

<指令8048488>调用tail函数,该调用将返回地址(EIP指令指针寄存器的内容)压入栈中,调用该指令后的栈空间如图13所示。压栈的返回地址是0x804848d,从图9中可看出该地址指向middle函数内调用tail函数的后一条指令,当tail函数返回时将从该地址处继续运行程序。调用<指令8048488>也意味着进入tail函数的栈帧,tail函数采用与middle函数相同方式的建立自己的栈帧。前面图8所示正是tail函数建立栈帧时的内存布局。

图13 StackReg运行中栈帧结构-4

通过以上运行时分析,可看到函数调用过程中堆栈扩展与恢复的动态过程。%esp和%ebp两个寄存器之间的赋值时机,正是主调函数和被调函数职责交替之时。也正是该时机的正确,才能保证堆栈的恢复。

6.3 栈帧的消亡

在把程序控制权返还给主调函数前,被调函数若有返回值,则先将其保存在相应寄存器(通常是%eax)中,然后按需恢复%ebx、%esi和%edi寄存器的值,最后从栈里弹出返回地址。

下面观察tail函数内进行函数返回时栈空间如何变化。<指令804847a>为leave指令,将%esp寄存器的值设置为%ebp寄存器值并做一次弹栈操作,将弹栈操作的内容放入%ebp寄存器中。该指令的功能等价于"mov %ebp, %esp"加"pop %ebp",可将tail函数所建立的栈帧清除。该指令执行后的栈布局与图13完全相同。<指令804847b>用于将栈上的返回地址弹出到%eip寄存器中,执行该指令后程序返回到middle函数的0x804848d地址处。该指令执行后的栈结构与图12相同。

6.4 返回结构体

分析以下示例程序:

 //StackStrt.c
#include <stdio.h> 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 *)&paraFunc - ));
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汇编片段

<指令804839a>将局部变量locStrtMain结构体在栈中的地址存入%eax寄存器。<指令804839d>将标量参数(100)入栈,因<指令8048397>已预留好存储空间,故此处等效于"pushl $0x64"。<指令8048397>将%eax中保存的结构体地址(&locStrtMain)入栈,此处等效于"pushl %eax"。

<指令804835a>将8(%ebp)处所存储的主调函数locStrtMain结构体地址存入%edx寄存器。<指令804835d>至<指令804836b>对被调函数栈内的局部变量locStrtFunc结构体赋值。<指令8048372>至<指令8048380>将locStrtFunc结构体的各个成员变量值依次存入%edx寄存器所指向的内存地址处(&locStrtMain)。<指令8048383>将暂存的%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寄存器。<指令8048397>和<指令804839a>分别变为sub $0x1c, %esp和lea 0xffffffe8(%ebp), %eax。

从以上分析亦知,当函数以结构体或联合体作为返回值时,函数第一个参数存放在栈帧12(%ebp)位置处,而8(%ebp)位置处存放返回值的地址。

 

C语言函数调用栈(三)的更多相关文章

  1. C语言函数调用栈

    C语言函数调用栈 栈溢出(stack overflow)是最常见的二进制漏洞,在介绍栈溢出之前,我们首先需要了解函数调用栈. 函数调用栈是一块连续的用来保存函数运行状态的内存区域,调用函数(calle ...

  2. C语言函数调用栈(一)

    程序的执行过程可看作连续的函数调用.当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行.函数调用过程通常使用堆栈实现,每个用户态进程对应一个调用栈结构(call sta ...

  3. C语言函数调用栈(二)

    5 函数调用约定 创建一个栈帧的最重要步骤是主调函数如何向栈中传递函数参数.主调函数必须精确存储这些参数,以便被调函数能够访问到它们.函数通过选择特定的调用约定,来表明其希望以特定方式接收参数.此外, ...

  4. 测试c语言函数调用性能因素之测试三

    函数调用:即调用函数调用被调用函数,调用函数压栈,被调用函数执行,调用函数出栈,调用函数继续执行的一个看似简单的过程,系统底层却做了大量操作. 操作: 1,               调用函数帧指针 ...

  5. go语言调度器源代码情景分析之四:函数调用栈

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第3小节. 什么是栈 栈是一种“后进先出”的数据结构,它相当于一个容器,当需要往容器里面添加元素时只能放在最上面的一个元素之上,需 ...

  6. C语言函数调用时候内存中栈的动态变化详细分析(彩图)

    版权声明:本文为博主原创文章,未经博主允许不得转载.欢迎联系我qq2488890051 https://blog.csdn.net/kangkanglhb88008/article/details/8 ...

  7. 从栈上理解 Go语言函数调用

    转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/518 本文使用的go的源码 1.15.7 前言 函数调用类型 这篇文 ...

  8. C语言数据结构----栈与递归

    本节主要说程序中的栈函数栈的关系以及栈和递归算法的关系. 一.函数调用时的栈 1.程序调用时的栈是也就是平时所说的函数栈是数据结构的一种应用,函数调用栈一般是从搞地质向低地址增长的,栈顶为内存的低地址 ...

  9. C语言函数调用完整过程

    C语言函数调用详细过程 函数调用是步骤如下: 按照调用约定传参 调用约定是调用方(Caller)和被调方(Callee)之间按相关标准 对函数的某些行为做出是商议,其中包括下面内容: 传参顺序:是从左 ...

随机推荐

  1. 自学Python3.2-函数分类(内置函数)

    自学Python之路-Python基础+模块+面向对象自学Python之路-Python网络编程自学Python之路-Python并发编程+数据库+前端自学Python之路-django 自学Pyth ...

  2. 【BZOJ2228】[ZJOI2011]礼物(单调栈)

    [BZOJ2228][ZJOI2011]礼物(单调栈) 题面 BZOJ 洛谷 题解 如果这个玩意不是一个三维立方体,而是一个二维的矩形,让你在里面找一个最大正方形,那么全世界都会做. 丢到三维上?似乎 ...

  3. 一种BCD码转换的算法

    #include "stdio.h" typedef unsigned char uint8_t; typedef unsigned short uint16_t; typedef ...

  4. hdu 2158 最短区间版大家来找碴(尺取法)

    Problem Description 给定一个序列,有N个整数,数值范围为[0,N).有M个询问,每次询问给定Q个整数,可能出现重复值.要求找出一个最短区间,该区间要包含这Q个整数数值.你能找的出来 ...

  5. Spring Cloud(五) --- zuul

    微服务网关 在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务.当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用 ...

  6. 使用android快速开发框架afinal的FinalDb操作android数据库

    http://my.oschina.net/yangfuhai/blog/87459 今天给大家介绍下#afinal#来操作android的数据库sqlite. #afinal#是一个android的 ...

  7. C++11并发——多线程lock_gurad ,unique_lock (三)

    http://www.cnblogs.com/haippy/p/3346477.html struct defer_lock_t {}; 该类型的常量对象 defer_lock(defer_lock ...

  8. git branch 分支管理

    在多人协作的情况下,master通常是稳定的分支.可以再建一些"develop","testing"等名称的分支.主管master的人做开发的话最好也建立自己的 ...

  9. 洛谷 P1083 借教室

    传送门:Probem 1083 https://www.cnblogs.com/violet-acmer/p/9721160.html 一.暴力简述 首先我们不难看出,这道题--并不是一道多难的题,因 ...

  10. 装饰页面decorators.xml

    WEB-INF/decorators.xml 这个配置可以增加页面的 装饰页面