函数栈解析

本文深入解析函数栈,包括程序内存逻辑分配、函数栈初始化、调用过程及清理。通过实例分析main和Fun函数的栈操作,探讨了栈中局部变量的存放和安全检查机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文将对函数栈进行解析,在此之前先对程序的内存分配有个大概了解。

程序内存逻辑分配

一个程序中内存分布如下图:

 

代码段:保存程序文本,可读可执行不可写,指令寄存器EIP指向代码段中将要执行的代码行首地址

数据段:保存已初始化的全局变量和静态变量,可读可写不可执行

BBS:未初始化的全局变量和静态变量

Heap(堆):动态内存,数据存放向内存高端增长,可读可写可执行

Stack(栈):存放局部变量,函数参数,函数调用信息,寄存器状态等,函数栈便是在栈区,数据存放向内存低端增长,可读可写可执行

 

简单介绍下几个常用的寄存器

通用寄存器:

EAX:累加寄存器。用于算术运算的主要寄存器,也常用来存放函数的返回值。在保护模式下也可作为内存偏移指针。

ECX:计数器寄存器。常用于特定指令的计数。

EBX:基地址寄存器。内存寻址时用于存放基地址。

EDX:多功能寄存器。常用于乘除法和I/0指针。

ESI:源变址寄存器。通常在内存操作指令中作为“源地址指针”使用。

EDI:目的变址寄存器。通常在内存操作指令中作为“目的地址指针”使用。

ESP:栈指针。作为指针指向当前栈的栈顶(栈顶为栈的低地址)。

EBP:帧指针。作为指针指向当前栈的栈底(栈底为栈的高地址)。

EIP:指令寄存器。作为指针指向下一条指令,该寄存器的值不能直接修改。

 

函数栈解析:

本文将以一个示例程序对函数栈进行解析:

#include <stdio.h>
#include <Windows.h>

int Fun(int a, int b)
{	
	return a + b;
}

int main()
{
	int a, b;

	a = 0x11111111;
	b = 0x22222222;

	Fun(3, 4);

	system("pause > nul");
	return 0;
}


函数栈初始化

下面以main函数的函数栈为例介绍函数栈的初始化:

main函数初始化在VS下的反汇编源码如下:

  

14:  int main()
	15: {
001D1410	push		ebp
001D1411	mov		ebp, esp
001D1413	sub		esp, 0D8h
001D1419	push		ebx
001D141A	push		esi
001D141B	push		edi
001D141C	lea		edi, [ebp – 0D8h]
001D1422	mov		ecx, 36h
001D1427	mov		eax, 0CCCCCCCCh
001D142C	rep  stos	dword ptr es : [edi]

包括调用main函数,每个函数栈的初始化都是相似的。被调用函数会先将各状态寄存器和堆栈寄存器压栈,完成对原始寄存器状态的保存;并分配函数栈的大小并初始化函数栈。

接下来对每行汇编源码进行分析:

push  ebp、ebx、esi、edi 分别将四个寄存器压栈,压栈的起始位置为(ESP – 1),每次压栈ESP -= 4

第二行mov  ebp,  esp是在完成对原始EBP寄存器的保存后,将调用函数的栈顶设为被调用函数的栈底

第三行sub  esp,  0D8h即esp -= 0D8h,即将(esp – 0D8h)设为main函数的栈顶

在完成对各寄存器状态的保存和函数栈大小的分配后,最后四行汇编源码是对函数栈初始化:

lea  edi, [ebp – 0D8h]将(ebp – 0F4h) 赋值给EDI寄存器,由于mov操作不支持第二个操作数是一个寄存器减去一个数值,故用lea操作。这里是将栈顶地址赋给EDI寄存器

mov  ecx, 36h将数值36h赋值给ECX寄存器,0X36 = 0D8h >> 2,即为main函数栈内存的DWORD数目

mov  eax, 0CCCCCCCCh将EAX赋值为0CCCCCCCCh,即为INT 3中断

rep  stos dword ptr es : [edi]   rep指令是stos字符串指令的前缀,rep指令使其后的字符串指令被重复直至ECX为0,故ECX即为重复次数,stos指令是一个字符串指令,是将EAX中的值拷贝到ES : EDI指向的地址,再EDI += 4,而dword ptr则是指stos指令一次拷贝双字

main函数的函数栈初始化如下图所示:

 

在调用Fun函数时

      21: Fun(3, 4);

001D143C   push     4

001D143E    push     3

001D1440    call Fun  (01D1127h)

001D1445    add asp,  8

Fun函数的调用方式为__cdecl,故实参从右向左压栈,并且__cdecl方式由调用函数清理栈中的实参, add  esp, 8为清除实参的指令。

call  Fun (01D1127h)将跳转到Fun函数的jmp跳转指令地址0x01D1127h处,注意call指令有一个隐含的压栈操作:将函数的返回地址(Fun函数结束后将执行的指令地址,本例中为0x001D1445h) 压栈。

Fun函数的函数栈初始化也和main函数相似,不再赘述。

Fun函数的函数栈初始化后如下图所示:

 

函数栈的清理

Fun函数结束处的汇编源码如下:

      6:   return a + b;

001D13EC   mov     eax, dword ptr [a]

001D13EF    add eax, dword ptr [b]

      7:   }

001D13F2    pop      edi

001D13F3    pop      esi

001D13F4    pop      ebx

001D13F5    mov     esp, ebp

001D13F7    pop      ebp

001D13F8    ret

Fun函数的返回值存放在EAX寄存器中,之后EDI、ESI、EBX寄存器分别出栈,出栈时不对内存操作,栈顶指针ESP += 4,EDI、ESI、EBX寄存器恢复为栈中保存的值

栈顶指针ESP和栈底指针EBP分别恢复为call指令之后的状态

如下图:

 

上文提到call指令隐含函数返回地址的压栈,ret指令与call指令是相对的,隐含函数返回地址的出栈,指令寄存器EIP指向出栈的返回地址所指向的指令,同时ESP+= 4。

因此不难想到:可以在被调用函数中通过帧指针EBP寻址并修改函数返回地址,从而实现跳转。

在ret指令和实参出栈后,Fun函数的函数栈清理也就完成了。

看到这里,对于函数栈的初始化和恢复也就清楚了。

扩展:

一、      函数栈的安全检查:

在一些程序中main函数的函数栈初始化完成后经常会看到如下三行:

00B113DE A1 00 80 B1 00       mov         eax,dword ptr ds:[00B18000h] 

00B113E3 33 C5                xor         eax,ebp 

00B113E5 89 45 FC             mov         dword ptr [ebp-4],eax

这是VS添加的基于cookie的安全检查,引用Microsoft的解释:

Theprolog contains an instruction that fetches a copy of the cookie, followed byan instruction that does a logical xor of the cookie and the return address,and then finally an instruction that stores the cookie on the stack directlybelow the return address. From this point forward, the function will execute asit does normally. When a function returns, the last thing to execute is thefunction’s epilog, which is the opposite of the prolog.Without security checks, it will reclaim the stack space and return, such asthe following instructions:

关于cookie变量安全检查这里便不细说,详细的可以看微软的MSDN文档CompilerSecurity Checks In Depth

 

二、      函数栈局部变量存放问题

在一些博文中看到过通用栈指针EBP来获取函数栈内的局部变量的方法,不过这里需要注意:函数栈中局部变量的存放并不一定是连续的。以上面的程序为例,main函数的函数栈中有两个局部变量a(0x11111111h)、b(0x22222222h)。VS2013下,其在函数栈中的存放如下图所示:

 

红色框即为两个局部变量a、b,绿色框为栈底(EBP所指处)。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值