iOS中的HOOK技術

簡介

OC語言是動態的

C語言是純靜態的,C函數的調用是通過函數地址,函數地址在編譯時期就必須確認。如果要hook,要直接修改二進制,要直接去寫彙編代碼。

fishhook

介紹

fishhook是facebook出品的一個開源庫。利用mach-o文件加載原理,通過rebind_symbols函數修改__DATA Segment的符號指針指向,來動態的Hook C函數。

主要信息

結構體

struct rebinding {
  const char *name;   //函數名稱
  void *replacement;  //新的函數地址
  void **replaced;   //保存原始函數地址變量的指針(通常要存儲下來,在替換後的方法裏調用)
};

主要接口

  /*
  交換方法
  arg1: 存放rebinding結構體的數組
  arg2: 數組的長度
  */
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);

實現

在這裏插入圖片描述
hook OC函數

//
//  ViewController.m
//  fishhookTest
//
//  Created by  on 2020/3/3.
//  Copyright © 2020 Shae. All rights reserved.
//

#import "ViewController.h"
#import "fishhook.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //這裏必須要先加載一次要交換的函數,否則符號表裏面不會出現要交換的函數的地址
    NSLog(@"我是純正的NSLog函數");
    
    //定義rebinding結構體
    struct rebinding manager;
    //要交換的函數的名稱
    manager.name="NSLog";
    //新的函數的地址
    manager.replacement=new_NSLog;
    //保存原始函數地址變量的指針(存儲下來,在替換的方法裏調用)
    manager.replaced=(void*)&old_NSLog;
    //定義數組
    struct rebinding rebs[]={manager};
    /*
     交換方法
     arg1: 存放rebinding結構體的數組
     arg2: 數組的長度
     */
    rebind_symbols(rebs, 1);
    
}
//函數指針,用來保存原始的函數地址
static void (* old_NSLog)(NSString *format, ...);
//新的NSLog函數
void new_NSLog(NSString *format, ...){
    format=[format stringByAppendingString:@"被勾上了"];
    //再調原來的
    old_NSLog(format);

}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"點擊屏幕");
}

@end

hook C函數

#import "ViewController.h"
#import "fishhook.h"

@interface ViewController ()

@end

@implementation ViewController
- (void)viewDidLoad {
  [super viewDidLoad];

  //這裏必須要先加載一次要交換的函數,否則符號表裏面不會出現要交換的函數的地址
  printf("我是純正的printf函數\n");

  //定義rebinding結構體
  struct rebinding manager;
  //要交換函數的名稱
  manager.name = "printf";
  //新的函數地址
  manager.replacement = new_printf;
  //保存原始函數地址變量的指針(存儲下來,在替換後的方法裏調用)
  manager.replaced = (void *)&old_printf;

  //定義數組
  struct rebinding rebs[] = {manager};

 /*
  交換方法
  arg1: 存放rebinding結構體的數組
  arg2: 數組的長度
  */
  rebind_symbols(rebs, 1);
}

//函數指針,用來保存原始的函數地址
static int (* old_printf)(const char *, ...);

//新的printf函數
void new_printf(const char * name, ...) {
  //再調用原來的
  old_printf("勾上了!\n");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  printf("點擊屏幕");
}
@end

在這裏插入圖片描述

Method Swizzle

在這裏插入圖片描述
Method Swizzle是利用OC的Runtime特性,動態改變SEL(方法編號)和IMP(方法實現)的對應關係,達到OC方法調用流程改變的目的。主要用於OC方法調換或往系統方法注入代碼。

Runtime 術語的數據結構

SEL

它是selector在 Objc 中的表示(Swift 中是 Selector 類)。selector 是方法選擇器,其實作用就和名字一樣,日常生活中,我們通過人名辨別誰是誰,注意 Objc 在相同的類中不會有命名相同的兩個方法。selector 對方法名進行包裝,以便找到對應的方法實現。它的數據結構是:

typedef struct objc_selector *SEL;

我們可以看出它是個映射到方法的 C 字符串,你可以通過 Objc 編譯器器命令@selector() 或者 Runtime 系統的 sel_registerName 函數來獲取一個 SEL 類型的方法選擇器。

注意:
不同類中相同名字的方法所對應的selector是相同的,由於變量的類型不同,所以不會導致它們調用方法實現混亂。

id

id 是一個參數類型,它是指向某個類的實例的指針。定義如下:

typedef struct objc_object *id;
struct objc_object { Class isa; };

以上定義,看到 objc_object 結構體包含一個 isa 指針,根據 isa 指針就可以找到對象所屬的類。

