KSCrash源码分析

0x01 安装过程

1.1 抛砖引玉

KSCrashInstallationStandard* installation = [KSCrashInstallationStandard sharedInstance];
installation.url = [NSURL URLWithString:@"http://put.your.url.here"];
[installation install];

以上代码是KSCrash的安装代码,[KSCrashInstallationStandard init]底层对自身的属性进行赋值,然后,进入[KSCrashInstallationStandard install],这里会调用[KSCrash init]方法来设置要初始化的Crash监控器,然后调用[KSCrash install],这里会根据前面的设置好要启动的监控器来启动哪些监控器。

- (void) install
{
    KSCrash* handler = [KSCrash sharedInstance];
    @synchronized(handler)
    {
        g_crashHandlerData = self.crashHandlerData;
        handler.onCrash = crashCallback;
        [handler install];
    }
}	

在安装监控器的前面,还设置了crash的回调(onCrash),最终,会调用void kscm_setActiveMonitors(KSCrashMonitorType monitorTypes)来启动监控器,monitorTypes就是我们一开始设定好的g_monitoring,在KSCrashMonitor里面,设定好了监控器数组g_monitors,通过数组里面的monitorType来跟我们设定好的g_monitors进行运算,运算出的结果来决定我们是否安装该monitorType对应的监控器,下面,分析各个监控器的安装过程。

1.2 Mach kernel exceptions

Mach内核异常,由枚举KSCrashMonitorTypeMachException表示,由KSCrashMonitor_MachException来实现相关方法,如果添加至全局枚举g_monitoring里面,则需要开启,通过通用Apistatic void setEnabled(bool isEnabled)最后调用installExceptionHandler

static bool installExceptionHandler()
{
    ...

//    备份异常端口
    KSLOG_DEBUG("Backing up original exception ports.");
    kr = task_get_exception_ports(thisTask,
                                  mask,
                                  g_previousExceptionPorts.masks,
                                  &g_previousExceptionPorts.count,
                                  g_previousExceptionPorts.ports,
                                  g_previousExceptionPorts.behaviors,
                                  g_previousExceptionPorts.flavors);
    
    if(kr != KERN_SUCCESS)
    {
        KSLOG_ERROR("task_get_exception_ports: %s", mach_error_string(kr));
        goto failed;
    }

    if(g_exceptionPort == MACH_PORT_NULL)
    {
//          分配新端口并赋予接收权限
        KSLOG_DEBUG("Allocating new port with receive rights.");
        kr = mach_port_allocate(thisTask,
                                MACH_PORT_RIGHT_RECEIVE,
                                &g_exceptionPort);
        if(kr != KERN_SUCCESS)
        {
            KSLOG_ERROR("mach_port_allocate: %s", mach_error_string(kr));
            goto failed;
        }
        
//          给端口添加发送权限
        KSLOG_DEBUG("Adding send rights to port.");
        kr = mach_port_insert_right(thisTask,
                                    g_exceptionPort,
                                    g_exceptionPort,
                                    MACH_MSG_TYPE_MAKE_SEND);
        if(kr != KERN_SUCCESS)
        {
            KSLOG_ERROR("mach_port_insert_right: %s", mach_error_string(kr));
            goto failed;
        }
    }
// 将端口设置为接受异常的端口
    KSLOG_DEBUG("Installing port as exception handler.");
    kr = task_set_exception_ports(thisTask,
                                  mask,
                                  g_exceptionPort,
                                  EXCEPTION_DEFAULT,
                                  THREAD_STATE_NONE);
    if(kr != KERN_SUCCESS)
    {
        KSLOG_ERROR("task_set_exception_ports: %s", mach_error_string(kr));
        goto failed;
    }

//     创建辅助异常线程
    KSLOG_DEBUG("Creating secondary exception thread (suspended).");
//    创建一个分离线程
    pthread_attr_init(&attr);
    attributes_created = true;
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    error = pthread_create(&g_secondaryPThread,
                           &attr,
                           &handleExceptions,
                           kThreadSecondary);
    if(error != 0)
    {
        KSLOG_ERROR("pthread_create_suspended_np: %s", strerror(error));
        goto failed;
    }
//     获取分离线程的线程id
    g_secondaryMachThread = pthread_mach_thread_np(g_secondaryPThread);
    ksmc_addReservedThread(g_secondaryMachThread);

//    创建主要异常线程。
    KSLOG_DEBUG("Creating primary exception thread.");
    error = pthread_create(&g_primaryPThread,
                           &attr,
                           &handleExceptions,
                           kThreadPrimary);
    if(error != 0)
    {
        KSLOG_ERROR("pthread_create: %s", strerror(error));
        goto failed;
    }
    pthread_attr_destroy(&attr);
    g_primaryMachThread = pthread_mach_thread_np(g_primaryPThread);
    ksmc_addReservedThread(g_primaryMachThread);

    KSLOG_DEBUG("Mach exception handler installed.");
    return true;

···
}		

