JSPatch 支持了动态调用 C 函数,无需在编译前桥接每个要调用的 C 函数,只需要在 JS 里调用前声明下这个函数,就可以直接调用:

require('JPEngine').addExtensions(['JPCFunction'])
defineCFunction("malloc", "void *, size_t")
malloc(10)

我们一步步来看看怎样可以做到动态调用 C 函数。

函数地址

首先若要动态调用 C 函数,第一步就是需要通过传入一个函数名字符串找到这个函数地址,这里一个必要的前提条件就是 C 编译后的可执行文件里必须有原函数名的信息,才有可能做到通过函数名字符串找到函数地址。我们写个简单的程序来看看它编译后可执行文件的内容有没有这个信息:

//main.m
void test() {
} int main() {
return 0;
}

编译这个文件,并用otool看下它的汇编:

gcc main.m -o main.o
otool -tV main.o

输出:

main.o:
(__TEXT,__text) section
_test:
0000000100000f90 pushq %rbp
0000000100000f91 movq %rsp, %rbp
0000000100000f94 popq %rbp
0000000100000f95 retq
0000000100000f96 nopw %cs:(%rax,%rax)
_main:
0000000100000fa0 pushq %rbp
0000000100000fa1 movq %rsp, %rbp
0000000100000fa4 xorl %eax, %eax
0000000100000fa6 movl $0x0, -0x4(%rbp)
0000000100000fad popq %rbp
0000000100000fae retq

可以看到函数名 test 和 main 都清楚地记录在可执行文件里,只不过前面多了个下划线_,所以完全可以在运行时通过函数名字符串查到这个函数地址。

dlsym()

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

void test() {
printf("testFunc");
} int main() {
void (*funcPointer)() = dlsym(RTLD_DEFAULT, "test");
funcPointer();
return 0;
}

好了现在我们可以通过函数名拿到对应的函数地址了,这样就可以自由动态调用所有 C 函数了吗?还不行,这样只能动态调用返回值和参数都为空的 C 函数,上面 funcPointer指针只能在指向参数返回值都为空的函数时才能正确调用到。对于有返回值和有参数的 C 函数,这里定义时需要指明参数和返回值类型才能使用:

int testFunc(int n, int m) {
printf("testFunc");
return 1;
} int main() {
// ①
int (*funcPointer)(int, int) = dlsym(RTLD_DEFAULT, "testFunc");
funcPointer(1, 2); // ②
void (*funcPointer)() = dlsym(RTLD_DEFAULT, "testFunc");
funcPointer(1, 2); //error return 0;
}

(这里①和②两个调用方式下文会多次提到,①表示调用正确定义了函数参数/返回值类型的函数指针,②表示调用没有正确定义参数/返回值类型的函数指针)

这个例子中 dlsym 返回了 testFunc 的函数指针,必须像 ① 那样指明它的返回类型和参数类型后,才能调用成功,如果像 ② 那样定义这个指针,没有正确的参数类型和返回值类型,在调用时就会出现crash。

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

Calling Convention

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

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

再简单分析下,如果按①那样正确的定义方式定义funcPointer,然后调用它,这里编译成汇编后,在调用处会有相应指令把参数 n,m 的值 1 和 2 入栈,然后跳过去 testFunc()函数实体执行,这个函数执行时,按约定它知道n,m两个参数值已经在栈上,就可以取出来使用了。

而如果按②那样定义,编译后这里不会把参数 n,m 的值 1 和 2 入栈,因为这里编译器把它当成了没有参数和没有返回值的函数,也就不需要进行参数入栈的操作,然后在 testFunc()函数实体里按约定去栈上取参数时就会发现栈上本来应该存参数 n 和 m 的地方并没有数据,或者是其他错误的数据,导致调用出错。

所以你需要在调用前明确告诉编译器这个函数的参数和返回值类型是什么,编译器才能生成对应的正确的汇编代码,让被调用的函数执行时能正常取到参数。

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

在 C 语言这个层面上是解决不了这个问题的,要解决只能再往底层走,靠汇编。

