一、前言
1.1 敘敘舊
距離上一次寫文章已經過去3個月了,當初計劃至少一個月一篇,不曾想這一拖就是三個月。一直不寫的主要原因是當把一個問題弄清楚了,或者說掌握了一個東西,就覺得沒有什麼可值得寫;另外寫文章也會花費一定的時間。不過想想阮一峯和王建碩討論的寫文章一方面可以提高自己的表述能力,一方面可以加深自己對知識的理解,於是便又拿起筆寫下今天這篇文章。
1.2 文章摘要
這篇文章主要對函數調用棧的理論進行講解,然後通過一個簡單的例子,通過GDP-peda對彙編代碼進行斷點跟蹤,加深對函數調用棧的理解。此外也對32位CPU的寄存器做一個簡單的介紹。
二、基本理論
2.1 寄存器
32位處理器有數據寄存器、變址寄存器、指針寄存器、段寄存器、指令寄存器和標誌寄存器,上圖中簡單介紹了寄存器的名稱和基本作用。如果想要更加詳細的連接寄存器,請參考Daryl的文章通用32位CPU 常用寄存器及其作用。
2.2 函數調用棧
關於函數調用棧我們需要知道的是棧空間是從高地址向低地址填充的,在進行函數調用的時候,首先將被調用函數的參數壓入棧空間,壓入參數的順序是, … ;接着壓入函數的返回地址,函數的返回地址就是call指令的下一條指令的地址;接着壓入被調用函數的基地址,基地址存放在EBP寄存器中;最後壓入被調用函數的局部變量。關於函數調用棧的更多信息可以參考長亭科技的Jwizard的手把手教你棧溢出從入門到放棄。
三、調試分析
通過一個簡單的C語言程序,通過GCC編譯器將其編譯爲32位的程序,然後使用GDB-peda對其進行跟蹤調試。通過觀察寄存器中值的變化,來更加深刻的理解函數調用棧。
3.1 源代碼
#include<stdio.h>
int sum(int a, int b) {
int c = 10;
int sum;
sum = a + b + c;
return sum;
}
int main() {
int a, b, res;
a = 2;
b = 3;
res = sum(a, b);
printf("%d\n", res);
return 0;
}
3.2 GCC編譯爲32位
gcc -g sum.c -o sum -m32
3.3 GDB調試
通過gdb sum
命令對sum文件進行調試,在gdb-peda中使用l命令可以列出源代碼。
3.4 開始調試
在gdp-peda中輸入start
命令開始調試,從下圖中可以看到寄存器(registers),彙編代碼(code),棧空間數據(stack)。此時基地址EBP寄存器的值是0xffffd5a8
,棧頂寄存器ESP的值是0xffffd590
,而指令寄存器EIP的值則是代碼區中正準備執行的指令地址0x804843e
。從圖中可以看出0x804843b
彙編指令分配了0x14
個字節的空間供局部變量使用。
3.4.1 給變量a賦值
使用ni
命令,執行0x804843e
地址的彙編代碼,可以發現把2
(a的值)賦值給地址爲[ebp-0x14] = 0xffffd594
。
3.4.2 給變量b賦值
使用ni
命令,執行0x8048445
地址的彙編代碼,可以發現把3
(b的值)賦值給地址爲[ebp-0x10] = 0xffffd598
。
3.4.3 將b壓入棧空間
在壓入之前,esp的值爲0xffffd590
,所以b
在棧空間的位置是0xffffd590 - 4 = 0xffffd58c
。
3.4.4 將a壓入棧空間
和壓入b同理,不多贅述。
3.4.5 進入sum函數
使用si
命令,單步進入sum函數。從下圖可以看出執行call指令的時候把call指令的下一條指令的地址0x8048457
(見3.4.4圖)作爲返回地址壓入了棧空間。接着需要將main函數的基地址(存放在ebp寄存器)壓入棧空間。
3.4.6 壓入main函數的基地址
使用ni
命令執行0x804840c
地址的指令,從上面的理論部分得知在壓入調用函數的基地址之後,需要將當前棧頂(esp寄存器的值)賦值給ebp作爲被調用函數的基地址,因爲接下來就是執行被調用函數中的彙編代碼。這裏需要注意被調用函數的基地址存儲的值是調用函數的基地址。
3.4.7 棧空間分佈
此時我們已經完成了調用函數的入棧操作,通過stack
命令可以查看當前棧空間的內容0xffffd59c
我們暫時不管,這是main函數啓動時的相關數據。從高地址到低地址分別存放的是0xffffd598
=> main的局部變量b,0xffffd594
=> main的局部變量a,0xffffd590
=> 暫未使用,
0xffffd58c
=> sum函數的參數b,0xffffd588
=> sum函數的參數a,0xffffd584
=> sum函數的返回地址,0xffffd580
=> main函數的基地址。
3.4.8 退出操作
在壓入main函數的基地址後,執行了sum函數內部的加法操作,這裏不做詳細介紹。在執行完成以後會把結果放在eax寄存器中。然後執行leave
指令,leave
指令相當於mov esp,ebp; pop ebp;
。執行mov esp,ebp
將棧頂地址設置爲被調用函數(sum函數)的基地址,被調用函數的基地址就是存儲main函數基地址的地址。執行pop ebp
,將ebp以下的地址包括ebp全部彈出棧空間(sum函數內部處理的時候也會把一些數據入棧,此時操作完成全部彈出),並把main函數的基地址賦值給ebp,成功完成函數的退棧。
最後執行ret
指令,類似pop eip
。把函數的返回地址賦值給eip,使其繼續執行。