1. 程式人生 > >連結、裝載與庫——程序的棧

連結、裝載與庫——程序的棧

記憶體是承載程式的介質,是程式進行運算和表達的場所。

未有特殊說明,則預設在32bit作業系統中。

1. 程式的記憶體佈局

作業系統會將記憶體空間中的一部分分給核心使用,應用程式無法訪問這段記憶體,這段記憶體被稱為核心空間。Windows預設情況將高地址的2GB空間分配給核心,Linux預設情況將高地址的1GB空間分配給核心。
剩下的記憶體空間稱為使用者空間,使用者空間中有許多預設區域。

  • :棧用於維護函式呼叫的上下文。棧通常在使用者空間的最高地址處分配,一般大小位數兆位元組

  • :堆用來容納應用程式動態分配的記憶體區域。堆通常在棧的下方(低地址方向)。堆一般比棧大可以有幾十至數百兆位元組的容量

  • 可執行檔案映像:儲存著可執行檔案在記憶體裡的映像。

  • 保留區:保留區不是一個單一的記憶體區域,而是對記憶體中受到保護而禁止訪問的記憶體區域的總稱

  • Linux地址空間佈局
  • 棧向低地址增長,堆向高地址增長。當棧或堆現有的大小不夠用時,它們將按照增長方向擴大,直到預留的的空間被用完為止

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函式反彙編
      main反彙編

    • foo函式反彙編
      foo反彙編

    • foo函式return之前時暫存器
      foo函式返回之前時暫存器

    • foo函式return之前時記憶體
      foo函式返回之前時記憶體

    • foo函式反彙編程式碼解析
      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函式返回值反匯程式碼
        main返回值反彙編
      • return_test函式反彙編程式碼
        return_test反彙編
      • main函式中n的地址
        main中n的地址
      • 臨時物件給main函式中的n賦值
        臨時物件給n賦值
    • 首先呼叫者在棧上將一部分空間作為傳遞返回值的臨時物件
    • 將臨時物件的地址作為隱藏引數傳遞給函式
    • 函式將資料拷貝給臨時物件,並將臨時物件的地址用eax傳出
    • 函式返回後,呼叫者將eax指向臨時物件的內容拷貝給區域性變數
    • 返回值傳遞流程
      返回值傳遞流程
    • 需要注意的是返回物件的拷貝情況完全不具備可移植性,不同的編譯器產生的結果可能不同。函式傳遞大尺寸的返回值所使用的方法不是可移植的,不同編譯器、不同平臺、不同調用慣例、不同編譯引數可能採用不同的實現方法
    • 在C++中要使用返回值優化技術(Return Value Optimization, RVO),直接將物件構造在臨時物件上,減少一次從函式內區域性變數對臨時物件的拷貝構造步驟
      cpp_obj return_test()
      {
          return cpp_obj();
      }

    *碼字好累。。。→_→*