1.动态调用函数
1.1 Calling Convention
调用函数时,需要约定好参数的传递顺序、传递方式,栈维护的方式,名字修饰。这种函数调用者和被调用者之间关于如何调用函数的约定,称作调用惯例(Calling Convention)。编译时,会生成遵循调用惯例的代码。
不同 CPU 架构的调用惯例不一样,例如因为 64 位机器的寄存器更多,而寄存器的访问速度较快,所以会优先使用寄存器来传递参数。只有当参数数量超出寄存器数量时,才会使用栈传递参数。
编译时需要按照调用惯例来针对不同 CPU 架构进行编译,确定好栈和寄存器。 如果少了编译过程,直接在运行时去动态地调用函数,就需要先生成动态调用相应寄存器和栈状态的汇编指令。即需要使用汇编语言。
1.2 Objective-C 的函数调用
Objective-C 的函数调用,使用的是 objc_msgSend 函数。objc_msgSend 函数是使用汇编语言编写的,其结构分为序言准备(Prologue)、函数体(Body)、结束收尾(Epilogue)三部分。 序言准备部分会保存之前程序执行的状态,还会将输入的参数保存到寄存器和栈上。然后在函数体执行自身指令或者跳转其他函数,最后在结束收尾部分恢复寄存器,回到调用函数之前的状态。
objc_msgSend 使得函数调用无需通过编译生成汇编代码来遵循调用惯例,进而使得 Objective-C 具备了动态调用函数的能力。但 objc_msgSend 无法直接调用 C 函数。
1.3 动态调用 C 函数
使用 libffi 来动态调用 C 函数。
libffi 的作用类似于动态编译器,能在运行时生成遵循调用惯例的代码。
libffi 通过调用 ffi_call(函数调用) 来进行函数调用,ffi_call 的输入是 ffi_cif(模板)、函数指针、参数地址。其中,ffi_cif 由 ffi_type(参数类型) 和 参数个数生成,也可以使用 ffi_closure(闭包)。
1.3.1 ffi_type(参数类型)
ffi_type 用于描述 C 语言基本类型,如 uint32、void *、struct 等,定义如下:
typedef struct _ffi_type
{
size_t size; // 所占大小
unsigned short alignment; //对齐大小
unsigned short type; // 标记类型的数字
struct _ffi_type **elements; // 结构体中的元素
} ffi_type;
1.3.2 ffi_cif(模板)
ffi_cif 由参数类型(ffi_type)和参数个数生成:
typedef struct {
ffi_abi abi; // 不同 CPU 架构下的 ABI,一般设置为 FFI_DEFAULT_ABI
unsigned nargs; // 参数个数
ffi_type **arg_types; // 参数类型
ffi_type *rtype; // 返回值类型
unsigned bytes; // 参数所占空间大小,16的倍数
unsigned flags; // 返回类型是结构体时要做的标记
#ifdef FFI_EXTRA_CIF_FIELDS
FFI_EXTRA_CIF_FIELDS;
#endif
} ffi_cif;
1.3.3 ffi_call(函数调用)
ffi_call 函数的主要处理都在 ffi_call_SYSV 汇编函数中。函数的各参数会被依次保存在参数寄存器中。
extern void ffi_call_SYSV (void *stack, void *frame,
void (*fn)(void), void *rvalue,
int flags, void *closure);
核心代码:
//分配 stack 和 frame
cfi_def_cfa(x1, 32);
stp x29, x30, [x1]
mov x29, x1
mov sp, x0
cfi_def_cfa_register(x29)
cfi_rel_offset (x29, 0)
cfi_rel_offset (x30, 8)
// 记录函数指针 fn
mov x9, x2 /* save fn */
// 记录返回值 rvalue
mov x8, x3 /* install structure return */
#ifdef FFI_GO_CLOSURES
// 记录闭包 closure
mov x18, x5 /* install static chain */
#endif
// 保存 rvalue 和 flags
stp x3, x4, [x29, #16] /* save rvalue and flags */
//先将向量参数传到寄存器
tbz w4, #AARCH64_FLAG_ARG_V_BIT, 1f
ldp q0, q1, [sp, #0]
ldp q2, q3, [sp, #32]
ldp q4, q5, [sp, #64]
ldp q6, q7, [sp, #96]
1:
// 再将参数传到寄存器
ldp x0, x1, [sp, #16*N_V_ARG_REG + 0]
ldp x2, x3, [sp, #16*N_V_ARG_REG + 16]
ldp x4, x5, [sp, #16*N_V_ARG_REG + 32]
ldp x6, x7, [sp, #16*N_V_ARG_REG + 48]
//释放上下文,留下栈里参数
add sp, sp, #CALL_CONTEXT_SIZE
// 调用函数指针 fn
blr x9
// 重新读取 rvalue 和 flags
ldp x3, x4, [x29, #16]
// 析构部分栈指针
mov sp, x29
cfi_def_cfa_register (sp)
ldp x29, x30, [x29]
// 保存返回值
adr x5, 0f
and w4, w4, #AARCH64_RET_MASK
add x5, x5, x4, lsl #3
br x5
ffi_call_SYSV 处理过程分为下面几步:
- ffi_call_SYSV 会先分配 stack 和 frame,保存记录 fn、rvalue、closure、flags;
- 将向量参数传到寄存器,按照参数放置规则,调整 sp 的位置;
- 将参数放入寄存器,存放完毕,就开始释放上下文,留下栈里的参数;
- 通过 blr 指令调用 x9 中的函数指针 fn ,以调用函数;
- 调用完函数指针,就重新读取 rvalue 和 flags,析构部分栈指针;
- 保存返回值。
libffi 调用函数的原理和 objc_msgSend 的实现原理类似。 objc_msgSend 原理,可以参考 Dissecting objc_msgSend on ARM64。
Demo: 集成了 iOS 可用的 libffi 库。