iOS中相关Crash知识点

我只是代码的搬运工


crash文件获取方式

  1. 应用集成第三方的crash SDK,自动采集相关运行堆栈,发送到服务器上,开发者上传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)、线程状态(Thread State)、库信息(Binary Images)这个六个部分。

CrashLogFormat.jpg
  1. 头部(Header):硬件型号,系统版本,进程名称、id,bundleid,崩溃时间,crash日志报告格式版本号等信息。

    • Incident Identifier: 事件标识符,每个crash文件对应一个唯一的标识符
    • CrashReporter Key: 匿名设备标识符
    • Hardware Model: 设备型号
    • Process: 进程名
    • Identifier: app Identifier
    • Exception Type: 异常类型
    • Exception Codes: 异常代码
    • Termination Reason: 进程被结束的原因
  2. 异常信息(Exception Information): 崩溃类型、崩溃代码及触发崩溃的线程等信息。

    • Exception Codes: 处理器的具体信息有关的异常编码成一个或多个64位进制数。通常情况下,这个区域不会被呈现,因为将异常代码解析成人们可以看懂的描述是在其它区域进行的。
    • Exception Subtype: 供人们可读的异常代码的名字。
    • Exception Message: 从异常代码中提取的额外的可供人们阅读的信息。
    • Exception Note: 不是特定于一个异常类型的额外信息.如果这个区域包含SIMULATED (这不是一个崩溃)然后进程没有崩溃,但是被watchdog杀掉了。
    • Termination Reason: 当一个进程被终止的时的原因。
    • Triggered by Thread: 异常所在的线程。
  3. 诊断信息(Additional Diagnostic Information): 非常简略的诊断信息。不是每个崩溃都会有诊断信息。

  4. 线程堆栈(Backtraces): 崩溃发生时,各个线程的方法调用栈的详细信息。触发崩溃的线程会被标记上Crashed。

    • 第一列的数字表示对应线程的调用栈顺序
    • 第二列表示对应的镜像文件名称,如系统corefoundtion
    • 第三列表示当前行对应的符号地址
    • 第四列如果已经符号化过则表示对应的符号,反之为镜像的起始地址+文件偏移地址,这2个值相加实际上就是第三列的地址
  5. 线程状态(Thread State): 崩溃时寄存器的状态。

  6. 库信息(Binary Images): 加载的动态库信息。

查看crash日志时,首先会在【异常信息(Exception Information)】中通过“Triggered by Thread”的字段判断是哪个线程发生了crash。在【线程堆栈(Backtraces)】信息中,会看到各个线程号,而崩溃发生的线程号下面会有“Thread xx Crashed”标记该线程发生了crash。

在【线程堆栈(Backtraces)】信息中,有方法编号,方法所属模块名,方法地址,方法符号信息或者方法所在的段地址及偏移量。每个方法的地址是包含在所属模块的地址范围内,比如GrowSDKDemo模块(0x10049400 - 0x100887fff)。

CrashLogAnalyze.jpg

图中显示,0号线程(com.apple.main-thread 即主线程)发生了crash,地址是0x00000001004b6508,在GrowSDKDemo这个模块内。在【库信息(Binary Images)】信息中可以找到这个二进制模块,也就是App可执行文件GrowSDKDemo,和其他的模块是第三方动态库(YLDataSDK)、系统加载的动态库(UIKit、CoreFoundation等)。

二、Exception类型

2.1 Bad Memory Access — EXC_BAD_ACCESS (SIGSEGV、SIGBUS)

当进程尝试的去访问一个不可用或者不允许访问的内存空间时,会发生野指针异常, Exception Subtype 中包含了错误的描述及想要访问的地址。
SIGSEGV在ARC以后应该很少见到,SIGBUS为总线错误,与SIGSEGV访问的是无效地址,而SIGBUS访问的是有效地址,但总线访问异常(如地址对齐问题)

  • 如果 obj_msgSendobjc_release 符号信息位于crash线程的最上面,说明该进程可能尝试访问已经释放的对象。开启僵尸模式进行调试、对程序做Analyze分析。
  • 如果 gpus_ReturnNotPermittedKillClient 符号在crash线程的最上面,说明进程试图在后台使用OpenGL ES或Metal进行渲染,而被杀掉。
  • 在debug模式下打开 Address Sanitizer,它会在编译的时候自动添加一些关于内存访问的工具,在crash发生的时候,Xcode会给出对应的详细信息。