然后,使用分离线程,执行以下方法

static void* handleExceptions(void* const userData)
{
    ...
    for(;;)
    {
        KSLOG_DEBUG("Waiting for mach exception");

        // 等待异常触发
        kern_return_t kr = mach_msg(&exceptionMessage.header,
                                    MACH_RCV_MSG,
                                    0,
                                    sizeof(exceptionMessage),
                                    g_exceptionPort,
                                    MACH_MSG_TIMEOUT_NONE,
                                    MACH_PORT_NULL);
        if(kr == KERN_SUCCESS)
        {
            break;
        }

        // Loop and try again on failure.
        KSLOG_ERROR("mach_msg: %s", mach_error_string(kr));
    }

   ...
}

1.3 Fatal signals

Fatal signals在代码中,以KSCrashMonitorTypeSignal代表是否开启,在KSCrashMonitor_Signal里面实现相关实现,通用通过通用APIstatic void setEnabled(bool isEnabled),最后调用的是installSignalHandler

1.4 C++ exceptions

在代码里面,以枚举KSCrashMonitorTypeCPPException代表C++异常,在类KSCrashMonitor_CPPException里面实现相关代码。在通用APIstatic void setEnabled(bool isEnabled)里面通过std::set_terminate(terminate_handler)设置回调函数。

1.5 Objective-C exceptions

在代码里面,以KSCrashMonitorTypeNSException表示枚举值,在KSCrashMonitor_NSException里面实现具体代码,在通用APIstatic void setEnabled(bool isEnabled)里面通过void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable)设置回调

1.6 Main thread deadlock

Main thread deadlock是监控主线程死锁异常,在代码里面以枚举KSCrashMonitorTypeMainThreadDeadlock表示,在类KSCrashMonitor_Deadlock里面实现相关代码,在static void setEnabled(bool isEnabled)里面通过调用KSCrashDeadlockMonitor的初始化来安装死锁监控。

1.7 Custom crashes

自定义Crash,在代码里面用KSCrashMonitorTypeUserReported表示,在类KSCrashMonitor_User作具体实现,这个可以自定义crash,跟上面不一样的是,自定义的crash没有标准的crash时机,需要自己定义,也就是,遇到某些特发情况,可以收集信息,然后塞进KSCrashMonitor_User这里面,做后续处理。

0x02 运行过程

2.1 捕获

2.1.1 Mach kernel exceptions

在Mach中,异常是通过内核的基础设施——消息传递机制处理的。异常由出错的线程或任务(通过msg_send())抛出,然后由一个处理程序(msg_recv())捕捉。处理程序可以处理异常,也可以清除异常(即将异常标记为完成并继续),还可以决定终止线程。


Mach异常处理模型和其他的异常处理模型不同,其他模型的异常处理程序运行在出错的线程的上下文中,而Mach的异常处理程序在不同的上下文运行异常处理程序,出错的线程向预先制定好的异常端口发送消息,然后等待应答。每一个任务都可以注册一个异常端口,这个异常端口会对同一个任务中的所有线程起效。此外,单个线程还可以通过thread_set_exception_prots注册自己的异常端口。


