1. 程式人生 > 實用技巧 >函式呼叫過程與棧幀結構

函式呼叫過程與棧幀結構

程序記憶體佈局

32 位保護模式下 Linux 中程序的記憶體佈局如下:


    0xFFFFFFFF  ------> +----------------------+ <--+ 
                        |                      |    | 
                        |       OS Kernel      |    | 1GB
                        |                      |    |
    0xC0000000  ------> +----------------------+ <--+ 
                    |   |                      |    |
                    |   |         stack        |    |
                    v   |                      |    |
                        +----------------------+    |
                        |                      |    |
                        +----------------------+    |
                        | Memory Mapping Region|    |
    0x40000000  ------> +----------------------+    |
                        |                      |    |
                        +----------------------+    |
                    ^   |                      |    |
                    |   |         heap         |    | 3GB
                    |   |                      |    |
                        +----------------------+    |
                        |  Read/Write Segments |    |
                        |        .bss          |    |
                        |        .data         |    |
                        +----------------------+    |
                        |  Read-only Segments  |    |
                        | .init .text .rodata  |    |
                        |                      |    |
    0x08048000  ------> +----------------------+    |
                        |       reserved       |    |
    0x00000000  ------> +----------------------+ <--+

32 位下定址空間為 4GB,其中高地址的 1GB 是給作業系統核心使用的(Windows 中預設為 2GB),稱為核心空間(Kernel Space),剩餘的 3GB 分配給應用程式使用,稱為使用者空間(User Space)。當程序執行在核心空間時就處於核心態(Kernel Mode),當程序執行在使用者空間時就處於使用者態(User Mode)。

區分使用者態和核心態是為了提高系統的穩定性和安全性,使作業系統能夠控制資源的訪問,能夠防止應用程式執行一些危險的指令。計算機體系結構中,在硬體上提供不同的特權態,即 Rings Protection,如 Intel CPU 有 4 個特權級:Ring0、Ring1、Ring2、Ring3,一般作業系統只使用 Ring0 和 Ring3,Ring0 具有最高許可權,能夠訪問任何資源,Ring3 訪問受限,需要陷入(trap)到核心態才能訪問特權資源。

(stack)從虛擬地址 0xC0000000 往低地址增長。(heap)正好相反,從低地址往高地址增長。棧用於函式呼叫以及存放區域性變數等,堆用於動態記憶體分配,如 C 語言中的 malloc() 函式。棧與作業系統核心之間有一個隨機的 offset,堆與讀/寫段之間也有一個隨機的 offset。

記憶體對映段(Memory Mapping Segment)用於將檔案內容對映到記憶體,用於載入動態連結庫,可以通過 mmap() 系統呼叫實現記憶體對映。

bss 段放的是未初始化的全域性變數和靜態變數,預設都初始化為 0,data 段存放的是初始化的全域性變數和靜態變數。

text 段存放的是程式程式碼,除了可讀還具有可執行許可權。

棧及其操作

棧是一種先進後出的結構,包含兩種操作:pushpop

在 IA-32 體系結構中,通常用 ESPEBP 維護一個棧,EBP 指向棧底,ESP 指向棧頂。

IA-32 中 PUSH 指令先減少 ESP 的值,再將源運算元複製到堆中。

PUSH EAX 等價於

SUB ESP, 4
MOV [ESP], EAX

POP 指令正好相反,如 POP EBX 等價於

MOV EBX, [ESP]
ADD ESP, 4

棧幀

棧幀(stack frame)用於函式呼叫,每一次函式呼叫都會有一個獨立的棧幀,包含了返回地址、區域性變數、上下文等資訊。

                        
            high address        
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
               +------> |    previous EBP    |    |
               |        +--------------------+    |
               |        |  saved registers   |    |
               |        +--------------------+    |
               |        |  local variables   |    | caller's frame
               |        +--------------------+    |
               |        |   callee's args    |    |
               |        +--------------------+    |
               |        |   return address   |    |
               |        +--------------------+ <--+
               +------- |    previous EBP    |    |
                        +--------------------+    | 
                        |  local variables   |    |
                        +--------------------+    |
                        |                    |    | callee's frame
                        |                    |    |
                        |                    |    |
                        |                    |    |
                        |                    |    |
                        +--------------------+ <--+
            low address      


