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

序: 最近在日常开发中遇到了一次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 -> SIGBUS

//  比较老的手机: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?

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

推荐阅读更多精彩内容