找回密码
 会员注册
查看: 19|回复: 0

学会黑科技,一招搞定iOS14.2的libfficrash

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
73650
发表于 2024-10-1 08:10:13 | 显示全部楼层 |阅读模式
作者:字节移动技术 —— 谢俊逸苹果升级 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'tfindaffi_typeof%@",self.signature.returnType);NSUIntegerargumentCount=self->_argsCount;_args=malloc(sizeof(ffi_type*)*argumentCount);for(inti=0;ifree_count=FFI_REMAP_TRAMPOLINE_COUNT/2;table->config_page=config_page;table->trampoline_page=trampoline_page;......returntable;}首先 ffi 在创建 trampoline 时,会分配两个连续的 pagetrampoline 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的处理函数elsestart=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_ptradr 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 .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_bridgenopnopadr x16, #0b _ffi_local_forwarding_bridgenopnopadr x16, #0b _ffi_local_forwarding_bridgenopnopadr x16, #0b _ffi_local_forwarding_bridgenopnopadr 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关于字节移动平台团队字节跳动移动平台团队(Client Infrastructure)是大前端基础技术行业领军者,负责整个字节跳动的大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率,支持的产品包括但不限于抖音、今日头条、西瓜视频、火山小视频等,在移动端、Web、Desktop 等各终端都有深入研究。就是现在!客户端/前端/服务端/测试开发 面向社会+校园招聘,Base 北上广深杭成!一起来用技术改变世界,感兴趣可以联系邮箱:chenxuwei.cxw@bytedance.com ,邮件主题:简历-姓名-求职意向-电话。点击阅读原文,快来加入我们吧!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-13 10:45 , Processed in 1.567303 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表