從彙編視角解析函式呼叫中的堆疊運作

Test0ne發表於2024-09-28

引言
組合語言是計算機硬體操作的最直接表達方式,透過彙編程式碼可以深入理解計算機底層的工作機制。本文將以一個簡單的C語言程式碼為例,深入分析其對應的彙編程式碼中的堆疊變化,探討計算機在執行過程中如何透過堆疊來進行函式呼叫、引數傳遞和結果返回。

  1. C語言程式碼與彙編程式碼概述
    我們從如下簡單的C語言程式碼開始:
int g(int x) {
    return x + 2024;
}

int f(int x) {
    return g(x);
}

int main(void) {
    return f(2024) + 1;
}

上述程式碼透過函式g、f、main進行一系列呼叫。其彙編程式碼如下:

g:
.LFB0:
    pushl   %ebp
    movl    %esp, %ebp
    call    __x86.get_pc_thunk.ax
    addl    $_GLOBAL_OFFSET_TABLE_, %eax
    movl    8(%ebp), %eax
    addl    $2024, %eax
    popl    %ebp
    ret

f:
.LFB1:
    pushl   %ebp
    movl    %esp, %ebp
    call    __x86.get_pc_thunk.ax
    addl    $_GLOBAL_OFFSET_TABLE_, %eax
    pushl   8(%ebp)
    call    g
    addl    $4, %esp
    leave
    ret

main:
.LFB2:
    pushl   %ebp
    movl    %esp, %ebp
    call    __x86.get_pc_thunk.ax
    addl    $_GLOBAL_OFFSET_TABLE_, %eax
    pushl   $2024
    call    f
    addl    $4, %esp
    addl    $1, %eax
    leave
    ret

__x86.get_pc_thunk.ax:
.LFB3:
    movl    (%esp), %eax
    ret  
  1. 堆疊在函式呼叫中的變化分析
    2.1 函式 main 的堆疊變化
    main 函式是程式的入口點。彙編程式碼如下:
main:
.LFB2:
    pushl   %ebp
    movl    %esp, %ebp
    call    __x86.get_pc_thunk.ax
    addl    $_GLOBAL_OFFSET_TABLE_, %eax
    pushl   $2024
    call    f
    addl    $4, %esp
    addl    $1, %eax
    leave
    ret

pushl %ebp:將當前ebp壓入堆疊,儲存上一層的基址指標,以便稍後恢復。
堆疊內容:[舊ebp]
movl %esp, %ebp:將當前的棧頂指標esp賦值給ebp,建立新的棧幀。
堆疊內容:[舊ebp],此時ebp和esp指向同一位置。
pushl $2024:將常數2024壓入堆疊,作為傳遞給f函式的引數。
堆疊內容:[舊ebp] [2024]
call f:呼叫f函式,程式跳轉至f函式的入口地址。此時返回地址被壓入堆疊,以便在f函式執行完後能夠返回到main函式的下一條指令。
堆疊內容:[舊ebp] [2024] [返回地址]
2.2 函式 f 的堆疊變化
函式f的彙編程式碼如下:

f:
.LFB1:
    pushl   %ebp
    movl    %esp, %ebp
    call    __x86.get_pc_thunk.ax
    addl    $_GLOBAL_OFFSET_TABLE_, %eax
    pushl   8(%ebp)
    call    g
    addl    $4, %esp
    leave
    ret

pushl %ebp:儲存上一層的基址指標。
堆疊內容:[舊ebp] [2024] [返回地址] [舊ebp]
movl %esp, %ebp:建立新的棧幀。
堆疊內容:[舊ebp] [2024] [返回地址] [舊ebp],ebp和esp指向最後一箇舊ebp。
pushl 8(%ebp):將main函式中傳遞給f的引數(即2024)壓入堆疊,作為傳遞給g函式的引數。
堆疊內容:[舊ebp] [2024] [返回地址] [舊ebp] [2024]
call g:呼叫g函式,返回地址被壓入堆疊。
堆疊內容:[舊ebp] [2024] [返回地址] [舊ebp] [2024] [返回地址]

2.3 函式 g 的堆疊變化
函式g的彙編程式碼如下:

g:
.LFB0:
    pushl   %ebp
    movl    %esp, %ebp
    call    __x86.get_pc_thunk.ax
    addl    $_GLOBAL_OFFSET_TABLE_, %eax
    movl    8(%ebp), %eax
    addl    $2024, %eax
    popl    %ebp
    ret

pushl %ebp:儲存上一層的基址指標。
堆疊內容:[舊ebp] [2024] [返回地址] [舊ebp] [2024] [返回地址] [舊ebp]
movl %esp, %ebp:建立新的棧幀。
堆疊內容:[舊ebp] [2024] [返回地址] [舊ebp] [2024] [返回地址] [舊ebp]
movl 8(%ebp), %eax:從棧中取出2024並放入eax暫存器。
addl $2024, %eax:將2024加到eax暫存器中,結果為2024 + 2024 = 4048。
popl %ebp:恢復上一層的基址指標。
ret:從棧中彈出返回地址,並跳轉回到f函式中繼續執行。

3.總結:計算機是如何工作的
透過對彙編程式碼的深入分析,我們可以看到,堆疊在函式呼叫過程中起到了關鍵的作用。每當發生函式呼叫時,堆疊會儲存當前函式的執行狀態(如基址指標ebp和返回地址),併為被呼叫函式分配空間。函式呼叫結束後,透過恢復堆疊中的資料,程式能夠準確地回到上一次的執行狀態。

堆疊不僅用於函式呼叫和返回,它還用於引數傳遞和區域性變數的儲存。計算機的工作可以被理解為:透過指令和堆疊的相互作用來管理程式的執行流程,確保不同函式之間的資訊能夠無縫地傳遞和恢復。

總結來說,計算機的工作原理是透過CPU執行一條條指令,同時透過暫存器和堆疊維護程式的狀態。堆疊在這一過程中扮演了“資訊中轉站”的角色,確保程式能夠正確且有序地執行各個函式及其關聯操作。

相關文章