所以Mach kernel exceptions中,使用mach_task_self获取当前任务进程,因为Mach异常其实是一个消息转发的异常,所以需要消息接收权限,在初始化异常端口的时候就赋予了mach_port_allocate(thisTask,MACH_PORT_RIGHT_RECEIVE,&g_exceptionPort,然而后面还赋予改端口的MACH_MSG_TYPE_MAKE_SEND,这是后面的task_set_exception_ports要求这个权限,然后task_set_exception_ports将这个端口设置为目标任务的异常端口。


至此,已经捕获了异常,但是没有做任何处理,为此,需要使用mach_msg在异常端口创建一个活动监听者。异常处理可以有同一个程序的另外一个线程来完成。也可以有另外一个程序实现异常处理。launched注册的进程的异常端口就是这么做。所以在后面,分别起了两个分离线程g_secondaryMachThreadg_primaryPThread来等待异常触发。

for(;;)
    {
        KSLOG_DEBUG("Waiting for mach exception");
	// 消息循环,一直阻塞,直到收到一条消息,而且必须是一条异常消息。
    // 其他消息也不会到达异常端口。
        kern_return_t kr = mach_msg(&exceptionMessage.header,
                                    MACH_RCV_MSG,
                                    0,
                                    sizeof(exceptionMessage),
                                    g_exceptionPort,
                                    MACH_MSG_TIMEOUT_NONE,
                                    MACH_PORT_NULL);
        if(kr == KERN_SUCCESS)
        {
            break;
        }

        // Loop and try again on failure.
        KSLOG_ERROR("mach_msg: %s", mach_error_string(kr));
    }

Mach的等待异常的过程如下图:

37E95EFEF7A37779DA43AA40BC62D132.jpg

2.1.2 Fatal signals

Mach已经通过异常机制提供了底层的陷阱处理,而BSD则在异常机制之上构建了信号处理机制。硬件产生的信号被Mach捕捉,然后转换为对应的UNIX信号。为了维护一个统一的机制,操作系统和用户尝试的信号首先被转换为Mach异常,然后再转换为信号(Signals),如下图所示:

7A6E63A9B9ED07F096D6EDB504F189C7.jpg

可以看出,跟我们自定义的Mach异常捕获不一样的是在于捕获到Mach异常的处理上。


当BSD进程(用户态进程)被bsdinit_task()函数启动时,会设置一个名为ux_handle的Mach内核线程。而ux_handle,与上面的Mach异常捕捉基本类似,只是他处理的是讲Mach异常转换为信号。


硬件产生的信号始于处理器陷阱。处理器陷阱与平台有关。ux_exception负责将陷阱转换为信号。为了处理机器相关的情况,ux_exception会调用machine_exception首先尝试处理机器陷阱。如果这个函数无法转换信号,ux_exception则处理一般情况。


如果信号不是由硬件产生的,那么这个信号来源于两个API调用:kill或pthread_kill。这两个函数分别向进程发送信号。


综上,信号可以看做是对硬件异常跟软件异常的封装。 硬件软件的错误对应了相应的信号,在KSCrash中,对一下信号进行了注册回调。

static const int g_fatalSignals[] =
{
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGPIPE,
    SIGSEGV,
    SIGSYS,
    SIGTRAP,
};

2.1.3 C++ exceptions

C++ exceptions使用系统封装好的函数std::set_terminate(CPPExceptionTerminate)来设置回调。

2.1.4 Objective-C exceptions

Objective-C exceptions使用了NSSetUncaughtExceptionHandler系统回调来调用。 需要注意的是,这里可能会出现覆盖注册的问题。

2.1.5 Main thread deadlock

首先,在子线程执行runMonitor方法,然后执行watchdogPulse方法,在watchdogPulse里面通过dispatch_async到主线程来复位标志位,来鉴别是否发生死锁。

2.2 记录

2.2.1 流程

采集:数据的采集,从Crash发生就开始,主要是采集系统信息,以及crash信息,crash的信息主要是包括堆栈地址,原因等。 流程:分别依次从reportbinary_imagesprocesssystemcrash这些字段的流程来记录,下面,详细说下符号还原。

符号采集

NSException中,直接获取callStackReturnAddresses地址堆栈。 在Mach异常中,堆栈来源是寄存器的地址,首先会获取当前pc寄存器的值地址,然后符号还原,然后再获取lr指针,然后进行符号还原,然后再获取当前的fp指针,符号还原,然后不断的重复递归fp指针,还原符号的操作,知道递归到当前地址为0或者前置帧为空 Signal异常中,堆栈获取跟Mach相同。

符号还原

符号还原的对象是地址,符号还原的核心代码在下面:首先,通过imageIndexContainingAddress方法来获取当前的传入地址所在的Image的index,怎么获取Image的index?

bool ksdl_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;

    const uint32_t idx = imageIndexContainingAddress(address);
    if(idx == UINT_MAX)
    {
        return false;
    }
    const struct mach_header* header = _dyld_get_image_header(idx);
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
    const uintptr_t addressWithSlide = address - imageVMAddrSlide;
    const uintptr_t segmentBase = segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
    if(segmentBase == 0)
    {
        return false;
    }

    info->dli_fname = _dyld_get_image_name(idx);
    info->dli_fbase = (void*)header;

    // Find symbol tables and get whichever symbol is closest to the address.
    const STRUCT_NLIST* bestMatch = NULL;
    uintptr_t bestDistance = ULONG_MAX;
    uintptr_t cmdPtr = firstCmdAfterHeader(header);
    if(cmdPtr == 0)
    {
        return false;
    }
    for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++)
    {
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        if(loadCmd->cmd == LC_SYMTAB)
        {
            const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;
            const STRUCT_NLIST* symbolTable = (STRUCT_NLIST*)(segmentBase + symtabCmd->symoff);
            const uintptr_t stringTable = segmentBase + symtabCmd->stroff;

            for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++)
            {
                // If n_value is 0, the symbol refers to an external object.
                if(symbolTable[iSym].n_value != 0)
                {
                    uintptr_t symbolBase = symbolTable[iSym].n_value;
                    uintptr_t currentDistance = addressWithSlide - symbolBase;
                    if((addressWithSlide >= symbolBase) &&
                       (currentDistance <= bestDistance))
                    {
                        bestMatch = symbolTable + iSym;
                        bestDistance = currentDistance;
                    }
                }
            }
            if(bestMatch != NULL)
            {
                info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
                if(bestMatch->n_desc == 16)
                {
                    // This image has been stripped. The name is meaningless, and
                    // almost certainly resolves to "_mh_execute_header"
                    info->dli_sname = NULL;
                }
                else
                {
                    info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
                    if(*info->dli_sname == '_')
                    {
                        info->dli_sname++;
                    }
                }
                break;
            }
        }
        cmdPtr += loadCmd->cmdsize;
    }
    
    return true;
}

