本文主要记录了 iOS 移动端的一个疑难 bug 的排查过程,以及介绍经过给 bitcode 打补丁从新生成机器码,为有问题的第三方库修复 bug 的方法。html
主要涉及到的知识点以下:前端
经过内部的崩溃监控发现,有一个内部 App,近期出现了较多的崩溃现象。其中数量占比最多的崩溃,其崩溃线程捕获到的调用栈以下:node
libsystem_kernel.dylib 0x00000001cc78c414 ___pthread_kill + 7
libsystem_c.dylib 0x00000001a7db2b74 _abort + 103
App 0x0000000103092868 ___48-[BLYLogicManager abortAfterSendingReportIfNeed]_block_invoke + 87
libdispatch.dylib 0x000000019e60824c __dispatch_call_block_and_release + 31
libdispatch.dylib 0x000000019e609db0 __dispatch_client_callout + 19
libdispatch.dylib 0x000000019e61aa68 __dispatch_root_queue_drain + 655
libdispatch.dylib 0x000000019e61b120 __dispatch_worker_thread2 + 115
libsystem_pthread.dylib 0x00000001ea1e77c8 __pthread_wqthread + 215
复制代码
这个调用栈并无提供什么有效的信息,只能看出来是 bugly 框架已经检测到了崩溃建立了新的 dispatch queue 并终止进程,也就是说,其实有效的崩溃信息被 bugly 给吃掉了。ios
看一下其余线程,是否有可用的信息,通常能够在其余线程的调用栈上搜索如下内容:c++
_ZSt9terminateEv
: C++ 的终端异常处理( std::terminate(void)
)__sigtramp
: 信号中断处理例程入口终于搜索到了如下内容:git
Thread #52: id=1a6c6, name=
libsystem_kernel.dylib 0x00000001cc78cf5c ___ulock_wait + 7
libdispatch.dylib 0x000000019e60a528 __dispatch_thread_event_wait_slow + 55
libdispatch.dylib 0x000000019e618708 ___DISPATCH_WAIT_FOR_QUEUE__ + 351
libdispatch.dylib 0x000000019e6182b0 __dispatch_sync_f_slow + 147
App 0x00000001030925f0 -[BLYLogicManager executeEmergencyLogic:] + 695
App 0x000000010308b6a8 -[BLYCrashManager sendLiveCrashReport] + 203
App 0x000000010305f478 _BLYCrashHandlerCallback + 5555
App 0x000000010305bc2c _BLYBSDSignalHandlerCallback + 95
libsystem_platform.dylib 0x00000001ea1e1290 __sigtramp + 55
App 0x00000001029543dc *redacted*
App 0x00000001029543dc *redacted*
App 0x00000001028a1918 *redacted*
App 0x00000001027ea9c4 *redacted*
App 0x00000001027ea794 *redacted*
App 0x00000001027ead60 *redacted*
libsystem_pthread.dylib 0x00000001ea1e5b40 __pthread_start + 319
复制代码
内部应用同时集成了 Bugly 和自有的崩溃捕获,一般状况下 Bugly 会在本身捕获完成后,将崩溃现场转交给其余框架,使两次捕获的崩溃现场相同。而这个崩溃则否则, Bugly 捕获了崩溃后,直接调用 abort
结束了应用,致使自有崩溃只捕获到了 SIGABRT
。github
经过检查主线程调用栈,发现了一些不一样:算法
Thread #0: id=1a0d3, name=
libsystem_kernel.dylib 0x00000001cc78c1ac ___psynch_cvwait + 7
libc++.1.dylib 0x00000001b3a25328 __ZNSt3__118condition_variable4waitERNS_11unique_lockINS_5mutexEEE + 27
App 0x000000010280e5c8 *redacted*
App 0x00000001027e9414 *redacted*
App 0x00000001027e9380 *redacted*
libsystem_c.dylib 0x00000001a7d930b8 ___cxa_finalize_ranges + 423
libsystem_c.dylib 0x00000001a7d93400 _exit + 27
UIKitCore 0x00000001a13d4bdc -[UIApplication _terminateWithStatus:] + 503
UIKitCore 0x00000001a0a23648 -[_UISceneLifecycleMultiplexer _evalTransitionToSettings:fromSettings:forceExit:withTransitionStore:] + 127
UIKitCore 0x00000001a0a23278 -[_UISceneLifecycleMultiplexer forceExitWithTransitionContext:scene:] + 219
UIKitCore 0x00000001a13ca644 -[UIApplication workspaceShouldExit:withTransitionContext:] + 211
FrontBoardServices 0x00000001ae6d2780 -[FBSUIApplicationWorkspaceShim workspaceShouldExit:withTransitionContext:] + 87
FrontBoardServices 0x00000001ae701390 ___63-[FBSWorkspaceScenesClient willTerminateWithTransitionContext:]_block_invoke_2 + 79
FrontBoardServices 0x00000001ae6e54a0 -[FBSWorkspace _calloutQueue_executeCalloutFromSource:withBlock:] + 239
FrontBoardServices 0x00000001ae701328 ___63-[FBSWorkspaceScenesClient willTerminateWithTransitionContext:]_block_invoke + 131
libdispatch.dylib 0x000000019e609db0 __dispatch_client_callout + 19
libdispatch.dylib 0x000000019e60d738 __dispatch_block_invoke_direct + 267
FrontBoardServices 0x00000001ae72a250 ___FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 47
FrontBoardServices 0x00000001ae729ee0 -[FBSSerialQueue _targetQueue_performNextIfPossible] + 447
FrontBoardServices 0x00000001ae72a434 -[FBSSerialQueue _performNextFromRunLoopSource] + 31
CoreFoundation 0x000000019e99176c ___CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 27
CoreFoundation 0x000000019e991668 ___CFRunLoopDoSource0 + 207
CoreFoundation 0x000000019e9909cc ___CFRunLoopDoSources0 + 375
CoreFoundation 0x000000019e98aa8c ___CFRunLoopRun + 823
CoreFoundation 0x000000019e98a21c _CFRunLoopRunSpecific + 599
GraphicsServices 0x00000001b648e784 _GSEventRunModal + 163
UIKitCore 0x00000001a13c8fe0 -[UIApplication _run] + 1071
UIKitCore 0x00000001a13ce854 _UIApplicationMain + 167
App 0x00000001037dc93c *redacted*
App 0x00000001032625c4 *redacted*
App 0x00000001027bc618 main (main.swift:9:13)
libdyld.dylib 0x000000019e64a6b0 _start + 3
复制代码
能够看到调用栈中有 exit
,这代表应用正在正常退出。swift
调用栈中,在 exit
这一项的上面,能够看到 __cxa_finalize_ranges
,这是由 C++ 代码产生的调用,经过 __cxa_atexit
注册回调,在应用退出时调用,用来正在进行全局变量的销毁。api
由此能够看出,这是一个因为 C++ 全局变量在应用退出时销毁,致使其余线程引用到了销毁的资源产生的崩溃。这也能够解释为何 Bugly 没有把崩溃现场交给其余框架进行处理了: Bugly 检测到了应用正在退出,直接调用了 executeEmergencyLogic:
方法,优先保证本身的处理。
__cxa_atexit
是 Itanium C++ ABI 运行时规范的一部分,它用来支持 C++ 语法中的全局变量。咱们知道,C++ 对象分为 POD 和非 POD 两类,其中 POD 以及 constexpr 构造器能够在编译期初始化,而非 constexpr 构造的类型只能经过构造器在运行期间来构造。C++ 对于这两类对象,都支持它们做为全局变量并提供初始值,那么这些全局变量就要在 dso 加载期间调用构造器来初始化。
一样地,为了防止内存/资源泄漏,C++ 规定这样初始化的全局变量要在 dso 卸载时析构。
咱们能够经过查看反汇编,对它的实现机制一探究竟。
举个例子,有以下 C++ 代码:
#include <iostream>
class Test {
public:
virtual ~Test();
Test();
};
Test::Test() {}
Test::~Test() {
std::cout << "Test: dtor" << std::endl;
}
static Test t = Test();
复制代码
使用 clang++ 对以上文件进行编译,查看生成的汇编代码:
xcrun clang++ -sdk iphoneos -arch arm64 1.cc -s -o 1.s
复制代码
.section __TEXT,__StaticInit,regular,pure_instructions
.p2align 2 ; -- Begin function __cxx_global_var_init
___cxx_global_var_init: ; @__cxx_global_var_init
.cfi_startproc
; %bb.0:
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 16-byte Folded Spill
add x29, sp, #16 ; =16
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
adrp x0, __ZL1t@PAGE
add x0, x0, __ZL1t@PAGEOFF
str x0, [sp, #8] ; 8-byte Folded Spill
bl __ZN4TestC1Ev
ldr x1, [sp, #8] ; 8-byte Folded Reload
adrp x0, __ZN4TestD1Ev@PAGE
add x0, x0, __ZN4TestD1Ev@PAGEOFF
adrp x2, ___dso_handle@PAGE
add x2, x2, ___dso_handle@PAGEOFF
bl ___cxa_atexit ; ②
ldp x29, x30, [sp, #16] ; 16-byte Folded Reload
add sp, sp, #32 ; =32
ret
.cfi_endproc
; -- End function
.section __TEXT,__StaticInit,regular,pure_instructions
.p2align 2 ; -- Begin function _GLOBAL__sub_I_1.cc
__GLOBAL__sub_I_1.cc: ; @_GLOBAL__sub_I_1.cc
.cfi_startproc
; %bb.0:
stp x29, x30, [sp, #-16]! ; 16-byte Folded Spill
mov x29, sp
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
bl ___cxx_global_var_init
ldp x29, x30, [sp], #16 ; 16-byte Folded Reload
ret
.cfi_endproc
; -- End function
.section __DATA,__mod_init_func,mod_init_funcs
.p2align 3
.quad __GLOBAL__sub_I_1.cc ; ①
复制代码
从反汇编代码中,能够看出实际的方案是:
__mod_init_funcs
中去,这样 dso 加载时,动态连接器会主动执行它们(位于汇编代码①处);__cxa_atexit
,传入已经构造的对象的指针和 deleting destructor,这样 dso 卸载时,这些构造的对象会被销毁(位于汇编代码②处)。不少使用 C++ 的库都会分配线程资源进行并发执行。若是这些正在执行的线程须要引用全局变量,同时触发了 dso 卸载,那么就会发生线程跑着跑着,全局变量析构了,因而进程就崩溃了。
理论上讲,dso 的卸载是可控的,由于咱们总能够控制逻辑,让动态库的资源都释放掉之后,再去卸载动态库/退出进程。
可是在 iOS 移动应用上,有一个例外——
用户能够经过多任务手势,杀死应用。若是被杀的应用恰巧在前台运行,那么 iOS 会给这个应用发送 SIGTERM
信号。UIKit 收到信号后会调用应用代理的 applicationWillTerminate(_:)
方法,使得应用有机会保存一些状态数据,而后正常退出应用。
这个时候咱们是没有机会释放线程资源的,由于 terminate 的生命周期很短,没有时间给咱们等待异步线程结束,因此这个崩溃就没法避免了。
所幸的是,这种崩溃并不会被用户感知到:即便应用不崩溃,也会当即正常退出,对于用户来讲表现是同样的。
其实同类的问题之前在该 App 中也是发生过的——咱们有一个内部 SDK 一样也是 C++ 写成,拥有全局状态变量,开启异步线程池访问这些变量,用户在前台杀死应用时触发崩溃。
当时的解决方案是:升级工具链。根据 Apple 发布的 Xcode 11 更新日志,apple clang++ 编译器增长了禁用全局变量析构的编译参数 -fno-c++-static-destructors
。使用该标记编译的 C++ 源文件,不会生成对全局变量进行析构的代码。
这对 iOS 应用来讲是安全的——由于 iOS 应用几乎不会在运行时卸载动态库,无需考虑动态库卸载的资源泄漏问题。
然而此次的问题又有所不一样——出现问题的是一个由第三方提供的二进制库,咱们手里是没有它的源代码的,也就没法经过修改编译参数的方式来从新编译生成机器码。
可是咱们可否再深刻一下,帮助三方库来修复这个 bug 呢?
修复该问题的直接方案,就是修改机器码,消除对 __cxa_atexit
的调用。
一个三方静态库 SDK,通常由如下文件组成:
不管是采用零散的文件,仍是采用 .framework
封装,它们的组成基本上是一致的。
咱们要修改的是它的部分机器码,因此要将其中的 .a 静态库解开,再进行编辑。
首先来查看一下 .a 文件的内容:
❯ lipo -info libsample.a
Architectures in the fat file: libsample.a are: armv7 arm64
复制代码
这是一个 Universal binary,包含了两种 iOS 真机的 CPU 架构的代码。咱们先针对主流机型使用的 arm64 架构尝试调整。
使用 lipo
命令将 arm64 架构单独抽取出来:
❯ lipo -thin arm64 libsample.a -o libsample_arm64.a
复制代码
只有把 Universal binary 中特定的架构抽取出来,才能使用 ar(1) 操做:
❯ mkdir objects
❯ cd objects
# 打印 .a 中包含的文件列表
❯ ar t ../libsample_arm64.a
__.SYMDEF
sample.o
sample.o
# 解包 .a 文件
❯ ar -x ../libsample_arm64.a
复制代码
使用上述命令进行 .a 文件的展开后,出现了一个问题: ar t
命令中,列出了两个 sample.o
文件,可是 ar x
命令只解出来了一个。这是由于 ar 归档中,没有目录的概念,不一样目录下的同名目标文件,在 ar 归档的过程当中,会被打平,致使 ar 归档中包含多个同名文件。
这会致使咱们使用 ar x
解包的时候,相同的文件会被覆盖成一个,也无法把它们单独解压出来。
那么如何才能把 ar 归档中的同名文件分别解包出来呢……那么就得提到「游手好闲」的 7-zip 了……
7-zip 做为一个压缩软件,除了支持常规的压缩文件格式以外,还支持了不少归档文件以及 PE 可执行文件(特别地,支持了部分安装器的 SFX 模块)。咱们来尝试一下它是否支持 .a 归档:
❯ 7z l ./libsample_arm64.a
7-Zip [64] 17.04 : Copyright (c) 1999-2021 Igor Pavlov : 2017-08-28
p7zip Version 17.04 (locale=utf8,Utf16=on,HugeFiles=on,64 bits,16 CPUs x64)
Scanning the drive for archives:
1 file, 78960 bytes (78 KiB)
Listing archive: ./libsample_arm64.a
--
Path = ./libsample_arm64.a
Type = Ar
Physical Size = 78960
SubType = a:BSD
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2021-06-23 15:05:09 ..... 1710 1710 1.txt
2021-06-23 15:03:54 ..... 38616 38616 1.sample.o
2021-06-23 15:04:02 ..... 38616 38616 2.sample.o
------------------- ----- ------------ ------------ ------------------------
2021-06-23 15:05:09 78942 78942 3 files
复制代码
能够看到,7-zip 自动为 .a 中的文件名进行了修正。同时,7-zip 在解压的时候遇到同名文件,会提供是否覆盖及自动重命名文件的选项:
❯ 7z x ./libsample_arm64.a
7-Zip [64] 17.04 : Copyright (c) 1999-2021 Igor Pavlov : 2017-08-28
p7zip Version 17.04 (locale=utf8,Utf16=on,HugeFiles=on,64 bits,16 CPUs x64)
Scanning the drive for archives:
1 file, 78960 bytes (78 KiB)
Extracting archive: ./libsample_arm64.a
--
Path = ./libsample_arm64.a
Type = Ar
Physical Size = 78960
SubType = a:BSD
Would you like to replace the existing file:
Path: ./sample.o
Size: 2736 bytes (3 KiB)
Modified: 2017-05-15 11:59:49
with the file from archive:
Path: sample.o
Size: 76088 bytes (75 KiB)
Modified: 2017-05-15 11:58:47
? (Y)es / (N)o / (A)lways / (S)kip all / A(u)to rename all / (Q)uit? u
Everything is Ok
Files: 3
Size: 78942
Compressed: 78960
复制代码
只要咱们选择 Auto rename all,7-zip 就会自动帮咱们处理文件重名的问题了。而咱们从新打包 .a 文件时,.o 文件的名称并不重要,能够随便取,因此这里改为其余名字也没有关系。
咱们再来回顾一下典型的全局变量析构调用的注册:
LDR X1, [SP,#0x10+var_8]
ADRP X0, #__ZN4TestD1Ev@PAGE ; Test::~Test()
ADD X0, X0, #__ZN4TestD1Ev@PAGEOFF ; Test::~Test()
ADRP X2, #___dso_handle@PAGE
ADD X2, X2, #___dso_handle@PAGEOFF
BL ___cxa_atexit
LDP X29, X30, [SP,#0x10+var_s0]
ADD SP, SP, #0x20
RET
复制代码
经过阅读 Itanium C++ ABI,能够看到 __cxa_atexit
的函数签名以下:
// 3.3.6.3 Runtime API
extern _LIBCXXABI_FUNC_VIS int __cxa_atexit(void (*f)(void *), void *p, void *d);
复制代码
对比反汇编代码,能够看到 X0 传入了对象类型的删除析构( ...D1Ev
)函数的指针,X1 传入了对象地址,X2 传入了 dso 句柄,与函数签名相符。
要消除对 __cxa_atexit
的调用,只须要把其中的 bl
指令改为 nop
便可。
反汇编软件 IDA 提供了即时汇编的功能,能够经过手写汇编指令,由 IDA 生成机器码直接写入文件中。惋惜这个功能对于 arm64 架构没有支持,咱们须要找另外的方法。
好在咱们能够查阅 AArch64 指令集架构文档,其中提到:
经过文档,咱们看到了在 AArch64 架构下, NOP
指令的具体编码。
因为 Apple arm64 CPU 是小端序,那么咱们应该把 bl
指令对应的四个字节替换为:
1F 20 03 D5 ; NOP
复制代码
除此以外,还有数种不一样的状况,须要针对性地作不一样的修改。如下列出了两种不一样状况。
尾调用:
; 各类填写参数...
B ___cxa_atexit
; end of function
复制代码
此时要把 B
指令改成 RET lr
。
返回值校验:
; 各类填写参数...
BL ___cxa_atexit
CBZ W0, check_pass
BL assert_fail
check_pass:
; ... 正常逻辑
复制代码
此时要把 B
指令改成 MOV w0, wzr
,才能经过校验。
至此咱们能够看出,经过这种方式修改机器码,存在很大的局限性:
那么是否存在更好的解决方案呢?
在使用 otool
检查解包出来的 .o 文件时,发现了以下区段:
Section
sectname __bitcode
segname __LLVM
addr 0x0000000000000ee8
size 0x0000000000005f70
offset 4928
align 2fn:0 (1)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
复制代码
这意味着,这个目标文件内嵌了 bitcode。众所周知,Clang/LLVM 是苹果亲儿子,苹果基于这一套体系搞出了许多新鲜玩意儿,bitcode 就是其中之一。
clang 编译器会先将源文件编译为 LLVM IR,再把 IR 编译到机器码。IR 的大部分设计都是平台中立的,少部分平台相关的代码在 CPU 架构不发生大变化时基本兼容,并且从 IR 生成机器码的过程能够单独优化。
LLVM IR 有不一样的表示方案,有文本形式的 IR 汇编、二进制编码的 bitcode。
Apple 容许应用在编译时将 bitcode 内嵌在二进制文件内,随应用一块儿提交给 Apple。一旦 Apple 推出了效率更高的机器码生成方案,或者是推出了新款 CPU,Apple 能够根据你提交的 bitcode 从新生成更高效的机器指令,开发者无需作任何事便可享受到这个优化。
好比 iPhone X 的 CPU 架构有小升级,内嵌了 bitcode 的应用就能够免费得到 arm64e CPU 架构的支持。
使用开源项目 LibEBC 能够提取 .o 文件中的 bitcode :
❯ /path/to/ebcutil -e ./1.sample.o
Mach-O arm64
File name: 1.sample.o
Arch: arm64
UUID: 00000000-0000-0000-0000-000000000000
Wrapper: D809E5ED-7D43-4E42-B829-7EFF246EE28C
IR: 250BD0A9-67D6-499B-9E63-9D628FB0D7C7
❯ mv ./250BD0A9-67D6-499B-9E63-9D628FB0D7C7 ./1.sample.bc
复制代码
使用 LLVM 项目(须要经过 Homebrew 安装 llvm)提供的 llvm-dis
工具能够将 bc 文件转换为可读的 IR 汇编格式:
❯ llvm-dis ./1.sample.bc
复制代码
这会生成一个同名的 .ll 文件,能够用文本编辑器打开。其中关于全局变量初始化的部分以下:
; 省略无关代码
; Function Attrs: noinline ssp uwtable
define internal void @__cxx_global_var_init() #3 section "__TEXT,__StaticInit,regular,pure_instructions" {
%1 = call %class.Sample1* @_ZN7Sample1C1Ev(%class.Sample1* @_ZL2s1)
%2 = call i32 @__cxa_atexit(void (i8*)* bitcast (%class.Sample1* (%class.Sample1*)* @_ZN7Sample1D1Ev to void (i8*)*), i8* bitcast (%class.Sample1* @_ZL2s1 to
i8*), i8* @__dso_handle) #4
ret void
}
; Function Attrs: nounwind
declare i32 @__cxa_atexit(void (i8*)*, i8*, i8*) #4
复制代码
IR 的详细语法在此就不展开介绍了,有兴趣的同窗能够查看LLVM 官方文档。其中比较重要的有:
declare
用来声明对外部符号的引用,例如此处引用了外部函数 __cxa_atexit
。call
用来作函数调用须要注意的是,在 IR 中,全部 %
加数字组成的标号必须连续。例如若是我注释了上述代码中的 %1
所在的一行,就会产生 IR 汇编错误,此时就必须把下一行的 %2
改为 %1
,才能符合规则汇编经过。
在上述代码中,咱们只须要把 %2
所在的一行给注释掉,便可完成修复。若是一个 IR 函数内有多个调用,就须要按照标号连续的规则,将注释掉的代码后面的全部标号依次提早了。
正确的 IR 操做姿式是写一个 IR pass,而后经过 llvm-opt 去加载这个 pass,读取 .bc 文件而不是人类可读的 .ll 文件,来对原有的 bitcode 作变换。可是写一个 pass 须要的成本比临时修复问题要高得多,对于少数几个目标文件的修复,能够经过文本替换工具或脚本语言来替换标号。例如使用 node.js:
function replaceLabels(from, to, diff) {
let source = fs.readFileSync('tmp.ll', 'utf8');
for (let i = from; i <= to; ++i) {
// 修改 % 变量标号
let re = new RegExp('%'+i+'\\b', 'g');
source = source.replace(re, '%'+(i - diff));
// 修改 jump label 标号
let re2 = new RegExp('\\b'+i+':', 'g');
source = source.replace(re2, ''+(i - diff)+':');
}
fs.writeFileSync('tmp.ll', source)
}
复制代码
修改事后的 .ll 文件,能够经过如下方式从新生成机器码:
# 生成 arm64 汇编文件
❯ llc ./1.sample.ll
# 调用汇编器从新生成目标文件
❯ xcrun -sdk iphoneos as -arch arm64 ./1.sample.s -o ./1.sample.o
复制代码
这样作有一个缺点,就是生成的目标文件没有内嵌 bitcode,之后再想改就很差改了。
好在 clang driver 功能齐全,能够直接接受 bitcode 以及 IR 汇编文件:
❯ xcrun -sdk iphoneos clang -arch arm64 -target arm64-apple-ios6.0.0 -fembed-bitcode -c ./1.sample.ll -o ./1.sample.o
复制代码
对存在问题的 .o 文件打补丁后,便可将全部的 .o 文件从新合成静态库:
❯ xcrun libtool -static -o ../libsample_arm64_patched.a *.o
复制代码
经过调用堆栈,咱们已经能够知道这个问题的复现方式:
可是在链接调试器的状况下,经过多任务手势杀应用会致使调试器断开,不容易观察是否有崩溃的现象。
因此,须要找到一个让应用正常退出,而又不影响调试器的方法。
经过查询 iOS system framework class dump,能够知道 UIApplication
有一个未公开的方法: UIApplication.terminateWithSuccess()
。
通过实际试验,这个方法确实可使应用直接退出。
所以,咱们能够修改应用代码,在进入可以触发问题的场景下,经过代码来让应用退出,就能够经过调试器来观察应用是否触发崩溃了。分别使用修复前、修复后的库进行实机验证,结果为:
这代表咱们的修复是成功的。
本文经过修改 bitcode,成功地在没有源码的状况下,修复了一个三方库的 bug。其中用到的知识点总结以下:
exit
,多半是因为 C++ 全局变量析构 + 多线程致使的;郭同窗,便利蜂客户端基础框架团队的一名 iOS 工程师,负责移动客户端的基础建设。对跨端技术、App 框架及系统有所研究,专治各类客户端疑难杂症。
[1]Xcode 11 更新日志: developer.apple.com/documentati…
[2]Itanium C++ ABI: itanium-cxx-abi.github.io/cxx-abi/abi…
[3]AArch64 指令集架构文档: developer.arm.com/architectur…
[4]LibEBC: github.com/Guardsquare…
[5]LLVM 官方文档: llvm.org/docs/LangRe…
[6]iOS system framework class dump: developer.limneos.net/?ios=14.4&f…