低于0.01%的极致Crash率是怎么做到的?

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

原文地址:https://www.cnblogs.com/wetest/p/9243593.html

时间: 2024-10-03 14:45:08

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

【腾讯优测干货】Crash率从2.2%降至0.2%,这个团队是怎么做到的?

特约供稿人:腾讯公司天天P图项目 乔伟康.梁小龙 小优有话说: App Crash就像地雷. 你怕它,想当它不存在.无异于让你的用户去探雷,一旦引爆,用户就没了. 你鼓起勇气去扫雷,它却神龙见首不见尾. 你告诫自己一定开发过程中减少crash,少埋点地雷,但总是不得其法. 降低Crash率,需要的是技巧.工具.耐心与时间. 本文由腾讯天天P图测试团队现身说法,为你讲述他们将Crash率直降90%背后的故事,希望能为你"排雷"提供一些思路. PS:以后每周四记得关注这里哦!小优将为你精选

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

小优有话说: App Crash就像地雷. 你怕它,想当它不存在.无异于让你的用户去探雷,一旦引爆,用户就没了. 你鼓起勇气去扫雷,它却神龙见首不见尾. 你告诫自己一定开发过程中减少crash,少埋点地雷,但总是不得其法. 降低Crash率,需要的是技巧.工具.耐心与时间. 本文由腾讯天天P图测试团队现身说法,为你讲述他们将Crash率直降90%背后的故事,希望能为你“排雷”提供一些思路. PS:以后每周四记得关注这里哦!小优将为你精选“干货”,让腾讯的开发&测试大牛们陪你一起为提升产品质量!

我在Mariana里挖到了0.01个比特币!

这是我第二次通过区块链挣到钱,第一次是去年年底,那时赶上数字货币的牛市,买了一点EOS.XRP,一周时间赚了小2000元,这次是在Mariana(马里亚纳)数字夺宝游戏里挖矿,一天挖了3个MT,卖了近66美元,相当于0.01个BTC! 10月份就注册了Mariana(马里亚纳),刚开始觉得新鲜,但那会儿APP总是更新,里面很多规则也不全,玩了一会,觉得没什么好玩的,就一直没玩过,11月初,闲来无事打开它,发现精美了很多,抱着试试看的态度,签到免费领取Key,然后参与了数字夺宝游戏,上午投注了[3

【实战笔记】锦标赛0.10$+0.01$ 第446名

手牌A8s,筹码只有3个大盲了,在cut-off位置,前面所有人都fold,只有上家miausita跟了一个大盲,这个时候我选择了all-in搏一把,果然如预期,除了miausita其他人都fold了. 结局是悲催的,被人主宰踢脚然后出局了.现在回顾这个牌局,总的是没有什么遗憾的,运气不在我这一边.以事后诸葛亮的角度,这个时候ante是60,我手里筹码还有1960,坚持10个hand不是问题,A8s的踢脚还是太小,应该等到一副对子或者有个大点的踢脚才放手一搏的. ----------------

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

转自:http://blog.csdn.net/major1985/article/details/50210293 一般我们使用BigDecimal进行比较精密的计算,我这里计算金额.注意使用double构造器的本质与String构造的本质,避免造成问题. 我这里出现的问题是金额总是多了0.01 问题出现在这段代码: if(valueDiffDays > 0){ logger.info("该账单{}需要缴纳{}天利息",loanBillLogDO.getBillSeqNo(),

NCG CAM 14.0.01 Win32_64 2CD独立的 HSM CAM 系统

NCG CAM 14.0.01 Win32_64 2CD独立的 HSM CAM 系统NCG CAM 是独立的 HSM CAM 系统,能够与现有的 CAD 和 CAM 系统集成(包括 Pro/ENGINEER 和 SolidWorks).NCG CAM 拥有大量创新功能,包括自动三维初步加工.这一功能适用于所有形状类型,在为 HSM 机械加工创建优化且光滑的切割运动的同时 ,出色的表面处理还有助于延长工具寿命,最小化机械工具和生成零件的磨损. CFTurbo v10.0.5.650 Win32_6

JavaScript开发规范 0.01版

JavaScript开发规范v0.01 终极原则:简洁,明晰,优雅. 所谓简洁,可以只写一遍的代码,绝不写两遍: 所谓明晰,尽量只用人类能一眼看明白的写法. 所谓优雅,能让阅读代码的人感受到一种莫名的和谐感(即美感). 先不谈优雅,书写简洁而明晰的代码应该是所有程序员都应该遵守的天然法则.冗余而晦涩的代码,不但容易出错,而且会造成不可估量的维护麻烦. 1,定义普通变量 var name = "Shadow Fiend"; var i, pid, price; 在任何时候,定义新的变量时

如何修改订单金额实现 0.01 元买 iPhone X?

作者简介: 肖志华,rNma0y,信安自学,白帽子.(在安全圈子里,一直以来有"黑帽"."白帽"的说法.黑帽子是指那些造成破坏的黑客,而白帽子均已建设更安全的互联网为己任.) 漏洞盒子排名 TOP 前 100,撞过很多坑,跌撞复前行.研究方向 Web,反黑产.目前某不知名安全团队成员,专职攻防领域,涉及面较广,爱好一切新鲜事物. Web 漏洞里有 SQL 注入.XSS 等漏洞,但是逻辑漏洞等问题也是一个大的关键点. 逻辑漏洞比传统安全漏洞更难发现,也可以理解为在相关

小鸡饲料任务,蚂蚁庄园养鸡线下扫码付款收饲料地址,蚂蚁庄园养鸡线下扫码付款0.01元收饲料地址

蚂蚁养鸡线下扫码付款 0.01元,收 180 克饲料:(良心地址,欢迎收藏) 原文地址:https://www.cnblogs.com/xiluhua/p/10216797.html