x86、arm、mips架構函式呼叫例項分析
原文網址:http://nieyong.github.com/wiki_cpu/
在看過了上面的幾節之後,在潛意識中你想記住的東西肯定很多了。這個時候,你需要靜下心來休息一下在沉澱一下。
"Now is a good point to take a break to let this information sink in."
下面,我們就看看C語言撰寫的程式,在不同的CPU架構下,生成的組合語言是怎麼樣的,各有什麼特點,這和前面介紹的各種CPU架構的知識是如何聯絡的。如果你覺得還不夠,也很高興一起來探討一下在不同的CPU架構下,函式的呼叫時如何實現的,以及各有什麼特點。
反彙編檔案
測試用的C原始碼如下,main.c檔案:
- #include <stdio.h>
- int add(int a,int b)
- {
- return a+b;
- }
- int main(int argc,char** argv)
- {
- int a,b,c;
- a = 1;
- b = 2;
- c = fn(a,b);
- return c;
- }
下面為編譯以及反彙編的過程。當然,ARM和MIPS的編譯和反彙編是使用的交叉編譯工具鏈。例如在我的平臺下,對應的命令就是ccarm,objdumparm和ccmips,objdumpmips。
#gcc -o main.o -c main.c #objdump -d main.o>main.a
編譯成x86下面的main.o檔案,注意,沒有連結。然後反彙編為main_x86.a如下所示。這裡需要指出的是,下面的組合語言並不是我們在《微機原理》課本上學習到的x86的彙編(我們稱為intel彙編),而叫做AT&T彙編。那麼,什麼是AT&T彙編呢?
在將Unix移植到80386處理器上時,Unix圈內人士根據Unix領域的習慣和需要而定義了AT&T彙編。而GNU主要是在Unix領域活動,因此GNU開發的各種系統工具繼承了AT&T的386彙編格式,這也是我們使用GNU工具進行反彙編的時候,看到的組合語言。
- main.o: file format elf32-i386
- Disassembly of section .text:
- 00000000 <add>:
- 0: 55 push %ebp
- 1: 89 e5 mov %esp,%ebp
- 3: 8b 55 0c mov 0xc(%ebp),%edx
- 6: 8b 45 08 mov 0x8(%ebp),%eax
- 9: 01 d0 add %edx,%eax
- b: 5d pop %ebp
- c: c3 ret
- 0000000d <main>:
- d: 8d 4c 24 04 lea 0x4(%esp),%ecx
- 11: 83 e4 f0 and $0xfffffff0,%esp
- 14: ff 71 fc pushl -0x4(%ecx)
- 17: 55 push %ebp
- 18: 89 e5 mov %esp,%ebp
- 1a: 51 push %ecx
- 1b: 83 ec 24 sub $0x24,%esp
- 1e: c7 45 f0 01 00 00 00 movl $0x1,-0x10(%ebp)
- 25: c7 45 f4 02 00 00 00 movl $0x2,-0xc(%ebp)
- 2c: 8b 45 f4 mov -0xc(%ebp),%eax
- 2f: 89 44 24 04 mov %eax,0x4(%esp)
- 33: 8b 45 f0 mov -0x10(%ebp),%eax
- 36: 89 04 24 mov %eax,(%esp)
- 39: e8 fc ff ff ff call 3a <main+0x2d>
- 3e: 89 45 f8 mov %eax,-0x8(%ebp)
- 41: 8b 45 f8 mov -0x8(%ebp),%eax
- 44: 83 c4 24 add $0x24,%esp
- 47: 59 pop %ecx
- 48: 5d pop %ebp
- 49: 8d 61 fc lea -0x4(%ecx),%esp
- 4c: c3 ret
編譯成mips下面的main.o檔案,沒有連結。然後反彙編為main_mips.a:
- main.o: file format elf32-bigmips
- Disassembly of section .text:
- 0000000000000000 <add>:
- 0: 27bdfff8 addiu $sp,$sp,-8
- 4: afbe0000 sw $s8,0($sp)
- 8: 03a0f021 move $s8,$sp
- c: afc40008 sw $a0,8($s8)
- 10: afc5000c sw $a1,12($s8)
- 14: 8fc20008 lw $v0,8($s8)
- 18: 8fc3000c lw $v1,12($s8)
- 1c: 00000000 nop
- 20: 00431021 addu $v0,$v0,$v1
- 24: 03c0e821 move $sp,$s8
- 28: 8fbe0000 lw $s8,0($sp)
- 2c: 03e00008 jr $ra
- 30: 27bd0008 addiu $sp,$sp,8
- 0000000000000034 <main>:
- 34: 27bdffd8 addiu $sp,$sp,-40
- 38: afbf0024 sw $ra,36($sp)
- 3c: afbe0020 sw $s8,32($sp)
- 40: 03a0f021 move $s8,$sp
- 44: afc40028 sw $a0,40($s8)
- 48: afc5002c sw $a1,44($s8)
- 4c: 24020001 li $v0,1
- 50: afc20010 sw $v0,16($s8)
- 54: 24020002 li $v0,2
- 58: afc20014 sw $v0,20($s8)
- 5c: 8fc40010 lw $a0,16($s8)
- 60: 8fc50014 lw $a1,20($s8)
- 64: 0c000000 jal 0 <add>
- 68: 00000000 nop
- 6c: afc20018 sw $v0,24($s8)
- 70: 8fc20018 lw $v0,24($s8)
- 74: 03c0e821 move $sp,$s8
- 78: 8fbf0024 lw $ra,36($sp)
- 7c: 8fbe0020 lw $s8,32($sp)
- 80: 03e00008 jr $ra
- 84: 27bd0028 addiu $sp,$sp,40
- ...
編譯成arm下的main.o,沒有連結。然後反彙編為main_arm.a:
- main.o: file format elf32-littlearm
- Disassembly of section .text:
- 00000000 <add>:
- 0: e1a0c00d mov r12, sp
- 4: e92dd800 stmdb sp!, {r11, r12, lr, pc}
- 8: e24cb004 sub r11, r12, #4 ; 0x4
- c: e24dd008 sub sp, sp, #8 ; 0x8
- 10: e50b0010 str r0, [r11, -#16]
- 14: e50b1014 str r1, [r11, -#20]
- 18: e51b3010 ldr r3, [r11, -#16]
- 1c: e51b2014 ldr r2, [r11, -#20]
- 20: e0833002 add r3, r3, r2
- 24: e1a00003 mov r0, r3
- 28: ea000009 b 2c <add+0x2c>
- 2c: e91ba800 ldmdb r11, {r11, sp, pc}
- 00000030 <main>:
- 30: e1a0c00d mov r12, sp
- 34: e92dd800 stmdb sp!, {r11, r12, lr, pc}
- 38: e24cb004 sub r11, r12, #4 ; 0x4
- 3c: e24dd014 sub sp, sp, #20 ; 0x14
- 40: e50b0010 str r0, [r11, -#16]
- 44: e50b1014 str r1, [r11, -#20]
- 48: e3a03001 mov r3, #1 ; 0x1
- 4c: e50b3018 str r3, [r11, -#24]
- 50: e3a03002 mov r3, #2 ; 0x2
- 54: e50b301c str r3, [r11, -#28]
- 58: e51b0018 ldr r0, [r11, -#24]
- 5c: e51b101c ldr r1, [r11, -#28]
- 60: ebfffffe bl 0 <add>
- 64: e1a03000 mov r3, r0
- 68: e50b3020 str r3, [r11, -#32]
- 6c: e51b3020 ldr r3, [r11, -#32]
- 70: e1a00003 mov r0, r3
- 74: ea00001c b 78 <main+0x48>
- 78: e91ba800 ldmdb r11, {r11, sp, pc}
不同CPU架構下組合語言的特點
程式碼長度
首先,我們看同一個C語言程式,在不同的處理器下,程式碼長度。程式碼長度為反彙編檔案的第一列,可以看到,在x86下是0x4c+1個位元組,在MIPS下是0x84+4個位元組,在ARM下是0x78+4個位元組。為什麼這樣呢?
首先看的是CISC和RISC的區別 CISC架構下,指令的長度不是固定的,而RISC為了實現流水線,每條指令的長度都是固定的。看到反彙編檔案的第二列,x86的指令中,最短為一個位元組,例如push,pop,ret等指令,最長為7個位元組,例如movl指令。而對於ARM和MIPS,所有的指令長度都固定為4個位元組。這也是ARM和MIPS的機器程式碼長度要明顯比x86長的原因。
另外一個原因,就是CISC可能為某個特殊操作實現了一條指令,而RISC處理器則需要用多條指令組合來完成該操作。最明顯的就是出入棧操作。
然後我們看MIPS和ARM的區別 ARM的程式碼長度比MIPS程式碼長度稍稍小,這也驗證了人們常說的MIPS是純粹的RISC架構,而ARM則在RISC的基礎上吸收了CISC中的某些優點。我們還是要看看,ARM是因為吸收了哪些CISC的特點,才做到程式碼長度的減少的呢?最明顯的是出入棧,在ARM中實現了多暫存器load/store指令,請注意main_arm.a檔案的0x4,0x2c,0x34,0x78行的stmdb和ldmdb指令。那麼,這和CISC有什麼關係呢?仔細想想,這就是CISC中的為了某個特殊的操作而實現一條指令的思想。另外,多暫存器的load/store指令破壞了RISC中指令的執行週期必須是單週期的規定,這一點,也是人們指責ARM不是純粹的RISC架構的證據之一。
另外還有就是ARM實現了條件標誌,實現條件執行,這樣可以減少分支指令的數目,提高程式碼的密度。不過在我們這個例項中沒有這方面的應用。
出棧入棧
入棧指將CPU通用暫存器的值放入棧(儲存器)中儲存起來。出棧則是指將棧中的值恢復到CPU中相應通用暫存器。
在x86下面,有專門的出棧和入棧指令pop和push。出棧和入棧在棧中的位置是當前堆疊指標sp所指向的位置。在MIPS和ARM下面,沒有專門的指令,所以入棧和出棧首先使用的是load/store指令將資料放入堆疊或者彈出堆疊,然後使用add/stub指令修改堆疊指標的值。比較特殊的是ARM有特殊的多暫存器load/store指令來快速的完成出入棧。由此可見,在MIPS和ARM下面,出入棧都是使用幀指標或者堆疊指標的相對位置,在執行之後,並不會影響堆疊指標sp的值。
棧的生長 棧的生長指堆疊指標sp的變化。例如,在函式呼叫的時候,需要一次性的分配被呼叫函式的棧空間大小。在x86,ARM,MIPS下都是使用的對堆疊指標暫存器直接加減的辦法。而且,在MIPS和ARM下,也只有這種辦法可以改變堆疊指標SP的值。而在x86下,除了直接加減的辦法之外,出入棧操作指令pop和push還可以改變sp的值。
不同CPU架構下函式呼叫的特點
C語言函式的呼叫,請參考C語言-Stack的相關內容。涉及堆疊幀(stack frame),活動記錄(active record),呼叫慣例(call convention)等相關概念。建議參考《程式設計師的自我修養-連結、裝載與庫》的第10.2節-棧和呼叫慣例。
毫無疑問,這裡都是使用的C語言的預設呼叫慣例cdecl。cdecl呼叫管理的特點如下:
- 呼叫的出棧方為函式呼叫方;
- 引數的傳遞為從右向左入棧;
- 名字修飾為下劃線+函式名;
首先我們給出一個cdecl呼叫慣例的模型,不針對任何處理器架構,然後我們再看看不同的處理器架構下cdecl呼叫慣例有什麼樣的特點。下圖為cdecl呼叫慣例的一般情況示意圖。
下面分析cdecl呼叫慣例中,呼叫函式和被呼叫函式分別負責活動記錄(堆幀棧)中的什麼操作呢?首先是被呼叫函式,在函式的入口,需要做以下操作:
- 需要將返回地址入棧,例如MIPS下的
sw \(ra,28(\)sp)
一句,就是將函式的返回地址入棧。對於x86架構,由於call命令會自動將返回地址入棧,所以在函式入口處沒有返回地址入棧的指令; - 需要將呼叫函式的幀指標入棧,然後設定本身自己函式的幀指標。在x86下,是
push %ebp;mov %esp,%ebp
兩句,而在MIPS下,是sw \(s8,24(\)sp);move \(s8,\)sp;
兩句。這裡也可以看出x86的擁有專門入棧指令的特點push的特點; - 調整堆疊指標的大小,升棧到函式需要的堆疊大小。
上面是被呼叫函式需要負責的事情,那麼呼叫函式需要負責什麼呢?主要有一下幾點:
- 準備好函式呼叫引數;
- 呼叫指令call跳轉到被呼叫函式。
X86的函式呼叫
這個呼叫中沒有調整堆疊指標的操作,因為不需要用到額外的堆疊空間。所以,只有上面提到的第一點和第二點。
- push %ebp
- mov %esp,%ebp
- ......
- pop %ebp
- ret
MIPS的函式呼叫
MIPS的堆疊,在被呼叫函式中有8bytes的升棧操作,下面的ARM中也是一樣的。
- addiu $sp,$sp,-8
- sw $s8,0($sp)
- move $s8,$sp
- ......
- move $sp,$s8
- lw $s8,0($sp)
- jr $ra
- addiu $sp,$sp,8
ARM的函式呼叫
相比於MIPS架構,一條stmdb和ldmdb就可以完成多個暫存器的儲存(store)和載入(load)。
- mov r12, sp
- stmdb sp!, {r11, r12, lr, pc}
- sub r11, r12, #4 ; 0x4
- sub sp, sp, #8 ; 0x8
- ......
- b 2c <add+0x2c>
- ldmdb r11, {r11, sp, pc}