当前位置:首页 » 编程软件 » 函数栈帧的长度在编译时被确定

函数栈帧的长度在编译时被确定

发布时间: 2022-03-30 02:14:22

❶ C/C++的实现问题

咱们一个问题问题来看:
1.主函数调用FUNCA时将EBP入栈问题:
链接器在最终链接所有目标文件生成可执行文件的时候,会寻找你的程序入口点main,main实际上会被运行时库中的某个函数所调用(main并非是程序真正的入口点,真正的入口点在运行时库中的某个函数,具体跟平台有关),所以在main函数中,首先保存原来的ebp值(push ebp),然后将原来的栈顶设为栈底(mov ebp, esp),到这里已经开辟一个新的栈帧。
为什么没有把esp入栈呢,因为esp已经保存到ebp中了,而ebp已经入栈了,如果要恢复以前的栈帧的话,只需要把ebp的值放入esp,从栈中弹出保存的ebp值到ebp寄存器即可,所以根本没有保存esp的必要。你可以按这个解释画个草图,一画便知。x86下,栈是向小地址方向增长的,即压栈会导致esp的值减小。
然后就是为局部变量分配存储空间(sub esp,0x10),这里为何用减呢?原因上面已经说过,栈是向小地址方向增长的,所以应该用减。跟小尾序表示无关,小尾序仅仅代表数字在内存中的存储布局。

2.其余的问题
leave并非是一个伪指令,而是一个真正的x86指令。它相当于mov esp,ebp; pop ebp这两条指令。前面已经说过,ebp中放的是上一个栈帧中esp的值,所以mov esp,ebp将恢复esp的值。因为esp这时已经和ebp一样,故pop ebp弹出的恰好是已经保存的ebp的值。这样,leave指令执行完毕后,main函数的栈帧就被销毁,所以函数内部的局部变量也就被销毁了。
如果局部变量加上了static限定符,该局部变量实际上并不是在当前函数的栈帧中,而是在初始化数据段里,该变量的地址是固定的,所以编译器无须为该静态变量分配栈空间,所有对该变量的引用都是通过一个固定地址来进行的。栈空间就是通过基址寄存器ebp和栈顶指针寄存器esp来限定的,故esp上移实际上导致了释放出一部分空闲空间,因为在ebp到esp这段地址范围内已经无法找到以前的临时变量,故我们说变量已经被销毁。

兄弟,看在哥哥写这么长的份儿上,给分儿吧!

❷ 传智的老师讲c语言函数的栈帧调用过程讲错了吧大家进来看看

你的老师是没说错的。栈的规则是先进后出,后进先出。进入栈不代表会运行,它只是进入了1个运行的队列。按照你的方法那这个栈基本没意义了,因为你是进栈就出栈了,直接就运行了。并没有一个队列顺序。
1、printf(n2)它最先进栈,所以它的排在队列的最后面,它最后运行。这符合程序的运行顺序
2、printf(n1)它第二进栈,所以它是倒数第二个运行的。以此类推下去。
3、如果按照你的说法,main最先进栈,你会发现它是在栈的底部,它无法最先运行。因为它上面还有你说的printf1,2,3,4,5等等。先出栈也是它们,意味着先运行也是它们。这和事实不符的。因为运行一个程序是从主函数开始运行的。
4、根据栈的规定,这里main应该是最后进栈的才对,因为它最后进栈,所以他最先出栈,也是它最先运行。

❸ 在进行函数调用时,函数的栈帧有什么作用

栈的规则是先进后出,后进先出。进入栈不代表会运行,它只是进入了1个运行的队列。按照你的方法那这个栈基本没意义了,因为你是进栈就出栈了,直接就运行了。并没有一个队列顺序。 1、printf(n2)它最先进栈,所以它的排在

❹ masm32问题,关于函数堆栈平衡

