我只是代碼的搬運(yùn)工
crash文件獲取方式
- 應(yīng)用集成第三方的crash SDK,自動(dòng)采集相關(guān)運(yùn)行堆棧,發(fā)送到服務(wù)器上,開發(fā)者上傳dSYM文件進(jìn)行解析,得到符號(hào)化的堆棧信息。
- 某設(shè)備上的crash可以通過(guò)Xcode導(dǎo)出crash文件查看崩潰日志,Xcode -> window -> Devices and Simulators -> 選擇設(shè)備 -View Device Logs。
- 自己應(yīng)用加入中收集異常代碼
void uncaughtExceptionHandler(NSException *exception){
NSLog(@"CRASH: %@", exception);
NSLog(@"name %@",[exception name]);
NSLog(@"userInfo %@",[exception userInfo]);
NSLog(@"reason %@",[exception reason]);
NSLog(@"callStackReturnAddresses %@",[exception callStackReturnAddresses]);
NSLog(@"Stack Trace: %@",[exception callStackSymbols]);
// Internal error reporting
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
...
return YES;
}
一、iOS crash日志格式
iOS的crash報(bào)告日志可以分為頭部(Header)、異常信息(Exception Information)、診斷信息(Additional Diagnostic Information)、線程堆棧(Backtraces)、線程狀態(tài)(Thread State)、庫(kù)信息(Binary Images)這個(gè)六個(gè)部分。

頭部(Header):硬件型號(hào),系統(tǒng)版本,進(jìn)程名稱、id,bundleid,崩潰時(shí)間,crash日志報(bào)告格式版本號(hào)等信息。
- Incident Identifier: 事件標(biāo)識(shí)符,每個(gè)crash文件對(duì)應(yīng)一個(gè)唯一的標(biāo)識(shí)符
- CrashReporter Key: 匿名設(shè)備標(biāo)識(shí)符
- Hardware Model: 設(shè)備型號(hào)
- Process: 進(jìn)程名
- Identifier: app Identifier
- Exception Type: 異常類型
- Exception Codes: 異常代碼
- Termination Reason: 進(jìn)程被結(jié)束的原因
異常信息(Exception Information): 崩潰類型、崩潰代碼及觸發(fā)崩潰的線程等信息。
- Exception Codes: 處理器的具體信息有關(guān)的異常編碼成一個(gè)或多個(gè)64位進(jìn)制數(shù)。通常情況下,這個(gè)區(qū)域不會(huì)被呈現(xiàn),因?yàn)閷惓4a解析成人們可以看懂的描述是在其它區(qū)域進(jìn)行的。
- Exception Subtype: 供人們可讀的異常代碼的名字。
- Exception Message: 從異常代碼中提取的額外的可供人們閱讀的信息。
- Exception Note: 不是特定于一個(gè)異常類型的額外信息.如果這個(gè)區(qū)域包含SIMULATED (這不是一個(gè)崩潰)然后進(jìn)程沒有崩潰,但是被watchdog殺掉了。
- Termination Reason: 當(dāng)一個(gè)進(jìn)程被終止的時(shí)的原因。
- Triggered by Thread: 異常所在的線程。
診斷信息(Additional Diagnostic Information): 非常簡(jiǎn)略的診斷信息。不是每個(gè)崩潰都會(huì)有診斷信息。
線程堆棧(Backtraces): 崩潰發(fā)生時(shí),各個(gè)線程的方法調(diào)用棧的詳細(xì)信息。觸發(fā)崩潰的線程會(huì)被標(biāo)記上Crashed。
- 第一列的數(shù)字表示對(duì)應(yīng)線程的調(diào)用棧順序
- 第二列表示對(duì)應(yīng)的鏡像文件名稱,如系統(tǒng)corefoundtion
- 第三列表示當(dāng)前行對(duì)應(yīng)的符號(hào)地址
- 第四列如果已經(jīng)符號(hào)化過(guò)則表示對(duì)應(yīng)的符號(hào),反之為鏡像的起始地址+文件偏移地址,這2個(gè)值相加實(shí)際上就是第三列的地址
線程狀態(tài)(Thread State): 崩潰時(shí)寄存器的狀態(tài)。
庫(kù)信息(Binary Images): 加載的動(dòng)態(tài)庫(kù)信息。
查看crash日志時(shí),首先會(huì)在【異常信息(Exception Information)】中通過(guò)“Triggered by Thread”的字段判斷是哪個(gè)線程發(fā)生了crash。在【線程堆棧(Backtraces)】信息中,會(huì)看到各個(gè)線程號(hào),而崩潰發(fā)生的線程號(hào)下面會(huì)有“Thread xx Crashed”標(biāo)記該線程發(fā)生了crash。
在【線程堆棧(Backtraces)】信息中,有方法編號(hào),方法所屬模塊名,方法地址,方法符號(hào)信息或者方法所在的段地址及偏移量。每個(gè)方法的地址是包含在所屬模塊的地址范圍內(nèi),比如GrowSDKDemo模塊(0x10049400 - 0x100887fff)。

