一、函数调用约定(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库的学习,发现libffi的使用简洁,但功能却非常强大,非常适合做一些hook操作。目前,GitHub上也有两个使用libffi来实现hook的开源库BlockHook和stinger,值得大家去探究学习。