C靜態(tài)庫符號沖突,引起的iOS隨機崩潰問題

崩潰現(xiàn)象

iOS工程引入三方SDK后(GDTMobSDK-iOS V4.14.0),運行App,會產(chǎn)生隨機崩潰,最終指向malloc相關的線索。

堆棧1:


free.jpg

堆棧2:

    frame #0: 0x00000001b07b0af8 libsystem_malloc.dylib`malloc_error_break
    frame #1: 0x00000001b07bf6e4 libsystem_malloc.dylib`malloc_vreport + 440
    frame #2: 0x00000001b07bf998 libsystem_malloc.dylib`malloc_zone_error + 104
    frame #3: 0x00000001b07a3774 libsystem_malloc.dylib`nanov2_allocate_from_block$VARIANT$mp + 540
    frame #4: 0x00000001b07a29d0 libsystem_malloc.dylib`nanov2_allocate$VARIANT$mp + 140
    frame #5: 0x00000001b07a28f4 libsystem_malloc.dylib`nanov2_malloc$VARIANT$mp + 60
    frame #6: 0x00000001b07b19a0 libsystem_malloc.dylib`malloc_zone_malloc + 156
    frame #7: 0x00000001b07b23b0 libsystem_malloc.dylib`malloc + 32
    frame #8: 0x00000001b06ae428 libsystem_c.dylib`_vasprintf + 144
    frame #9: 0x00000001b06a6d7c libsystem_c.dylib`asprintf + 72
    frame #10: 0x00000001b0aefabc CoreFoundation`-[NSObject(NSObject) __dealloc_zombie] + 72

內(nèi)存分析

1、按照Xcode打印的提示,設置符號斷點 malloc_error_break,調(diào)試無果

2、開啟僵尸對象,無果

3、開啟 Address Sanitizer,基本鎖定問題

Address Sanitizer.png

關于 Address Sanitizer可參考博文 https://blog.csdn.net/u014600626/article/details/119506854

Address Sanitizer的原理是當程序創(chuàng)建變量分配一段內(nèi)存時,將此內(nèi)存后面的一段內(nèi)存也凍結(jié)住,標識為中毒內(nèi)存。如圖所示,黃色是變量所占內(nèi)存,紫色是凍結(jié)的中毒內(nèi)存。

error_memory.png.jpeg

當程序訪問到中毒內(nèi)存時(越界訪問),就會拋出異常,并打印出相應log信息。調(diào)試者可以根據(jù)中斷位置和的log信息,識別bug。如果變量釋放了,變量所占的內(nèi)存也會標識為中毒內(nèi)存,這時候訪問這段內(nèi)存同樣會拋出異常(訪問已經(jīng)釋放的對象)

再次崩潰時,Xcode捕獲到靜態(tài)庫c代碼中的崩潰:

    thread #66, stop reason = Heap buffer overflow
    frame #0: 0x000000011bcd26f4 libclang_rt.asan_ios_dynamic.dylib`__asan::AsanDie()
    frame #1: 0x000000011bcea1fc libclang_rt.asan_ios_dynamic.dylib`__sanitizer::Die() + 192
    frame #2: 0x000000011bcd0580 libclang_rt.asan_ios_dynamic.dylib`__asan::ScopedInErrorReport::~ScopedInErrorReport() + 1124
    frame #3: 0x000000011bccf858 libclang_rt.asan_ios_dynamic.dylib`__asan::ReportGenericError(unsigned long, unsigned long, unsigned long, unsigned long, bool, unsigned long, unsigned int, bool) + 1436
    frame #4: 0x000000011bca1a30 libclang_rt.asan_ios_dynamic.dylib`wrap_memcpy + 616
    frame #5: 0x00000001080bfb14 xxx`SHA1Update + 276
    frame #6: 0x00000001080bfbfc xxx`SHA1Final + 204
    frame #7: 0x000000010aa86530 xxx`xxx::xxx::CSha1::final(unsigned char*, unsigned long) + 184
    frame #8: 0x000000010aa6fea4 xxx`xxx::xxx::wsse_calc_digest(char*, char*, char*, char*) + 156
    frame #9: 0x000000010aa6fd90 xxx`xxx::xxx::wsse_make_digest(xxx::xxx::HTTP_WSSE*, char*) + 112
    frame #10: 0x000000010aa22568 xxx`xxx::xxx::GenerateRequest(xxx::xxx::HttpReqPars&, xxx::xxx::HTTP_REC&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >&, int) + 36
    frame #11: 0x000000010aa26a64 xxx`xxx::xxx::CDeviceInfo::sendRequest(xxx::xxx::ServerInfo const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, int) + 276
    frame #12: 0x000000010aa275a0 xxx`xxx::xxx::CDeviceInfo::onInit(xxx::xxx::ServerInfo const&, unsigned long long) + 56
    frame #13: 0x000000010aa27440 xxx`xxx::xxx::CDeviceInfo::heartbeat(xxx::xxx::ServerInfo const&, unsigned long long, unsigned long long) + 72
    frame #14: 0x000000010aa28704 xxx`xxx::xxx::CDeviceInfoMgr::heartbeat(unsigned long long) + 184
    frame #15: 0x000000010aa314e4 xxx`xxx::xxx::CMultiStunClient::threadProc() + 72
    frame #16: 0x000000010aa87ef8 xxx`(anonymous namespace)::InternalThreadBody(void*) + 272