2.2 Abnormal Exit(异常退出) — EXC_CRASH (SIGABRT)

非正常退出,大多数发生这种crash的原因是因为未捕获的Objective-C/C++异常,导致进程调用 abort() 方法退出。
中止当前进程,返回一个错误代码。该函数产生SIGABRT信号并发送给自己。实际就是系统发现操作异常,调用发送SIGABRT(abort()函数)信号,杀死进程。
此异常,系统会知道程序在什么地方有不合法的操作,会在控制台输出异常信息。例如:向一个对象发送它无法识别的消息

[self performSelector:@selector(notExistFunc:)];

2.3 Killed — EXC_CRASH (SIGKILL)

进程被系统强制结束,通过查看Termination Reason找到crash信息
如果APP消耗了太多的时间在初始化(在20s内没有启动), watchdog (时间狗)就会终止程序运行。如以下代码

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    ...
    sleep(20);
    return YES;
}

2.4 Trace Trap(追踪捕获) — EXC_BREAKPOINT (SIGTRAP)

这个异常是为了让一个附加的调试器有机会在执行过程中的某个特定时刻中断进程,我们可以在代码里面添加 __builtin_trap() 方法手动触发这个异常。如果没有附加调试器,则会生成crash文件。
较低级别的库(例如libdispatch)会在遇到致命错误时捕获该进程
swift代码在遇到下面2种情况的时候也会抛出这个异常:

  • 一个非可选类型的值为nil
  • 错误的强制类型转换,如把NSString转换为NSDate等等

2.5 Illegal Instruction(非法指令) — EXC_BAD_INSTRUCTION (SIGILL)

程序尝试的去执行一个非法的或者没有定义的指令。如果配置的函数地址有问题,进程在执行函数跳转的时候,就会发生这个crash。

2.6 Quit — SIGQUIT 跨进程相关

该进程在具有管理其生命周期的权限的另一进程的请求下终止。 SIGQUIT并不意味着进程崩溃,但是可以说明该进程存在一些问题。
比如在iOS中,第三方键盘应用可能在在其他APP中被唤起,但是如果键盘应用需要很长的时间去加载,则会被强制退出。

Xcode连机调试下时间狗不起作用,只有安装后脱离Xcode,手动点击运行会出现退出。

2.7 Resource Limit — EXC_RESOURCE

进程使用的资源超出的限制。这是一个从操作系统通知,进程是使用太多的资源。这虽然不是崩溃但也会生成崩溃日志。

2.8 Other

  • 0xbaaaaaad: 该code表示这个crash文件是系统的stackshot,该Crash log并非一个真正的Crash,它仅仅只是包含了整个系统某一时刻的运行状态。通常可以通过同时按Home键和音量键,可能由于用户不小心触发。
  • 0x8badf00d: 读做 “ate bad food”! (把数字换成字母,是不是很像 :p)该编码表示应用是因为发生watchdog超时而被iOS终止的。 通常是应用花费太多时间而无法启动、终止或响应用系统事件。
  • 0xbad22222: 该编码表示 VoIP 应用因为过于频繁重启而被终止。
  • 0xdead10cc: 读做 “dead lock”!该代码表明应用因为在后台运行时占用系统资源,如通讯录数据库不释放而被终止。
  • 0xdeadfa11: 读做 “dead fall”! 该代码表示应用是被用户强制退出的。根据苹果文档, 强制退出发生在用户长按开关按钮直到出现 “滑动来关机”, 然后长按 Home按钮。强制退出将产生 包含0xdeadfa11 异常编码的崩溃日志, 因为大多数是强制退出是因为应用阻塞了界面。
  • 0xc00010ff: 当操作系统响应thermal事件的时候,会强制的kill进程,如程序执行大量耗费CPU和GPU的运算,导致设备过热,触发系统过热保护被系统终止

三、SDK开发者,定位crash是由APP代码还是SDK代码导致

在开发iOS平台上的SDK,提供给开发者使用。为了监控定位SDK自身引起的崩溃,及统计崩溃率,我们需在SDK中加入抓取crash的功能。但是收集到的日志都是开发者应用的crash(此crash有可能是开发者自己代码引起,也有可能是第三方静态库引起),由于接入SDK的应用数量很多,产生的崩溃日志量非常庞大,靠人力从海量的日志中筛选出我们SDK的crash日志非常困难。

