这篇小文章中尝试在汇编中调用简单的 C 语言函数,主要是为了加深对 C 语言调用约定的
理解和记忆,也是对汇编编写简单程序的一次小尝试。程序非常简单,主要是尝试对这几天
学习的知识进行运用。
环境说明
使用的环境为:
- Ubuntu 14.04, 64-bit
- Intel i7
- gcc 4.8.4
预备知识
调用约定
根据维基百科的描述
x86-64 下 Linux 下 gcc 的参数传递使用寄存器的顺序为 RDI, RSI, RDX, RCX, R8, R9,
XMM0–7
栈帧结构和调用流程
1 | push 函数参数 |
说明:
- 第1行 函数参数压栈并不一定要用 pushq,包括第6行的为局部变量开辟空间,方式可以
参考这篇博客。 - 返回地址的压栈应该通过 call 指令进行。
- 这段程序并没有严格按照 AT&T 汇编语法进行书写,具体请参考下面的例子代码。
leave
指令的作用相当于mov %ebp, %esp
、pop %ebp
两条指令,和ret
配合返
回调用函数是常用做法。
代码示例
被调 C 函数
1 | /* print.c */ |
都是十分简单的 C 函数,应该不需要什么特别的说明。
汇编代码
先从最简单的开始,即调用无参数无返回值的函数,代码如下:
1 | .globl print_hello, main, print_int, print_add, return_add |
编译运行如下:
1 | ➜ asm_call_c gcc main.s print.c |
其中,➜是 zshell 的提示符,asm_call_c 是当前目录,其后是命令。在这里看的不是很
直观,其实在运行./a.out
后,zshell 的提示符变成了红色,表示上一个命令的返回值不
为 0,原因是我们的汇编代码中没有返回值,在代码中加入即可。如下所示:
1 | .globl print_hello, main, print_int, print_add, return_add |
返回值是通过eax
进行传递的。重新编译运行,可以看到返回值正常了:
1 | ➜ asm_call_c gcc main.s print.c |
然后尝试调用有参数的 print_int
。代码如下:
1 | .globl print_hello, main, print_int, print_add, return_add |
根据调用约定可知通过寄存器传递参数时,使用寄存器的顺序为RDI, RSI, RDX, RCX, R8,
R9, XMM0–7
,所以将要传递的参数放在edi
中。
注意:参数传递的方式在Windows下有所不同,具体请查看前面的维基链接或查看相关文档。
编译运行如下:
1 | ➜ asm_call_c gcc main.s print.c |
可以看到结果如预期所料。
再后尝试调用两个参数的print_add
:
1 | .globl print_hello, main, print_int, print_add, return_add |
在 C 语言中,参数一般是从右向左压栈,所以此处先对esi
进行赋值,再对edi
进行赋
值,为与其一致。编译运行如下:
1 | ➜ asm_call_c gcc main.s print.c && ./a.out |
可以看到 a、b 得到了相应的赋值,结果也是正确的。
最后,我们尝试调用有返回值的return_add
,代码如下:
1 | .globl print_hello, main, print_int, print_add, return_add |
运行结果如下:
1 | ➜ asm_call_c gcc main.s print.c && ./a.out |
这里注意,函数的返回值是通过eax
传递的,这也是约定。
结语
本文尝试了在汇编代码中调用几种简单的 C 语言函数,当然这是非常简单的情形,尚不包
括比较复杂的如:参数个数多于 6 个,参数或返回值为浮点数,参数或返回值为结构体等
情况,不过也可以管中窥豹了。更复杂的情形可以通过查看编译器产生的汇编代码来了解,
不必非要自己写汇编程序。
在 Linux gcc 环境下,可以通过命令
1 | gcc -S test.c |
这条命令会产生一个名为test.s
的文件,也可以通过-o file_name
来指定输出文件名。
References
x86 calling conventions
C 语言函数调用栈(一)
第二篇文章对 C 语言函数的调用过程及堆栈状态和约定有非常清楚的描述,还有很好的图片
说明,强烈推荐。