[iOS]libffi動(dòng)態(tài)調(diào)用C函數(shù)

前言:在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)在棧上,就可以取出來使用了:

image.png

而如果按②那樣定義,編譯后這里不會(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ò):

image.png

所以要在調(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語言的runtimelibffi通過調(diào)用 ffi_call(函數(shù)調(diào)用)來進(jìn)行函數(shù)調(diào)用,ffi_call的輸入是ffi_cif(模板)函數(shù)指針參數(shù)地址。其中,ffi_cifffi_type(參數(shù)類型)參數(shù)個(gè)數(shù)生成,也可以是ffi_closure(閉包)

2.2 libffi使用

2.2.1 ffi_type (參數(shù)類型)

ffi_type的作用是,描述C語言的基本類型,比如uint32void *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_uint32

  • rtype
    表示返回類型,如果返回類型是結(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)到cifclosure函數(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)用上文中的impffi_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ù)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,802評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內(nèi)容