C语言的runtime -- libffi

前言:

对于C语言来说,我们在函数调用前,需要明确的告诉编译器这个函数的参数和返回值类型是什么,函数才能正常执行。

如此说来动态的调用一个C函数是不可能实现的。

因为我们在编译前,就要将遵循调用规则的函数调用写在需要调用的地方,然后通过编译器编译生成对应的汇编代码,将相应的栈和寄存器状态准备好。

如果想在运行时动态去调用的话,将没有人为我们做这一系列的处理。

所以我们需要解决这个问题:当我们在运行时动态调用一个函数时,自己要先将相应栈和寄存器状态准备好,然后生成相应的汇编指令。而 <mark>libffi</mark> 库正好替我们解决了此难题。


什么是FFI?

FFI(Foreign Function Interface)允许以一种语言编写的代码调用另一种语言的代码,而libffi库提供了最底层的、与架构相关的、完整的FFI。libffi的作用就相当于编译器,它为多种调用规则提供了一系列高级语言编程接口,然后通过相应接口完成函数调用,底层会根据对应的规则,完成数据准备,生成相应的汇编指令代码。

FFI 的核心API

(1)函数原型结构体

typedef struct {
  ffi_abi abi;
  unsigned nargs;
  ffi_type **arg_types;
  ffi_type *rtype;
  unsigned bytes;
  unsigned flags;
#ifdef FFI_EXTRA_CIF_FIELDS
  FFI_EXTRA_CIF_FIELDS;
#endif
} ffi_cif;

(2)封装函数原型

/* 封装函数原型
        ffi_prep_cif returns a libffi status code, of type ffi_status. 
        This will be either FFI_OK if everything worked properly; 
        FFI_BAD_TYPEDEF if one of the ffi_type objects is incorrect; 
        or FFI_BAD_ABI if the abi parameter is invalid.
    */
        ffi_status ffi_prep_cif(ffi_cif *cif,
                    ffi_abi abi,                  //abi is the ABI to use; normally FFI_DEFAULT_ABI is what you want. Multiple ABIs for more information.
                    unsigned int nargs,           //nargs is the number of arguments that this function accepts. ‘libffi’ does not yet handle varargs functions; see Missing Features for more information.
                    ffi_type *rtype,              //rtype is a pointer to an ffi_type structure that describes the return type of the function. See Types.
                    ffi_type **atypes);           //argtypes is a vector of ffi_type pointers. argtypes must have nargs elements. If nargs is 0, this argument is ignored.

(3)函数对象的回调结构体

typedef struct {
#if 0
  void *trampoline_table;
  void *trampoline_table_entry;
#else
  char tramp[FFI_TRAMPOLINE_SIZE];
#endif
  ffi_cif   *cif;
  void     (*fun)(ffi_cif*,void*,void**,void*);
  void      *user_data;
} ffi_closure

(4)函数回调对象的创建

int (* blockImp)(char *);  //声明一个函数指针
ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), &bound_puts);

(5)关联函数原型与回调函数

ffi_status ffi_prep_closure_loc (ffi_closure *closure,  //闭包,一个ffi_closure对象
   ffi_cif *cif,  //函数原型
   void (*fun) (ffi_cif *cif, void *ret, void **args, void*user_data), //函数实体
   void *user_data, //函数上下文,函数实体实参
   void *codeloc)   //函数指针,指向函数实体

将函数的参数等数据信息与回调对象_closure关联起来,当程序调用到此函数时,也会执行此回调地址的代码,同时将获得此函数的所有参数

调用:ffi_prep_closure_loc(closure, &cif, calCircleArea,stdout, bound_calCircleArea)

参数解析:

Prepare a closure function.

参数 closure is the address of a ffi_closure object; this is the writable address returned by ffi_closure_alloc.

参数 cif is the ffi_cif describing the function parameters.

参数 user_data is an arbitrary datum that is passed, uninterpreted, to your closure function.

参数 codeloc is the executable address returned by ffi_closure_alloc.

函数实体 fun is the function which will be called when the closure is invoked. It is called with the arguments:

