彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”

Gcow安全團隊發表於2020-02-04

作為剛往底層方向走的一隻菜鳥,今天為各位分享一篇名為彙編眼中的函式呼叫引數傳遞以及全域性、區域性變數與“基址”,好了,廢話不多說,先來看看C語言程式碼:

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

本次的分享主要以畫堆疊圖為主,透過畫圖的方式來看看這段程式碼是如何運作的

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

我們先寫一句彙編程式碼,mov eax,eax其實這句程式碼並沒有什麼用,也就是將eax的值移入eax中,這句程式碼對於我們的作用僅作為斷點,我們F5執行下程式並且切換到反編譯介面

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

右鍵之後點選Go To Disassemble,也就是進入反編譯介面,我們來看看反編譯的程式碼

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

這就是我們們main函式反編譯後的結果,那麼現在我們記錄一下ebp,esp的值,並且畫出現在的堆疊圖

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

ESP = 0019FEF4

EBP = 0019FF40

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”

黃色=ebp~esp初始的記憶體

那好,我們繼續來看程式碼

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

這裡是我們自己寫的彙編程式碼,編譯器也沒有改動過可以說是本色出演,這裡記憶體沒有變化,好,那麼我們接著來看add函式這裡的呼叫

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

可以看到函式呼叫的時候首先是將3、1這兩個值推入棧中,但是又有疑問了“add函式的呼叫是這樣的add(1,3),為啥首先推入棧中的是3而不是1呢?”之所以這樣是因為棧中是先進後出的,所以引數進入棧中的順序是從右向左的,當然這裡也可以看到函式中的引數是壓入棧中然後取出來而不是透過通用暫存器eax,edx,ebx這些來傳送引數的,其實這也好理解,因為通用暫存器只有八個,像esp,eip,ebp這樣的暫存器還不能隨便改,能用的也只有剩下的幾個,引數超過剩下的幾個咋辦?那就只能用堆疊了。我們繼續來看F10單步執行一下,看看堆疊的變化

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

我們再看看EBP和ESP

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

這裡可以看到壓入一個3後棧頂指標減去了4h,至於為啥減去4h呢?是因為一個int型別的資料寬度是4Byte=32Bit,能存入的最大數也就是0xFFFFFFFF,16進位制數又是2進位制數的簡寫形式,一個二進位制數需要4Bit來儲存,所以4位二進位制數最大的值為1111轉換為16進位制後剛好為F,這樣也方便了開發者,儲存空間中我們看到的資料都是16進位制數

十六進位制 二進位制

0h 0b

1h 1b

2h 10b

3h 11b

4h 100b

5h 101b

6h 110b

7h 111b

8h 1000b

9h 1001b

Ah 1010b

Bh 1011b

Ch 1100b

Dh 1101b

Eh 1110b

Fh 1111b

可能以上講得有些出入,如有錯誤,請幫忙糾正,好了,我們接著來看,接下來push 1進去,我們來看看現在的堆疊圖:

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”

繼續看下面的程式碼:

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

接著是call,call指令在彙編中多用於函式呼叫,call指令做了兩個操作,

1、Push 00401103(下一行程式碼地址)

2、Jmp 0040100A(函式地址)

這裡我們F11一下,遇到call指令後按F11進入函式即可,這樣我們就可以看到函式體中的指令

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

這裡CALL之後看看堆疊中的變化:

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”

繼續向下走,F11後跳轉到的結果如下

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

這裡是編譯器決定的,不是所有編譯器call後會進入一個jmp指令中轉,再F11一下

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

Jmp執行之後直接進入了函式體,這裡將通用暫存器ebp的值存入棧中,之所以存入棧中是因為每一個函式中都要使用ebp來定址,所以需要將ebp的原始值存入棧中,隨後將esp棧頂指標的值移入ebp中,sub esp,40h是將棧頂指標加到40h這個位置,之所以是減40h是因為棧空間是從高到低的,現在我們單步執行一下看看棧中的變化

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”


 

