二十万分之一几率:if语句变do-while卡死问题分析

背景

某次灰度发布之后没多久就收到线上ANR告警,经排查定位到是某个页面onCreate方法执行太久导致,而火焰图中的耗时堆栈指向了我们用于监控页面启动速度的一段插桩代码,反编译Apk之后发现本该是if语句的代码竟变成了一个do-while语句,形成了死循环最终导致主线程卡死。

此后每构建二、三十次都会复现一次该问题,且每次的异常页面,异常方法完全随机。
在这里插入图片描述

问题分析

if和do-while两个完全不相干的语句为什么出现互相转化的情况?在jadx反编译而来的smali代码中不难看出,if语句对应的标签正常情况下应该指向的是return语句,和Java源码中if语句块后面紧跟着return语句对应。而异常情况下标签跑到了整个函数的开头,故被jadx翻译成了do-while,因此问题的关键就在这个label上面。
在这里插入图片描述

初步分析

出现此问题的这段插桩代码出自我们的APM页面启动监控,原本是插桩在Activity和Fragment的onCreate等关键生命周期中用于耗时统计。其所在的类是由我们自定义的插桩plugin weaver所生成(基于byteX开发的一个plugin,支持插入,代理和替换等自定义的插桩行为)。

因此我们要对从该plugin所在的byteX transform开始,直到最终产出dex文件的R8 transform结束这期间的所有transform挨个分析。

由于问题偶现,且每次异常的类和方法完全随机,说明大概率是一个多线程并发读写的问题,因此我们在分析过程中会需要重点关注涉及并发读写的逻辑。

分析R8

我们在输入给R8的jar包中找到了这个异常类的class文件,这里可以看到jadx反编译这个方法会失败,看class字节码中if语句跳转指向的标签L29,但是函数中并没有定义L29指向的是哪里,并且smali视图下查看可以看到if语句指向的标签在整个函数体中也没有声明,但是前面反编译DEX文件得到的结论是标签有声明但是在函数体的第一行,两者不一致,说明R8可能在执行过程中编辑了字节码导致异常。

(这里我们早期误以为标签丢失并不会导致语句变化这种程度的错误,因此直接将范围锁定在了R8,虽然后续证明了此问题与R8无关,但这段分析也为最终解开谜底提供了关键线索)
在这里插入图片描述
在这里插入图片描述

环境准备

R8目前已经不再单独提供jar包,而是一同打包在AGP中,且开启了混淆,因此想要调试/修改代码就需要自行clone源码,切到自己项目AGP版本对应的git tag来构建R8.jar并指定,具体操作可以参考R8的git仓库中描述:https://r8.googlesource.com/r8

阶段产物分析

目前的R8是由早期的D8融合了一系列的包体/性能优化的操作而来,dx负责将jar包整合压缩成DEX文件,它相对于后来新增的编译优化操作来说出现问题的概率更低,因此我们优先关注R8中涉及对字节码进行编辑的优化功能。

由于R8在输入了jar包之后一直在内存中进行操作,并无中间产物,因此我们需要在相关功能的开始结束点手动将内存中所有由自定义weaver plugin生成的class(有统一的后缀名)写到文件并保存。
在这里插入图片描述
在这里插入图片描述

在多次打包复现问题之后,对阶段产物进行分析并未发现异常方法的字节码有任何变动,直到dx这一步,我们发现if语句在class字节码中跳转到指定标签的行为,在dex文件的smali字节码中被编译成了跳转到指定的函数偏移量。

而之前class字节码中if语句指向的label找不到声明的问题,在smali中表现为直接将函数偏移量设为默认值0X00,正好是函数体的第一行,和一开始反编译apk得到的结果吻合,这也就解释了为什么if语句最终会变成一个do-while语句。
在这里插入图片描述

小结

至此,我们已经知晓为什么if语句会变成毫不相干的do-while语句,同时也排除了R8的嫌疑,接下来就是要继续回溯transform,排查为什么class字节码中if语句指向的标签的声明会丢失。

分析weaver

在回溯排查完所有途径transform的产物之后确认这个异常的方法在一开始weaver生成他时就已经是异常状态,因此问题范围锁定到此plugin。

