日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

压栈, 跳转,执行,返回:从汇编看函数调用

發布時間:2024/7/23 编程问答 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 压栈, 跳转,执行,返回:从汇编看函数调用 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

?

From:https://www.jianshu.com/p/594357dff57e

C函數調用過程原理及函數棧幀分析:https://blog.csdn.net/zsy2020314/article/details/9429707

?

?

從本篇開始,我們討論一些高級語言中的基礎設施:堆棧,函數調用,變量生命周期等等話題。因為這里本身會涉及到比較多的匯編層面的基礎概念。為了向大家說明匯編層的函數調用實現細節,無奈我只能羅列出很多匯編上的概念,因為本文假定讀者不需要具有任何匯編知識。我討厭長篇大論,但本篇的解釋可能仍然不夠明晰。在此為自己知識的淺薄表示歉意。

?

?

1. 從代碼的順序執行說起

?

每一個程序員腦子里應該都有這么一種印象:“程序是順序執行的”。這個觀點其實和我們開篇所講的cpu的流水線執行過程直接相關。
讓我們再回憶一下腦海中關于函數調用的概念,也許會是這個樣子:

這里的“控制流轉移”又是如何發生的呢?在解釋這個之前,也許我們需要科普一點有關于匯編的知識。

?

?

?

2. 函數調用中的一些細節說明

?

2.1 函數調用中的關鍵寄存器

?

2.1.1 程序計數器PC

程序計數器是一個計算機組成原理中講過的概念,下面給出一個百度百科中的簡單解釋

程序計數器是用于存放下一條指令所在單元的地址的地方。
當執行一條指令時,首先需要根據PC中存放的指令地址,將指令由內存取到指令寄存器中,此過程稱為“取指令”。與此同時,PC中的地址或自動加1或由轉移指針給出下一條指令的地址。此后經過分析指令,執行指令。完成第一條指令的執行,而后根據PC取出第二條指令的地址,如此循環,執行每一條指令。

可以看到,程序計數器是一個cpu執行指令代碼過程中的關鍵寄存器:它指向了當前計算機要執行的指令地址,CPU總是從程序計數器取出當前指令來執行。當指令執行后,程序計數器的值自動增加,指向下一條將要執行的指令。

在x86匯編中,執行程序計數器功能的寄存器被叫做EIP,也叫作指令指針寄存器。

?

2.1.2 基址指針,棧指針和程序棧

棧是程序設計中的一種經典數據結構,每個程序都擁有自己的程序棧。很重要的一點是,棧是向下生長的。所謂向下生長是指從內存高地址->低地址的路徑延伸,那么就很明顯了,棧有棧底和棧頂,那么棧頂的地址要比棧底低。對x86體系的CPU而言,其中
---> 寄存器ebp(base pointer )可稱為“幀指針”或“基址指針”,其實語意是相同的。
---> 寄存器esp(stack pointer)可稱為“ 棧指針”。
在C和C++語言中,臨時變量分配在棧中,臨時變量擁有函數級的生命周期,即“在當前函數中有效,在函數外無效”。這種現象就是函數調用過程中的參數壓棧,堆棧平衡所帶來的。對于這種實現的細節,我們會在接下來的環節中詳細討論。

?

2.2. 堆棧平衡

堆棧平衡這個概念指的是函數調完成后,要返還所有使用過的棧空間。這種說法可能有點抽象,我們可以舉一個簡單的例子來類比:
我們都知道函數的臨時變量存放在棧中。那我們來看下面的代碼,它是一個很簡單的函數,用來交換傳入的2個參數的值:

void __stdcall swap(int& a,int& b) {int c = a;a = b;b = c; }

我們可以看到,在這個函數中使用了一個臨時變量int c;這個變量分配在棧中,我們可以簡單的理解為,在聲明臨時變量c后,我們就向當前的程序棧中壓入了一個int值:

int c = a; <==> push(a); //簡單粗暴,臨時變量的聲明理解為簡單地向棧中push一個值。

