iOS那些事

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

2021-05-25  本文已影响0人  ArthasMay

序: 最近在日常开发中遇到了一次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,并且整理到以下线索

在这里吐槽下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异常

Unix信号

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

Signal信号类型:

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新增了两个指令:

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?

上一篇下一篇

猜你喜欢

热点阅读