Android ART dex2oat 載入加速淺析
前言
手機淘寶外掛化框架Atlas在ART上首次啟動的時候,會通過禁用dex2oat來達到外掛迅速啟動的目的。之後後臺進行dex2oat,下次啟動如果dex2oat完成了則啟用dex2oat,如果沒有完成則繼續禁用dex2oat。但是這部分程式碼淘寶並沒有開源。且由於Atlas後續持續維護的可能性極低,加上Android 9.0上禁用失敗及64位動態庫在部分系統上禁用會發生crash。此文結合逆向與正向的角度來分析Atlas是通過什麼手段達到禁用dex2oat的,以及微店App是如何實踐達到禁用的目的。
逆向日誌分析
由於手淘Atlas這部分程式碼是閉源的,因此我們無法正向分析其原理。所以我們可以從逆向的角度進行分析。逆向分析的關鍵一步就是懂得看控制檯日誌,從日誌中入手進行分析。
通過在Android 5.0,Android 6.0,Android 7.0,Android 8.0 和 Android 9.0上執行外掛化的App,我們發現,控制檯會輸出一部分關鍵性的日誌。內容如下
通過在AOSP中查詢關鍵日誌 Generation of oat file .... not attempt because dex2oat is disabled 即可繼續發現貓膩。最終我們會發現這部分資訊出現在了class_linker.cc類或者oat_file_manager.cc類中。
正向原始碼分析
有了以上基礎,我們嘗試從原始碼角度進行正向分析。
在Java層我們載入一個Dex是通過DexFile.loadDex()方法進行載入。此方法最終會走到native方法 openDexFileNative,Android 5.0的原始碼如下
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方法
對應程式碼過長,這裡不貼了,見
OpenDexFilesFromOat函式主要做了如下幾步
- 1、檢測我們是否已經有一個開啟的oat檔案
- 2、如果沒有已經開啟的oat檔案,則從磁碟上檢測是否有一個已經生成的oat檔案
- 3、如果磁碟上有一個生成的oat檔案,則檢測該oat檔案是否過期了以及是否包含了我們所有的dex檔案
- 4、如果以上都不滿足,則會重新生成
首次開啟時,1-3步必然是不滿足的,最終會走到第四個邏輯,這一步有一個關鍵性的程式碼直接決定了生成oat檔案是否生成成功
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)等型別函式,相關程式碼見如下連結。
- android-9.0.0_r18/runtime/native/dalvik_system_DexFile.cc#267
- android-9.0.0_r18/runtime/oat_file_manager.cc#394
- android-7.0.0_r1/runtime/oat_file_assistant.cc#206 (Androiod 6.0-7.0)
- android-9.0.0_r18/runtime/oat_file_assistant.cc#251 (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_;
};
複製程式碼
注意,尤其需要注意內部佈局中存在對齊問題,即 一、結構體變數中成員的偏移量必須是成員大小的整數倍(0被認為是任何數的整數倍) 二、結構體大小必須是所有成員大小的整數倍。
所以我們必須完整的定義原資料結構,不能存在偏移。否則結構體地址就會錯亂。
之後將runtime強制轉換為PartialRuntime90*即可
PartialRuntime90 *partialRuntime = (PartialRuntime90 *) runtime;
複製程式碼
拿到PartialRuntime90之後,直接修改該資料結構中的image_dex2oat_enabled_即可完成禁用
partialRuntime->image_dex2oat_enabled_ = false
複製程式碼
不過這整個流程需要注意幾個問題,通過相容性測試報告反饋來看,存在瞭如下幾個問題 1、Android 5.1-Android 9.0相容性極好 2、Android 5.0存在部分產商自定義該資料結構,加入了成員導致image_dex2oat_enabled_向後偏移4位元組,又或是部分產商Android 5.0使用了Android 5.1的資料結構導致。 3、部分x86的PAD執行arm的APP,此種場景十分特殊,因此我們選擇無視此種機型,不處理 4、考慮校驗性問題,需要使用一個變數校驗我們是否定址正確,進行適當降級操作,我們選擇以指令集變數instruction_set_作為參考。它是一個列舉變數,正常取值範圍為int 型別 1-7,如果該值不滿足,我們選擇不處理,避免不必要的crash問題。 5、一旦定址失敗,我們選擇使用兜底策略進行重試,直接查詢指令集變數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!由於Xposed執行期對art進行了hook,實際使用的是libxposed_art.so而不是libart.so,並且對應資料結構存在篡改現象,以5.0-6.0篡改的最為惡劣,其專案地址為 github.com/rovo89/andr…
- github.com/rovo89/andr…
- github.com/rovo89/andr…
- github.com/rovo89/andr…
- github.com/rovo89/andr…
- 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_現在偏移後的地址,實際上還是達到了禁用的效果。這裡有點繞,可以細細品味一下。這個操作,可以相容大部分機型。
這裡貼出一部分資料結構存在偏移的機型。
題外話 Dalvik上dex2opt加速
在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達到加速。
作者簡介
李樟取,@WeiDian,2016年加入微店,目前主要負責微店App的基礎支撐開發工作。