arm64 实现闭包转C函数
调用约定
arm64的C调用约定如下:
首八个不超过8bytes的非浮点数使用r0-r7寄存器
首八个浮点数使用d0-d7寄存器
大于八个参数则使用栈传递
结构体分为三种情况
不超过8bytes,那么此结构体直接使用上述规则传递
超过8bytes但不超过16bytes,那么此结构体使用两个64bits寄存器传输
超过16bytes,传递结构体指针
返回值若不超过8bytes,则使用r0或d0传递,否则调用者使用x8传递接受对象的指针,被调用者将值写入指针
实现
下面将使用Rust语言实现少于七个参数无浮点的闭包转换
闭包是一个含有状态的“函数”,使用起来十分方便,但是要使用一些C函数回调的时候就很不友好了
下面我们来“改造”闭包
一个闭包可以当作一个结构体,他当然也是有地址的
于是对于一个闭包
Fn(T) -> R
我们可以改造为
extern "C" fn(*const (), T) -> R
要实现这点也不难,只需要将寄存器一一往后排,再将x0寄存器赋值对应指针
难点是如何生成这样的机器码
生成机器码
通过上面的分析和查阅资料,找到我们需要使用的指令

BR
我们先来反汇编分析C函数的跳转


可见其跳转使用了BLR指令,那么为什么我们使用BR指令进行跳转呢?
因为我们不需要“返回”
BLR指令的含义为:存储返回地址到LR(x30)寄存器
RET指令的含义为:使用LR寄存器进行返回 (return)
那么使用BR指令直接跳转即可,不需要动LR寄存器

MOVK
MOVK指令可以将一个16位立即数偏移存入指定寄存器,并且保留寄存器其他位的数值(即MOV'Keep')
那么我们可以硬编码一个指针进入寄存器,以实现寻址,即:
MOVK x0, part0, LSL 0
MOVK x0, part1, LSL 16
MOVK x0, part2, LSL 32
MOVK x0, part3, LSL 48
使用四条指令即可存储一个指针进入寄存器

MOV (register)
我们需要移动寄存器,那么是肯定需要使用MOV指令的了

机器码生成
接下来就是生成机器码,机器码的详细定义请参阅Arm手册(见文末)
通过阅读arm手册我们得到这几个指令的机器码生成函数






然后就是期待已久的机器码生成环节~
首先我们定义参数

根据参数列表生成对应的机器码
规则如下:
如果小于32bits,则使用Wn传递
否则使用Xn传递
第一个参数为x0, 将其转移至x1
第二个参数为x1, 将其转移至x2
... 以此类推
然后将参数指针传入x0, 函数指针传入Xn (注意不可占用x8)
那么对于参数列表,我们可以做如下工作

遍历参数列表,对寄存器数字递增
然后将生成的机器码反转
第二个参数为x1, 将其转移至x2
第一个参数为x0, 将其转移至x1
(防止破坏参数)


最后存入参数指针和函数指针


那么到这里,机器码的生成就完毕了,接下来就是将其写入可执行内存并尝试执行,此内容详见文末(VirtualAlloc / mmap)
接下来就是执行测试!



完美通过!

本文源码:https://github.com/LaoLittle/binary-learn
Armv8 手册:https://developer.arm.com/documentation/ddi0487/fc/
VirtualAlloc / VirtualProtect / VirtualFree : https://learn.microsoft.com/en-us/windows/win32/api/memoryapi
mmap / munmap / mprotect : https://linux.die.net/man/2