在介紹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,值得你們去探究學習。