在 iOS 客戶端上經過 bitcode 爲第三方庫修復 bug

摘要

本文主要記錄了 iOS 移動端的一個疑難 bug 的排查過程,以及介紹經過給 bitcode 打補丁從新生成機器碼,爲有問題的第三方庫修復 bug 的方法。html

主要涉及到的知識點以下:前端

  • ARM 彙編
  • C++ 運行時
  • 靜態庫文件的結構
  • bitcode 及 LLVM IR

平臺監控找崩潰

經過內部的崩潰監控發現,有一個內部 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++

  1. _ZSt9terminateEv: C++ 的終端異常處理( std::terminate(void)
  2. __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 結束了應用,致使自有崩潰只捕獲到了 SIGABRTgithub

經過檢查主線程調用棧,發現了一些不一樣:算法

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,通常由如下文件組成:

  • 一組頭文件,提供了公開的函數/OC 類及方法聲明;
  • 一個 .a 靜態庫,包含了這個庫的代碼實現,由多個編譯單元生成的 .o 目標文件打包而成;
  • 一組資源文件,提供代碼運行時的外部數據(圖片、以及其餘資源)。

不管是採用零散的文件,仍是採用 .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 指令集架構文檔,其中提到:

nop.png

經過文檔,咱們看到了在 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 ,才能經過校驗。

至此咱們能夠看出,經過這種方式修改機器碼,存在很大的侷限性:

  • 作人肉彙編器真的很難;
  • 對象文件中的跳轉記錄在 GOT 表中,直接刪除它們的引用會致使連接失敗;
  • 不是全部 CPU 架構上都存在能夠等長替換的指令,所以對於部分 CISC 指令集架構無能爲力。

那麼是否存在更好的解決方案呢?

蘋果又有新科技

在使用 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。其中用到的知識點總結以下:

  1. 崩潰現場中,在主線程發現 exit ,多半是因爲 C++ 全局變量析構 + 多線程致使的;
  2. 在有源碼的狀況下,能夠經過調整編譯參數消除全局變量析構;
  3. 使用 7-zip 能夠無損解包靜態庫文件;
  4. 使用 otool 能夠看到目標文件是否嵌入了 bitcode;
  5. 使用 llvm 提供的工具,能夠對 bitcode 進行修改、從新生成機器碼;
  6. 能夠經過私有 API 來模擬應用退出,製造復現場景。

做者

郭同窗,便利蜂客戶端基礎框架團隊的一名 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…

產研招聘

  • Java 架構師/高級工程師、前端高級工程師和測試開發工程師、數據算法等崗位;
  • 感興趣的同窗歡迎聯繫咱們!
  • tech-hiring@bianlifeng.com

招聘官網

相關文章
相關標籤/搜索