在本文中以一段汇编代码为例介绍一下在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
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:
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
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位的时候一致.