问题:如何区分SDK内部的crash和App的crash?

3.1 确定动态库crash

我们知道动态库的crash的方法栈中是带动态库的名字的,能直接看出是哪个模块发生了crash。crash日志,区分App的crash和引入的动态库SDK的crash比较简单,App运行时crash后,可以通过crash的地址,找到包含这个地址的二进制模块(地址范围)就能定位到。如 crash日志格式中 库信息块除第一个应用模块其他都是动态库,且前面都带有地址范围。

3.2 确定静态库的crash

通过crash的地址可以找到该方法所属的二进制模块。如果SDK是静态库,引入到应用工程中,其代码会被加入到App的代码段中,SDK的代码和App自身代码属于同一个二进制模块,这样就不容易判断了。在调用SDK中方法时出现crash,其在crash文件异常堆栈中定位到的模块属于应用可执行模块,即App的代码段,这样就不知道是App还是SDK内部crash了。

解决方案:通过符号来判断、通过地址来判断

3.2.1 通过符号来判断

即服务端收集crash日志后,通过符号文件解析出堆栈信息,然后通过crash符号类名+方法来判断是app的crash还是sdk内部的crash。
人为干预查看crash日志识别
1、符号化服务端crash日志
2、收集SDK中所有的特征符号,类名、方法名
此方式费时费力

3.2.2 通过地址来判断

获取crash发生的地址,通过地址来判断是否在SDK内部,就像上面动态库一样,只要确定静态库地址范围。问题变成了如何获取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系统引入了ASLR机制,即Address space layout randomization。在App运行时,iOS系统会给加载进内存的二进制模块一个随机的偏移地址,我们只需要把运行时的地址减掉这个偏移地址即可,上面获取偏移值方法:getExecuteImageSlide,原理:遍历加载的镜像列表(应用的可执行二进制文件、动态库等),搜索出可执行文件类型来获取偏移值。如下图Demo集成SDK,目的是获取进行ASLR之前sdk地址(这个地址同一个可执行二进制文件是固定的)。

ASLR_IN_APP.png

上图中slide值为随机的偏移值,进程每一次启动,地址空间都会简单地随机化偏移(安全性考虑,每一次启动的虚拟内存镜像都是一致,黑客容易采取重写内存的方式破解程序)。
我们最终拿到了两个地址分别是0x1000061cc和0x10000ce28,那么我们如何校验呢?
这里使用到了MachOView工具进行分析(开源地址):获取SDK模块代码在App二进制文件中的地址。

静态库SDK二进制文件结构

MachOView_SDK.png

(图中FatFile/FatBinary,就是一个由不同的编译架构后的Mach-O产物所合成的集合体)
上图SDK包含的object文件内容,为编译后的产物。它的顺序是由xcode工程中【Targets->Build Phases->Complie Sources】编译顺序决定,如下图

M_SDK.png

SDK的文件相当于一个object文件的容器,把源文件的编译产物按顺序打包组织在一起就是SDK的二进制文件了。SDK二进制连接进App可执行文件是怎么样的,如下图
应用二进制文件结构简单描述:

MachOView_APP.png

Load Commands
LC_SEGMENT_64:将可执行文件(64位)映射到进程地址空间,32位系统的是LC_SEGMENT,是加载的主要命令,负责指导内核来设置进程的内存空间
LC_DYLD_INFO_ONLY:动态链接相关信息
LC_SYMTAB:符号表地址
LC_DYSYMTAB:动态符号地址表
LC_LOAD_DYLINKER:加载一个动态链接器,路径“/usr/lib/dyld”
LC_UUID:二进制文件的标识ID,dSYM文件、crash中都存在这个值,确定两个文件是否匹配,分析出对应的崩溃位置
LC_VERSION_MIN_MACOSX:二进制文件要求的最低系统版本,和xcode中配置的target有关
LC_MAIN:设置程序的入口,编译型的语言需要指定入口地址,解释器语言对此没有邀请
LC_SOURCE_VERSION:构建二进制文件使用的源代码版本
LC_LOAD_DYLIB(...):加载一个动态链接共享库,"..." 如Foundation、libobjc.A.dylib等系统动态库。
LC_RPATH:用户动态库的位置
LC_FUNCTION_STARTS:定义函数的起始地址表,使我们的调试器很容易看到地址
LC_DATA_IN_CODE:定义在代码段内的非指令数据

