函数调用栈帧包含哪些内容

烤肉调料2022-07-07  27

0预备知识0.1程序加载和数据存储

在运行程序之前,应该将代码加载到内存的代码区,包括全局变量和静态变量。

当程序运行时,可以动态地请求堆内存。

栈区是被程序重用的存储区,栈区空之间的重用由两个寄存器ebp和esp存储栈区的相对地址控制。当一个函数被调用时,该函数所需的一个堆栈区空被打开,这个堆栈区被称为堆栈框架。函数返回时,会偏移ebp和esp的值,这样stack 空 room就可以重用上一次函数调用占用的空 room。

以下是程序在windows平台上运行时的内存分配机制:

0.2函数调用

函数调用看似简单,其实是一个相当复杂的过程。当涉及到分步调用时,代码需要能够回到最初的调用点。

比如一个人出行,经过几个路口如何正确回到原来的出发位置?一个可行的方案是,每经过一个路口,用扑克牌记下路口的状态。每经过一个路口,都要用一张牌,放在用过的扑克牌上。当你返回时,你可以依次读取卡片上的信息,然后回到原来的位置。这种扑克牌的放取,就是所谓的栈的“后进先出”机制。

函数调用也应该使用类似的机制,并由编译器完成。每一级函数调用对应一个堆栈框架(像扑克牌一样),记录参数值、返回地址、一些寄存器状态和局部变量值。调用函数和被调用函数如何分工来完成这些任务?不同的通话约定有不同的分工。

0.3数组名转换成某个上下文中数组第一个元素的指针。

数组也是如此,看似简单,实则复杂。

一个数组的名字会被转换成一个指针,指向某个上下文中数组的第一个元素。为什么会这样,当然是有道理的。

首先,理解指针的算术运算。(堆栈存储器空之间的地址反向增加)