函数实体参数 cif
The ffi_cif passed to ffi_prep_closure_loc. 
函数实体参数 ret
A pointer to the memory used for the function's return value. fun must fill this, unless the function is declared as returning void. 
函数实体参数 args
A vector of pointers to memory holding the arguments to the function. 
函数实体参数 user_data
The same user_data that was passed to ffi_prep_closure_loc.
ffi_prep_closure_loc will return FFI_OK if everything went ok, and something else on error.

After calling ffi_prep_closure_loc, you can cast codeloc to the appropriate pointer-to-function type.

You may see old code referring to ffi_prep_closure. This function is deprecated, as it cannot handle the need for separate writable and executable addresses.

(6)释放函数回调

 ffi_closure_free(closure);   //释放闭包

如何动态调用 C 函数

(1)步骤:

1. 准备一个函数实体 
2. 声明一个函数指针 
3. 根据函数参数个数/参数及返回值类型生成一个函数原型 
4. 创建一个ffi_closure对象,并用其将函数原型、函数实体、函数上下文、函数指针关联起来 
5. 释放closure

(2)示例代码:

#include <stdio.h>
#include "libffi/ffi.h"
// 函数实体
void calCircleArea(ffi_cif *cif,
                  float *ret,
                  void *args[],
                  FILE *stream) {
    float pi = 3.14;
    float r = **(float **)args[0];
    float area = pi * r * r;
    *ret = area;
    printf("我是那个要被动态调用的函数\n area:%.2f\n *ret = %.2f",area,*ret);
}


int main(int argc, const char * argv[]) {

    ///函数原型
    ffi_cif cif;
    ///参数
    ffi_type *args[1];
    ///回调闭包
    ffi_closure *closure;
    ///声明一个函数指针,通过此指针动态调用已准备好的函数
    float (*bound_calCircleArea)(float *);
    float rc = 0;
    
    /* Allocate closure and bound_calCircleArea */  //创建closure
    closure = ffi_closure_alloc(sizeof(ffi_closure), &bound_calCircleArea);
    
    if (closure) {
        /* Initialize the argument info vectors */
        args[0] = &ffi_type_pointer;
        /* Initialize the cif */  //生成函数原型 &ffi_type_float:返回值类型
        if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1, &ffi_type_float, args) == FFI_OK) {
            /* Initialize the closure, setting stream to stdout */
                // 通过 ffi_closure 把 函数原型_cifPtr / 函数实体JPBlockInterpreter / 上下文对象self / 函数指针blockImp 关联起来
            if (ffi_prep_closure_loc(closure, &cif, calCircleArea,stdout, bound_calCircleArea) == FFI_OK) {
                    float r = 10.0;
                    //当执行了bound_calCircleArea函数时,获得所有输入参数, 后续将执行calCircleArea。
                    //动态调用calCircleArea
                    rc = bound_calCircleArea(&r);
                    printf("rc = %.2f\n",rc);
                }
            }
        }
    /* Deallocate both closure, and bound_calCircleArea */
    ffi_closure_free(closure);   //释放闭包
    return 0;
}

由上可知:如果我们利用好ffi_prep_closure_loc 的第四个参数 user_data,用其传入我们想要的函数实现,将函数实体变成一个通用的函数实体,然后将函数指针改为void*,通过结构体创建一个block保存函数指针并返回,那么我们就可以实现JS调用含有任意类型block参数的OC方法了。

总结

根据以上的思想:

我们可以将 ffi_closure 关联的指针替换原方法的IMP,

当对象收到该方法的消息时 objc_msgSend(id self, SEL sel, ...) ,

将最终执行自定义函数 void ffifunction(fficif *cif, void *ret, void **args, void *userdata) 。

而实现这一切的主要工作是:设计可行的结构,存储类的多个hook信息;

根据包含不同参数的方法和切面block,生成包含匹配 ffi_type 的cif;

替换类某个方法的实现为 ffi_closure 关联的imp,记录hook;

在 ffi_function 里,根据获得的参数,动态调用原始imp和block。

后续我们可以利用这个库,结合lexyacc将OC或任意的语言编译成C代码去执行,从而实现hotfix

猜你喜欢

转载自blog.csdn.net/weixin_33813128/article/details/86784070