Android ART dex2oat 加載加速淺析

前言

手機淘寶插件化框架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

dex2oat-log.png

經過在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函數主要作了以下幾步數據結構

  • 一、檢測咱們是否已經有一個打開的oat文件
  • 二、若是沒有已經打開的oat文件,則從磁盤上檢測是否有一個已經生成的oat文件
  • 三、若是磁盤上有一個生成的oat文件,則檢測該oat文件是否過時了以及是否包含了咱們全部的dex文件
  • 四、若是以上都不知足,則會從新生成

首次打開時,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!因爲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-address-error1.png

art-address-error2.png

art-address-error3.png

題外話 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%+能獲得正確的偏移值,從而進行重試。

部分異常機型數據結構偏移以下

dalvik-address-error1.png
dalvik-address-error2.png

思考:是否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技術團隊官方公衆號

微店App技術團隊
相關文章
相關標籤/搜索