b2科目四模拟试题多少题驾考考爆了怎么补救
b2科目四模拟试题多少题 驾考考爆了怎么补救

堆栈框架的含义和功能

电脑杂谈  发布时间:2020-04-05 09:12:39  来源:网络整理

扩展帧和标准帧_jvm栈帧_栈帧

堆栈框架的含义和功能

堆栈由堆栈框架组成,每个堆栈框架都对应一个(未完成的)函数. 接下来,我们将通过解释堆栈框架的布局,形成和消失来了解堆栈框架如何在函数调用中工作.

堆栈框架布局

图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(

栈帧_jvm栈帧_扩展帧和标准帧

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

jvm栈帧_栈帧_扩展帧和标准帧

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()函数的一部分.

jvm栈帧_栈帧_扩展帧和标准帧

嵌入3.jpg

在正常情况下,每个函数都有其自己的堆栈框架. 每个堆栈帧中都有一个字段,用于存储上一个调用函数的堆栈帧的基地址. 通过此字段,所有调用和被调用函数的堆栈框架都以链接列表的形式链接在一起. 堆栈框架的这种组织结构说明了为什么函数调用次数越多,占用的堆栈空间就越大. 这也解释了为什么在嵌入式软件开发中需要谨慎使用递归函数.

堆栈框架的形成

为了便于说明,我们必须获取与图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)

栈帧_jvm栈帧_扩展帧和标准帧

401219: e8 da 01 00 00致电4013f8 <_printf>

......结果被删除...

图10.10

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

嵌入4.jpg

在存储器地址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所示.

嵌入5.jpg

位于内存地址401206的指令是调用tail()函数的指令. 该调用将导致返回地址被压入堆栈. 调用此指令后的堆栈空间如图10.14所示.

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

嵌入6.jpg

堆栈框架的消亡

让我们看一下当函数在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

    相关阅读
      发表评论  请自觉遵守互联网相关的政策法规,严禁发布、暴力、反动的言论

      热点图片
      拼命载入中...