一个c/c++编译的程序占用的内存分为以下几个部分:
1.栈区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值,返回数据,返回地址等。操作方式类似于数据结构中的栈。
2.堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中的堆是两码事,分配方式类似于链表。
3.全局区(静态区)(static):存放全局变量、静态数据、常量。程序结束后由系统释放。
4.文字常量区:常量字符串放在此,程序结束后由系统释放。
5.程序代码区:存放函数体(类成员函数和全局函数)的二进制代码。
内存区域分配如图所示:
其中堆和栈相对而生。
堆和栈的申请方式:
栈由系统自动分配,速度较快,在windows下栈是向低地址扩展的数据结构,是一块连续的内存区域,大小是2MB。
堆需要程序员自己申请,并指明大小,速度比较慢。在C中用malloc申请,C++中用new申请。另外,堆是向高地址扩展的数据结构,是不连续的内存区域,堆的大小受限于计算机的虚拟内存。因此堆空间获取和使用比较灵活,可用空间较大。
接下来由一个实例具体分析调用过程
#include <stdio.h>
#include <windows.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int ret = 0;
ret = Add(a, b);
printf("you should run here!\n ret = %d\n", ret);
system("pause");
return 0;
}
在具体分析之前先了解3个重要寄存器:
EBP:基址寄存器,存放指向函数栈帧栈底的地址。
ESP:栈顶寄存器,存放指向函数栈帧栈顶的地址。
EIP/IP/PC:程序计数器,保存的内容指向下一条指令。
main函数汇编语言如下:
int main()
{
00E91410 push ebp //将ebp压栈处理(方便函数返回之后的现场恢复)
00E91411 mov ebp,esp //使esp的值赋给ebp,产生新的ebp
00E91413 sub esp,0E4h //给esp减去一个16进制数字0E4h,产生新的esp
00E91419 push ebx
00E9141A push esi
00E9141B push edi
00E9141C lea edi,[ebp-0E4h]
00E91422 mov ecx,39h
00E91427 mov eax,0CCCCCCCCh //把栈帧预开辟的空间全部初始化为0xCCCCCCCC
00E9142C rep stos dword ptr es:[edi] //从edi开始向下重复赋值ecx次,赋值内容为eax的内容
int a = 10;
00E9142E mov dword ptr [ebp-4],0Ah
int b = 20;
00E91435 mov dword ptr [ebp-8],14h
int ret = 0;
00E9143C mov dword ptr [ebp-0ch],0
ret = Add(a, b);
00E91443 mov eax,dword ptr [ebp-8]
00E91446 push eax //参数压栈,先压b
00E91447 mov ecx,dword ptr [ebp-4]
00E9144A push ecx
00E9144B call _Add (0E910E6h)
00E91450 add esp,8 //此地址将被压入栈中
00E91453 mov dword ptr [ret],eax
printf("you should run here!\n ret = %d\n", ret);
00E91456 mov esi,esp
00E91458 mov eax,dword ptr [ret]
00E9145B push eax
00E9145C push 0E95858h
00E91461 call dword ptr ds:[0E99118h]
00E91467 add esp,8
00E9146A cmp esi,esp
00E9146C call __RTC_CheckEsp (0E91140h)
system("pause");
00E91471 push 0E9587Ch
00E91476 call _system (0E910A5h)
00E9147B add esp,4
return 0;
00E9147E xor eax,eax
}
00E91490 mov esp,ebp
00E91492 pop ebp
00E91493 ret
call的作用:
1.通过修改IP实现函数的跳转(jmp)
2.call将当前正在执行的指令的下一条指令的地址保存起来。
Add函数汇编语言如下:
int Add(int x, int y)
{
00E913D0 push ebp //将ebp压栈
00E913D1 mov ebp,esp //将esp赋值给ebp
00E913D3 sub esp,0CCh //给esp减去一个16进制数字
00E913D9 push ebx
00E913DA push esi
00E913DB push edi
00E913DC lea edi,[ebp-0CCh]
00E913E2 mov ecx,33h
00E913E7 mov eax,0CCCCCCCCh
00E913EC rep stos dword ptr es:[edi]
int z = 0;
00E913EE mov dword ptr [ebp-8],0 //创建变量z
z = x + y;
00E913F5 mov eax,dword ptr [ebp+8] //获取形参a和b相加
00E913F8 add eax,dword ptr [ebp+0ch]
00E913FB mov dword ptr [ebp-4],eax //将结果存储到z中
return z;
00E913FE mov eax,dword ptr [ebp-4] //将结果存储到eax寄存器,通过寄存器带回函数的返回值
}
00E91401 pop edi //出栈
00E91402 pop esi //出栈
00E91403 pop ebx //出栈,使esp向下移动
00E91404 mov esp,ebp //将ebp赋值给esp
00E91406 pop ebp
00E91407 ret //ret指令会使得出栈一次,并将出栈的内容当作地址,将程序执行跳转到该地址处。
00E91450 add esp,8 //此地址将被压入栈中
00E91453 mov dword ptr [ebp-0Ch],eax
由此分析,x,y是连续存放的,所以我们可以通过修改x的地址进而修改y的值
如:
#include <stdio.h>
#include <windows.h>
int Add(int x, int y)
{
printf("before y:%d\n", y);
int *p = &x;
p++;
*p = 30;
printf("after y:%d\n", y);
return 0;
}
int main()
{
int a = 10;
int b = 20;
int ret = 0;
ret = Add(a, b);
printf("you should run here!\n ret = %d\n", ret);
system("pause");
return 0;
}
又如:
#include <stdio.h>
#include <windows.h>
void bug()
{
printf("I am a bug!\n");
system("pause");
}
int Add(int x, int y)
{
printf("Add......done!\n");
int *p = &x;
p--;
*p =(int) bug;
return 0;
}
int main()
{
int a = 10;
int b = 20;
printf("main.....begin!\n");
int ret = 0;
ret = Add(a, b);
printf("you should run here!\n ret = %d\n", ret);
system("pause");
return 0;
}
程序执行后崩溃,bug函数调用后没返回到main函数,引起栈结构不平衡。
由此进行改进,使bug函数找到其返回值地址:
#include <stdio.h>
#include<windows.h>
void *c = NULL;
void bug()
{
int a = 0;
int *p = &a;
p += 2;
*p = (int)c;
printf("I am a bug!\n");
system("pause");
}
int Add(int x, int y)
{
printf("Add......done!\n");
int *p = &x;
p--;
c = (void*)*p; //将p中的内容保存
*p =(int) bug;
return 0;
}
int main()
{
int a = 10;
int b = 20;
printf("main.....begin!\n");
int ret = 0;
ret = Add(a, b);
printf("you should run here!\n ret = %d\n", ret);
system("pause");
return 0;
}
程序还是会崩溃,因为调用Add函数时使用的是call汇编语句,而调用bug函数是直接修改其IP地址。但两个函数最后都有一个ret结构,将栈顶的返回值弹出值EIP中,所以经历了两次出栈,却只有一次压栈。需要平衡栈帧结构。
改进如下:
#include <stdio.h>
#include<windows.h>
void *c = NULL;
void bug()
{
int a = 0;
int *p = &a;
p += 2;
*p = (int)c;
printf("I am a bug!\n");
system("pause");
}
int Add(int x, int y)
{
printf("Add......done!\n");
int *p = &x;
p--;
c = (void*)*p; //将p中的内容保存
*p =(int) bug;
return 0;
}
int main()
{
int a = 10;
int b = 20;
printf("main.....begin!\n");
int ret = 0;
ret = Add(a, b);
printf("you should run here!\n ret = %d\n", ret);
_asm
{
sub esp,4 //为了平衡栈帧结构
}
system("pause");
return 0;
}