注意:
isa 指針在代碼運行時並不總指向實例對象所屬的類型,所以不能依靠它來確定類型,要想確定類型還是需要用對象的 -class 方法。(如:KVO 的實現機理就是將被觀察對象的 isa 指針指向一箇中間類而不是真實類型)

Class

typedef struct objc_class *Class;

Class 其實是指向 objc_class 結構體的指針。objc_class 的數據結構如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;   //類的isa指針,指向其所屬的元類(meta)

#if !__OBJC2__
    Class super_class  //父類指針                       OBJC2_UNAVAILABLE;
    const char *name   //類名                           OBJC2_UNAVAILABLE;
    long version  //是類的版本信息                       OBJC2_UNAVAILABLE;
    long info  //是類的詳情                             OBJC2_UNAVAILABLE;
    long instance_size  //該類的實例對象的大小            OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars  //成員變量列表         OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists  //方法列表    OBJC2_UNAVAILABLE;
    struct objc_cache *cache  //緩存                    OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols  //協議列表     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

·Class 也有一個 isa 指針,指向其所屬的元類(meta)。
·super_class:指向其超類。
·name:是類名。
·version:是類的版本信息。
·info:是類的詳情。
·instance_size:是該類的實例對象的大小。
·ivars:指向該類的成員變量列表。
·methodLists:指向該類的實例方法列表,它將方法選擇器和方法實現地址聯繫起來。methodLists 是指向 ·objc_method_list 指針的指針,也就是說可以動態修改 *methodLists 的值來添加成員方法,這也是 Category 實現的原理,同樣解釋了 Category 不能添加屬性的原因。
·cache:Runtime 系統會把被調用的方法存到 cache 中(理論上講一個方法如果被調用,那麼它有可能今後還會被調用),下次查找的時候效率更高。
·protocols:指向該類的協議列表。

其中 objc_ivar_list 和 objc_method_list 分別是成員變量列表和方法列表:

//成員變量列表
struct objc_ivar_list {
    int ivar_count                                     OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                          OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                      OBJC2_UNAVAILABLE;
}                                                      OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                  OBJC2_UNAVAILABLE;

    int method_count                                   OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                          OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                  OBJC2_UNAVAILABLE;
}

由此可見,我們可以動態修改 *methodList 的值來添加成員方法,這也是 Category 實現的原理,同樣解釋了 Category 不能添加屬性的原因。

objc_ivar_list 結構體用來存儲成員變量的列表,而 objc_ivar 則是存儲了單個成員變量的信息;同理,objc_method_list 結構體存儲着方法數組的列表,而單個方法的信息則由 objc_method 結構體存儲。

值得注意的時,objc_class 中也有一個 isa 指針,這說明 Objc 類本身也是一個對象。爲了處理類和對象的關係,Runtime 庫創建了一種叫做 Meta Class(元類) 的東西,類對象所屬的類就叫做元類。Meta Class 表述了類對象本身所具備的元數據。

在這裏插入圖片描述

Method

Method 代表類中某個方法的類型

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                    OBJC2_UNAVAILABLE;
    char *method_types                                 OBJC2_UNAVAILABLE;
    IMP method_imp                                     OBJC2_UNAVAILABLE;
}

objc_method 存儲了方法名,方法類型和方法實現:

方法名,類型爲 SEL
方法類型 method_types 是個 char 指針,存儲方法的參數類型和返回值類型
method_imp 指向了方法的實現,本質是一個函數指針

Ivar

Ivar 是表示成員變量的類型。

typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char *ivar_name                                    OBJC2_UNAVAILABLE;
    char *ivar_type                                    OBJC2_UNAVAILABLE;
    int ivar_offset                                    OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                           OBJC2_UNAVAILABLE;
#endif
}

其中 ivar_offset 是基地址偏移字節

IMP

IMP在objc.h中的定義是:

typedef id (*IMP)(id, SEL, ...);

它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息之後,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP 這個函數指針就指向了這個方法的實現。

你會發現 IMP 指向的方法與 objc_msgSend 函數類型相同,參數都包含 id 和 SEL 類型。每個方法名都對應一個 SEL 類型的方法選擇器,而每個實例對象中的 SEL 對應的方法實現肯定是唯一的,通過一組 id和 SEL參數就能確定唯一的方法實現地址。

Cache

Cache 定義如下:

typedef struct objc_cache *Cache

struct objc_cache {
    unsigned int mask /* total = mask + 1 */           OBJC2_UNAVAILABLE;
    unsigned int occupied                              OBJC2_UNAVAILABLE;
    Method buckets[1]                                  OBJC2_UNAVAILABLE;
};

