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

做者:字節移動技術 —— 謝俊逸前端

蘋果升級 14.2,全球 iOS 遭了秧。libffi 在 iOS14.2 上發生了 crash,我司的許多 App 深受困擾,有許多基礎庫都是用了 libffi。 程序員

通過定位,發現是 vmremap 致使的 code sign error。咱們經過使用靜態 trampoline 的方式讓 libffi 不須要使用 vmremap,解決了這個問題。這裏就介紹一下相關的實現原理。編程

libffi 是什麼

高層語言的編譯器生成遵循某些約定的代碼。這些公約部分是單獨彙編工做所必需的。「調用約定」本質上是編譯器對函數入口處將在哪裏找到函數參數的假設的一組假設。「調用約定」還指定函數的返回值在哪裏找到。markdown

一些程序在編譯時可能不知道要傳遞給函數的參數。例如,在運行時,解釋器可能會被告知用於調用給定函數的參數的數量和類型。Libffi 可用於此類程序,以提供從解釋器程序到編譯代碼的橋樑。函數

libffi 庫爲各類調用約定提供了一個便攜式、高級的編程接口。這容許程序員在運行時調用調用接口描述指定的任何函數。oop

ffi 的使用性能

簡單的找了一個使用 ffi 的庫看一下他的調用接口測試

ffi_type *returnType = st_ffiTypeWithType(self.signature.returnType);
NSAssert(returnType, @"can't find a ffi_type of %@", self.signature.returnType);

NSUInteger argumentCount = self->_argsCount;
_args = malloc(sizeof(ffi_type *) * argumentCount) ;

for (int i = 0; i < argumentCount; i++) {
  ffi_type* current_ffi_type = st_ffiTypeWithType(self.signature.argumentTypes[i]);
  NSAssert(current_ffi_type, @"can't find a ffi_type of %@", self.signature.argumentTypes[i]);
  _args[i] = current_ffi_type;
}

// 建立 ffi 跳板用到的 closure
_closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&xxx_func_ptr);

// 建立 cif,調用函數用到的參數和返回值的類型信息, 以後在調用時會結合call convention 處理參數和返回值
if(ffi_prep_cif(&_cif, FFI_DEFAULT_ABI, (unsigned int)argumentCount, returnType, _args) == FFI_OK) {

        // closure 寫入 跳板數據頁
  if (ffi_prep_closure_loc(_closure, &_cif, _st_ffi_function, (__bridge void *)(self), xxx_func_ptr) != FFI_OK) {
    NSAssert(NO, @"genarate IMP failed");
  }
} else {
  NSAssert(NO, @"");
}
複製代碼

看完這段代碼,大概能理解 ffi 的操做。ui

  1. 提供給外界一個指針(指向 trampoline entry)
  2. 建立一個 closure, 將調用相關的參數返回值信息放到 closure 裏
  3. 將 closure 寫入到 trampoline 對應的 trampoline data entry 處

以後咱們調用 trampoline entry func ptr 時,spa

  1. 會找到 寫入到 trampoline 對應的 trampoline data entry 處的 closure 數據
  2. 根據 closure 提供的調用參數和返回值信息,結合調用約定,操做寄存器和棧,寫入參數 進行函數調用,獲取返回值。

那 ffi 是怎麼找到 trampoline 對應的 trampoline data entry 處的 closure 數據 呢?

咱們從 ffi 分配 trampoline 開始提及:

static ffi_trampoline_table * ffi_remap_trampoline_table_alloc (void) {
.....
  /* Allocate two pages -- a config page and a placeholder page */
  config_page = 0x0;
  kt = vm_allocate (mach_task_self (), &config_page, PAGE_MAX_SIZE * 2,
                    VM_FLAGS_ANYWHERE);
  if (kt != KERN_SUCCESS)
      return NULL;

  /* Allocate two pages -- a config page and a placeholder page */
  //bdffc_closure_trampoline_table_page

  /* Remap the trampoline table on top of the placeholder page */
  trampoline_page = config_page + PAGE_MAX_SIZE;
  trampoline_page_template = (vm_address_t)&ffi_closure_remap_trampoline_table_page;
#ifdef __arm__
  /* bdffc_closure_trampoline_table_page can be thumb-biased on some ARM archs */
  trampoline_page_template &= ~1UL;
#endif
  kt = vm_remap (mach_task_self (), &trampoline_page, PAGE_MAX_SIZE, 0x0,
                 VM_FLAGS_OVERWRITE, mach_task_self (), trampoline_page_template,
                 FALSE, &cur_prot, &max_prot, VM_INHERIT_SHARE);
  if (kt != KERN_SUCCESS)
  {
      vm_deallocate (mach_task_self (), config_page, PAGE_MAX_SIZE * 2);
      return NULL;
  }


  /* We have valid trampoline and config pages */
  table = calloc (1, sizeof (ffi_trampoline_table));
  table->free_count = FFI_REMAP_TRAMPOLINE_COUNT/2;
  table->config_page = config_page;
  table->trampoline_page = trampoline_page;

......
  return table;
}
複製代碼

首先 ffi 在建立 trampoline 時,會分配兩個連續的 page

trampoline page 會 remap 到咱們事先在代碼中彙編寫的 ffi_closure_remap_trampoline_table_page。

其結構如圖所示:

