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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容