如何动态调用 C 函数

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

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

函数地址

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

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

输出:

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

dlsym()

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

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

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

这个例子中 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去调用,这个神奇的方法支持任意返回值任意参数类型和个数,而它的定义仅是这样:

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

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

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

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

libffi

对 libffi 的介绍可以看 [这里],简单来说它就是提供了动态调用任意 C 函数的功能。

先来看看怎样通过 libffi 动态调用一个 C 函数:

看起来挺复杂的,梳理一下就这几步:

  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就是做这层转换的。

总结

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

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

原文地址:https://www.cnblogs.com/feng9exe/p/10397135.html

时间: 2024-12-19 06:31:00

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

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

动态调用DLL函数有时正常,有时报Access violation的异常 typedef int (add *)(int a,int b); void test() { hInst=LoadLibraryA("aimdtl.dll"); (FARPROC &)add=GetProcAddress(hInst,"add"); add(1,2); } 按这个代码执行,add函数有时OK,有时报Access violation的异常.看到提示,第一反应就是内存异常

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

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title></title> </head> <body> <script> function f1() { return 1 + 1; } function f2() { return 1000 + 3002 + "s

Delphi函数动态调用实现免杀

Delphi 源码免杀之函数动态调用实现免杀的下载者自己编译这份代码看看过N多杀软没什么技术含量只是发出来给不懂的人入入门也防止有新人老是来问 ShellApi,URLMon 单元 //Delphi动态调用API函数 procedure TForm1.Button1Click(Sender: TObject); var SourceFile:ansistring; DestFile :ansistring; down2:function(Caller: IUnknown; URL: PChar;

动态调用动态库方法 .so

关于动态调用动态库方法说明 一.       动态库概述 1.  动态库的概念 日常编程中,常有一些函数不需要进行编译或者可以在多个文件中使用(如数据库输入/输 出操作或屏幕控制等标准任务函数).可以事先对这些函数进行编译,然后将它们放置在一些特殊的目标代码文件中,这些目标代码文件就称为库.库文件中的函数 可以通过连接程序与应用程序进行链接,这样就不必在每次开发程序时都对这些通用的函数进行编译了. 动态库是一种在已经编译完毕的程序开始启动运行时,才被加载来调用其中函数的库.其加载方式与静态库截然

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

php中可使用call_user_func进行方法的动态调用,可以动态调用普通函数.类方法以及带参数的类方法1.定义一个普通函数getCurrentDate,用于获取今天日期.call_user_func带上的参数为要被调用的函数名fucntion getCurrentDate(){echo 'getCurrentDate:' . date('Y-m-d');}call_user_func('getCurrentDate'); 程序会自动执行getCurrentDate函数并获得期望的结果get

二十、函数指针高级(动态调用)

1.动态调用是系统普遍支持的技术 2.要实现动态调用需要一个对应表 3.示例:使用函数名的字符表调用函数 函数名  函数指针 “max”   maxValue “min”    minValue “avg”    avgValue 4.动态调用 PFUN getFunctionFromName(char *name); int getValue(int a , int b , char *name); 5.示例: typedef int(*PFUN)(int , int); //相当于把int

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

以下文章出自  http://blog.rongzhiwang.com/king/archive/2012/08/13/javascriptjseval.aspx 今天碰到人问这样一个问题:用jQuery做一个菜单,每次使用时传进去一个参数,这个参数是一个函数名,在菜单中怎么执行这个函数. 大概就这么个意思吧,当时听到就很熟悉的感觉,有的说为什么不用switch case,但是说实话有时候用判断确实不爽,好像自己之前也这样做过,但是真想不起来怎么实现的了.然后搜了一下,找到eval解决,决定记录

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

C#动态调用C++编写的DLL函数 动态加载DLL需要使用Windows API函数:LoadLibrary.GetProcAddress以及FreeLibrary.我们可以使用DllImport在C#中使用这三个函数. [DllImport("Kernel32")] public static extern int GetProcAddress(int handle, String funcname); [DllImport("Kernel32")] public

python 动态调用函数

1. 根据字符串名称 动态调用 python文件内的方法eval("function_name")(参数) 2. 根据字符串 动态调用类中的静态方法,getattr(ClassName,"function_name")(参数) 3. apply(functoin_name,parameters)  这个function_name不是字符串,而是函数对象本身:parameters是参数,类似(a,b,...)这样的格式 4. 当函数不确定参数的数目时候,采用 一个 *