void foo(int a){ int f = 45;int * ptr = f;*(ptr+1)= 12;//编译通过,运行时出错,试图修改ebp *(ptr+2)= 34;//编译通过,运行时出错,试图修改函数返回地址*(ptr+3)= 56;//尝试修改存放函数参数值的堆栈内存单元int * p =(int *)malloc(sizeof(int)* 5);*(p+3)= 23;//p[3]= 23;}p+n,是从ptr偏移的n个int addresses 空之间的地址值。

编译器会计算p+sizeof(int),C编译器不关心偏移量是否是有效合法的地址。

ptr+n也是如此。

如果有数组int arr0.5.5一个完整的函数帧包括:函数参数、返回地址、前一个EBP的值、局部变量空、3个寄存器。在函数内部代码执行之前,已经建立了一个完整的函数框架。= { 0 };

数组名是一个基址,它的索引表示地址的偏移量。Arr[i]是指针算术运算的语法糖*(arr+i)。编译器应该考虑指针的算术运算有实际意义。C编译器的规定是arr+i,它的地址不是简单的偏移I字节,没有意义,而是sizeof(指向类型)长度的字节数,有意义。它的计算是由编译器完成的。当指针类型为数组时,偏移I数组的长度没有任何意义,只有偏移I数组元素的长度才有实际意义。这也是C编译器的规定。比如:

int a[3][4][5];//a的类型是int[3][4][5],元素的类型是int[4][5],int(* p)[4][5]= a;a+2;//偏移量地址是a+2 * sizeof(int)* 4 * 5 int b[4][5];//b的类型是int[4][5],元素的类型是int[5],int(* p)[5]= b;b+2;//偏移量地址为b+2 * sizeof(int)* 5 int c[5];//c的类型是int[5],元素的类型是int,int * p = c;c+2;//偏移量地址是b+2* sizeof(int)C) C .编译器只负责偏移量地址的计算。编译时不检查数组是否越界或地址是否合法。

arr[i]的I可以是任意有符号的int,越界时会访问相邻的堆栈内存。

0.4一些简单的汇编代码

1.mov-传输指令

定义:通过将一个字节、字或双字操作数从源位置传送到目的位置,可以立即计数到通用寄存器或主存储器,通用寄存器和通用寄存器之间的传送,主存储器或段寄存器之间的传送,主存储器和段寄存器之间的传送。

示例:mv ebp,esp

说明:相当于C语言中的赋值语句=,ebp=esp

2.推-推指令

定义:push指令首先将ESP缩减到当前栈顶,然后可以将立即数、通用寄存器和段寄存器或内存操作数转移到当前栈顶。

格式:推送src

示例:推送ebp

解释:相当于C语言中的esp+=4,*esp=ebp。

函数:是ebp的当前地址,存放在栈顶的空室,调用子函数时作为就地保护。

3.弹出-弹出指令

定义:与push指令相反,它首先将栈顶的数据转移到通用寄存器、存储单元或段寄存器,然后将ESP添加为当前栈顶。

格式:pop src

示例:pop ebp

解释:相当于C语言中的ebp=*esp,esp+=4

函数:调用子函数后恢复主函数的ebp。

4.加法-加法指令

格式:添加目的地,src

解释:相当于dest+=src

5、减法指令

格式:子目标,src

解释:相当于dest-=src

6.调用函数调用指令

格式:调用函数名

功能:(1)将程序当前执行位置的IP压入堆栈;(2)转移到被调用的子程序。

其他:

RET指令将返回地址从堆栈弹回到指令指针寄存器。Ret相当于流行EIP。

Rep stos dword ptr [edi] //rep是重复上面的指令,ECX是重复的次数。

ILT是增量链接表的缩写。这个@ILT实际上是一个静态函数跳转表。它记录一些函数的入口,然后跳过它们。每个跳转jmp占用一个字节,然后是一个四字节的内存地址,总共是五个字节。

LEA(加载有效地址)取有效地址指令,取源操作数地址的偏移量,传送到目的操作数所在的单元。

0.5函数堆栈

0.5.1 Stack 空增长模式:从高地址到低地址,是程序可以复用的一块数据空;

Stack 空由编译器维护(需要在调试模式下才能跟踪);

0.5.2栈间对齐空: X86按4字节对齐,X64按8字节对齐;

0.5.3在两个跟踪堆栈之间注册ESP和EBP空

ESP跟踪堆栈上的操作,以指示堆栈的顶部。对于推送和调用,Esp -= 4,对于弹出和返回,esp += 4。

EBP用于指代函数参数和局部变量。EBP相当于一个“参考指针”。从主调优函数传递给被调优函数的参数和被调优函数本身的局部变量,都可以用这个基准指针作为参考,加上偏移量找到。

*ebp(表示栈地址ebp对应的值)=前一个ebp的值(栈地址);

Ebp-4 =第一个局部变量的地址;

Ebp+4 =函数返回地址;

Ebp+8 =函数第一个参数的地址;

0.5.4不同的函数调用约定在参数的堆栈顺序、堆栈的回收(是调用者还是被调用者负责)、函数的命名等方面都会有所不同。

[64]

下面是一个完整的例子来理解函数调用的堆栈框架机制以及对数组向堆栈底部越界的分析:

请看下面的代码:

# include ltstdio.h gtint array bound(int a){ int b =-1;//[ebp-4]int arr012fd10 48ff12 00 00 00 00 00 F0 FD 7f h...= { 0 };//[ebp-18Ch]int c = 1;// [ebp-190h],栈反向增长,栈顶地址值>:栈顶arr[-1]= c * a;//数组向栈顶越界访问,arr[-1]对应int c arr[98]= b * a;//数组被越界访问到堆栈底部,arr[98]对应int b//printf( # 34;% d % d \ n # 34,b,c);// -5 5返回b;} int main(){ int e = array bound(5);//-5//printf( # 34;% d \ n # 34,e);返回0;}主函数main()的堆栈框架:

主函数main()调用arraybound(),此时汇编代码:

15:int e = array bound(5);// -50040D4D8推送50040 D4 da call @ ILT+10(array bound)(0040100 f)0040 D4 df添加esp,40040D4E2 mov dword ptr [ebp-4],eax 16://printf( # 34;% d \ n # 34,e);17:返回0;040d4e5xor eax,eax18:} 1调用函数调用被调用函数时函数参数的堆栈指针值:

Ebp 0x0012ff48 // ebp是堆栈指针的底部

Esp 0x0012fef8 // esp是栈底指针,栈的push和pop操作会同时改变esp (esp move)的值。

此时堆栈顶部指针附近的内存映像:

2F 42 00 83 00 00 00 68 20 1F 00 0/B.....h..

012年2月30日...

012 ff 04 CCC cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc

执行:

040 D4 D8 push 5//esp+= 4 = 0x 0012 fef 4,* esp = 5堆栈帧内存:

2F 42 00 83 00 00 00 00 05 00 00 00 00 0/B.........

2返回地址栈汇编指令调用对应两个操作:推送返回地址和jmp指令。

040 D4 call @ ILT+10(array bound)(0040100 f)0012 feec 30 2f 42 00 DFD 4 40 00 05 00 00 00 0/b...//返回地址是0040D4DF。

esp = 0x0012fef0,*esp = 0040D4DF

0040100F jmp数组绑定(0040d820)代码跳转:

0040D820推ebp0040D821 mov ebp,esp0040D823 sub esp,1D0h0040D829推ebx0040D82A推esi0040D82B推edi0040D82C lea edi,[ebp-1D0h]0040D832 mov ecx,74h0040D837 mov eax,0 ccccccc 0040d 83 c rep stos dword ptr[EDI]5:int b//[EBP-4]0040 d83 emovword PTR[EBP-4],0FFFFFFFFH EBP值堆栈:

040D820Push按下EBP//在分配EBP之前,通过按下堆栈来保存之前的状态。ESP = 0x0012FEEC,* ESP = ebp 0012 FEC 48 ff 1200 DFD 4 40 00 05 00 00 00h...@.....

040d821movebp,esp//ebp = esp = 0x0012feec堆栈区:

0040d823subesp,1d0h//1d0h = 464 = 400+64,esp = 0x0012fd1c在函数栈帧空之间分配。这时,栈顶指针附近的随机存储器值:

0012 FD 10 FE FF FF FF FF FE 60 75 77 76 A3 71 77....uwvw

012 fd1c 00 00 1f 00 63 01 00 50d 3 5d6e 77...C...p“西北”

012 FD 28 cf 79 14 75 00 00 00 00 00 00 00 00 00 00........

3.1寄存器堆栈

寄存器状态保持(堆栈按压)

040 d829 push ebx 0040 d82 a push ESI 0040 d82 b push EDI//esp = 0012 FD 10,* esp = EDI堆栈内存:

012fd10 48ff12 00 00 00 00 00 F0 FD 7f h...

此时,esp = 0x0012fd10,三个寄存器使用的堆栈存储器距离为464字节。

堆栈区域:

3.2堆栈帧分配的空之间的每个字节设置为0xCC。

040D82C LEA EDI,[ebp-1D0h] 0040D832MOVECX,74H0040D837MOVEAX,0 cccccccch 0040 d83c REPSTOS DWORD PTR[EDI]此时esp和ebp之间的栈空:

[98]

012 fd1c cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc,热,热,热,热,热

012 fee 0 cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc

012 feec 48 ff 12 00 df 40 00 05 00 00 00 00h..........................

3.3 stack空从ebp初始化局部变量。

5:int b =-1;//[EBP-4]0040 d83 emovword PTR[EBP-4],0FFFFFFFFH栈间空此时:

012 fee 0 cc cc cc cc cc cc cc cc cc ff ff ff ff hot hot hot hot hot hot hot hot hot hot hot hot hot hot....

012 feec 48 ff 12 00 df 40 00 05 00 00 00 00h..........................

汇编代码继续:

6:int arr[98]= { 0 };//[ebp-18c h]0040 d845 mov dword ptr[ebp-18c h],00040D84F mov ecx,61h0040D854 xor eax,eax0040D856 lea edi,[ebp-188h]0040 d85c rep stos dword ptr[EDI]7:int c = 1;// [ebp-190h],栈反向增长,栈顶地址值>:Stack 0040 d85e MOV DWORD PTR[EBP-190h],1 Stack 空此时:

012 FD 50 CCC cc cc cc cc cc cc cc cc cc cc cc cc cc热热热热热

0012 fd5c 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00............

0012 FD 68 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00............

8:arr[-1]= c * a;//数组被访问到堆栈顶部的边界之外

040d868mov eax,dword ptr[EBP-190h]0040d 86e IMU leax,dword ptr [EBP+8] 0040d872ea ecx,[EBP-18c h]0040d 878 mov dword ptr[ecx-4],此时eax的栈/[/]

012 FD 50 CCC cc cc cc cc cc cc cc cc cc cc cc cc cc热热热热热

0012 fd5c 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00............

0012 FD 68 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00............

9:arr[98]= b * a;//对数组的访问超出了堆栈底部的界限

040D87BOV EDX,DWORD PTR [EBP-4] 0040D87E伊穆尔EDX,DWORD PTR[EBP+8]0040 d82 mov DWORD PTR[EBP-4],此时EDX的stack 空:

0012 fedc 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00............

012 fee 8 FB ff ff ff ff ff 48ff 12 00 df 40 00.....h......@

0012 fef 405 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00............

堆栈区域:

3.4价值回报

10://printf( # 34;% d % d \ n # 34,b,c);// -5 511:返回b;040d885moveax,dword ptr[EBP-4]//返回值保存在寄存器eax 3.5寄存器状态恢复中

此时,esp = 0x0012fd10

040D888 POPEDI//ESP+= 4,EDI = * esp040 d889 pope si 040 d88 a pope bx堆栈框架空上方的堆栈空是464以外的空。

此时,esp = 0x0012fd1c

ebp = 0x0012feec

040D88BMOVESP,ebp//栈顶指针更新,栈空回收。这时,EBP的相应栈空

012 feec 48 ff 12 00 df 40 00 05 00 00 00 00h..........................

040d88d此时POPEBP esp = 0x0012fef0

ebp = 0x0012ff48

040d88eret函数返回:

0040D4DF add esp,4 // 4是实际参数堆栈时使用的字节数。如果按了三个int,就是Ch0040D4 E2MOV DWORD PTR [EBP-4],EAX 0012 FF 3C CC CC CC CC CC CC CC CC FB FF FF FF....

0012 FF 48 88 FF 12 00 F9 11 40 00 01 00 00 00 00......@.....

Ebp-4是主调节函数的局部变量E在堆栈存储器中的存储位置。

如果函数的返回值是复合类型,超过了两个寄存器的大小,那么将在主音调函数的堆栈框架中打开空用于返回值。

4数组越界访问向栈底越界1 int空:对应局部变量int b;

向栈底越界2 int空:对应ebp本身;

向栈底越界3 int空:对应的函数返回地址[EBP+4];

向堆栈底部超出4 int空的界限:对应的函数参数存储位置[ebp=8]:

# include ltstdio.h gtvoid array bound(int a){ int b =-1;int arr[98]= { 0 };int c = 1;arr[98]= a;arr[101]= 555;b = a;printf( # 34;% d \ n # 34,b);//555 } int main(){ int c = 5;数组绑定(c);printf( # 34;% d \ n # 34,c);//5 while(1);返回0;} 3 int空向堆栈底部越界:对应的函数返回地址:

arr[100]= 0040 D4 df;//对数组的访问超出了堆栈底部的界限

当赋给arr[100]的值是合法的内存地址时,正常运行;否则,它会错误运行。

5缓冲区溢出有以下代码:

# include ltcstdio gtvoid func(){ char buff[4]= { 0 };printf( # 34;一些输入: # 34;);gets(buff);puts(buff);} int main(){ func();返回0;}运行到gets时的堆栈区域(buff):

如果你输入 # 34;abc # 34,只需填充buff,其中buff[3]= # 39;[146]#39;

如果输入“abcdefg”,那么 # 34;efg # 34[ebp]指向的值将被填充。

如果输入“abcdefghijk”,那么“ijk”会被填入[ebp+4]的值,这是函数func()的返回地址。

-结束-

转载请注明原文地址:https://juke.outofmemory.cn/read/628736.html

最新回复(0)