0%

汇编代码里直接嵌入二进制

demo的主程序如下:(asm_binary.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

extern int add2(int);

int main()
{
int a = 1, ret = 0;

ret = add2(a);
printf("ret is %d\n", ret);

return 0;
}

add2这个函数的实现如下:(add.S)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// sf op S        sh imm12        rn    rd
// 1 0 0 100010 0 xxxxxxxxxxxx xxxxx xxxxx
//
// imm12: 2 x10 x12
// 1 0 0 100010 0 000000000010 01010 01100 ---+
// |
// 10010001000000000000100101001100 <-----+
.text
.global add2
add2:
mov x10, x0
.word 0b10010001000000000000100101001100
mov x0, x12
ret

汇编指令和二进制指令被封装到一个叫add2的函数里,这个函数接受一个输入,返回输入+2
的值。这里嵌入的二进制是立即数add这个指令的内容,这个指令把rn的值和imm12域段表示
的立即数相加后的值写入rd寄存器。

这里需要注意的有以下两点:1. C代码和汇编代码如何衔接;2. 汇编代码和二进制指令如何衔接。

这里因为是函数调用形式,所以C代码和汇编代码直接的接口是满足ARM64 call convention
ABI接口的,这里编译器会把函数的入参先放到x0上,所以add2里可以直接使用x0,它的值就是
add2的入参。

继续看第二个问题。体系架构的汇编手册一般都会支持直接插入二进制的语法,比如这里就是
用.word表示一个32bit的二进制编码,编译器和汇编器看到这样的写法,不会关心这32bit的
内容,直接把它放到最后的二进制里。寄存器如何排布,这里其实可以直接只用x0作为rn寄存器,
把x0的值再传给x10,是为了说明什么样的寄存器是可以用的,在我们这种场景下,caller
save的寄存器都可以直接使用(ARM64下,x10/x12都是caller save的寄存器)。

所谓caller save寄存器,就是程序在过程调用时由主调方保存的寄存器,因为被调方可能
使用caller save寄存器,主调方必须在发起调用前主动保存自己随后还需要使用的寄存器,
所以主调方也只要保存自己在使用的caller save寄存器就好。

还是如上的例子,如果我们在add2里再调用一个函数,而且在add2里使用了x10,就需要在
调用新函数之前保存x10的值。

gcc asm_binary.c add.S做编译,使用qemu-aarch64 a.out运行这个demo,可以看见打印的
返回值为3。