写点什么

学会黑科技,一招搞定 iOS 14.2 的 libffi crash

  • 2021-05-17
  • 本文字数:3868 字

    阅读完需:约 13 分钟

学会黑科技,一招搞定 iOS 14.2 的 libffi crash

苹果升级 14.2,全球 iOS 遭了秧。libffi 在 iOS14.2 上发生了 crash, 我司的许多 App 深受困扰,有许多基础库都是用了 libffi。



经过定位,发现是 vmremap 导致的 code sign error。我们通过使用静态 trampoline 的方式让 libffi 不需要使用 vmremap,解决了这个问题。这里就介绍一下相关的实现原理。

libffi 是什么

高层语言的编译器生成遵循某些约定的代码。这些公约部分是单独汇编工作所必需的。“调用约定”本质上是编译器对函数入口处将在哪里找到函数参数的假设的一组假设。“调用约定”还指定函数的返回值在哪里找到。

一些程序在编译时可能不知道要传递给函数的参数。例如,在运行时,解释器可能会被告知用于调用给定函数的参数的数量和类型。Libffi 可用于此类程序,以提供从解释器程序到编译代码的桥梁。

libffi 库为各种调用约定提供了一个便携式、高级的编程接口。这允许程序员在运行时调用调用接口描述指定的任何函数。

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 的操作。

  1. 提供给外界一个指针(指向 trampoline entry)

  2. 创建一个 closure, 将调用相关的参数返回值信息放到 closure 里

  3. 将 closure 写入到 trampoline 对应的 trampoline data entry 处


之后我们调用 trampoline entry func ptr 时,

  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_statusffi_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_SIZEldp x17, x16, [x16]br x16nop
复制代码


通过 .rept 我们创建 PAGE_MAX_SIZE / FFI_TRAMPOLINE_SIZE 个跳板,刚好一个页的大小


# 动态remap的 page.align PAGE_MAX_SHIFTCNAME(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.textCNAME(_ffi_static_trampoline_page1):
_ffi_local_forwarding_bridge:adrp x17, ffi_closure_static_trampoline_table_page_start@PAGE;// text pagesub x16, x16, x17;// offsetadrp x17, _ffi_static_trampoline_data_page1@PAGE;// data pageadd x16, x16, x17;// data addressldp x17, x16, [x16];// x17 closure x16 startbr x16nopnop.align PAGE_MAX_SHIFTCNAME(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, #0b _ffi_local_forwarding_bridgenopnop
adr x16, #0b _ffi_local_forwarding_bridgenopnop
adr x16, #0b _ffi_local_forwarding_bridgenopnop
adr x16, #0b _ffi_local_forwarding_bridgenopnop
adr x16, #0b _ffi_local_forwarding_bridgenopnop
// 5 * 4.rept (PAGE_MAX_SIZE*5-5*4) / FFI_TRAMPOLINE_SIZEadr x16, #0b _ffi_local_forwarding_bridgenopnop.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
复制代码


本文转载自:字节跳动技术团队(ID:BytedanceTechBlog)

原文链接:学会黑科技,一招搞定 iOS 14.2 的 libffi crash

2021-05-17 13:002314

评论

发布
暂无评论
发现更多内容

Invalid bound statement (not found)

任广印

Java MyBatisPlus

碎碎念之「程序员的时间都花在了哪?」

Justin

设计原则 代码规范 28天写作 技术债

LiteOS调测利器:backtrace函数原理知多少

华为云开发者联盟

架构 内存 函数 LiteOS backtrace

微服务转型系列1:农商行数字化转型的烦恼

BoCloud博云

微服务 银行数字化转型 API 服务治理

666666666666666666666

Paul

大数据

低代码:Microsoft Power Platform

lidaobing

低代码 28天写作 Power Platform

阿里巴巴管理三板斧

Ian哥

28天写作

HTTPS实现原理

架构精进之路

https 七日更 28天写作

java中的类和object,其实没那么难~

田维常

类集

从设计模式理解Vue响应式(多图警告)

coolFish(呔呆)

JavaScript vue.js 响应式 大前端 设计模式

我的算法学习之路

熊斌

学习方法 算法 28天写作

凝聚人心并不难,小诀窍让团队跟你一条心

一笑

管理 激励 28天写作

云算力挖矿系统开发app,矿机租赁交易平台搭建

v16629866266

“大禹针”在北江大堤上线,浪潮助力广东水利新基建落地

新基建

15道类和对象面试题,快看看自己会几道

田维常

类集

太牛了!美团Android开发工程师岗位职能要求,大厂面试题汇总

欢喜学安卓

android 程序员 面试 移动开发

一文带你解读Volcano架构设计与原理

华为云开发者联盟

架构 Kubernetes 负载 Volcano 集群

西少爷肉夹馍的股权纠纷 | 视频号28天(22)

赵新龙

28天写作

网站自动化任务脚本

Kylin

七日更

Serverless Kubernetes:理想,现实与未来

阿里巴巴云原生

Serverless 容器 运维 云原生 k8s

企业级低代码平台的选型和建设思考

李小腾

扎根CNCF社区贡献五年是怎样的体验?听听华为云原生开源团队的负责人怎么说

华为云开发者联盟

容器 Volcano cncf kubeedge 代码开发

重温亮剑-感悟

superman

音视频传输协议众多, 5G时代不同业务应该如何选择?

华为云开发者联盟

5G 音视频 直播 流媒体

顺利拿到OPPO公司Android架构师offer,Android跨进程通信导论,全套教学资料

欢喜学安卓

android 程序员 面试 移动开发

【JS】预编译

德育处主任

JavaScript 大前端 js 28天写作

个人隐私后续

张老蔫

28天写作

大数据丨ClickHouse在京东能源管理平台的实践

京东科技开发者

数据库 大数据

技术赋能教育,浅谈教育机构转型的制胜关键

华为云开发者联盟

音视频 在线教育

宅米网技术架构演进分析

Andy

个人web分享92道JavaScript面试题附加回答

我是哪吒

程序员 面试 大前端 程序媛

学会黑科技,一招搞定 iOS 14.2 的 libffi crash_移动_字节跳动技术团队_InfoQ精选文章