前言
不積跬步無以至千里乌奇,不積小流無以成江海没讲。學(xué)如逆水行舟,不進(jìn)則退礁苗。我是平平無奇游蕩于各平臺(tái)的搬運(yùn)工爬凑。優(yōu)秀的人已經(jīng)點(diǎn)贊了。本文主要記錄了 iOS 移動(dòng)端的一個(gè)疑難 bug 的排查過程试伙,以及介紹通過給 bitcode 打補(bǔ)丁重新生成機(jī)器碼嘁信,為有問題的第三方庫修復(fù) bug 的方法。廢話不多說疏叨,直接給大家上干貨潘靖,希望能對(duì)你有所幫助,
主要涉及到的知識(shí)點(diǎn)如下:
- ARM 匯編
- C++ 運(yùn)行時(shí)
- 靜態(tài)庫文件的結(jié)構(gòu)
- bitcode 及 LLVM IR
\
平臺(tái)監(jiān)控找崩潰
通過內(nèi)部的崩潰監(jiān)控發(fā)現(xiàn)蚤蔓,有一個(gè)內(nèi)部 App卦溢,近期出現(xiàn)了較多的崩潰現(xiàn)象。其中數(shù)量占比最多的崩潰,其崩潰線程捕獲到的調(diào)用棧如下:
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
\
調(diào)用現(xiàn)場(chǎng)出端倪
這個(gè)調(diào)用棧并沒有提供什么有效的信息既绕,只能看出來是 bugly 框架已經(jīng)檢測(cè)到了崩潰創(chuàng)建了新的 dispatch queue 并終止進(jìn)程,也就是說涮坐,其實(shí)有效的崩潰信息被 bugly 給吃掉了凄贩。
看一下其他線程,是否有可用的信息袱讹,一般可以在其他線程的調(diào)用棧上搜索以下內(nèi)容:
-
_ZSt9terminateEv
: C++ 的終端異常處理(std::terminate(void)
) -
__sigtramp
: 信號(hào)中斷處理例程入口
終于搜索到了以下內(nèi)容:
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
內(nèi)部應(yīng)用同時(shí)集成了 Bugly 和自有的崩潰捕獲疲扎,通常情況下 Bugly 會(huì)在自己捕獲完成后,將崩潰現(xiàn)場(chǎng)轉(zhuǎn)交給其他框架捷雕,使兩次捕獲的崩潰現(xiàn)場(chǎng)相同椒丧。而這個(gè)崩潰則不然生均, Bugly 捕獲了崩潰后肥荔,直接調(diào)用 abort
結(jié)束了應(yīng)用肆资,導(dǎo)致自有崩潰只捕獲到了 SIGABRT
凌简。
通過檢查主線程調(diào)用棧出牧,發(fā)現(xiàn)了一些不同:
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
可以看到調(diào)用棧中有 exit
约郁,這表明應(yīng)用正在正常退出蜡饵。
調(diào)用棧中烂瘫,在 exit
這一項(xiàng)的上面精盅,可以看到 __cxa_finalize_ranges
帽哑,這是由 C++ 代碼產(chǎn)生的調(diào)用,通過 __cxa_atexit
注冊(cè)回調(diào)叹俏,在應(yīng)用退出時(shí)調(diào)用妻枕,用來正在進(jìn)行全局變量的銷毀。
由此可以看出粘驰,這是一個(gè)由于 C++ 全局變量在應(yīng)用退出時(shí)銷毀屡谐,導(dǎo)致其他線程引用到了銷毀的資源產(chǎn)生的崩潰。這也可以解釋為什么 Bugly 沒有把崩潰現(xiàn)場(chǎng)交給其他框架進(jìn)行處理了: Bugly 檢測(cè)到了應(yīng)用正在退出蝌数,直接調(diào)用了 executeEmergencyLogic:
方法康嘉,優(yōu)先保證自己的處理。
\
全局變量會(huì)析構(gòu)
__cxa_atexit
是 Itanium C++ ABI 運(yùn)行時(shí)規(guī)范的一部分籽前,它用來支持 C++ 語法中的全局變量亭珍。我們知道,C++ 對(duì)象分為 POD 和非 POD 兩類枝哄,其中 POD 以及 constexpr 構(gòu)造器可以在編譯期初始化肄梨,而非 constexpr 構(gòu)造的類型只能通過構(gòu)造器在運(yùn)行期間來構(gòu)造。C++ 對(duì)于這兩類對(duì)象挠锥,都支持它們作為全局變量并提供初始值众羡,那么這些全局變量就要在 dso 加載期間調(diào)用構(gòu)造器來初始化。
同樣地蓖租,為了防止內(nèi)存/資源泄漏粱侣,C++ 規(guī)定這樣初始化的全局變量要在 dso 卸載時(shí)析構(gòu)羊壹。
我們可以通過查看反匯編,對(duì)它的實(shí)現(xiàn)機(jī)制一探究竟齐婴。
舉個(gè)例子油猫,有如下 C++ 代碼:
#include <iostream>
class Test {
public:
virtual ~Test();
Test();
};
Test::Test() {}
Test::~Test() {
std::cout << "Test: dtor" << std::endl;
}
static Test t = Test();
使用 clang++ 對(duì)以上文件進(jìn)行編譯,查看生成的匯編代碼:
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 ; ①
從反匯編代碼中柠偶,可以看出實(shí)際的方案是:
- 生成一個(gè)函數(shù)用來構(gòu)造當(dāng)前編譯單元內(nèi)的所有全局(以及靜態(tài))變量情妖,將該函數(shù)寫到
__mod_init_funcs
中去,這樣 dso 加載時(shí)诱担,動(dòng)態(tài)鏈接器會(huì)主動(dòng)執(zhí)行它們(位于匯編代碼①處)毡证; - 在這個(gè)生成的函數(shù)中,調(diào)用
__cxa_atexit
蔫仙,傳入已經(jīng)構(gòu)造的對(duì)象的指針和 deleting destructor料睛,這樣 dso 卸載時(shí),這些構(gòu)造的對(duì)象會(huì)被銷毀(位于匯編代碼②處)摇邦。
很多使用 C++ 的庫都會(huì)分配線程資源進(jìn)行并發(fā)執(zhí)行秦效。如果這些正在執(zhí)行的線程需要引用全局變量,同時(shí)觸發(fā)了 dso 卸載涎嚼,那么就會(huì)發(fā)生線程跑著跑著阱州,全局變量析構(gòu)了,于是進(jìn)程就崩潰了法梯。
理論上講苔货,dso 的卸載是可控的,因?yàn)槲覀兛偪梢钥刂七壿嬃⒀疲寗?dòng)態(tài)庫的資源都釋放掉以后夜惭,再去卸載動(dòng)態(tài)庫/退出進(jìn)程。
但是在 iOS 移動(dòng)應(yīng)用上铛绰,有一個(gè)例外——
用戶可以通過多任務(wù)手勢(shì)诈茧,殺死應(yīng)用。如果被殺的應(yīng)用恰巧在前臺(tái)運(yùn)行捂掰,那么 iOS 會(huì)給這個(gè)應(yīng)用發(fā)送 SIGTERM
信號(hào)敢会。UIKit 收到信號(hào)后會(huì)調(diào)用應(yīng)用代理的 applicationWillTerminate(_:)
方法,使得應(yīng)用有機(jī)會(huì)保存一些狀態(tài)數(shù)據(jù)这嚣,然后正常退出應(yīng)用鸥昏。
這個(gè)時(shí)候我們是沒有機(jī)會(huì)釋放線程資源的,因?yàn)?terminate 的生命周期很短姐帚,沒有時(shí)間給我們等待異步線程結(jié)束吏垮,所以這個(gè)崩潰就無法避免了。
所幸的是,這種崩潰并不會(huì)被用戶感知到:即使應(yīng)用不崩潰膳汪,也會(huì)立即正常退出唯蝶,對(duì)于用戶來說表現(xiàn)是一樣的。
\
解決需要重編譯遗嗽?
其實(shí)同類的問題以前在該 App 中也是發(fā)生過的——我們有一個(gè)內(nèi)部 SDK 同樣也是 C++ 寫成粘我,擁有全局狀態(tài)變量,開啟異步線程池訪問這些變量媳谁,用戶在前臺(tái)殺死應(yīng)用時(shí)觸發(fā)崩潰涂滴。
當(dāng)時(shí)的解決方案是:升級(jí)工具鏈友酱。根據(jù) Apple 發(fā)布的 Xcode 11 更新日志晴音,apple clang++ 編譯器增加了禁用全局變量析構(gòu)的編譯參數(shù) -fno-c++-static-destructors
。使用該標(biāo)記編譯的 C++ 源文件缔杉,不會(huì)生成對(duì)全局變量進(jìn)行析構(gòu)的代碼锤躁。
這對(duì) iOS 應(yīng)用來說是安全的——因?yàn)?iOS 應(yīng)用幾乎不會(huì)在運(yùn)行時(shí)卸載動(dòng)態(tài)庫,無需考慮動(dòng)態(tài)庫卸載的資源泄漏問題或详。
然而這次的問題又有所不同——出現(xiàn)問題的是一個(gè)由第三方提供的二進(jìn)制庫系羞,我們手里是沒有它的源代碼的,也就無法通過修改編譯參數(shù)的方式來重新編譯生成機(jī)器碼霸琴。
但是我們能否再深入一下椒振,幫助三方庫來修復(fù)這個(gè) bug 呢?
修復(fù)該問題的直接方案梧乘,就是修改機(jī)器碼澎迎,消除對(duì) __cxa_atexit
的調(diào)用。
\
靜態(tài)庫里有什么
一個(gè)三方靜態(tài)庫 SDK选调,一般由以下文件組成:
- 一組頭文件夹供,提供了公開的函數(shù)/OC 類及方法聲明;
- 一個(gè) .a 靜態(tài)庫仁堪,包含了這個(gè)庫的代碼實(shí)現(xiàn)哮洽,由多個(gè)編譯單元生成的 .o 目標(biāo)文件打包而成;
- 一組資源文件弦聂,提供代碼運(yùn)行時(shí)的外部數(shù)據(jù)(圖片鸟辅、以及其他資源)。
無論是采用零散的文件莺葫,還是采用 .framework
封裝剔桨,它們的組成基本上是一致的。
我們要修改的是它的部分機(jī)器碼徙融,所以要將其中的 .a 靜態(tài)庫解開洒缀,再進(jìn)行編輯。
首先來查看一下 .a 文件的內(nèi)容:
? lipo -info libsample.a
Architectures in the fat file: libsample.a are: armv7 arm64
這是一個(gè) Universal binary,包含了兩種 iOS 真機(jī)的 CPU 架構(gòu)的代碼树绩。我們先針對(duì)主流機(jī)型使用的 arm64 架構(gòu)嘗試調(diào)整萨脑。
使用 lipo
命令將 arm64 架構(gòu)單獨(dú)抽取出來:
? lipo -thin arm64 libsample.a -o libsample_arm64.a
只有把 Universal binary 中特定的架構(gòu)抽取出來,才能使用 ar(1) 操作:
? mkdir objects
? cd objects
# 打印 .a 中包含的文件列表
? ar t ../libsample_arm64.a
__.SYMDEF
sample.o
sample.o
# 解包 .a 文件
? ar -x ../libsample_arm64.a
使用上述命令進(jìn)行 .a 文件的展開后饺饭,出現(xiàn)了一個(gè)問題: ar t
命令中渤早,列出了兩個(gè) sample.o
文件,但是 ar x
命令只解出來了一個(gè)瘫俊。這是因?yàn)?ar 歸檔中鹊杖,沒有目錄的概念,不同目錄下的同名目標(biāo)文件扛芽,在 ar 歸檔的過程中骂蓖,會(huì)被打平,導(dǎo)致 ar 歸檔中包含多個(gè)同名文件川尖。
這會(huì)導(dǎo)致我們使用 ar x
解包的時(shí)候登下,相同的文件會(huì)被覆蓋成一個(gè),也沒法把它們單獨(dú)解壓出來叮喳。
那么如何才能把 ar 歸檔中的同名文件分別解包出來呢……那么就得提到「不務(wù)正業(yè)」的 7-zip 了……
\
壓縮軟件有妙用
7-zip 作為一個(gè)壓縮軟件被芳,除了支持常規(guī)的壓縮文件格式之外,還支持了很多歸檔文件以及 PE 可執(zhí)行文件(特別地馍悟,支持了部分安裝器的 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 自動(dòng)為 .a 中的文件名進(jìn)行了修正锣咒。同時(shí)侵状,7-zip 在解壓的時(shí)候遇到同名文件,會(huì)提供是否覆蓋及自動(dòng)重命名文件的選項(xiàng):
? 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 就會(huì)自動(dòng)幫我們處理文件重名的問題了壹将。而我們重新打包 .a 文件時(shí),.o 文件的名稱并不重要毛嫉,可以隨便取诽俯,所以這里改成其他名字也沒有關(guān)系。
\
人肉寫出機(jī)器碼
我們?cè)賮砘仡櫼幌碌湫偷娜肿兞课鰳?gòu)調(diào)用的注冊(cè):
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
的函數(shù)簽名如下:
// 3.3.6.3 Runtime API
extern _LIBCXXABI_FUNC_VIS int __cxa_atexit(void (*f)(void *), void *p, void *d);
對(duì)比反匯編代碼暴区,可以看到 X0 傳入了對(duì)象類型的刪除析構(gòu)( ...D1Ev
)函數(shù)的指針,X1 傳入了對(duì)象地址辛臊,X2 傳入了 dso 句柄仙粱,與函數(shù)簽名相符。
要消除對(duì) __cxa_atexit
的調(diào)用彻舰,只需要把其中的 bl
指令改成 nop
即可伐割。
反匯編軟件 IDA 提供了即時(shí)匯編的功能候味,可以通過手寫匯編指令,由 IDA 生成機(jī)器碼直接寫入文件中隔心“兹海可惜這個(gè)功能對(duì)于 arm64 架構(gòu)沒有支持,我們需要找另外的方法硬霍。
好在我們可以查閱 AArch64 指令集架構(gòu)文檔帜慢,其中提到:
通過文檔,我們看到了在 AArch64 架構(gòu)下唯卖, NOP
指令的具體編碼粱玲。
由于 Apple arm64 CPU 是小端序,那么我們應(yīng)該把 bl
指令對(duì)應(yīng)的四個(gè)字節(jié)替換為:
1F 20 03 D5 ; NOP
除此之外拜轨,還有數(shù)種不同的情況抽减,需要針對(duì)性地做不同的修改。以下列出了兩種不同情況撩轰。
尾調(diào)用:
; 各種填寫參數(shù)...
B ___cxa_atexit
; end of function
此時(shí)要把 B
指令改為 RET lr
胯甩。
返回值校驗(yàn):
; 各種填寫參數(shù)...
BL ___cxa_atexit
CBZ W0, check_pass
BL assert_fail
check_pass:
; ... 正常邏輯
此時(shí)要把 B
指令改為 MOV w0, wzr
昧廷,才能通過校驗(yàn)堪嫂。
至此我們可以看出,通過這種方式修改機(jī)器碼木柬,存在很大的局限性:
- 做人肉匯編器真的很難皆串;
- 對(duì)象文件中的跳轉(zhuǎn)記錄在 GOT 表中,直接刪除它們的引用會(huì)導(dǎo)致鏈接失斆颊怼恶复;
- 不是所有 CPU 架構(gòu)上都存在可以等長替換的指令,因此對(duì)于部分 CISC 指令集架構(gòu)無能為力速挑。
那么是否存在更好的解決方案呢谤牡?
\
蘋果又有新科技
在使用 otool
檢查解包出來的 .o 文件時(shí),發(fā)現(xiàn)了如下區(qū)段:
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
這意味著姥宝,這個(gè)目標(biāo)文件內(nèi)嵌了 bitcode翅萤。眾所周知,Clang/LLVM 是蘋果親兒子腊满,蘋果基于這一套體系搞出了許多新鮮玩意兒套么,bitcode 就是其中之一。
clang 編譯器會(huì)先將源文件編譯為 LLVM IR碳蛋,再把 IR 編譯到機(jī)器碼胚泌。IR 的大部分設(shè)計(jì)都是平臺(tái)中立的,少部分平臺(tái)相關(guān)的代碼在 CPU 架構(gòu)不發(fā)生大變化時(shí)基本兼容肃弟,而且從 IR 生成機(jī)器碼的過程可以單獨(dú)優(yōu)化玷室。
LLVM IR 有不同的表示方案零蓉,有文本形式的 IR 匯編、二進(jìn)制編碼的 bitcode穷缤。
Apple 允許應(yīng)用在編譯時(shí)將 bitcode 內(nèi)嵌在二進(jìn)制文件內(nèi)壁公,隨應(yīng)用一起提交給 Apple。一旦 Apple 推出了效率更高的機(jī)器碼生成方案绅项,或者是推出了新款 CPU紊册,Apple 可以根據(jù)你提交的 bitcode 重新生成更高效的機(jī)器指令,開發(fā)者無需做任何事即可享受到這個(gè)優(yōu)化快耿。
比如 iPhone X 的 CPU 架構(gòu)有小升級(jí)囊陡,內(nèi)嵌了 bitcode 的應(yīng)用就可以免費(fèi)獲得 arm64e CPU 架構(gòu)的支持。
使用開源項(xiàng)目 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 項(xiàng)目(需要通過 Homebrew 安裝 llvm)提供的 llvm-dis
工具可以將 bc 文件轉(zhuǎn)換為可讀的 IR 匯編格式:
? llvm-dis ./1.sample.bc
這會(huì)生成一個(gè)同名的 .ll 文件掀亥,可以用文本編輯器打開撞反。其中關(guān)于全局變量初始化的部分如下:
; 省略無關(guān)代碼
; 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 的詳細(xì)語法在此就不展開介紹了,有興趣的同學(xué)可以查看LLVM 官方文檔搪花。其中比較重要的有:
-
declare
用來聲明對(duì)外部符號(hào)的引用遏片,例如此處引用了外部函數(shù)__cxa_atexit
。 -
call
用來做函數(shù)調(diào)用
需要注意的是撮竿,在 IR 中吮便,所有 %
加數(shù)字組成的標(biāo)號(hào)必須連續(xù)。例如如果我注釋了上述代碼中的 %1
所在的一行幢踏,就會(huì)產(chǎn)生 IR 匯編錯(cuò)誤髓需,此時(shí)就必須把下一行的 %2
改成 %1
,才能符合規(guī)則匯編通過房蝉。
在上述代碼中僚匆,我們只需要把 %2
所在的一行給注釋掉,即可完成修復(fù)搭幻。如果一個(gè) IR 函數(shù)內(nèi)有多個(gè)調(diào)用咧擂,就需要按照標(biāo)號(hào)連續(xù)的規(guī)則,將注釋掉的代碼后面的所有標(biāo)號(hào)依次提前了檀蹋。
正確的 IR 操作姿勢(shì)是寫一個(gè) IR pass松申,然后通過 llvm-opt 去加載這個(gè) pass,讀取 .bc 文件而不是人類可讀的 .ll 文件续扔,來對(duì)原有的 bitcode 做變換攻臀。但是寫一個(gè) pass 需要的成本比臨時(shí)修復(fù)問題要高得多,對(duì)于少數(shù)幾個(gè)目標(biāo)文件的修復(fù)纱昧,可以通過文本替換工具或腳本語言來替換標(biāo)號(hào)刨啸。例如使用 node.js:
function replaceLabels(from, to, diff) {
let source = fs.readFileSync('tmp.ll', 'utf8');
for (let i = from; i <= to; ++i) {
// 修改 % 變量標(biāo)號(hào)
let re = new RegExp('%'+i+'\b', 'g');
source = source.replace(re, '%'+(i - diff));
// 修改 jump label 標(biāo)號(hào)
let re2 = new RegExp('\b'+i+':', 'g');
source = source.replace(re2, ''+(i - diff)+':');
}
fs.writeFileSync('tmp.ll', source)
}
\
重新組裝靜態(tài)庫
修改過后的 .ll 文件,可以通過以下方式重新生成機(jī)器碼:
# 生成 arm64 匯編文件
? llc ./1.sample.ll
# 調(diào)用匯編器重新生成目標(biāo)文件
? xcrun -sdk iphoneos as -arch arm64 ./1.sample.s -o ./1.sample.o
這樣做有一個(gè)缺點(diǎn)识脆,就是生成的目標(biāo)文件沒有內(nèi)嵌 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
對(duì)存在問題的 .o 文件打補(bǔ)丁后离例,即可將所有的 .o 文件重新合成靜態(tài)庫:
? xcrun libtool -static -o ../libsample_arm64_patched.a *.o
\
實(shí)機(jī)驗(yàn)證大成功
通過調(diào)用堆棧换团,我們已經(jīng)可以知道這個(gè)問題的復(fù)現(xiàn)方式:
- 在應(yīng)用中進(jìn)入使用該三方庫內(nèi)部觸發(fā)多線程工作的場(chǎng)景
- 直接開啟多任務(wù)手勢(shì),殺死應(yīng)用
但是在連接調(diào)試器的情況下宫蛆,通過多任務(wù)手勢(shì)殺應(yīng)用會(huì)導(dǎo)致調(diào)試器斷開艘包,不容易觀察是否有崩潰的現(xiàn)象。
所以耀盗,需要找到一個(gè)讓應(yīng)用正常退出想虎,而又不影響調(diào)試器的方法。
通過查詢 iOS system framework class dump叛拷,可以知道 UIApplication
有一個(gè)未公開的方法: UIApplication.terminateWithSuccess()
舌厨。
經(jīng)過實(shí)際試驗(yàn),這個(gè)方法確實(shí)可以使應(yīng)用直接退出忿薇。
因此裙椭,我們可以修改應(yīng)用代碼,在進(jìn)入能夠觸發(fā)問題的場(chǎng)景下署浩,通過代碼來讓應(yīng)用退出揉燃,就可以通過調(diào)試器來觀察應(yīng)用是否觸發(fā)崩潰了。分別使用修復(fù)前瑰抵、修復(fù)后的庫進(jìn)行實(shí)機(jī)驗(yàn)證你雌,結(jié)果為:
- 使用舊版庫時(shí)器联,有概率引發(fā)調(diào)試器由于崩潰觸發(fā)斷點(diǎn)二汛;
- 使用修改機(jī)器碼的庫后,不會(huì)觸發(fā)崩潰拨拓;
- 不影響正常的業(yè)務(wù)功能肴颊。
這表明我們的修復(fù)是成功的。
總結(jié)
本文通過修改 bitcode渣磷,成功地在沒有源碼的情況下婿着,修復(fù)了一個(gè)三方庫的 bug。其中用到的知識(shí)點(diǎn)總結(jié)如下:
- 崩潰現(xiàn)場(chǎng)中醋界,在主線程發(fā)現(xiàn)
exit
竟宋,多半是由于 C++ 全局變量析構(gòu) + 多線程導(dǎo)致的; - 在有源碼的情況下形纺,可以通過調(diào)整編譯參數(shù)消除全局變量析構(gòu)丘侠;
- 使用 7-zip 可以無損解包靜態(tài)庫文件;
- 使用 otool 可以看到目標(biāo)文件是否嵌入了 bitcode逐样;
- 使用 llvm 提供的工具蜗字,可以對(duì) bitcode 進(jìn)行修改打肝、重新生成機(jī)器碼;
- 可以通過私有 API 來模擬應(yīng)用退出挪捕,制造復(fù)現(xiàn)場(chǎng)景粗梭。
作者
郭同學(xué),便利蜂客戶端基礎(chǔ)框架團(tuán)隊(duì)的一名 iOS 工程師级零,負(fù)責(zé)移動(dòng)客戶端的基礎(chǔ)建設(shè)断医。對(duì)跨端技術(shù)、App 框架及系統(tǒng)有所研究奏纪,專治各種客戶端疑難雜癥孩锡。
推薦觀看:Fluuter手把手教你從入門到精通
參考資料
[1]Xcode 11 更新日志: https://developer.apple.com/documentation/xcode-release-notes/xcode-11-release-notes
[2]Itanium C++ ABI: https://itanium-cxx-abi.github.io/cxx-abi/abi.html#dso-dtor-runtime-api
[3]AArch64 指令集架構(gòu)文檔: https://developer.arm.com/architectures/cpu-architecture/a-profile/exploration-tools
[4]LibEBC: https://github.com/Guardsquare/LibEBC
[5]LLVM 官方文檔: https://llvm.org/docs/LangRef.html
[6]iOS system framework class dump: https://developer.limneos.net/?ios=14.4&framework=UIKitCore.framework&header=UIApplication.h
搬運(yùn)自知乎,如有侵犯亥贸,請(qǐng)聯(lián)系小編刪除哦躬窜。