連結、裝載與庫——程序的棧
記憶體是承載程式的介質,是程式進行運算和表達的場所。
未有特殊說明,則預設在32bit作業系統中。
1. 程式的記憶體佈局
作業系統會將記憶體空間中的一部分分給核心使用,應用程式無法訪問這段記憶體,這段記憶體被稱為核心空間。Windows預設情況將高地址的2GB空間分配給核心,Linux預設情況將高地址的1GB空間分配給核心。
剩下的記憶體空間稱為使用者空間,使用者空間中有許多預設區域。
棧:棧用於維護函式呼叫的上下文。棧通常在使用者空間的最高地址處分配,一般大小位數兆位元組
堆:堆用來容納應用程式動態分配的記憶體區域。堆通常在棧的下方(低地址方向)。堆一般比棧大可以有幾十至數百兆位元組的容量
可執行檔案映像:儲存著可執行檔案在記憶體裡的映像。
保留區:保留區不是一個單一的記憶體區域,而是對記憶體中受到保護而禁止訪問的記憶體區域的總稱
- 棧向低地址增長,堆向高地址增長。當棧或堆現有的大小不夠用時,它們將按照增長方向擴大,直到預留的的空間被用完為止
2. 棧(stack)
將資料壓入棧中(入棧,push),將已經壓入棧中的資料彈出(出棧,pop),即先入棧的資料後出棧(First In Last Out,FIFO)
壓棧操作使棧增大,彈出操作使棧減小
棧總是向低地址增長的,棧頂由esp暫存器進行定位,棧底由ebp暫存器進行定位。壓棧操作使棧頂地址減小,彈出操作使棧頂地址增大
棧儲存了一個函式呼叫所需要的維護資訊,其被稱為棧幀(Stack Frame)或活動記錄(Activate Record),棧幀包含如下內容:
函式的返回地址和引數
臨時變數:包括函式的非靜態區域性變數、編譯器自動生成的其他臨時變數
儲存的上下文: 包括在函式呼叫前後需要保持不變的暫存器
一個函式的活動範圍由ebp和esp暫存器劃定範圍。esp暫存器始終指向棧的頂部即當前函式的活動記錄的頂部,ebp暫存器指向函式活動記錄的底部,ebp暫存器也被稱為幀指標(Frame Pointer)
i386下的函式呼叫步驟如下
把所有或一部分引數壓入棧中,如果有其他引數沒有入棧,那麼使用某些特定的暫存器傳遞
把當前指令的下一條指令的地址壓入棧中
跳轉到函式體執行
第二、三步由指令call一起執行
i386下的函式體“標準”開頭如下
push ebp(儲存本棧幀的ebp)
mov ebp, esp(將ebp移動到棧頂)
sub esp, XXX(開闢新的棧幀)
push XXX(儲存暫存器)
i386下的函式體“標準”結束如下
pop XXX(恢復暫存器)
mov esp, ebp(恢復成呼叫者所在棧幀的棧頂)
pop ebp(恢復成呼叫者所在棧幀的棧基址)
ret(從棧頂取得下一條指令的地址,並跳轉)
示例如下
測試程式碼
main函式反彙編
foo函式反彙編
foo函式return之前時暫存器
foo函式return之前時記憶體
foo函式反彙編程式碼解析
某些場合,編譯器生成函式的進入和退出指令序列不按照標準的方式進行,比如C函式滿足:
函式被宣告為static(不可在此編譯單元之外訪問)
函式在本編譯單元僅被直接呼叫,沒有顯示或隱式的取地址(即沒有任何函式指標指向過這個函式)
編譯器確信滿足這兩條的函式不會在其他編譯單元內被呼叫,因此可以修改指令,達到優化目的
函式呼叫慣例(Calling Convention)
函式引數的傳遞順序和方式:規定函式呼叫方將引數壓入棧的順序(從左到右、從右至左);規定函式引數的傳遞方式(通過棧傳遞,函式呼叫方將引數壓入棧,自己在從棧中將引數取出、使用暫存器傳遞,提高效能)
棧的維護:函式體執行完後,之前壓入棧中的引數需要彈出,可以由函式呼叫方完成,也可以由函式體本身完成
名字修飾(Name-mangling)策略:連結時區分呼叫慣例,不同的呼叫慣例有不同的名字修飾策略
C語言預設呼叫慣例是cdecl,任何一個沒有顯式指定呼叫慣例的函式都預設是cdecl,比如:
int _cdecl foo(int a, int b, int c)
函式返回值傳遞
- 當返回值小於等於4位元組時,函式將返回值儲存在eax,呼叫者讀取eax
- 當返回值大於4位元組,小於等於8位元組時,函式使用eax和edx聯合返回的方式。eax儲存低4位元組,edx高4位元組
- 當返回值大於8位元組時,函式會使用一個臨時的棧上記憶體空間(臨時物件)作為中轉,返回值物件會被拷貝兩次
- 當返回值大於8位元組時,函式返回值傳遞示例如下:
- 函式返回值傳遞測試程式碼
- main函式返回值反匯程式碼
- return_test函式反彙編程式碼
- main函式中n的地址
- 臨時物件給main函式中的n賦值
- 函式返回值傳遞測試程式碼
- 首先呼叫者在棧上將一部分空間作為傳遞返回值的臨時物件
- 將臨時物件的地址作為隱藏引數傳遞給函式
- 函式將資料拷貝給臨時物件,並將臨時物件的地址用eax傳出
- 函式返回後,呼叫者將eax指向臨時物件的內容拷貝給區域性變數
- 返回值傳遞流程
- 需要注意的是返回物件的拷貝情況完全不具備可移植性,不同的編譯器產生的結果可能不同。函式傳遞大尺寸的返回值所使用的方法不是可移植的,不同編譯器、不同平臺、不同調用慣例、不同編譯引數可能採用不同的實現方法
- 在C++中要使用返回值優化技術(Return Value Optimization, RVO),直接將物件構造在臨時物件上,減少一次從函式內區域性變數對臨時物件的拷貝構造步驟
cpp_obj return_test() { return cpp_obj(); }
*碼字好累。。。→_→*