在继续分析问题之前我们来了解下weaver的插桩原理:

weaver插桩原理

weaver基于byteX实现了一些自定义的插桩行为,这次出问题的是insert行为,也就是在目标函数开头插入代码的模式,其实现原理是预先写好要插桩的代码,在plugin执行期间会用ASM的classNode读取这个类,并将其中的方法复制到一个新建的内部类中,这个内部类会被添加到在注解中指定的目标类中,再在目标类的生命周期函数中调用这个内部类对应的方法即可完成对生命周期的插桩。
在这里插入图片描述

走码分析

虽然我们已经确认是weaver在生成内部类中方法时出现异常,但是生成的过程是从0到1,此时再去加日志打印class字节码分析中间产物已经没有意义,并且由于其极低的复现概率,我们也无法在本地做调试分析。

遂走码分析,最后发现在从旧方法中复制方法提供给内部类的过程中,出现了ASM版本不一致的问题,由于整个byteX组件全局指定了ASM的版本是9,但是weaver中使用了ASM9的methodNode去clone出一个指定为ASM5的methodNode,但是很遗憾这并不是根因,在修正版本后依旧会复现问题。
在这里插入图片描述
在这里插入图片描述
我们目前已知的只有class字节码中if语句指向的label没有声明,遂猜测是methodNode的指令链表中丢失了labelNode,但添加了相应的检测逻辑之后并未命中,故排除labelNode丢失的可能。

关键线索缺失

前文中提到过推测这个问题和多线程有关,因此理论上在本地固定输入输出,并用大量线程并发死循环跑是能够复现问题从而debug找到根因的,但是苦于没有明确的检测逻辑,即不知道这个methodNode在什么状态下才算异常,哪怕问题复现了也无法断点。

逆向分析异常字节码

当务之急是找到合适的异常字节码检测手段,但是在常规思路都碰壁时,不妨用逆向思维试试,于是把异常的class文件直接用ASM的classNode类读取到内存,仔细观察异常方法和正常方法的指令链表中labelNode是否有什么不一样。最终发现异常methodNode的指令链表中,jumpNode持有的labelNode和链表中的labelNode不是同一个对象(正常情况下是)。

带着这个逆向得到的结论,再正向去验证他,即编码实现主动将某个方法的labelNode给替换成新的对象,再输出为class文件,发现和前面得到的异常class完全一致,至此我们就得到了一个准确的异常检测逻辑。
在这里插入图片描述
带着前面得到的精准检测逻辑,我们在本地写demo开16线程并发,瞬间就复现了此问题,随后顺着这个线索走码也找到了问题根因。

这里使用我们正常运行时使用的forkjoinpool,并发死循环执行前面提到的methodNode复制过程,模拟正常构建过程的并发度,最终得出结果是大约每执行20w次可以复现一次问题,除以我们App中相关方法的量级,正好和之前约每20次~30次构建复现一次的频率吻合。
在这里插入图片描述
在这里插入图片描述

小结

至此我们已经定位到了引起问题的代码,也通过多种手段验证了根因就是多线程复制methodNode,但稳妥起见还是要刨根问底弄明白并发复制到底是怎么引起的labelNode对象被替换,防止还有更深层次的问题被掩盖。

揭露谜底

ASM方法复制原理

methodNode复制流程图如下:
在这里插入图片描述

ASM的methodNode类,通过其accept方法可以将这个方法复制给一个methodVIsitor,通常情况下只会使用一次,如果有1次以上的复制行为,就会在复制之前将指令链表中的labelNode中记录跳转地址的label对象置为null。

(clone方法理应是创建一个全新的对象,不应该和旧对象有任何共用的数据,ASM这里的处理没问题,但是没有适配多线程的情况)

在这里插入图片描述

在这里插入图片描述
随后在指令复制的过程中,在遍历到jump指令(通过持有labelNode来形成指向关系)时,会通过getLabel方法将刚刚被置null的label对象重新new出来,同时再从新的label对象中new一个新的LabelNode交给新的JumpNode。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
等遍历到对应的LabelNode时,此时getLabel拿到的是刚刚new出来的新Label,同样的链路再走一遍,此时无需再new新的,并且新方法中的JumpNode持有的labelNode也和当前是一个对象。
在这里插入图片描述
在这里插入图片描述

