iOS中相關Crash知識點

我只是代碼的搬運工


crash文件獲取方式

  1. 應用集成第三方的crash SDK洞焙,自動采集相關運行堆棧吕朵,發(fā)送到服務器上绰垂,開發(fā)者上傳dSYM文件進行解析嗅榕,得到符號化的堆棧信息顺饮。
  2. 某設備上的crash可以通過Xcode導出crash文件查看崩潰日志,Xcode -> window -> Devices and Simulators -> 選擇設備 -View Device Logs凌那。
  3. 自己應用加入中收集異常代碼
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)這個六個部分佃乘。

CrashLogFormat.jpg
  1. 頭部(Header):硬件型號囱井,系統(tǒng)版本,進程名稱趣避、id庞呕,bundleid,崩潰時間鹅巍,crash日志報告格式版本號等信息千扶。

    • Incident Identifier: 事件標識符,每個crash文件對應一個唯一的標識符
    • CrashReporter Key: 匿名設備標識符
    • Hardware Model: 設備型號
    • Process: 進程名
    • Identifier: app Identifier
    • Exception Type: 異常類型
    • Exception Codes: 異常代碼
    • Termination Reason: 進程被結束的原因
  2. 異常信息(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: 異常所在的線程。
  3. 診斷信息(Additional Diagnostic Information): 非常簡略的診斷信息蹦掐。不是每個崩潰都會有診斷信息技羔。

  4. 線程堆棧(Backtraces): 崩潰發(fā)生時,各個線程的方法調用棧的詳細信息卧抗。觸發(fā)崩潰的線程會被標記上Crashed藤滥。

    • 第一列的數(shù)字表示對應線程的調用棧順序
    • 第二列表示對應的鏡像文件名稱,如系統(tǒng)corefoundtion
    • 第三列表示當前行對應的符號地址
    • 第四列如果已經符號化過則表示對應的符號社裆,反之為鏡像的起始地址+文件偏移地址拙绊,這2個值相加實際上就是第三列的地址
  5. 線程狀態(tài)(Thread State): 崩潰時寄存器的狀態(tài)。

  6. 庫信息(Binary Images): 加載的動態(tài)庫信息泳秀。

查看crash日志時标沪,首先會在【異常信息(Exception Information)】中通過“Triggered by Thread”的字段判斷是哪個線程發(fā)生了crash。在【線程堆棧(Backtraces)】信息中嗜傅,會看到各個線程號金句,而崩潰發(fā)生的線程號下面會有“Thread xx Crashed”標記該線程發(fā)生了crash。

在【線程堆棧(Backtraces)】信息中磺陡,有方法編號趴梢,方法所屬模塊名漠畜,方法地址,方法符號信息或者方法所在的段地址及偏移量坞靶。每個方法的地址是包含在所屬模塊的地址范圍內,比如GrowSDKDemo模塊(0x10049400 - 0x100887fff)憔狞。

CrashLogAnalyze.jpg

圖中顯示,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_msgSendobjc_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中編譯順序

SDK_Address.png

應用運行的時候我們可以通過代碼來獲取當前[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í)行二進制文件是固定的)潦刃。

ASLR_IN_APP.png

上圖中slide值為隨機的偏移值,進程每一次啟動懈叹,地址空間都會簡單地隨機化偏移(安全性考慮乖杠,每一次啟動的虛擬內存鏡像都是一致,黑客容易采取重寫內存的方式破解程序)澄成。
我們最終拿到了兩個地址分別是0x1000061cc和0x10000ce28,那么我們如何校驗呢胧洒?
這里使用到了MachOView工具進行分析(開源地址):獲取SDK模塊代碼在App二進制文件中的地址。

靜態(tài)庫SDK二進制文件結構

MachOView_SDK.png

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

M_SDK.png

SDK的文件相當于一個object文件的容器肾砂,把源文件的編譯產物按順序打包組織在一起就是SDK的二進制文件了列赎。SDK二進制連接進App可執(zhí)行文件是怎么樣的,如下圖
應用二進制文件結構簡單描述:

MachOView_APP.png

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)不準確。

MachOView_SDK_IN_APP_01.png

MachOView_SDK_IN_APP_02.png

從上圖中我們得到APP內[CaptchaSDKCodeAddressBegin startAddress]和[CaptchaSDKCodeAddressEnd endAddress]兩個地址分別為0x1000061CC和0x10000CE28,與我們之前獲取的完全吻合只酥,而且從圖上看出SDK內其他方法都在這兩個地址范圍之內褥实。至此,我們就能通過crash時的方法地址來判斷是否是SDK內部的crash了裂允。

3.3 總結

  1. 動態(tài)庫有明確的起止地址损离,可以用crash方法的地址直接判斷
  2. 靜態(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);
}
CallStack.png

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;
}


五、參考地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末此改,一起剝皮案震驚了整個濱河市趾撵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌共啃,老刑警劉巖占调,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異移剪,居然都是意外死亡究珊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門纵苛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來剿涮,“玉大人言津,你說我怎么就攤上這事∪∈裕” “怎么了悬槽?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瞬浓。 經常有香客問我初婆,道長,這世上最難降的妖魔是什么瑟蜈? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任烟逊,我火速辦了婚禮渣窜,結果婚禮上铺根,老公的妹妹穿的比我還像新娘。我一直安慰自己乔宿,他們只是感情好位迂,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著详瑞,像睡著了一般掂林。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坝橡,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天泻帮,我揣著相機與錄音,去河邊找鬼计寇。 笑死锣杂,一個胖子當著我的面吹牛,可吹牛的內容都是我干的番宁。 我是一名探鬼主播元莫,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蝶押!你這毒婦竟也來了踱蠢?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤棋电,失蹤者是張志新(化名)和其女友劉穎茎截,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赶盔,經...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡企锌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了招刨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片霎俩。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡哀军,死狀恐怖,靈堂內的尸體忽然破棺而出打却,到底是詐尸還是另有隱情杉适,我是刑警寧澤,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布柳击,位于F島的核電站猿推,受9級特大地震影響,放射性物質發(fā)生泄漏捌肴。R本人自食惡果不足惜蹬叭,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望状知。 院中可真熱鬧秽五,春花似錦、人聲如沸饥悴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽西设。三九已至瓣铣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贷揽,已是汗流浹背棠笑。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留禽绪,地道東北人蓖救。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像丐一,于是被迫代替她去往敵國和親藻糖。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351