在说堆栈调用之前我们先来看看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个字节的,用过临时量带出返回值。
对于类类型的返回值来说,必须由临时对象将返回值值带出,临时对象存在调动放的栈帧中。