谁告诉你
在masm32中一个函数即使堆栈不平衡,在函数的末尾也有mov esp,ebp pop ebp进行平衡

的?

函数开始的push ebp是用来保存ebp寄存器(函数调用者栈帧的栈底)的,同时对与微软系列的编译器,局部变量采用ebp-X来访问
mov edb,esp是函数的调用者的栈帧的栈顶作为一个新的栈帧的开始

两行代码之后
堆栈内容应该是这样的



调用者的栈帧底地址

该函数返回地址



现在我再来一句 push eax

此时堆栈变成这样



eax的内容

调用者的栈帧底地址

该函数返回地址



此时堆栈已经不平衡
然后我用你的mov esp,ebp pop ebp准备返回

请问能正确返回吗?

当执行到ret的时候堆栈的情况是这样的(甚至ebp的值都不能恢复)



调用者的栈帧底地址

该函数返回地址



这样直接返回到了函数开始的时候ebp所以位置了
你说这两个代码能保证函数的返回吗?

函数要正确返回,堆栈必须平衡(排除你自己设置堆栈让ret返回的情况)!

❺ C语言中,函数调用是通过栈实现的,怎样理解这句话

在一个函数中调用另一个函数,会将当前状态入栈,等执行完下一个后出栈,恢复状态继续往下执行
例:
int
myfun()//假如指向到此函数
{
...
myfun1();
//调用myfun1函数,那我之前的变量怎么办?入栈保存,然后跳转到函数myfun1
....
}
int
myfun1()
{
myfun2();
//又要调用新的怎么办,入栈保存现有变量,然后进入myfun2
}
int
myfun2()
{
return
0;
//运行完myfun2,终于返回了。然后回到myfun1,保存的变量出栈,继续执行myfun1
}

❻ 栈帧是什么

栈帧也叫过程活动记录,是编译器用来实现过程或函数调用的一种数据结构。
C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。
栈帧,顾名思义,就是栈中的一帧,栈分成很多帧,就如同一个视频动作分成好多帧一样。每个栈帧,对应一个函数,就是这个函数在栈中占用的部分。

❼ C++申请释放动态内存的问题

还是系统会记住之前早请这块内存的信息,释放时只要是这个指针,不管是什么类型都正确释放?
就是这样。在windows下,new与malloc分配的内存都是在c++运行时库函数用HeapCreate创建的堆上申请的。堆结构本身记录了各块的内存大小与类型
在执行delete或free时,内部调用HeapFree,你传入的指针free(p);其中的p在free内部会经过(void *)的转换,所以说free类似函数只关心p的数值而不关心p的类型。HeapFree可以根据p的值查询堆管理器维护的数据结构来得到那块内存的大小并释放。

delete操作符也是类似。不过数组要用delete []p;,这不是说HeapFree需要知道p是个数组,这只是c++的特性,是高级语言规则。

---------------------------------------------------
还有int pa=new int[100]这样形式的,delete (char*)pa会泄漏吗?
不会泄漏,只是这不合章法。delete是操作符,你要按c++标准来运用它。虽然这在windows上可以,可是说不定在其他操作系统上不行。
正确的做法就是delete []pa;

--------------------------------------------------
ivaniren的实验很有启发性。
为了理解new及delete,观察它们的代码以及栈帧
以下是new的源码
void * operator new( unsigned int cb )
{
void *res = _nh_malloc( cb, 1 );

return res;
}
可见new是个操作符,_nh_malloc是一个内部函数没有源码,观察栈上它的调用可知:_nh_malloc调用了_heap_alloc,而_heap_alloc又调用了RtlHeapAlloc,可见new操作符的本质还是HeapAlloc。至于new操作符会调用构造函数的问题,观察ivaniren给出的代码的汇编:
43: B * pb = new B;
004011D3 6A 04 push 4
004011D5 E8 C6 85 00 00 call operator new (004097a0)
004011DA 83 C4 04 add esp,4
004011DD 89 45 E0 mov dword ptr [ebp-20h],eax
004011E0 C7 45 FC 00 00 00 00 mov dword ptr [ebp-4],0
004011E7 83 7D E0 00 cmp dword ptr [ebp-20h],0
004011EB 74 0D je main+5Ah (004011fa)
004011ED 8B 4D E0 mov ecx,dword ptr [ebp-20h]
004011F0 E8 6F FE FF FF call @ILT+95(B::B) (00401064)