圖中顯示,0號(hào)線程(com.apple.main-thread 即主線程)發(fā)生了crash,地址是0x00000001004b6508,在GrowSDKDemo這個(gè)模塊內(nèi)。在【庫(kù)信息(Binary Images)】信息中可以找到這個(gè)二進(jìn)制模塊,也就是App可執(zhí)行文件GrowSDKDemo,和其他的模塊是第三方動(dòng)態(tài)庫(kù)(YLDataSDK)、系統(tǒng)加載的動(dòng)態(tài)庫(kù)(UIKit、CoreFoundation等)。
二、Exception類型
2.1 Bad Memory Access — EXC_BAD_ACCESS (SIGSEGV、SIGBUS)
當(dāng)進(jìn)程嘗試的去訪問(wèn)一個(gè)不可用或者不允許訪問(wèn)的內(nèi)存空間時(shí),會(huì)發(fā)生野指針異常, Exception Subtype 中包含了錯(cuò)誤的描述及想要訪問(wèn)的地址。
SIGSEGV在ARC以后應(yīng)該很少見到,SIGBUS為總線錯(cuò)誤,與SIGSEGV訪問(wèn)的是無(wú)效地址,而SIGBUS訪問(wèn)的是有效地址,但總線訪問(wèn)異常(如地址對(duì)齊問(wèn)題)
- 如果
obj_msgSend或objc_release符號(hào)信息位于crash線程的最上面,說(shuō)明該進(jìn)程可能嘗試訪問(wèn)已經(jīng)釋放的對(duì)象。開啟僵尸模式進(jìn)行調(diào)試、對(duì)程序做Analyze分析。 - 如果
gpus_ReturnNotPermittedKillClient符號(hào)在crash線程的最上面,說(shuō)明進(jìn)程試圖在后臺(tái)使用OpenGL ES或Metal進(jìn)行渲染,而被殺掉。 - 在debug模式下打開 Address Sanitizer,它會(huì)在編譯的時(shí)候自動(dòng)添加一些關(guān)于內(nèi)存訪問(wèn)的工具,在crash發(fā)生的時(shí)候,Xcode會(huì)給出對(duì)應(yīng)的詳細(xì)信息。
2.2 Abnormal Exit(異常退出) — EXC_CRASH (SIGABRT)
非正常退出,大多數(shù)發(fā)生這種crash的原因是因?yàn)槲床东@的Objective-C/C++異常,導(dǎo)致進(jìn)程調(diào)用 abort() 方法退出。
中止當(dāng)前進(jìn)程,返回一個(gè)錯(cuò)誤代碼。該函數(shù)產(chǎn)生SIGABRT信號(hào)并發(fā)送給自己。實(shí)際就是系統(tǒng)發(fā)現(xiàn)操作異常,調(diào)用發(fā)送SIGABRT(abort()函數(shù))信號(hào),殺死進(jìn)程。
此異常,系統(tǒng)會(huì)知道程序在什么地方有不合法的操作,會(huì)在控制臺(tái)輸出異常信息。例如:向一個(gè)對(duì)象發(fā)送它無(wú)法識(shí)別的消息
[self performSelector:@selector(notExistFunc:)];
2.3 Killed — EXC_CRASH (SIGKILL)
進(jìn)程被系統(tǒng)強(qiáng)制結(jié)束,通過(guò)查看Termination Reason找到crash信息
如果APP消耗了太多的時(shí)間在初始化(在20s內(nèi)沒有啟動(dòng)), watchdog (時(shí)間狗)就會(huì)終止程序運(yùn)行。如以下代碼
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
sleep(20);
return YES;
}
2.4 Trace Trap(追蹤捕獲) — EXC_BREAKPOINT (SIGTRAP)
這個(gè)異常是為了讓一個(gè)附加的調(diào)試器有機(jī)會(huì)在執(zhí)行過(guò)程中的某個(gè)特定時(shí)刻中斷進(jìn)程,我們可以在代碼里面添加 __builtin_trap() 方法手動(dòng)觸發(fā)這個(gè)異常。如果沒有附加調(diào)試器,則會(huì)生成crash文件。
較低級(jí)別的庫(kù)(例如libdispatch)會(huì)在遇到致命錯(cuò)誤時(shí)捕獲該進(jìn)程
swift代碼在遇到下面2種情況的時(shí)候也會(huì)拋出這個(gè)異常:
- 一個(gè)非可選類型的值為nil
- 錯(cuò)誤的強(qiáng)制類型轉(zhuǎn)換,如把NSString轉(zhuǎn)換為NSDate等等
2.5 Illegal Instruction(非法指令) — EXC_BAD_INSTRUCTION (SIGILL)
程序嘗試的去執(zhí)行一個(gè)非法的或者沒有定義的指令。如果配置的函數(shù)地址有問(wèn)題,進(jìn)程在執(zhí)行函數(shù)跳轉(zhuǎn)的時(shí)候,就會(huì)發(fā)生這個(gè)crash。
2.6 Quit — SIGQUIT 跨進(jìn)程相關(guān)
該進(jìn)程在具有管理其生命周期的權(quán)限的另一進(jìn)程的請(qǐng)求下終止。 SIGQUIT并不意味著進(jìn)程崩潰,但是可以說(shuō)明該進(jìn)程存在一些問(wèn)題。
比如在iOS中,第三方鍵盤應(yīng)用可能在在其他APP中被喚起,但是如果鍵盤應(yīng)用需要很長(zhǎng)的時(shí)間去加載,則會(huì)被強(qiáng)制退出。
Xcode連機(jī)調(diào)試下
時(shí)間狗不起作用,只有安裝后脫離Xcode,手動(dòng)點(diǎn)擊運(yùn)行會(huì)出現(xiàn)退出。
2.7 Resource Limit — EXC_RESOURCE
進(jìn)程使用的資源超出的限制。這是一個(gè)從操作系統(tǒng)通知,進(jìn)程是使用太多的資源。這雖然不是崩潰但也會(huì)生成崩潰日志。
2.8 Other
- 0xbaaaaaad: 該code表示這個(gè)crash文件是系統(tǒng)的stackshot,該Crash log并非一個(gè)真正的Crash,它僅僅只是包含了整個(gè)系統(tǒng)某一時(shí)刻的運(yùn)行狀態(tài)。通??梢酝ㄟ^(guò)同時(shí)按Home鍵和音量鍵,可能由于用戶不小心觸發(fā)。
- 0x8badf00d: 讀做 “ate bad food”! (把數(shù)字換成字母,是不是很像 :p)該編碼表示應(yīng)用是因?yàn)榘l(fā)生watchdog超時(shí)而被iOS終止的。 通常是應(yīng)用花費(fèi)太多時(shí)間而無(wú)法啟動(dòng)、終止或響應(yīng)用系統(tǒng)事件。
- 0xbad22222: 該編碼表示 VoIP 應(yīng)用因?yàn)檫^(guò)于頻繁重啟而被終止。
- 0xdead10cc: 讀做 “dead lock”!該代碼表明應(yīng)用因?yàn)樵诤笈_(tái)運(yùn)行時(shí)占用系統(tǒng)資源,如通訊錄數(shù)據(jù)庫(kù)不釋放而被終止。
- 0xdeadfa11: 讀做 “dead fall”! 該代碼表示應(yīng)用是被用戶強(qiáng)制退出的。根據(jù)蘋果文檔, 強(qiáng)制退出發(fā)生在用戶長(zhǎng)按開關(guān)按鈕直到出現(xiàn) “滑動(dòng)來(lái)關(guān)機(jī)”, 然后長(zhǎng)按 Home按鈕。強(qiáng)制退出將產(chǎn)生 包含0xdeadfa11 異常編碼的崩潰日志, 因?yàn)榇蠖鄶?shù)是強(qiáng)制退出是因?yàn)閼?yīng)用阻塞了界面。
- 0xc00010ff: 當(dāng)操作系統(tǒng)響應(yīng)thermal事件的時(shí)候,會(huì)強(qiáng)制的kill進(jìn)程,如程序執(zhí)行大量耗費(fèi)CPU和GPU的運(yùn)算,導(dǎo)致設(shè)備過(guò)熱,觸發(fā)系統(tǒng)過(guò)熱保護(hù)被系統(tǒng)終止
三、SDK開發(fā)者,定位crash是由APP代碼還是SDK代碼導(dǎo)致
在開發(fā)iOS平臺(tái)上的SDK,提供給開發(fā)者使用。為了監(jiān)控定位SDK自身引起的崩潰,及統(tǒng)計(jì)崩潰率,我們需在SDK中加入抓取crash的功能。但是收集到的日志都是開發(fā)者應(yīng)用的crash(此crash有可能是開發(fā)者自己代碼引起,也有可能是第三方靜態(tài)庫(kù)引起),由于接入SDK的應(yīng)用數(shù)量很多,產(chǎn)生的崩潰日志量非常龐大,靠人力從海量的日志中篩選出我們SDK的crash日志非常困難。
問(wèn)題:如何區(qū)分SDK內(nèi)部的crash和App的crash?
3.1 確定動(dòng)態(tài)庫(kù)crash
我們知道動(dòng)態(tài)庫(kù)的crash的方法棧中是帶動(dòng)態(tài)庫(kù)的名字的,能直接看出是哪個(gè)模塊發(fā)生了crash。crash日志,區(qū)分App的crash和引入的動(dòng)態(tài)庫(kù)SDK的crash比較簡(jiǎn)單,App運(yùn)行時(shí)crash后,可以通過(guò)crash的地址,找到包含這個(gè)地址的二進(jìn)制模塊(地址范圍)就能定位到。如 crash日志格式中 庫(kù)信息塊除第一個(gè)應(yīng)用模塊其他都是動(dòng)態(tài)庫(kù),且前面都帶有地址范圍。
3.2 確定靜態(tài)庫(kù)的crash
通過(guò)crash的地址可以找到該方法所屬的二進(jìn)制模塊。如果SDK是靜態(tài)庫(kù),引入到應(yīng)用工程中,其代碼會(huì)被加入到App的代碼段中,SDK的代碼和App自身代碼屬于同一個(gè)二進(jìn)制模塊,這樣就不容易判斷了。在調(diào)用SDK中方法時(shí)出現(xiàn)crash,其在crash文件異常堆棧中定位到的模塊屬于應(yīng)用可執(zhí)行模塊,即App的代碼段,這樣就不知道是App還是SDK內(nèi)部crash了。
解決方案:通過(guò)符號(hào)來(lái)判斷、通過(guò)地址來(lái)判斷
3.2.1 通過(guò)符號(hào)來(lái)判斷
即服務(wù)端收集crash日志后,通過(guò)符號(hào)文件解析出堆棧信息,然后通過(guò)crash符號(hào)類名+方法來(lái)判斷是app的crash還是sdk內(nèi)部的crash。
人為干預(yù)查看crash日志識(shí)別
1、符號(hào)化服務(wù)端crash日志
2、收集SDK中所有的特征符號(hào),類名、方法名
此方式費(fèi)時(shí)費(fèi)力
3.2.2 通過(guò)地址來(lái)判斷
獲取crash發(fā)生的地址,通過(guò)地址來(lái)判斷是否在SDK內(nèi)部,就像上面動(dòng)態(tài)庫(kù)一樣,只要確定靜態(tài)庫(kù)地址范圍。問(wèn)題變成了如何獲取SDK代碼被連接進(jìn)App后的起始地址和結(jié)束地址。
給SDK添加兩個(gè)文件"CaptchaSDKCodeAddressBegin.m"、"CaptchaSDKCodeAddressEnd.m"及用于訪問(wèn)的頭文件"CaptchaSDKCodeAddress.h"。
CaptchaSDKCodeAddress.h文件:
#import <Foundation/Foundation.h>
@interface CaptchaSDKCodeAddressBegin : NSObject
+(void *)startAddress;
+(long)getExecuteImageSlide;
@end
@interface CaptchaSDKCodeAddressEnd : NSObject
+(void *)endAddress;
@end
CaptchaSDKCodeAddressBegin.m文件:
#import "CaptchaSDKCodeAddress.h"
#import <mach-o/dyld.h>
#import <objc/runtime.h>
@implementation CaptchaSDKCodeAddressBegin
+(void *)startAddress
{
Method method = class_getClassMethod([self class], NSSelectorFromString(@"startAddress"));
IMP classResumeIMP = method_getImplementation(method);
return classResumeIMP;
}
+(long)getExecuteImageSlide
{
static long slide = -1;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
if (_dyld_get_image_header(i)->filetype == MH_EXECUTE) {
slide = _dyld_get_image_vmaddr_slide(i);
break;
}
}
});
return slide;
}
@end
CaptchaSDKCodeAddressEnd.m文件:
#import "CaptchaSDKCodeAddress.h"
#import <objc/runtime.h>
@implementation CaptchaSDKCodeAddressEnd
+(void *)endAddress
{
Method method = class_getClassMethod([self class], NSSelectorFromString(@"endAddress"));
IMP classResumeIMP = method_getImplementation(method);
return classResumeIMP;
}
@end
調(diào)整下在SDK中編譯順序

