middle函数C语言,C语言函数调用栈(三)

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给出程序的编译和运行结果。

69ad4c7f2fda3a187bd9d1a5c886b427.png

图7 StackReg运行结果

为便于理解输出结果中数据间的关系,将其转化为图8所示。图左还示出栈的增长方向和栈的内存地址。黑色箭头和寄存器名表示当前栈帧,否则用灰色表示。图中表示tail函数内所看到的栈布局,其中完整示出tail和middle函数的栈帧结构,以及main函数的部分。注意,形参1、2、3(常量)不在栈内。

1c29e06e0fb5b54e1258380647d4b1d5.png

图8 StackReg栈帧布局

通常每个函数都有自己的栈帧。各栈帧中存放前一个调用函数的栈帧基址,通过该地址域将所有主调函数与被调函数的栈帧以链表形式连在一起。函数调用级数越多,占用的栈空间也越大,因此应小心使用递归函数。

6.2 栈帧的形成

为方便讲解,获取StackReg示例程序所对应的汇编代码片段,如图9所示。在汇编代码中,最左列为指令在内存中的地址,栈帧中的返回地址(return

address)即指此类地址。最右列为待执行的汇编指令语句,中间列为该指令在代码段中的16进制表示,可见push

%ebp指令仅占一个字节(0x55)。每次CPU执行都要先读取%eip寄存器值,然后定位到%eip指向的汇编指令内存地址,读取该指令并执行。读取指令会使%eip寄存器值增加相应指令的长度(字节数),执行指令后%eip值为下条待执行指令的跳转地址。

95f05e9ad4d94fdf1a818200a7ddf18d.png

图9 StackReg汇编片段

假设程序运行在main刚调用middle函数时,观察栈帧布局如何变化。程序进入middle函数所运行的第一条指令位于内存地址0x804847c处,在运行该指令之前的栈帧结构如图10所示。此时EBP指向main函数栈帧的头部,而ESP所指向的内存中存放程序返回到main函数的指令位置(0x080485c5)。

bde4f0ac422eefafd053e1ef72686a22.png

图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在栈帧中的位置。

fefb1950bb8f7b977fc0927b3b5cbc9c.png

图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所示。

1f671a261eb9a369520dc70e370aa9b4.png

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

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

54fa14dbdac693a191fafcdf217d1119.png

图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函数内所看到的栈布局。

3f33e6b0701ff5a6a67c4b1b41f9c2a4.png

图14 StackStrt运行结果

d56250c628d9c0a272a80660a243369b.png

图15 StackStrt栈帧布局

从图中可看出,main函数调用func函数时除将后者所需的参数压入栈中外,还将局部变量locStrtMain地址也压入栈中;func函数返回时将locStrtFunc变量的值通过该地址直接拷贝到main函数的locStrtMain变量中,从而省去一次通过栈的中转拷贝。

删除打印等无关语句后,查看StackStrt.c源文件汇编代码如下图所示(略有删减):

1cc45c66d3b2b01ba31ac7dd3b54f826.png

图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)位置处存放返回值的地址。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/421998.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

短信恢复 android,苹果手机短信恢复:安卓+苹果手机短信恢复教程,必须收藏!...

原标题:苹果手机短信恢复:安卓苹果手机短信恢复教程,必须收藏!手机短信怎么恢复?日常生活中,大家或多或少会不小心把删除一些不必要的短信,比如注册某软件的验证码的短信,面试通知地…

Linux编程简介——VI

VI是Linux/Unix下标配的一个纯字符界面的文本编辑器。由于不支持鼠标功能,也没有图形界面,相关的操作都要通过键盘指令来完成,需要记忆大量命令。因此很多人不大喜欢它,但同时由于键盘的方式往往比鼠标来得快,一旦熟练…

xamarin android 标签,安卓端Tabbedpage调整在底部位置和标签及取消Android API28 以下的点击特效—-xamarin.forms学习笔记(一)...

使用tabbedpage时将安卓端导航放在底部,官网也有说明方法.总结:xmlns"http://xamarin.com/schemas/2014/forms"xmlns:x"http://schemas.microsoft.com/winfx/2009/xaml"xmlns:local"clr-namespace:aya"x:Class"aya.MainPage"xmlns:and…

android广播第三方库,Android Support 库:LocalBroadcastManager

在介绍完 Android Support 库发展历程(http://blog.chengyunfeng.com/?p1047)后, 再分别介绍下 Android Support 库中有用但是被忽略的一些功能。了解这些功能,在需要的时候可以避免在引入其他类似的第三方库或者避免自己重复制造轮子,提高开…

没有android:padding属性,android自定义无上下padding的textview

因为工作需要需要无上下padding的textview,经过查阅资料,很多说xml中的配置includefontpaddingfalse,但无法实现0 padding的需要。也有说通过设置负数的marginTop实现,但是不同字体,需要设置的值不同(因为自带的paddin…

科技论文的时态问题

如何在科技论文中使用时态英语谓语动词时态共有16种,在英文科技论文中用得较为频繁的主要有三种:即一般现在时、一般过去时和将来时。正确地使用动词时态是科研写作的基本功,我们在撰写英文论文时,如不能正确选用时态,…

android 卡片放大变详情页,ConstraintLayout动画实现布局卡片式滑动放大缩放

ConstraintLayout(约束布局)是Android Studio 2.2中主要的新增功能之一,Android studio升级到2.3版本之后,不管是新建Activity或fragment,xml默认布局由RelativeLayout更改为ConstraintLayout了。按照以往惯例先上效果图:GIF.gif上…

开源路由软件zebra的命令存储原理及使用方法

ZZ FROM: http://www.cnblogs.com/iTsihang/archive/2012/12/05/2783252.html zebra在设计命令格式的时候,将命令节点Node和命令Command分开,整个命令结构是一个树状的,如图: NODEn表示命令节点,CMDn表示具体的命令。在zebra的整…

网易邮箱写邮件HTML转换按钮,网易邮箱推出虚拟场景写信功能 身临其境写邮件...

当你用电子邮件给好友发出问候时,是否怀念着曾经繁茂成荫、绿野同行的过往?当你给他(她)写信表达思念时,是否憧憬着秋风初起、阳光清透的意境?近期,网易邮箱独家推出“虚拟场景写信”功能,可以帮助用户在写…

使用自定义表类型(SQL Server 2008)

在 SQL Server 2008 中,用户定义表类型是指用户所定义的表示表结构定义的类型。您可以使用用户定义表类型为存储过程或函数声明表值参数,或者声明您要在批处理中或在存储过程或函数的主体中使用的表变量。有关如何定义表结构的详细信息,请参阅…

用excel打开文本内容

linux中直接从数据库导出数据存为txt文本,用文本编辑器打开是这样的,不适合统计数据,转成excel就万事大吉了,这点事微软怎么会想不到呢直接上图:every thing goes well!! :-)来自为知笔记(Wiz)转载于:https://www.cnblogs.com/fir…

html框架有什么作用,使用HTML5+CSS+JS框架有那些好处

使用HTML5CSSJS框架有那些好处2017-12-08相信很多程序猿朋友都用过框架,不过你是否知道你用的是HTML框架、CSS框架还是JS框架,其实这都不重要,重要的是使用框架的目的是什么?是不是节约了开发项目时间陈本,这事多么伟大…