汇编中调用 C 语言函数

这篇小文章中尝试在汇编中调用简单的 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
2
3
4
5
6
7
8
9
10
11
12
13
push 函数参数
call <proc> ; push <proc> 的返回地址 ;调用函数
--------------------------------------------------------------------
push %ebp ;被调函数
mov %esp, %ebp
sub $<n>, %esp ; 为局部变量开辟空间

;; ... work to do

leave
ret
--------------------------------------------------------------------
;; 处理返回值或其他 ;调用函数

说明:

  1. 第1行 函数参数压栈并不一定要用 pushq,包括第6行的为局部变量开辟空间,方式可以
    参考这篇博客
  2. 返回地址的压栈应该通过 call 指令进行。
  3. 这段程序并没有严格按照 AT&T 汇编语法进行书写,具体请参考下面的例子代码。
  4. leave 指令的作用相当于 mov %ebp, %esppop %ebp 两条指令,和ret配合返
    回调用函数是常用做法。

代码示例

被调 C 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* print.c */
#include <stdio.h>

// 无参数无返回值
void print_hello(void)
{
printf("hello\n");
}

// 有参数无返回值
void print_int(int i)
{
printf("%d\n", i);
}

// 有两个参数无返回值
void print_add(int a, int b)
{
printf("a: %d, b: %d, a+b: %d\n", a, b, a+b);
}

// 有参数有返回值
int return_add(int a, int b)
{
return a+b;
}

都是十分简单的 C 函数,应该不需要什么特别的说明。

汇编代码

先从最简单的开始,即调用无参数无返回值的函数,代码如下:

1
2
3
4
5
6
7
8
9
    .globl print_hello, main, print_int, print_add, return_add

main:
pushq %rbp
movq %rsp, %rbp
call print_hello

leave
ret

编译运行如下:

1
2
3
4
5
➜  asm_call_c gcc main.s print.c
➜ asm_call_c ./a.out
hello
➜ asm_call_c echo $?
6

其中,➜是 zshell 的提示符,asm_call_c 是当前目录,其后是命令。在这里看的不是很
直观,其实在运行./a.out后,zshell 的提示符变成了红色,表示上一个命令的返回值不
为 0,原因是我们的汇编代码中没有返回值,在代码中加入即可。如下所示:

1
2
3
4
5
6
7
8
9
10
    .globl print_hello, main, print_int, print_add, return_add

main:
pushq %rbp
movq %rsp, %rbp
call print_hello

movl $0, %eax
leave
ret

返回值是通过eax进行传递的。重新编译运行,可以看到返回值正常了:

1
2
3
4
5
➜  asm_call_c gcc main.s print.c
➜ asm_call_c ./a.out
hello
➜ asm_call_c echo $?
0

然后尝试调用有参数的 print_int。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
    .globl print_hello, main, print_int, print_add, return_add

main:
pushq %rbp
movq %rsp, %rbp
call print_hello

movl $42, %edi
call print_int

movl $0, %eax
leave
ret

根据调用约定可知通过寄存器传递参数时,使用寄存器的顺序为RDI, RSI, RDX, RCX, R8, R9, XMM0–7,所以将要传递的参数放在edi中。

注意:参数传递的方式在Windows下有所不同,具体请查看前面的维基链接或查看相关文档。

编译运行如下:

1
2
3
4
➜  asm_call_c gcc main.s print.c
➜ asm_call_c ./a.out
hello
42

可以看到结果如预期所料。

再后尝试调用两个参数的print_add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    .globl print_hello, main, print_int, print_add, return_add

main:
pushq %rbp
movq %rsp, %rbp
call print_hello

movl $42, %edi
call print_int

movl $2, %esi
movl $1, %edi
call print_add

movl $0, %eax
leave
ret

在 C 语言中,参数一般是从右向左压栈,所以此处先对esi进行赋值,再对edi进行赋
值,为与其一致。编译运行如下:

1
2
3
4
➜  asm_call_c gcc main.s print.c && ./a.out 
hello
42
a: 1, b: 2, a+b: 3

可以看到 a、b 得到了相应的赋值,结果也是正确的。

最后,我们尝试调用有返回值的return_add,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    .globl print_hello, main, print_int, print_add, return_add

main:
pushq %rbp
movq %rsp, %rbp
call print_hello

movl $42, %edi
call print_int

movl $2, %esi
movl $1, %edi
call print_add

movl $4, %esi
movl $3, %edi
call return_add ; 调用有返回值的加法
movl %eax, %edi
call print_int ; 调用 print_int 打印返回值

movl $0, %eax
leave
ret

运行结果如下:

1
2
3
4
5
➜  asm_call_c gcc main.s print.c && ./a.out
hello
42
a: 1, b: 2, a+b: 3
7

这里注意,函数的返回值是通过eax传递的,这也是约定。

结语

本文尝试了在汇编代码中调用几种简单的 C 语言函数,当然这是非常简单的情形,尚不包
括比较复杂的如:参数个数多于 6 个,参数或返回值为浮点数,参数或返回值为结构体等
情况,不过也可以管中窥豹了。更复杂的情形可以通过查看编译器产生的汇编代码来了解,
不必非要自己写汇编程序。

在 Linux gcc 环境下,可以通过命令

1
gcc -S test.c

这条命令会产生一个名为test.s的文件,也可以通过-o file_name来指定输出文件名。

References

x86 calling conventions
C 语言函数调用栈(一)

第二篇文章对 C 语言函数的调用过程及堆栈状态和约定有非常清楚的描述,还有很好的图片
说明,强烈推荐。

0%