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,一经查实,立即删除!

相关文章

c语言多个形参,C中子函数最多有几个形参

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼C89 31个,C99 127个。ANSI C892.2.4.1 Translation limitsThe implementation shall be able to translate and execute at least one program that contains at least one instance of every one of the following lim…

什么叫ERP软件

1.ERP是什么? erp是个管理系统!是一个成套的统称!构成:进销存BOM生产MRP质检(IQC/FQC/PQC)车间日报/简单排产应收应付固定资产实际成本算法自动财务。就是通过软件管理,实现最小的库存和最大的利益! 2.什么…

android 杀 服务器,android busybox httpd搭建简单web服务器

安卓手机利用busybox中的httpd命令可以创建一个简单的web服务器。我们可以在终端模拟器中输入一下命令查看httpd的使用帮助。busybox httpd --help1|u0_a84cancro:/ $ busybox httpd --helpBusyBox v1.20.2-MIUI (2012-08-20 20:59:32 CST) multi-call binary.Usage: httpd [-i…

sublimeclang里面使用相对路径

在sublime的project里面,可以添加sublimeclang的options,比如这样 {"folders":[{"path": "/C/EverBox/gitCode/clangxx"}],"settings": {"sublimeclang_options": ["-Wall","-IC:\\EverBox\\gitC…

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

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

Shell应用:批量将文件编码由gbk转utf-8

Shell应用:批量将文件编码由gbk转utf-8,实例代码: [plain]#!/bin/bash // batch_change_GB2312_to_UTF-8 cd directory find ./ -type f -name "*.java" | while read line;do echo $line iconv -f GB2312 -t UTF-8 $line > ${lin…

c语言立体图像编程,C语言-编程实例-三视图的画法

#includeint dx[12]{0,60,60,0,0,60,60,0,60,0,60,0}; /*确定组合体X坐标*/int dy[12]{0,0,120,120,0,0,40,40,80,80,120,120};/*确定组合体Y坐标*/int dz[12]{0,0,0,0,80,80,80,80,40,40,40,40};/*确定组合体Z坐标*/int ld[24]{0,1,2,3,0,4,5,6,7,4,10,11,9,8,10,2,3,11,8,6,9…

【js拾遗】名称空间

function NameSpace(ns) {if (!ns) {return null;}var arr ns.split(.);//第一次调用的时候window.utry的值为{};//第二次调用的时候window.utry的值为window.utry;window[arr[0]] window[arr[0]] || {};//obj是window.utry的引用var obj window[arr[0]];//result也是window…

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…

算法之递推及其应用(递推关系的建立及在信息学竞赛中的应用 安徽 高寒蕊)...

定义&#xff1a;给定一个数的序列H0,H1,…,Hn,…若存在整数n0&#xff0c;使当nn0时,可以用等号(或大于号、小于号)将Hn与其前面的某些项Hn(0i<n)联系起来&#xff0c;这样的式子就叫做递推关系。 递推关系中存在着三大基本问题&#xff1a;如何建立递推关系&#xff0c;已…

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

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

2012年总结

2012年是一个特别的年头。 这一年&#xff0c;我的准研究生生活和研究生生活加起来已经一年了。最近在进行期末考试&#xff0c;自己也对自己这一年的经历进行一下总结&#xff0c;留在研一结束的时候对照一下。 自己在保研的时候不是很顺利&#xff08;主要是能力不足&#xf…

android 7调用摄像头,Android调用摄像头拍照(兼容7.0)

【实例简介】Android调用摄像头拍照(兼容7.0)Demo&#xff0c;原博客文章https://blog.csdn.net/u010356768/article/details/70808162【实例截图】【核心代码】4dc3b10b-51e5-4d8e-aff6-806753f897c7└── Camera├── app│ ├── app.iml│ ├── build.gradle│ …

字符串拼接成insert语句[简单记录]

1. 001,自吸过滤式防颗粒物呼吸器,,False,1,0,,False,,001001,随弃式面罩,,False,2,,,False,,001002,可更换式半面罩,,False,2,,,False,,001003,可更换式全面罩,,False,2,,,False,,001001001,有呼气阀,,False,3,,,,, 生成下面这种格式 insert into base_productclass2 values(0…

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

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

判断iframe是否加载完成的方法[转]

原文&#xff1a;http://www.planabc.net/2009/09/22/iframe_onload/ javascript code&#xff1a; 1 var iframe document.createElement("iframe");2 iframe.src "http://www.planabc.net";3 4 if (iframe.attachEvent){5 iframe.attachEvent(&quo…