呼叫者(Caller) 呼叫 被呼叫者,每次呼叫都會有新的棧幀壓棧,所以一般深度優先搜尋可以用棧來代替遞迴,以達到更深的搜尋深度。

ESPEBP 只維護當前的棧幀,因此之前的棧幀都要儲存下來,棧幀的頂部儲存了上一個棧幀的 EBP 指向的位置,函式返回時 EBP 能夠恢復到上一個棧幀的 EBP

函式呼叫過程

呼叫過程

呼叫者 呼叫 被呼叫者 前,先儲存返回地址,即下一條指令的地址,用於返回後繼續執行,然後進入被呼叫者函式。

被呼叫者開闢了新的棧幀,因此需要儲存呼叫者的棧幀,通常使用以下兩條指令:

PUSH EBP
MOV EBP, ESP

先把舊的 EBP 入棧,然後讓 EBP 指向舊的 EBP,此時 EBP 已經作為新的棧幀的棧底了。

函式呼叫時,為了防止暫存器被覆蓋,有時需要將暫存器內容也儲存到棧中。

引數的傳遞

IA-32 下,在呼叫者函式中,引數從右往左入棧。進入被呼叫者函式時,引數以及區域性變數的訪問以 EBP 為基址,通過 EBP 加上偏移量訪問。

x86-64 下引數傳遞略有不同。如果函式呼叫引數少於 7 個,則用暫存器傳遞引數,引數從左到右依次放入暫存器 RDIRSIRDXRCXR8R9。引數超過 7 個時,前 6 個引數同樣通過暫存器傳參,之後的引數與 32 位一樣從右往左壓入棧中。

返回過程

返回值儲存在 EAX 暫存器中。

返回時先將 EBP 恢復至上一個棧幀的棧底,通常使用以下兩條指令:

MOV ESP, EBP
POP EBP

然後將儲存的返回地址放入 EIP 暫存器。

最後將儲存的引數出棧。

涉及的指令

  • CALL 指令

先將當前 EIP(即下一條指令的地址作為返回地址)壓入棧中,然後 EIP 轉移到被呼叫者的入口地址。

  • RET 指令

從棧頂彈出原來儲存的地址至 EIP。

  • LEAVE 指令

通常將返回時涉及的兩條指令用 LEAVE 指令代替,也就是說 LEAVE 等價於上述提到的兩條指令:

MOV ESP, EBP
POP EBP

函式的呼叫約定

上述引數從右往左入棧、返回值存入 EAX 暫存器等不是硬性規定的,而是遵守函式的呼叫約定(calling convention)。函式的呼叫約定規定了引數的傳遞順序、引數和返回值放置的位置、呼叫前後設定的工作由呼叫者完成還是被呼叫者完成等。上文提到的都是 C 呼叫約定(cdecl呼叫約定)。其他的呼叫約定有 stdcall呼叫約定、fastcall呼叫約定、thiscall呼叫約定等。

一個簡單的例子

以一個加法函式為例:

// file: add.c
#include <stdio.h>

int add(int a, int b) {
    int c = a + b;
    return c;
}

int main() {
    int a = 1, b = 2;
    int c = add(a, b);
    printf("%d\n", c);
    return 0;
}

編譯成 32 位程式:

gcc add.c -o add -m32

檢視彙編程式碼(彙編程式碼可以用 gdb、objdump、IDA 等工具檢視,也可以直接用 gcc 編譯成彙編程式碼)。

輸入 layout asm 檢視彙編程式碼:

進入 main() 函式後,先儲存上一個棧幀的 EBP(main 函式不是第一個被呼叫的函式)。

                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
        ESP,EBP ------> |    previous EBP    |    | main()'s frame
                        +--------------------+ <--+
            low address       

然後儲存暫存器。

                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
            EBP ------> |    previous EBP    |    |
                        +--------------------+    | main()'s frame
            ESP ------> |  saved registers   |    |
                        +--------------------+ <--+
            low address       

開闢一定的空間儲存區域性變數。

                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
            EBP ------> |    previous EBP    |    |
                        +--------------------+    |
                        |  saved registers   |    |
                        +--------------------+    |
                        |       ......       |    |
                        +--------------------+    | main()'s frame
                        |        b=2         |    |
                        +--------------------+    |
                        |        a=1         |    |
                        +--------------------+    |
            ESP ------> |                    |    |
                        +--------------------+ <--+
            low address       

