我只是代碼的搬運工
crash文件獲取方式
- 應用集成第三方的crash SDK洞焙,自動采集相關運行堆棧吕朵,發(fā)送到服務器上绰垂,開發(fā)者上傳dSYM文件進行解析嗅榕,得到符號化的堆棧信息顺饮。
- 某設備上的crash可以通過Xcode導出crash文件查看崩潰日志,Xcode -> window -> Devices and Simulators -> 選擇設備 -View Device Logs凌那。
- 自己應用加入中收集異常代碼
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報告日志可以分為頭部(Header)、異常信息(Exception Information)帽蝶、診斷信息(Additional Diagnostic Information)赦肋、線程堆棧(Backtraces)、線程狀態(tài)(Thread State)、庫信息(Binary Images)這個六個部分佃乘。
頭部(Header):硬件型號囱井,系統(tǒng)版本,進程名稱趣避、id庞呕,bundleid,崩潰時間鹅巍,crash日志報告格式版本號等信息千扶。
- Incident Identifier: 事件標識符,每個crash文件對應一個唯一的標識符
- CrashReporter Key: 匿名設備標識符
- Hardware Model: 設備型號
- Process: 進程名
- Identifier: app Identifier
- Exception Type: 異常類型
- Exception Codes: 異常代碼
- Termination Reason: 進程被結束的原因
異常信息(Exception Information): 崩潰類型骆捧、崩潰代碼及觸發(fā)崩潰的線程等信息澎羞。
- Exception Codes: 處理器的具體信息有關的異常編碼成一個或多個64位進制數(shù)。通常情況下敛苇,這個區(qū)域不會被呈現(xiàn)妆绞,因為將異常代碼解析成人們可以看懂的描述是在其它區(qū)域進行的。
- Exception Subtype: 供人們可讀的異常代碼的名字枫攀。
- Exception Message: 從異常代碼中提取的額外的可供人們閱讀的信息括饶。
- Exception Note: 不是特定于一個異常類型的額外信息.如果這個區(qū)域包含SIMULATED (這不是一個崩潰)然后進程沒有崩潰,但是被watchdog殺掉了来涨。
- Termination Reason: 當一個進程被終止的時的原因图焰。
- Triggered by Thread: 異常所在的線程。
診斷信息(Additional Diagnostic Information): 非常簡略的診斷信息蹦掐。不是每個崩潰都會有診斷信息技羔。
線程堆棧(Backtraces): 崩潰發(fā)生時,各個線程的方法調用棧的詳細信息卧抗。觸發(fā)崩潰的線程會被標記上Crashed藤滥。
- 第一列的數(shù)字表示對應線程的調用棧順序
- 第二列表示對應的鏡像文件名稱,如系統(tǒng)corefoundtion
- 第三列表示當前行對應的符號地址
- 第四列如果已經符號化過則表示對應的符號社裆,反之為鏡像的起始地址+文件偏移地址拙绊,這2個值相加實際上就是第三列的地址
線程狀態(tài)(Thread State): 崩潰時寄存器的狀態(tài)。
庫信息(Binary Images): 加載的動態(tài)庫信息泳秀。
查看crash日志時标沪,首先會在【異常信息(Exception Information)】中通過“Triggered by Thread”的字段判斷是哪個線程發(fā)生了crash。在【線程堆棧(Backtraces)】信息中嗜傅,會看到各個線程號金句,而崩潰發(fā)生的線程號下面會有“Thread xx Crashed”標記該線程發(fā)生了crash。
在【線程堆棧(Backtraces)】信息中磺陡,有方法編號趴梢,方法所屬模塊名漠畜,方法地址,方法符號信息或者方法所在的段地址及偏移量坞靶。每個方法的地址是包含在所屬模塊的地址范圍內,比如GrowSDKDemo模塊(0x10049400 - 0x100887fff)憔狞。
圖中顯示,0號線程(com.apple.main-thread 即主線程)發(fā)生了crash,地址是0x00000001004b6508,在GrowSDKDemo這個模塊內彰阴。在【庫信息(Binary Images)】信息中可以找到這個二進制模塊瘾敢,也就是App可執(zhí)行文件GrowSDKDemo,和其他的模塊是第三方動態(tài)庫(YLDataSDK)尿这、系統(tǒng)加載的動態(tài)庫(UIKit簇抵、CoreFoundation等)。
二射众、Exception類型
2.1 Bad Memory Access — EXC_BAD_ACCESS (SIGSEGV钞馁、SIGBUS)
當進程嘗試的去訪問一個不可用或者不允許訪問的內存空間時怒炸,會發(fā)生野指針異常阿迈, Exception Subtype
中包含了錯誤的描述及想要訪問的地址畦韭。
SIGSEGV在ARC以后應該很少見到,SIGBUS為總線錯誤罗洗,與SIGSEGV訪問的是無效地址愉舔,而SIGBUS訪問的是有效地址,但總線訪問異常(如地址對齊問題)
- 如果
obj_msgSend
或objc_release
符號信息位于crash線程的最上面伙菜,說明該進程可能嘗試訪問已經釋放的對象轩缤。開啟僵尸模式進行調試、對程序做Analyze分析贩绕。 - 如果
gpus_ReturnNotPermittedKillClient
符號在crash線程的最上面火的,說明進程試圖在后臺使用OpenGL ES或Metal進行渲染,而被殺掉丧叽。 - 在debug模式下打開 Address Sanitizer,它會在編譯的時候自動添加一些關于內存訪問的工具卫玖,在crash發(fā)生的時候公你,Xcode會給出對應的詳細信息踊淳。
2.2 Abnormal Exit(異常退出) — EXC_CRASH (SIGABRT)
非正常退出,大多數(shù)發(fā)生這種crash的原因是因為未捕獲的Objective-C/C++異常陕靠,導致進程調用 abort()
方法退出迂尝。
中止當前進程,返回一個錯誤代碼剪芥。該函數(shù)產生SIGABRT信號并發(fā)送給自己垄开。實際就是系統(tǒng)發(fā)現(xiàn)操作異常,調用發(fā)送SIGABRT(abort()函數(shù))信號税肪,殺死進程溉躲。
此異常榜田,系統(tǒng)會知道程序在什么地方有不合法的操作,會在控制臺輸出異常信息锻梳。例如:向一個對象發(fā)送它無法識別的消息
[self performSelector:@selector(notExistFunc:)];
2.3 Killed — EXC_CRASH (SIGKILL)
進程被系統(tǒng)強制結束箭券,通過查看Termination Reason找到crash信息
如果APP消耗了太多的時間在初始化(在20s內沒有啟動), watchdog
(時間狗)就會終止程序運行疑枯。如以下代碼
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
sleep(20);
return YES;
}
2.4 Trace Trap(追蹤捕獲) — EXC_BREAKPOINT (SIGTRAP)
這個異常是為了讓一個附加的調試器有機會在執(zhí)行過程中的某個特定時刻中斷進程辩块,我們可以在代碼里面添加 __builtin_trap()
方法手動觸發(fā)這個異常。如果沒有附加調試器荆永,則會生成crash文件废亭。
較低級別的庫(例如libdispatch)會在遇到致命錯誤時捕獲該進程
swift代碼在遇到下面2種情況的時候也會拋出這個異常:
- 一個非可選類型的值為nil
- 錯誤的強制類型轉換,如把NSString轉換為NSDate等等
2.5 Illegal Instruction(非法指令) — EXC_BAD_INSTRUCTION (SIGILL)
程序嘗試的去執(zhí)行一個非法的或者沒有定義的指令具钥。如果配置的函數(shù)地址有問題豆村,進程在執(zhí)行函數(shù)跳轉的時候,就會發(fā)生這個crash骂删。
2.6 Quit — SIGQUIT 跨進程相關
該進程在具有管理其生命周期的權限的另一進程的請求下終止你画。 SIGQUIT并不意味著進程崩潰,但是可以說明該進程存在一些問題桃漾。
比如在iOS中坏匪,第三方鍵盤應用可能在在其他APP中被喚起,但是如果鍵盤應用需要很長的時間去加載撬统,則會被強制退出适滓。
Xcode連機調試下
時間狗
不起作用,只有安裝后脫離Xcode恋追,手動點擊運行會出現(xiàn)退出凭迹。
2.7 Resource Limit — EXC_RESOURCE
進程使用的資源超出的限制。這是一個從操作系統(tǒng)通知,進程是使用太多的資源苦囱。這雖然不是崩潰但也會生成崩潰日志嗅绸。
2.8 Other
- 0xbaaaaaad: 該code表示這個crash文件是系統(tǒng)的stackshot,該Crash log并非一個真正的Crash撕彤,它僅僅只是包含了整個系統(tǒng)某一時刻的運行狀態(tài)鱼鸠。通常可以通過同時按Home鍵和音量鍵羹铅,可能由于用戶不小心觸發(fā)蚀狰。
- 0x8badf00d: 讀做 “ate bad food”! (把數(shù)字換成字母,是不是很像 :p)該編碼表示應用是因為發(fā)生watchdog超時而被iOS終止的职员。 通常是應用花費太多時間而無法啟動麻蹋、終止或響應用系統(tǒng)事件。
- 0xbad22222: 該編碼表示 VoIP 應用因為過于頻繁重啟而被終止焊切。
- 0xdead10cc: 讀做 “dead lock”!該代碼表明應用因為在后臺運行時占用系統(tǒng)資源扮授,如通訊錄數(shù)據庫不釋放而被終止芳室。
- 0xdeadfa11: 讀做 “dead fall”! 該代碼表示應用是被用戶強制退出的。根據蘋果文檔, 強制退出發(fā)生在用戶長按開關按鈕直到出現(xiàn) “滑動來關機”, 然后長按 Home按鈕刹勃。強制退出將產生 包含0xdeadfa11 異常編碼的崩潰日志, 因為大多數(shù)是強制退出是因為應用阻塞了界面渤愁。
- 0xc00010ff: 當操作系統(tǒng)響應thermal事件的時候,會強制的kill進程深夯,如程序執(zhí)行大量耗費CPU和GPU的運算抖格,導致設備過熱,觸發(fā)系統(tǒng)過熱保護被系統(tǒng)終止
三咕晋、SDK開發(fā)者雹拄,定位crash是由APP代碼還是SDK代碼導致
在開發(fā)iOS平臺上的SDK,提供給開發(fā)者使用掌呜。為了監(jiān)控定位SDK自身引起的崩潰滓玖,及統(tǒng)計崩潰率,我們需在SDK中加入抓取crash的功能质蕉。但是收集到的日志都是開發(fā)者應用的crash(此crash有可能是開發(fā)者自己代碼引起势篡,也有可能是第三方靜態(tài)庫引起),由于接入SDK的應用數(shù)量很多,產生的崩潰日志量非常龐大模暗,靠人力從海量的日志中篩選出我們SDK的crash日志非常困難禁悠。
問題:如何區(qū)分SDK內部的crash和App的crash?
3.1 確定動態(tài)庫crash
我們知道動態(tài)庫的crash的方法棧中是帶動態(tài)庫的名字的,能直接看出是哪個模塊發(fā)生了crash兑宇。crash日志碍侦,區(qū)分App的crash和引入的動態(tài)庫SDK的crash比較簡單,App運行時crash后隶糕,可以通過crash的地址瓷产,找到包含這個地址的二進制模塊(地址范圍)就能定位到。如 crash日志格式中 庫信息塊除第一個應用模塊其他都是動態(tài)庫枚驻,且前面都帶有地址范圍濒旦。
3.2 確定靜態(tài)庫的crash
通過crash的地址可以找到該方法所屬的二進制模塊。如果SDK是靜態(tài)庫再登,引入到應用工程中尔邓,其代碼會被加入到App的代碼段中,SDK的代碼和App自身代碼屬于同一個二進制模塊霎冯,這樣就不容易判斷了铃拇。在調用SDK中方法時出現(xiàn)crash,其在crash文件異常堆棧中定位到的模塊屬于應用可執(zhí)行模塊钞瀑,即App的代碼段沈撞,這樣就不知道是App還是SDK內部crash了。
解決方案:通過符號來判斷雕什、通過地址來判斷
3.2.1 通過符號來判斷
即服務端收集crash日志后缠俺,通過符號文件解析出堆棧信息显晶,然后通過crash符號類名+方法來判斷是app的crash還是sdk內部的crash。
人為干預查看crash日志識別
1壹士、符號化服務端crash日志
2磷雇、收集SDK中所有的特征符號,類名躏救、方法名
此方式費時費力
3.2.2 通過地址來判斷
獲取crash發(fā)生的地址唯笙,通過地址來判斷是否在SDK內部,就像上面動態(tài)庫一樣盒使,只要確定靜態(tài)庫地址范圍崩掘。問題變成了如何獲取SDK代碼被連接進App后的起始地址和結束地址。
給SDK添加兩個文件"CaptchaSDKCodeAddressBegin.m"少办、"CaptchaSDKCodeAddressEnd.m"及用于訪問的頭文件"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
調整下在SDK中編譯順序
應用運行的時候我們可以通過代碼來獲取當前[CaptchaSDKCodeAddressBegin startAddress]和[CaptchaSDKCodeAddressEnd endAddress]兩個方法在內存中地址值,以此確認了SDK所有代碼塊在應用程序運行時所對應的內存地址范圍英妓。
由于iOS系統(tǒng)引入了ASLR機制挽放,即Address space layout randomization。在App運行時蔓纠,iOS系統(tǒng)會給加載進內存的二進制模塊一個隨機的偏移地址辑畦,我們只需要把運行時的地址減掉這個偏移地址即可,上面獲取偏移值方法:getExecuteImageSlide,原理:遍歷加載的鏡像列表(應用的可執(zhí)行二進制文件腿倚、動態(tài)庫等)航闺,搜索出可執(zhí)行文件類型來獲取偏移值。如下圖Demo集成SDK猴誊,目的是獲取進行ASLR之前sdk地址(這個地址同一個可執(zhí)行二進制文件是固定的)潦刃。
上圖中slide值為隨機的偏移值,進程每一次啟動懈叹,地址空間都會簡單地隨機化偏移(安全性考慮乖杠,每一次啟動的虛擬內存鏡像都是一致,黑客容易采取重寫內存的方式破解程序)澄成。
我們最終拿到了兩個地址分別是0x1000061cc和0x10000ce28,那么我們如何校驗呢胧洒?
這里使用到了MachOView工具進行分析(開源地址):獲取SDK模塊代碼在App二進制文件中的地址。
靜態(tài)庫SDK二進制文件結構
(圖中FatFile/FatBinary,就是一個由不同的編譯架構后的Mach-O產物所合成的集合體)
上圖SDK包含的object文件內容墨状,為編譯后的產物卫漫。它的順序是由xcode工程中【Targets->Build Phases->Complie Sources】編譯順序決定,如下圖
SDK的文件相當于一個object文件的容器肾砂,把源文件的編譯產物按順序打包組織在一起就是SDK的二進制文件了列赎。SDK二進制連接進App可執(zhí)行文件是怎么樣的,如下圖
應用二進制文件結構簡單描述:
Load Commands
LC_SEGMENT_64:將可執(zhí)行文件(64位)映射到進程地址空間,32位系統(tǒng)的是LC_SEGMENT,是加載的主要命令镐确,負責指導內核來設置進程的內存空間
LC_DYLD_INFO_ONLY:動態(tài)鏈接相關信息
LC_SYMTAB:符號表地址
LC_DYSYMTAB:動態(tài)符號地址表
LC_LOAD_DYLINKER:加載一個動態(tài)鏈接器包吝,路徑“/usr/lib/dyld”
LC_UUID:二進制文件的標識ID饼煞,dSYM文件、crash中都存在這個值诗越,確定兩個文件是否匹配砖瞧,分析出對應的崩潰位置
LC_VERSION_MIN_MACOSX:二進制文件要求的最低系統(tǒng)版本,和xcode中配置的target有關
LC_MAIN:設置程序的入口嚷狞,編譯型的語言需要指定入口地址块促,解釋器語言對此沒有邀請
LC_SOURCE_VERSION:構建二進制文件使用的源代碼版本
LC_LOAD_DYLIB(...):加載一個動態(tài)鏈接共享庫,"..." 如Foundation床未、libobjc.A.dylib等系統(tǒng)動態(tài)庫褂乍。
LC_RPATH:用戶動態(tài)庫的位置
LC_FUNCTION_STARTS:定義函數(shù)的起始地址表,使我們的調試器很容易看到地址
LC_DATA_IN_CODE:定義在代碼段內的非指令數(shù)據
下圖查看兩個類方法對應地址位置
需要注意這里使用MachOView查看應用APP文件要與手機上執(zhí)行APP為同一個包即硼,避免再次打包app導致SDK編譯進可執(zhí)行文件中位置發(fā)生變化逃片,校驗兩個地址出現(xiàn)不準確。
從上圖中我們得到APP內[CaptchaSDKCodeAddressBegin startAddress]和[CaptchaSDKCodeAddressEnd endAddress]兩個地址分別為0x1000061CC和0x10000CE28,與我們之前獲取的完全吻合只酥,而且從圖上看出SDK內其他方法都在這兩個地址范圍之內褥实。至此,我們就能通過crash時的方法地址來判斷是否是SDK內部的crash了裂允。
3.3 總結
- 動態(tài)庫有明確的起止地址损离,可以用crash方法的地址直接判斷
- 靜態(tài)庫處理
- 靜態(tài)庫中的方法會按編譯時的順序連接進App可執(zhí)行文件
- 在SDK中第一個編譯文件最前面添加一個方法,返回其自身地址绝编,作為SDK的起始地址
- 在SDK中最后一個編譯文件的最后面添加一個方法僻澎,返回其自身地址,作為SDK的所有編譯文件方法的結束地址
- crash時十饥,把獲取到的crash地址與SDK的起止地址進行比較就能知道是否是SDK內部的crash窟勃。主要比較地址,如果crash地址減去slide,則對應的SDK起止地址也應減去slide
四逗堵、獲取線程堆棧
4.1 函數(shù)調用棧(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為棧頂指針,并在引用匯編代碼時分別記為%ebp和%esp蜒秤。
不同架構的CPU汁咏,寄存器名稱被添加不同前綴以指示寄存器的大小。例如x86架構用字母“e(extended)”作名稱前綴作媚,指示寄存器大小為32位攘滩;x86_64架構用字母“r”作名稱前綴,指示各寄存器大小為64位纸泡。
圖上表示一個棧(32位CPU)漂问,這里將高地址放到下邊,這樣看起來更好理解,也更加符合[線程堆棧]信息上邊棧頂级解,底部棧底冒黑。棧分為若干棧幀(frame),每個棧幀對應一個函數(shù)調用田绑。粉色部分是func_1
函數(shù)的棧幀勤哗,它在執(zhí)行的過程中調用了func_2
函數(shù),這里func_2
的棧幀用綠色表示掩驱。
棧幀由三部分組成:函數(shù)參數(shù)芒划、局部變量及恢復前一棧幀所需的數(shù)據(如上一幀下一條執(zhí)行地址[Return Address],上一棧幀地址[EBP of Previous Stack Frame])欧穴,如上圖民逼,在調用func_2
函數(shù)時首先把函數(shù)參數(shù)入棧,隨后將前一棧幀所需數(shù)據入棧(當函數(shù)執(zhí)行完后回到哪里繼續(xù)執(zhí)行)涮帘,函數(shù)內部定義的變量則屬于第三部分入棧拼苍,當函數(shù)返回時此棧幀被從堆棧中彈出也就是出棧。
大多數(shù)操作系統(tǒng)中调缨,每個棧幀還保存了上一個棧幀的Fram Pointer,因此只要知道當前棧幀的Stack Pointer和Frame Pointer疮鲫,就能知道上一個棧幀的Stack Pointer和Frame Pointer,從而遞歸獲取棧底的幀弦叶。
4.2 Mach_Thread
iOS開發(fā)中俊犯,系統(tǒng)提供了task_threads
方法,可以獲取到所有的線程伤哺,這里的線程是最底層的mach線程燕侠,NSThread
只是對進行了封裝,可以通過設置線程名稱方式找到相應的thread_t
,之后將名稱設置回去立莉。對于每一個線程绢彤,可以用thread_get_state
方法獲取它的所有信息(棧頂指針、當前的棧幀蜓耻、上一棧幀及return address等之后可以遍歷獲取所有棧幀地址)杖虾,信息填充在_STRUCT_MCONTEXT
類型的參數(shù)中,存儲了當前線程的Stack Pointer和最頂部棧幀的Frame Pointer,遍歷從而獲取到整個線程的調用棧媒熊。
根據棧幀F(xiàn)rame Pointer后通過dladdr
獲取這個函數(shù)調用的符號名奇适,這樣就能打印出線程的所有堆棧信息。
獲取符號名步驟:
- 根據Frame Pointer找到函數(shù)調用的地址
- 找到Frame Pointer屬于哪個鏡像文件
- 找到鏡像文件的符號表
- 在符號表中找到函數(shù)調用地址對應的符號名
下面代碼可以結合[上圖]函數(shù)調用棧圖更加容易理解
4.2.1 獲取線程的信息
通過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
//根據當前 task 獲取所有線程
kern_return_t kr = task_threads(this_task, &threads, &thread_count);
通過thread_info
獲取各個線程詳細信息
//存儲 thread 信息的結構體
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();
}
// 當前線程調用棧信息
+ (NSString *)ws_backtraceOfCurrentThread {
return [self ws_backtraceOfNSThread:[NSThread currentThread]];
}
// 主線程調用棧信息
+ (NSString *)ws_backtraceOfMainThread {
return [self ws_backtraceOfNSThread:[NSThread mainThread]];
}
// 任意線程調用棧信息
+ (NSString *)ws_backtraceOfNSThread:(NSThread *)thread {
return _ws_backtraceOfThread(_ws_machThreadFromNSThread(thread));
}
// 全部線程調用棧信息
+ (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轉thread_t
thread_t _ws_machThreadFromNSThread(NSThread *nsthread) {
char name[256];
mach_msg_type_number_t count;
thread_act_array_t list;
//根據當前 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)的一個標準嚷往,特別是與 Unix 兼容的操作系統(tǒng)。np 表示與標準不兼容
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個棧幀
int i = 0;
NSMutableString *resultString = [[NSMutableString alloc] initWithFormat:@"Backtrace of Thread %u:\n", thread];
//回溯棧的算法
_STRUCT_MCONTEXT machineContext;
//通過 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,根據CPU架構選擇
int THREAD_STATE = WS_THREAD_STATE;//arm64:ARM_THREAD_STATE64, arm:ARM_THREAD_STATE, x86_64:x86_THREAD_STATE64, i386: x86_THREAD_STATE32,根據CPU架構選擇
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];
}
//通過指令指針來獲取當前指令地址
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};
//通過椘と剩基址指針獲取當前棧幀地址
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,對地址進行符號化解析
//1.找到地址所屬的內存鏡像,
//2.然后定位鏡像中的符號表
//3.最后在符號表中找到目標地址的符號
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;
//根據地址獲取是哪個 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---------*/
//根據 image 的序號獲取 mach_header
const struct mach_header* header = _dyld_get_image_header(idx);
//返回 image_index 索引的 image 的虛擬內存地址 slide 的數(shù)量贷祈,如果 image_index 超出范圍返回0
//動態(tài)鏈接器加載 image 時趋急,image 必須映射到未占用地址的進程的虛擬地址空間。動態(tài)鏈接器通過添加一個值到 image 的基地址來實現(xiàn)势誊,這個值是虛擬內存 slide 數(shù)量
const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
/*-----------ASLR 的偏移量---------*/
const uintptr_t addressWithSlide = address - imageVMAddrSlide;
//根據 Image 的 Index 來獲取 segment 的基地址
//段定義Mach-O文件中的字節(jié)范圍以及動態(tài)鏈接器加載應用程序時這些字節(jié)映射到虛擬內存中的地址和內存保護屬性呜达。 因此,段總是虛擬內存頁對齊粟耻。 片段包含零個或多個節(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;
}
//遍歷每個 segment 判斷目標地址是否落在該 segment 包含的范圍里
for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
const struct load_command* loadCmd = (struct load_command*)cmdPtr;
/*----------目標 Image 的符號表----------*/
//Segment 除了 __TEXT 和 __DATA 外還有 __LINKEDIT segment,它里面包含動態(tài)鏈接器的使用的原始數(shù)據挤忙,比如符號霜威,字符串和重定位表項。
//LC_SYMTAB 描述了 __LINKEDIT segment 內查找字符串和符號表的位置
if(loadCmd->cmd == LC_SYMTAB) {
//獲取字符串和符號表的虛擬內存偏移量册烈。
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 指向外部對象
if(symbolTable[iSym].n_value != 0) {
//給定的偏移量是文件偏移量,減去 __LINKEDIT segment 的文件偏移量獲得字符串和符號表的虛擬內存偏移量
uintptr_t symbolBase = symbolTable[iSym].n_value;
uintptr_t currentDistance = addressWithSlide - symbolBase;
//尋找最小的距離 bestDistance赏僧,因為 addressWithSlide 是某個方法的指令地址大猛,要大于這個方法的入口。
//離 addressWithSlide 越近的函數(shù)入口越匹配
if((addressWithSlide >= symbolBase) &&
(currentDistance <= bestDistance)) {
bestMatch = symbolTable + iSym;
bestDistance = currentDistance;
}
}
}
if(bestMatch != NULL) {
//將虛擬內存偏移量添加到 __LINKEDIT segment 的虛擬內存地址可以提供字符串和符號表的內存 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 的已經被處理好了
// 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;
}
//通過 address 找到對應的 image 的游標胎署,從而能夠得到 image 的更多信息
uint32_t _ws_imageIndexContainingAddress(const uintptr_t address) {
//返回當前 image 數(shù),這里 image 不是線程安全的窑滞,因為另一個線程可能正處在添加或者刪除 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++) {
//返回一個指向由 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 時判斷 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;
}