下图查看两个类方法对应地址位置

需要注意这里使用MachOView查看应用APP文件要与手机上执行APP为同一个包,避免再次打包app导致SDK编译进可执行文件中位置发生变化,校验两个地址出现不准确。

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. 动态库有明确的起止地址,可以用crash方法的地址直接判断
  2. 静态库处理
    • 静态库中的方法会按编译时的顺序连接进App可执行文件
    • 在SDK中第一个编译文件最前面添加一个方法,返回其自身地址,作为SDK的起始地址
    • 在SDK中最后一个编译文件的最后面添加一个方法,返回其自身地址,作为SDK的所有编译文件方法的结束地址
    • crash时,把获取到的crash地址与SDK的起止地址进行比较就能知道是否是SDK内部的crash。主要比较地址,如果crash地址减去slide,则对应的SDK起止地址也应减去slide

四、获取线程堆栈

4.1 函数调用栈(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),每个栈帧对应一个函数调用。粉色部分是func_1函数的栈帧,它在执行的过程中调用了func_2函数,这里func_2的栈帧用绿色表示。

栈帧由三部分组成:函数参数、局部变量及恢复前一栈帧所需的数据(如上一帧下一条执行地址[Return Address],上一栈帧地址[EBP of Previous Stack Frame]),如上图,在调用func_2函数时首先把函数参数入栈,随后将前一栈帧所需数据入栈(当函数执行完后回到哪里继续执行),函数内部定义的变量则属于第三部分入栈,当函数返回时此栈帧被从堆栈中弹出也就是出栈。

大多数操作系统中,每个栈帧还保存了上一个栈帧的Fram Pointer,因此只要知道当前栈帧的Stack Pointer和Frame Pointer,就能知道上一个栈帧的Stack Pointer和Frame Pointer,从而递归获取栈底的帧。

4.2 Mach_Thread

iOS开发中,系统提供了task_threads方法,可以获取到所有的线程,这里的线程是最底层的mach线程,NSThread只是对进行了封装,可以通过设置线程名称方式找到相应的thread_t,之后将名称设置回去。对于每一个线程,可以用thread_get_state方法获取它的所有信息(栈顶指针、当前的栈帧、上一栈帧及return address等之后可以遍历获取所有栈帧地址),信息填充在_STRUCT_MCONTEXT类型的参数中,存储了当前线程的Stack Pointer和最顶部栈帧的Frame Pointer,遍历从而获取到整个线程的调用栈。

根据栈帧Frame Pointer后通过dladdr获取这个函数调用的符号名,这样就能打印出线程的所有堆栈信息。
获取符号名步骤:

  • 根据Frame Pointer找到函数调用的地址
  • 找到Frame Pointer属于哪个镜像文件
  • 找到镜像文件的符号表
  • 在符号表中找到函数调用地址对应的符号名

下面代码可以结合[上图]函数调用栈图更加容易理解

4.2.1 获取线程的信息

通过task_threads获取所有的线程

thread_act_array_t threads; //int 组成的数组
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 是指操作系统的一个标准,特别是与 Unix 兼容的操作系统。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 状态信息
    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 的数量,如果 image_index 超出范围返回0
    //动态链接器加载 image 时,image 必须映射到未占用地址的进程的虚拟地址空间。动态链接器通过添加一个值到 image 的基地址来实现,这个值是虚拟内存 slide 数量
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
    /*-----------ASLR 的偏移量---------*/
    const uintptr_t addressWithSlide = address - imageVMAddrSlide;
    //根据 Image 的 Index 来获取 segment 的基地址
    //段定义Mach-O文件中的字节范围以及动态链接器加载应用程序时这些字节映射到虚拟内存中的地址和内存保护属性。 因此,段总是虚拟内存页对齐。 片段包含零个或多个节。
    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,它里面包含动态链接器的使用的原始数据,比如符号,字符串和重定位表项。
        //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 越近的函数入口越匹配
                    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 数,这里 image 不是线程安全的,因为另一个线程可能正处在添加或者删除 image 期间
    const uint32_t imageCount = _dyld_image_count();
    const struct mach_header* header = 0;
    
    //O(n2)的方式查找,考虑优化
    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位的,大部分系统的 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 查找某字符在字符串中最后一次出现的位置
    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;
}


五、参考地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344