學會黑科技,一招搞定 iOS 14.2 的 libffi crash

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"蘋果升級 14.2,全球 iOS 遭了秧。libffi 在 iOS14.2 上發生了 crash, 我司的許多 App 深受困擾,有許多基礎庫都是用了 libffi。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ff\/ffb365e13b7e5fbe0409a94b1c586752.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過定位,發現是 vmremap 導致的 code sign error。我們通過使用靜態 trampoline 的方式讓 libffi 不需要使用 vmremap,解決了這個問題。這裏就介紹一下相關的實現原理。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"libffi 是什麼"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"高層語言的編譯器生成遵循某些約定的代碼。這些公約部分是單獨彙編工作所必需的。“調用約定”本質上是編譯器對函數入口處將在哪裏找到函數參數的假設的一組假設。“調用約定”還指定函數的返回值在哪裏找到。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一些程序在編譯時可能不知道要傳遞給函數的參數。例如,在運行時,解釋器可能會被告知用於調用給定函數的參數的數量和類型。Libffi 可用於此類程序,以提供從解釋器程序到編譯代碼的橋樑。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"libffi 庫爲各種調用約定提供了一個便攜式、高級的編程接口。這允許程序員在運行時調用調用接口描述指定的任何函數。"}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"ffi 的使用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡單的找了一個使用 ffi 的庫看一下他的調用接口"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"ffi_type *returnType = st_ffiTypeWithType(self.signature.returnType);\nNSAssert(returnType, @\"can't find a ffi_type of %@\", self.signature.returnType);\n\nNSUInteger argumentCount = self->_argsCount;\n_args = malloc(sizeof(ffi_type *) * argumentCount) ;\n\nfor (int i = 0; i < argumentCount; i++) {\n ffi_type* current_ffi_type = st_ffiTypeWithType(self.signature.argumentTypes[i]);\n NSAssert(current_ffi_type, @\"can't find a ffi_type of %@\", self.signature.argumentTypes[i]);\n _args[i] = current_ffi_type;\n}\n\n\/\/ 創建 ffi 跳板用到的 closure\n_closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&xxx_func_ptr);\n\n\/\/ 創建 cif,調用函數用到的參數和返回值的類型信息, 之後在調用時會結合call convention 處理參數和返回值\nif(ffi_prep_cif(&_cif, FFI_DEFAULT_ABI, (unsigned int)argumentCount, returnType, _args) == FFI_OK) {\n\n \/\/ closure 寫入 跳板數據頁\n if (ffi_prep_closure_loc(_closure, &_cif, _st_ffi_function, (__bridge void *)(self), xxx_func_ptr) != FFI_OK) {\n NSAssert(NO, @\"genarate IMP failed\");\n }\n} else {\n NSAssert(NO, @\"\");\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看完這段代碼,大概能理解 ffi 的操作。"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"提供給外界一個指針(指向 trampoline entry)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"創建一個 closure, 將調用相關的參數返回值信息放到 closure 裏"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"將 closure 寫入到 trampoline 對應的 trampoline data entry 處"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"之後我們調用 trampoline entry func ptr 時,"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"會找到 寫入到 trampoline 對應的 trampoline data entry 處的 closure 數據"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"根據 closure 提供的調用參數和返回值信息,結合調用約定,操作寄存器和棧,寫入參數 進行函數調用,獲取返回值。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"那 ffi 是怎麼找到 trampoline 對應的 trampoline data entry 處的 closure 數據 呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們從 ffi 分配 trampoline 開始說起:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"static ffi_trampoline_table *\nffi_remap_trampoline_table_alloc (void)\n{\n.....\n \/* Allocate two pages -- a config page and a placeholder page *\/\n config_page = 0x0;\n kt = vm_allocate (mach_task_self (), &config_page, PAGE_MAX_SIZE * 2,\n VM_FLAGS_ANYWHERE);\n if (kt != KERN_SUCCESS)\n return NULL;\n\n \/* Allocate two pages -- a config page and a placeholder page *\/\n \/\/bdffc_closure_trampoline_table_page\n\n \/* Remap the trampoline table on top of the placeholder page *\/\n trampoline_page = config_page + PAGE_MAX_SIZE;\n trampoline_page_template = (vm_address_t)&ffi_closure_remap_trampoline_table_page;\n#ifdef __arm__\n \/* bdffc_closure_trampoline_table_page can be thumb-biased on some ARM archs *\/\n trampoline_page_template &= ~1UL;\n#endif\n kt = vm_remap (mach_task_self (), &trampoline_page, PAGE_MAX_SIZE, 0x0,\n VM_FLAGS_OVERWRITE, mach_task_self (), trampoline_page_template,\n FALSE, &cur_prot, &max_prot, VM_INHERIT_SHARE);\n if (kt != KERN_SUCCESS)\n {\n vm_deallocate (mach_task_self (), config_page, PAGE_MAX_SIZE * 2);\n return NULL;\n }\n\n\n \/* We have valid trampoline and config pages *\/\n table = calloc (1, sizeof (ffi_trampoline_table));\n table->free_count = FFI_REMAP_TRAMPOLINE_COUNT\/2;\n table->config_page = config_page;\n table->trampoline_page = trampoline_page;\n\n......\n return table;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先 ffi 在創建 trampoline 時,會分配兩個連續的 page"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"trampoline page 會 remap 到我們事先在代碼中彙編寫的 ffi_closure_remap_trampoline_table_page。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其結構如圖所示:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/d1\/d166916814f762230b4626512361114a.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我們 "},{"type":"codeinline","content":[{"type":"text","text":"ffi_prep_closure_loc(_closure, &_cif, _st_ffi_function, (__bridge void *)(self), entry1))"}]},{"type":"text","text":" 寫入 closure 數據時, 會寫入到 entry1 對應的 closuer1。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"ffi_status\nffi_prep_closure_loc (ffi_closure *closure,\n ffi_cif* cif,\n void (*fun)(ffi_cif*,void*,void**,void*),\n void *user_data,\n void *codeloc)\n{\n......\n if (cif->flags & AARCH64_FLAG_ARG_V)\n start = ffi_closure_SYSV_V; \/\/ ffi 對 closure的處理函數\n else\n start = ffi_closure_SYSV;\n\n void **config = (void**)((uint8_t *)codeloc - PAGE_MAX_SIZE);\n config[0] = closure;\n config[1] = start;\n......\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是怎麼對應到的呢? closure1 和 entry1 距離其所屬 Page 的 offset 是一致的,通過 offset,成功建立 trampoline entry 和 trampoline closure 的對應關係。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在我們知道這個關係,我們通過代碼看一下到底在程序運行的時候 是怎麼找到 closure 的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這四條指令是我們 trampoline entry 的代碼實現,就是 ffi 返回的 xxx_func_ptr"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"adr x16, -PAGE_MAX_SIZE\nldp x17, x16, [x16]\nbr x16\nnop"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 .rept 我們創建 PAGE_MAX_SIZE \/ FFI_TRAMPOLINE_SIZE 個跳板,剛好一個頁的大小"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"# 動態remap的 page\n.align PAGE_MAX_SHIFT\nCNAME(ffi_closure_remap_trampoline_table_page):\n.rept PAGE_MAX_SIZE \/ FFI_TRAMPOLINE_SIZE\n # 這是我們的 trampoline entry, 就是ffi生成的函數指針\n adr x16, -PAGE_MAX_SIZE \/\/ 將pc地址減去PAGE_MAX_SIZE, 找到 trampoine data entry\n ldp x17, x16, [x16] \/\/ 加載我們寫入的 closure, start 到 x17, x16\n br x16 \/\/ 跳轉到 start 函數\n nop \/* each entry in the trampoline config page is 2*sizeof(void*) so the trampoline itself cannot be smaller that 16 bytes *\/\n.endr"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 pc 地址減去 PAGE_MAX_SIZE 就找到對應的 trampoline data entry 了。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"靜態跳板的實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於代碼段和數據段在不同的內存區域。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們此時不能通過 像 vmremap 一樣分配兩個連續的 PAGE,在尋找 trampoline data entry 只是簡單的-PAGE_MAX_SIZE 找到對應關係,需要稍微麻煩點的處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要是通過 adrp 找到"},{"type":"codeinline","content":[{"type":"text","text":"_ffi_static_trampoline_data_page1"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"_ffi_static_trampoline_page1"}]},{"type":"text","text":"的起始地址,用 pc-"},{"type":"codeinline","content":[{"type":"text","text":"_ffi_static_trampoline_page1"}]},{"type":"text","text":"的起始地址計算 offset,找到 trampoline data entry。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"# 靜態分配的page\n#ifdef __MACH__\n#include \n\n.align 14\n.data\n.global _ffi_static_trampoline_data_page1\n_ffi_static_trampoline_data_page1:\n .space PAGE_MAX_SIZE*5\n.align PAGE_MAX_SHIFT\n.text\nCNAME(_ffi_static_trampoline_page1):\n\n_ffi_local_forwarding_bridge:\nadrp x17, ffi_closure_static_trampoline_table_page_start@PAGE;\/\/ text page\nsub x16, x16, x17;\/\/ offset\nadrp x17, _ffi_static_trampoline_data_page1@PAGE;\/\/ data page\nadd x16, x16, x17;\/\/ data address\nldp x17, x16, [x16];\/\/ x17 closure x16 start\nbr x16\nnop\nnop\n.align PAGE_MAX_SHIFT\nCNAME(ffi_closure_static_trampoline_table_page):\n\n#這個label 用來adrp@PAGE 計算 trampoline 到 trampoline page的offset\n#留了5個用來調試。\n# 我們static trampoline 兩條指令就夠了,這裏使用4個,和remap的保持一致\nffi_closure_static_trampoline_table_page_start:\nadr x16, #0\nb _ffi_local_forwarding_bridge\nnop\nnop\n\nadr x16, #0\nb _ffi_local_forwarding_bridge\nnop\nnop\n\nadr x16, #0\nb _ffi_local_forwarding_bridge\nnop\nnop\n\nadr x16, #0\nb _ffi_local_forwarding_bridge\nnop\nnop\n\nadr x16, #0\nb _ffi_local_forwarding_bridge\nnop\nnop\n\n\/\/ 5 * 4\n.rept (PAGE_MAX_SIZE*5-5*4) \/ FFI_TRAMPOLINE_SIZE\nadr x16, #0\nb _ffi_local_forwarding_bridge\nnop\nnop\n.endr\n\n.globl CNAME(ffi_closure_static_trampoline_table_page)\nFFI_HIDDEN(CNAME(ffi_closure_static_trampoline_table_page))\n#ifdef __ELF__\n .type CNAME(ffi_closure_static_trampoline_table_page), #function\n .size CNAME(ffi_closure_static_trampoline_table_page), . - CNAME(ffi_closure_static_trampoline_table_page)\n#endif\n#endif"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:字節跳動技術團隊(ID:BytedanceTechBlog)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/XLqcCfcNhpCA8Tg6LknBCQ","title":"xxx","type":null},"content":[{"type":"text","text":"學會黑科技,一招搞定 iOS 14.2 的 libffi crash"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章