【PHP7源碼學習】2019-04-12 C語言函數調用的壓棧

baiyansegmentfault

所有視頻:https://segmentfault.com/a/11...sass

引入

  • 咱們知道,在函數調用的過程當中,須要先進行壓棧,待函數運行結束後,再出棧,回到原始函數的調用位置處繼續向下執行代碼。那麼咱們舉一個例子,來清晰地看一下C語言函數調用壓棧的過程:
int bar(int c, int d){
    int e = c + d;
    return e;
}

int foo(int a, int b){
    return bar(a, b);
}

int main(void){
    foo(2, 5);
    return 0;
}
  • 在具體分析以前,咱們首先須要瞭解一下寄存器的基本概念:

寄存器

  • 寄存器就是CPU上的一塊存儲區域,存取速度比普通存儲器高好幾個數量級。爲了提高程序運行的效率,程序運行期間產生的數據每每會存到寄存器中。
  • 寄存器的分類有多種:數據寄存器、變址寄存器、指針寄存器、段寄存器、指令指針寄存器、標誌寄存器等,用於存儲不一樣類型的數據,下面咱們逐個介紹:

數據寄存器

  • 數據寄存器主要用來保存操做數和運算結果等信息,從而節省讀取操做數所需佔用總線和訪問存儲器的時間。RAX、RBX、RCX、RDX和EAX、EBX、ECX、EDX以及AX、BX、CX、DX分別稱爲64位、32位、16位數據寄存器(通用寄存器)。

變址寄存器

  • 變址寄存器主要用於存放存儲單元在段內的偏移量,用它們可實現多種存儲器操做數的尋址方式,爲以不一樣的地址形式訪問存儲單元提供方便。 寄存器RSI、RDI和ESI、EDI和SI、DI分別稱爲64位、32位、16位變址寄存器(Index Register)。

指針寄存器

  • 指針寄存器主要用於存放堆棧內存的地址,用它們可實現多種存儲器操做數的尋址方式,爲以不一樣的地址形式訪問存儲單元提供方便。 寄存器RBP、RSP和EBP、ESP和BP、SP稱分別爲64位、32位、16位指針寄存器(PointerRegister),它可分爲兩類:

(1)BP爲基指針(BasePointer)寄存器,指向棧底,用它可直接存取堆棧中的數據;
(2)SP爲堆棧指針(StackPointer)寄存器,用它只可訪問棧頂函數

段寄存器

  • 段寄存器是根據內存分段的管理模式而設置的。內存單元的物理地址由段寄存器的值和一個偏移量值組合而成的,這樣可用兩個較少位數的值組合成一個可訪問較大物理空間的內存地址,CS、DS、ES、SS、FS、GS。

指令指針寄存器

  • 指令指針寄存器是存放下一次將要執行的指令在代碼段的偏移量。在具備預取指令功能的系統中,下次要執行的指令一般已被預取到指令隊列中,除非發生轉移狀況。因此,在理解它們的功能時,不考慮存在指令隊列的狀況。 RIP、EIP、IP(Instruction Pointer)分別爲64位、32位、16位指令指針寄存器。spa

    • 咱們重點關注這兩個個寄存器:RBP(指向棧底)/RSP(指向棧頂)。

