前言:在iOS開發(fā)中可以使用Runtime動(dòng)態(tài)調(diào)用OC方法,但是無法動(dòng)態(tài)調(diào)用C函數(shù),那么該如何動(dòng)態(tài)調(diào)用C函數(shù)呢?值得思考一下。
1. 函數(shù)調(diào)用
1.1 函數(shù)地址
C語言編譯后,在可執(zhí)行文件中會(huì)有函數(shù)名信息
。如果想要?jiǎng)討B(tài)調(diào)用一個(gè)C函數(shù),首先需要根據(jù)函數(shù)名找到這個(gè)函數(shù)地址,然后根據(jù)函數(shù)地址進(jìn)行調(diào)用。
動(dòng)態(tài)鏈接器已經(jīng)提供一個(gè) API:dlsym()
,可以通過函數(shù)名字拿到函數(shù)地址:
void test() {
printf("testFunc");
}
int main() {
void (*funcPointer)() = dlsym(RTLD_DEFAULT, "test");
funcPointer();
return 0;
}
從上面代碼中可以看出,test方法是沒有返回值和參數(shù)的。所以funcPointer
只能在指向參數(shù)和返回值都是空的函數(shù)時(shí)才能正確調(diào)用到。對于有返回值和有參數(shù)的C函數(shù),需要指明參數(shù)和返回值類型才能使用。
int testFunc(int n, int m) {
printf("testFunc");
return 1;
}
int main() {
// ① 表示正確定義了函數(shù)參數(shù)/返回值類型的函數(shù)指針
int (*funcPointer)(int, int) = dlsym(RTLD_DEFAULT, "testFunc");
funcPointer(1, 2);
// ② 表示沒有正確定義參數(shù)/返回值類型的函數(shù)指針
void (*funcPointer)() = dlsym(RTLD_DEFAULT, "testFunc");
funcPointer(1, 2); //error
return 0;
}
如上代碼,② 在執(zhí)行的時(shí)候會(huì)crash,因?yàn)闆]有定義正確的參數(shù)類型和返回值類型。
如果所有C函數(shù)的參數(shù)類型和數(shù)量,以及返回類型也一樣,那么使用dlsym()就能實(shí)現(xiàn)動(dòng)態(tài)的調(diào)用C函數(shù),但是這根本不現(xiàn)實(shí)。
不同的函數(shù)都有不同的參數(shù)和返回值類型,也就沒辦法通過一個(gè)萬能的函數(shù)指針去支持所有函數(shù)的動(dòng)態(tài)調(diào)用,必須要讓函數(shù)的參數(shù)/返回值類型都對應(yīng)上才能調(diào)用。因?yàn)楹瘮?shù)的調(diào)用方和被調(diào)用方會(huì)遵循一種約定:調(diào)用慣例(Calling Convention)。
1.2 調(diào)用慣例(Calling Convention)
高級編程語言的函數(shù)在調(diào)用時(shí),需要約定好參數(shù)的傳遞順序、傳遞方式,棧維護(hù)的方式,名字修飾。這種函數(shù)調(diào)用者和被調(diào)用者對函數(shù)如何調(diào)用的約定,就叫作調(diào)用慣例(Calling Convention)。高級語言編譯時(shí),會(huì)生成遵循調(diào)用慣例的匯編代碼。
- 參數(shù)傳遞方式
調(diào)用函數(shù)時(shí),參數(shù)可以選擇使用棧或者使用寄存器進(jìn)行傳遞- 參數(shù)傳遞順序
參數(shù)壓棧的順序可以從左到右也可以從右到左- 棧維護(hù)方式
函數(shù)調(diào)用后參數(shù)從棧彈出可以由調(diào)用方完成,也可以由被調(diào)用方完成
在日常工作中,比較少接觸到這個(gè)概念。因?yàn)榫幾g器已經(jīng)幫我們完成了這一工作,我們只需要遵循正確的語法規(guī)則即可,編譯器會(huì)根據(jù)不同的架構(gòu)生成對應(yīng)的匯編代碼,從而確保函數(shù)調(diào)用約定的正確性。
函數(shù)調(diào)用者和被調(diào)用者需要遵循這同一套約定,上述②,就是函數(shù)本身遵循了這個(gè)約定,而調(diào)用者沒有遵守,導(dǎo)致調(diào)用出錯(cuò)。
以上面例子簡單分析下,如果按①那樣正確的定義方式定義funcPointer,然后調(diào)用它,這里編譯成匯編后,在調(diào)用處會(huì)有相應(yīng)指令把參數(shù) n,m 的值 1 和 2 入棧(這里是舉例),然后跳過去 testFunc()
函數(shù)實(shí)體執(zhí)行,這個(gè)函數(shù)執(zhí)行時(shí),按約定它知道n,m兩個(gè)參數(shù)值已經(jīng)在棧上,就可以取出來使用了:
而如果按②那樣定義,編譯后這里不會(huì)把參數(shù) n,m 的值 1 和 2 入棧,因?yàn)檫@里編譯器把它當(dāng)成了沒有參數(shù)和沒有返回值的函數(shù),也就不需要進(jìn)行參數(shù)入棧的操作,然后在 testFunc()
函數(shù)實(shí)體里按約定去棧上取參數(shù)時(shí)就會(huì)發(fā)現(xiàn)棧上本來應(yīng)該存參數(shù) n 和 m 的地方并沒有數(shù)據(jù),或者是其他錯(cuò)誤的數(shù)據(jù),導(dǎo)致調(diào)用出錯(cuò):
所以要在調(diào)用前明確告訴編譯器函數(shù)的參數(shù)和返回值類型,編譯器才能生成對應(yīng)的正確的匯編代碼,讓被調(diào)用的函數(shù)執(zhí)行時(shí)能正常取到參數(shù)。
也就是說如果需要?jiǎng)討B(tài)調(diào)用任意 C 函數(shù),有一種笨方案就是事先準(zhǔn)備好任意參數(shù)類型/參數(shù)個(gè)數(shù)/返回值類型 排列組合的 C 函數(shù)指針,讓最終的匯編把所有情況都準(zhǔn)備好,最后調(diào)用時(shí)通過 switch 去找到正確的那個(gè)去執(zhí)行就可以了??。
在C語言層面上解決不了這個(gè)問題,只能再往底層走,從匯編考慮了。
1.3 objc_msgSend
OC的方法調(diào)用走的都是objc_msgSend
函數(shù),這個(gè)函數(shù)支持任意返回值以及任意參數(shù)類型和個(gè)數(shù),它的定義僅是下面這樣:
void objc_msgSend(void /* id self, SEL op, ... */ )
了解OC底層的都知道,objc_msgSend
是用匯編實(shí)現(xiàn)的,其結(jié)構(gòu)分為序言準(zhǔn)備(Prologue)、函數(shù)體(Body)、結(jié)束收尾(Epilogue)三部分。
序言準(zhǔn)備部分的作用是會(huì)保存之前程序執(zhí)行的狀態(tài),還會(huì)將輸入的參數(shù)保存到寄存器和棧上。這樣,objc_msgSend
就能夠先將未知的參數(shù)保存到寄存器和棧上,然后在函數(shù)體執(zhí)行自身指令或者跳轉(zhuǎn)其它函數(shù),最后在結(jié)束收尾部分恢復(fù)寄存器,回到調(diào)用函數(shù)之前的狀態(tài)。
得益于序言準(zhǔn)備部分可以事先準(zhǔn)備好寄存器和棧,objc_msgSend
可以做到函數(shù)調(diào)用無需通過編譯生成匯編代碼來遵循調(diào)用慣例(通過蘋果自己寫的匯編接管了,不需要編譯器參與了),進(jìn)而使得OC具備了動(dòng)態(tài)調(diào)用函數(shù)的能力。
但是,不同的 CPU 架構(gòu),在編譯時(shí)會(huì)執(zhí)行不同的 objc_msgSend
函數(shù),而且 objc_msgSend
函數(shù)無法直接調(diào)用 C 函數(shù),所以想要實(shí)現(xiàn)動(dòng)態(tài)地調(diào)用 C 函數(shù)就需要使用另一個(gè)用匯編語言編寫的庫 libffi
。
2. libffi
2.1 libffi簡介
“FFI” 的全名是 Foreign Function Interface
(外部函數(shù)接口),通過外部函數(shù)接口允許用一種語言編寫的代碼調(diào)用用另一種語言編寫的代碼。libffi提供了最底層的接口,在不確定參數(shù)個(gè)數(shù)和類型的情況下,根據(jù)相應(yīng)規(guī)則,完成所需數(shù)據(jù)的準(zhǔn)備,生成相應(yīng)匯編指令的代碼來完成函數(shù)調(diào)用。
libffi
還提供了可移植的高級語言接口,可以不使用函數(shù)簽名間接調(diào)用C函數(shù)。比如,腳本語言Python
在運(yùn)行時(shí)會(huì)使用libffi
高級語言的接口去調(diào)用C函數(shù)。libffi
的作用類似于一個(gè)動(dòng)態(tài)的編譯器,在運(yùn)行時(shí)就能夠完成編譯時(shí)所做的調(diào)用慣例函數(shù)調(diào)用代碼生成。
libffi
可以認(rèn)為是實(shí)現(xiàn)了C語言的runtime
,libffi
通過調(diào)用 ffi_call(函數(shù)調(diào)用)
來進(jìn)行函數(shù)調(diào)用,ffi_call
的輸入是ffi_cif(模板)
、函數(shù)指針
、參數(shù)地址
。其中,ffi_cif
由ffi_type(參數(shù)類型)
和參數(shù)個(gè)數(shù)
生成,也可以是ffi_closure(閉包)
。
2.2 libffi使用
2.2.1 ffi_type (參數(shù)類型)
ffi_type
的作用是,描述C語言的基本類型,比如uint32
、void *
、struct
等,定義如下:
typedef struct _ffi_type
{
size_t size; // 所占大小
unsigned short alignment; //對齊大小
unsigned short type; // 標(biāo)記類型的數(shù)字
struct _ffi_type **elements; // 結(jié)構(gòu)體中的元素
} ffi_type;
其中,size 表述該類型所占的大小,alignment 表示該類型的對齊大小,type 表示標(biāo)記類型的數(shù)字,element 表示結(jié)構(gòu)體的元素。當(dāng)類型是 uint32 時(shí),size 的值是 4,alignment 也是 4,type 的值是 9,elements 是空。
同時(shí),libffi也提供了許多內(nèi)置類型,用于描述參數(shù)和返回類型:
比如ffi_type_void、ffi_type_uint8、ffi_type_sint8、ffi_type_float、ffi_type_double等等。
2.2.2 ffi_cif (模板)
ffi_cif
由參數(shù)類型(ffi_type)
和參數(shù)個(gè)數(shù)
生成,定義如下:
typedef struct {
ffi_abi abi; // 不同 CPU 架構(gòu)下的 ABI,一般設(shè)置為 FFI_DEFAULT_ABI
unsigned nargs; // 參數(shù)個(gè)數(shù)
ffi_type **arg_types; // 參數(shù)類型
ffi_type *rtype; // 返回值類型
unsigned bytes; // 參數(shù)所占空間大小,16的倍數(shù)
unsigned flags; // 返回類型是結(jié)構(gòu)體時(shí)要做的標(biāo)記
#ifdef FFI_EXTRA_CIF_FIELDS
FFI_EXTRA_CIF_FIELDS;
#endif
} ffi_cif;
如代碼所示,ffi_cif包含了函數(shù)調(diào)用時(shí)需要的一些信息:
abi
表示不同CPU架構(gòu)下的ABI,一般設(shè)置為FFI_DEFAULT_ABI(在移動(dòng)設(shè)備上 CPU 架構(gòu)是 ARM64 時(shí),F(xiàn)FI_DEFAULT_ABI 就是 FFI_SYSV;使用蘋果公司筆記本 CPU 架構(gòu)是 X86_DARWIN 時(shí),F(xiàn)FI_DEFAULT_ABI 就是 FFI_UNIX64)nargs
表示輸入?yún)?shù)的個(gè)數(shù)arg_types
表示參數(shù)的類型,比如 ffi_type_uint32rtype
表示返回類型,如果返回類型是結(jié)構(gòu)體,字段flags需要設(shè)置數(shù)值作為標(biāo)記,以便在ffi_prep_cif_machdep函數(shù)中處理,如果返回的不是結(jié)構(gòu)體,flags不做標(biāo)記bytes
表示輸入?yún)?shù)所占空間的大小,是16的倍數(shù)
ffi_cif
是由 ffi_prep_cif
函數(shù)生成的,返回值是ffi_status
類型,一個(gè)枚舉,表明結(jié)果如何,代碼如下:
ffi_status ffi_prep_cif(ffi_cif *cif,
ffi_abi abi,
unsigned int nargs,
ffi_type *rtype,
ffi_type **atypes);
2.2.3 ffi_call (函數(shù)調(diào)用)
準(zhǔn)備好函數(shù)模板之后,就可以使用ffi_call
調(diào)用指定函數(shù)了,簡單看個(gè)例子,結(jié)合了模板生成和函數(shù)調(diào)用步驟:
先定義一個(gè)C函數(shù):
double addFunc(int a, double b){
return a + b;
}
使用libffi調(diào)用這個(gè)C函數(shù):
void libffi_add(){
ffi_cif cif;
// 參數(shù)值
int a = 100;
double b = 0.5;
void *args[2] = { &a , &b};
// 參數(shù)類型數(shù)組
ffi_type *argTyeps[2] = { &ffi_type_sint, &ffi_type_double };
// 參數(shù)返回值類型
ffi_type *rettype = &ffi_type_double;
//根據(jù)參數(shù)和返回值類型,設(shè)置cif模板
ffi_prep_cif(&cif, FFI_DEFAULT_ABI, sizeof(args) / sizeof(void *), rettype, argTyeps);
// 返回值
double result = 0;
//使用cif函數(shù)簽名信息,調(diào)用函數(shù)
ffi_call(&cif, (void *)&addFunc, &result, args);
// assert
assert(result == 100.5);
}
2.2.4 ffi_prep_closure_loc
如下代碼所示,在 testFFIClosure
函數(shù)準(zhǔn)備好 cif
后,會(huì)聲明一個(gè)新的函數(shù)指針,這個(gè)新的函數(shù)指針會(huì)和分配的 ffi_closure
關(guān)聯(lián),ffi_closure
還會(huì)通過ffi_prep_closure_loc
函數(shù)關(guān)聯(lián)到cif
、closure
、函數(shù)實(shí)體 closureCalled
,當(dāng)我們調(diào)用addNumA:numB:
方法的時(shí)候,會(huì)調(diào)用到那個(gè)imp
,之后會(huì)調(diào)用到關(guān)聯(lián)的函數(shù)實(shí)體closureCalled
中:
// 用來hook原有方法的函數(shù)
void closureCalled(ffi_cif *cif, void *ret, void **args, void *userdata) {
int bar = *((int *)args[2]);
int baz = *((int *)args[3]);
*((int *)ret) = bar * baz;
}
void testFFIClosure() {
// 準(zhǔn)備模板
ffi_cif cif;
ffi_type *argumentTypes[] = {&ffi_type_pointer, &ffi_type_pointer, &ffi_type_sint32, &ffi_type_sint32};
ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 4, &ffi_type_pointer, argumentTypes);
// 新的函數(shù)指針
IMP newIMP;
// 分配一個(gè)closure關(guān)聯(lián)新聲明的函數(shù)指針
ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), (void *)&newIMP);
// ffi_closure 關(guān)聯(lián) cif closure 函數(shù)實(shí)體 closureCalled
ffi_prep_closure_loc(closure, &cif, closureCalled, NULL, NULL);
// 使用Runtime 接口將 fooWithBar:baz 方法綁定到 closureCalled 函數(shù)指針上
Method method = class_getInstanceMethod([TestFFI class], @selector(addNumA:numB:));
method_setImplementation(method, newIMP);
// after hook
TestFFI *test = [TestFFI new];
int ret = [test addNumA:123 numB:456];
NSLog(@"ffi_closure: %d", ret);
}
libffi能調(diào)用任意C函數(shù)的原理和
objc_msgSend
的原理類似,底層都是用匯編實(shí)現(xiàn)的,ffi_call
根據(jù)模板cif和參數(shù)值,把參數(shù)都按規(guī)則塞到棧/寄存器里,調(diào)用的函數(shù)可以按規(guī)則取到參數(shù),調(diào)用完再獲取返回值,清理數(shù)據(jù)。
通過其他方式調(diào)用上文中的imp
,ffi_closure可根據(jù)棧/寄存器、模板cif拿到所有的參數(shù)
,接著執(zhí)行自定義函數(shù)testFFIClosure
里的代碼。
通過libffi可以hook系統(tǒng)的方法實(shí)現(xiàn),在一些支持熱修復(fù)的庫中,也有用到libffi,更多的了解和使用,還是看libffi的github吧。
相關(guān)鏈接
libffi的github地址
sunnyxxx的libffi示例
如何動(dòng)態(tài)調(diào)用C函數(shù)
利用libffi實(shí)現(xiàn)AOP
動(dòng)態(tài)調(diào)用和定義C函數(shù)