在有操作系统的情况下使用纯汇编开发程序的情况非常少,即便是不得不用汇编的场景也常常是使用汇编结合高级编程语言的方式进行开发。下面我们来了解一下如何在汇编之中调用C程序以及在C程序中调用汇编。
在汇编中调用C
我们借助gcc也可以替代ld完成连接。除此之外,我们还可以在汇编代码中调用C标准函数:
; filename: callc.asm
global main
extern puts
section .text
main:
mov rdi, message
call puts
ret
message:
db "Hello, World", 0
这是由之前的示例代码修改而成的调用C标准函数puts
打印输出的代码,它的编译连接方法如下:
nasm -felf64 callc.asm # 首先生成目标文件
gcc -no-pie callc.o # 对目标文件进行连接操作
当我们在64位的Linux操作系统下编写这类调用C标准库的汇编程序时应该注意到一些函数调用的规范。关于详细的信息可以从维基百科中获得。这里主要列出最重要的几点:
- 传递参数时,按照从左到右的顺序,将尽可能多的参数依次保存在寄存器中。存放位置的寄存器顺序是确定的:
- 对于整数和指针,rdi,rsi,rdx,rcx,r8,r9。
- 对于浮点数(float和double类型),xmm0,xmm1,xmm2,xmm3,xmm4,xmm5,xmm6,xmm7。
- 剩下的参数将按照从右到左的顺序压入栈中,并在调用之后由调用函数推出栈。
- 等所有的参数传入后,会生成调用指令。所以当被调用函数得到控制权后,返回地址会被保存在[rsp]中,第一个局部变量会被保存在[rsp+8]中,以此类推。
- 栈指针rsp在调用前必须进行16字节对齐处理。当然,调用的过程中只会把一个8bytes的返回地址推入栈中,所以当函数得到控制权时,rsp并没有对齐。你需要向栈中压入数据或者从rsp减去8来使之对齐。
- 被调用函数需要保存如下的寄存器:rbp,rbx,r12,r13,r14,r15。其他的寄存器可以自由使用。
- 被调用函数也需要保存XMCSR的控制位和x87指令集的控制字,但是x87指令在64位系统上十分少见所以你不必担心这点。
- 整数返回值存放在rax或者rdx:rax中,浮点数返回值存放在xmm0或者xmm1:xmm0中。
下列代码是上述规范的示例:
global main
extern printf
section .text
main:
push rax ; 压入rax的值到栈
mov rdi, format ; 从左到右第一个参数 format
mov rsi, 10 ; 从左到右第二个参数 数字10
mov rdx, 20 ; 从左到右第三个参数 数字20
mov rcx, rsi
add rcx, rdx ; 从左到右第四个参数 10 + 20
; 我们压入了一个rax加上返回地址就对齐了16bytes
call printf
pop rax ; 从栈中弹出原本rax的值到rax
ret
format:
db "%ld + %ld = %ld", 10, 0
在C语言中调用汇编程序
我们先来用汇编写一个int64类型max函数,用于返回两个int64类型的参数中最大的数值:
global maxi64
section .text
maxi64:
; 根据规则,rax存储返回值,先将第一个参数rdi的值给到它
mov rax, rdi
; 对比第一个参数和第二个参数大小
cmp rax, rsi
; cmovl表示根据上一个cmp的比较结果来决定是否进行mov操作
; 如果是rax < rsi则mov rax, rsi
cmovl rax, rsi
ret
接下来我们编写C语言程序调用它:
#include <stdio.h>
#include <inttypes.h>
int64_t maxi64(int64_t, int64_t);
int main() {
printf("%ld\n", maxi64(1, 7));
printf("%ld\n", maxi64(3, 5));
printf("%ld\n", maxi64(5, 3));
printf("%ld\n", maxi64(7, 1));
return 0;
}
假定汇编文件名为maxi64.asm
;C程序文件名为test_maxi64.c
。我们使用如下命令进行编译与运行:
nasm -felf64 maxi64.asm
gcc test_maxi64.c maxi64.o
./a.out