那現在這個函數swap調用結束了,我們是否需要退棧,把之前臨時變量c使用的棧空間返還回去?需要嗎?不需要嗎?
我們假設不需要,當我們頻繁調用swap的時候,會發生什么?每次調用,程序棧都在生長。直到棧滿,我們就會收到stack overflow錯誤,程序掛掉了。
所以為了避免這種烏龍的事情發生,我們需要在函數調用結束后,退棧,把堆棧還原到函數調用前的狀態,這些被pop掉的臨時變量,自然也就失效了,這也解釋了我們一直以來關于臨時變量僅在當前函數內有效的認知。其實堆棧平衡這個概念本身比這種粗淺的理解要復雜的多,還應包括壓棧參數的平衡,暫時我們可以簡單地這樣理解,后面再做詳細說明。

?

2.3. 函數的參數傳遞和調用約定

函數的參數傳遞是一個參數壓棧的過程。函數的所有參數,都會依次被push到棧中。那調用約定有是什么呢?
C和C++程序員應該對所謂的調用約定有一定的印象,就像下面這種代碼:

void __stdcall add(int a,int b);

函數聲明中的__stdcall就是關于調用約定的聲明。其中標準C函數的默認調用約定是__stdcall,C++全局函數和靜態成員函數的默認調用約定是__cdecl,類的成員函數的調用約定是__thiscall。剩下的還有__fastcall,__naked等。

為什么要用所謂的調用約定?調用約定其實是一種約定方式,它指明了函數調用中的參數傳遞方式和堆棧平衡方式。

?

2.3.1 參數傳遞方式

還是之前那個例子,swap函數有2個參數,int a,int b。這兩個參數,入棧的順序誰先誰后?
其實是從左到右入棧還是從右到左入棧都可以,只要函數調用者和函數內部使用相同的順序存取參數即可。在上述的所有調用約定中,參數總是從右到左壓棧,也就是最后一個參數先入棧。我們可以使用一份偽代碼描述這個過程

push b; //先壓入參數b push a; //再壓入參數a call swap; //調用swap函數

其實從這里我們就可以理解為什么在函數內部,不能改變函數外部參數的值:因為函數內部訪問到的參數其實是壓入棧的變量值,對它的修改只是修改了棧中的"副本"。指針和引用參數才能真正地改變外部變量的值。

?

2.3.2 堆棧平衡方式

因為函數調用過程中,參數需要壓棧,所以在函數調用結束后,用于函數調用的壓棧參數也需要退棧。那這個工作是交給調用者完成,還是在函數內部自己完成?其實兩種都可以。調用者負責平衡堆棧的主要好處是可以實現可變參數(關于可變參數的話題,在此不做過多討論。如果可能的話,我們可以以一篇單獨的文章來講這個問題),因為在參數可變的情況下,只有調用者才知道具體的壓棧參數有幾個。
下面列出了常見調用約定的堆棧平衡方式:

調用約定堆棧平衡方式
__stdcall函數自己平衡
__cdecl調用者負責平衡
__thiscall調用者負責平衡
__fastcall調用者負責平衡
__naked編譯器不負責平衡,由編寫者自己負責

2.4. 棧幀的概念:從esp和ebp說起

為什么我們需要ebp和esp2個寄存器來訪問棧?這種觀念其實來自于函數的層級調用:函數A調用函數B,函數B調用函數C,函數C調用函數D...
這種調用可能會涉及非常多的層次。編譯器需要保證在這種復雜的嵌套調用中,能夠正確地處理每個函數調用的堆棧平衡。所以我們引入了2個寄存器:

  • 1. ebp指向了本次函數調用開始時的棧頂指針,它也是本次函數調用時的“棧底”(這里的意思是,在一次函數調用中,ebp向下是函數的臨時變量使用的空間)。在函數調用開始時,我們會使用?mov ebp,esp 把當前的esp保存在ebp中。
  • 2.?esp,它指向當前的棧頂,它是動態變化的,隨著我們申請更多的臨時變量,esp值不斷減小(正如前文所說,棧是向下生長的)。
  • 3.?函數調用結束,我們使用?mov esp,ebp?來還原之前保存的esp。