應(yīng)用運(yùn)行的時(shí)候我們可以通過(guò)代碼來(lái)獲取當(dāng)前[CaptchaSDKCodeAddressBegin startAddress]和[CaptchaSDKCodeAddressEnd endAddress]兩個(gè)方法在內(nèi)存中地址值,以此確認(rèn)了SDK所有代碼塊在應(yīng)用程序運(yùn)行時(shí)所對(duì)應(yīng)的內(nèi)存地址范圍。
由于iOS系統(tǒng)引入了ASLR機(jī)制,即Address space layout randomization。在App運(yùn)行時(shí),iOS系統(tǒng)會(huì)給加載進(jìn)內(nèi)存的二進(jìn)制模塊一個(gè)隨機(jī)的偏移地址,我們只需要把運(yùn)行時(shí)的地址減掉這個(gè)偏移地址即可,上面獲取偏移值方法:getExecuteImageSlide,原理:遍歷加載的鏡像列表(應(yīng)用的可執(zhí)行二進(jìn)制文件、動(dòng)態(tài)庫(kù)等),搜索出可執(zhí)行文件類型來(lái)獲取偏移值。如下圖Demo集成SDK,目的是獲取進(jìn)行ASLR之前sdk地址(這個(gè)地址同一個(gè)可執(zhí)行二進(jìn)制文件是固定的)。

