文章目錄
簡介
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
用來研究應用的可執行文件的。