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

堆棧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可參考博文 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)存。

當程序訪問到中毒內(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)