壓入 add() 函式的引數。

                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
            EBP ------> |    previous EBP    |    |
                        +--------------------+    |
                        |  saved registers   |    |
                        +--------------------+    |
                        |       ......       |    |
                        +--------------------+    |
                        |        b=2         |    | main()'s frame
                        +--------------------+    |
                        |        a=1         |    |
                        +--------------------+    |
                        |         1          |    |
                        +--------------------+    |
            ESP ------> |         2          |    |
                        +--------------------+ <--+
            low address       

call add() 函式,先儲存下一條指令的地址,然後跳轉到 add() 函式。

                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
            EBP ------> |    previous EBP    |    |
                        +--------------------+    |
                        |  saved registers   |    |
                        +--------------------+    |
                        |       ......       |    |
                        +--------------------+    |
                        |        b=2         |    |
                        +--------------------+    | main()'s frame
                        |        a=1         |    |
                        +--------------------+    |
                        |         1          |    |
                        +--------------------+    |
                        |         2          |    |
                        +--------------------+    |
            ESP ------> |       0x122b       |    |
                        +--------------------+ <--+
            low address       

進入 add() 函式,先儲存 main() 函式的 EBP

                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
                        |    previous EBP    |    |
                        +--------------------+    |
                        |  saved registers   |    |
                        +--------------------+    |
                        |       ......       |    |
                        +--------------------+    |
                        |        b=2         |    |
                        +--------------------+    | main()'s frame
                        |        a=1         |    |
                        +--------------------+    |
                        |         1          |    |
                        +--------------------+    |
                        |         2          |    |
                        +--------------------+    |
                        |       0x122b       |    |
                        +--------------------+ <--+
        ESP,EBP ------> |    previous EBP    |    | add()'s frame
                        +--------------------+ <--+
            low address       

開闢空間用以儲存區域性變數。

                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
                        |    previous EBP    |    |
                        +--------------------+    |
                        |   saved registers  |    |
                        +--------------------+    |
                        |       ......       |    |
                        +--------------------+    |
                        |        b=2         |    |
                        +--------------------+    | main()'s frame
                        |        a=1         |    |
                        +--------------------+    |
                        |         1          |    |
                        +--------------------+    |
                        |         2          |    |
                        +--------------------+    |
                        |       0x122b       |    |
                        +--------------------+ <--+
            EBP ------> |    previous EBP    |    |
                        +--------------------+    |
                        |       ......       |    | add()'s frame
                        +--------------------+    |
            ESP ------> |                    |    |
                        +--------------------+ <--+
            low address       

通過 EBP 加上偏移獲取 add() 函式的兩個引數,然後求和,結果儲存在 EAX 暫存器(EAX 是累加暫存器)。

累加結果放入區域性變數 c。

                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
                        |    previous EBP    |    |
                        +--------------------+    |
                        |   saved registers  |    |
                        +--------------------+    |
                        |       ......       |    |
                        +--------------------+    |
                        |        b=2         |    |
                        +--------------------+    | main()'s frame
                        |        a=1         |    |
                        +--------------------+    |
                        |         1          |    |
                        +--------------------+    |
                        |         2          |    |
                        +--------------------+    |
                        |       0x122b       |    |
                        +--------------------+ <--+
            EBP ------> |    previous EBP    |    |
                        +--------------------+    |
                        |        c=3         |    |
                        +--------------------+    | add()'s frame
                        |       ......       |    |
                        +--------------------+    |
            ESP ------> |                    |    |
                        +--------------------+ <--+
            low address      

將 c 的值放入 EAX 暫存器作為返回值。

然後 add() 函式返回,先執行 LEAVE 指令,恢復 main() 函式的 EBP

MOV ESP, EBP
                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
                        |    previous EBP    |    |
                        +--------------------+    |
                        |  saved registers   |    |
                        +--------------------+    |
                        |       ......       |    |
                        +--------------------+    |
                        |        b=2         |    |
                        +--------------------+    | main()'s frame
                        |        a=1         |    |
                        +--------------------+    |
                        |         1          |    |
                        +--------------------+    |
                        |         2          |    |
                        +--------------------+    |
                        |       0x122b       |    |
                        +--------------------+ <--+
        ESP,EBP ------> |    previous EBP    |    |
                        +--------------------+    |
                        |        c=3         |    |
                        +--------------------+    | add()'s frame
                        |       ......       |    |
                        +--------------------+    |
                        |                    |    |
                        +--------------------+ <--+
            low address       

