系統學習-C++記憶體分配
目錄
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兩個暫存器來維護,具體可參考
相信開發同學們對於函式呼叫棧的結構早就清楚了,但是有沒有想過為什麼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 [[email protected]@@[email protected][email protected]@[email protected]@@[email protected] (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 [[email protected]@@[email protected][email protected]@[email protected]@@[email protected] (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
段。