(P.S. 在不同 CPU 架构上调用惯例不同,例如arm32位所有参数都通过栈传递,arm64位会让部分参数通过寄存器传递,超出寄存器大小的参数才通过栈传递,因为64位机器多出了寄存器,通过寄存器传递比栈快。不过就算所有CPU架构调用惯例相同,也不影响我们碰到的这个问题,你可以忽略这点。)

objc_msgSend

实际上你会发现 OC 上有个函数脱离了上述限制,就是 objc_msgSend。OC 所有方法调用最终都会走到 objc_msgSend去调用,这个神奇的方法支持任意返回值任意参数类型和个数,而它的定义仅是这样:

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

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

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

这里会在调用真正的函数之前,根据 Calling Convention 准备好栈帧/寄存器数据和状态,最后再 jump/call 到函数实体执行就可以了,这时函数实体按约定去取参数是取得到的,可以正常执行。于是 objc 就做到了在编译前只需要定义一个简单的 objc_msgSend,就支持运行时动态调用任意类型的 C 函数(所有 OC 方法的 IMP)。

所以我们要仿照 objc_msgSend做一遍这个事情吗?难度好高:(。不用怕, libffi 这个神器已经帮你做了。

libffi

对 libffi 的介绍可以看 [这里],简单来说它就是提供了动态调用任意 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. 通过 dlsym 拿到函数指针。
  2. 给每个参数申请内存空间,按 ffi 要求把参数数据组装成数组。(用alloca()申请空间,不需要free()去释放)
  3. 用函数参数个数/参数类型/返回值类型组装成 cif 对象,表示这个函数原型。(有点像OC的methodSignature)
  4. 申请内存空间用于保存函数返回值。
  5. 把 cif 函数原型,函数指针,返回值内存指针,参数数据 传入 ffi_call调用这个函数。

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

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

到这里已经完成了动态调用 C 函数,接下来的工作就只是在 JS 和 libffi 之间加一层转换,就可以让 JSPatch 支持动态调用 C 函数了,JPCFunction就是做这层转换的。

JPCFunction

目前 JPCFunction比较简单,直接看代码就可以了,简单说下流程:

  1. 调用 C 函数之前需要通过 defineCFunction定义这个函数的各参数类型和返回值类型,defineCFunction 里解析了类型字符串,转换成一个 JPCFunctionSignature对象,每个函数对应一个 JPCFunctionSignature对象,这里模拟了 OC 方法的NSMethodSignature。
  2. 调用函数时根据函数名拿到 JPCFunctionSignature对象,集齐了 参数个数/各参数类型/返回值类型/各参数数据 这些信息,组装成 ffi 需要的格式进行调用。

这里第二步的处理中对于 struct 类型会比较麻烦,目前还未支持参数/返回值类型为 struct 的 C 函数,后续会补上。

总结

回顾下动态调用 C 函数的探索过程,先是通过 dlsym()拿到函数指针,然后需要告诉编译器这个函数的参数/返回值类型,编译器才会根据 Calling Convention 约定生成对应的汇编代码,在调用函数时对参数进行正确的入栈/存入寄存器等操作,让函数成功调用,这一步在运行时在 C 语言层面上无法做到,所以 objc_msgSend()和 libffi 都用汇编模拟了这一过程,达到动态调用 C 函数的目的。

http://blog.cnbang.net/tech/3219/

如何动态调用 C 函数的更多相关文章

  1. 动态调用DLL函数有时正常,有时报Access violation的异常

    动态调用DLL函数有时正常,有时报Access violation的异常 typedef int (add *)(int a,int b); void test() {     hInst=LoadL ...

  2. 【php 之根据函数名称动态调用该函数】

    解释函数:call_user_func()以及函数call_user_func_array() 对于PHP程序员而言,函数是再熟悉不过的事物了,毕竟我们整天都在和PHP内置函数以及我们自定义的函数打交 ...

  3. 几种动态调用js函数方案的性能比较

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. [源码]Delphi源码免杀之函数动态调用 实现免杀的下载者

    [免杀]Delphi源码免杀之函数动态调用 实现免杀的下载者 2013-12-30 23:44:21         来源:K8拉登哥哥's Blog   自己编译这份代码看看 过N多杀软  没什么技 ...

  5. php中怎么使用call_user_func动态调用方法

    php中可使用call_user_func进行方法的动态调用,可以动态调用普通函数.类方法以及带参数的类方法1.定义一个普通函数getCurrentDate,用于获取今天日期.call_user_fu ...

  6. C#动态调用C++编写的DLL函数

    C#动态调用C++编写的DLL函数 动态加载DLL需要使用Windows API函数:LoadLibrary.GetProcAddress以及FreeLibrary.我们可以使用DllImport在C ...

  7. Cortex-M3动态加载三(模块调用系统函数)

    在我的arm动态加载实验中需要解决一个模块调用系统函数的问题,可以使用以下的一个方法.将系统函数固定在某一段地址空间,然后导出这一块的符号表到符号文件中,要记载的模块link的时候使用这个符号表文件, ...

  8. [转]JavaScript通过参数动态调用函数——js中eval实现反射

    以下文章出自  http://blog.rongzhiwang.com/king/archive/2012/08/13/javascriptjseval.aspx       今天碰到人问这样一个问题 ...

  9. 在动态THML语句中调用JS函数传递带空格参数的问题

    刚刚遇到一个问题,调用js函数的参数里带空格,造成调用失败的问题.   部分代码如下: html+="<div><a href=javascript:confirm(&qu ...

随机推荐

  1. java 生成Excel开门篇

    本随笔的Excel所用的poi jar包(3.17版本)链接: https://pan.baidu.com/s/1gaa3dJueja8IraUDYCSLIQ 提取密码: 9xr7 简单实现:两个类: ...

  2. 【 js 片段 】点击空白或者页面其他地方,关闭弹框

    $(document).mouseup(function(e){ var _con = $(' 目标区域 '); // 设置目标区域 if(!_con.is(e.target) && ...

  3. C# 如何在Linux操作系统下读取文件

    发布在Window环境上的微服务需要部署在Linux环境上,本以为没有什么问题,结果因为一处读取文件路径的原因报错了,在此记录一下两个问题:1.C#如何判断当前运行环境是什么操作系统:2.C#读取文件 ...

  4. [AngularJS] “路由”的定义概念、使用详解——AngularJS学习资料教程

    这是小编的一些学习资料,理论上只是为了自己以后学习需要的,但是还是需要认真对待的 以下内容仅供参考,请慎重使用学习 AngularJS“路由”的定义概念 AngularJS最近真的很火,很多同事啊同学 ...

  5. 模拟时钟(AnalogClock)

    模拟时钟(AnalogClock) 显示一个带时钟和分针的表面 会随着时间的推移变化 常用属性: android:dial 可以为表面提供一个自定义的图片 下面我们直接看代码: 1.Activity ...

  6. css文字飞入效果

    一.页面的主体布局 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> & ...

  7. runloop timer

    RunLoop这个东西,其实我们一直在用,但一直没有很好地理解它,或者甚至没有知道它的存在.RunLoop可以说是每个线程都有的一个对象,是用来接受事件和分配任务的loop.永远不要手动创建一个run ...

  8. Dlink DIR-823G 漏洞挖掘过程

    前言 本文由 本人 首发于 先知安全技术社区: https://xz.aliyun.com/u/5274 初步分析 首先下载固件 https://gitee.com/hac425/blog_data/ ...

  9. JVM知识(二):类加载器原理

    我们知道我们编写的java代码,会经过编译器编译成字节码(class文件),再把字节码文件装载到JVM中,最后映射到各个内存区域中,我们的程序就可以在内存中运行了.那么问题来了,这些字节码文件是怎么装 ...

  10. Vue2学习笔记:事件对象、事件冒泡、默认行为

    1.事情对象 <!DOCTYPE html> <html> <head> <title></title> <meta charset= ...