最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

学会黑科技,一招搞定 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:001867

评论

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

数据仓库实战教程

kingcall

数据仓库 数据湖 数据安全 数仓 数仓建模

华为云Astro的前世今生:用7年时间革新低代码开发观念

科技怪授

jvm(三)类加载机制、javac编译

想要飞的猪

JVM类加载

redis 集群实现方案与原理

想要飞的猪

redis哨兵集群 redid集群cluster

7分钟带你细致解析4个Java算法必刷题

好程序员IT教育

Java

在 Spring 生态中玩转 RocketMQ

Apache RocketMQ

RocketMQ spring could

唯一获奖容器厂商!灵雀云斩获2022信创“大比武”通信赛道大奖

York

容器 云原生 数字化转型 国产化 信创云

MACH架构的质量工程指南

俞凡

架构 微服务 云原生

【Ajax】全面详细了解git的基础操作【万字教学+面试常客】

坚毅的小解同志

git 11月月更

边旅游边赚钱!数字游民离不开远程控制软件

RayLink远程工具

远程控制软件 远程办公软件 远控软件 RayLink 远程控制连接

Meetup预告:SeaTunnel在天翼云数据集成平台的探索实践

Apache SeaTunnel

大数据 技术分享 数据同步 数据集成 Apache SeaTunnel

华为云弹性负载均衡ELB,如何保障服务器不瘫痪?

清欢科技

深度解读隐语密态计算设备SPU

隐语SecretFlow

机器学习 开源 隐私计算 开源框架 隐语

前端培训班学习真的靠谱吗?

小谷哥

Unload data from Databend | 新手篇(4)

Databend

databend

需求收集方法工具,以及进行需求分析的6大要素

PingCode

Alluxio 2.9新版发布 | 重塑架构,支持大规模多租户环境

Alluxio

分布式 新闻 Alluxio 大数据 开源 数据编排

数字化转型,要把功夫炼到任督二脉

脑极体

前端食堂技术周刊第 60 期:TypeScript 4.9、Ant Design 5.0、用 vanilla-extract 编写高性能的 CSS、Node.js 安全最佳实践

童欧巴

华为云企业交换机ESW,让数据业务无缝迁移上云

爱科技的水月

高标准企业级安全性,华为云会议为线上沟通保驾护航

秃头也爱科技

第二章STP应用配置

初学者

网络 11月月更

Java集合与数据类型

kingcall

Java JAVA集合 Java集合框架 java数据类型

低代码会使初级码农失业吗

秃头也爱科技

中国银河证券:缺少DevOps,企业数字化转型就是带着脚镣跳舞

嘉为蓝鲸

DevOps 数字化转型 金融 证券

第一章三层交换应用

初学者

网络 11月月更

国内外排名最靠前的20+项目管理软件

PingCode

零基础转行前端培训学习好吗?

小谷哥

netty系列之:在netty中使用proxy protocol

程序那些事

Java 架构 Netty 程序那些事

“PKS+生态(智方舟)加速营”圆满结束,九科信息与中电智方舟达成战略合作

九科Ninetech

打造用户喜爱的产品,离不开需求助推器|影响地图Impact Mapping

老彦

敏捷开发 软件工程 设计思维 用户故事地图 影响地图

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