Android P内联优化导致的一个诡异Bug
问题背景
最近,我们的业务在动态加载一款第三方游戏时出现了奇怪的现象,本地开发测试体验良好,但是使用CI构建的正式包体验时会出现启动闪退。
问题分析
分析日志
首先,我们自然而然看了下Crash日志,如下:
--------- beginning of crash
2020-04-16 09:19:07.724 31492-31596/? E/AndroidRuntime: FATAL EXCEPTION: PriorityExecutor #1Process: com.example.脱敏:脱敏00, PID: 31492java.lang.RuntimeException: An error occured while executing doInBackground()at com.youzu.android.framework.task.PriorityAsyncTask$2.done(PriorityAsyncTask.java:87)at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:383)at java.util.concurrent.FutureTask.setException(FutureTask.java:252)at java.util.concurrent.FutureTask.run(FutureTask.java:271)at com.youzu.android.framework.task.PriorityRunnable.run(PriorityRunnable.java:16)at java.util.concurrent.ThreadPoolExecutor.processTask(ThreadPoolExecutor.java:1187)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)at java.lang.Thread.run(Thread.java:784)Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.net.URI.toString()' on a null object referenceat com.youzu.android.framework.http.HttpHandler.sendRequest(HttpHandler.java:183)at com.youzu.android.framework.http.HttpHandler.doInBackground(HttpHandler.java:212)at com.youzu.android.framework.http.HttpHandler.doInBackground(HttpHandler.java:52)at com.youzu.android.framework.task.PriorityAsyncTask$1.call(PriorityAsyncTask.java:74)at java.util.concurrent.FutureTask.run(FutureTask.java:266)at com.youzu.android.framework.task.PriorityRunnable.run(PriorityRunnable.java:16) at java.util.concurrent.ThreadPoolExecutor.processTask(ThreadPoolExecutor.java:1187) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) at java.lang.Thread.run(Thread.java:784)
是游戏里面产生的Crash,这时候问题就产生了:为什么debug包可以正常启动,release包就不行呢?
经过反复测试,我们发现是debuggable
这个字段在作祟。那么问题就变成了:为什么我们宿主的debuggable
为false
时会导致被加载的游戏Crash?
同时,在Crash日志之外,我们还发现一个系统打印的Error
级别的日志:
Maybe bug 77342775, looking for Lorg/apache/http/HttpEntity; 0x14021040[continuous;main space (region space)] defined in /system/framework/org.apache.http.legacy.boot.jar/0xe8aa5300with loader: com.tencent.脱敏.core.脱敏ClassLoader/0xdfa67020[hit:continuous;main space (region space)];java.lang.BootClassLoader/0xe8aa0260in interface table for Ljava/util/concurrent/locks/ReentrantLock; 0x6f101c78[image;/data/dalvik-cache/arm/system@framework@boot.art;+;0x6f080000] defined in /system/framework/core-oj.jar/0xe8aa3380 ifcount=2with loader BootClassLoaderiface #0: java.util.concurrent.locks.Lockiface #1: java.io.Serializable
但是我们逆向了这个游戏之后,发现这几个类根本对应不上!!HttpEntity
和Lock
没有任何关联。
缩小范围
一时没有头绪,于是我们做了以下实验来缩小问题范围:
•release包加载,debug包覆盖安装,打开游戏失败•debug包加载,release包覆盖安装,打开游戏成功•8.0加载,打开成功;9.0加载,打开失败•华为9.0、小米9.0均会出现这个错误•其他游戏release包加载后也能打开
由此可以得出三个结论:
•这个问题产生的位置是加载,而不是打开•这个问题是Android P普遍存在的一个问题•问题游戏包一定有什么特殊的代码存在(Apache)
第二点意味着我们可以从Android的源码里面挖掘一些信息了!!!
发现突破点
既然是加载阶段的问题,那我们就比较一下加载阶段的产物,果然有重大发现:
这时候,我们在审视一下之前的日志:
Maybe bug 77342775, looking for Lorg/apache/http/HttpEntity; 0x14021040[continuous;main space (region space)] defined in /system/framework/org.apache.http.legacy.boot.jar/0xe8aa5300with loader: com.tencent.脱敏.core.脱敏ClassLoader/0xdfa67020[hit:continuous;main space (region space)];java.lang.BootClassLoader/0xe8aa0260in interface table for Ljava/util/concurrent/locks/ReentrantLock; 0x6f101c78[image;/data/dalvik-cache/arm/system@framework@boot.art;+;0x6f080000] defined in /system/framework/core-oj.jar/0xe8aa3380 ifcount=2with loader BootClassLoaderiface #0: java.util.concurrent.locks.Lockiface #1: java.io.Serializable
这个日志可以在源码里面找到,以下是相关代码:
// http://androidxref.com/9.0.0_r3/xref/art/runtime/debug_print.cc#132
void DumpB77342775DebugData(ObjPtr<mirror::Class> target_class, ObjPtr<mirror::Class> src_class) {......LOG(ERROR) << "Maybe bug 77342775, looking for " << target_descriptor<< " " << target_class.Ptr() << "[" << DescribeSpace(target_class) << "]"<< " defined in " << target_class->GetDexFile().GetLocation()<< "/" << static_cast<const void*>(&target_class->GetDexFile())<< "\n with loader: " << DescribeLoaders(target_class->GetClassLoader(), target_descriptor);if (target_class->IsInterface()) {ObjPtr<mirror::IfTable> iftable = src_class->GetIfTable();CHECK(iftable != nullptr);size_t ifcount = iftable->Count();LOG(ERROR) << " in interface table for " << source_descriptor<< " " << src_class.Ptr() << "[" << DescribeSpace(src_class) << "]"<< " defined in " << src_class->GetDexFile().GetLocation()<< "/" << static_cast<const void*>(&src_class->GetDexFile())<< " ifcount=" << ifcount<< "\n with loader " << DescribeLoaders(src_class->GetClassLoader(), source_descriptor);......
} // namespace art// http://androidxref.com/9.0.0_r3/xref/art/runtime/common_throws.cc#272
void ThrowIncompatibleClassChangeErrorClassForInterfaceSuper(ArtMethod* method,ObjPtr<mirror::Class> target_class,ObjPtr<mirror::Object> this_object,ArtMethod* referrer) {// Referrer is calling interface_method on this_object, however, the interface_method isn't// implemented by this_object.CHECK(this_object != nullptr);std::ostringstream msg;msg << "Class '" << mirror::Class::PrettyDescriptor(this_object->GetClass())<< "' does not implement interface '" << mirror::Class::PrettyDescriptor(target_class)<< "' in call to '"<< ArtMethod::PrettyMethod(method) << "'";DumpB77342775DebugData(target_class, this_object->GetClass());ThrowException("Ljava/lang/IncompatibleClassChangeError;",referrer != nullptr ? referrer->GetDeclaringClass() : nullptr,msg.str().c_str());
}// http://androidxref.com/9.0.0_r3/xref/art/runtime/entrypoints/entrypoint_utils-inl.h#432
template<InvokeType type, bool access_check>
inline ArtMethod* FindMethodFromCode(uint32_t method_idx,ObjPtr<mirror::Object>* this_object,ArtMethod* referrer,Thread* self) {ClassLinker* const class_linker = Runtime::Current()->GetClassLinker();constexpr ClassLinker::ResolveMode resolve_mode =access_check ? ClassLinker::ResolveMode::kCheckICCEAndIAE: ClassLinker::ResolveMode::kNoChecks;ArtMethod* resolved_method;......// It is an interface.if (access_check) {if (!method_reference_class->IsAssignableFrom(h_this->GetClass())) {ThrowIncompatibleClassChangeErrorClassForInterfaceSuper(resolved_method,method_reference_class,h_this.Get(),referrer);return nullptr; // Failure.}}......
}
同时,结合谷歌官方文档关于Android P的一项变更:
//https://developer.android.com/about/versions/pie/android-9.0-changes-28#apache-p
在 Android 6.0 中,我们移除了对 Apache HTTP 客户端的支持。从 Android 9 开始,该内容库已从 bootclasspath 中移除,且默认情况下应用无法使用它。
至此,问题似乎有了一个靠谱的怀疑方向:
被加载的游戏包内置了一个Apache的jar包,但是里面什么也没有,纯粹为了编译,我们加载的时候这个jar包里面的类发生了内联优化,真正打开的时候,我们又去模拟系统把真正的Apache的jar包放在最前面,这样游戏包就会拿着发生了内联的方法索引,去没有做相同内联优化的系统的Apache的jar包里面寻找对应方法,由此发生了错误。
这样,Maybe bug 77342775
这个错误,为什么发生在Android 9版本,为什么debug包被release覆盖安装后没问题、为什么odex的大小发生了变化、为什么其他游戏包没有问题就都可以解释了。
但是不是这样呢?
系统安装
如果是这样,系统安装为什么没问题呢?于是,我们抓取了系统安装、debug加载、release加载的日志,如下:
//系统安装
I/dex2oat: /system/bin/dex2oat --input-vdex-fd=-1 --output-vdex-fd=31 --compiler-filter=speed-profile -j4 --classpath-dir=/data/app/com.tencent.tmgp.youzu.脱敏-oJz4mDEvj93nM4l8cFRHMA== --class-loader-context=PCL[/system/framework/org.apache.http.legacy.boot.jar] --generate-mini-debug-info --compact-dex-level=none --compilation-reason=install// debug加载
I/dex2oat: The ClassLoaderContext is a special shared library.
I/dex2oat: /system/bin/dex2oat --debuggable -j4 --debuggable --generate-mini-debug-info --dex-file=/data/user/0/com.example.脱敏/脱敏/com.tencent.tmgp.youzu.脱敏/apk/base-1.apk --output-vdex-fd=64 --oat-fd=66 --oat-location=/data/user/0/com.example.脱敏/脱敏/com.tencent.tmgp.youzu.脱敏/apk/oat/arm/base-1.odex --compiler-filter=quicken --class-loader-context=& --compilation-reason=dynamic-load
I/dex2oat: runtime mdmPath /system/emui/base/jar/Mdm/hw_mdm_framework.jar
W/dex2oat: Accessing hidden field Landroid/graphics/drawable/Drawable;->DEFAULT_TINT_MODE:Landroid/graphics/PorterDuff$Mode; (dark greylist, linking)
I/dex2oat: dex2oat took 1.719s (3.040s cpu) (threads: 4) arena alloc=252KB (258240B) java alloc=5MB (5400672B) native alloc=6MB (7064328B) free=4MB (4994296B)//release加载
I/dex2oat: The ClassLoaderContext is a special shared library.
I/dex2oat: /system/bin/dex2oat -j4 --dex-file=/data/user/0/com.example.脱敏/脱敏/com.tencent.tmgp.youzu.脱敏/apk/base-1.apk --output-vdex-fd=55 --oat-fd=57 --oat-location=/data/user/0/com.example.脱敏/脱敏/com.tencent.tmgp.youzu.脱敏/apk/oat/arm/base-1.odex --compiler-filter=quicken --class-loader-context=& --compilation-reason=dynamic-load
I/dex2oat: runtime mdmPath /system/emui/base/jar/Mdm/hw_mdm_framework.jar
W/dex2oat: Accessing hidden field Landroid/graphics/drawable/Drawable;->DEFAULT_TINT_MODE:Landroid/graphics/PorterDuff$Mode; (dark greylist, linking)
I/dex2oat: dex2oat took 2.276s (3.511s cpu) (threads: 4) arena alloc=376KB (385944B) java alloc=5MB (5384288B) native alloc=10MB (10731936B) free=3MB (3423840B)
通过查阅资料,我们发现--debuggable
这个参数,会阻止内联,所以debug包一直没有暴露这个问题,而系统之所以没有这个问题就在于--class-loader-context
这个字段,系统安装直接把真正的jar的路径作为上下文传过去了,所以就算内联索引也是能对上号了,我们就惨了~
问题解决
如果猜想正确,那么有以下几个方向:
•让游戏方不要把这个仅用于编译的jar包打包进去•我们也传一个--class-loader-context
的上下文•禁用dex2oat
首先第一个不太现实,第二个和第三个本质都是要修改Runtime的变量,第三个相对来说会简单一些(第二个的传递路径极其复杂,源码直接把我看懵逼了)。可能有人会觉得这个能力是系统提供的,禁用的是不是会影响性能?
我们查阅Android 10的行为变更会发现以下信息:
// https://developer.android.com/about/versions/10/behavior-changes-10?hl=zh-cn#system-only-oat
Android 运行时 (ART) 不再从应用进程调用 dex2oat。这项变更意味着 ART 将仅接受系统生成的 OAT 文件。
可能,谷歌也觉察到了开发者的各种骚操作会破坏用户体验,于是干脆不提供给应用进程了,本着先解决,后优化的思想,我们选择了第三种方案。
通过ART的源码可知:
// jni.h
jint GetJavaVM(JavaVM** vm) {return functions->GetJavaVM(this, vm);
}
可以通过这个jni方法拿到ART虚拟机的指针,进一步拿到运行时的指针。再看一下运行时的结构
// http://androidxref.com/9.0.0_r3/xref/art/runtime/runtime.h#840private:...static constexpr uint32_t kCalleeSaveSize = 6u;// 64 bit so that we can share the same asm offsets for both 32 and 64 bits.uint64_t callee_save_methods_[kCalleeSaveSize];GcRoot<mirror::Throwable> pre_allocated_OutOfMemoryError_;GcRoot<mirror::Throwable> pre_allocated_NoClassDefFoundError_;ArtMethod* resolution_method_;ArtMethod* imt_conflict_method_;// Unresolved method has the same behavior as the conflict method, it is used by the class linker// for differentiating between unfilled imt slots vs conflict slots in superclasses.ArtMethod* imt_unimplemented_method_;// Special sentinel object used to invalid conditions in JNI (cleared weak references) and// JDWP (invalid references).GcRoot<mirror::Object> sentinel_;InstructionSet instruction_set_;QuickMethodFrameInfo callee_save_method_frame_infos_[kCalleeSaveSize];CompilerCallbacks* compiler_callbacks_;bool is_zygote_;bool must_relocate_;bool is_concurrent_gc_enabled_;bool is_explicit_gc_disabled_;bool dex2oat_enabled_;bool image_dex2oat_enabled_;
我们要修改的dex2oat_enabled_
前面有一些变量,于是我们可以用指针+偏移
拿到这个变量的指针地址,进而赋值,具体操作见这篇文章 art dex2oat 加载加速浅析[1]。
这个解法也存在一些风险,就是如果厂商修改了这个结构体,我们的偏移就错了,因此存在一定的兼容性问题。但是,也可以做一些校验预警,比如InstructionSet
的结构如下:
enum class InstructionSet {kNone,kArm,kArm64,kThumb2,kX86,kX86_64,kMips,kMips64,kLast = kMips64
};
那么如果指针偏移错了,那么instruction_set_
的值也很难落在0~7
这个区间
复盘总结
本文的分析总结处于一个上帝视角,其实解决这个问题的过程中,我和另外一个同事也是饱受煎熬,花了将近一周的时间才从千头万绪中找到真相。尤其是分析问题原因的时候,由于错误日志和原因之间的联系并不是那么明显,很多推测也无法立刻验证,中间的很多尝试由于篇幅都没有写出来,最后是反复通过阅读Android的源码和做对比实验才找到原因。
参考文章
•行为变更:以 API 级别 28 及更高级别为目标的应用 | Android 开发者 | Android Developers[2]•行为变更:以 API 29 及更高级别为目标平台的应用 | Android 开发者 | Android Developers[3]•Android老版本httpclient高版本兼容_移动开发_u011339364的专栏-CSDN博客[4]•行为变更:以 API 级别 28 及更高级别为目标的应用 | Android 开发者 | Android Developers[5]•article/ART下的方法内联策略及其对Android热修复方案的影响分析.md at master · WeMobileDev/article[6]•Android Q BaseDexClassLoader 变动 | 区长[7]•谈谈 Android P 行为变更与内联优化 | 区长[8]•application | Android 开发者 | Android Developers[9]•一种在ART上快速加载dex的方法 - 残页的小博客[10]•通告 | Android P新增检测项 应用热修复受重大影响
References
[1]
art dex2oat 加载加速浅析: https://fucknmb.com/2018/12/30/art-dex2oat%E5%8A%A0%E8%BD%BD%E5%8A%A0%E9%80%9F%E6%B5%85%E6%9E%90/
[2]
行为变更:以 API 级别 28 及更高级别为目标的应用 | Android 开发者 | Android Developers: https://developer.android.com/about/versions/pie/android-9.0-changes-28#apache-p
[3]
行为变更:以 API 29 及更高级别为目标平台的应用 | Android 开发者 | Android Developers: https://developer.android.com/about/versions/10/behavior-changes-10?hl=zh-cn
[4]
Android老版本httpclient高版本兼容_移动开发_u011339364的专栏-CSDN博客: https://blog.csdn.net/u011339364/article/details/103909478
[5]
行为变更:以 API 级别 28 及更高级别为目标的应用 | Android 开发者 | Android Developers: https://developer.android.com/about/versions/pie/android-9.0-changes-28
[6]
article/ART下的方法内联策略及其对Android热修复方案的影响分析.md at master · WeMobileDev/article: https://github.com/WeMobileDev/article/blob/master/ART%E4%B8%8B%E7%9A%84%E6%96%B9%E6%B3%95%E5%86%85%E8%81%94%E7%AD%96%E7%95%A5%E5%8F%8A%E5%85%B6%E5%AF%B9Android%E7%83%AD%E4%BF%AE%E5%A4%8D%E6%96%B9%E6%A1%88%E7%9A%84%E5%BD%B1%E5%93%8D%E5%88%86%E6%9E%90.md
[7]
Android Q BaseDexClassLoader 变动 | 区长: https://fucknmb.com/2019/04/04/Android-Q-BaseDexClassLoader%E5%8F%98%E5%8A%A8/
[8]
谈谈 Android P 行为变更与内联优化 | 区长: https://fucknmb.com/2019/04/03/%E8%B0%88%E8%B0%88Android-P%E8%A1%8C%E4%B8%BA%E5%8F%98%E6%9B%B4%E4%B8%8E%E5%86%85%E8%81%94%E4%BC%98%E5%8C%96/
[9]
application | Android 开发者 | Android Developers: https://developer.android.com/guide/topics/manifest/application-element.html#vmSafeMode
[10]
一种在ART上快速加载dex的方法 - 残页的小博客: https://canyie.github.io/2020/02/15/fast-load-dex-on-art-runtime/
Android P内联优化导致的一个诡异Bug相关推荐
- android内联优化导致Inlined method resolution crossed dex file boundary
最近App在android11上出现了一个诡异的native 崩溃,很不容易出现,但都是有个特点就是安装App后过一段时间才会出现,杀进程没用,覆盖安装同一个apk,崩溃立刻消失,日志为如下: 124 ...
- Iar环境c语言调用汇编函数,如何在IAR EWARM中通过内联汇编程序在另一个模块中调用C函数?...
我在硬故障处理程序中有一些程序集.程序集基本上是为了传递当前堆栈指针作为参数(在R0中).它看起来像这样...如何在IAR EWARM中通过内联汇编程序在另一个模块中调用C函数? __asm(&quo ...
- go 基准测试 找不到函数_Go 中的内联优化 | Linux 中国
本文讨论 Go 编译器是如何实现内联的,以及这种优化方法如何影响你的 Go 代码.https://linux.cn/article-12176-1.html作者:Dave Cheney译者:Xiaob ...
- 【JVM】JVM 内联优化
文章目录 1.概念 2.过程 3.场景 4.案例 5.案例2 6.java内联配置 7.方法内联的规则 8.案例 8.1 内联案例1 1.概念 内联概念:把函数调用的方法直接内嵌到方法内部,减少函数调 ...
- VC 内联汇编中的一个注意事项
2019独角兽企业重金招聘Python工程师标准>>> 为了方便 有时候在汇编里面直接引用 函数的参数 这样是可行的 但是要注意 编译器默认使用 ebp 作为标准来寻址 所以 需要前 ...
- 内联函数和编译器对Go代码的优化
什么是内联函数 图片版权:Renee French. 在很多讲 Go 语言底层的技术资料和博客里都会提到内联函数这个名词,也有人把内联函数说成代码内联.函数展开.展开函数等等,其实想表达的都是 Go ...
- Javascript性能优化【内联缓存】 V8引擎特性
javascript 是单线程.动态类型语言,那么我们在编码时候如何编写性能最优代码呢?下面将讲解V8引擎的内联优化.利用内联缓存这个特性我们可以编写更加优秀的代码. 什么是内联缓存 引用官方的描述: ...
- 提高C++性能的编程技术笔记:内联+测试代码
内联类似于宏,在调用方法内部展开被调用方法,以此来代替方法的调用.一般来说表达内联意图的方式有两种:一种是在定义方法时添加内联保留字的前缀:另一种是在类的头部声明中定义方法. 虽然内联方法的调用方式和 ...
- C++对象模型9——临时对象的生命周期、模板及实例化分析、内联函数
一.临时对象的生命周期 T c=a+b 假设T是一个类型,那么上述代码执行时,首先会产生一个临时对象用来存放a+b的结果(拷贝初始化临时对象),然后用该临时对象拷贝初始化c,最后临时对象被释放.如果开 ...
最新文章
- 移动端,input输入框被手机输入法解决方案
- 21. Leetcode 203. 移除链表元素 (链表-基础操作类-删除链表的节点)
- Flink从入门到精通100篇(二十一)-万字长文详解 Flink 中的 CopyOnWriteStateTable
- QTcpServer / QTcpSocket 简单示例
- (转)前置++和后置++的区别
- Move_base理解
- 学习vim的正确姿势!
- java string 日期_java string类型日期比较
- androidstudio调用系统相机为什么resultcode一直返回0_函数递归调用?看这文就够了...
- 数据包络分析方法与maxdea软件_SEM常用的4种数据分析方法,让你的优化工作事半功倍!...
- yum安装:zabbix-web-4.2.8-1.el7.noarch: [Errno 256] No more mirrors to try
- 如何防范和应对Redis勒索,腾讯云教你出招
- 用shc加密shell脚本
- 【风电功率预测】基于matlab粒子群算法优化LSTM风电功率预测【含Matlab源码 941期】
- C语言基础—进制转换
- android中listview刷新数据,Android动态刷新listview中的数据?
- 传输层协议 ——— UDP协议
- linux系统查看usb转串口驱动,Linux下使用USB转串口驱动的方法
- 英语名言警句100句
- 威廉 哈特 史密斯《当你抚触》
热门文章
- iWebShop 电商项目实战001----测试环境搭建(上)
- ls -l 与 ls -a 、ls -al 的区别
- 拉格朗日启发式算法matlab,基于时间满意的最大覆盖选址问题
- 行为识别 - X3D: Expanding Architectures for Efficient Video Recognition
- 通达OA 通过程序直接处理手机提醒短信息及事务提醒(图文)
- Git查看具体代码提交记录
- 我的学习计划-2018
- ObjectARX® for Beginners: An Introduction
- 云从科技姚志强:聚焦To B 重视协同 我们只做擅长的事情
- 自然语言处理之中文语料收集