1. 程式人生 > >系統學習-C++記憶體分配

系統學習-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兩個暫存器來維護,具體可參考

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段。