包含呼叫函式的範例

我們來看一個比較複雜的範例,看看有呼叫函式的程式會被編成什麼樣的組合語言指令。

呼叫函式和一般的跳躍不同,在呼叫結束後必須回到原本呼叫的地方,原本執行中的位址被叫做「回傳位址」(return address)。如果說呼叫只會發生一次的話,隨便找一個暫存器存回傳位址就好了;但是函式呼叫可以一層一層呼叫下去,所以必須把回傳位址存在記憶體裡。實務上,回傳位址被存在記憶體中的堆疊(stack)裡。

堆疊,被實作成只能使用堆疊空間最上方位址所存的一個變數。而這個紀錄堆疊最上方的紀錄空間被稱為「堆疊指標」(stack pointer)。x86-64 中,為了方便寫呼叫函式的程式,提供了堆疊指標專用的暫存器,和使用這個暫存器的指令。往堆疊上堆資料的操作是「push」,而取出堆疊資料的操作是「pop」。

接下來我們來看看操作堆疊的範例。試想以下的C程式碼:

int plus(int x, int y) {
  return x + y;
}

int main() {
  return plus(3, 4);
}

而底下是與這個C程式碼對應的組合語言指令:

.intel_syntax noprefix
.global plus, main

plus:
        add rsi, rdi
        mov rax, rsi
        ret

main:
        mov rdi, 3
        mov rsi, 4
        call plus
        ret

第一行是指定組合語言文法的指令。由.global開始的第二行,是指定mainplus這兩個可見於程式全體(program scope),非檔案範圍(file scope)的函式的組合語言指令。(譯註:有關 program scope 和 file scope,請參考C語言的相關資料。)現階段可以暫時忽略沒有關係。

首先來看main。C程式看起來就是在main呼叫了plus函式;在組合語言部分,默認第一引數(argument)放在 RDI 暫存器、第二引數放在 RSI 暫存器,main的最初兩行便是設定這兩個暫存器的值。

call就是呼叫函式的指令。具體來說call做了下列兩件事情:

  • call的下一行指令的位址 push 進堆疊中

  • 跳到call的引數所給的位址

於是,call實行時,CPU 便會開始執行plus

接下來看plusplus函式包含有三個指令。

add是加法的指令。在這裡是把 RSI 和 RDI 兩個暫存器內的值相加,再寫回 RSI 暫存器中。x86-64 的整數運算指令通常都只接受兩個暫存器,所以必須要把運算結果寫回其中一個暫存器中。

函式的回傳值則被寫進 RAX 暫存器。由於我們想要的是加法的結果,所以必須把 RSI 暫存器的值複製到 RAX 暫存器,這邊用來複製的指令是mov指令。mov是 move 的省略,但實際上並不是移動而是複製資料的指令。

plus函式的最後,執行ret指令從函式中回傳。ret指令具體來說做的是下列兩件事:

  • 從堆疊中 pop 一個位址出來

  • 跳到該位址

也就是說,ret指令是跳回執行call的地方,並從該處開始繼續執行的指令。callret是成對設計的指令。

plus傳回之後就是main裡的ret指令。從C程式看,main是把plus的回傳值直接傳回去。在這裡 RAX 裡放的是plus的回傳值,所以這裡main直接回傳,RAX 裡的值也就成了main的回傳值了。

Last updated