1. 程式人生 > >x86、arm、mips架構函式呼叫例項分析

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檔案:

  1. #include <stdio.h>
  2. int add(int a,int b)  
  3. {  
  4.     return a+b;  
  5. }  
  6. int main(int argc,char** argv)  
  7. {  
  8.     int a,b,c;  
  9.     a = 1;  
  10.     b = 2;  
  11.     c = fn(a,b);  
  12.     return c;  
  13. }  

下面為編譯以及反彙編的過程。當然,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工具進行反彙編的時候,看到的組合語言。
  1. main.o:     file format elf32-i386  
  2. Disassembly of section .text:  
  3. 00000000 <add>:  
  4.    0:   55                      push   %ebp  
  5.    1:   89 e5                   mov    %esp,%ebp  
  6.    3:   8b 55 0c                mov    0xc(%ebp),%edx  
  7.    6:   8b 45 08                mov    0x8(%ebp),%eax  
  8.    9:   01 d0                   add    %edx,%eax  
  9.    b:   5d                      pop    %ebp  
  10.    c:   c3                      ret      
  11. 0000000d <main>:  
  12.    d:   8d 4c 24 04             lea    0x4(%esp),%ecx  
  13.   11:   83 e4 f0                and    $0xfffffff0,%esp  
  14.   14:   ff 71 fc                pushl  -0x4(%ecx)  
  15.   17:   55                      push   %ebp  
  16.   18:   89 e5                   mov    %esp,%ebp  
  17.   1a:   51                      push   %ecx  
  18.   1b:   83 ec 24                sub    $0x24,%esp  
  19.   1e:   c7 45 f0 01 00 00 00    movl   $0x1,-0x10(%ebp)  
  20.   25:   c7 45 f4 02 00 00 00    movl   $0x2,-0xc(%ebp)  
  21.   2c:   8b 45 f4                mov    -0xc(%ebp),%eax  
  22.   2f:   89 44 24 04             mov    %eax,0x4(%esp)  
  23.   33:   8b 45 f0                mov    -0x10(%ebp),%eax  
  24.   36:   89 04 24                mov    %eax,(%esp)  
  25.   39:   e8 fc ff ff ff          call   3a <main+0x2d>  
  26.   3e:   89 45 f8                mov    %eax,-0x8(%ebp)  
  27.   41:   8b 45 f8                mov    -0x8(%ebp),%eax  
  28.   44:   83 c4 24                add    $0x24,%esp  
  29.   47:   59                      pop    %ecx  
  30.   48:   5d                      pop    %ebp  
  31.   49:   8d 61 fc                lea    -0x4(%ecx),%esp  
  32.   4c:   c3                      ret      

編譯成mips下面的main.o檔案,沒有連結。然後反彙編為main_mips.a:

  1. main.o:     file format elf32-bigmips  
  2. Disassembly of section .text:  
  3. 0000000000000000 <add>:  
  4.    0:   27bdfff8    addiu   $sp,$sp,-8  
  5.    4:   afbe0000    sw  $s8,0($sp)  
  6.    8:   03a0f021    move    $s8,$sp  
  7.    c:   afc40008    sw  $a0,8($s8)  
  8.   10:   afc5000c    sw  $a1,12($s8)  
  9.   14:   8fc20008    lw  $v0,8($s8)  
  10.   18:   8fc3000c    lw  $v1,12($s8)  
  11.   1c:   00000000    nop  
  12.   20:   00431021    addu    $v0,$v0,$v1  
  13.   24:   03c0e821    move    $sp,$s8  
  14.   28:   8fbe0000    lw  $s8,0($sp)  
  15.   2c:   03e00008    jr  $ra  
  16.   30:   27bd0008    addiu   $sp,$sp,8  
  17. 0000000000000034 <main>:  
  18.   34:   27bdffd8    addiu   $sp,$sp,-40  
  19.   38:   afbf0024    sw  $ra,36($sp)  
  20.   3c:   afbe0020    sw  $s8,32($sp)  
  21.   40:   03a0f021    move    $s8,$sp  
  22.   44:   afc40028    sw  $a0,40($s8)  
  23.   48:   afc5002c    sw  $a1,44($s8)  
  24.   4c:   24020001    li  $v0,1  
  25.   50:   afc20010    sw  $v0,16($s8)  
  26.   54:   24020002    li  $v0,2  
  27.   58:   afc20014    sw  $v0,20($s8)  
  28.   5c:   8fc40010    lw  $a0,16($s8)  
  29.   60:   8fc50014    lw  $a1,20($s8)  
  30.   64:   0c000000    jal 0 <add>  
  31.   68:   00000000    nop  
  32.   6c:   afc20018    sw  $v0,24($s8)  
  33.   70:   8fc20018    lw  $v0,24($s8)  
  34.   74:   03c0e821    move    $sp,$s8  
  35.   78:   8fbf0024    lw  $ra,36($sp)  
  36.   7c:   8fbe0020    lw  $s8,32($sp)  
  37.   80:   03e00008    jr  $ra  
  38.   84:   27bd0028    addiu   $sp,$sp,40  
  39.     ...  