這裡40h移動的位置=40h/4h=10h=16,我們看看現在堆疊的變化

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”

Sub esp,40h這段程式碼我們是為函式開闢一塊棧空間出來供函式存取值的,也就是我們常說的緩衝區,通常用來儲存函式中的區域性變數,我們接著往下看

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

接著向棧中推入了ebx,esi,edi,棧頂指標[esp-Ch],然後lea指令將[ebp-40h]的地址放入edi中,給ecx賦值為10h=16,也就是迴圈16次又將0xCCCCCCCCh賦給eax,這也被稱為斷點字元,然後使用rep指令將緩衝區中的值賦值為0xCCCCCCCCh,現在我們再來看看堆疊中的變化

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”

我們看一下單步執行後的esp和ebp的結果

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

好勒,我們們接著往下走

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

這裡可以看到首先是將棧中的[ebp+8]=0019FEE4+8=0019FEEC地址中的值移動到eax中,然後將eax與[ebp+Ch]=0019FEE4+Ch=0019FEF0地址中的值相加,並且將相加後的值存入eax中,棧空間無任何變化,變化的僅僅是eax,我們們單步執行看下

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

好了,我們接著往下看

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

首先將edi,esi,ebx中的值取出來,這裡可以看到,我們推入和取出的順序剛好相反,先進後出的道理,隨後將ebp的值移動到esp中,這裡也就改變了棧頂指標,然後pop ebp,最後ret,ret的做的操作:

pop eip(這裡取出的是返回地址)

我們們單步執行一下

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”

我們接著來看程式碼

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

這裡esp+8是為了堆疊平衡,恢復最初的堆疊,我們單步一下

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

堆疊的變化如下:

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”

這樣就和我們沒進入函式之前的堆疊一樣了,程式到這裡就解釋了函式呼叫以及傳參的問題。我們接著往下走

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

函式呼叫又是一個call,call的兩個操作:

1、Push 0040110B(下一行程式碼地址)

2、Jmp 00401005(函式地址)

單步會遇到jmp,我們直接單步進入函式體

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

對於這裡的堆疊就不畫了,主要講解一下這裡的全域性變數以及“基址”是啥,這裡我們的全域性變數z是由變數定義的時候分配指定的記憶體地址,在每一個函式中都可以找到,每一個全域性變數都有一個唯一的記憶體地址,有且只有一個,在遊戲外掛中經常會聽到找“基址”,然而這個“基址”就是全域性變數的地址,只要程式被編譯那麼就只有這麼一個指定的地址,我們這個程式中z的地址[00424a30],開啟CE

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

首先我們執行一下我們寫好的程式

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

選擇我們剛才執行的程式,點選加入地址,我們將00424a30這個記憶體地址加入進去

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

點選ok,我們看看它的初始值

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

這裡完全沒有自己輸入值,我們改一下值,看看程式的輸出

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

點選OK,再看一下改了之後輸出的值

彙編眼中的函式呼叫引數傳遞以及全域性與區域性變數與“基址”
 

總結:

1、全域性變數是編譯後分配的一個指定記憶體空間,因為是公共的所以任何程式或者程式中的函式都可以呼叫以及修改。

2、區域性變數的地址是隨機的,因為每次進入函式都會隨機分配一段地址給函式,這段分配的地址稱為緩衝區,緩衝區也是用來儲存區域性變數的。

3、“基址”就是全域性變數,這是外掛開發中常用到的一個詞彙。

4、函式呼叫使用call,call指令做的兩個操作:

(1)push call指令下一行地址

(2)Jmp 函式地址(編譯器決定,可能先跳轉到中轉地址,然後跳轉到函式地址)

5、彙編中的函式就是指令的集合,唯一不同的是函式最後都會用ret返回

6、函式中的引數傳遞是使用堆疊來傳遞的。

相關文章