多线程问题根因

至此我们能得知在复制methodNode的过程中,针对labelNode有多次读写操作。而weaver为了加快执行速度,对每一个class都单独安排了一个task,全都提交给一个forkJoinPool来执行,并且按照前面介绍的weaver插桩原理,提前写好的这个类里的方法,总计会复制成千上万次,提供给每一个Activity的内部类。因此在多线程高并发执行时就会出现以下顺序:
在这里插入图片描述

这样最终就会出现jumpNode持有的LabelNode和指令链表中的LabelNode不一致的问题。

修复方案

ASM为了规避同一个methodNode在多次复制时,复制出来的新methodNode的labelNode全都指向同一个对象的问题,加了这个resetLabel的标签重置逻辑,但是并没有考虑到多线程并发执行的场景,因此该问题最终加一个类锁即可解决,放那已上线验证有效。
在这里插入图片描述

总结

这类多线程引起的字节码异常问题潜伏期可达到数年之久,例如本文遇到的问题在App的页面量级较低时几乎不会触发,但随着App的业务规模增长,又或是打包机器的一次升级换代,问题就会悄然出现,而他极低的复现概率和随机性又很容易使其被忽视。

字节码异常问题在互联网鲜有参考资料,倘若字节码损坏直接崩溃还则罢了,遇到这种恰巧能被当成其他语句继续执行的情况分析起来着实麻烦。因此开发插桩这类涉及代码编辑操作的plugin,针对"写”操作务必要慎重开发,重点测试下极端并发的场景。这类问题如果是发生在定时大量推送的活动页或者热修sdk之类稳定性兜底的功能,其危害可想而知。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/63457.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

React v19稳定版发布12.5

🤖 作者简介:水煮白菜王 ,一位资深前端劝退师 👻 👀 文章专栏: 前端专栏 ,记录一下平时在博客写作中,总结出的一些开发技巧✍。 感谢支持💕💕💕 目…

Android笔记【17】返回数据的两种方法

