美文网首页iOS那些事
iOS开发之Crash追踪之旅(一)

iOS开发之Crash追踪之旅(一)

作者: ArthasMay | 来源:发表于2021-05-25 15:54 被阅读0次

    序: 最近在日常开发中遇到了一次Crash引起的Crash的血灾,在5月初的一次发版把笔者开发的App的Crash率直接从万一干到了接近千二,当时项目负责人正好需要向上报告项目QA相关情况,当时就懵逼了😂。

    问题

    由于年初花了大功夫把原来OC为主体的项目完全迁移到Swift,由于Swift的安全性,crash保持的一直不错,忽然这一出搞的也挺懵,查了一下UMeng的crash追踪,全是报Attempted to dereference garbage pointer 0x18ffd63d72d0,符号化之后的crash函数调用栈也挺迷的,在不定线程crash,app的函数符号定位都是在一个模型类的0行。
    整理了一下相关的crash log 和umeng上的用户行为,知道应该是碰到了内存问题或野指针了,这种伤脑的问题如果只是几个零星的crash就果断放后面解决了,但是千分之二的crash直接影响了饭碗问题,只能硬着头皮去解决,也是为了重塑曾今的那钟对技术极致追求的精神。

    结果

    在整理了Umeng的Crash Log和行为日志以及App Connent用户上传的Crash Log,可以确定是由于野指针/内存问题引起的随机Crash,并且整理到以下线索

    • 随机crash出现在app运行5分钟之后的占比很高
    • 刚冷启动时(用户轨迹只有adviewcontroller)也会发生crash的,说明有问题的代码应该在启动那块执行
    • 和idfa的获取方式变化有关,因为新版本(2.1.0)由于idfa政策变化被拒过,因此可以定位到应该是集团提供的风控SDK嫌疑很大,修改提审和风控部门对过他们sdk有收集idfa,回忆在集成代码的时候发现风控sdk是使用c和c++开发的,当时改Swift集成的时候还因此加了桥接文件。
    • 在高版本iOS系统和新手机(arm64e)设备上搜集到的crash多,低版本和高版本的原始crash的Exception Type是不一样的:arm64e 是 SIGSEGV arm64 是 SIGBUS

    在这里吐槽下Umeng的crash收集,没有显示原始的错误,都归纳为Attempted to dereference garbage pointer,不利于排查和定位错误

    解决步骤:

    1. 拉取2.1.0发版代码
    2. xcode 打开 Address Sanitizer(Asan)重新编译运行 -> buggy address 的确可以查到有内存使用问题
    3. 注释启动相关代码 风控sdk初始化代码,发现的确是风控sdk导致的
    4. 联系风控组,替换SDK,通过测试
    5. 等待上线验证

    到这里,这次Crash问题告一段落了,但是在追踪过程中查看和回归了以前很多相关的底层技术和工具,在解决问题后再次坐下深入的总结和记录。

    涉及的技术点

    1. iOS内存管理机制: OC C C++的这方面资料很多,可以拓展去看下Swift的坐下总结
    2. 符号文件解析, LLDB高级调试和插件编写,ASDN相关
    3. iOS系统crash:Exception(mach oc) 和 unix的bsd 的signal错误
    4. bugly的apm工具原理和实现
    5. PAC(PAC技术)[https://justinyan.me/post/4129]

    我会以若干篇章去深入探索下相关技术点

    iOS系统中的Crash

    1. Crash的分类

    记得在之前文章中探索过为什么移动应用会有crash:内存管理,因为移动系统为了保护闪存而舍弃了Swap机制。

    Crash的主要原因是App收到未处理的信号,iOS的核心操作系统是Darwin,Darwin内核是XNU("X is Not UNIX"),XNU是一个基于Mach+BSD的混合内核,所以引起Crash的信号可以分为三种:

    1. Mach异常:Mach负责XNU比较底层的任务,所以Mach异常是指底层的内核级异常,用户态的开发者可以直接通过Mach API设置thread、task和host的异常端口来捕获Mach异常
    2. Unix信号:又称BSD信号(XNU中的BSD发出),如果开发者没有捕捉Mach异常,则会被host层的方法ux_exception()转化为对应的Unix信号,并通过threadsignal()将信号投递到出错的线程,可以通过signal(x, SignalHandler)来捕获signal
    3. NSException:应用级异常,也可以认为是OC语言层面的异常,导致程序向自身发送了SIGABORT信号而crash,可以try catch捕获或者通过NSSetUncaughtExceptionHandler()机制来捕获

    Swift的异常机制这方面的大佬们分享的很少 ,可以研究下Swift的错误机制

    上面三个层面的Crash,语言层面的(OC)应用级的Crash是最好解决的,数组越界、 runtime的msg_send消息转发机制导致的crash,kvc等OC语言机制的crash可以通过crash log中的backtrace很快定位到。而对于Mach异常和Unix信号导致的crash则对于高级开发来说也是很大的挑战。

    2. Mach异常和Unix信号

    Mach异常是什么?它又是如何与Unix信号建立联系的?

    // crash log头部
    Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
    Exception Subtype: KERN_INVALID_ADDRESS at 0x0022000000000000 -> 0x0000000000000000 (possible pointer authentication failure)
    
    1. Mach异常是XNU的微内核核心Mach运行中出现的内核级异常,每个thread、task、host(这个host是什么?)都有一个异常端口数组,Mach的部分API暴露给用户态,用户态的开发者可以直接通过Mach API设置thread、task、host的异常端口,来捕获Mach异常。
    2. 所有未处理的Mach异常,都会通过ux_exception()转化为Unix信号,通过threadsignal将信号传递到出错的线程。iOS的POSIX API就是通过Mach上层的BSD层实现的。

    注:Mach 最基础的对象是“主机(host)”,也就是表示机器本身的对象

    如上面的贴的Crash Log头部摘自我这次Crash的日志,EXC_BAD_ACCESS(访问无效内存)异常,因为没有在Mach层捕获,被host层转化为SIGSEGV信号传递给了出错的线程。

    所以:

    1. 未处理Mach异常是会转为Unix Signal,应用级异常未捕获也会在转为NSException, 然后调用C的Abort(),kernel对App发出__pthread_kill信号,触发Mach异常,所以只要未捕获的异常都是会转化为一条Unix信号。
    2. 硬件产生的信号(通过CPU的trap机制:mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用)被Mach捕获,然后转化为Unix信号。
    3. Apple为了统一机制,操作系统或者用户产生的信号(kill和thread_kill)也会转化为Mach异常,最后转为Unix信号。
    Crash产生原理和传递过程

    4. Mach异常和Unix信号的分类

    常见的Mach异常

    • EXC_CRASH: 进程异常退出(SIGABORT) 或者 watch dog超时杀死App(SIGKILL)
    • EXC_BREAKPOINT (SIGTRAP)
    • EXC_BAD_ACCESS :内存访问无效
    • EXC_BAD_INSTRUCTION:线程试图访问非法/无效的指令或将无效的参数(操作数)传递给指令
    • EXC_ARITMETHIC:除以0或整数溢出/下溢引发的异常
    • EXC_SYSCALL 和 EXC_MACH_SYSCALL:应用程序访问内核服务(如文件I/O)或网络访问时发出
    • 其他Mach异常定义在mach/exception_types.h中。与处理器相关的异常定义在mach/(i386,ppc,...)/exception.h中
      在开发中最常见的异常应该是EXC_BAD_ACCESS,就比如这次追踪到的

    Unix信号

    信号处理函数可以通过 signal() 系统调用来设置。如果没有为一个信号设置对应的处理函数,就会使用默认的处理函数,否则信号就被进程截获并调用相应的处理函数。在没有处理函数的情况下,程序可以指定两种行为:忽略这个信号 SIG_IGN 或者用默认的处理函数 SIG_DFL 。但是有两个信号是无法被截获并处理的: SIGKILL、SIGSTOP 。

    Signal信号类型:

    • SIGABRT--程序中止命令中止信号
    • SIGALRM--程序超时信号
    • SIGFPE--程序浮点异常信号
    • SIGILL--程序非法指令信号
    • SIGHUP--程序终端中止信号
    • SIGINT--程序键盘中断信号
    • SIGKILL--程序结束接收中止信号
    • SIGTERM--程序kill中止信号
    • SIGSTOP--程序键盘中止信号
    • SIGSEGV--程序无效内存中止信号
    • SIGBUS--程序内存字节未对齐中止信号
    • SIGPIPE--程序Socket发送失败中止信号

    5. 模拟Mach Message发送和捕获Mach异常

    5.1 Mach

    Mach是XNU的微内核
    Mach的几个基本概念:
    Tasks: 拥有一组系统资源的对象,允许thread在其中执行
    Threads: 执行的基本单位,拥有task的上下文,并共享其资源
    Ports: task之间通讯的一组受保护的消息队列,task可以对任何port发送/接收数据
    Message:有类型的数据对象集合,只可以发送给Host

    5.2 模拟Mach Message的发送
    1. 创建 post 授权
    + (mach_port_t)createPortAndListener {
        // 在Mach的头文件中找到的 mach_port_t 完全等价于 mach_port_name_t
        // typedef unsigned int            __darwin_natural_t;
        // typedef __darwin_natural_t __darwin_mach_port_name_t; /* Used by mach */
        // typedef __darwin_mach_port_name_t __darwin_mach_port_t; /* Used by mach */
        // typedef __darwin_mach_port_t mach_port_t;
        // typedef natural_t mach_port_name_t;
        // typedef __darwin_natural_t      natural_t;
        
        mach_port_t server_port;
        kern_return_t kr = mach_port_allocate(mach_task_self(),
                                              MACH_PORT_RIGHT_RECEIVE,
                                              &server_port);
        assert(kr == KERN_SUCCESS);
        
        NSLog(@"Create a port: %d", server_port);
        
        kr = mach_port_insert_right(mach_task_self(),
                                    server_port,
                                    server_port,
                                    MACH_MSG_TYPE_MAKE_SEND);
        assert(kr == KERN_SUCCESS);
        
        return server_port;
    }
    
    
    1. Mach 端口监听
    + (void)setMachPortListener:(mach_port_t)mach_port {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            mach_msg_header_t mach_message;
            
            mach_message.msgh_size = 1024;
            mach_message.msgh_local_port = mach_port;
            
            mach_msg_return_t mr;
            
            while (true) {
                mr = mach_msg(&mach_message,
                              MACH_RCV_MSG | MACH_RCV_LARGE,
                              0,
                              mach_message.msgh_size,
                              mach_message.msgh_local_port,
                              MACH_MSG_TIMEOUT_NONE,
                              MACH_PORT_NULL);
                if (mr != MACH_MSG_SUCCESS && mr != MACH_RCV_TOO_LARGE) {
                    NSLog(@"error!");
                }
                
                mach_msg_id_t msg_id = mach_message.msgh_id;
                mach_port_t remote_port = mach_message.msgh_remote_port;
                mach_port_t local_port = mach_message.msgh_local_port;
                
                NSLog(@"Recevie a mach messag:[%d], remote_port: %d, local_port: %d, exception",
                      msg_id, remote_port, local_port);
            }
        });
    }
    
    1. 向创建的 mach port 发送消息
    + (void)sendMachPostMessage:(mach_port_t)mach_port {
        kern_return_t kr;
        mach_msg_header_t msg_header;
        msg_header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
        msg_header.msgh_size = sizeof(mach_msg_header_t);
        msg_header.msgh_remote_port = mach_port;
        msg_header.msgh_local_port = MACH_PORT_NULL;
        msg_header.msgh_id = 100;
        NSLog(@"Send a mach message: [%d]", msg_header.msgh_id);
        
        kr = mach_msg(&msg_header,
                      MACH_SEND_MSG,
                      msg_header.msgh_size,
                      0,
                      MACH_PORT_NULL,
                      MACH_MSG_TIMEOUT_NONE,
                      MACH_PORT_NULL);
    }
    
    
    5.3 在Mach层捕获异常

    6. Signal注册和处理

    7.PAC

    在这次的Crash追溯的过程中,我发现:

    1. 在比较新的机器上(一般iOS系统版本也比较高), Crash的概率比较大
    2. 通过App Connect搜集到的原始Crash Log中Crash的Mach异常转化后的Signal是不一样的

    比较老设备收集到的Crash Log头部:Unix Signal -> SIGSEGV

    //  比较老的手机:iPhone 8
    Incident Identifier: 9DCFF105-1CBE-4947-B386-68E4375EC340
    Hardware Model:      iPhone10,1
    Process:             esport-app [15056]
    Path:                /private/var/containers/Bundle/Application/86BE49B4-3975-45D9-AC97-CD9CABF4F7D0/esport-app.app/esport-app
    Identifier:          com.wmzq.esportapp
    Version:             2 (2.1.0)
    AppStoreTools:       12E262
    AppVariant:          1:iPhone10,1:13
    Beta:                YES
    Code Type:           ARM-64 (Native)
    Role:                Foreground
    Parent Process:      launchd [1]
    Coalition:           com.wmzq.esportapp [2538]
    
    
    Date/Time:           2021-05-06 15:08:30.2709 +0800
    Launch Time:         2021-05-06 15:08:28.6146 +0800
    OS Version:          iPhone OS 13.7 (17H35)
    Release Type:        User
    Baseband Version:    5.70.01
    Report Version:      104
    
    Exception Type:  EXC_BAD_ACCESS (SIGBUS)
    Exception Subtype: KERN_PROTECTION_FAILURE at 0x000000016c1bfdc0
    VM Region Info: 0x16c1bfdc0 is in 0x16c1bc000-0x16c1c0000;  bytes after start: 15808  bytes before end: 575
          REGION TYPE                      START - END             [ VSIZE] PRT/MAX SHRMOD  REGION DETAIL
          Stack                  000000016c0ec000-000000016c1bc000 [  832K] rw-/rwx SM=COW  thread 21
    --->  STACK GUARD            000000016c1bc000-000000016c1c0000 [   16K] ---/rwx SM=NUL  ...for thread 22
          Stack                  000000016c1c0000-000000016c248000 [  544K] rw-/rwx SM=COW  thread 22
    
    Termination Signal: Bus error: 10
    Termination Reason: Namespace SIGNAL, Code 0xa
    Terminating Process: exc handler [15056]
    Triggered by Thread:  22
    
    

    新设备收集到的Crash Log头部:Unix Signal -> SIGSEGV

    // iPhone XR
    Incident Identifier: 95414C75-D357-4AFC-9951-2EAE098F31B3
    Hardware Model:      iPhone11,8
    Process:             esport-app [13681]
    Path:                /private/var/containers/Bundle/Application/07BF60A9-D0A0-4B29-A9F2-C5E6C99D84EC/esport-app.app/esport-app
    Identifier:          com.wmzq.esportapp
    Version:             2105031 (2.1.0)
    AppStoreTools:       12E262
    AppVariant:          1:iPhone11,8:14
    Beta:                YES
    Code Type:           ARM-64 (Native)
    Role:                Foreground
    Parent Process:      launchd [1]
    Coalition:           com.wmzq.esportapp [742]
    
    
    Date/Time:           2021-05-08 09:41:52.5606 +0800
    Launch Time:         2021-05-08 08:38:34.2531 +0800
    OS Version:          iPhone OS 14.5 (18E199)
    Release Type:        User
    Baseband Version:    3.03.05
    Report Version:      104
    
    Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
    Exception Subtype: KERN_INVALID_ADDRESS at 0x0022000000000000 -> 0x0000000000000000 (possible pointer authentication failure)
    VM Region Info: 0 is not in any region.  Bytes before following region: 4373348352
          REGION TYPE                 START - END      [ VSIZE] PRT/MAX SHRMOD  REGION DETAIL
          UNUSED SPACE AT START
    --->  
          __TEXT                   104ac0000-104b24000 [  400K] r-x/r-x SM=COW  ...pp/esport-app
    
    Termination Signal: Segmentation fault: 11
    Termination Reason: Namespace SIGNAL, Code 0xb
    Terminating Process: exc handler [13681]
    Triggered by Thread:  11
    

    通过查阅资料知道了Apple在A12开始支持了arm64e指令集,提供了指令地址加密功能,即PAC(Pointer Authentication Code的缩写)

    7.1 PAC是什么

    PAC是ARMv8.3 新增的功能,因为虽然系统是64位的,但是arm64指令地址根本用不满,所以把高位的部分(upper bits)拿来存一个指针地址的签名。
    PAC指针验证码就是在CPU执行指令前先拿指针的高位签名和低位的实际地址部分坐下校验,失败了直接抛出异常
    为了实现PAC, arm64e新增了两个指令:

    • PACIASP 计算 PAC 加密并加到指针地址上
    • AUTIASP 校验加密部分,并还原指针地址
    7.2 PAC应用举例

    在这里我主要记录了下这次Cras追溯的过程和总结了下iOS系统Crash的产生原理,Crash从内核态 -> 抛出至用户态的过程,以及PAC等一些概念性的东西。
    后面的几篇文章我会总结Xcode的内存诊断工具,Zombie Objects、 Address Sanitizer、Malloc Scribble的原理和使用,尽量通过代码和WoWCrash示例去实现一个搜集定位内存问题的APM工具。

    好久没有好好的深入研究一些技术了,之前一度认为iOS的技术深入不划算了,现今的开发都是页面党,替代性太强了,所以一直犹豫是否转后端或者web。但这次的Crash追踪过程,让我觉得成为相关方面资深开发甚至专家对我还是有诱惑力的,从Crash Log分析->逆向工具使用->底层原理认知->解决问题获取那种喜悦,让我重新找回方向,加油,走出舒适区💪。

    参考资料

    iOS Mach 异常、Unix 信号 和NSException 异常
    iOS Mach异常和signal信号
    为什么 arm64e 的指针地址有空余支持 PAC?

    相关文章

      网友评论

        本文标题:iOS开发之Crash追踪之旅(一)

        本文链接:https://www.haomeiwen.com/subject/rctadltx.html