libffi探究
2020/6/1 23:27:13
本文主要是介绍libffi探究,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
一、函数调用约定(Calling Convention)
在介绍libffi库之前,我们先来了解一个概念:函数调用约定,因为libffi库的工作原理就是基于这个条件进行的。
函数调用约定,简而言之就是对函数调用的一些规定,通过遵循这些规定,来确保函数能正常被调用。具体包含以下内容:
- 参数的传递方式,参数是通过栈传递还是寄存器传递
- 参数的传递顺序,是从左到右,还是从右到左
- 栈的维护方式,比如函数调用后参数从栈中弹出是由调用方处理还是被调用方处理
当然函数调用约定并非都是统一的,不同的设备架构体系,对应的规则也是不同的。比如iOS的
arm
架构和Mac的x86
架构,两者的调用约定是不同的。其实,在日常工作中,通常比较少接触到这个概念。因为编译器已经帮我们完成了这一工作,我们只需要遵循正确的语法规则即可,编译器会根据不同的架构生成对应的汇编代码,从而确保函数调用约定的正确性。
二、libffi的使用
libffi is a foreign function interface library. It provides a C programming language interface for calling natively compiled functions given information about the target function at run time instead of compile time. It also implements the opposite functionality: libffi can produce a pointer to a function that can accept and decode any combination of arguments defined at run time.
引用一段wiki上对libffi的介绍。
简单来说,libffi可以实现在运行时动态调用函数,同时也可以在运行时生成一个指针,绑定到对应的函数,并能接收和解析传递过来的参数。那么它是怎么做到的呢?
我们上面说了函数能正确被调用的前提条件是遵循函数调用约定,而这一工作通常是由编译器负责的,在编译过程中生成对应的汇编代码。如果我们想在运行时中去动态调用函数,意味着这个过程是无法被编译的,那么就无法保证函数函数调用约定。而libffi在运行时帮我们做到做到了这点,它实际上就等同于一个动态的编译器,能够在运行时中完成编辑器在编译时对函数调用约定的处理。
了解完libffi的原理之后,接下来,我们就进入实操过程!
a. libffi的导入
笔者一开始是按照github上的文档进行操作的,结果发现行不通,提示一堆错误,最终在这里找到了一个能编译成功的版本。下载后,进入到
libffi-master
目录,然后执行以下操作:
- 运行
./autogen.sh
脚本,如果提示出错,可能是没下载autoconf
,automake
,libtool
这些库,分别brew install xxx
即可 - 运行
libffi.xcodeproj
- 选择
libffi-iOS
,然后运行编译 - 不出意外,就能编译成功,然后在
Products
中找到生成的库libffi.a
- 将
libffi.a
导入到需要使用的工程中,并把include
对应的头文件也添加到工程中。
b. libffi的使用
ffi_call
调用函数
int func1(int a, int b) { return a + b; } - (void)libffiTest { //1. ffi_type **argTypes; argTypes = malloc(sizeof(ffi_type *) * 2); argTypes[0] = &ffi_type_sint; argTypes[1] = &ffi_type_sint; //2. ffi_type *retType = &ffi_type_sint; //3. ffi_cif cif; ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, retType, argTypes); //4. void **args = malloc(sizeof(void *) * 2); int x = 1, y = 2; args[0] = &x; args[1] = &y; int ret; //5. ffi_call(&cif, (void(*)(void))func1, &ret, args); NSLog(@"libffi return value: %d", ret); } 运行结果:libffi return value: 3 复制代码
如上所示,通过ffi_call
方法实现了函数func1
的调用,我们来具体分析下整个流程。
- 定义函数的参数类型,
func1
的参数为两个int
类型,这里使用argTypes
指针数组,先创建对应的大小,然后分别赋值int
对应的ffi_type_sint
类型。 - 定义函数的返回类型,
func1
的返回类型为int
,所以retType
赋值为ffi_type_sint
。 - 定义函数模板
ffi_cif
, 通过ffi_prep_cif
创建对应的函数模板,第一个参数为ffi_cif
模板;第二个参数表示不同CPU架构下的ABI,通常选择FFI_DEFAULT_ABI
,会根据不同CPU架构选择到对应的ABI;第三个参数为函数参数个数;第四个参数为定义的函数参数类型;最后一个参数为函数返回值类型。 - 创建函数对应的参数值和返回值。
- 调用
ffi_call
方法,分别传入函数模板cif
,绑定的函数func1
,函数返回值ret
和函数参数args
。
ffi_prep_closure_loc
绑定函数指针
- (void)libffiBindTest { //1. ffi_type **argTypes; ffi_type *returnTypes; argTypes = malloc(sizeof(ffi_type *) * 2); argTypes[0] = &ffi_type_sint; argTypes[1] = &ffi_type_sint; returnTypes = malloc(sizeof(ffi_type *)); returnTypes = &ffi_type_pointer; ffi_cif *cif = malloc(sizeof(ffi_cif)); ffi_status status = ffi_prep_cif(cif, FFI_DEFAULT_ABI, 2, returnTypes, argTypes); if (status != FFI_OK) { NSLog(@"ffi_prep_cif return %u", status); return; } //2. char* (*funcInvoke)(int, int); //3. ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), &funcInvoke); //4. status = ffi_prep_closure_loc(closure, cif, bind_func, (__bridge void *)self, funcInvoke); if (status != FFI_OK) { NSLog(@"ffi_prep_closure_loc return %u", status); return; } //5. char *result = funcInvoke(2, 3); NSLog(@"libffi return func value: %@", [NSString stringWithUTF8String:result]); ffi_closure_free(closure); } // 6. void bind_func(ffi_cif *cif, char **ret, int **args, void *userdata) { //7. LibffiViewController *viewController = (__bridge LibffiViewController *)userdata; int value1 = viewController.value; int value2 = *args[0]; int value3 = *args[1]; const char *result = [[NSString stringWithFormat:@"str-%d", (value1 + value2 + value3)] UTF8String]; //8. *ret = result; } 输出结果:libffi return func value: str-6 复制代码
- 这一段主要是创建函数模板
ffi_cif
,具体过程跟上一个例子一样,这里就不重复了。 - 定义一个用来绑定的函数指针
funcInvoke
。 - 创建一个
ffi_closure
对象,并将funcInvoke
函数指针传递进去。 - 通过
ffi_prep_closure_loc
方法将ffi_clousure
对象、函数模板cif
、绑定的函数bind_func
、绑定函数bind_func
中传递的数据、函数指针funcInvoke
等绑定在一起。 - 调用函数指针,会进入到绑定的函数
bind_func
中。 - 回调函数
bind_func
的参数类型分别是:函数模板ffi_cif
,函数返回类型指针,函数参数类型指针,ffi_prep_closure_loc
中传递进来的数据。 - 获取到对应的参数值,以及传入的
self
对象,然后进行相关逻辑处理。 - 最后将处理的结果返回给
ret
指针对象,作为返回值。
三、libffi的应用
上面讲解了libffi中
ffi_call
和ffi_prep_closure_loc
两个方法的使用,接下来,我们将通过这两个方法来看看libffi在iOS中的两个应用。
a. block hook
在某些场景,可能需要对某个block进行hook,以实现在block调用前后插入相关代码,或替换该block等功能。
我们知道Block实际上为一个struct对象,其对应的结构类型如下:
struct Block_layout { void *isa; volatile int32_t flags; // contains ref count int32_t reserved; BlockInvokeFunction invoke; struct Block_descriptor_1 *descriptor; // imported variables }; typedef void(*BlockInvokeFunction)(void *, ...); 复制代码
其中结构体中的invoke
表示block对应的函数指针,那么如果我们想对block进行hook,就可以考虑从这里下手——替换invoke
函数指针。因此,我们可以先定义一个新的函数指针newInvoke
,然后使用libffi将该指针绑定到对应的回调函数中,最后将block的invoke
指针替换为newInvoke
。这样,当block调用时,就会进入到libffi绑定的回调函数里,那么就可以在这里做一些额外的操作了。
清楚整体流程后,我们便逐一来进行,首先第一步是需要将block转换为对应的结构体,这样我们才能拿到其invoke
函数指针。
struct JBlockLiteral { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct JBlockDecriptor1 *descriptor; }; struct JBlockLiteral *blockRef = (__bridge struct JBlockLiteral *)self.originalBlock; self.originalInvoke = blockRef->invoke; 复制代码
转换的方式也非常容易,先定义一个与block内部相同结构的结构体JBlockLiteral
,然后使用__bridge
强制转换即可,这样就可以获取到其invoke
函数指针了。
获取到block的函数指针后,就可以定义一个新的函数指针,替换掉block的函数指针
void *newInvoke; blockRef->invoke = newInvoke; 复制代码
当然直接这么做是会有问题的,因为newInvoke
还是个未处理的野指针,我们需要通过libffi对其进行处理,并与回调函数进行绑定。
通过上面libffi的两个例子,我们知道首先需要创建对应的函数模板ffi_cif
,而创建模板是需要知道函数参数和返回值类型的,所以得先获取到block对应的参数和返回值类型。通常我们可以将block转换为struct结构体,然后获取到它的函数签名,最后从函数签名中获取到参数和返回值类型。
NSMethodSignature* NSMethodSignatureForBlock(id block) { struct JBlockLiteral *blockRef = (__bridge struct JBlockLiteral *)block; if (!(blockRef->flags & JBLOCK_HAS_SIGNATURE)) { return nil; } void *desc = blockRef->descriptor; desc += sizeof(struct JBlockDecriptor1); if (blockRef->flags & JBLOCK_HAS_COPY_DISPOSE) { desc += sizeof(struct JBlockDecriptor2); } struct JBlockDecriptor3 *desc3 = (struct JBlockDecriptor3 *)desc; const char *signature = desc3->signature; if (signature) { return [NSMethodSignature signatureWithObjCTypes:signature]; } return nil; } NSMethodSignature *signature = NSMethodSignatureForBlock(self.originalBlock); NSUInteger arguments = signature.numberOfArguments; 复制代码
关于block的签名获取这里就不细讲了,之前在对Block的一些理解这篇文章中已经讲解过了,所以直接看到模板创建部分吧。
ffi_type **argTypes = malloc(sizeof(ffi_type *) * arguments); //1. argTypes[0] = &ffi_type_pointer; //第一个参数为block本身 for (int i = 1; i < arguments; i ++) { const char *argType = [signature getArgumentTypeAtIndex:i]; argTypes[i] = [self ffi_typeForTypeEncoding:argType]; } //2. const char *returnTypeEncoding = signature.methodReturnType; ffi_type *returnType = [self ffi_typeForTypeEncoding:returnTypeEncoding]; //3. ffi_cif *cif = malloc(sizeof(ffi_cif)); ffi_status status = ffi_prep_cif(cif, FFI_DEFAULT_ABI, (int)arguments, returnType, argTypes); 复制代码
- block的参数列表中,第一个为block本身,所以第一个位置放置
ffi_type_pointer
,然后根据参数的type encoding
来设置对应的类型。
- (ffi_type *)ffi_typeForTypeEncoding:(const char *)encoding { if (!strcmp(encoding, "c")) { return &ffi_type_schar; } else if (!strcmp(encoding, "i")) { return &ffi_type_sint; } else if (!strcmp(encoding, "@")) { return &ffi_type_pointer; } // .... return &ffi_type_pointer; } 复制代码
这里只是罗列了一部分,完整部分可以参考这里
-
返回值类型也是类似,先获取到对应的
type encoding
,然后再设置对应的类型。 -
传入对应的参数,创建函数模板
cif
。
void *newInvoke; ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), &newInvoke); status = ffi_prep_closure_loc(closure, cif, BlockInvokeFunc, (__bridge void *)self, newInvoke); 复制代码
创建模板后,通过ffi_prep_closure_loc
将指针newInvoke
和回调函数BlockInvokeFunc
绑定在一起。
struct JBlockLiteral *blockRef = (__bridge struct JBlockLiteral *)self.originalBlock; self.originalInvoke = blockRef->invoke; blockRef->invoke = newInvoke; 复制代码
将block的invoke
函数指针保存到originalInvoke
中(以便后面的使用),然后使用newInvoke
替换为block的函数指针。意味着,当block调用时,newInvoke
会被调用,其绑定的回调函数BlockInvokeFunc
也会被调用。
通常对于block的hook处理一般为block调用前后插入代码或使用其他的block替换。因此,我们可以定义三种mode来表示不同的场景。
typedef enum : NSUInteger { JBlockHookModeBefore, JBlockHookModeInstead, JBlockHookModeAfter, } JBlockHookMode; 复制代码
在回调函数中,根据传入的不同的mode
来分别进行处理
void BlockInvokeFunc(ffi_cif *cif, void *ret, void **args, void *userdata) { JBlockHook *blockHook = (__bridge JBlockHook *)userdata; JBlockHookMode mode = blockHook.mode; id handleBlock = blockHook.handleBlock; void *invoke = blockHook.originalInvoke; switch (mode) { case JBlockHookModeBefore:{ invokeHandleBlock(handleBlock, args, YES); invokeOriginalBlockOrMethod(cif, ret, args, invoke); } break; case JBlockHookModeInstead: { invokeHandleBlock(handleBlock, args, YES); } break; case JBlockHookModeAfter: { invokeOriginalBlockOrMethod(cif, ret, args, invoke); invokeHandleBlock(handleBlock, args, YES); } break; } } 复制代码
JBlockHook
为自定义的一个对象,用来封装hook相关信息,分别获取到mode
、插入(或替换)的block,以及原本的block。- 根据
mode
的值,分别进行对应的处理。invokeHandleBlock
为调用新添加的block,invokeOriginalBlockOrMethod
为调用原来的block。
void invokeHandleBlock(id handleBlock, void **args, BOOL isBlock) { NSMethodSignature *signature = NSMethodSignatureForBlock(handleBlock); NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; int offset = isBlock ? 1 : 2; for (int i = 0; i < signature.numberOfArguments-1; i ++) { [invocation setArgument:args[i+offset] atIndex:i+1]; } [invocation invokeWithTarget:handleBlock]; } 复制代码
我们知道NSInvocation
可以对某个对象直接发送消息,不过需要获取到方法签名,所以首先获取到block的函数签名,然后将block的参数分别设置到invocation
中,最后调用invokeWithTarget
方法即可调用。由于block的第一个参数为自身,所以我们从args
的第二个位置开始取值。
void invokeOriginalBlockOrMethod(ffi_cif *cif, void *ret, void **args, void *invoke) { if (invoke) { ffi_call(cif, invoke, ret, args); } } 复制代码
原本block的调用:我们之前存储了block原本的invoke
函数指针,所以这里可以使用ffi_call
直接调用原本的函数指针,并传入对应的参数和返回值即可。
至此,block的hook工作就完成,外部调用如下:
- (void)blockHook { int (^block)(int, int) = ^int(int x, int y) { int result = x + y; NSLog(@"%d + %d = %d", x, y, result); return result; }; [JBlockHook hookBlock:block mode:JBlockHookModeBefore handleBlock:^(int x, int y){ NSLog(@"hook block call before with %d, %d", x, y); }]; [JBlockHook hookBlock:block mode:JBlockHookModeAfter handleBlock:^(int x, int y){ NSLog(@"hook block call after with %d, %d", x, y); }]; block(2, 3); } 输出结果: 2020-06-01 11:15:49.387353+0800 JOCDemos[6713:99228] hook block call before with 2, 3 2020-06-01 11:15:49.387890+0800 JOCDemos[6713:99228] 2 + 3 = 5 2020-06-01 11:15:49.388672+0800 JOCDemos[6713:99228] hook block call after with 2, 3 复制代码
小结
hook block的本质就是通过替换block的invoke函数指针,并使用libffi将新的函数指针绑定到对应的回调函数中,在回调函数中根据不同mode来进行不同的处理。
b. hook method
block的hook是通过替换其invoke指针,那么method的hook呢?其实也是类似的,我们知道每个OC方法都会有一个对应的
IMP
指针,该指针指向的是方法对应的实现。如果想要对方法进行hook,那么可以考虑通过替换方法对应的IMP
指针。
话不多说,直接来看代码:
//1. Method method = class_getInstanceMethod(cls, sel); const char *methodTypeEncoding = method_getTypeEncoding(method); NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:methodTypeEncoding]; NSUInteger argumentsNum = signature.numberOfArguments; //2. ffi_type **argTypes = malloc(sizeof(ffi_type *) * (argumentsNum)); argTypes[0] = &ffi_type_pointer; argTypes[1] = &ffi_type_pointer; for (int i = 2; i < argumentsNum; i ++) { const char *argType = [signature getArgumentTypeAtIndex:i]; argTypes[i] = [self ffi_typeForTypeEncoding:argType]; } ffi_type *returnType = [self ffi_typeForTypeEncoding:signature.methodReturnType]; //3. ffi_cif *cif = malloc(sizeof(ffi_cif)); ffi_status status = ffi_prep_cif(cif, FFI_DEFAULT_ABI, (int)argumentsNum, returnType, argTypes); if (status != FFI_OK) { NSLog(@"ffi_prep_cif return: %u", status); return; } void *methodInvoke; ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), &methodInvoke); status = ffi_prep_closure_loc(closure, cif, methodInvokeFunc, (__bridge void *)self, methodInvoke); if (status != FFI_OK) { NSLog(@"ffi_prep_closure_loc return: %u", status); return; } 复制代码
- 通过传入的
Class
和SEL
,可以获取到具体的method
对象,然后根据method
的typeEncoding
获取到方法的函数签名。 - 因为函数签名的参数中前两个参数分别为方法本身和
_cmd
,所以参数解析直接从第三个参数开始。 - 获取到对应的参数和返回值后,下面的过程就和block hook的处理一致。
IMP originalIMP = method_getImplementation(method); self.originalIMP = originalIMP; IMP replaceIMP = methodInvoke; if (!class_addMethod(cls, sel, replaceIMP, methodTypeEncoding)) { class_replaceMethod(cls, sel, replaceIMP, methodTypeEncoding); } 复制代码
获取到method
的IMP
指针,然后通过addMethod
或replaceMethod
的方法将上面ffi_prep_closure_loc
处理的指针替换方法原来的IMP
指针。
这样当方法被调用时,methodInvoke
指针就会被触发,其绑定的回调方法methodInvokeFunc
就会被调用。
void methodInvokeFunc(ffi_cif *cif, void *ret, void **args, void *userdata) { JBlockHook *hook = (__bridge JBlockHook *)userdata; JBlockHookMode mode = hook.mode; id handleBlock = hook.handleBlock; IMP originalIMP = hook.originalIMP; switch (mode) { case JBlockHookModeBefore:{ invokeHandleBlock(handleBlock, args, NO); invokeOriginalBlockOrMethod(cif, ret, args, (void *)originalIMP); } break; case JBlockHookModeInstead: { invokeHandleBlock(handleBlock, args, NO); } break; case JBlockHookModeAfter: { invokeOriginalBlockOrMethod(cif, ret, args, (void *)originalIMP); invokeHandleBlock(handleBlock, args, NO); } break; } } 复制代码
这里的处理方式与BlockInvokeFunc
类似,根据mode
的值,分别进行不同的操作。
void invokeHandleBlock(id handleBlock, void **args, BOOL isBlock) { NSMethodSignature *signature = NSMethodSignatureForBlock(handleBlock); NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; int offset = isBlock ? 1 : 2; for (int i = 0; i < signature.numberOfArguments-1; i ++) { [invocation setArgument:args[i+offset] atIndex:i+1]; } [invocation invokeWithTarget:handleBlock]; } 复制代码
这里要注意的是args
对于method hook
需要从第三个位置取值,因为前两个位置分别放置了self
和_cmd
。
至此,method的hook工作就完成了,外部调用如下:
- (void)libffiMethod:(NSString *)value { NSLog(@"libffi method call: %@", value); } - (void)libffiHookMethod { [JBlockHook hookSel:@selector(libffiMethod:) forCls:self.class mode:JBlockHookModeInstead handleBlock:^(NSString *value){ NSLog(@"hook method call instead with : %@", value); }]; } 输出结果: hook method call instead with : hook-method 复制代码
小结
通过替换方法的IMP指针即可达到hook method目的,与hook block的原理类似。
- libffi demo
四、总结
笔者通过这几天对libffi库的学习,发现libffi的使用简洁,但功能却非常强大,非常适合做一些hook操作。目前,GitHub上也有两个使用libffi来实现hook的开源库BlockHook和stinger,值得大家去探究学习。
参考资料
- Hook Objective-C Block with Libffi
- libffi:动态调用和定义 C 函数
这篇关于libffi探究的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值