在介绍libffi库以前,咱们先来了解一个概念:函数调用约定,由于libffi库的工做原理就是基于这个条件进行的。git
函数调用约定,简而言之就是对函数调用的一些规定,经过遵循这些规定,来确保函数能正常被调用。具体包含如下内容:github
- 参数的传递方式,参数是经过栈传递仍是寄存器传递
- 参数的传递顺序,是从左到右,仍是从右到左
- 栈的维护方式,好比函数调用后参数从栈中弹出是由调用方处理仍是被调用方处理
固然函数调用约定并不是都是统一的,不一样的设备架构体系,对应的规则也是不一样的。好比iOS的
arm
架构和Mac的x86
架构,二者的调用约定是不一样的。数组其实,在平常工做中,一般比较少接触到这个概念。由于编译器已经帮咱们完成了这一工做,咱们只须要遵循正确的语法规则便可,编译器会根据不一样的架构生成对应的汇编代码,从而确保函数调用约定的正确性。xcode
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能够实如今运行时动态调用函数,同时也能够在运行时生成一个指针,绑定到对应的函数,并能接收和解析传递过来的参数。那么它是怎么作到的呢?ide
咱们上面说了函数能正确被调用的前提条件是遵循函数调用约定,而这一工做一般是由编译器负责的,在编译过程当中生成对应的汇编代码。若是咱们想在运行时中去动态调用函数,意味着这个过程是没法被编译的,那么就没法保证函数函数调用约定。而libffi在运行时帮咱们作到作到了这点,它实际上就等同于一个动态的编译器,可以在运行时中完成编辑器在编译时对函数调用约定的处理。函数
了解完libffi的原理以后,接下来,咱们就进入实操过程!post
笔者一开始是按照github上的文档进行操做的,结果发现行不通,提示一堆错误,最终在这里找到了一个能编译成功的版本。下载后,进入到
libffi-master
目录,而后执行如下操做:学习
./autogen.sh
脚本,若是提示出错,多是没下载autoconf
, automake
, libtool
这些库,分别brew install xxx
便可libffi.xcodeproj
libffi-iOS
,而后运行编译Products
中找到生成的库libffi.a
libffi.a
导入到须要使用的工程中,并把include
对应的头文件也添加到工程中。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中
ffi_call
和ffi_prep_closure_loc
两个方法的使用,接下来,咱们将经过这两个方法来看看libffi在iOS中的两个应用。
在某些场景,可能须要对某个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);
复制代码
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来进行不一样的处理。
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
,因此参数解析直接从第三个参数开始。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库的学习,发现libffi的使用简洁,但功能却很是强大,很是适合作一些hook操做。目前,GitHub上也有两个使用libffi来实现hook的开源库BlockHook和stinger,值得你们去探究学习。