这段代码先调用了operator new (004097a0),后调用了@ILT+95(B::B) (00401064),也就是构造函数。那么程序自己怎么知道是要调用B的构造函数呢?这是在编译过程中编译器根据new B来确定的,这就是说,对源码中出现的new操作符,编译器先计算出需要分配的大小,分配空间,再调用构造函数

对于delete,它的源码是:
void operator delete( void * p )
{
free( p );
}
之所以它会调用析构函数,是因为和上述分析的new一个原因

关于ivaniren的问题
pba = new B[3];
delete pba; //~ 只调用了一个7构函数,是否释放了内存,还有待高人解答
那么我告诉你,内存释放了,原因就是operator delete的源码,但是析构函数却没有正确调用

---------------------------------------------------
在补充一点:
以下是类中delete操作符的一段汇编:
004016F0 E8 A6 F9 FF FF call @ILT+150(B::~B) (0040109b)
004016F5 8B 45 08 mov eax,dword ptr [ebp+8]
004016F8 83 E0 01 and eax,1
004016FB 85 C0 test eax,eax
004016FD 74 0C je B::`scalar deleting destructor'+3Bh (0040170b)
004016FF 8B 4D FC mov ecx,dword ptr [ebp-4]
00401702 51 push ecx
00401703 E8 88 14 00 00 call operator delete (00402b90)

可见,delete时与new相反,是先call @ILT+150(B::~B) (0040109b)调用析构函数
再call operator delete (00402b90)释放内存。
所以对delete pba;这样的代码首先调用pba[0]的析构函数,然后再释放3个B的内存空间
虽然是这么说,但是释放3个B的内存空间时却在_CRTIsValidHeapPoint上中止了,为什么呢?
因为调式版本对delete有检查,若delete的调用和new的调用不相符合,则会发生错误,在发行版本中则不会出错

---------------------------------------------------
楼主,你领会上文要领了吗?delete一般不会泄漏。但我刚才又想到了一种情况:某C++类在构造函数中调用malloc,在析构函数中调用free,那么如果delete与new不匹配,虽然c++类的空间被释放了,可是构造函数调用malloc申请的内存却没机会释放。如果这样的c++类被创造出一个大数组的数量又被错误的delete掉,那么内存的损失将是巨大的。
所以最好的办法还是遵守规则

--------------------------------
什么我不见了!我因为修改答复所以到后面来了

❽ 关于c语言程序的,概念问题,这句话对么

一般的局部变量是保存在堆栈中函数结束后堆栈会被清空,局部变量也就不复存在了。
静态的局部变量是保存在全局静态区域的,即使函数结束他仍然存在。再次调用时的值是上一次调用后的值。只要主函数没有结束他就一直存在。
我刚学的时候在这点上走了不少弯路给你推荐篇文章吧。看了你会明白很多的。
找了一圈没找到。你到网上搜林锐的C/C++高质量编程,在内存管理那一章有详细的介绍。
希望可以帮到你。

❾ 函数调用时,实参存储在哪个函数的栈帧中

程序中,一个函数是一个过程,这个过程可以分为包括传入参数、过程代码、返回三部分构成。由于一个函数过程需要用到内部变量、临时变量等,所以需要在进程空间的栈空间分配一段存储片段来存储函数过程中的这些参数,该内存片段即为栈帧。
栈帧的由来:
为一个函数的过程提供一个存储函数局部变量,参数,返回地址和其他临时变量;
栈帧的结构:

图片来源(现代编译原理)

栈帧的周期:
进入函数~函数返回,该阶段内栈帧作为
不同的语言具体的实现方式略有不同,但是,总体上,fun(a,b);

局部变量:
包括函数传入的形参和函数内部定义的变量;

返回地址:
指令指针P指向call fun,那么fun栈帧存储的返回地址为P+1;现今的编译器的一个约定是将返回地址存到一个固定的寄存器中,这样比读取栈帧(内存)效率要高。

临时变量:
主要为计算,运算过程中的中间临时变量;

参数传递:
其一:如果fun中调用另一个函数k(a,b...n);那么,传递的参数是形参,按照现代编译规定,前k个形参是通过寄存器传递,后n-k个形参通过栈帧的实参部分(栈帧的尾部)来传递;
其二:主要为在fun中要调用函数g(&a,&b),那么为g()函数传出实参(形参是通过寄存器来传递的)是通过“传出实参”区块进行的,这么做主要是为了保证该实参能够被内层嵌套的函数访问。比如,g函数由调用一个k(a地址)函数,同样需要用到a的地址,所以fun在传递参数时必须为实参(&a)传递申请固定的内存存储空间(而非用寄存器)这样k函数可以通过固定的内存地址(fun的形参列表来获取参数值)。这时的g的栈帧为fun栈帧的下一帧(相邻的空间地址),即调用者和被调用者的栈帧是相连的;

保护的寄存器:
栈帧作为函数过程的一个临时内存存储区块,同时负责函数调用过程中寄存器值的保存和还原。即:假设fun函数目前占用了寄存器Ri存储一个临时变量t,而此时调用了函数g(),在g()函数中可能需要用到寄存器Ri做运算的临时存储,那么如何确保g()函数调用返回后,Ri恢复到fun中t的原来值。这就需要在调用者或者被调用者中选择其一来保存原有Ri的值,即caller-save或者callee-save。

最后一个问题:进程空间中栈帧的体现是什么?
进程的栈空间 & 栈帧的增长:
调用函数和被调用函数间的栈帧是相邻的,即如果进程进入一个递归函数f(),递归k层。那么在第k层嵌套时,进程的的栈空间已产生出新的k个连续的f()函数的栈帧,当然每个栈帧的内存储的变量值是因函数过程而定的。

❿ "栈"和"栈帧"这两个概念到底如何区分

1、栈:FILO先进后出的数据结构

栈底是第一个进栈的数据的位置(压箱底)

栈顶是最后一个进栈的数据位置

2、根据SP指针指向的位置,栈可分为满栈和空栈

满栈:当sp指针总是指向最后压入堆栈的数据(ARM采用满栈)


栈的作用:

1)保存局部变量

分析代码:

[html]view plain

  • #include<stdio.h>

  • intmain()

  • {

  • inta;

  • a++;

  • returna;

  • }</span>


  • 反汇编之后的代码;

    [html]view plain

  • stack:fileformatelf32-littlearm

  • Disassemblyofsection.text:

  • 00000000<main>:

  • #include<stdio.h>

  • intmain()

  • {

  • 0:e52db004push{fp};(strfp,[sp,#-4]!)@将栈帧底部指针FP压入栈中;创建属于main函数的栈帧。

  • 4:e28db000addfp,sp,#0;0x0@fp指针为函数栈帧的底部,

  • 8:e24dd00csubsp,sp,#12;0xc@sp指针为栈帧的顶部,同时为栈的栈顶。

  • inta;

  • a++;

  • c:e51b3008ldrr3,[fp,#-8]@由此三句可知变量a在栈帧中执行了加法操作,及栈帧具有保存局部变量的作用

  • 10:e2833001addr3,r3,#1;0x1

  • 14:e50b3008strr3,[fp,#-8]

  • returna;

  • 18:e51b3008ldrr3,[fp,#-8]

  • }

  • </span>



  • 2)保存函数的参数

    分析代码:

    [html]view plain

  • <spanstyle="font-size:18px;">#include<stdio.h>

  • voidfunc1(inta,intb,intc,intd,inte,intf)

  • {

  • intk;

  • k=e+f;

  • }

  • intmain()

  • {

  • func1(1,2,3,4,5,6);

  • return0;

  • }

  • 反汇编之后的代码;

  • voidfunc1(inta,intb,intc,intd,inte,intf)@多于4个参数

  • {

  • 0:e52db004push{fp};(strfp,[sp,#-4]!)@保存main函数的栈帧底部指针FP

  • 4:e28db000addfp,sp,#0;0x0

  • 8:e24dd01csubsp,sp,#28;0x1c@由栈帧顶部指针SP创建一片栈帧保存子函数的前四个参数

  • c:e50b0010strr0,[fp,#-16]@a

  • 10:e50b1014strr1,[fp,#-20]@b

  • 14:e50b2018strr2,[fp,#-24]@c

  • 18:e50b301cstrr3,[fp,#-28]@d

  • intk;

  • k=e+f;

  • 1c:e59b3004ldrr3,[fp,#4]@在子函数的栈帧中实现第五个参数与第六个参数的运算

  • 20:e59b2008ldrr2,[fp,#8]@由ldrr2,[fp,#8]知参数保存在main函数的栈帧中,并运算

  • 24:e0833002addr3,r3,r2@以子函数的栈帧底部指针(fp)做参考坐标实现对参数的查找

  • 28:e50b3008strr3,[fp,#-8]

  • }

  • 2c:e28bd000addsp,fp,#0;0x0

  • 30:e8bd0800pop{fp}

  • 34:e12fff1ebxlr

  • 00000038<main>:

  • intmain()

  • {

  • 38:e92d4800push{fp,lr}@由于调用子函数,先保存main函数的栈帧底部指针FP和返回地址LR(当前PC指针的下一地址)

  • 3c:e28db004addfp,sp,#4;0x4@可知先压入FP,后压入lr.把此时子函数(被调用者)的栈帧底部指针FP指向保存在子函数栈帧的main函数(调用者)的栈帧底部指针FP

  • 40:e24dd008subsp,sp,#8;0x8@创建栈

  • func1(1,2,3,4,5,6);

  • 44:e3a03005movr3,#5;0x5

  • 48:e58d3000strr3,[sp]

  • 4c:e3a03006movr3,#6;0x6

  • 50:e58d3004strr3,[sp,#4]

  • 54:e3a00001movr0,#1;0x1@用通用寄存器保存前四个参数的值

  • 58:e3a01002movr1,#2;0x2

  • 5c:e3a02003movr2,#3;0x3

  • 60:e3a03004movr3,#4;0x4

  • 64:ebfffffebl0<func1>

  • return0;

  • 68:e3a03000movr3,#0;0x0

  • }

  • 6c:e1a00003movr0,r3

  • 70:e24bd004subsp,fp,#4;0x4

  • 74:e8bd4800pop{fp,lr}

  • 78:e12fff1ebxlr</span>


  • 注:C中,若函数的参数小于等于4个,则用通用寄存器保存其参数值,多于4个的参数保存在栈中

    3)保存寄存器的值

    分析代码:

    [html]view plain

  • <spanstyle="font-size:18px;">include<stdio.h>

  • voidfunc2(inta,intb)

  • {

  • intk;

  • k=a+b;

  • }

  • voidfunc1(inta,intb)

  • {

  • intc;

  • func2(3,4);

  • c=a+b;

  • }

  • intmain()

  • {

  • func1(1,2);

  • return0;

  • }</span>


  • 反汇编之后的代码;

    [html]view plain

  • <spanstyle="font-size:18px;">voidfunc2(inta,intb)

  • {

  • 0:e52db004push{fp};(strfp,[sp,#-4]!)

  • 4:e28db000addfp,sp,#0;0x0

  • 8:e24dd014subsp,sp,#20;0x14

  • c:e50b0010strr0,[fp,#-16]@保存寄存器的值

  • 10:e50b1014strr1,[fp,#-20]

  • intk;

  • k=a+b;

  • 14:e51b3010ldrr3,[fp,#-16]

  • 18:e51b2014ldrr2,[fp,#-20]

  • 1c:e0833002addr3,r3,r2

  • 20:e50b3008strr3,[fp,#-8]

  • }

  • 24:e28bd000addsp,fp,#0;0x0

  • 28:e8bd0800pop{fp}

  • 2c:e12fff1ebxlr

  • 00000030<func1>:

  • voidfunc1(inta,intb)

  • {

  • 30:e92d4800push{fp,lr}

  • 34:e28db004addfp,sp,#4;0x4

  • 38:e24dd010subsp,sp,#16;0x10

  • 3c:e50b0010strr0,[fp,#-16]@代码44行调用func2函数后,又使用r0 1保存参数,所以此时将r0 1寄存器的

  • 40:e50b1014strr1,[fp,#-20]@值放入栈中

  • intc;

  • func2(3,4);

  • 44:e3a00003movr0,#3;0x3

  • 48:e3a01004movr1,#4;0x4

  • 4c:ebfffffebl0<func2>

  • c=a+b;

  • 50:e51b3010ldrr3,[fp,#-16]

  • 54:e51b2014ldrr2,[fp,#-20]

  • 58:e0833002addr3,r3,r2

  • 5c:e50b3008strr3,[fp,#-8]

  • }

  • 60:e24bd004subsp,fp,#4;0x4

  • 64:e8bd4800pop{fp,lr}

  • 68:e12fff1ebxlr

  • 0000006c<main>:

  • intmain()

  • {

  • 6c:e92d4800push{fp,lr}

  • 70:e28db004addfp,sp,#4;0x4

  • func1(1,2);

  • 74:e3a00001movr0,#1;0x1

  • 78:e3a01002movr1,#2;0x2

  • 7c:ebfffffebl30<func1>

  • return0;

  • 80:e3a03000movr3,#0;0x0

  • }

  • 84:e1a00003movr0,r3

  • 88:e24bd004subsp,fp,#4;0x4

  • 8c:e8bd4800pop{fp,lr}

  • 90:e12fff1ebxlr</span>


  • 初始化栈:即对SP指针赋予一个内存地址(统一标准:2440、6410、210)

    在内存的64MB位置即ldr sp, =0x34000000(2440)

    ldr sp, =0x54000000(6410)

    ldr sp, =0x24000000(210)

    由上可知ARM采用满栈(指向刚入栈的数据)、降栈(由高地址向低地址入栈)

    问题:因为ARM不同工作模式有不同的栈,定义栈的技巧是什么,避免定义相同的地址使用不同栈?

    转自:http://blog.csdn.net/u011467781/article/details/39559737

热点内容
安卓微信下载的压缩文件在哪里 发布:2025-01-23 12:44:56 浏览:17
广州电信上传速度 发布:2025-01-23 12:43:22 浏览:896
怎么清除最常访问 发布:2025-01-23 12:42:29 浏览:527
女人资产如何配置 发布:2025-01-23 12:39:22 浏览:27
sql判断字符 发布:2025-01-23 12:37:44 浏览:531
sql存储过程返回值 发布:2025-01-23 12:32:31 浏览:274
陌陌怎么改密码 发布:2025-01-23 12:24:41 浏览:751
linux文件大小查看 发布:2025-01-23 12:19:35 浏览:974
三星s4文件加密 发布:2025-01-23 12:18:55 浏览:373
备份密码解锁在哪里点 发布:2025-01-23 12:14:27 浏览:857