當咱們 ffi_prep_closure_loc(_closure, &_cif, _st_ffi_function, (__bridge void *)(self), entry1)) 寫入 closure 數據時, 會寫入到 entry1 對應的 closuer1。

ffi_status ffi_prep_closure_loc (ffi_closure *closure, ffi_cif* cif, void (*fun)(ffi_cif*,void*,void**,void*), void *user_data, void *codeloc) {
......
  if (cif->flags & AARCH64_FLAG_ARG_V)
      start = ffi_closure_SYSV_V; // ffi 對 closure的處理函數
  else
      start = ffi_closure_SYSV;

  void **config = (void**)((uint8_t *)codeloc - PAGE_MAX_SIZE);
  config[0] = closure;
  config[1] = start;
......
}

複製代碼

這是怎麼對應到的呢? closure1 和 entry1 距離其所屬 Page 的 offset 是一致的,經過 offset,成功創建 trampoline entry 和 trampoline closure 的對應關係。

如今咱們知道這個關係,咱們經過代碼看一下到底在程序運行的時候 是怎麼找到 closure 的。

這四條指令是咱們 trampoline entry 的代碼實現,就是 ffi 返回的 xxx_func_ptr

adr x16, -PAGE_MAX_SIZE
ldp x17, x16, [x16]
br x16
nop
複製代碼

經過 .rept 咱們建立 PAGE_MAX_SIZE / FFI_TRAMPOLINE_SIZE 個跳板,恰好一個頁的大小

# 動態remap的 page
.align PAGE_MAX_SHIFT
CNAME(ffi_closure_remap_trampoline_table_page):
.rept PAGE_MAX_SIZE / FFI_TRAMPOLINE_SIZE
  # 這是咱們的 trampoline entry, 就是ffi生成的函數指針
  adr x16, -PAGE_MAX_SIZE                         // 將pc地址減去PAGE_MAX_SIZE, 找到 trampoine data entry
  ldp x17, x16, [x16]                             // 加載咱們寫入的 closure, start 到 x17, x16
  br x16                                          // 跳轉到 start 函數
  nop        /* each entry in the trampoline config page is 2*sizeof(void*) so the trampoline itself cannot be smaller that 16 bytes */
.endr
複製代碼

經過 pc 地址減去 PAGE_MAX_SIZE 就找到對應的 trampoline data entry 了。

靜態跳板的實現

因爲代碼段和數據段在不一樣的內存區域。

咱們此時不能經過 像 vmremap 同樣分配兩個連續的 PAGE,在尋找 trampoline data entry 只是簡單的-PAGE_MAX_SIZE 找到對應關係,須要稍微麻煩點的處理。

主要是經過 adrp 找到_ffi_static_trampoline_data_page1_ffi_static_trampoline_page1的起始地址,用 pc-_ffi_static_trampoline_page1的起始地址計算 offset,找到 trampoline data entry。

# 靜態分配的page
#ifdef __MACH__
#include <mach/machine/vm_param.h>

.align 14
.data
.global _ffi_static_trampoline_data_page1
_ffi_static_trampoline_data_page1:
    .space PAGE_MAX_SIZE*5
.align PAGE_MAX_SHIFT
.text
CNAME(_ffi_static_trampoline_page1):

_ffi_local_forwarding_bridge:
adrp x17, ffi_closure_static_trampoline_table_page_start@PAGE;// text page
sub  x16, x16, x17;// offset
adrp x17, _ffi_static_trampoline_data_page1@PAGE;// data page
add x16, x16, x17;// data address
ldp x17, x16, [x16];// x17 closure x16 start
br x16
nop
nop
.align PAGE_MAX_SHIFT
CNAME(ffi_closure_static_trampoline_table_page):

#這個label 用來adrp@PAGE 計算 trampoline 到 trampoline page的offset
#留了5個用來調試。
# 咱們static trampoline 兩條指令就夠了,這裏使用4個,和remap的保持一致
ffi_closure_static_trampoline_table_page_start:
adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop

adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop

adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop

adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop

adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop

// 5 * 4
.rept (PAGE_MAX_SIZE*5-5*4) / FFI_TRAMPOLINE_SIZE
adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop
.endr

.globl CNAME(ffi_closure_static_trampoline_table_page)
FFI_HIDDEN(CNAME(ffi_closure_static_trampoline_table_page))
#ifdef __ELF__
        .type        CNAME(ffi_closure_static_trampoline_table_page), #function
        .size        CNAME(ffi_closure_static_trampoline_table_page), . - CNAME(ffi_closure_static_trampoline_table_page)
#endif
#endif
複製代碼

關於字節移動平臺團隊

字節跳動移動平臺團隊(Client Infrastructure)是大前端基礎技術行業領軍者,負責整個字節跳動的大前端基礎設施建設,提高公司全產品線的性能、穩定性和工程效率,支持的產品包括但不限於抖音、今日頭條、西瓜視頻、火山小視頻等,在移動端、Web、Desktop 等各終端都有深刻研究。

就是如今!客戶端/前端/服務端/測試開發 面向社會+校園招聘,base 北上廣深杭成!一塊兒來用技術改變世界,感興趣能夠聯繫郵箱 chenxuwei.cxw@bytedance.com郵件主題:簡歷-姓名-求職意向-電話

相關文章
相關標籤/搜索