一、堆和栈
(1)首先要清楚的是程序对内存的使用分为以下几个区:
1、 栈区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值等。操作方式类似于数据结构中的栈。栈用于维护函数调用的上下文,离开了栈函数调用就没法实现。
2、 堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中的堆是两码事,分配方式类似于链表。堆是用来容纳应用程序动态分配的内存区域,当程序使用malloc或new分配内存时,得到的内存来自堆里。
3、 全局区(static):全局变量和静态变量存放在此。
4、 常量区:常量字符串放在此,程序结束后由系统释放。
5、 代码区:存放函数体的二进制代码。
Linux下一个进程里典型的内存布局
(2)申请方式
栈由系统自动分配,速度较快,在windows下栈是向低地址扩展的数据结构,是一块连续的内存区域,大小是2MB。
堆需要程序员自己申请,并指明大小,速度比较慢。在C中用malloc,C++中用new。堆是向高地址扩展的数据结构,是不连续的内存区域,堆的大小受限于计算机的虚拟内存。因此堆空间获取和使用比较灵活,可用空间较大。
二、栈帧
1、栈在函数调用中的作用:参数传递、局部变量分配、保存调用的返回地址、保存寄存器以供恢复。
2、栈帧(stack Frame):一次函数调用包括将数据和控制从代码的一个部分传递到另外一个部分,栈帧与某个过程调用一一映射。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低址地)。
3、函数调用规则:
_cdecl:按从右至左的顺序压参数入栈,由调用者把参数弹出栈。由于每次函数调用都要由编译器产生清楚堆栈的代码,所以使用_cdecl的代码比使用_stdcall的代码要大很多,但是这种方式支持可变参数。对于C函数,名字修饰约定为在函数名前加下划线。对于C++,除非特变使用extern C,C++使用不同的名字修饰方式。
_stdcall:按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数。
_fastcall:主要特点就是快,因为它是通过寄存器来传送参数的,和__stdcall很象,唯一差别就是头两个参数通过寄存器传送。注意通过寄存器传送的两个参数是从左向右的,即第一个参数进ecx,第2个进edx,其他参数是从右向左的入stack。返回仍然通过eax。
三、函数调用过程
int sum(int a,int b)
{
int temp = 0;
temp = a+b;
return temp;
}
int main()
{
int a = 10;
int b = 20;
int ret = 0;
ret = sum(a,b);
cout<return 0;
}
//转到反汇编
int sum(int a,int b)
{
00D6AA00 push ebp
00D6AA01 mov ebp,esp
00D6AA03 sub esp,0CCh
00D6AA09 push ebx
00D6AA0A push esi
00D6AA0B push edi
00D6AA0C lea edi,[ebp-0CCh]
00D6AA12 mov ecx,33h
00D6AA17 mov eax,0CCCCCCCCh
00D6AA1C rep stos dword ptr es:[edi]
int temp = 0;
00D6AA1E mov dword ptr [temp],0
temp = a+b;
00D6AA25 mov eax,dword ptr [a]
00D6AA28 add eax,dword ptr [b]
00D6AA2B mov dword ptr [temp],eax
return temp;
00D6AA2E mov eax,dword ptr [temp]
}
00D6AA31 pop edi
00D6AA32 pop esi
00D6AA33 pop ebx
00D6AA34 mov esp,ebp
00D6AA36 pop ebp
00D6AA37 ret
int main()
{
00FBAB60 push ebp
00FBAB61 mov ebp,esp
00FBAB63 sub esp,0E4h
00FBAB69 push ebx
00FBAB6A push esi
00FBAB6B push edi
00FBAB6C lea edi,[ebp-0E4h]
00FBAB72 mov ecx,39h
00FBAB77 mov eax,0CCCCCCCCh
00FBAB7C rep stos dword ptr es:[edi]
int a = 10;
00FBAB7E mov dword ptr [a],0Ah
int b = 20;
00FBAB85 mov dword ptr [b],14h
int ret = 0;
00FBAB8C mov dword ptr [ret],0
ret = sum(a,b);
00FBAB93 mov eax,dword ptr [b]
00FBAB96 push eax
00FBAB97 mov ecx,dword ptr [a]
00FBAB9A push ecx
00FBAB9B call sum (0FB1195h)
00FBABA0 add esp,8
00FBABA3 mov dword ptr [ret],eax
cout<00FBABA6 mov esi,esp
00FBABA8 mov eax,dword ptr ds:[00FC7364h]
00FBABAD push eax
00FBABAE mov edi,esp
00FBABB0 mov ecx,dword ptr [ret]
00FBABB3 push ecx
00FBABB4 mov ecx,dword ptr ds:[0FC7378h]
00FBABBA call dword ptr ds:[0FC735Ch]
00FBABC0 cmp edi,esp
00FBABC2 call __RTC_CheckEsp (0FB1550h)
00FBABC7 mov ecx,eax
00FBABC9 call dword ptr ds:[0FC7358h]
00FBABCF cmp esi,esp
00FBABD1 call __RTC_CheckEsp (0FB1550h)
return 0;
00FBABD6 xor eax,eax
}
首先可以发现在高级源代码之前都有固定的汇编指令
00FBAB60 push ebp
00FBAB61 mov ebp,esp
00FBAB63 sub esp,0E4h
00FBAB69 push ebx
00FBAB6A push esi
00FBAB6B push edi
00FBAB6C lea edi,[ebp-0E4h]
00FBAB72 mov ecx,39h
00FBAB77 mov eax,0CCCCCCCCh
00FBAB7C rep stos dword ptr es:[edi]
每个函数在调用前都会做一件事情。
(1)把调用方的ebp入到自己的栈里去
(2)用esp指向新的栈顶位置,把esp的值赋给ebp,产生新的ebp
(3)开辟空间,初始0XCCCCCCCC。
ebp入栈,实参入栈,形参入栈,调用call指令。call有两步,把下一行指令的地址入栈,跳转到调用的函数执行。形参内存主调方开辟,主调方释放。
在栈上访问局部变量都是通过ebp指针的偏移量,局部变量属于指令。
C和C++调用函数时实参从右向左压。因为C和C++支持可变参数函数
sum函数栈帧的回退
00D6AA34 mov esp,ebp //把ebp的值给esp
00D6AA36 pop ebp //出栈。esp向下挪,把出栈的元素赋给ebp
00D6AA37 ret
ret包含两个动作:
1、出栈 ,栈顶指针向下移。出栈元素是下一行指令地址,赋给PC寄存器,PC寄存器永远存放下一行指令的地址。
2、ret运行完以跳转到下一行指令的地址
00FBABA0 add esp,8 //回退形参变量