动态调用C函数

函数地址&函数指针

在如何动态调用C函数之前,我们先来看一个demo

int func(int value) {
    return value;
}

int main(int argc, char * argv[]) {
    int (*funcPtr)(int) = &func;
    int value = (*funcPtr)(10); // value = 10
    return 0;
}

这涉及到两个知识点,函数指针,函数地址。

如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

了解到函数指针和函数地址的联系之后,我们也清楚了,若要动态调用C函数,第一步就是需要找到函数地址,也就是这个函数名字符串。通过上述例子,我们其实已经得出C编译后的可执行文件存在原函数名的信息,不过既然是解析,还是有必要去验证一番。

使用otool查看上述demo的汇编,发现函数名字符串的确记录下来了,言验证了上面的结论.

main.o:
(__TEXT,__text) section
_func:
0000000100000f60    pushq   %rbp
0000000100000f61    movq    %rsp, %rbp
0000000100000f64    movl    %edi, -0x4(%rbp)
0000000100000f67    movl    -0x4(%rbp), %eax
0000000100000f6a    popq    %rbp
0000000100000f6b    retq
0000000100000f6c    nopl    (%rax)
_main:
0000000100000f70    pushq   %rbp
0000000100000f71    movq    %rsp, %rbp
0000000100000f74    subq    $0x20, %rsp
0000000100000f78    movl    $0xa, %eax
0000000100000f7d    leaq    _func(%rip), %rcx
0000000100000f84    movl    $0x0, -0x4(%rbp)
0000000100000f8b    movl    %edi, -0x8(%rbp)
0000000100000f8e    movq    %rsi, -0x10(%rbp)
0000000100000f92    movq    %rcx, -0x18(%rbp)
0000000100000f96    movl    %eax, %edi
0000000100000f98    callq   _func
0000000100000f9d    xorl    %edi, %edi
0000000100000f9f    movl    %eax, -0x1c(%rbp)
0000000100000fa2    movl    %edi, %eax
0000000100000fa4    addq    $0x20, %rsp
0000000100000fa8    popq    %rbp
0000000100000fa9    retq

通过函数名得到对应的函数地址之后,这样就可以自由调用所有C函数了吗?答案当然是否定的。还是举一个例子来说明

int func(int n, int m) {
  printf("func");
  return 1;
}

int main() {
  // 1
  int (*funcPtr)(int, int) = &func;
  funcPtr(1, 2);

  // 2
  void (*funcPtr)() = &func;
  funcPtr(1, 2); //error

  return 0;
}

(1表示调用正确定义了函数参数/返回值类型的函数指针,2表示调用没有正确定义参数/返回值类型的函数指针)

PS: 这边想提下dlsym(),这是动态链接器提供的一个 API,本来是用于动态加载库(DLL),然后通过这个接口拿到函数地址,它也可以应用于当前可执行文件镜像,原理是一样的

  // 两者是一致的
  int (*funcPtr)(int, int) = &func;
  int (*funcPtr)(int, int) = dlsym(RTLD_DEFAULT, "func");

这个例子中虽然我们得到了func的函数指针,必须像 1 那样指明它的返回类型和参数类型后,才能调用成功,如果像 2那样定义这个指针,没有正确的参数类型和返回值类型,在调用时就会出现crash。

也就是说我们没法通过定义一个万能的函数指针去支持所有函数的动态调用,这里必须让函数的参数/返回值类型都对应上才能调用,为什么必须要对应上呢?因为函数的调用方和被调用方是会遵循一种叫调用惯例(Calling Convention)的约定的。

调用惯例

一个函数的调用过程中,函数的参数既可以使用栈传递,也可以使用寄存器传递,参数压栈的顺序可以从左到右也可以从右到左,函数调用后参数从栈弹出这个工作可以由函数调用方完成,也可以由被调用方完成。如果函数的调用方和被调用方(函数本身)不遵循统一的约定,有这么多分歧这个函数调用就没法完成。这个双方必须遵守的统一约定就叫做调用惯例(Calling Convention),调用惯例规定了参数的传递的顺序和方式,以及栈的维护方式。

函数调用者和被调用者需要遵循这同一套约定,上述2这样的情况,就是函数本身遵循了这个约定,而调用者没有遵守,导致调用出错。

