匯編語言是直接對應系統指令集的低級語言,在語言越來越抽象的今天,匯編語言並不像高級語言那樣使用廣泛,僅僅在驅動程序,嵌入式系統等對性能要求苛刻的領域才能見到它們的身影。但是這並不表示匯編語言就已經沒有用武之地了,通過閱讀匯編代碼,有助於我們理解編譯器的優化能力,並分析代碼中隱含的低效率,所以能夠閱讀和理解匯編代碼也是一項很重要的技能。因為我平時都是在linux環境下工作的,這篇文章就講講linux下的匯編語言。
一、匯編語法風格
匯編語言分為intel風格和AT&T風格,前者被Microsoft Windows/Visual C++采用,Linux下,基本采用的是AT&T風格匯編,兩者語法有很多不同的地方。
AT&T |
Intel |
pushl %eax |
push eax |
AT&T |
Intel |
pushl $1 |
push 1 |
AT&T |
Intel |
addl $1, %eax |
add eax, 1 |
4. 字長表示不同。在 AT&T 匯編格式中,操作數的字長由操作符的最后一個字母決定,后綴'b'、'w'、'l'分別表示操作數為byte、word和long;而在 Intel 匯編格式中,操作數的字長是用 "byte ptr" 和 "word ptr" 等前綴來表示的。例如:
AT&T |
Intel |
movb val, %eax |
mov al, byte ptr val |
5. 尋址方式表示不同。在 AT&T 匯編格式中,內存操作數的尋址方式是
section:disp(base, index, scale)
而在 Intel 匯編格式中,內存操作數的尋址方式為:
section:[base + index*scale + disp]
由於 Linux 工作在保護模式下,用的是 32 位線性地址,所以在計算地址時不用考慮段基址和偏移量,而是采用如下的地址計算方法:
disp + base + index * scale
Intel |
AT&T |
|
內存直接尋址 |
seg_reg: [base + index * scale + immed32] |
seg_reg: immed32 (base, index, scale) |
寄存器間接尋址 |
[reg] |
(%reg) |
寄存器變址尋址 |
[reg + _x] |
_x(%reg) |
立即數變址尋址 |
[reg + 1] |
1(%reg) |
整數數組尋址 |
[eax*4 + array] |
_array (,%eax, 4) |
二、IA32寄存器
1.通用寄存器
顧名思義,通用寄存器是那些你可以根據自己的意願使用的寄存器,但有些也有特殊作用,IA32處理器包括8個通用寄存器,分為3組
1) 數據寄存器
EAX 累加寄存器,常用於運算;在乘除等指令中指定用來存放操作數,另外,所有的I/O指令都使用這一寄存器與外界設備傳送數據。
EBX 基址寄存器,常用於地址索引
ECX 計數寄存器,常用於計數;常用於保存計算值,如在移位指令,循環(loop)和串處理指令中用作隱含的計數器.
EDX 數據寄存器,常用於數據傳遞。
2) 變址寄存器
ESI 源地址指針
EDI 目的地址指針
3) 指針寄存器
EBP為基址指針(Base Pointer)寄存器,存儲當前棧幀的底部地址。
ESP為堆棧指針(Stack Pointer)寄存器,一直記錄棧頂位置,不可直接訪問,push時ESP減小,pop時增大。
2. 指令指針寄存器
EIP 保存了下一條要執行的指令的地址, 每執行完一條指令EIP都會增加當前指令長度的位移,指向下一條指令。用戶不可直接修改EIP的值,但jmp、call和ret等指令也會改變EIP的值,jmp將EIP修改為目的指令地址,call修改EIP為被調函數第一條指令地址,ret從棧中取出(pop)返回地址存入EIP。
三、函數調用過程
函數調用時的具體步驟如下:
1. 調用函數將被調用函數參數入棧,入棧順序由調用約定規定,包括cdecl,stdcall,fastcall,naked call等,c編譯器默認使用cdecl約定,參數從右往座入棧。
2. 執行call命令。
call命令做了兩件事情,一是將EIP寄存器內的值壓入棧中,稱為返回地址,函數完成后還要到這個地址繼續執行程序。然后將被調用函數第一條指令地址存入EIP中,由此進入被調函數。
3. 被調函數開始執行,先准備當前棧幀的環境,分為3步
pushl %ebp 保存調用函數的基址到棧中,
movl %esp, %ebp 設置EBP為當前被調用函數的基址指針,即當前棧頂
subl $xx, %esp 為當前函數分配xx字節棧空間用於存儲局部變量
4. 執行被調函數主體
5. 被調函數結束返回,恢復現場,第3步的逆操作,由leave和ret兩條指令完成,
leave 主要恢復棧空間,相當於
movl %ebp, %esp 釋放被調函數棧空間
popl %ebp 恢復ebp為調用函數基址
ret 與call指令對應,等於pop %EIP,
6. 返回到調用函數,從下一條語句繼續執行
我們來看兩個具體例子,第一個求數組和,
int ArraySum(int *array, int n){ int t = 0; for(int i=0; i<n; ++i) t += array[i]; return t; } int main() { int a[5] = {1, 2, 3, 4, 5 }; int sum = ArraySum(a, 5); return sum; }
編譯成匯編代碼
gcc -std=c99 -S -o sum.s sum.c
gcc加入了很多匯編器和連接器用到的指令,與我們討論的內容無關,簡化匯編代碼如下:
ArraySum: pushl %ebp movl %esp, %ebp subl $16, %esp //分配16字節棧空間 movl $0, -8(%ebp) //初始化t movl $0, -4(%ebp) //初始化i jmp .L2 .L3: movl -4(%ebp), %eax sall $2, %eax //i<<2, 即i*4, 一個int占4字節 addl 8(%ebp), %eax //得到array[i]地址,array+i*4 movl (%eax), %eax //array[i] addl %eax, -8(%ebp) //t+=array[i] addl $1, -4(%ebp) .L2: movl -4(%ebp), %eax cmpl 12(%ebp), %eax //比較i<n jl .L3 movl -8(%ebp), %eax //return t; 默認eax存函數返回值 leave ret main: .LFB1: pushl %ebp movl %esp, %ebp subl $40, %esp movl $1, -24(%ebp) //初始化a[0] movl $2, -20(%ebp) //初始化a[1] movl $3, -16(%ebp) //初始化a[2] movl $4, -12(%ebp) //初始化a[3] movl $5, -8(%ebp) //初始化a[4] movl $5, 4(%esp) //5作為第二個參數傳給 ArraySum leal -24(%ebp), %eax //leal產生數組a的地址 movl %eax, (%esp) //作為第一個參數傳給ArraySum call ArraySum movl %eax, -4(%ebp) //返回值傳給sum movl -4(%ebp), %eax //return sum leave ret
棧變化過程如下:
執行call指令前 執行call指令后
從圖中可以看出
1. 數組連續排列,用move指令逐個賦值,讀取數組元素方法是,用leal得到數組首地址,再計算偏移量
2. 參數從右往左入棧
3. gcc為了保證數據是嚴格對齊的,分配的空間大於使用的空間,有部分空間是浪費的
下面這個例子說明了struct結構的實現方法,
struct Point{ int x; int y; }; void PointInit(struct Point *p, int x, int y){ p->x = x; p->y = y; } int main() { struct Point p; int x = 10; int y = 20; PointInit(&p, x, y); return 0; }
編譯成匯編代碼,簡化如下:
PointInit: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax //p的地址 movl 12(%ebp), %edx //x movl %edx, (%eax) //p->x=x movl 8(%ebp), %eax movl 16(%ebp), %edx //y movl %edx, 4(%eax) //p->y=y popl %ebp ret main: pushl %ebp movl %esp, %ebp subl $28, %esp movl $10, -8(%ebp) //x=10 movl $20, -4(%ebp) y=20 movl -4(%ebp), %eax movl %eax, 8(%esp) movl -8(%ebp), %eax movl %eax, 4(%esp) leal -16(%ebp), %eax //取p地址&p movl %eax, (%esp) call PointInit movl $0, %eax leave ret
棧圖就不畫了,可以清楚地看出struct跟數組類似,連續排列,通過相對位移訪問struct的成員,p->y與*(p+sizeof(p->x))有一樣的效果。
四、disassemble和objdump
在linux下有兩個跟匯編有重要關系的命令,一個是objdump,另一個是gdb中的disassemble。
objdump幫助我們從可執行文件中反匯編出匯編代碼,從而逆向分析工程。
objdump -d sum
部分匯編代碼如下
080483b4 <ArraySum>: 80483b4: 55 push %ebp 80483b5: 89 e5 mov %esp,%ebp 80483b7: 83 ec 10 sub $0x10,%esp 80483ba: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp) 80483c1: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp) 80483c8: eb 12 jmp 80483dc <ArraySum+0x28> 80483ca: 8b 45 fc mov -0x4(%ebp),%eax 80483cd: c1 e0 02 shl $0x2,%eax 80483d0: 03 45 08 add 0x8(%ebp),%eax 80483d3: 8b 00 mov (%eax),%eax 80483d5: 01 45 f8 add %eax,-0x8(%ebp) 80483d8: 83 45 fc 01 addl $0x1,-0x4(%ebp) 80483dc: 8b 45 fc mov -0x4(%ebp),%eax 80483df: 3b 45 0c cmp 0xc(%ebp),%eax 80483e2: 7c e6 jl 80483ca <ArraySum+0x16> 80483e4: 8b 45 f8 mov -0x8(%ebp),%eax 80483e7: c9 leave 80483e8: c3 ret
disassemble可以顯示調試程序的匯編代碼,用法如下
disas 反匯編當前函數
disas sum 反匯編sum函數
disas 0x801234 反匯編位於地址 0x801234附近的函數
disas 0x801234 0x802234 返匯編指定范圍內函數
reference: