WeTest 导读

看似系统Bug的Crash 99%都不是系统问题!本文将与你一起探索Crash分析的科学方法。


在移动互联网闯荡多年的iOS手机管家,经过不断迭代创新,已经涵盖了隐私(加密相册)、安全(骚扰拦截、短信过滤)、工具(网络检测、照片清理、极简提醒等)等等各个方面,为千万用户提供安全专业的服务。但与此同时,工程代码也越来越庞大(近30万行),一丁点的问题都会影响大量的用户,所以手管一直在质量上下狠功夫,对Crash率更是追求极致。近几个迭代对Crash做了专项分析,Crash率在原本0.02%的基础上稳定降到0.01%,7.7.1版本逼近0.009%,此文将对两类典型的Crash案例进行分析总结。

一、案例分析

 

Crash主要产生在Objective-C方法调用或系统方法调用,所以本文的两个典型案例正是针对OC和C方法调用来展开:

1.1. Crash发生在objc_msgSend

Crash堆栈长这样!Σ(゚д゚lll)

图1

是的,看到这个堆栈我也很方,一眼望去只有一行是工程的代码堆栈,还是个main,但深入分析了Objective-C的消息机制后我们还是能找到问题的突破口的。

Crash类型

首先我们看到这是一个SEGV_ACCERR类型的Crash,访问了错误的地址。

其次,通过汇编代码分析objc_msgSend方法,我们可以得知objc_msgSend + 16这一行代码(如下图2)是在读取当前OC方法的receiver的isa指针偏移0x10的处的值(见附录推荐的objc_msgSend链接文章),由于对象已经被释放了,所以读取该地址导致了读取错误地址,也即产生了野指针Crash。

图2

查找寄存器

 

于是,我们查看Crash时各寄存器的值(见图3),其中x0是发生Crash的函数的第一个参数,针对objc_msgSend来说x0同时表示指向发生Crash的对象的地址,x1是Crash的函数的第二个参数,在objc_msgSend中表示Crash的对象调用的selector,RDM很贴心,已经帮我们查询出该selector为respondsToSelector:。如果x1是我们工程中自己写的一个方法就很容易分析问题了,直接查找工程代码,定位到该函数即可找到原因,可是respondsToSelector:调用的地方太多了,怎么办呢?我们还要继续往里挖。

因为respondsToSelector:的参数是一个selector,所以只要再查出这个selector是什么(对应查询x2在符号表中的符号),也可以马上定位到问题代码。但是很遗憾,x2不在Crash报告中Binary Images中的任一个模块的地址范围内,那,还有办法吗?

图3

办法还是有的,我们知道lr寄存器是当前函数的上一层函数调用地址,如果能知道lr寄存器执行的方法就可以进一步确定问题,很幸运,lr的值刚好就是Binary Images中管家模块地址范围内(见图3,lr是0x000000010508be44,管家模块范围是0x104c24000 - 0x1055affff),于是在符号表中搜索lr对应的符号,得到如下的信息:(下图中的MQQABC为你的app的符号表文件,在xcode打包提交时需要保存下来,对应XXX.app.dSYM/Contents/Resources/DWARF/XXX)

图4

至此,我们知道图1那个只有main信息的堆栈产生的Crash是在-[MQQAlertView didDismissWithButtonIndex:]的第530行,产生Crash的原因是调用了respondsToSelector:,已经十分接近答案了,但是MQQAlertView是管家一个通用的弹窗组件,所以还需要知道是哪个页面出现了这个Crash。

定位问题页面

手管利用RDM的Crash上报组件可以在Crash产生时上报附件的特性,将一些关键的信息存储到了附件上(当前的ViewController堆栈、上一次释放的ViewController、applicationState等),可以在RDM平台上查看这些附件信息,于是我们查看附件信息,发现是在用户退出某页面A时产生的Crash。

得出问题原因→→

 

至此,Crash的路径已经很清楚了:用户进入页面A,页面A弹出一个弹窗,在弹窗未弹出前用户快速退出页面,退出页面时没有把弹窗关掉,然后用户点击了弹窗,由于弹窗的delegate是页面A,而页面A已经释放,所以导致了访问了野指针。

问题原因查明,问题代码定位精确,问题也就不难修复了。

注:objc_msgSend + 16是典型的野指针导致的Crash堆栈,遇到这类问题,基本上按照上述思路都可以顺利解决。

 

1.2. Crash发生在C函数

棘手的Crash通常关键堆栈都是落在系统函数上,这也为我们把锅甩给系统找到一个很好的借口,但想办法解决问题才是目标,毕竟系统是没办法帮你背这个锅的¯\_(ツ)_/¯下面这个例子是结合Crash报告提供的信息分析解决问题的典型案例:

图5

从Crash报告可以看到几个关键信息:

1)Crash类型同样是访问了非法地址SEGV_ACCERR,非法地址是0x68

2)Crash发生在子线程(Thread 7)

3)Crash是落在flockfile + 24的位置上

于是我们通过Xcode调试到flockfile函数,并定位到 + 24的位置(如下图6断点的位置)

图6

ldr    x8,  [x19,  #0x68]  这句汇编代码的含义是从x19偏移0x68的地址上加载数据存储到x8中。结合SEGV_ACCERR,我们知道这个地址非法了,而且非法地址是0x68,也就是说x19 + 0x68 = 0x68,推出=> x19 = 0,再往上看到第5行:mov    x19,  x0,可以知道x19的值是由x0赋值得来的,所以x0 = 0,又因为x0是函数的第一个参数,所以可以得出flockfile的入参为0,查看flockfile的定义:

void                flockfile(FILE  *);

可见,这里的FILE *指针为空了。结合堆栈中管家工程中的代码调用:

 - [MQQCBKAsdfUpdater mgPchAsdfCfgFileWithOFP:pFP:toFP:result:error:]

可以看到,传入了三个文件路径,所以问题必定是其中一个文件不存在了。至此就是我们从Crash报告中能分析出来的信息,再结合查看工程代码得出:问题代码最初是在主线程执行,中间dispatch到子线程(从Crash报告得出),线程间状态没有控制好导致切换到子线程执行的过程中文件被删除了而导致了Crash。

二、方法总结

 

以上分析仅是对过程的回顾,略去了许多细节,这一节进行补充。因为Crash分析主要就是要搞清楚发生Crash时函数调用发生了什么,所以这一节主要分为几个部分:

1)ARM64的函数调用约定

2)常用汇编指令

3)Objective-C函数调用的特点

4)查找符号表

5)Crash报告关键信息

2.1. ARM64函数调用约定

由于目前主流机型都是iPhone 5s以上的机型了,所以这里只介绍ARM64。

2.1.1. ARM64指令集的寄存器

图7(摘自ARM64参考手册)

ARM64指令集有31个64bit的通用整形寄存器:x0到x30(w0到w30表示只取这些寄存器的低32位)

x0到x7用来做参数传递,以及从子函数返回结果(通常通过x0返回,如果是一个比较大的结构体则结果会存在x8的执行地址上)

LR:即x30寄存器,也叫链接寄存器,一般是保存返回上一层调用的地址

FP:即r29,栈底寄存器

外加一个栈顶寄存器SP

2.1.2. 栈

栈是从高地址到低地址延伸的,栈底是高地址,栈顶是低地址

fp指向当前栈帧的栈低,即高地址

sp指向当前栈帧的栈顶,即低地址

下图8是_funcA调用_funcB的栈帧情况:

图8(摘自技术博客)

_funcB的前三行代码如图8的汇编代码所示:

第1行stp指令是表示将_funcA的栈底指针fp、链接寄存器lr存到_funcA的栈顶sp - 0x10的地址上,并将sp设置为sp - 0x10(图中fp_B),方便后续从_funcB返回_funcA,并恢复_funcA的栈帧

第2行是把sp赋给fp,即设置_funcB的栈底指针(图中fp_B)

第3行是把sp设置为sp - 0x30。由此完成了_funcA对_funcB的调用。

2.1.3. 实例分析

下面通过一个实例来分析函数的参数传递

图9

如图9有两个方法,OC方法是一个按钮点击事件,点击后调用上面的C方法,为了调试方便C方法有11个参数,本例中入参的值是1到11,可以观察到超过8个参数时是怎么传参的。

为了看到调用过程的汇编代码,我们需要在- (IBAction)testCmethodCall1:(id)sender中设置断点,然后在Xcode中设置Always Show Disaasembly(见图10),这样调试过程中看的就是汇编代码了

图10

我们断点到OC方法,汇编代码如图11

图11

函数调用状态切换

 

第1行:sub    sp,  sp,  #0x40  设置新的栈顶寄存器(sp)

第2行:stp    x29,  x30,  [sp,  #0x30]  把栈底寄存器(x29即fp)、链接寄存器(x30即lr)保存起来

第3行:add    x29,  sp,  #0x30  把fp(x29)设置为sp + 0x30,即设置新的栈底寄存器

这3行,完成了系统对按钮点击事件方法的调用所需的状态切换工作

为C函数准备入参

接下来直到str    w13,  [sp,  #0x8]  都是在为调用C方法准备参数,因为没有经过优化所以显得很啰嗦。

orr    w8,  wzr,  #0x1  是一个或指令,把零寄存器或上1的值赋给w8寄存器,就是w8 = 1,下面的类似,分别把2到11赋给w9-w10、w3-w7、w11-w13

stur    x0,  [x29,  #-0x8]  把x0保存到x29 - 0x8上

stur    x1,  [x29,  #-0x10]  把x1保存到x29 - 0x10上

str    x2,  [sp,  #0x18]  把x2保存到sp + 0x18上

mov    x0,  x8把前面赋值为1的x8(orr    w8,  wzr,  #0x1)赋给x0

mov    x1,  x9,同理,把2赋给x1

mov    x2,  x10,同理,把3赋给x2,由此可见前面w8、w9、w10只是中转用的,至此x0-x7已经将可以直接传值的寄存器都赋上了正确的值,接下来的3行则可以看到是怎么处理超过8个整形参数的情况

通过栈传参

 

str    w11,  [sp]  把前面赋值为9(mov    w11,  #0x9)的w11存到栈顶位置

str    w12,  [sp,  #0x4]  把前面赋值为10(mov    w12,  #0xa)的w12存到栈顶偏移0x4的位置

str    w13,  [sp,  #0x8]  把前面赋值为11(mov    w13,  #0xb)的w13存到栈顶偏移0x8的位置

调用C函数

 

至此,入参全部准备完毕,接下来调用bl    0x104fc237c就可以调用C函数了

图12

进入C函数的汇编代码,我们先明确下这段C函数的任务是:return a1 + a2 + a11,所以应该是把OC函数中w0、w1、w13(w13存在栈上[sp,  #0x8])的值拿出来相加,得到的结果存到x0上,然后返回,所以:

sub    sp,  sp,  #0x30  把栈顶指针设置为sp - 0x30,这样的话,之前w13存在的栈的位置就变成了[sp,  #0x38],所以你会看到图12最后一个红圈ldr    w1,  [sp,  #0x38]其实就是把之前保存w13的值load到w1中

str    w0,  [sp,  #0x2c]和str    w1,  [sp,  #0x28]把w0、w1的值存到栈上,然后又用ldr    w0,  [sp,  #0x2c]和ldr    w1,  [sp,  #0x28]把w0、w1的值取出来,没优化的汇编真的很啰嗦

add    w0,  w0,  w1  把w0、w1的值加起来存到w0(即计算了a1 + a2)

ldr    w1,  [sp,  #0x38]  前面说过取出w13的值存到w1

add    w0,  w0,  w1  把w0、w1的值加起来存到w0(即计算了a1 + a2 + a11),现在计算完的结果存到了w0中。

由上面的分析过程我们可以看到:

 

  • 子函数开头的汇编代码会调整fp、sp指针

  • 参数传递少于8个的使用x0-x7寄存器

  • 超过8个的则使用栈来传递

  • 子函数的返回值一般存在x0中

  • 因为x0、x1、x29、x30等寄存器有特殊含义,所以有时候会把这些寄存器的值先存到栈上,然后再使用它们

2.2. 常用汇编指令

 

2.1节已经接触了几个汇编指令,下面整理下常用的几个汇编指令:

mov    a,  b  即a = b

ldr    a,  [b]  将b指针所在地址上的内容加载a寄存器中

str    a,  [b]  将a寄存器存储到b指针指向地址上

ldr    a,  [b,  #0x10]  从b寄存器地址+0x10的地址上加载内容到a寄存器中

ldr    a,  [b,  #0x10]!  带感叹号的意思是把内容加载到a寄存器中,并且修改b寄存器为b = b + 0x10

cmp    a,  b  比较a、b寄存器的值,会修改cpsr

cbz    xd,  addr  判断xd寄存器是否为0,是则跳转到addr地址处执行

cbnz    xd,  addr  判断xd寄存器是否不为0,不为0则跳转到addr

b  跳转指令,不修改lr寄存器,所以子函数调用过程不会出现在堆栈中

bl  跳转指令,修改lr寄存器,所以子函数调用过程会出现在堆栈中

stp    a,  b,  [c]  从c地址中取出两个64位值分别存储到a、b两个寄存器中

ldp    a,  b,  [c]  把a、b两个寄存器的值存储到c地址中

2.3. Objective-C函数调用的特点

Objective-C函数调用是一种特殊的函数调用,但最终也是转化为C函数调用的方式。

我们都知道Objective-C调用最终都会调用objc_msgSend(id self, SEL selector, ...),然后再用前面的知识分析objc_msgSend即可

可以看到,x0就是调用的receiver,x1就是调用的selector,后面则是参数。具体可以查看附录中相关的文章。

2.4. 查找符号表

图13

Crash报告中有Binary Images:

1)模块的起止地址:比如图13中MQQABC模块的起始地址是0x104c24000,结束地址是0x1055affff,所以我们可以通过这些模块的起止地址来判断一个我们感兴趣的寄存器的地址是属于哪个模块的

2)模块的UUID,如图13中MQQABC的UUID是f130b043a0c832d9958d89dab8339961,通过它可以判定你的符号文件是正确的,如图14用dwarfdump

图14

3)用atos查找地址对应的符号,-l需要提供1)中提到的模块起始地址

图15

4)如果用atos查找出来的结果仍然是个地址,还需要在mach-O文件的__TEXT段或__RODATA段的__objc_methname中进一步查找(注意:第一个红框中查询出来的0x在otool查找Mach-O文件中要去掉)

图16

2.5. Crash报告关键信息

图17

图18 结合寄存器值查找关键信息

图19 确定符号表UUID及起止地址

 


腾讯WeTest是由腾讯官方推出的一站式质量开放平台。十余年品质管理经验,致力于质量标准建设、产品质量提升。腾讯WeTest为移动开发者提供兼容性测试、云真机、性能测试、安全防护、企鹅风讯(舆情分析)等优秀研发工具,为百余行业提供解决方案,覆盖产品在研发、运营各阶段的测试需求,历经千款产品磨砺。金牌专家团队,通过5大维度,41项指标,360度保障您的产品质量。

腾讯互娱为提高苹果应用的审核通过率,专门成立了苹果审核测试团队,打造出iOS预审工具这款产品。经过长时间的内部运营和磨炼,腾讯苹果应用审核通过率从平均35%提升到90%+。点击链接;http://wetest.qq.com/product/ios 邀您立刻体验。

如果使用当中有任何疑问,欢迎联系腾讯WeTest企业QQ:800024531

低于0.01%的极致Crash率是怎么做到的?的更多相关文章

  1. 【腾讯优测干货】看腾讯的技术大牛如何将Crash率从2.2%降至0.2%?

    小优有话说: App Crash就像地雷. 你怕它,想当它不存在.无异于让你的用户去探雷,一旦引爆,用户就没了. 你鼓起勇气去扫雷,它却神龙见首不见尾. 你告诫自己一定开发过程中减少crash,少埋点 ...

  2. java如何使用 tesseract 4.0.0-1.4.4

    提示: 建议直接使用tess4j,tess4j是对tesseract的封装,使用更简单 首先引入依赖 <!-- https://mvnrepository.com/artifact/org.by ...

  3. 驱动开发学习笔记. 0.01 配置arm-linux-gcc 交叉编译器

    驱动开发读书笔记. 0.01 配置arm-linux-gcc 交叉编译器 什么是gcc: 就像windows上的VS 工具,用来编译代码,具体请自己搜索相关资料 怎么用PC机的gcc 和 arm-li ...

  4. javascript中0.01*2324=23.240000000000002 ?

    js中的乘法运算的小问题 0.01*2324=23.240000000000002 ? , 结果为什么出现这么多小数位呢?

  5. new BigDecimal(0.01) 与 new BigDecimal(String.valueOf(0.01))的区别 (转)

    转自:http://blog.csdn.net/major1985/article/details/50210293 一般我们使用BigDecimal进行比较精密的计算,我这里计算金额.注意使用dou ...

  6. pyserial timeout=1 || timeout=0.01

    昨天在做串口通信时候发现,串口参数(timeout=1 || timeout=0.01)对通信的读数据竟然影响很大,代码如下: self.ser = serial.Serial(port=serial ...

  7. 在Livemedia的基础上开发自己的流媒体客户端 V 0.01

    在Livemedia的基础上开发自己的流媒体客户端 V 0.01 桂堂东 xiaoguizi@gmail.com 2004-10 2004-12 友情申明: 本文档适合已经从事流媒体传输工作或者对网络 ...

  8. KmdKit4D 0.01正式版发布了(0.02版已放出)(Delphi做驱动)

    此版本较0.01预览版已经有了脱胎换骨的变化,主要表现在以下几个方面:    1.对程序的结构进行了调整,将原来的ntutils.dcu分成fcall.dcu.halfcall.dcu和macros. ...

  9. Flyway Validate failed: Migration checksum mismatch for migration version 1.0.0.01 错误

    在运行系统的时候出现错误: org.springframework.beans.factory.BeanCreationException: Error creating bean with name ...

随机推荐

  1. luogu P3941 入阵曲

    嘟嘟嘟 这道题我觉得跟最大子矩阵那道题非常像,都是O(n4)二维前缀和暴力很好想,O(n3)正解需要点转化. O(n4)暴力就不说啦,二维前缀和,枚举所有矩形,应该能得55分. O(n3)需要用到降维 ...

  2. 关于mysql 出现 1264 Out of range value for column 错误的解决办法

    今天给客服恢复mysql数据的时候.本来测试好的数据.但是到了客户那里却死活不干活了.老报错! INSERT INTO ka_tan4 set num='716641385999', username ...

  3. Java 和 .NET SHA1算法记录

    最近做了一个.NET访问Java接口的小Demo,其中用到了SHA1加密,大体思路就是.NET 传一些参数然后SHA1加密,Java端接收到之后在SHA1加密对比. Java代码: public fi ...

  4. fc全连接层的作用、卷积层的作用、pooling层、激活函数的作用

    fc:1.起到分类器的作用.对前层的特征进行一个加权和,(卷积层是将数据输入映射到隐层特征空间)将特征空间通过线性变换映射到样本标记空间(也就是label) 2.1*1卷积等价于fc:跟原featur ...

  5. [转]Python中下划线以及命名空间的意义

    Python 用下划线作为变量前缀和后缀指定特殊变量/方法. 主要存在四种情形 1. 1. object # public    2. __object__ # special, python sys ...

  6. UCOS阅读问题累积

    1.#ifdef __cplusplus   extern "C" {  #endif 作用: 一般用于将C++代码以标准C形式输出(即以C的形式被调用),这是因为C++虽然常被认 ...

  7. Before start of result set

    ResultSet:在处理结果集的时候出现了问题. 解决办法:while(rs.next())

  8. Web项目开发中常见安全问题及防范

    计算机程序主要就是输入数据 经过处理之后 输出结果,安全问题由此产生,凡是有输入的地方都可能带来安全风险.根据输入的数据类型,Web应用主要有数值型.字符型.文件型. 要消除风险就要对输入的数据进行检 ...

  9. Spring知识点小结(三)

    一.aop的简介 aop:面向切面编程    aop是一种思想,面向切面编程思想,Spring内部提供了组件对aop进行实现    aop是在运行期间使用动态代理技术实现的思想    aop是oop延 ...

  10. JavaScript常用DOM操作方法和函数

    查找节点ocument.querySelector(selectors) //接受一个CSS选择器作为参数,返回第一个匹配该选择器的元素节点.document.querySelectorAll(sel ...