作為剛往底層方向走的一只菜鳥,今天為各位分享一篇名為匯編眼中的函數調用參數傳遞以及全局、局部變量與“基址”,好了,廢話不多說,先來看看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指令做了兩個操作,
這里我們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、函數中的參數傳遞是使用堆棧來傳遞的。