Cache 爲方法調用的性能進行優化,每當實例對象接收到一個消息時,它不會直接在 isa 指針指向的類的方法列表中遍歷查找能夠響應的方法,因爲每次都要查找效率太低了,而是優先在 Cache 中查找。

Runtime 系統會把被調用的方法存到 Cache 中,如果一個方法被調用,那麼它有可能今後還會被調用,下次查找的時候就會效率更高。就像計算機組成原理中 CPU 繞過主存先訪問 Cache 一樣。

方法交換(Method Swizzling)

下面代碼對UIViewController中的viewDidLoad方法進行交換
UIViewController+swizzling.m文件

#import "UIViewController+swizzling.h"
#import <objc/runtime.h>

@implementation UIViewController (swizzling)

//load類方法(當某個類的代碼被讀到內存後調用)
+ (void)load
{
    /*
     SEL: 它就是個映射到方法的C字符串,可以用Objective-C編譯器命令@selector()或者Runtime系統的sel_registerName函數來獲得一個SEL類型的方法選擇器。
     
     數據結構是:
     typedef struct objc_selector *SEL;
     */
    SEL origSel = @selector(viewDidLoad);
    SEL swizSel = @selector(swiz_viewDidLoad);
  
    [UIViewController swizzleMethods:[self class] originalSelector:origSel swizzledSelector:swizSel];
}

//交換兩個方法的實現
+ (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel
{
    /*
     Method 代表類中某個方法的類型
     
     typedef struct objc_method *Method;
     
     struct objc_method {
     SEL method_name                                    OBJC2_UNAVAILABLE;
     char *method_types                                 OBJC2_UNAVAILABLE;
     IMP method_imp                                     OBJC2_UNAVAILABLE;
     }
     objc_method 存儲了方法名,方法類型和方法實現:
     
     方法名類型爲 SEL
     方法類型 method_types 是個 char 指針,存儲方法的參數類型和返回值類型
     method_imp 指向了方法的實現,本質是一個函數指針
     
     
     class_getInstanceMethod(class, origSel);
     返回class類的origSel方法。
    */
    Method origMethod = class_getInstanceMethod(class, origSel);
    Method swizMethod = class_getInstanceMethod(class, swizSel);
    
    /*
     周全起見,有兩種情況要考慮一下
     第一種情況:要交換的方法並沒有在目標類中實現,而是在其父類中實現了, 這時使用class_getInstanceMethod函數獲取到的originalSelector指向的就是父類的方法
     第二種情況:要交換的方法已經存在於目標類中

     
      class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod));
      往class這個類,添加方法origSel,新增方法的實現地址爲method_getImplementation(swizMethod),參數及返回值類型爲method_getTypeEncoding(swizMethod)
      添加成功返回YES,失敗返回NO(例如,該類已經包含一個同名的方法實現)
     
      method_getImplementation(swizMethod)  返回swizMethod方法實現
      method_getTypeEncoding(swizMethod)  返回描述swizMethod方法參數和返回值類型的字符串
     */
    //判斷是情況一還是情況二
    BOOL didAddMethod = class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod));
  
    if (didAddMethod)
    {
        /*
          第一種情況:要交換的方法已經在父類中實現了,則替換父類方法
          使用replaceMethod來替換給定類的方法實現(將origMethod方法替換成swizSel方法)
         */
        class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    }
    else
    {
        /*
          第二種情況:要交換的方法已經存在於目標類中
          通過method_exchangeImplementations來交換方法
         */
        method_exchangeImplementations(origMethod, swizMethod);
    }
}

//自己實現的用於交換的方法
- (void)swiz_viewDidLoad
{
    NSLog(@"調用了swiz_viewDidLoad方法");
    
    /*
       需要注入的代碼寫在此處
     */
    
    //執行這句的時候跳轉到viewDidLoad方法中
    [self swiz_viewDidLoad];
}
@end

ViewController.m文件

#import "ViewController.h"
#import "UIViewController+swizzling.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    //執行這句的時候跳到swiz_viewDidLoad方法中
    [super viewDidLoad];
  
    NSLog(@"調用了viewDidLoad方法");
}
@end

當執行[super viewDidLoad]時,跳轉到swiz_viewDidLoad方法中。當執行到[self swiz_viewDidLoad]時,再跳轉到viewDidLoad方法中。因此,先打印“調用了swiz_viewDidLoad方法”,再打印“調用了viewDidLoad方法”

在這裏插入圖片描述

macoView

用來研究應用的可執行文件的。

fishhook替換oc方法demo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章