韓洋
原創作品轉載請注明出處
《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000
寫在開始,本文為因為參加MOOC相關課程而寫的作業,如有疏漏,還請指出。
選了一門Linux內核分析課程,因為閱讀內核代碼中或多或少要涉及到At&T匯編代碼的閱讀,所以這里寫下一個對一個簡單C命令行程序的反匯編分析過程,一方面完成作業,另一方面當作練手。下面開始:
1、編寫我們的C語言小程序
這里我們使用簡單的例子,代碼如下:
1 #include <stdio.h> 2 3 int exG(int x) 4 { 5 return x + 5; 6 } 7 8 int exF(int x) 9 { 10 return exG(x); 11 } 12 13 int main(void) 14 { 15 return exF(10) + 2; 16 }
使用vim等編輯器寫入上述代碼,保存到main.c,然后使用下面命令生成匯編源文件:
x86系統:
$gcc -S -o main.s main.c
x64系統:
$gcc -m32 -S -o main.s main.c
因為我們這里以32位平台為例子,所以在x64機器上要加上-m32來使GCC生成32位的匯編源文件。
2、處理源文件
執行完上述命令后,當前目錄下就會有一個main.s的文件,使用vim打開,不需要的鏈接信息[以"."開頭的行],得到如下匯編代碼:
1 exG: 2 pushl %ebp 3 movl %esp, %ebp 4 movl 8(%ebp), %eax 5 addl $5, %eax 6 popl %ebp 7 ret 8 exF: 9 pushl %ebp 10 movl %esp, %ebp 11 pushl 8(%ebp) 12 call exG 13 addl $4, %esp 14 leave 15 ret 16 main: 17 pushl %ebp 18 movl %esp, %ebp 19 pushl $10 20 call exF 21 addl $4, %esp 22 addl $2, %eax 23 leave 24 ret
可以看到這個文件里是GCC幫我們生成的匯編代碼,這里需要說明下AT&T格式和intel格式,這兩種格式GCC是都可以生成的,如果要生成intel格式的匯編代碼,只需要加上 -masm=intel選項即可,但是Linux下默認是使用AT&T格式來書寫匯編代碼,Linux Kernel代碼中也是AT&T格式,我們要慢慢習慣使用AT&T格式書寫匯編代碼。這里最需要注意的AT&T和intel匯編格式不同點是:
AT&T格式的匯編指令是“源操作數在前,目的操作數在后”,而intel格式是反過來的,即如下:
AT&T格式:movl %eax, %edx
Intel格式:mov edx, eax
表示同一個意思,即把eax寄存器的內容放入edx寄存器。這里需要注意的是AT&T格式的movl里的l表示指令的操作數都是32位,類似的還是有movb,movw,movq,分別表示8位,16位和64位的操作數。更具體的AT&T匯編語法請執行Google或者查閱相關書籍。
3、匯編代碼分析
下面開始分析匯編代碼,運行程序后,C Runtime會在進行一系列准備工作后把我們讓eip指向我們的main函數開始執行,所以這里從main開始分析:
首先進入gdb調試環境:
在我們的機器上輸入如下命令生成帶有調試信息的elf文件,然后進入gdb進行調試:
$gcc -m32 -g -o main main.c
$gdb main -tui -q
進入gdb后,輸入layout asm切換到反匯編視圖,同時在main函數處下斷點:
(gdb)layout asm
(gdb)b main
然后我們使用
(gdb)si
來逐條指令執行並觀察寄存器變化情況,如圖:
對於main函數:
逐條指令執行
(gdb)si
pushl %ebp movl %esp, %ebp
...
這兩條是Prolog,其作用包含保存當前的棧環境,以確保函數能正確返回和為當前函數開辟新的棧空間。這兩句的執行效果是把當前的ebp值入棧,再把ebp入棧后的esp中的值放入ebp。此時,esp和ebp都指向同一個內存地址。
這里需要說明的是入棧和出棧操作,在intel的x86架構上,棧是從高地址向低地址增長,所以:
入棧等價於:1、esp先下移留出對應的空間;2、把相應數值放入剛剛留出的空間完成入棧
出棧等價於:1、從當前esp指向內存取出數值;2、esp向上移動,釋放相應空間
此時棧中的情況如下如所示:[從這里開始,下圖中每個空格皆表示4字節內存空間]
圖1
繼續逐條指令執行
1 pushl $10 2 call exF 3 addl $4, %esp 4 ....
pushl $10,當前esp先減4,然后把寬度為4直接的數值10放入esp當前指向的內存中。
call exF ,函數調用指令,首先把當前eip的值[當前eip指向第三條指令,即addl $4, %esp]入棧,然后跳轉到exF函數的第一條指令開始執行。
此時棧中的情況如下如所示:
圖2
對於exF函數:
逐條指令執行
(gdb)si
1 pushl %ebp 2 movl %esp, %ebp 3 pushl 8(%ebp) 4 call exG 5 addl $4, %esp 6 ....
這里前條指令和main函數的頭兩條指令作用相同,保存當前棧環境,為exF函數開辟新的棧空間
pushl 8(%ebp),該指令把當前ebp中的數值加8后作為內存地址,並把該內存地址指向的內存空間內的數值"10"放入棧中。[參考圖2可以發現其實就是把調用函數是傳入的參數入棧]
call exG,函數調用指令,當前eip入棧后,跳轉到exG函數的第一條指令執行。
此時棧中的情況如下如所示:
圖3
對於exG函數:
逐條指令執行
(gdb)si
1 pushl %ebp 2 movl %esp, %ebp 3 movl 8(%ebp), %eax 4 addl $5, %eax 5 popl %ebp 6 ret
首先依然是函數前言(Prolog),保存棧環境,開辟新的棧空間
此時棧中的情況如下如所示:
圖4
此時GDB里使用bt 查看運行棧情況如下圖:
movl 8(%ebp),%eax 該指令把當前ebp中的數值加8后作為內存地址,並把該內存地址指向的內存空間內的數值“10”放入eax寄存器中。[參照圖4可以發現就是把調用函數是傳入的參數放入eax寄存器]
addl $5, %eax AT&T匯編語言中$符號后面跟上數字表示一個立即數,這里即為把eax中的值加上5,再放回eax,此時eax的值為15.
popl %ebp,從棧中獲取舊的esp值,並放入ebp寄存器。[這里之所以沒有再加上一條movl %ebp, %esp是因為函數中esp的值並沒有改變,依然指向存放舊esp值的內存空間]
ret 等價於pop eip,從當前棧頂,即esp所指內存處獲取值,作為eip,然后跳轉到eip中存放的地址繼續執行。
此時棧中情況如圖:
圖5
到這里,函數exG已經返回,其返回值存儲在eax寄存器中,即返回值為15
返回到函數exF中
1 ... 2 addl $4, %esp 3 leave 4 ret
程序從上述指令開始繼續執行,
addl $4, %esp 回收棧空間,棧空間收縮4個字節,
leave,等價於 如下兩條指令
movl %ebp, %esp
pop %ebp
即函數結語[EpiLog],釋放exF函數使用的棧空間,此時棧中情況如圖:
圖6
再接着是ret指令,該指令執行后,函數exF返回,程序回到main函數繼續執行,此時棧中情況如圖:
圖7
此時eax中存放的是函數exF的返回值,即15
回到main函數繼續執行
1 ... 2 addl $4, %esp 3 addl $2, %eax 4 leave 5 ret
addl $4, %esp 棧收縮4個字節,回收棧空間
addl $2, %eax 此時eax中的值是main函數調用函數exF的得到的返回值,即15,本條指令將eax中的值加2后放回eax,執行后eax中的值為17
leave 函數結語,本條指令執行后,ebp的值為圖7中黑色Old EBP表示的值,esp指向圖7中黑色Old ebp所在內存空間的上一個內存空間,該處存放的是指向CRT調用main函數后緊接的指令的所在的內存地址
ret main函數返回
4、總結
計算機工作的過程實際上就是“取指令,執行指令”的循環,程序在執行時被裝入內存,計算機從內存中某個位置開始讀取指令按照一定邏輯順序執行,直到程序結束。在執行過程中根據需要為程序中各個模塊在內存中開辟一定的空間[如棧,堆],運行棧對應函數調用十分重要,函數參數和自動變量都存儲於運行棧中。計算機從內存的什么地方開始執行指令完全由cpu中指令指針寄存器[EIP]中的值決定,並不會區分內存中什么地方是代碼段,什么地方是數據段。