目录 一、问题 二、具体分析 1、代码 2、区别 1. 目的和使用场景 resultLauncher startActivity 2. 数据传递方式 3. 返回结果的管理 4. 代码示例对比 使用 resultLauncher 启动活动并处理返回结果: 使用 startActivity 启动活动(不处理返回&…

flutter修改状态栏学习

在flutter中如何动态更改状态栏的颜色和风格。 前置知识点学习 AnnotatedRegion AnnotatedRegion 是 Flutter 中的一个小部件,用于在特定区域中提供元数据(metadata)以影响某些系统级的行为或外观。它通常用于改变系统 UI 的外观&#xff…

功能篇:JAVA使用jwt

在Java中实现JWT(JSON Web Token)认证通常涉及以下几个步骤: 1. 添加依赖 2. 创建JWT工具类 3. 实现登录接口,生成JWT 4. 实现过滤器,验证JWT ### 1. 添加依赖 首先,你需要在项目中添加JWT库的依赖。如果…

Chrome扩展程序开发示例

项目文件夹内文件如下: manifest.json文件内容: {"manifest_version": 3,"name": "我的法宝","description": "我的有魔法的宝贝","version": "1.0","icons": {"…

前端知识1html

VScode一些快捷键 Ctrl/——注释 !——生成html框架元素 *n——生成n个标签 直接书写html的名字回车生成对应的标签 常见标签 span&#xff1a; <span style"color: red;">hello</span> <span>demo</span> span实现&#xff1a; 标题…

计算机键盘简史 | 键盘按键功能和指法

注&#xff1a;本篇为 “计算机键盘简史 | 键盘按键功能和指法” 相关文章合辑。 英文部分机翻未校。 The Evolution of Keyboards: From Typewriters to Tech Marvels 键盘的演变&#xff1a;从打字机到技术奇迹 Introduction 介绍 The keyboard has journeyed from a humb…

mongoDb的读session和写session权限报错问题

go在使用mongoDb时用到了全局会话&#xff0c;发现在创建的session的逻辑相同&#xff0c;首先会进行数据的查询&#xff0c;此时获取了全局session执行读操作&#xff0c;查询所有文档&#xff0c;则当前会话为读会话&#xff0c;当再去插入时发现会报错&#xff0c;此时sessi…

【C++】求第二大的数详细解析

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目描述&#x1f4af;输入描述&#x1f4af;解题思路分析1. 题目核心要求2. 代码实现与解析3. 核心逻辑逐步解析定义并初始化变量遍历并处理输入数据更新最大值与次大值输…

redis-stack redisSearch环境安装搭建

RedisSearch在redis许可证变更之后显得是redis中的一大特色&#xff0c;闲来无事学习记录一下。 尝试通过源码编译redisSearch&#xff0c;貌似非常费劲&#xff0c;所以建议使用docker或者Linux的发行包进行安装redis-stack。redis-stack是基于redis的模块化机制进行一个扩展…

JavaCV录屏到网络流

1、pom文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.…

scala 编写 hdfs 工具类

scala 编写 hdfs 工具类 scala 创建 删除 hdfs 文件或目录 scala 上传 下载 hdfs 文件 scala 读取 写入 hdfs 文件 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi&quo…

从零用java实现 小红书 springboot vue uniapp (1)

前言 偶尔会用小红书发一些笔记 闲来无事 想自己实现一个小红书 正好可以学习一下 帖子 留言 im 好友 推送 等功能 下面我们就从零 开发一个小红书 后台依旧用我们的会员系统的脚手架 演示 http://120.26.95.195:8889/ 客户端我们使用uniapp 我们首先对主页进行一个分解 顶部我…

Cesium 按区域生成高度图

Cesium 按区域生成高度图 Cesium 按区域生成高度图 const cmd new CustomDrawCommand({vertexArray,shaderProgram,commandType: Compute,outputTexture: bufferColor,uniformMap,postExecute: () > {const url getImageByTexture(bufferAColor, gl);viewer.scene.primiti…

RESTful API设计原则与最佳实践

在当今的数字化时代&#xff0c;应用程序编程接口&#xff08;API&#xff09;已成为企业间数据交换、系统集成和业务扩展的关键工具。RESTful API作为一种基于HTTP协议的轻量级、无状态、可扩展的架构设计风格&#xff0c;在Web服务、移动应用、物联网等多个领域得到了广泛应用…

SpringMVC全局异常处理

一、Java中的异常 定义&#xff1a;异常是程序在运行过程中出现的一些错误&#xff0c;使用面向对象思想把这些错误用类来描述&#xff0c;那么一旦产生一个错误&#xff0c;即创建某一个错误的对象&#xff0c;这个对象就是异常对象。 类型&#xff1a; 声明异常&#xff1…

最小绝对偏差(Least Absolute Deviation, LAD)---子梯度法

最小绝对偏差&#xff08;Least Absolute Deviations&#xff0c;简称LAD&#xff09;是一种用于回归分析的统计方法&#xff0c;其目标是最小化残差的绝对值之和&#xff0c;而不是最小二乘法中的残差平方和。LAD回归特别适用于存在异常值的数据集&#xff0c;因为它对异常值不…

Linux - 进程等待和进程替换

进程等待 前面我们了解了如果父进程没有回收子进程, 那么当子进程接收后, 就会一直处于僵尸状态, 导致内存泄漏, 那么我们如何让父进程来回收子进程的资源. waitpid 我们可以通过 Linux 提供的系统调用函数 wait 系列函数来等待子进程死亡, 并回收资源. #include <sys/t…

mac下载安装jdk

背景 长时间不折腾mac全部忘记 特此记录 安装 1.下载jdk 根据需要下载对应的jdk 我直接 下载到/Applicatiions目录 https://www.oracle.com/java/technologies/downloads/#java8-mac 2.解压 cd /Applicatiions tar -zxvf jdk-8u431-macosx-x64.tar.gz 3.配置环境 …

【Java】—— 图书管理系统

基于往期学习的类和对象、继承、多态、抽象类和接口来完成一个控制台版本的 “图书管理系统” 在控制台界面中实现用户与程序交互 任务目标&#xff1a; 1、系统中能够表示多本图书的信息 2、提供两种用户&#xff08;普通用户&#xff0c;管理员&#xff09; 3、普通用户…