上圖中slide值為隨機(jī)的偏移值,進(jìn)程每一次啟動(dòng),地址空間都會(huì)簡(jiǎn)單地隨機(jī)化偏移(安全性考慮,每一次啟動(dòng)的虛擬內(nèi)存鏡像都是一致,黑客容易采取重寫內(nèi)存的方式破解程序)。
我們最終拿到了兩個(gè)地址分別是0x1000061cc和0x10000ce28,那么我們?nèi)绾涡r?yàn)?zāi)兀?br>
這里使用到了MachOView工具進(jìn)行分析(開源地址):獲取SDK模塊代碼在App二進(jìn)制文件中的地址。
靜態(tài)庫(kù)SDK二進(jìn)制文件結(jié)構(gòu)

(圖中FatFile/FatBinary,就是一個(gè)由不同的編譯架構(gòu)后的Mach-O產(chǎn)物所合成的集合體)
上圖SDK包含的object文件內(nèi)容,為編譯后的產(chǎn)物。它的順序是由xcode工程中【Targets->Build Phases->Complie Sources】編譯順序決定,如下圖

SDK的文件相當(dāng)于一個(gè)object文件的容器,把源文件的編譯產(chǎn)物按順序打包組織在一起就是SDK的二進(jìn)制文件了。SDK二進(jìn)制連接進(jìn)App可執(zhí)行文件是怎么樣的,如下圖
應(yīng)用二進(jìn)制文件結(jié)構(gòu)簡(jiǎn)單描述:

Load Commands
LC_SEGMENT_64:將可執(zhí)行文件(64位)映射到進(jìn)程地址空間,32位系統(tǒng)的是LC_SEGMENT,是加載的主要命令,負(fù)責(zé)指導(dǎo)內(nèi)核來(lái)設(shè)置進(jìn)程的內(nèi)存空間
LC_DYLD_INFO_ONLY:動(dòng)態(tài)鏈接相關(guān)信息
LC_SYMTAB:符號(hào)表地址
LC_DYSYMTAB:動(dòng)態(tài)符號(hào)地址表
LC_LOAD_DYLINKER:加載一個(gè)動(dòng)態(tài)鏈接器,路徑“/usr/lib/dyld”
LC_UUID:二進(jìn)制文件的標(biāo)識(shí)ID,dSYM文件、crash中都存在這個(gè)值,確定兩個(gè)文件是否匹配,分析出對(duì)應(yīng)的崩潰位置
LC_VERSION_MIN_MACOSX:二進(jìn)制文件要求的最低系統(tǒng)版本,和xcode中配置的target有關(guān)
LC_MAIN:設(shè)置程序的入口,編譯型的語(yǔ)言需要指定入口地址,解釋器語(yǔ)言對(duì)此沒有邀請(qǐng)
LC_SOURCE_VERSION:構(gòu)建二進(jìn)制文件使用的源代碼版本
LC_LOAD_DYLIB(...):加載一個(gè)動(dòng)態(tài)鏈接共享庫(kù),"..." 如Foundation、libobjc.A.dylib等系統(tǒng)動(dòng)態(tài)庫(kù)。
LC_RPATH:用戶動(dòng)態(tài)庫(kù)的位置
LC_FUNCTION_STARTS:定義函數(shù)的起始地址表,使我們的調(diào)試器很容易看到地址
LC_DATA_IN_CODE:定義在代碼段內(nèi)的非指令數(shù)據(jù)
下圖查看兩個(gè)類方法對(duì)應(yīng)地址位置
需要注意這里使用MachOView查看應(yīng)用APP文件要與手機(jī)上執(zhí)行APP為同一個(gè)包,避免再次打包app導(dǎo)致SDK編譯進(jìn)可執(zhí)行文件中位置發(fā)生變化,校驗(yàn)兩個(gè)地址出現(xiàn)不準(zhǔn)確。


從上圖中我們得到APP內(nèi)[CaptchaSDKCodeAddressBegin startAddress]和[CaptchaSDKCodeAddressEnd endAddress]兩個(gè)地址分別為0x1000061CC和0x10000CE28,與我們之前獲取的完全吻合,而且從圖上看出SDK內(nèi)其他方法都在這兩個(gè)地址范圍之內(nèi)。至此,我們就能通過(guò)crash時(shí)的方法地址來(lái)判斷是否是SDK內(nèi)部的crash了。
3.3 總結(jié)
- 動(dòng)態(tài)庫(kù)有明確的起止地址,可以用crash方法的地址直接判斷
- 靜態(tài)庫(kù)處理
- 靜態(tài)庫(kù)中的方法會(huì)按編譯時(shí)的順序連接進(jìn)App可執(zhí)行文件
- 在SDK中第一個(gè)編譯文件最前面添加一個(gè)方法,返回其自身地址,作為SDK的起始地址
- 在SDK中最后一個(gè)編譯文件的最后面添加一個(gè)方法,返回其自身地址,作為SDK的所有編譯文件方法的結(jié)束地址
- crash時(shí),把獲取到的crash地址與SDK的起止地址進(jìn)行比較就能知道是否是SDK內(nèi)部的crash。主要比較地址,如果crash地址減去slide,則對(duì)應(yīng)的SDK起止地址也應(yīng)減去slide
四、獲取線程堆棧
4.1 函數(shù)調(diào)用棧(call stack)
參考:https://blog.csdn.net/qq_36503007/article/details/82887811
int func_1(int x, int y);
int func_2(int x, int y, int z);
int main(int argc, char **argv)
{
int x = 1;
int y = 2;
int v = 0;
v = func_1(x, y);
printf("%d",v);
}
int func_1(int x, int y)
{
int a = 0;
int z = 100;
a = func_2(x, y, z);
return a;
}
int func_2(int x, int y, int z)
{
int w = 2;
return (x + y) * (z + w);
}