首先,通过传入地址减去地址偏移,得到地址在Image中的位置,暂且称之为处理过的地址,然后遍历当前加载的所有Image,然后在Image内部,在遍历所有的LC,比对地址是否在当前LC的的地址范围里面,具体:遍历每个Image,然后获取每个LC,然后比对当前处理过的地址是否大于VMAdress地址并且小于VM Adress 加上VM Size,如果符合条件,就是我们要找的Image。

1141930346A32F24CC2B214D89BC9812.jpg

回到ksdl_dladdr,获取到地址所在的Image,再获取segment base address of the specified image,同样,需要遍历当前Image的LC,然后获取到当前Image的LC_SEGMENT(__LINKEDIT),解析下__LINKEDIT__LINKEDIT动态库链接器需要使用的信息,包括重定位信息(例如符号表,字符串表),绑定信息,懒加载信息等,获取到LC_SEGMENT(__LINKEDIT)则返回VM_Address的值减去File Offset的值。然后用返回值加上实际的内存偏移。其实这里应该是求的当前符号表、字符串表的基地址,公式: 实际位置 = 当前__LINKEDIT在MachO的基地址 - 当前__LINKEDIT的文件偏移 + 实际的偏移量(这里已经包括了文件偏移)。

376D0C1799192A611F1B4A7DEAAFFF33.jpg

