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(0)

//也可使用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 + 1)); \

printf("[%s]: &"#_esp" = %p\n", __FUNCTION__, &_esp); \

printf("[%s]: &"#_ebp" = %p\n", __FUNCTION__, &_ebp); \

}while(0)

void tail(int paraTail){

int locTail = 0;

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 1;

}

int main(void){

int ebpReg, espReg;

int locMain = middle(1, 2, 3);

FETCH_SREG(ebpReg, espReg);

PRINT_SREG(ebpReg, espReg);

PRINT_ADDR(locMain);

return 0;

}

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=1, .member2=2, .member3=3};

int ebpReg, espReg;

FETCH_SREG(ebpReg, espReg);

PRINT_SREG(ebpReg, espReg);

PRINT_ADDR(paraFunc);

printf("[%s]: (BelowPara) = 0x%08x\n", __FUNCTION__, *((int *)&paraFunc - 1));

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(100);

FETCH_SREG(ebpReg, espReg);

PRINT_SREG(ebpReg, espReg);

PRINT_ADDR(locStrtMain.member1);

PRINT_ADDR(locStrtMain.member2);

PRINT_ADDR(locStrtMain.member3);

return 0;

}

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=1, .member2=2, .member3=3};

pStrtMain->member1 = locStrtFunc.member1;

pStrtMain->member2 = locStrtFunc.member2;

pStrtMain->member3 = locStrtFunc.member3;

return; //此句可有可无

}

modified func

若显式声明结构体指针参数,则可编写更高效的func函数代码:

void func(T_RET_STRT *pStrtMain, int paraFunc){

2 pStrtMain->member1 = 1;

3 pStrtMain->member2 = 2;

4 pStrtMain->member3 = 3;

5 }

注意,若T_RET_STRT locStrtMain = func(100)改为func(100),主调函数栈上仍会预留一个结构体变量的空间,然后将该变量地址存入%eax寄存器。和分别变为sub

$0x1c, %esp和lea 0xffffffe8(%ebp), %eax。

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

middle函数C语言,C语言函数调用栈(三)相关推荐

  1. c语言中staloc是什么意思,C语言函数调用栈(三)

    6 调用栈实例分析 本节通过代码实例分析函数调用过程中栈帧的布局.形成和消亡. 6.1 栈帧的布局 示例代码如下: //StackReg.c #include //获取函数运行时寄存器%ebp和%es ...

  2. linux调用堆栈函数,使用 backtrace 获得 Linux 函数调用栈

    一.源码 使用 backtrace 获得 Linux 函数调用栈 源码来自:man backtrace #include #include #include #include void myfunc3 ...

  3. 基于GDB-peda汇编调试理解函数调用栈

    一.前言 1.1 叙叙旧 距离上一次写文章已经过去3个月了,当初计划至少一个月一篇,不曾想这一拖就是三个月.一直不写的主要原因是当把一个问题弄清楚了,或者说掌握了一个东西,就觉得没有什么可值得写:另外 ...

  4. 【软件开发底层知识修炼】十七 快速学习GDB调试四 使用GDB进行函数调用栈的查看

    上一篇文章学习了如何使用GDB数据断点进行内存监测:[软件开发底层知识修炼]十五 快速学习GDB调试三 使用GDB的数据断点监测变量是否改变 本篇文章继续上一篇文章的学习:如何使用GDB进行函数调用栈 ...

  5. 在chrome开发者工具中观察函数调用栈、作用域链、闭包

    在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量对象,闭包,this等关键信息的变化.因此,断点调试对于快 ...

  6. C 语言 函数调用栈

    From:https://www.cnblogs.com/clover-toeic/p/3755401.html    https://www.cnblogs.com/clover-toeic/p/3 ...

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

    以下全文转载自:C语言函数调用栈(一) 程序的执行过程可看作连续的函数调用.当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行.函数调用过程通常使用堆栈实现,每个用户态 ...

  8. c语言结构体调用成员函数,c语言结构体函数调用参数如何设置

    c语言结构体函数调用参数怎么设置 函数结构是下面的代码,main函数中如何调用showinfo函数,参数应该怎么设置,对参数的设置不太明白 C/C++ code#include #define SIZ ...

  9. c语言中调用函数fn,C语言常见的函数调用

    C语言常见的函数调用 isatty,函数名,主要功能是检查设备类型,判断文件描述词是否为终端机. 函数名: isatty 用 法: int isatty(int desc); 返回值:如果参数desc ...

最新文章

  1. 个人开发者即时到账收款方案 BufPay.com
  2. 操作系统习题4—进程死锁
  3. 1143 Lowest Common Ancestor (30 分)【难度: 中 / 知识点: 最低公共祖先 未完成】
  4. 二级级联菜单---纯js实现
  5. java图片上传下载_java web 文件上传与下载
  6. 《DB 查询分析器》中断SQL语句的执行
  7. python开发Day05(计算器代码)
  8. netapp存储常用命令
  9. 开源MSSQL Express Profile 文件
  10. 想成为NLP算法工程师,你必须要看一下这10篇论文!
  11. 新一代RPC框架-GRPC
  12. 软件测试基础+微信小程序测试实战
  13. exoplay切换全屏_Flutter 实现视频全屏播放逻辑及解析
  14. OpenGL中PBR着色器的实现
  15. 搭建vmware虚拟机,搭建docker环境
  16. Vue 解决兄弟组件之间传值问题
  17. 宋图图的工学课程12
  18. 获取微信推文的实际链接地址(包含JSON数据以及推文msg_data_id)
  19. QT + FFmpeg 5.x + x264 + x265 + SDL2 音视频播放器
  20. DNSPod十问魏雪:云游戏的野心有多大?

热门文章

  1. 前端学习(338):堆栈
  2. cmd 调用JLink.exe 下载程序
  3. 无线网络共享到CM3计算板调试时 connect: Network is unreachable
  4. linux防火墙查看被动模式,Centos7搭建vsftpd及被动模式下的防火墙设置
  5. mysql 获取结果_【原创】7. MYSQL++中的查询结果获取(各种Result类型)
  6. Rstudio调用plot()函数时,出现错误的处理方法
  7. BI@Report烂笔头
  8. [No0000D7]img生成器.bat合并所有图片到html网页中
  9. python写http post请求的四种请求体
  10. Perl学习笔记(六)--文件(一)