EBP為幀基指針, ESP為棧頂指針,并在引用匯編代碼時(shí)分別記為%ebp和%esp。
不同架構(gòu)的CPU,寄存器名稱被添加不同前綴以指示寄存器的大小。例如x86架構(gòu)用字母“e(extended)”作名稱前綴,指示寄存器大小為32位;x86_64架構(gòu)用字母“r”作名稱前綴,指示各寄存器大小為64位。
圖上表示一個(gè)棧(32位CPU),這里將高地址放到下邊,這樣看起來(lái)更好理解,也更加符合[線程堆棧]信息上邊棧頂,底部棧底。棧分為若干棧幀(frame),每個(gè)棧幀對(duì)應(yīng)一個(gè)函數(shù)調(diào)用。粉色部分是func_1函數(shù)的棧幀,它在執(zhí)行的過(guò)程中調(diào)用了func_2函數(shù),這里func_2的棧幀用綠色表示。
棧幀由三部分組成:函數(shù)參數(shù)、局部變量及恢復(fù)前一棧幀所需的數(shù)據(jù)(如上一幀下一條執(zhí)行地址[Return Address],上一棧幀地址[EBP of Previous Stack Frame]),如上圖,在調(diào)用func_2函數(shù)時(shí)首先把函數(shù)參數(shù)入棧,隨后將前一棧幀所需數(shù)據(jù)入棧(當(dāng)函數(shù)執(zhí)行完后回到哪里繼續(xù)執(zhí)行),函數(shù)內(nèi)部定義的變量則屬于第三部分入棧,當(dāng)函數(shù)返回時(shí)此棧幀被從堆棧中彈出也就是出棧。
大多數(shù)操作系統(tǒng)中,每個(gè)棧幀還保存了上一個(gè)棧幀的Fram Pointer,因此只要知道當(dāng)前棧幀的Stack Pointer和Frame Pointer,就能知道上一個(gè)棧幀的Stack Pointer和Frame Pointer,從而遞歸獲取棧底的幀。
4.2 Mach_Thread
iOS開發(fā)中,系統(tǒng)提供了task_threads方法,可以獲取到所有的線程,這里的線程是最底層的mach線程,NSThread只是對(duì)進(jìn)行了封裝,可以通過(guò)設(shè)置線程名稱方式找到相應(yīng)的thread_t,之后將名稱設(shè)置回去。對(duì)于每一個(gè)線程,可以用thread_get_state方法獲取它的所有信息(棧頂指針、當(dāng)前的棧幀、上一棧幀及return address等之后可以遍歷獲取所有棧幀地址),信息填充在_STRUCT_MCONTEXT類型的參數(shù)中,存儲(chǔ)了當(dāng)前線程的Stack Pointer和最頂部棧幀的Frame Pointer,遍歷從而獲取到整個(gè)線程的調(diào)用棧。
根據(jù)棧幀F(xiàn)rame Pointer后通過(guò)dladdr獲取這個(gè)函數(shù)調(diào)用的符號(hào)名,這樣就能打印出線程的所有堆棧信息。
獲取符號(hào)名步驟:
- 根據(jù)Frame Pointer找到函數(shù)調(diào)用的地址
- 找到Frame Pointer屬于哪個(gè)鏡像文件
- 找到鏡像文件的符號(hào)表
- 在符號(hào)表中找到函數(shù)調(diào)用地址對(duì)應(yīng)的符號(hào)名
下面代碼可以結(jié)合[上圖]函數(shù)調(diào)用棧圖更加容易理解
4.2.1 獲取線程的信息
通過(guò)task_threads獲取所有的線程
thread_act_array_t threads; //int 組成的數(shù)組
mach_msg_type_number_t thread_count = 0; //mach_msg_type_number_t 是 int 類型
const task_t this_task = mach_task_self(); //int
//根據(jù)當(dāng)前 task 獲取所有線程
kern_return_t kr = task_threads(this_task, &threads, &thread_count);
通過(guò)thread_info獲取各個(gè)線程詳細(xì)信息
//存儲(chǔ) thread 信息的結(jié)構(gòu)體
typedef struct WSThreadInfoStruct {
double cpuUsage;
integer_t userTime;
} WSThreadInfoStruct;
// thread info
WSThreadInfoStruct threadInfoSt = {0};
thread_info_data_t threadInfo;
thread_basic_info_t threadBasicInfo;
mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
if (thread_info((thread_act_t)thread, THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
threadBasicInfo = (thread_basic_info_t)threadInfo;
if (!(threadBasicInfo->flags & TH_FLAGS_IDLE)) {
threadInfoSt.cpuUsage = threadBasicInfo->cpu_usage / 10;
threadInfoSt.userTime = threadBasicInfo->system_time.microseconds;
}
}
uintptr_t buffer[100];
int i = 0;
NSMutableString *reStr = [NSMutableString stringWithFormat:@"Stack of thread: %u:\n CPU used: %.1f percent\n user time: %d second\n", thread, threadInfoSt.cpuUsage, threadInfoSt.userTime];
4.2.2 獲取線程里所有棧的信息
#import "WSBacktraceLogger.h"
#import <mach/mach.h>
#include <dlfcn.h>
#include <pthread.h>
#include <sys/types.h>
#include <limits.h>
#include <string.h>
#include <mach-o/dyld.h>
#include <mach-o/nlist.h>
#pragma -mark DEFINE MACRO FOR DIFFERENT CPU ARCHITECTURE
#if defined(__arm64__)
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(3UL))
#define WS_THREAD_STATE_COUNT ARM_THREAD_STATE64_COUNT
#define WS_THREAD_STATE ARM_THREAD_STATE64
#define WS_FRAME_POINTER __fp
#define WS_STACK_POINTER __sp
#define WS_INSTRUCTION_ADDRESS __pc
#elif defined(__arm__)
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(1UL))
#define WS_THREAD_STATE_COUNT ARM_THREAD_STATE_COUNT
#define WS_THREAD_STATE ARM_THREAD_STATE
#define WS_FRAME_POINTER __r[7]
#define WS_STACK_POINTER __sp
#define WS_INSTRUCTION_ADDRESS __pc
#elif defined(__x86_64__)
#define DETAG_INSTRUCTION_ADDRESS(A) (A)
#define WS_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT
#define WS_THREAD_STATE x86_THREAD_STATE64
#define WS_FRAME_POINTER __rbp
#define WS_STACK_POINTER __rsp
#define WS_INSTRUCTION_ADDRESS __rip
#elif defined(__i386__)
#define DETAG_INSTRUCTION_ADDRESS(A) (A)
#define WS_THREAD_STATE_COUNT x86_THREAD_STATE32_COUNT
#define WS_THREAD_STATE x86_THREAD_STATE32
#define WS_FRAME_POINTER __ebp
#define WS_STACK_POINTER __esp
#define WS_INSTRUCTION_ADDRESS __eip
#endif
#define CALL_INSTRUCTION_FROM_RETURN_ADDRESS(A) (DETAG_INSTRUCTION_ADDRESS((A)) - 1)
#if defined(__LP64__)
#define TRACE_FMT "%-4d%-31s 0x%016lx %s + %lu"
#define POINTER_FMT "0x%016lx"
#define POINTER_SHORT_FMT "0x%lx"
#define WS_NLIST struct nlist_64
#else
#define TRACE_FMT "%-4d%-31s 0x%08lx %s + %lu"
#define POINTER_FMT "0x%08lx"
#define POINTER_SHORT_FMT "0x%lx"
#define WS_NLIST struct nlist
#endif
typedef struct WSStackFrameModel{
const struct WSStackFrameModel *const previous;
const uintptr_t return_address;
} WSStackFrameModel;
static mach_port_t main_thread_id;
@implementation WSBacktraceLogger
+ (void)load
{
main_thread_id = mach_thread_self();
}
// 當(dāng)前線程調(diào)用棧信息
+ (NSString *)ws_backtraceOfCurrentThread {
return [self ws_backtraceOfNSThread:[NSThread currentThread]];
}
// 主線程調(diào)用棧信息
+ (NSString *)ws_backtraceOfMainThread {
return [self ws_backtraceOfNSThread:[NSThread mainThread]];
}
// 任意線程調(diào)用棧信息
+ (NSString *)ws_backtraceOfNSThread:(NSThread *)thread {
return _ws_backtraceOfThread(_ws_machThreadFromNSThread(thread));
}
// 全部線程調(diào)用棧信息
+ (NSString *)ws_backtraceOfAllThread:(NSThread *)thread {
thread_act_array_t threads;
mach_msg_type_number_t thread_count = 0;
const task_t this_task = mach_task_self();
kern_return_t kr = task_threads(this_task, &threads, &thread_count);
if(kr != KERN_SUCCESS) {
return @"Fail to get information of all threads";
}
NSMutableString *resultString = [NSMutableString stringWithFormat:@"Call Backtrace of %u threads:\n", thread_count];
for(int i = 0; i < thread_count; i++) {
[resultString appendString:_ws_backtraceOfThread(threads[i])];
}
return [resultString copy];
}
// NSThread pthread轉(zhuǎn)thread_t
thread_t _ws_machThreadFromNSThread(NSThread *nsthread) {
char name[256];
mach_msg_type_number_t count;
thread_act_array_t list;
//根據(jù)當(dāng)前 task 獲取所有線程
task_threads(mach_task_self(), &list, &count);
NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
NSString *originName = [nsthread name];
[nsthread setName:[NSString stringWithFormat:@"%f", currentTimestamp]];
if ([nsthread isMainThread]) {
return (thread_t)main_thread_id;
}
for (int i = 0; i < count; ++i) {
//_np 是指 not POSIX ,這里的 POSIX 是指操作系統(tǒng)的一個(gè)標(biāo)準(zhǔn),特別是與 Unix 兼容的操作系統(tǒng)。np 表示與標(biāo)準(zhǔn)不兼容
pthread_t pt = pthread_from_mach_thread_np(list[i]);
if ([nsthread isMainThread]) {
if (list[i] == main_thread_id) {
return list[i];
}
}
if (pt) {
name[0] = '\0';
//獲得線程名字
pthread_getname_np(pt, name, sizeof name);
if (!strcmp(name, [nsthread name].UTF8String)) {
[nsthread setName:originName];
return list[i];
}
}
}
[nsthread setName:originName];
return mach_thread_self();
}
// 打印線程堆棧信息
NSString *_ws_backtraceOfThread(thread_t thread)
{
uintptr_t backtraceBuffer[50];//uintptr_t = unsigned long,取線程最多50個(gè)棧幀
int i = 0;
NSMutableString *resultString = [[NSMutableString alloc] initWithFormat:@"Backtrace of Thread %u:\n", thread];
//回溯棧的算法
_STRUCT_MCONTEXT machineContext;
//通過(guò) thread_get_state 獲取完整的 machineContext 信息,包含 thread 狀態(tài)信息
mach_msg_type_number_t state_count = WS_THREAD_STATE_COUNT;//arm64:ARM_THREAD_STATE64_COUNT, arm:ARM_THREAD_STATE_COUNT, x86_64:x86_THREAD_STATE64_COUNT, i386:x86_THREAD_STATE32_COUNT,根據(jù)CPU架構(gòu)選擇
int THREAD_STATE = WS_THREAD_STATE;//arm64:ARM_THREAD_STATE64, arm:ARM_THREAD_STATE, x86_64:x86_THREAD_STATE64, i386: x86_THREAD_STATE32,根據(jù)CPU架構(gòu)選擇
kern_return_t kr = thread_get_state(thread, THREAD_STATE, (thread_state_t)(&(machineContext.__ss)), &state_count);
if (kr != KERN_SUCCESS)
{
return [NSString stringWithFormat:@"Fail to get information about thread: %u", thread];
}
//通過(guò)指令指針來(lái)獲取當(dāng)前指令地址
const uintptr_t instructionAddress = machineContext.__ss.WS_INSTRUCTION_ADDRESS;//arm64:__pc, arm64:arm, x86_64:__rip, i386:__eip;
if(instructionAddress == 0)
{
return @"Fail to get instruction address";
}
backtraceBuffer[i] = instructionAddress;
++i;
uintptr_t linkRegister = 0;
#if defined(__i386__) || defined(__x86_64__)
linkRegister = 0;
#else
linkRegister = machineContext.__ss.__lr;
#endif
if (linkRegister)
{
backtraceBuffer[i] = linkRegister;
i++;
}
WSStackFrameModel frame = {0};
//通過(guò)?;分羔槴@取當(dāng)前棧幀地址
const uintptr_t framePtr = machineContext.__ss.WS_FRAME_POINTER;//arm64:__fp, arm:__r[7], x86_64:__rbp,i386:__ebp
vm_size_t bytesCopied = 0;
kern_return_t kr_vr = vm_read_overwrite(mach_task_self(), (vm_address_t)((void *)framePtr), (vm_size_t)(sizeof(frame)), (vm_address_t)(&frame), &bytesCopied);
if(framePtr == 0 || kr_vr != KERN_SUCCESS)
{
return @"Fail to get frame pointer";
}
for(; i < 50; i++)
{
backtraceBuffer[i] = frame.return_address;
kr_vr = vm_read_overwrite(mach_task_self(), (vm_address_t)((void *)frame.previous), (vm_size_t)(sizeof(frame)), (vm_address_t)(&frame), &bytesCopied);
if(backtraceBuffer[i] == 0 || frame.previous == 0 || kr_vr != KERN_SUCCESS)
{
break;
}
}
//處理dlsym,對(duì)地址進(jìn)行符號(hào)化解析
//1.找到地址所屬的內(nèi)存鏡像,
//2.然后定位鏡像中的符號(hào)表
//3.最后在符號(hào)表中找到目標(biāo)地址的符號(hào)
int backtraceLength = i;
Dl_info symbolicated[backtraceLength];
_ws_symbolicate(backtraceBuffer, symbolicated, backtraceLength, 0);
for (int i = 0; i < backtraceLength; ++i)
{
[resultString appendFormat:@"%@", _ws_logBacktraceEntry(i, backtraceBuffer[i], &symbolicated[i])];
}
[resultString appendFormat:@"\n"];
return [resultString copy];
}
#pragma mark - Symbolicate
void _ws_symbolicate(const uintptr_t* const backtraceBuffer,
Dl_info* const symbolsBuffer,
const int numEntries,
const int skippedEntries)
{
int i = 0;
if(!skippedEntries && i < numEntries) {
_ws_dladdr(backtraceBuffer[i], &symbolsBuffer[i]);
i++;
}
for(; i < numEntries; i++) {
_ws_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[i]), &symbolsBuffer[i]);
}
}
bool _ws_dladdr(const uintptr_t address, Dl_info* const info)
{
info->dli_fname = NULL;
info->dli_fbase = NULL;
info->dli_sname = NULL;
info->dli_saddr = NULL;
//根據(jù)地址獲取是哪個(gè) image
const uint32_t idx = _ws_imageIndexContainingAddress(address);
if(idx == UINT_MAX) {
return false;
}
/*
Header
------------------
Load commands
Segment command 1 -------------|
Segment command 2 |
------------------ |
Data |
Section 1 data |segment 1 <----|
Section 2 data | <----|
Section 3 data | <----|
Section 4 data |segment 2
Section 5 data |
... |
Section n data |
*/
/*----------Mach Header---------*/
//根據(jù) image 的序號(hào)獲取 mach_header
const struct mach_header* header = _dyld_get_image_header(idx);
//返回 image_index 索引的 image 的虛擬內(nèi)存地址 slide 的數(shù)量,如果 image_index 超出范圍返回0
//動(dòng)態(tài)鏈接器加載 image 時(shí),image 必須映射到未占用地址的進(jìn)程的虛擬地址空間。動(dòng)態(tài)鏈接器通過(guò)添加一個(gè)值到 image 的基地址來(lái)實(shí)現(xiàn),這個(gè)值是虛擬內(nèi)存 slide 數(shù)量
const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
/*-----------ASLR 的偏移量---------*/
const uintptr_t addressWithSlide = address - imageVMAddrSlide;
//根據(jù) Image 的 Index 來(lái)獲取 segment 的基地址
//段定義Mach-O文件中的字節(jié)范圍以及動(dòng)態(tài)鏈接器加載應(yīng)用程序時(shí)這些字節(jié)映射到虛擬內(nèi)存中的地址和內(nèi)存保護(hù)屬性。 因此,段總是虛擬內(nèi)存頁(yè)對(duì)齊。 片段包含零個(gè)或多個(gè)節(jié)。
const uintptr_t segmentBase = _ws_segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
if(segmentBase == 0) {
return false;
}
info->dli_fname = _dyld_get_image_name(idx);
info->dli_fbase = (void*)header;
/*--------------Mach Segment-------------*/
//地址最匹配的symbol
//Find symbol tables and get whichever symbol is closest to the address.
const WS_NLIST* bestMatch = NULL;
uintptr_t bestDistance = ULONG_MAX;
uintptr_t cmdPtr = _ws_firstCmdAfterHeader(header);
if(cmdPtr == 0) {
return false;
}
//遍歷每個(gè) segment 判斷目標(biāo)地址是否落在該 segment 包含的范圍里
for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
const struct load_command* loadCmd = (struct load_command*)cmdPtr;
/*----------目標(biāo) Image 的符號(hào)表----------*/
//Segment 除了 __TEXT 和 __DATA 外還有 __LINKEDIT segment,它里面包含動(dòng)態(tài)鏈接器的使用的原始數(shù)據(jù),比如符號(hào),字符串和重定位表項(xiàng)。
//LC_SYMTAB 描述了 __LINKEDIT segment 內(nèi)查找字符串和符號(hào)表的位置
if(loadCmd->cmd == LC_SYMTAB) {
//獲取字符串和符號(hào)表的虛擬內(nèi)存偏移量。
const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;
const WS_NLIST* symbolTable = (WS_NLIST*)(segmentBase + symtabCmd->symoff);
const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
// 如果 n_value 是0,symbol 指向外部對(duì)象
if(symbolTable[iSym].n_value != 0) {
//給定的偏移量是文件偏移量,減去 __LINKEDIT segment 的文件偏移量獲得字符串和符號(hào)表的虛擬內(nèi)存偏移量
uintptr_t symbolBase = symbolTable[iSym].n_value;
uintptr_t currentDistance = addressWithSlide - symbolBase;
//尋找最小的距離 bestDistance,因?yàn)?addressWithSlide 是某個(gè)方法的指令地址,要大于這個(gè)方法的入口。
//離 addressWithSlide 越近的函數(shù)入口越匹配
if((addressWithSlide >= symbolBase) &&
(currentDistance <= bestDistance)) {
bestMatch = symbolTable + iSym;
bestDistance = currentDistance;
}
}
}
if(bestMatch != NULL) {
//將虛擬內(nèi)存偏移量添加到 __LINKEDIT segment 的虛擬內(nèi)存地址可以提供字符串和符號(hào)表的內(nèi)存 address。
info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
if(*info->dli_sname == '_') {
info->dli_sname++;
}
//所有的 symbols 的已經(jīng)被處理好了
// This happens if all symbols have been stripped.
if(info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
info->dli_sname = NULL;
}
break;
}
}
cmdPtr += loadCmd->cmdsize;
}
return true;
}
//通過(guò) address 找到對(duì)應(yīng)的 image 的游標(biāo),從而能夠得到 image 的更多信息
uint32_t _ws_imageIndexContainingAddress(const uintptr_t address) {
//返回當(dāng)前 image 數(shù),這里 image 不是線程安全的,因?yàn)榱硪粋€(gè)線程可能正處在添加或者刪除 image 期間
const uint32_t imageCount = _dyld_image_count();
const struct mach_header* header = 0;
//O(n2)的方式查找,考慮優(yōu)化
for(uint32_t iImg = 0; iImg < imageCount; iImg++) {
//返回一個(gè)指向由 image_index 索引的 image 的 mach 頭的指針,如果 image_index 超出了范圍,那么久返回 NULL
header = _dyld_get_image_header(iImg);
if(header != NULL) {
// 查找 segment command
uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg);
uintptr_t cmdPtr = _ws_firstCmdAfterHeader(header);
if(cmdPtr == 0) {
continue;
}
for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
const struct load_command* loadCmd = (struct load_command*)cmdPtr;
//在遍歷mach header里的 load command 時(shí)判斷 segment command 是32位還是64位的,大部分系統(tǒng)的 segment 都是32位的
if(loadCmd->cmd == LC_SEGMENT) {
const struct segment_command* segCmd = (struct segment_command*)cmdPtr;
if(addressWSlide >= segCmd->vmaddr &&
addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
return iImg;
}
}
else if(loadCmd->cmd == LC_SEGMENT_64) {
const struct segment_command_64* segCmd = (struct segment_command_64*)cmdPtr;
if(addressWSlide >= segCmd->vmaddr &&
addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
return iImg;
}
}
cmdPtr += loadCmd->cmdsize;
}
}
}
return UINT_MAX;
}
uintptr_t _ws_segmentBaseOfImageIndex(const uint32_t idx) {
const struct mach_header* header = _dyld_get_image_header(idx);
//查找 segment command 返回 image 的地址
uintptr_t cmdPtr = _ws_firstCmdAfterHeader(header);
if(cmdPtr == 0) {
return 0;
}
for(uint32_t i = 0;i < header->ncmds; i++) {
const struct load_command* loadCmd = (struct load_command*)cmdPtr;
if(loadCmd->cmd == LC_SEGMENT) {
const struct segment_command* segmentCmd = (struct segment_command*)cmdPtr;
if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
return segmentCmd->vmaddr - segmentCmd->fileoff;
}
}
else if(loadCmd->cmd == LC_SEGMENT_64) {
const struct segment_command_64* segmentCmd = (struct segment_command_64*)cmdPtr;
if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
return (uintptr_t)(segmentCmd->vmaddr - segmentCmd->fileoff);
}
}
cmdPtr += loadCmd->cmdsize;
}
return 0;
}
uintptr_t _ws_firstCmdAfterHeader(const struct mach_header* const header) {
switch(header->magic) {
case MH_MAGIC:
case MH_CIGAM:
return (uintptr_t)(header + 1);
case MH_MAGIC_64:
case MH_CIGAM_64:
return (uintptr_t)(((struct mach_header_64*)header) + 1);
default:
return 0; // Header is corrupt
}
}
NSString* _ws_logBacktraceEntry(const int entryNum,
const uintptr_t address,
const Dl_info* const dlInfo)
{
char faddrBuff[20];
char saddrBuff[20];
//獲取路徑最后文件名
//strrchr 查找某字符在字符串中最后一次出現(xiàn)的位置
const char* fname = _ws_lastPathEntry(dlInfo->dli_fname);
if(fname == NULL) {
sprintf(faddrBuff, POINTER_FMT, (uintptr_t)dlInfo->dli_fbase);
fname = faddrBuff;
}
uintptr_t offset = address - (uintptr_t)dlInfo->dli_saddr;
const char* sname = dlInfo->dli_sname;
if(sname == NULL) {
sprintf(saddrBuff, POINTER_SHORT_FMT, (uintptr_t)dlInfo->dli_fbase);
sname = saddrBuff;
offset = address - (uintptr_t)dlInfo->dli_fbase;
}
return [NSString stringWithFormat:@"%-30s 0x%08" PRIxPTR " %s + %lu\n" ,fname, (uintptr_t)address, sname, offset];
}
const char* _ws_lastPathEntry(const char* const path) {
if(path == NULL) {
return NULL;
}
char* lastFile = strrchr(path, '/');
return lastFile == NULL ? path : lastFile + 1;
}