然后,同样利用遍历LC来获取当前的符号表信息,然后遍历符号表:然后用逐步靠近目标地址的方法,来获取最佳匹配的地址。

C8FFD20CF1AA7855E01AA08A9AA2F41A.jpg
DEED97C7798DF3D3FF6837AA328CBF16.jpg

获取到了最加的符号地址匹配,然后求出对应的字符串,一下是单个符号的结构体,所以,我们利用n_strx 也就是String Table Index就可以获取到字符串了,至此,单个地址还原完成,其他的以此类推。

struct nlist_64 {
    union {
        uint32_t  n_strx; /* index into the string table */
    } n_un;
    uint8_t n_type;        /* type flag, see below */
    uint8_t n_sect;        /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};

整个捕获流程,如下图所示

8ED95119ABB64DCDA6FA374F6AFF4AFB.jpg

0x03 参考资料

《深入解析Mac OS X & iOS操作系统》

《iOS逆向应用与安全》

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

Adam笔记

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Star先生的专栏

从源码中分析 Hadoop 的 RPC 机制

RPC是Remote Procedure Call(远程过程调用)的简称,这一机制都要面对两个问题:对象调用方式余与序列/反序列化机制。本文给大家介绍从源码中分...

77100
来自专栏Java后端技术

Java发送邮件初窥

  最近朋友的公司有用到这个功能,之前对这一块也不是很熟悉,就和他一起解决出现的异常的同时,也初窥一下使用Apache Common Email组件进行邮件发送...

16220
来自专栏xingoo, 一个梦想做发明家的程序员

日志那点事儿——slf4j源码剖析

前言: 说到日志,大多人都没空去研究,顶多知道用logger.info或者warn打打消息。那么commons-logging,slf4j,logback...

28150
来自专栏cmazxiaoma的架构师之路

IDEA入门(1)--lombok和Junit generator2插件的运用

26930
来自专栏生信宝典

分子对接简明教程 (二)

用PyMOL展示配体和受体相互作用的原子和氢键 为了简化展示过程,我们设计了一个pml脚本 (脚本内有很详细的解释),只需要修改脚本里面受体和配体的名字,然后在...

58250
来自专栏生信宝典

简单可视化-送你一双发现美的眼睛

用PyMOL展示配体和受体相互作用的原子和氢键 为了简化展示过程,我们设计了一个pml脚本 (脚本内有很详细的解释),只需要修改脚本里面受体和配体的名字,然后在...

26860
来自专栏大数据平台TBDS

Flume-Hbase-Sink针对不同版本flume与HBase的适配研究与经验总结

导语:本文细致而全面地讲解使用flume输出数据到HBase的三种不同 Flume-Hbase-Sink 之间的差异性,以及技术细节。并且透彻而全面地总结了不同...

2.2K120
来自专栏码生

ios NSOperation & Queue 官方文档学习

The NSOperationQueue class regulates the execution of a set of NSOperation objec...

9830
来自专栏Java3y

Spring入门这一篇就够了

前言 前面已经学习了Struts2和Hibernate框架了。接下来学习的是Spring框架…本博文主要是引入Spring框架… Spring介绍 Spring...

2.7K60
来自专栏程序猿DD

Spring Batch入门篇

Spring Batch,一个很多人还觉得陌生的框架,它是Spring Cloud Task的基础,主要用来实现批量任务的处理。该框架在国内的使用非常少,所以一...

29850

扫码关注云+社区

领取腾讯云代金券