編譯成arm下的main.o,沒有連結。然後反彙編為main_arm.a:

  1. main.o:     file format elf32-littlearm  
  2. Disassembly of section .text:  
  3. 00000000 <add>:  
  4.    0:   e1a0c00d    mov r12, sp  
  5.    4:   e92dd800    stmdb   sp!, {r11, r12, lr, pc}  
  6.    8:   e24cb004    sub r11, r12, #4    ; 0x4  
  7.    c:   e24dd008    sub sp, sp, #8  ; 0x8  
  8.   10:   e50b0010    str r0, [r11, -#16]  
  9.   14:   e50b1014    str r1, [r11, -#20]  
  10.   18:   e51b3010    ldr r3, [r11, -#16]  
  11.   1c:   e51b2014    ldr r2, [r11, -#20]  
  12.   20:   e0833002    add r3, r3, r2  
  13.   24:   e1a00003    mov r0, r3  
  14.   28:   ea000009    b   2c <add+0x2c>  
  15.   2c:   e91ba800    ldmdb   r11, {r11, sp, pc}  
  16. 00000030 <main>:  
  17.   30:   e1a0c00d    mov r12, sp  
  18.   34:   e92dd800    stmdb   sp!, {r11, r12, lr, pc}  
  19.   38:   e24cb004    sub r11, r12, #4    ; 0x4  
  20.   3c:   e24dd014    sub sp, sp, #20 ; 0x14  
  21.   40:   e50b0010    str r0, [r11, -#16]  
  22.   44:   e50b1014    str r1, [r11, -#20]  
  23.   48:   e3a03001    mov r3, #1  ; 0x1  
  24.   4c:   e50b3018    str r3, [r11, -#24]  
  25.   50:   e3a03002    mov r3, #2  ; 0x2  
  26.   54:   e50b301c    str r3, [r11, -#28]  
  27.   58:   e51b0018    ldr r0, [r11, -#24]  
  28.   5c:   e51b101c    ldr r1, [r11, -#28]  
  29.   60:   ebfffffe    bl  0 <add>  
  30.   64:   e1a03000    mov r3, r0  
  31.   68:   e50b3020    str r3, [r11, -#32]  
  32.   6c:   e51b3020    ldr r3, [r11, -#32]  
  33.   70:   e1a00003    mov r0, r3  
  34.   74:   ea00001c    b   78 <main+0x48>  
  35.   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的函式呼叫

這個呼叫中沒有調整堆疊指標的操作,因為不需要用到額外的堆疊空間。所以,只有上面提到的第一點和第二點。

  1. push   %ebp  
  2. mov    %esp,%ebp  
  3. ......  
  4. pop    %ebp  
  5. ret      

MIPS的函式呼叫

MIPS的堆疊,在被呼叫函式中有8bytes的升棧操作,下面的ARM中也是一樣的。

  1. addiu   $sp,$sp,-8  
  2. sw  $s8,0($sp)  
  3. move    $s8,$sp  
  4. ......  
  5. move    $sp,$s8  
  6. lw  $s8,0($sp)  
  7. jr  $ra  
  8. addiu   $sp,$sp,8  

ARM的函式呼叫

相比於MIPS架構,一條stmdb和ldmdb就可以完成多個暫存器的儲存(store)和載入(load)。

  1. mov r12, sp  
  2. stmdb   sp!, {r11, r12, lr, pc}  
  3. sub r11, r12, #4    ; 0x4  
  4. sub sp, sp, #8  ; 0x8  
  5. ......  
  6. b   2c <add+0x2c>  
  7. ldmdb   r11, {r11, sp, pc}