POP EBP
                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
            EBP ------> |    previous EBP    |    |
                        +--------------------+    |
                        |  saved registers   |    |
                        +--------------------+    |
                        |       ......       |    |
                        +--------------------+    |
                        |        b=2         |    |
                        +--------------------+    | main()'s frame
                        |        a=1         |    |
                        +--------------------+    |
                        |         1          |    |
                        +--------------------+    |
                        |         2          |    |
                        +--------------------+    |
            ESP ------> |       0x122b       |    |
                        +--------------------+ <--+
                        |    previous EBP    |    |
                        +--------------------+    |
                        |        c=3         |    |
                        +--------------------+    | add()'s frame
                        |       ......       |    |
                        +--------------------+    |
                        |                    |    |
                        +--------------------+ <--+
            low address        

然後執行 RET 指令,將返回地址賦給 EIP,從 add() 函式返回至 main() 函式,EIP 的值為 0x122b,即下一條指令為 main() 函式的 ADD ESP, 0x8

                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
            EBP ------> |    previous EBP    |    |
                        +--------------------+    |
                        |  saved registers   |    |
                        +--------------------+    |
                        |       ......       |    |
                        +--------------------+    |
                        |        b=2         |    | main()'s frame
                        +--------------------+    |
                        |        a=1         |    |
                        +--------------------+    |
                        |         1          |    |
                        +--------------------+    |
            ESP ------> |         2          |    |
                        +--------------------+ <--+
            low address       

執行 ADD ESP, 0x8,清除 add() 函式的兩個引數。

                        
            high address      
                        +--------------------+ <--+ 
                        |                    |    | 
                        |       ......       |    | previous frame
                        |                    |    |
                        +--------------------+ <--+ 
            EBP ------> |    previous EBP    |    |
                        +--------------------+    |
                        |   saved registers  |    |
                        +--------------------+    |
                        |       ......       |    | main()'s frame
                        +--------------------+    |
                        |        b=2         |    |
                        +--------------------+    |
            ESP ------> |        a=1         |    |
                        +--------------------+ <--+
            low address       

至此 add() 函式的呼叫完成,可以在 main() 函式繼續執行之後的指令了。

系統呼叫

應用程式無法呼叫核心函式,需要通過系統呼叫陷入到核心態,由作業系統執行。

系統呼叫通過 INT 0x80 中斷實現,不同的系統呼叫有不同的系統呼叫號,系統呼叫號需放在 EAX 暫存器中。

系統呼叫號在 /usr/include/asm/unistd.h 中定義。

檢視 i386 體系結構下的系統呼叫號:

陷入到核心態前需要保護現場,將暫存器、程式計數器壓入核心棧,然後呼叫相應的核心函式(系統呼叫)。核心函式執行完成後,將返回值放入 EAX 暫存器,然後恢復現場,將暫存器、程式計數器從核心棧恢復,
然後恢復到使用者態。保護現場和恢復現場均由中斷處理程式完成。

系統呼叫引數傳遞與函式呼叫引數傳遞略有不同,當系統呼叫引數不超過 6 個時,引數從左到右放到暫存器 EBXECXEDXESIEDIEBP 中,如果引數超過 6 個,所有引數應該放在一塊連續的記憶體區域裡(C 結構體),用暫存器 EBX 儲存指向該記憶體區域的指標。

應用程式一般通過 API 去完成系統呼叫。API 與 系統呼叫 不同,一個 API 可能呼叫多個系統呼叫,不同的 API 也可能呼叫同一個系統呼叫。

基本的流程可以概括為:

  • 應用程式呼叫 API
  • API 將系統呼叫號存入 EAX,然後通過中斷呼叫系統呼叫
  • 中斷處理程式儲存暫存器至核心棧,陷入核心態,根據系統呼叫號呼叫對應的核心函式
  • 核心函式完成工作,儲存返回值至 EAX
  • 中斷處理程式從核心棧恢復暫存器,恢復到使用者態,返回到 API
  • API 將 EAX 返回給應用程式

系統呼叫的跟蹤可用 strace 工具。

參考

CTF Linux pwn快速入門 - 嗶哩嗶哩

Linux 核心分析 - 網易雲課堂

《Operating Systems: Three Easy Pieces》