在本文中以一段彙編代碼爲例介紹一下在x86和x64彙編語言中調用C 函數的過程。樣例代碼在ubuntu12.04 i386 環境下調試通過。此外本文還介紹了在將這段樣例代碼移植到X64環境下應該注意的問題。
樣例代碼的作用是計算兩個整數的除法,並通過C語言的printf函數打印計算結果。
.section .data
dividend:
.quad 8335
divisor:
.int 25
quotient:
.int 0
remainder:
.int 0
output:
.asciz "The quotient is %d, and the remainder is %d\n"
.section .text
.globl _start
_start:
movl dividend, %eax
movl dividend+4, %edx
divl divisor
movl %eax, quotient
movl %edx, remainder
pushl remainder
pushl quotient
pushl $output
call printf
add $12, %esp
pushl $0
call exit
編譯過程如下:
lil@lil-kvm:~/assembly$as -o divtest.o divtest.s
lil@lil-kvm:~/assembly$ld --dynamic-linker /lib/ld-linux.so.2 -lc -o divtest divtest.o
其中-lc 選項表示需要連接libc.so庫,--dynamic-linker /lib/ld-linux.so.2 也必須指定,否則即使連接未報錯,也會在運行時出現bash: ./divtest: No such file or directory 錯誤。
編譯後運行
lil@lil-kvm:~/assembly$./divtest
The quotient is 333, and the remainder is 10
然後將彙編代碼在ubuntu 12.04 AMD64環境下編譯
liliang@lil:~/assembly$as -o divtest_i64.o divtest_i64.s
divtest_i64.s:Assembler messages:
divtest_i64.s:20:Error: invalid instruction suffix for `push'
divtest_i64.s:21:Error: invalid instruction suffix for `push'
divtest_i64.s:22:Error: invalid instruction suffix for `push'
divtest_i64.s:25:Error: invalid instruction suffix for `push'
修改彙編代碼中的相關指令後的代碼如下:
.section .data
dividend:
.quad 8335
divisor:
.int 25
quotient:
.int 0
remainder:
.int 0
output:
.asciz "The quotient is %d, and the remainder is %d\n"
.section .text
.globl _start
_start:
movl dividend, %eax
movl dividend + 4, %edx
divl divisor
movl %eax, quotient
movl %edx, remainder
push remainder
push quotient
push $output
call printf
add $24, %esp
push $0
call exit
進行編譯連接,注意此時的參數--dynamic-linker/lib64/ld-linux-x86-64.so.2
liliang@lil:~/assembly$as-o divtest_i64.o divtest_i64.s
liliang@lil:~/assembly$ld --dynamic-linker/lib64/ld-linux-x86-64.so.2 -lc -odivtest_i64divtest_i64.o
liliang@lil:~/assembly$ ./divtest_i64
Segmentation fault (core dumped)
後又嘗試着修改了幾處指定,均未能解決問題,通過gdb調試幾次將問題鎖定在了call printf, 每次當執行printf時就會爆出異常,開始懷疑也許跟printf的參數有關。
於是用C寫了下面的測試程序ctest.c:
#include <stdio.h>
int main(int argc,char* argv[])
{
int divident = 333;
int remainder = 10;
printf("dievident=%d, remainder=%d\n", divident, remainder);
}
將程序編譯成目標文件,並進行反彙編。
liliang@lil:~/assembly$gcc -c ctest.c
liliang@lil:~/assembly$objdump -d ctest.o
ctest.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000<main>:
0: 55 push %rbp
1: 48 89e5 mov %rsp,%rbp
4: 48 83 ec20 sub $0x20,%rsp
8: 89 7dec mov %edi,-0x14(%rbp)
b: 48 89 75e0 mov %rsi,-0x20(%rbp)
f: c7 45 f8 4d 01 00 00 movl $0x14d,-0x8(%rbp)
16: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp)
1d: b8 00 00 00 00 mov $0x0,%eax
22: 8b 55fc mov -0x4(%rbp),%edx
25: 8b 4df8 mov -0x8(%rbp),%ecx
28: 89 ce mov %ecx,%esi
2a: 48 89c7 mov %rax,%rdi
2d: b8 00 00 00 00 mov $0x0,%eax
32: e8 00 00 00 00 callq 37 <main+0x37>
37: c9 leaveq
38: c3 retq
通過反匯編出來的代碼發現在x64的彙編下,調用printf所用的參數傳遞方式與x86下面有很大的不同,x86下面是通過堆棧來傳遞,而在x64下是用寄存器來傳遞。於是照葫蘆畫瓢將彙編代碼改成了下面的樣子:
.section .data
dividend:
.quad 8335
divisor:
.int 25
quotient:
.int 0
remainder:
.int 0
output:
.asciz "The quotient is %d, and the remainder is %d\n"
.section .text
.globl _start
_start:
movl dividend, %eax
movl dividend+4, %edx
divl divisor
movl %eax, quotient
movl %edx, remainder
movl remainder,%edx
movl quotient, %esi
mov $output, %rdi
callq printf
mov $0, %rdi
callq exit
重新彙編,連接,運行
liliang@lil:~/assembly$as -o divtest_i64.o divtest_i64.s
liliang@lil:~/assembly$ld --dynamic-linker /lib64/ld-linux-x86-64.so.2 -lc -o divtest_i64 -lc divtest_i64.o
liliang@lil:~/assembly$./divtest_i64
The quotient is 333, and the remainder is 10
終得預期結果。
結論:在x64環境下,gcc所用的參數傳遞方式跟在x86下不同,前者用的是寄存器,後者用的是堆棧。
事後百度出來一篇win_hate的文章,作者通過實驗列出了在x64下gcc的參數傳遞規則:
我試驗了多個參數的情況,發現一般規則爲, 當參數少於7個時, 參數從左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。當參數爲 7 個以上時, 前 6 個與前面一樣, 但後面的依次從 "右向左" 放入棧中。
(1) 參數個數少於7個:
f (a, b, c, d, e, f);
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9
g (a, b)
a->%rdi, b->%rsi
有趣的是, 實際上將參數放入寄存器的語句是從右到左處理參數表的, 這點與32位的時候一致.