
堆栈框架的含义和功能
堆栈由堆栈框架组成,每个堆栈框架都对应一个(未完成的)函数. 接下来,我们将通过解释堆栈框架的布局,形成和消失来了解堆栈框架如何在函数调用中工作.
堆栈框架布局
图10.7显示了一个简单的测试程序,可以帮助我们了解堆栈框架.
嵌入式/代码/应用程序/ stackframe / main.c
00001: #include
00002:
00003: //棉绒-e530 -e123
00004:
00005: 空尾巴(int _param)
00006: {
00007: int local = 0;
00008: 整数reg_esp,reg_ebp;
00009:
00010: asm volatile(
00011: //获取EBP
00012: “移动%% ebp,%0 \ n”
00013: //获取ESP
00014: “ %% esp,%1 \ n”
00015 : : “ = r”(reg_ebp),“ = r”(reg_esp)
00016: );
00017: printf(“ tail(): EBP =%x \ n”,reg_ebp);
00018: printf(“ tail(): ESP =%x \ n”,reg_esp);
00019: printf(“ tail(): ( EBP)=%x \ n”,*(int *)reg_ebp);
00020: printf(“ tail(): 返回地址=%x \ n”,*((((int *)reg_ebp + 1)));
00021: printf(“ tail(): &local =%p \ n”,&local);
00022: printf(“ tail(): &reg_esp =%p \ n”,&reg_esp);
00023: printf(“ tail(): &reg_ebp =%p \ n”,&reg_ebp);
00024: printf(“ tail(): &_param =%p \ n”,&_param);
00025: }
00026:
00027: int中间(int _p0,int _p1,int _p2)
00028: {
00029: int reg_esp,reg_ebp;
00030:
00031: asm volatile(

00032: //获取EBP
00033: “移动%% ebp,%0 \ n”
00034: //获取ESP
00035: “ %% esp,%1 \ n”
00036 : : “ = r”(reg_ebp),“ = r”(reg_esp)
00037: );
00038: 尾巴(_p0);
00039: printf(“中间(): EBP =%x \ n”,reg_ebp);
00040: printf(“ middle(): ESP =%x \ n”,reg_esp);
00041: printf(“中间(): ( EBP)=%x \ n”,*(int *)reg_ebp);
00042: printf(“中间(): 返回地址=%x \ n”,*((((int *)reg_ebp + 1)));
00043: printf(“中间(): &reg_esp =%p \ n”,&reg_esp);
00044: printf(“中间(): &reg_ebp =%p \ n”,&reg_ebp);
00045: printf(“中间(): &_p0 =%p \ n”,&_p0);
00046: printf(“ middle(): &_p1 =%p \ n”,&_p1);
00047: printf(“中间(): &_p2 =%p \ n”,&_p2);
00048: 返回1;
00049: }
00050:
00051: int main()
00052: {
00053: 整数reg_esp,reg_ebp;
00054: 整数局部=中间(1、2、3);
00055:
00056: asm volatile(
00057: //获取EBP
00058: “移动%% ebp,%0 \ n”
00059: //获取ESP
00060: “ %% esp,%1 \ n”
00061 : : “ = r”(reg_ebp),“ = r”(reg_esp)
00062: );
00063: printf(“ main(): EBP =%x \ n”,reg_ebp);
00064: printf(“ main(): ESP =%x \ n”,reg_esp);
00065: printf(“ main(): (EBP)=%x \ n”,*(int *)reg_ebp);
00066: printf(“ main(): 返回地址=%x \ n”,*((((int *)reg_ebp + 1)));
00067: printf(“ main(): &reg_esp =%p \ n“,&reg_esp);

00068: printf(“ main(): &reg_ebp =%p \ n”,&reg_ebp);
00069: printf(“ main(): &local =%p \ n”,&local);
00070: 返回0;
00071: }
图10.7
此小程序的每个功能中都嵌入了汇编代码,以便在每个功能运行时获得ESP和EBP寄存器的值. 另外,每个函数都会打印出EBP寄存器指向的内存地址的值,以及位于其后的函数的返回地址. 稍后将详细描述其原因. 图10.8显示了该程序的编译和运行结果.
yunli.blog.51CTO.com /嵌入式/构建
$ make
yunli.blog.51CTO.com /嵌入式/构建
$ ./release/stackframe.exe
tail(): EBP = 22cd08
tail(): ESP = 22ccf0
tail(): (EBP)= 22cd28
tail(): 返回地址= 40120b
tail(): &local = 0x22cd04
tail(): &reg_esp = 0x22cd00
tail(): &reg_ebp = 0x22ccfc
tail(): &_param = 0x22cd10
中间(): EBP = 22cd28
中间(): ESP = 22cd10
中间(): ( EBP)= 22cd58
中间(): 返回地址= 401302
中间(): &reg_esp = 0x22cd24
中间(): &reg_ebp = 0x22cd20
中间(): &_p0 = 0x22cd30
中间(): &_p1 = 0x22cd34
中间(): &_p2 = 0x22cd38
main(): EBP = 22cd58
main(): ESP = 22cd30
main(): (EBP)= 22cd98
main(): 返回地址= 61006e73
main(): &reg_esp = 0x22cd50
main(): &reg_ebp = 0x22cd4c
main(): &local = 0x22cd48
图10.8
为了更好地理解输出结果中数据之间的关系,我们将其转换为图形,如图10.9所示. 该图的左侧还示出了堆栈的生长方向和堆栈的存储器地址. 黑色箭头和寄存器名称指示当前的堆栈帧,否则为灰色. 该图显示了tail()函数中的堆栈布局,充分说明了tail()和middle()函数的堆栈框架结构,以及main()函数的一部分.


在正常情况下,每个函数都有其自己的堆栈框架. 每个堆栈帧中都有一个字段,用于存储上一个调用函数的堆栈帧的基地址. 通过此字段,所有调用和被调用函数的堆栈框架都以链接列表的形式链接在一起. 堆栈框架的这种组织结构说明了为什么函数调用次数越多,占用的堆栈空间就越大. 这也解释了为什么在嵌入式软件开发中需要谨慎使用递归函数.
堆栈框架的形成
为了便于说明,我们必须获取与图10.7中所示的示例程序相对应的汇编代码片段,如图10.10所示. 在该图中,删除了tail()函数汇编代码的中间部分,仅使用head和tail来创建和删除堆栈框架的内容. 在汇编代码中,指令的最左边地址在内存中列出. 接下来,当解释堆栈帧中的返回地址信息时,它将引用该地址.
yunli.blog.51CTO.com /嵌入式/构建
$ objdump -d ./release/stackframe.exe> stackframe.txt
yunli.blog.51CTO.com /嵌入式/构建
$ vi stackframe.txt
00401130 <_tail>:
401130: 55%的ebp推送
401131: 89 e5 mov%esp,%ebp
401133: 83 EC 18子$ 0x18,%esp
401136: c7 45 fc 00 00 00 00 movl $ 0x0栈帧,-0x4(%ebp)
40113d: 89 ea mov%ebp,%edx
40113f: 89 e0 mov%esp,%eax
401141: 89 55 f4 mov%edx,-0xc(%ebp)
401144: 89 54 24 04 mov%edx,0x4(%esp)
401148: 89 45 f8 mov%eax栈帧,-0x8(%ebp)
40114b: c7 04 24 a0 20 40 00 mov $ 0x4020a0,(%esp)
......结果被删除...
4011e1: c9离开
4011e2: c3 ret
004011f0 <_middle>:
4011f0: 55%的ebp推送
4011f1: 89 e5 mov%esp,%ebp
4011f3: 83 EC 18子$ 0x18,%esp
4011f6: 89 e8 mov%ebp,%eax
4011f8: 89 e2 mov%esp,%edx
4011fa: 89 45 f8 mov%eax,-0x8(%ebp)
4011fd: 8b 45 08 mov 0x8(%ebp),%eax
401200: 89 55 fc mov%edx,-0x4(%ebp)
401203: 89 04 24 mov%eax,(%esp)
401206: e8 25 ff ff ff致电401130 <_tail>
40120b: 8b 45 f8 mov -0x8(%ebp),%eax
40120e: c7 04 24 44 21 40 00 movl $ 0x402144,(%esp)
401215: 89 44 24 04 mov%eax,0x4(%esp)

401219: e8 da 01 00 00致电4013f8 <_printf>
......结果被删除...
图10.10
现在假设程序在main()刚刚调用middle()函数时运行,让我们看一下堆栈布局是如何变化的. 程序进入Middle()函数后立即运行的第一条指令位于内存地址4011f0. 运行该指令之前的堆栈结构如图10.11所示. 此时,EBP仍指向main()函数堆栈帧的开头,ESP指向的存储器中存储的是程序返回main()函数的指令位置,然后分析中间位置()函数改为tail()函数. 呼叫时也会涉及到.

在存储器地址4011f0〜4011f3中的指令功能是形成中间()功能的堆栈框架. 第一条指令是将调用函数(即main()函数,middle()是被调用函数)的堆栈帧基地址保存在堆栈上. 该指令是推入操作. 每个函数中的此操作使所有堆栈帧链接在一起.
第二条指令将ESP寄存器的值分配给EBP寄存器,也就是说,此时的ESP寄存器存储middle()函数的堆栈帧基地址. 请注意,基址不包括用于存储寄信人地址的空间.
第三条指令在ESP上执行减法运算,即将ESP移至低位地址24个字节,并将目标移至24个字节. 这是在堆栈上腾出空间来存储局部变量和传入要调用的函数的参数. 显然,函数中的局部变量越大,减小的值就越大.
运行完上述三个指令后,便形成了中间()函数的堆栈框架,如图10.12所示. 该图还说明了中间()函数在堆栈帧中局部变量reg_esp和reg_ebp的位置.
内存地址4011f6和4011f8上的指令是我们嵌入在middle()函数中的汇编代码,该代码用于此时获取EBP和ESP寄存器的值. 4011fa处的指令将EBP寄存器的值放置在局部变量reg_ebp中,401200fa处的指令将ESP寄存器的值放置在局部变量reg_esp中. 4011fd和401203处的指令将从main()函数传递的第一个变量_p0的值复制到ESP寄存器指向的内存中,并准备用于调用tail()函数的参数. 此时的堆栈空间如图10.13所示.

位于内存地址401206的指令是调用tail()函数的指令. 该调用将导致返回地址被压入堆栈. 调用此指令后的堆栈空间如图10.14所示.
压入堆栈的返回地址为40120b. 从图10.10中可以看到,该地址指向middle()函数中的最后一条指令,该指令调用tail()函数. 返回时,程序将从该地址继续运行. 该指令的调用还意味着输入了tail()函数的堆栈框架,并且tail()函数使用与middle()函数相同的“操作”来构建自己的堆栈框架. 上图10.9所示的内存布局恰好是tail()函数创建堆栈帧的时候.

堆栈框架的消亡
让我们看一下当函数在tail()函数中返回时,堆栈空间如何变化. 存储器地址4011e1处的离开指令具有以下功能: 将ESP寄存器的值设置到EBP寄存器并执行堆栈拆栈操作,并将堆栈拆栈操作的内容放入EBP寄存器. 该指令的功能等同于“ mov%ebp,%esp; pop%ebp”,它删除由tail()函数创建的堆栈帧. 执行该指令后的堆栈布局与图10.14完全相同. tail()函数的末尾是一个返回指令,该指令用于将堆栈的内容(即ESP寄存器指示的位置)弹出到PC寄存器中. 地址为40120b. 执行该指令后的堆栈结构与图10.13相同.
在这一点上,我们完全了解堆栈框架的形成和消亡. 实际上,对于每个C函数,编译器都会生成汇编代码,以在进入函数时创建其堆栈框架,并在从函数返回时删除其堆栈框架. 在x86 ABI规范中,这两部分分别称为“前言”和“后序”,其近似代码分别如图10.15和图10.16所示.
序言:
pushl%ebp //保存上一个函数的堆栈帧指针
movel%esp,%ebp //设置此函数的堆栈帧指针
subl $ 80,%ebp //分配函数的堆栈帧空间
pushl%edi //保存本地变量寄存器
pushl%esi //保存本地变量寄存器
pushl%ebx //保存本地变量寄存器
图10.15
结尾:
popl%ebx //恢复本地变量寄存器
popl%esi //恢复本地变量寄存器
popl%edi //恢复本地变量寄存器
leave //恢复调用此函数的堆栈帧指针
ret //返回调用该函数的函数
图10.16
在每个函数的“序言”部分,都有一条指令为堆栈帧分配大小(例如,图10.15中的“ subl $ 80,%ebp”). 调用函数的最大参数数量确定堆栈帧的大小.
此外,这两个图中还有EDI,ESI和EBX的推入和卸出操作. 如第10.3节所述,EDI,ESI和EBX用作局部变量寄存器. 换句话说,如果这三个寄存器用在一个函数(称为函数A)中,并且用在其调用的函数(称为函数B)中,则函数B必须在使用它们之前保存它们,以便可以在返回之前将其还原函数A. 但是,如果这两个函数不使用这些寄存器,则“智能”编译器将做出决定,而无需将其推入“序言”中,以提高程序的效率.
由于该函数返回其堆栈框架,因此它不再存在. 因此,我们不能将局部变量的指针用作函数的返回值.
如果读者现在回头看图10.6中的表格,我相信他们可以更好地理解其含义.
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/jisuanjixue/article-164564-1.html
廣告詞一套套的