使用gdb查看C函數調用的壓棧過程

  • 下面咱們使用gdb的反彙編(disassemble)命令,來查看函數執行的棧楨狀況:

  • 咱們觀察紅框中的部分,當前正在執行main函數,尚未進行函數foo的調用,在第10行代碼下的兩條指令尚未執行以前,當前main函數的執行棧楨的狀況以下:

  • 寄存器RBP(%rbp)的值指向棧底,寄存器RSP(%rsp)的值指向棧頂,當前沒有任何其它函數的入棧。
  • 執行push %rbp指令:將RBP寄存器的值入棧,是爲了保存調用者(caller)的地址,這樣在後面執行完調用函數以後,才能正確返回。執行push指令後,棧頂指針須要隨之移動,執行後的棧楨結構以下:

  • 執行mov %rsp,%rbp指令:將寄存器RSP的值賦到寄存器RBP中,執行後的棧楨結構以下:

  • 接下來gdb圖中的第11行代碼,會首先使用變址寄存器ESI和EDI保存函數調用的參數值2和5。由於這裏的參數要傳遞給foo函數供foo函數使用,因此才須要在這裏進行暫存。而後,使用callq指令真正地進行函數調用。咱們使用gdb的s命令進入foo函數的執行棧楨

  • 觀察第6行代碼的前兩個彙編指令,和以前的入棧操做一摸同樣,咱們直接畫圖:

  • 接下來觀察第3條彙編指令:sub $0x8, %rsp ,它表示用RSP寄存器的值減去0x8,而後把結果賦值給RSP寄存器。因爲棧的生長方向是從高地址到低地址,因此須要作減法,從而空出一段內存空間,作完sub操做的棧楨以下:

  • 接下來觀察第四、5條彙編指令,他們將EDI和ESI變址寄存器中的值2和5,拷貝到以rbp指針爲起始位置,並偏移-0x4與-0x8地址的位置。那麼,爲何要從寄存器拷貝到函數foo的執行棧楨上呢?由於只有這樣,在函數內部才能更加方便地使用這兩個變量:

  • 接下來咱們看上圖gdb的第7行代碼,即return bar(a, b)的代碼,它也是一個函數調用。一樣,傳入的參數也是2和5,這兩個參數也須要暫存起來,待後續傳遞給bar函數的棧楨,供bar函數內部使用。在上述gdb圖片中,首先是兩個mov指令,將foo函數棧楨上的值2和5拷貝到EDX和EAX寄存器中,而後再拷貝到以前咱們熟悉的ESI和EDI寄存器中得以暫存,以便後續再拷貝到bar函數的棧楨上得以使用。到這裏foo函數就執行完畢了,接下來會執行callq指令執行下一個函數bar的調用,進入bar函數執行棧楨的部分:

  • 咱們看第1行代碼的前兩行,咱們很是熟悉,就是一個入棧的操做。而後後面兩行將以前存儲在EDI寄存器中存儲的數值2和ESI寄存器中存儲的數值5一塊兒拷貝到bar函數的棧楨上,即rbp偏移量-0x14與-0x18的地址處(注意這裏0x14爲十進制的20,0x18爲24),咱們能夠畫出當前的棧楨結構:

  • 繼續執行第2行代碼,int e = c + d。前兩行將棧楨上的數值2和5拷貝到數據寄存器上,準備進行運算。第三行真正進行加法運算,其結果會存儲到EAX寄存器中,第四行將EAX寄存器中的數據存儲到棧楨偏移rbp-0x4的地址處。在return以後,因爲當前bar函數的調用棧已經被銷燬,因此還會再將這個運算結果7拷貝回EAX寄存器,等待外層調用接收該返回值以及進行後續使用。此時注意,bar函數的局部變量已失去保存的必要,因此這裏僅僅保存一個加法運算後的結果,是由於外層foo函數有可能還須要使用這個結果。而後,咱們注意到它的末尾有一個pop指令。因爲當前函數bar是最後一個被調用的函數,因此要將bar函數出棧。當前棧楨結構以下:

  • bar函數出棧完畢以後,咱們就返回到了foo函數的調用棧中:

  • 注意左側箭頭的指向,當前執行到leaveq指令,這個leaveq指令也是一個出棧指令,繼續將foo函數出棧,以此類推,直到最外層main函數也出棧,程序運行結束。這裏出棧的棧楨就不畫圖贅述了,相信你們看到這裏都可以理解。
  • 注意咱們在本身gdb的時候,須要重點關注rbp和rsp這兩個指針寄存器的值所指向的內存地址,以及函數的參數及返回值是如何在函數之間的調用過程當中,順利傳遞的。
  • 在視頻中還提到了PHP遞歸壓棧的過程,限於篇幅,在此再也不一一列出,有興趣的同窗能夠參照視頻中的步驟gdb一下。
相關文章
相關標籤/搜索