从底层分析C语言函数嵌套调用的内存布局

使用的工具

  • VC6.0
  • Excel

编写一个简单的程序

#include <stdio.h>				//头文件
int plus1(int x,int y)			//定义函数plus1,参数x,y
{
	return x+y;					//返回x+y的值
}
int plus(int x,int y,int z)		//定义函数plus,参数x,y,z
{
	int m = plus1(x,y);			//定义局部变量m的值为调用函数plush1的值
	return m+z;					//返回m+z的值
}

void main()						//程序入口
{
	int r;						//定义局部变量r
	r = plus(1,2,3);			//r的值为传参后函数plus的值

	printf("%d\n",r);			//以十进制输出计算结果
	return;						//程序结束

}

下断点开始分析

第一次函数调用

在这里插入图片描述
首先压入三个参数,然后CALL执行时把下一行地址压入栈顶,也就是所谓的返回地址。

push 3
push 2
push 1
call 0040100a

在这里插入图片描述
它在堆栈中是这样表示的:
在这里插入图片描述

接着push了个EBP,这里也就是把原EBP的值存到了堆栈中,随后把ESP的值赋给EBP,这么做可以理解为提升栈底,我们在堆栈中记录一下:

push ebp
mov ebp,esp

在这里插入图片描述

接着给esp的值减44,这一步是为了提升堆栈,给程序运行制造出缓冲区,接着保存了ebx,esi,edi这三个寄存器的值,这样可以理解为拍了个快照。

sub esp,44
push ebx
push esi
push edi

画一下堆栈图:
在这里插入图片描述
接下来的这里行指令是为了填充缓冲区,全部填充为CC指令,

lea edi,[ebp-44]
mov ecx,11
mov eax,CCCCCCCC
rep stos dword ptr [edi]

到这里整个函数的堆栈就成型了:
在这里插入图片描述

第二次函数调用

这里我们分析的是这行代码:

int m = plus1(x,y);	

接着这里把函数plus()的前两个参数(x,y)压入了栈顶,这里x,y也就是我们一开始压入的参数1,2,这里新的一轮堆栈操作开始了。

mov eax,dword ptr [ebp+C]
push eax
mov ecx,dword ptr [ebp+8]
push ecx
call 00401005

画一下堆栈图:
在这里插入图片描述
到这里我们就会发现,其实跟之前是一样的,传完参数之后准备为函数的执行腾出空间,从而提升堆栈创造缓冲区,填充CC指令,然后储存ebx,esi,edi这三个寄存器的原值。

push ebp
mov ebp,esp
sub esp,40
push ebx
push esi
push edi
lea edi,[ebp-40]
mov ecx,10
mov eax,CCCCCCCC
rep stos dword ptr[edi]

我们画一下堆栈图:
在这里插入图片描述
接下来的两行指令是计算x+y的值,并把结果返回到EAX中,这时EAX的值为3

mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]

这里堆栈没有变化,我们看一下程序的运行结果:

在这里插入图片描述

还原堆栈

接下来的三行指令是还原堆栈,把当前栈顶储存的值还原给指定的寄存器

pop ebx
pop esi
pop edi

画一下堆栈图:
在这里插入图片描述
接着把EBP的值赋给ESP,又是一个pop操作,然后ret返回到栈顶储存的地址,也就是第二个call的下一行地址,此时ESP储存的值应给是1。

mov esp,ebp
pop ebp
ret

画一下堆栈图:
在这里插入图片描述
看一下程序的运行结果:
在这里插入图片描述
接着跟了一条堆栈平衡的指令,这里用的方法是外平栈。

add esp+8

此时的堆栈变化:
在这里插入图片描述
接着把EAX的值存到了EBP-4,也就是缓冲区中。

mov dword ptr [ebp-4],eax

画一下堆栈图:

在这里插入图片描述

返回结果

接下来分析的是这行代码

return m+z;

到这里指令开始计算结果,并把结果返回到EAX中

mov eax,dword ptr [ebp-4]
add eax,dword ptr [ebp+10h]

此时EAX中的值应为3,我们运行一下程序看一下:
在这里插入图片描述

继续还原堆栈

接下来的这几条指令都是还原堆栈:

pop edi						//还原EDI的值
pop esi						//还原ESI的值
pop ebx						//还原EBX的值
add esp,44h					//减少堆栈,还原缓冲区
cmp ebp,esp					//EBP-ESP并进行比较
call  __chkesp (00401120)	//调用__chkesp()函数检测缓冲区是否溢出
mov esp,ebp					//把EBP的值赋给ESP,也是还原堆栈
pop ebp						//还原EBP的值
ret							//返回到第一个call的下一行地址

画一下堆栈:
在这里插入图片描述
运行一下程序看看结果是否一致:
在这里插入图片描述
然后是堆栈平衡,这里同样采用的外平栈方法,接着把EAX的值赋到EBP-4里:

add esp+8
mov ss:dword ptr[ebp-4],eax

堆栈变化:
在这里插入图片描述

输出结果

终于分析到printf了,画堆栈图真的好痛苦啊!接下来我们分析的代码:

printf("%d\n",r); 

这里我直接在汇编指令里分析了:

mov eax,dword ptr [ebp-4]			 //把之前储存的值存回EAX
push eax							 //把EAX的值压入栈顶,ESP的值-4
push offset string "%d\n" (0042201c) //把转义字符压入栈顶
call printf (00401160)				 //调用printf()函数,把结果输出到控制台
add esp,8							 //使用外平栈平衡堆栈

这里虽然调用了个printf()函数,但是跟我们之前画的堆栈图大同小异,这里就不分析printf这个函数了(其实我现在的水平也分析不明白)最后的堆栈样子是这样的:
在这里插入图片描述

总结

函数运行期间调用另外一个函数,在运行被调用函数之前,系统需要先完成3件事情:

  • 将所有的实参、返回地址等信息传递给被调用函数保存
  • 为被调用函数局部变量在栈上分配内存
  • 将控制转移到被调用函数入口

被调用函数返回调用函数之前,系统也需要相应的完成3件事情:

  • 在栈中保存被调用函数的返回值
  • 释放在栈中为被调用函数分配的缓冲区等
  • 依照被调用函数保存的返回地址将控制转移到调用函数
发布了60 篇原创文章 · 获赞 68 · 访问量 8392

猜你喜欢

转载自blog.csdn.net/qq_43573676/article/details/104885345