手机淘宝插件化框架Atlas在ART上首次启动的时候,会经过禁用dex2oat来达到插件迅速启动的目的。以后后台进行dex2oat,下次启动若是dex2oat完成了则启用dex2oat,若是没有完成则继续禁用dex2oat。可是这部分代码淘宝并无开源。且因为Atlas后续持续维护的可能性极低,加上Android 9.0上禁用失败及64位动态库在部分系统上禁用会发生crash。此文结合逆向与正向的角度来分析Atlas是经过什么手段达到禁用dex2oat的,以及微店App是如何实践达到禁用的目的。java
因为手淘Atlas这部分代码是闭源的,所以咱们没法正向分析其原理。因此咱们能够从逆向的角度进行分析。逆向分析的关键一步就是懂得看控制台日志,从日志中入手进行分析。android
经过在Android 5.0,Android 6.0,Android 7.0,Android 8.0 和 Android 9.0上运行插件化的App,咱们发现,控制台会输出一部分关键性的日志。内容以下git
经过在AOSP中查找关键日志 Generation of oat file .... not attempt because dex2oat is disabled 便可继续发现猫腻。最终咱们会发现这部分信息出如今了class_linker.cc类或者oat_file_manager.cc类中。github
有了以上基础,咱们尝试从源码角度进行正向分析。bootstrap
在Java层咱们加载一个Dex是经过DexFile.loadDex()方法进行加载。此方法最终会走到native方法 openDexFileNative,Android 5.0的源码以下c#
static jlong DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {
ScopedUtfChars sourceName(env, javaSourceName);
if (sourceName.c_str() == NULL) {
return 0;
}
NullableScopedUtfChars outputName(env, javaOutputName);
if (env->ExceptionCheck()) {
return 0;
}
ClassLinker* linker = Runtime::Current()->GetClassLinker();
std::unique_ptr<std::vector<const DexFile*>> dex_files(new std::vector<const DexFile*>());
std::vector<std::string> error_msgs;
//关键调用在这里
bool success = linker->OpenDexFilesFromOat(sourceName.c_str(), outputName.c_str(), &error_msgs,
dex_files.get());
if (success || !dex_files->empty()) {
// In the case of non-success, we have not found or could not generate the oat file.
// But we may still have found a dex file that we can use.
return static_cast<jlong>(reinterpret_cast<uintptr_t>(dex_files.release()));
} else {
// The vector should be empty after a failed loading attempt.
DCHECK_EQ(0U, dex_files->size());
ScopedObjectAccess soa(env);
CHECK(!error_msgs.empty());
// The most important message is at the end. So set up nesting by going forward, which will
// wrap the existing exception as a cause for the following one.
auto it = error_msgs.begin();
auto itEnd = error_msgs.end();
for ( ; it != itEnd; ++it) {
ThrowWrappedIOException("%s", it->c_str());
}
return 0;
}
}
复制代码
最终会调用到ClassLinker中的OpenDexFilesFromOat方法缓存
对应代码过长,这里不贴了,见bash
OpenDexFilesFromOat函数主要作了以下几步数据结构
首次打开时,1-3步必然是不知足的,最终会走到第四个逻辑,这一步有一个关键性的代码直接决定了生成oat文件是否生成成功app
if (Runtime::Current()->IsDex2OatEnabled() && has_flock && scoped_flock.HasFile()) {
// Create the oat file.
open_oat_file.reset(CreateOatFileForDexLocation(dex_location, scoped_flock.GetFile()->Fd(),
oat_location, error_msgs));
}
复制代码
核心函数Runtime::Current()->IsDex2OatEnabled(),判断dex2oat是否开启,若是开启,则建立oat文件并进行更新。
以上是Android 5.0的源码,Android 6.0-Android 9.0会有所差别。DexFile_openDexFileNative最终会调用到runtime->GetOatFileManager().OpenDexFilesFromOat(),继续会调用到OatFileAssistant类中的MakeUpToDate函数,一直调用到GenerateOatFile(Androiod 6.0-7.0)或GenerateOatFileNoChecks(Android 8.0-9.0)等类型函数,相关代码见以下连接。
最终咱们也会发现一段关键性的代码,以下
Runtime* runtime = Runtime::Current();
if (!runtime->IsDex2OatEnabled()) {
*error_msg = "Generation of oat file for dex location " + dex_location_
+ " not attempted because dex2oat is disabled.";
return kUpdateNotAttempted;
}
复制代码
能够看到,咱们已经看到了咱们逆向日志分析时,从控制台看到的日志内容,Generation of oat file....not attempted because dex2oat is disabled,这说明咱们源码找对了。
经过以上分析,咱们发现Android 5.0-Android 9.0最终都会走到Runtime::Current()->IsDex2OatEnabled()函数,若是dex2oat没有开启,则不会进行后续oat文件生成的操做,而是直接return返回。因此结论已经很明确了,就是经过设置该函数的返回值为false,达到禁用dex2oat的目的。
经过查看Runtime类的代码,能够发现IsDex2OatEnabled其实很简单,就是返回了一个dex2oat_enabled_成员变量与另外一个image_dex2oat_enabled_成员变量。源码见:
bool IsDex2OatEnabled() const {
return dex2oat_enabled_ && IsImageDex2OatEnabled();
}
bool IsImageDex2OatEnabled() const {
return image_dex2oat_enabled_;
}
复制代码
所以最终咱们的目的就很明确了,只要把成员变量dex2oat_enabled_的值和image_dex2oat_enabled_的值进行修改,将它们修改为false,就达到了直接禁用的目的。若是要从新开启,则从新还原他们的值为true便可,默认状况下,该值始终是true。
不过通过验证后发现手淘Atlas是经过禁用IsImageDex2OatEnabled()达到目的的,即它是经过修改image_dex2oat_enabled_而不是dex2oat_enabled_,这一点在兼容性方面十分重要,在必定程度上保障了部分机型的兼容性(好比一加,8.0以后加入了一个变量,致使数据结构向后偏移1字节;VIVO/OPPO部分机型加入变量,致使数据结构向后偏移1字节),所以为了保持策略上的一致性,咱们只修改image_dex2oat_enabled_,不修改dex2oat_enabled_。
有了以上理论基础,咱们必须进行实践,用结论验证猜测,才会有说服力了。
上面已经说到咱们只须要修改Runtime中image_dex2oat_enabled_成员变量的值,将其对应的image_dex2oat_enabled_变量修改成false便可。
所以第一步咱们须要拿到这个Runtime的地址。
在JNI中,每个Java中的native方法对应的jni函数,都有一个JNIEnv* 指针入参,经过该指针变量的GetJavaVM函数,咱们能够拿到一个JavaVM*的指针变量
JavaVM *javaVM;
env->GetJavaVM(&javaVM);
复制代码
而JavaVm在JNI中的数据结构定义为(源码地址见 android-9.0.0_r20/include_jni/jni.h)
typedef _JavaVM JavaVM;
struct _JavaVM {
const struct JNIInvokeInterface* functions;
};
复制代码
能够看到,只有一个JNIInvokeInterface*指针变量
而在Android中,实际使用的是JavaVMExt(源码地址见 android-9.0.0_r20/runtime/java_vm_ext.h),它继承了JavaVM,它的数据结构能够简单理解为
class JavaVMExt : public JavaVM {
private:
Runtime* const runtime_;
}
复制代码
根据内存布局,咱们能够将JavaVMExt等效定义为
struct JavaVMExt {
void *functions;
void *runtime;
};
复制代码
指针类型,在32位上占4字节,在64位上占8字节。
所以咱们只须要将咱们以前拿到的JavaVM *指针,强制转换为JavaVMExt*指针,经过JavaVMExt*指针拿到Runtime*指针
JavaVM *javaVM;
env->GetJavaVM(&javaVM);
JavaVMExt *javaVMExt = (JavaVMExt *) javaVM;
void *runtime = javaVMExt->runtime;
复制代码
剩下的事就很是简单了,咱们只须要将Runtime数据结构从新定义一遍,这里值得注意的是Android各版本Runtime数据结构不一致,因此须要进行区分,这里以Android 9.0为例。
/**
* 9.0, GcRoot中成员变量是class类型,因此用int代替GcRoot
*/
struct PartialRuntime90 {
// 64 bit so that we can share the same asm offsets for both 32 and 64 bits.
uint64_t callee_save_methods_[kCalleeSaveSize90];
int pre_allocated_OutOfMemoryError_;
int pre_allocated_NoClassDefFoundError_;
void *resolution_method_;
void *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.
void *imt_unimplemented_method_;
// Special sentinel object used to invalid conditions in JNI (cleared weak references) and
// JDWP (invalid references).
int sentinel_;
InstructionSet instruction_set_;
QuickMethodFrameInfo callee_save_method_frame_infos_[kCalleeSaveSize90]; // QuickMethodFrameInfo = uint32_t * 3
void *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_;
std::string compiler_executable_;
std::string patchoat_executable_;
std::vector<std::string> compiler_options_;
std::vector<std::string> image_compiler_options_;
std::string image_location_;
std::string boot_class_path_string_;
std::string class_path_string_;
std::vector<std::string> properties_;
};
复制代码
注意,尤为须要注意内部布局中存在对齐问题,即 1、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍) 2、结构体大小必须是全部成员大小的整数倍。
因此咱们必须完整的定义原数据结构,不能存在偏移。不然结构体地址就会错乱。
以后将runtime强制转换为PartialRuntime90*便可
PartialRuntime90 *partialRuntime = (PartialRuntime90 *) runtime;
复制代码
拿到PartialRuntime90以后,直接修改该数据结构中的image_dex2oat_enabled_便可完成禁用
partialRuntime->image_dex2oat_enabled_ = false
复制代码
不过这整个流程须要注意几个问题,经过兼容性测试报告反馈来看,存在了以下几个问题 一、Android 5.1-Android 9.0兼容性极好 二、Android 5.0存在部分产商自定义该数据结构,加入了成员致使image_dex2oat_enabled_向后偏移4字节,又或是部分产商Android 5.0使用了Android 5.1的数据结构致使。 三、部分x86的PAD运行arm的APP,此种场景十分特殊,所以咱们选择无视此种机型,不处理 四、考虑校验性问题,须要使用一个变量校验咱们是否寻址正确,进行适当降级操做,咱们选择以指令集变量instruction_set_做为参考。它是一个枚举变量,正常取值范围为int 类型 1-7,若是该值不知足,咱们选择不处理,避免没必要要的crash问题。 五、一旦寻址失败,咱们选择使用兜底策略进行重试,直接查找指令集变量instruction_set_偏移值,转换为另外一个公共的数据结构类型进行操做
这里贴出Android 5.0-9.0各系统Runtime的数据结构
/**
* 5.0,GcRoot中成员变量是指针类型,因此用void*代替GcRoot
*/
struct PartialRuntime50 {
void *callee_save_methods_[kCalleeSaveSize50]; //5.0 5.1 void *
void *pre_allocated_OutOfMemoryError_;
void *pre_allocated_NoClassDefFoundError_;
void *resolution_method_;
void *imt_conflict_method_;
void *default_imt_; //5.0 5.1
InstructionSet instruction_set_;
QuickMethodFrameInfo callee_save_method_frame_infos_[kCalleeSaveSize50]; // QuickMethodFrameInfo = uint32_t * 3
void *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_;
std::string compiler_executable_;
std::string patchoat_executable_;
std::vector<std::string> compiler_options_;
std::vector<std::string> image_compiler_options_;
std::string image_location_;
std::string boot_class_path_string_;
std::string class_path_string_;
std::vector<std::string> properties_;
};
/**
* 5.1,GcRoot中成员变量是指针类型,因此用void*代替GcRoot
*/
struct PartialRuntime51 {
void *callee_save_methods_[kCalleeSaveSize50]; //5.0 5.1 void *
void *pre_allocated_OutOfMemoryError_;
void *pre_allocated_NoClassDefFoundError_;
void *resolution_method_;
void *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.
void *imt_unimplemented_method_;
void *default_imt_; //5.0 5.1
InstructionSet instruction_set_;
QuickMethodFrameInfo callee_save_method_frame_infos_[kCalleeSaveSize50]; // QuickMethodFrameInfo = uint32_t * 3
void *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_;
std::string compiler_executable_;
std::string patchoat_executable_;
std::vector<std::string> compiler_options_;
std::vector<std::string> image_compiler_options_;
std::string image_location_;
std::string boot_class_path_string_;
std::string class_path_string_;
std::vector<std::string> properties_;
};
/**
* 6.0-7.1,GcRoot中成员变量是class类型,因此用int代替GcRoot
*/
struct PartialRuntime60 {
// 64 bit so that we can share the same asm offsets for both 32 and 64 bits.
uint64_t callee_save_methods_[kCalleeSaveSize50];
int pre_allocated_OutOfMemoryError_;
int pre_allocated_NoClassDefFoundError_;
void *resolution_method_;
void *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.
void *imt_unimplemented_method_;
// Special sentinel object used to invalid conditions in JNI (cleared weak references) and
// JDWP (invalid references).
int sentinel_;
InstructionSet instruction_set_;
QuickMethodFrameInfo callee_save_method_frame_infos_[kCalleeSaveSize50]; // QuickMethodFrameInfo = uint32_t * 3
void *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_;
std::string compiler_executable_;
std::string patchoat_executable_;
std::vector<std::string> compiler_options_;
std::vector<std::string> image_compiler_options_;
std::string image_location_;
std::string boot_class_path_string_;
std::string class_path_string_;
std::vector<std::string> properties_;
};
/**
* 8.0-8.1, GcRoot中成员变量是class类型,因此用int代替GcRoot
*/
struct PartialRuntime80 {
// 64 bit so that we can share the same asm offsets for both 32 and 64 bits.
uint64_t callee_save_methods_[kCalleeSaveSize80];
int pre_allocated_OutOfMemoryError_;
int pre_allocated_NoClassDefFoundError_;
void *resolution_method_;
void *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.
void *imt_unimplemented_method_;
// Special sentinel object used to invalid conditions in JNI (cleared weak references) and
// JDWP (invalid references).
int sentinel_;
InstructionSet instruction_set_;
QuickMethodFrameInfo callee_save_method_frame_infos_[kCalleeSaveSize80]; // QuickMethodFrameInfo = uint32_t * 3
void *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_;
std::string compiler_executable_;
std::string patchoat_executable_;
std::vector<std::string> compiler_options_;
std::vector<std::string> image_compiler_options_;
std::string image_location_;
std::string boot_class_path_string_;
std::string class_path_string_;
std::vector<std::string> properties_;
};
/**
* 9.0, GcRoot中成员变量是class类型,因此用int代替GcRoot
*/
struct PartialRuntime90 {
// 64 bit so that we can share the same asm offsets for both 32 and 64 bits.
uint64_t callee_save_methods_[kCalleeSaveSize90];
int pre_allocated_OutOfMemoryError_;
int pre_allocated_NoClassDefFoundError_;
void *resolution_method_;
void *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.
void *imt_unimplemented_method_;
// Special sentinel object used to invalid conditions in JNI (cleared weak references) and
// JDWP (invalid references).
int sentinel_;
InstructionSet instruction_set_;
QuickMethodFrameInfo callee_save_method_frame_infos_[kCalleeSaveSize90]; // QuickMethodFrameInfo = uint32_t * 3
void *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_;
std::string compiler_executable_;
std::string patchoat_executable_;
std::vector<std::string> compiler_options_;
std::vector<std::string> image_compiler_options_;
std::string image_location_;
std::string boot_class_path_string_;
std::string class_path_string_;
std::vector<std::string> properties_;
};
复制代码
数据结构转换完成后,咱们须要进行简单的校验,只须要找到一个特征进行校验,这里咱们校验指令集变量instruction_set_是否取值正确,该值是一个枚举,正常取值范围1-7
/**
* instruction set
*/
enum class InstructionSet {
kNone,
kArm,
kArm64,
kThumb2,
kX86,
kX86_64,
kMips,
kMips64,
kLast,
};
复制代码
只要该值不在范围内,则认为寻址失败
if (partialInstructionSetRuntime->instruction_set_ <= InstructionSet::kNone ||
partialInstructionSetRuntime->instruction_set_ >= InstructionSet::kLast) {
return NULL;
}
复制代码
寻址失败后,咱们经过运行期指令集特征变量进行重试查找
在C++中咱们能够经过宏定义,简单获取运行期的指令集
#if defined(__arm__)
static constexpr InstructionSet kRuntimeISA = InstructionSet::kArm;
#elif defined(__aarch64__)
static constexpr InstructionSet kRuntimeISA = InstructionSet::kArm64;
#elif defined(__mips__) && !defined(__LP64__)
static constexpr InstructionSet kRuntimeISA = InstructionSet::kMips;
#elif defined(__mips__) && defined(__LP64__)
static constexpr InstructionSet kRuntimeISA = InstructionSet::kMips64;
#elif defined(__i386__)
static constexpr InstructionSet kRuntimeISA = InstructionSet::kX86;
#elif defined(__x86_64__)
static constexpr InstructionSet kRuntimeISA = InstructionSet::kX86_64;
#else
static constexpr InstructionSet kRuntimeISA = InstructionSet::kNone;
#endif
复制代码
须要注意的是若是是InstructionSet::kArm,咱们须要优先将其转为成InstructionSet::kThumb2进行查找。若是C++中的运行期指令集变量查找失败,则咱们使用Java层获取的指令集变量进行查找
在Java中咱们经过反射能够获取运行期指令集
private static Integer currentInstructionSet = null;
enum InstructionSet {
kNone(0),
kArm(1),
kArm64(2),
kThumb2(3),
kX86(4),
kX86_64(5),
kMips(6),
kMips64(7),
kLast(8);
private int instructionSet;
InstructionSet(int instructionSet) {
this.instructionSet = instructionSet;
}
public int getInstructionSet() {
return instructionSet;
}
}
/**
* 当前指令集字符串,Android 5.0以上支持,如下返回null
*/
private static String getCurrentInstructionSetString() {
if (Build.VERSION.SDK_INT < 21) {
return null;
}
try {
Class<?> clazz = Class.forName("dalvik.system.VMRuntime");
Method currentGet = clazz.getDeclaredMethod("getCurrentInstructionSet");
return (String) currentGet.invoke(null);
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
/**
* 当前指令集枚举int值,Android 5.0以上支持,如下返回0
*/
private static int getCurrentInstructionSet() {
if (currentInstructionSet != null) {
return currentInstructionSet;
}
try {
String invoke = getCurrentInstructionSetString();
if ("arm".equals(invoke)) {
currentInstructionSet = InstructionSet.kArm.getInstructionSet();
} else if ("arm64".equals(invoke)) {
currentInstructionSet = InstructionSet.kArm64.getInstructionSet();
} else if ("x86".equals(invoke)) {
currentInstructionSet = InstructionSet.kX86.getInstructionSet();
} else if ("x86_64".equals(invoke)) {
currentInstructionSet = InstructionSet.kX86_64.getInstructionSet();
} else if ("mips".equals(invoke)) {
currentInstructionSet = InstructionSet.kMips.getInstructionSet();
} else if ("mips64".equals(invoke)) {
currentInstructionSet = InstructionSet.kMips64.getInstructionSet();
} else if ("none".equals(invoke)) {
currentInstructionSet = InstructionSet.kNone.getInstructionSet();
}
} catch (Throwable e) {
currentInstructionSet = InstructionSet.kNone.getInstructionSet();
}
return currentInstructionSet != null ? currentInstructionSet : InstructionSet.kNone.getInstructionSet();
}
复制代码
在C++和JAVA层获取到指令集变量的值后,咱们经过该变量的值进行寻址
template<typename T>
int findOffset(void *start, int regionStart, int regionEnd, T value) {
if (NULL == start || regionEnd <= 0 || regionStart < 0) {
return -1;
}
char *c_start = (char *) start;
for (int i = regionStart; i < regionEnd; i += 4) {
T *current_value = (T *) (c_start + i);
if (value == *current_value) {
LOGE("found offset: %d", i);
return i;
}
}
return -2;
}
//若是是arm则优先使用kThumb2查找,查找不到则再使用arm重试
int isa = (int) kRuntimeISA;
int instructionSetOffset = -1;
instructionSetOffset = findOffset(runtime, 0, 100, isa == (int) InstructionSet::kArm
? (int) InstructionSet::kThumb2
: isa);
if (instructionSetOffset < 0 && isa == (int) InstructionSet::kArm) {
//若是是arm用thumb2查找失败,则使用arm重试查找
LOGE("retry find offset when thumb2 fail: %d", InstructionSet::kArm);
instructionSetOffset = findOffset(runtime, 0, 100, InstructionSet::kArm);
}
//若是kRuntimeISA找不到,则使用java层传入的currentInstructionSet,该值由java层反射获取到传入jni函数中
if (instructionSetOffset <= 0) {
isa = currentInstructionSet;
LOGE("retry find offset with currentInstructionSet: %d", isa == (int) InstructionSet::kArm
? (int) InstructionSet::kThumb2
: isa);
instructionSetOffset = findOffset(runtime, 0, 100, isa == (int) InstructionSet::kArm
? (int) InstructionSet::kThumb2 : isa);
if (instructionSetOffset < 0 && isa == (int) InstructionSet::kArm) {
LOGE("retry find offset with currentInstructionSet when thumb2 fail: %d",
InstructionSet::kArm);
//若是是arm用thumb2查找失败,则使用arm重试查找
instructionSetOffset = findOffset(runtime, 0, 100, InstructionSet::kArm);
}
if (instructionSetOffset <= 0) {
return NULL;
}
}
复制代码
查找到instructionSetOffset的地址偏移后,经过各系统的数据结构,计算出image_dex2oat_enabled_地址偏移便可,这里再也不详细说明。
当你以为一切很美好的时候,一个深坑忽然冒了出来,Xposed!因为Xposed运行期对art进行了hook,实际使用的是libxposed_art.so而不是libart.so,而且对应数据结构存在篡改现象,以5.0-6.0篡改的最为恶劣,其项目地址为 github.com/rovo89/andr…
5.0 runtime.h
bool is_recompiling_;
bool is_zygote_;
bool is_minimal_framework_;
bool must_relocate_;
bool is_concurrent_gc_enabled_;
bool is_explicit_gc_disabled_;
bool dex2oat_enabled_;
bool image_dex2oat_enabled_;
复制代码
5.1 runtime.h
bool is_recompiling_;
bool is_zygote_;
bool is_minimal_framework_;
bool must_relocate_;
bool is_concurrent_gc_enabled_;
bool is_explicit_gc_disabled_;
bool dex2oat_enabled_;
bool image_dex2oat_enabled_;
复制代码
6.0 runtime.h
bool is_zygote_;
bool is_minimal_framework_;
bool must_relocate_;
bool is_concurrent_gc_enabled_;
bool is_explicit_gc_disabled_;
bool dex2oat_enabled_;
bool image_dex2oat_enabled_;
复制代码
能够看到,在5.0和5.1上,数据结构多了is_recompiling_和is_minimal_framework_,实际image_dex2oat_enabled_存在向后偏移2字节的问题;在6.0上,数据结构多了is_minimal_framework_,实际image_dex2oat_enabled_存在向后偏移1字节的问题;而在Android 7.0及以上,暂时未存在篡改runtime.h的现象。所以可在native层判断是否存在xposed框架,存在则手动校准偏移值。
判断是否存在xposed函数以下
static bool initedXposedInstalled = false;
static bool xposedInstalled = false;
/**
* xposed是否安装
* /system/framework/XposedBridge.jar
* /data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar
*/
bool isXposedInstalled() {
if (initedXposedInstalled) {
return xposedInstalled;
}
if (!initedXposedInstalled) {
char *classPath = getenv("CLASSPATH");
if (classPath == NULL) {
xposedInstalled = false;
initedXposedInstalled = true;
return false;
}
char *subString = strstr(classPath, "XposedBridge.jar");
xposedInstalled = subString != NULL;
initedXposedInstalled = true;
return xposedInstalled;
}
return xposedInstalled;
}
复制代码
而后进行偏移校准,这里也再也不细说。
作到了如上的几步以后,其实兼容性是至关不错了,经过testin的兼容性测试能够看出,基本已经覆盖常见机型,可是因为testin的兼容性只能覆盖testin上约50%左右的机型,剩余50%机型没法覆盖到,所以我选择了人肉远程真机调试,覆盖剩余50%机型,通过验证后,对testin上99%+的机型都是支持的,且同时支持32位和64位动态库,在兼容性方面,已经远远超越Atlas。
在兼容性测试中,发现一部分机型runtime数据结构存在篡改问题,进一步验证了Atlas为何修改image_dex2oat_enabled_变量而不是修改dex2oat_enabled_变量,由于dex2oat_enabled_可能存在向后偏移一字节的问题(甚至是2字节,如xposed和一加9.0.2比较新的系统就存在2字节偏移),致使寻址错误,修改的实际上是其原来的地址(即现有真实地址的前一个字节),致使禁用失败。而经过修改image_dex2oat_enabled_变量,即便dex2oat_enabled_向后偏移一字节,因为修改的是image_dex2oat_enabled_,因此实际修改的其实就是dex2oat_enabled_如今偏移后的地址,实际上仍是达到了禁用的效果。这里有点绕,能够细细品味一下。这个操做,能够兼容大部分机型。
这里贴出一部分数据结构存在偏移的机型。
在art上首次加载插件,会经过禁用dex2oat达到加速效果,那么在dalvik上首次加载插件,其实也存在相似的问题,dalvik上是经过dexopt进行dex的优化操做,这个操做,也是比较耗时的,所以在dalvik上,须要一种相似于dex2oat的方式来达到禁用dex2opt的效果。通过验证后,发现Atlas是经过禁用verify达到必定的加速,所以咱们只须要禁用class verify便可。
源码以Android 4.4.4进行分析,见 android.googlesource.com/platform/da…
在Java层咱们加载一个Dex是经过DexFile.loadDex()方法进行加载。此方法最终会走到native方法 openDexFileNative,Android 4.4.4的源码以下
android.googlesource.com/platform/da…
最终会调用到dvmRawDexFileOpen或者dvmJarFileOpen
这两个方法,最终都会先查找缓存文件是否存在,若是不存在,最终都会调用到dvmOptimizeDexFile函数,见:
android.googlesource.com/platform/da…
而dvmOptimizeDexFile函数开头有这么一段逻辑
bool dvmOptimizeDexFile(int fd, off_t dexOffset, long dexLength,
const char* fileName, u4 modWhen, u4 crc, bool isBootstrap)
{
const char* lastPart = strrchr(fileName, '/');
if (lastPart != NULL)
lastPart++;
else
lastPart = fileName;
ALOGD("DexOpt: --- BEGIN '%s' (bootstrap=%d) ---", lastPart, isBootstrap);
pid_t pid;
/*
* This could happen if something in our bootclasspath, which we thought
* was all optimized, got rejected.
*/
//关键代码
if (gDvm.optimizing) {
ALOGW("Rejecting recursive optimization attempt on '%s'", fileName);
return false;
}
//此处省略n行代码
}
复制代码
也就是说gDvm.optimizing的值为true的时候,直接被return了,所以咱们只须要修改此值为true,便可达到禁用dexopt的目的,可是当设此值为true时,那全部dexopt操做都会发生IOException,致使类加载失败,存在crash风险,因此不能修改此值,看来只能修改class verify为不校验了,没有其余好的方法。事实证实,去掉这一步校验能够节约至少1倍的时间。
此外发现部分4.2.2和4.4.4存在数据结构偏移问题,可经过几个特征数据结构进行重试,从新定位关键数据结构进行重试。这里咱们经过 dexOptMode,classVerifyMode,registerMapMode,executionMode四个特征变量的取值范围进行重试定位,有兴趣自行研究一下,再也不细说。
经过查看源码发现gDvm是导出的,见 android.googlesource.com/platform/da…
extern struct DvmGlobals gDvm;
复制代码
所以咱们只须要借助dlopen和dlsym拿到整个DvmGlobals数据结构的起始地址,修改对应的变量的值便可。不过不幸的是,Android 4.0-4.4这个数据结构各版本都不大一致,须要判断版本进行适配操做。这里以Android 4.4为例。
首先使用dlopen和dlsym得到对应导出符号表地址
void *dvm_handle = dlopen("libdvm.so", RTLD_LAZY);
dlerror();//清空错误信息
if (dvm_handle == NULL) {
return;
}
void *symbol = dlsym(dvm_handle, "gDvm");
const char *error = dlerror();
if (error != NULL) {
dlclose(dvm_handle);
return;
}
if (symbol == NULL) {
LOGE("can't get symbol.");
dlclose(dvm_handle);
return;
}
DvmGlobals44 *dvmGlobals = (DvmGlobals44 *) symbol;
复制代码
而后直接修改classVerifyMode的值便可
dvmGlobals->classVerifyMode = DexClassVerifyMode::VERIFY_MODE_NONE;
复制代码
至此,就完成了dexopt的禁用class verify操做,能够看到,整个逻辑和art上禁用dex2oat十分类似,只须要找到一个变量,修改它便可。
值得注意的是,这里有不少机型,存在部分数据结构向后偏移的问题,所以,这里得经过几个特征数据结构进行定位,从而获得目标数据结构,这里采用的数据结构为
struct DvmGlobalsRetry {
DexOptimizerMode *dexOptMode;
DexClassVerifyMode *classVerifyMode;
RegisterMapMode *registerMapMode;
ExecutionMode *executionMode;
/*
* VM init management.
*/
bool *initializing;
bool *optimizing;
};
复制代码
咱们经过变量的范围值,优先找到DexOptimizerMode和DexClassVerifyMode的偏移值,而后从DexClassVerifyMode以后找到RegisterMapMode的偏移值,从RegisterMapMode以后找到ExecutionMode的偏移值,最终获得classVerifyMode的偏移值,通过验证,该方法99%+能获得正确的偏移值,从而进行重试。
部分异常机型数据结构偏移以下
思考:是否AOSP中间某一个版本存在数据结构偏移? 经过查看AOSP源码发现并无相似偏移,所以不得而知为何这些Android 4.2.2中dexOptMode向后偏移4字节,Android 4.4.4中dexOptMode向后偏移16字节。偏移值是如此惊人的一致,所以可能的确存在一个git提交,该提交中DvmGlobals数据结构恰好存在如上偏移致使。
Android 4.0-Android 4.4.4,除个别机型偏移值没法计算出来以外,以及dlsym没法获取导出符号表(基本都是X86的PAD),这两种case不予支持,其他testin上4.0-4.4机型所有覆盖,兼容性几乎100%(部分偏移值错误可经过4个特征数据结构进行定位,最终获得正确的偏移值)
至此,完成了art上dex2oat禁用达到加速以及dalvik上dex2opt禁用class verify达到加速。
lizhangqu,@WeiDian,2016年加入微店,目前主要负责微店App的基础支撑开发工做。
微店App技术团队
官方公众号