也就是说如果需要动态调用任意 C 函数,就得先准备好任意 参数类型/参数个数/返回值类型 排列组合的 C 函数指针,让最终的汇编把所有情况都准备好,最后调用时通过 switch 去找到正确的那个去执行就可以了。但显然这是很糟糕的主意。

objc_msgSend

OC 所有方法调用最终都会走到 objc_msgSend去调用,这个神奇的方法支持任意返回值任意参数类型和个数,而它的定义仅是这样:

void objc_msgSend(void /* id self, SEL op, ... */ )

为什么它就可以支持所有函数调用呢,不是说调用者和函数本身要遵循调用惯例吗,这个函数跟我们上述的2有什么区别?

答案是在C语言层面上没区别,但人家在汇编上做了手脚,objc_msgSend是用汇编写的,在调用这个函数之前,会把栈/寄存器等数据都准备好,相当于调用前对参数入栈等处理由这个函数自己写的汇编代码接管了,不需要编译器在调用处去生成这些指令。

libffi

libffi是C的runtime,提供了动态调用任意C函数的动能
先来看看怎样通过libffi动态调用一个C函数

int testFunc(int m, int n) {
  printf("params: %d %d \n", n, m);
  return n+m;
}

int main() {
  //拿函数指针
  void* functionPtr = dlsym(RTLD_DEFAULT, "testFunc");
  int argCount = 2;

  //按ffi要求组装好参数类型数组
  ffi_type **ffiArgTypes = alloca(sizeof(ffi_type *) *argCount);
  ffiArgTypes[0] = &ffi_type_sint;
  ffiArgTypes[1] = &ffi_type_sint;

   //按ffi要求组装好参数数据数组
  void **ffiArgs = alloca(sizeof(void *) *argCount);
  void *ffiArgPtr = alloca(ffiArgTypes[0]->size);
  int *argPtr = ffiArgPtr;
  *argPtr = 1;
  ffiArgs[0] = ffiArgPtr;

  void *ffiArgPtr2 = alloca(ffiArgTypes[1]->size);
  int *argPtr2 = ffiArgPtr2;
  *argPtr2 = 2;
  ffiArgs[1] = ffiArgPtr2;

  //生成 ffi_cfi 对象,保存函数参数个数/类型等信息,相当于一个函数原型
  ffi_cif cif;
  ffi_type *returnFfiType = &ffi_type_sint;
  ffi_status ffiPrepStatus = ffi_prep_cif_var(&cif, FFI_DEFAULT_ABI, (unsigned int)0, (unsigned int)argCount, returnFfiType, ffiArgTypes);

  if (ffiPrepStatus == FFI_OK) {
    //生成用于保存返回值的内存
    void *returnPtr = NULL;
    if (returnFfiType->size) {
      returnPtr = alloca(returnFfiType->size);
    }
    //根据cif函数原型,函数指针,返回值内存指针,函数参数数据调用这个函数
    ffi_call(&cif, functionPtr, returnPtr, ffiArgs);

    //拿到返回值
    int returnValue = *(int *)returnPtr;
    printf("ret: %d \n", returnValue);
  }
}

梳理下主要的流程

  1. 获取函数指针
  2. 给每个参数申请内存空间,封装成参数类型数组
  3. 生成ffi_cfi对象,生成模板
  4. ffi_call调用

这里每一步都可以在运行时动态去做,也就做到了在运行时动态调用任意C函数

这里最终 libffi 能调用任意 C 函数的原理跟上面说的 objc_msgSend的原理差不多,ffi_call底层是用汇编实现的,它在调用我们传入的函数之前,会根据上面提到的函数原型 cif 和参数数据,把参数都按规则塞到栈/寄存器里,准备好数据和状态,这样调用的函数实体里就可以按规则取到这些参数,正常执行了。调用完再获取返回值,清理这些栈帧/寄存器数据。libffi 针对每个架构不同的 Calling Convention 写了不同的汇编代码去做这个事。

引申

上面讲到了使用libffi来动态调用,那假如支持任意参数类型的C函数呢?
常规我们能想到的:
对所有参数类型和个数进行排列组合,然后静态声明N个函数,在运行时根据参数类型个数分配对应的函数
那有其他方法动态定义对应的函数吗?

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

推荐阅读更多精彩内容