在函數調用過程中,ebp和esp之間的空間被稱為本次函數調用的“棧幀”。函數調用結束后,處于棧幀之前的所有內容都是本次函數調用過程中分配的臨時變量,都需要被“返還”。這樣在概念上,給了函數調用一個更明顯的分界。下圖是一個程序運行的某一時刻的棧幀圖:

?

?

?

3. 匯編中關于“函數調用”的實現

?

上面鋪陳了很多的匯編層面的概念后,我們終于可以切回到我們本次的主題:函數調用
函數調用其實可以看做4個過程,也就是本篇標題:

  • 壓棧: 函數參數壓棧,返回地址壓棧
  • 跳轉: 跳轉到函數所在代碼處執行
  • 執行: 執行函數代碼
  • 返回: 平衡堆棧,找出之前的返回地址,跳轉回之前的調用點之后,完成函數調用
  • ?

    1. call指令 壓棧和跳轉

    下面我們看一下函數調用指令

    0x210000 call swap; 0x210005 mov ecx,eax;

    我們可以把它理解為2個指令:

    push 0x210005; jmp swap;

    ?

    也就是,首先把call指令的下一條指令地址作為本次函數調用的返回地址壓棧,然后使用jmp指令修改指令指針寄存器EIP,使cpu執行swap函數的指令代碼。

    ?

    ?

    2. ret指令 返回

    匯編中有ret相關的指令,它表示取出當前棧頂值,作為返回地址,并將指令指針寄存器EIP修改為該值,實現函數返回。
    下面給出一組示意圖來演示函數的返回過程:

    ?

    1. 當前EIP的值為0x210004,指向指令ret 4,程序需要返回

    2. 執行ret指令,將當前esp指向的堆棧值當做返回地址,設置eip跳轉到此處并彈出該值

    經過這兩步,函數就返回到了調用處。

    ?

    ?

    ?

    4. 從實際匯編代碼看函數調用

    ?

    4.1 程序源碼和運行結果

    源碼:

    main.cpp#include <stdio.h>void __stdcall swap(int& a, int& b);int main(int argc, char* argv) {int a = 1, b = 2;printf("before swap: a = %d, b = %d\r\n", a, b);swap(a, b);printf("after swap: a = %d, b = %d\r\n", a, b); }void __stdcall swap(int& a, int& b) {int c = a;a = b;b = c; }

    程序運行結果:

    ?

    ?

    4.2 反匯編

    可以看到,在函數調用前,函數參數已被壓棧,此時:
    EBP = 00AFFCAC
    ESP = 00AFFBBC
    EIP = 00BF1853
    我們按F11,進入函數內部,此時:

    其實就是call swap指令的下一條指令地址,它就是本次函數調用的返回地址。

    下面是一個swap函數的詳細注釋:

    當程序運行到 ret 8時

    執行返回后:

    在返回前,ESP = 00AFFBB8,返回后 ESP = 00AFFBC4
    0x00AFFBC4 - 0x00AFFBB8 = 0xC
    這里的數值是字節數,而我們知道,int是4字節長度。所以0xC/4 = 3
    正好是2個壓棧參數+一個返回地址。

    ?

    ?

    4.3 調用堆棧

    調試程序的時候,我們經常關注的一個點就是VisualStudio顯示給我們的“調用堆?!惫δ?#xff0c;這次讓我們來仔細看一下它:
    我們重新執行一次程序,這次我們關注一下vs顯示的調用堆棧,如下圖

    第一行是當前指令地址
    第二行是外層調用者,我們雙擊它,跳轉到如下地址:

    也許這也是為什么這個功能被叫做“調用堆棧”的原因:它正是通過對程序棧的分析實現的。

    ?

    ?

    ?

    ?

    ?

    總結

    以上是生活随笔為你收集整理的压栈, 跳转,执行,返回:从汇编看函数调用的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。