包含呼叫函式的範例
我們來看一個比較複雜的範例,看看有呼叫函式的程式會被編成什麼樣的組合語言指令。
呼叫函式和一般的跳躍不同,在呼叫結束後必須回到原本呼叫的地方,原本執行中的位址被叫做「回傳位址」(return address)。如果說呼叫只會發生一次的話,隨便找一個暫存器存回傳位址就好了;但是函式呼叫可以一層一層呼叫下去,所以必須把回傳位址存在記憶體裡。實務上,回傳位址被存在記憶體中的堆疊(stack)裡。
堆疊,被實作成只能使用堆疊空間最上方位址所存的一個變數。而這個紀錄堆疊最上方的紀錄空間被稱為「堆疊指標」(stack pointer)。x86-64 中,為了方便寫呼叫函式的程式,提供了堆疊指標專用的暫存器,和使用這個暫存器的指令。往堆疊上堆資料的操作是「push」,而取出堆疊資料的操作是「pop」。
接下來我們來看看操作堆疊的範例。試想以下的C程式碼:
而底下是與這個C程式碼對應的組合語言指令:
第一行是指定組合語言文法的指令。由.global
開始的第二行,是指定main
和plus
這兩個可見於程式全體(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
。
接下來看plus
。plus
函式包含有三個指令。
add
是加法的指令。在這裡是把 RSI 和 RDI 兩個暫存器內的值相加,再寫回 RSI 暫存器中。x86-64 的整數運算指令通常都只接受兩個暫存器,所以必須要把運算結果寫回其中一個暫存器中。
函式的回傳值則被寫進 RAX 暫存器。由於我們想要的是加法的結果,所以必須把 RSI 暫存器的值複製到 RAX 暫存器,這邊用來複製的指令是mov
指令。mov
是 move 的省略,但實際上並不是移動而是複製資料的指令。
plus
函式的最後,執行ret
指令從函式中回傳。ret
指令具體來說做的是下列兩件事:
從堆疊中 pop 一個位址出來
跳到該位址
也就是說,ret
指令是跳回執行call
的地方,並從該處開始繼續執行的指令。call
和ret
是成對設計的指令。
從plus
傳回之後就是main
裡的ret
指令。從C程式看,main
是把plus
的回傳值直接傳回去。在這裡 RAX 裡放的是plus
的回傳值,所以這裡main
直接回傳,RAX 裡的值也就成了main的回傳值了。
Last updated