Linux下函数的堆栈调用

在说堆栈调用之前我们先来看看linux中一个程序的4G虚拟地址空间是如何进行分配的,我们以下面一段代码为例,


int glob1 = 10;
int glob2 = 0;
int glob3;

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

int main()
{
	int a = 10;
	int b = 20;
	int c = max(a,b);

	return 0;
}

在内核给程序维护的4G虚拟地址空间中,程序运行所需要的信息存储在不同的分段中,有存储指令的代码段(.text)、存放已初始化(初始化不为0)数据的数据段(.data)、存放未初始化或初始化为0的数据的数据段(.bss),以及堆区空间,栈区空间,以及内核空间等等。

    

在Windows系统中一般把4G的虚拟地址空间按照用户空间比内核空间等于2:2的比例划分,在linux系统中一般用户空间比内核空间的比例等于3:1,本篇博客讨论linux系统的内容,linux系统中4G的虚拟地址空间一般划分情况如下图所示:

.text段:代码段,也可以称之为文本段,用于存放程序执行的代码(即CPU指令),一般C语言执行的语句都编译成机器指令存放在代码段。通常代码段是可以共享的。

.data段:在本数据段中一般存放已初始化且初始化不为0的全局变量、静态全局变量和静态局部变量。数据段属于静态内存分区(静态存储区)。

.bss段:bss段通常存放程序中的以下符号:1、未初始化的全局变量和静态局部变量。2、初始值为0的全局变量和静态局部变量。

栈区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量等值。操作方式相当于数据结构的栈。栈用于维护函数调用的上下文,方便与从调用点返回调用方。

堆区(heap):一般有程序员自己分配是释放,若程序员不释放,则在程序运行结束后系统收回。但与数据结构中的堆没有关系,堆区的分配方式属于链表。堆是用来容纳应用程序动态分配的内存区域,当程序调用malloc或者new来分配内存时,得到的内存空进就来自堆区。

上面的代码我们可以转到反汇编来看。

int add(int a,int b)
{
00DD13D0  push        ebp  
00DD13D1  mov         ebp,esp  
00DD13D3  sub         esp,0C0h  
00DD13D9  push        ebx  
00DD13DA  push        esi  
00DD13DB  push        edi  
00DD13DC  lea         edi,[ebp-0C0h]  
00DD13E2  mov         ecx,30h  
00DD13E7  mov         eax,0CCCCCCCCh  
00DD13EC  rep stos    dword ptr es:[edi]  
	return a+b;
00DD13EE  mov         eax,dword ptr [a]  
00DD13F1  add         eax,dword ptr [b]  
}
00DD13F4  pop         edi  
00DD13F5  pop         esi  
00DD13F6  pop         ebx  
00DD13F7  mov         esp,ebp  
00DD13F9  pop         ebp  
00DD13FA  ret  

int main()
{
00DD1410  push        ebp  
00DD1411  mov         ebp,esp  
00DD1413  sub         esp,0E4h  
00DD1419  push        ebx  
00DD141A  push        esi  
00DD141B  push        edi  
00DD141C  lea         edi,[ebp-0E4h]  
00DD1422  mov         ecx,39h  
00DD1427  mov         eax,0CCCCCCCCh  
00DD142C  rep stos    dword ptr es:[edi]  
	int a = 10;
00DD142E  mov         dword ptr [a],0Ah  
	int b = 20;
00DD1435  mov         dword ptr [b],14h  
	int c = add(a,b);
00DD143C  mov         eax,dword ptr [b]  
00DD143F  push        eax  
00DD1440  mov         ecx,dword ptr [a]  
00DD1443  push        ecx  
00DD1444  call        add (0DD1096h)  
00DD1449  add         esp,8  
00DD144C  mov         dword ptr [c],eax  

	return 0;
00DD144F  xor         eax,eax  
}
00DD1451  pop         edi  
00DD1452  pop         esi  
00DD1453  pop         ebx  
00DD1454  add         esp,0E4h  
00DD145A  cmp         ebp,esp  
00DD145C  call        __RTC_CheckEsp (0DD113Bh)  
00DD1461  mov         esp,ebp  
00DD1463  pop         ebp  
00DD1464  ret  

常用寄存器:ebp:栈底指针寄存器、esp:栈顶指针寄存器、pc:下一条指令寄存器

每个含数在调用前都会做一件事,将调用方的ebp压入自己的栈帧中,把esp的值赋给ebp,再用esp指向新的栈顶位置,然后开辟空间并初始化为0xCCCCCCCC

分析反汇编代码如下图所示:

由上图我们可以总结得出,函数在进行堆栈调用时,会进行以下几步:

1)、形参初始化。形参初始化即是将被调方所需要的形参,以从右向左的顺序依次入栈(从右向左是调用约定的规定)。

2)、将下一行指令地址入栈。将下一行指令地址入栈,方便于程序从被调函数中返回时,从调用方函数的调用点之后继续执行。

3)、将调用方栈底指针寄存器的值入栈。将调用方的栈底指针寄存器的值入栈,相当于记录了当前栈帧中栈底的位置。方便于从被调放返回时,栈底指针重新回到调用方的栈底。

4)、移动ebp到调用方的栈顶。将ebp移动到调用方的栈顶,就可以从此ebp的位置为被调方开辟栈帧,此ebp就是新栈帧的ebp

5)、开辟局部变量活动所需要的栈空间,并将其初始化为0xCCCC CCCC.

在函数的堆栈调用中,有一点值得注意,在将函数的形参,下一行指令入栈时,所开辟的内存是由调用方管理的,属于调用方的栈帧。

最后我们还需讨论一下,在被调用方运行结束后,它的返回值是如何带入调用方的?

我们不难从上面这段反汇编代码看出,在调用完add函数并返回后,mov指令将eax寄存器的值赋给了c,也就是说add函数将返回值赋给了eax寄存器,再由eax寄存器带回了调用方函数。

函数返回值的返回方式(非类类型),返回值大于0个字节小于等于4个字节的,通过eax寄存器返回。返回值大于4个字节小于等于8个字节的,通过eax、ebx两个寄存器带出。返回值大于8个字节的,用过临时量带出返回值。

对于类类型的返回值来说,必须由临时对象将返回值值带出,临时对象存在调动放的栈帧中。

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值