C++内存分配是一个很基础的问题,明白这个分配机制,有很多C++的问题都可以很容易理解。比如const成员变量为何需要利用构造函数初始化列表才能进行初始化;static关键字为什么可以改变存储属性;new/malloc的内存分配方式等。
程序结构理解
这是描述32位系统下程序大致内存结构的经典老图(64位类似,只是32位的图网上有现成的),具体就不赘述了。
stack:函数栈区
heap:函数堆区
.bss:用来存放程序中未初始化的全局变量和静态变量
.data:数据段-静态存储区
.txt:代码段
程序运行过程
通过CS:IP两个寄存器一条一条的确定执行的指令地址,并依次执行指令。
基本流程:CS+IP->地址(地址总线)->取指令(数据总线)->执行
具体可以参考:X86处理器中的CS与IP寄存器
Stack区
主要是函数调用栈
说明一下在x86_64架构下,当寄存器足够存放参数时,是不会对参数进行压栈的,因此图中参数1到n(对应函数参数列表是从右到左)是可选的,当把上个栈帧的基址压入栈中时,新的栈帧就开始了。
不同栈之间主要通过EBP与ESP两个寄存器来维护,具体可参考EBP与ESP讲解
相信开发同学们对于函数调用栈的结构早就清楚了,但是有没有想过为什么c/cpp编写的程序函数调用栈长这样?其实没有为什么,只是因为gcc编译器是这么工作的,这是gcc为函数调用设计的规范(更合理的说法应该是编写gcc的大佬们),不过其设计背后的原因其实也不难想到:一是因为各个函数的指令集在物理空间上是独立的,自然需要处理指令的跳转;二是需要解决输入和输出的传递,为什么输入参数少的时候直接用寄存器呢?当然是因为CPU访问寄存器更快,可惜寄存器个数有限,不然我们就不需要缓存和内存了(寄存器也是一片存储空间,不同的寄存器名称只是对不同的地址块的引用而已)。
将寄存器中的变量拷贝到内存的原理:通过Mov
指令
也就是说gcc帮我们把c/cpp等高级语言编写的代码,按照规范转化为了汇编指令。
反汇编分析
源码
#include <iostream>
using namespace std;
int add1(int num1, int num2,int num3)
{
int a = 100;
return a+num1+num2+num3;
}
template <typename T>
T add2(T a, T b)
{
return a + b;
}
int main()
{
int a = 10;
int b = 15;
int c = 21;
int d = add1(a, b, c);
int t = 100;
int f = add2(d, t);
cout << f << endl;
cin.get();
}
反汇编
int main()
{
002F8F60 push ebp //通过ebp和esp来控制栈的界限
002F8F61 mov ebp,esp //将esp的值赋值给ebp,esp开始增长
002F8F63 sub esp,108h //是从高地址向低地址走的
002F8F69 push ebx
002F8F6A push esi
002F8F6B push edi
002F8F6C lea edi,[ebp-108h]
002F8F72 mov ecx,42h
002F8F77 mov eax,0CCCCCCCCh
002F8F7C rep stos dword ptr es:[edi]
int a = 10;
002F8F7E mov dword ptr [a],0Ah //这是一个赋值的过程
int b = 15;
002F8F85 mov dword ptr [b],0Fh //ptr就是内存的一个地址,现在将参数赋值到了内存上
int c = 21;
002F8F8C mov dword ptr [c],15h
int d = add1(a, b, c);
002F8F93 mov eax,dword ptr [c] //在函数调用时,是将参数按照从后往前的顺序依次赋值的
002F8F96 push eax
002F8F97 mov ecx,dword ptr [b] //顺序是c,b,a
002F8F9A push ecx
002F8F9B mov edx,dword ptr [a]
002F8F9E push edx //eax、ebx、ecx、edx为变量寄存器
002F8F9F call add1 (02EEA36h) //调用函数
002F8FA4 add esp,0Ch
002F8FA7 mov dword ptr [d],eax //函数返回值是通过eax传回来的,将值从寄存器中赋值到了内存上
int t = 100;
002F8FAA mov dword ptr [t],64h
int f = add2(d, t);
002F8FB1 mov eax,dword ptr [t]
002F8FB4 push eax
002F8FB5 mov ecx,dword ptr [d]
002F8FB8 push ecx
002F8FB9 call add2<int> (02EEA3Bh)
002F8FBE add esp,8
002F8FC1 mov dword ptr [f],eax //函数返回值是通过eax传回来的,将值从寄存器中赋值到了内存上
cout << f << endl;
002F8FC4 mov esi,esp
002F8FC6 push offset std::endl<char,std::char_traits<char> > (02ED48Dh)
002F8FCB mov edi,esp
002F8FCD mov eax,dword ptr [f]
002F8FD0 push eax
002F8FD1 mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0339140h)]
002F8FD7 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (033910Ch)]
002F8FDD cmp edi,esp
002F8FDF call __RTC_CheckEsp (02EDA5Ah)
002F8FE4 mov ecx,eax
002F8FE6 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0339108h)]
002F8FEC cmp esi,esp
002F8FEE call __RTC_CheckEsp (02EDA5Ah)
cin.get();
002F8FF3 mov esi,esp
002F8FF5 mov ecx,dword ptr [_imp_?cin@std@@3V?$basic_istream@DU?$char_traits@D@std@@@1@A (033914Ch)]
002F8FFB call dword ptr [__imp_std::basic_istream<char,std::char_traits<char> >::get (0339148h)]
002F9001 cmp esi,esp
002F9003 call __RTC_CheckEsp (02EDA5Ah)
}
002F9008 xor eax,eax
002F900A pop edi //退栈的一个过程
002F900B pop esi
002F900C pop ebx
002F900D add esp,108h
002F9013 cmp ebp,esp
002F9015 call __RTC_CheckEsp (02EDA5Ah)
002F901A mov esp,ebp
002F901C pop ebp
002F901D ret //返回标志位
Visual Studio中反汇编:设置断点,调试,之后点击调试->窗口->反汇编即可。
CodeBlocks中反汇编:设置断点,调试,之后点击debug->debugging windows->disassembly即可。
总结
编译器将程序代码编译成二进制文件,CS:IP指导一条条指令按序从.txt
执行。在执行的过程中,程序结构如上图,其中函数Stack
由EBP与ESP维护,动态申请的内存在Heap
上开辟空间,静态变量与全局变量在.data
段,未初始化的全局变量和静态变量在.bss
段。