iOS 开发高手课 学习笔记

94 阅读4分钟

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 库。