堆棧說明SHA1Update存在內(nèi)存分配問題,懷疑是新引入的庫,和工程原有的庫存在函數(shù)沖突,導致App鏈接出錯。

即三方SDK(.a文件)與工程原有的SDK,都為靜態(tài)庫,且均包含了SHA1Update函數(shù),但App卻鏈接了三方SDK中的函數(shù),導致原SDK在使用時產(chǎn)生崩潰。

4、符號文件導出

使用export_symbols.sh腳本導出兩個靜態(tài)庫的符號文件

filename="${1%%.*}" #刪除第一個.,以及右邊的字符串,得到文件名
nm -n $1 > ${filename}_symbols.txt 

5、比較符號文件

通過腳本(見附錄-過濾重復符號)對比兩個庫的符號文件,過濾出符號類型為't'或'T'且名稱一致的符號:

PS:常見符號類型

  • A 該符號的值在今后的鏈接中將不再改變;
  • B 該符號放在BSS段中,通常是那些未初始化的全局變量;
  • D 該符號放在普通的數(shù)據(jù)段中,通常是那些已經(jīng)初始化的全局變量;
  • T 該符號放在代碼段中,通常是那些全局非靜態(tài)函數(shù);
  • U 該符號未定義過,需要自其他對象文件中鏈接進來;
  • W 未明確指定的弱鏈接符號;同鏈接的其他對象文件中有它的定義就用上,否則就用一個系統(tǒng)特別指定的默認值。
___clang_at_available_requires_core_foundation_framework
___clang_at_available_requires_core_foundation_framework.cold.1
_SHA1Init
_SHA1Update
_SHA1Final

過濾_SHA1Init、SHA1Update、_SHA1Final三個符號,找到廠商符號文件libGDTMobSDK_symbols.txt對應的重復信息:

                U ___assert_rtn
                U ___stack_chk_fail
                U ___stack_chk_guard
                U _memcpy
0000000000000000 T _SHA1Transform
0000000000000004 C _x
0000000000000004 C _x.10
0000000000000004 C _x.12
0000000000000004 C _x.14
0000000000000004 C _x.16
0000000000000004 C _y
0000000000000004 C _y.11
0000000000000004 C _y.13
0000000000000004 C _y.15
0000000000000004 C _y.17
0000000000002373 T _SHA1Init
000000000000239c T _SHA1Update
0000000000002533 T _SHA1Final 

找到工程SDK符號文件對應的重復信息:

                U ___stack_chk_fail
                U ___stack_chk_guard
                U _memcpy
0000000000000000 T _SHA1Init
0000000000000032 T _SHA1Update
0000000000000b8c T _SHA1Final
0000000000000c80 s _padding

可以看到兩個庫中都實現(xiàn)了_SHA1Init、_SHA1Update、_SHA1Final三個c函數(shù)。

解決方法

解決兩個靜態(tài)庫C函數(shù)沖突,即可修復鏈接問題,幾種不同途徑的修復方法:

  • 函數(shù)名增加庫標識,代價小,推薦

  • 增加命名空間,代價小,推薦

  • 修改為動態(tài)庫,代價大,不推薦

經(jīng)過協(xié)商,最終由廠商修復該問題,提供了新的版本V4.14.12。

pod 'GDTMobSDK', '~> 4.14.12'

再次導出libGDTMobSDK.a符號文件,查看GdtSha1.o對應的符號:

libGDTMobSDK.a(GdtSha1.o):
                U ___assert_rtn
                U ___stack_chk_fail
                U ___stack_chk_guard
                U _memcpy
00000000000023a3 T _GDT_SHA1Init
00000000000023cc T _GDT_SHA1Update
00000000000025bc T _GDT_SHA1Final
00000000000027d9 T _gdt_sha1data
0000000000002850 T _tencent8913930322152434853208

可以看到,廠商在_SAH1Init前增加了前綴 GDT。

附錄

導出符號 export_symbols.sh

參數(shù)傳入二進制文件路徑

filename="${1%%.*}" #刪除第一個.,以及右邊的字符串,得到文件名
nm -n $1 > ${filename}_symbols.txt 

過濾重復符號 process.py

def filter_c_symbols(read_lines: [str]) -> {}:
    """
    過濾符號文件,輸出至map中
    :param read_lines: 原始的符號
    :return: [:]
    """
    map_lines = {}
    for line in read_lines:
        separators = line.split(" ")
        if (line.__contains__(' t ') or line.__contains__(' T ')) & len(separators) > 0:
            last = separators[len(separators) - 1]
            if not last.__contains__(']'): # 過濾OC符號
                # print(line)
                last = last.replace('\n', '')
                map_lines[last] = line
    return map_lines



if __name__ == '__main__':
    # 讀取mobsdk
    file = open(r'libGDTMobSDK_symbol.txt')
    map_mob = filter_c_symbols(file.readlines())

    # 讀取lcsdk
    file = open(r'LCSDK_symbols.txt')
    map_lcsdk = filter_c_symbols(file.readlines())

    # 遍歷mobsdk,比較lcsdk中是否有重復的符號,并寫入
    repeat_symbols = []
    for key in map_mob:
        if map_lcsdk.get(key):
            repeat_symbols.append(key)
            print(map_mob[key])


    # print(repeat_symbols)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容