1 3 函式呼叫反彙編解析以及呼叫慣例案例分析

2022-02-17 18:25:17 字數 3778 閱讀 2661

首先來段**來瞧瞧:

#include int add(int x,int

y)int

main()

乙個簡單的函式呼叫,我們把main函式裡的r=add(3,4)反彙編:

可以看到,(這裡採用c預設的函式呼叫慣例,)首先進行引數壓棧,看清楚了,是把引數從右往左壓棧,然後call這個函式。跟蹤,call跟進去後,發現call指令執行後,esp暫存器減4,也就是說,有往棧裡壓了個引數--函式返回位址。看記憶體變化:

其實,call指令等價於兩步操作:

push 返回位址

jmp 函式入口位址

我們繼續跟進,看被調函式的反彙編**:

首先,再提醒一下,前面知道了,已經壓棧了三次,前兩次是壓棧形參,第三次是壓棧返回位址。看這裡的前兩句,又把ebp壓棧了,然後把esp賦值給了ebp。有沒有覺得奇怪呢?通常情況下,ebp都儲存基址,這裡也不例外,由於後面可能出現多次壓棧出棧操作,esp是變動的,需要乙個基址暫存器來加減偏移量去棧上的值,畢竟剛才也看到了,棧裡可是有不少重要的東東哦,通過基址加減偏移量就可以訪問了。於是,ebp就暫時擔待了這個重任。後面,esp做了個減法,為函式內部區域性變數等留下一定的棧空間,又壓棧了幾個暫存器,以備使用他們而不至於毀壞原有資料(後面再出棧就恢復了)。看核心**,z=x+y後面,把ebp+8位址的值賦給eax,思考一下,ebp+8是哪塊記憶體?回憶下前面棧裡都壓了什麼進去?ebp+0存的是ebp原有值,ebp+4存的是返回位址,ebp+8存的是最後乙個被壓的引數,ebp+0c存的是......所以這裡就是作加法,然後賦值給了ebp-4.這又是什麼呢?這是z的位址。在監視視窗裡可以看到z的位址就是如此。so,這裡可以得出乙個結論:ebp+x存的是返回位址、形參等;ebp-x存的是區域性變數。

現在看return z後,又把z的值賦給了暫存器eax,現在可以明白,函式返回值是借用暫存器來實現的。暫存器是個好東西,但是有乙個缺點,就是數量少。要是返回的資料比較多,比如結構體,怎麼辦?這個後面說。接著看圖,出棧,與前面乙個個對應,注意順序蛤。然後,ret,這什麼東東?經跟蹤發現,ret執行後,esp+4,也就是說,有資料出棧,細細看下,是返回位址。ret它給eip提供了返回位址,即等價於pop eip。完了嗎?沒有完。別忘了,棧上還有引數呢。先壓的是形參,現在還沒出呢。看圖:

ret後,即執行到call後面的一句。這裡esp+8,這是維持棧平衡,之前形參佔據的空間就釋放了,也稱之為呼叫方清棧。然後後面把暫存器儲存的返回值賦給r,也就是ebp-4,別忘了,r是main函式的區域性變數呢。

這裡順便提乙個彙編知識。看那個call語句,他的16進製制編碼與返回值**有何端倪?ffffff84<>00401005。這是因為call語句不是直接傳絕對位址,而是傳偏移量,計算下0040107c與ffffff84結合能不能得到00401005呢?得出是00401000。結論:偏移量=跳轉到的位址-call指令後一條指令的起始位址。

下面,我把返回值改為乙個結構體:

#include struct

node;

struct node f(struct

node t)

intmain()

進行反彙編,先看下main函式裡的情況:

看call語句之前都做了什麼?壓棧。這裡把esp賦給eax,然後傳值給eax+0/4/8等價於壓棧的push操作。注意乙個地方,就是push edx。他又壓棧了個引數,這是什麼?後面就知道了。看函式裡的反彙編**:

直接return t。看return後一句,把ebp+8上的值賦給eax,ebp+0是ebp原有值,ebp+4是函式返回位址,ebp+8是main裡面call之前那個push edx操作,so,這裡把那個edx賦給了eax。而後面,把ebp+0c/10/14上的內容都賦給了eax+0/4/8。最後又把ebp+8也就是edx那個位址賦給了暫存器。別忘了,這裡是儲存函式返回值的。前面說到,暫存器不夠用,那怎麼辦?這裡edx傳進來乙個位址,然後返回值都依次存進了這個位址,這代表什麼?緩衝地帶。既然暫存器使不動了,那就靠記憶體,這裡拿這個緩衝地帶來中轉資料,解決了返回值過多的問題。那麼,呼叫結束後,main函式一定會把這個緩衝地帶的內容取出來賦給某個變數的。我們來看看:

記好了,剛才緩衝地帶的位址放進了eax裡面,這樣它就可以用來做基址了。故而把eax+0/4/8賦給一塊記憶體,這裡可以看到,賦給了乙個匿名區域性變數,剛好與前面那個r的位址緊挨著呢。

故而總結下:c預設的呼叫慣例,函式返回值可以用暫存器或記憶體來儲存,選擇方式依賴於暫存器是否有能力完成任務。

關於函式的呼叫慣例,可以參考我的這篇博文:

#include typedef 

int (*func)(int,int

);func pfunc;

int _stdcall add(int a,int

b)void

test()

intmain()

這裡用的函式指標,有不熟悉的可以看我的這篇博文:

簡單說下原始碼,就是定義乙個函式指標pfunc,把add強轉賦給pfunc,分別執行pfunc(1,2)和add(1,2),看有沒有什麼不一樣的地方。add被_stdcall約束。執行一下會發現,pfunc(1,2)執行出現異常。什麼原因呢?反彙編一下就知道了。

對比一下,發現pfunc多了乙個出棧操作,而add沒有。那個cmp和call是異常處理,不用管,add也有,下面沒有截出來而已。還記得函式呼叫之後出棧操作是為了什麼來著?維持棧平衡。這裡我基本可以估摸出是棧出了問題。跟進去看看:

發現問題了嗎?pfunc執行的時候,函式裡出棧8位元組,外面main那裡又出棧8位元組,畫蛇添足的結果就是自取滅亡。這裡的端倪在於呼叫慣例不同。c預設呼叫慣例是_cdecl,呼叫方清棧,而這裡的_stdcall是被呼叫方清棧。pfunc使用的是_cdecl,卻執行_stdcall約束的函式,這可能不錯嗎?原始碼裡的強轉如果去掉,編譯器會報錯的,而強轉就是欺騙編譯器。有時候,欺騙別人,往往到最後,把自己也騙了。

讓我想起來之前在博問裡乙個博友遇到的問題:

const_cast實行強轉,成功地騙過了編譯器,但是程式設計師自己寫出了未定義行為。我還是直接把我的回覆截過來好了。

const物件是不允許修改的,而const_cast的存在是為了有些特殊情況需要表面去除const屬性,比如函式傳參,把const物件傳進非const屬性引數裡,表面修改屬性實則不改變其內容。而你這裡表面上是修改了,*x=3,這種修改const物件屬於c++標準裡未定義行為,針對這樣的,是由編譯器來自行處理的。你可以看到他們位址都相同,但卻值不一樣,這是編譯器的處理效果。我們需要做的是,避免程式設計裡出現這種未定義行為。
c/c++賦給了我們強大的權力,我們不要去胡作非為......

總結一下就是,強轉是很方便,但我們使用的時候,千萬要注意,使用得當。

反彙編 函式巢狀呼叫

實現的功能 兩層函式呼叫,外層函式的傳入的兩個引數再傳到子函式中相加,實現四個數相加返回 004010e8 6a 03 push 3 壓入3 004010ea 6a 02 push 2 壓入2 004010ec 6a 01 push 1 壓入1 004010ee e8 12ffffff call s...

C 虛函式呼叫的反彙編解析

虛函式的呼叫如何能實現其 虛 作為c 多型的表現手段,估計很多人對其實現機制感興趣。大約一般的教科書就說到這個c 強大機制的時候,就是教大家怎麼用,何時用,而不會去 一下這個虛函式的真正實現細節。當然,因為不同的編譯器廠家,可能對虛函式有自己的實現,呵呵,這就算是虛函式對於編譯器的 多型 了 作為編...

C 虛函式呼叫的反彙編解析

虛函式的呼叫如何能實現其 虛 作為c 多型的表現手段,估計很多人對其實現機制感興趣。大約一般的教科書就說到這個c 強大機制的時候,就是教大家怎麼用,何時用,而不會去 一下這個虛函式的真正實現細節。當然,因為不同的編譯器廠家,可能對虛函式有自己的實現,呵呵,這就算是虛函式對於編譯器的 多型 了 作為編...