libffi探究

1、函數調用約定(Calling Convention)

在介紹libffi庫以前,咱們先來了解一個概念:函數調用約定,由於libffi庫的工做原理就是基於這個條件進行的。git

函數調用約定,簡而言之就是對函數調用的一些規定,經過遵循這些規定,來確保函數能正常被調用。具體包含如下內容:github

  • 參數的傳遞方式,參數是經過棧傳遞仍是寄存器傳遞
  • 參數的傳遞順序,是從左到右,仍是從右到左
  • 棧的維護方式,好比函數調用後參數從棧中彈出是由調用方處理仍是被調用方處理

固然函數調用約定並不是都是統一的,不一樣的設備架構體系,對應的規則也是不一樣的。好比iOS的arm架構和Mac的x86架構,二者的調用約定是不一樣的。數組

其實,在平常工做中,一般比較少接觸到這個概念。由於編譯器已經幫咱們完成了這一工做,咱們只須要遵循正確的語法規則便可,編譯器會根據不一樣的架構生成對應的彙編代碼,從而確保函數調用約定的正確性。xcode

2、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能夠實如今運行時動態調用函數,同時也能夠在運行時生成一個指針,綁定到對應的函數,並能接收和解析傳遞過來的參數。那麼它是怎麼作到的呢?ide

咱們上面說了函數能正確被調用的前提條件是遵循函數調用約定,而這一工做一般是由編譯器負責的,在編譯過程當中生成對應的彙編代碼。若是咱們想在運行時中去動態調用函數,意味着這個過程是沒法被編譯的,那麼就沒法保證函數函數調用約定。而libffi在運行時幫咱們作到作到了這點,它實際上就等同於一個動態的編譯器,可以在運行時中完成編輯器在編譯時對函數調用約定的處理。函數

瞭解完libffi的原理以後,接下來,咱們就進入實操過程!post

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的調用,咱們來具體分析下整個流程。

  1. 定義函數的參數類型,func1的參數爲兩個int類型,這裏使用argTypes指針數組,先建立對應的大小,而後分別賦值int對應的ffi_type_sint類型。
  2. 定義函數的返回類型,func1的返回類型爲int,因此retType賦值爲ffi_type_sint
  3. 定義函數模板ffi_cif, 經過ffi_prep_cif建立對應的函數模板,第一個參數爲ffi_cif模板;第二個參數表示不一樣CPU架構下的ABI,一般選擇FFI_DEFAULT_ABI,會根據不一樣CPU架構選擇到對應的ABI;第三個參數爲函數參數個數;第四個參數爲定義的函數參數類型;最後一個參數爲函數返回值類型。
  4. 建立函數對應的參數值和返回值。
  5. 調用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
複製代碼
  1. 這一段主要是建立函數模板ffi_cif,具體過程跟上一個例子同樣,這裏就不重複了。
  2. 定義一個用來綁定的函數指針funcInvoke
  3. 建立一個ffi_closure對象,並將funcInvoke函數指針傳遞進去。
  4. 經過ffi_prep_closure_loc方法將ffi_clousure對象、函數模板cif、綁定的函數bind_func、綁定函數bind_func中傳遞的數據、函數指針funcInvoke等綁定在一塊兒。
  5. 調用函數指針,會進入到綁定的函數bind_func中。
  6. 回調函數bind_func的參數類型分別是:函數模板ffi_cif,函數返回類型指針,函數參數類型指針,ffi_prep_closure_loc中傳遞進來的數據。
  7. 獲取到對應的參數值,以及傳入的self對象,而後進行相關邏輯處理。
  8. 最後將處理的結果返回給ret指針對象,做爲返回值。

3、libffi的應用

上面講解了libffi中ffi_callffi_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);
複製代碼
  1. 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;
}
複製代碼

這裏只是羅列了一部分,完整部分能夠參考這裏

  1. 返回值類型也是相似,先獲取到對應的type encoding,而後再設置對應的類型。

  2. 傳入對應的參數,建立函數模板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;
    }
}
複製代碼
  1. JBlockHook爲自定義的一個對象,用來封裝hook相關信息,分別獲取到mode、插入(或替換)的block,以及本來的block。
  2. 根據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;
}
複製代碼
  1. 經過傳入的ClassSEL,能夠獲取到具體的method對象,而後根據methodtypeEncoding獲取到方法的函數簽名。
  2. 由於函數簽名的參數中前兩個參數分別爲方法自己和_cmd,因此參數解析直接從第三個參數開始。
  3. 獲取到對應的參數和返回值後,下面的過程就和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);
}
複製代碼

獲取到methodIMP指針,而後經過addMethodreplaceMethod的方法將上面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的原理相似。

4、總結

筆者經過這幾天對libffi庫的學習,發現libffi的使用簡潔,但功能卻很是強大,很是適合作一些hook操做。目前,GitHub上也有兩個使用libffi來實現hook的開源庫BlockHookstinger,值得你們去探究學習。

參考資料

相關文章
相關標籤/搜索