Android ART dex2oat 加載加速淺析

前言

手機淘寶插件化框架Atlas在ART上首次啟動的時候匾灶,會通過禁用dex2oat來達到插件迅速啟動的目的戏锹。之后后臺進行dex2oat秽荞,下次啟動如果dex2oat完成了則啟用dex2oat招刨,如果沒有完成則繼續(xù)禁用dex2oat戚炫。但是這部分代碼淘寶并沒有開源袭厂。且由于Atlas后續(xù)持續(xù)維護的可能性極低墨吓,加上Android 9.0上禁用失敗及64位動態(tài)庫在部分系統(tǒng)上禁用會發(fā)生crash。此文結合逆向與正向的角度來分析Atlas是通過什么手段達到禁用dex2oat的纹磺,以及微店App是如何實踐達到禁用的目的帖烘。

逆向日志分析

由于手淘Atlas這部分代碼是閉源的,因此我們無法正向分析其原理橄杨。所以我們可以從逆向的角度進行分析秘症。逆向分析的關鍵一步就是懂得看控制臺日志,從日志中入手進行分析式矫。

通過在Android 5.0历极,Android 6.0,Android 7.0衷佃,Android 8.0 和 Android 9.0上運行插件化的App趟卸,我們發(fā)現(xiàn),控制臺會輸出一部分關鍵性的日志氏义。內容如下

dex2oat-log.png

通過在AOSP中查找關鍵日志 Generation of oat file .... not attempt because dex2oat is disabled 即可繼續(xù)發(fā)現(xiàn)貓膩锄列。最終我們會發(fā)現(xiàn)這部分信息出現(xiàn)在了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函數(shù)主要做了如下幾步

  • 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));
}

核心函數(shù)Runtime::Current()->IsDex2OatEnabled()孝赫,判斷dex2oat是否開啟,如果開啟红符,則創(chuàng)建oat文件并進行更新青柄。

以上是Android 5.0的源碼劫映,Android 6.0-Android 9.0會有所差異。DexFile_openDexFileNative最終會調用到runtime->GetOatFileManager().OpenDexFilesFromOat()刹前,繼續(xù)會調用到OatFileAssistant類中的MakeUpToDate函數(shù)泳赋,一直調用到GenerateOatFile(Androiod 6.0-7.0)或GenerateOatFileNoChecks(Android 8.0-9.0)等類型函數(shù),相關代碼見如下鏈接喇喉。

最終我們也會發(fā)現(xiàn)一段關鍵性的代碼祖今,如下

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,這說明我們源碼找對了膏斤。

通過以上分析徐绑,我們發(fā)現(xiàn)Android 5.0-Android 9.0最終都會走到Runtime::Current()->IsDex2OatEnabled()函數(shù),如果dex2oat沒有開啟莫辨,則不會進行后續(xù)oat文件生成的操作傲茄,而是直接return返回。所以結論已經很明確了沮榜,就是通過設置該函數(shù)的返回值為false盘榨,達到禁用dex2oat的目的。

通過查看Runtime類的代碼蟆融,可以發(fā)現(xiàn)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枪孩。

不過經過驗證后發(fā)現(xiàn)手淘Atlas是通過禁用IsImageDex2OatEnabled()達到目的的,即它是通過修改image_dex2oat_enabled_而不是dex2oat_enabled_藻肄,這一點在兼容性方面十分重要,在一定程度上保障了部分機型的兼容性(比如一加拒担,8.0之后加入了一個變量嘹屯,導致數(shù)據(jù)結構向后偏移1字節(jié);VIVO/OPPO部分機型加入變量从撼,導致數(shù)據(jù)結構向后偏移1字節(jié))州弟,因此為了保持策略上的一致性钧栖,我們只修改image_dex2oat_enabled_,不修改dex2oat_enabled_婆翔。

原理與實現(xiàn)

有了以上理論基礎拯杠,我們必須進行實踐,用結論驗證猜想啃奴,才會有說服力了潭陪。

上面已經說到我們只需要修改Runtime中image_dex2oat_enabled_成員變量的值,將其對應的image_dex2oat_enabled_變量修改為false即可最蕾。

因此第一步我們需要拿到這個Runtime的地址依溯。

在JNI中,每一個Java中的native方法對應的jni函數(shù)瘟则,都有一個JNIEnv* 指針入?yún)⒗杪ㄟ^該指針變量的GetJavaVM函數(shù),我們可以拿到一個JavaVM*的指針變量

JavaVM *javaVM;
env->GetJavaVM(&javaVM);

而JavaVm在JNI中的數(shù)據(jù)結構定義為(源碼地址見 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丹壕,它的數(shù)據(jù)結構可以簡單理解為

class JavaVMExt : public JavaVM {
private:
    Runtime* const runtime_;
}

根據(jù)內存布局洪添,我們可以將JavaVMExt等效定義為

struct JavaVMExt {
    void *functions;
    void *runtime;
};

指針類型,在32位上占4字節(jié)雀费,在64位上占8字節(jié)干奢。

因此我們只需要將我們之前拿到的JavaVM *指針,強制轉換為JavaVMExt*指針盏袄,通過JavaVMExt*指針拿到Runtime*指針

JavaVM *javaVM;
env->GetJavaVM(&javaVM);
JavaVMExt *javaVMExt = (JavaVMExt *) javaVM;
void *runtime = javaVMExt->runtime;

剩下的事就非常簡單了忿峻,我們只需要將Runtime數(shù)據(jù)結構重新定義一遍,這里值得注意的是Android各版本Runtime數(shù)據(jù)結構不一致辕羽,所以需要進行區(qū)分逛尚,這里以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_;
};

注意绰寞,尤其需要注意內部布局中存在對齊問題,即 一铣口、結構體變量中成員的偏移量必須是成員大小的整數(shù)倍(0被認為是任何數(shù)的整數(shù)倍) 二滤钱、結構體大小必須是所有成員大小的整數(shù)倍。

所以我們必須完整的定義原數(shù)據(jù)結構脑题,不能存在偏移件缸。否則結構體地址就會錯亂。

之后將runtime強制轉換為PartialRuntime90*即可

PartialRuntime90 *partialRuntime = (PartialRuntime90 *) runtime;

拿到PartialRuntime90之后叔遂,直接修改該數(shù)據(jù)結構中的image_dex2oat_enabled_即可完成禁用

partialRuntime->image_dex2oat_enabled_ = false

不過這整個流程需要注意幾個問題他炊,通過兼容性測試報告反饋來看争剿,存在了如下幾個問題
1、Android 5.1-Android 9.0兼容性極好
2痊末、Android 5.0存在部分產商自定義該數(shù)據(jù)結構蚕苇,加入了成員導致image_dex2oat_enabled_向后偏移4字節(jié),又或是部分產商Android 5.0使用了Android 5.1的數(shù)據(jù)結構導致凿叠。
3涩笤、部分x86的PAD運行arm的APP,此種場景十分特殊幔嫂,因此我們選擇無視此種機型辆它,不處理
4、考慮校驗性問題履恩,需要使用一個變量校驗我們是否尋址正確锰茉,進行適當降級操作,我們選擇以指令集變量instruction_set_作為參考切心。它是一個枚舉變量飒筑,正常取值范圍為int 類型 1-7,如果該值不滿足绽昏,我們選擇不處理协屡,避免不必要的crash問題。
5全谤、一旦尋址失敗肤晓,我們選擇使用兜底策略進行重試,直接查找指令集變量instruction_set_偏移值认然,轉換為另一個公共的數(shù)據(jù)結構類型進行操作

這里貼出Android 5.0-9.0各系統(tǒng)Runtime的數(shù)據(jù)結構


/**
 * 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_;
};

數(shù)據(jù)結構轉換完成后未巫,我們需要進行簡單的校驗窿撬,只需要找到一個特征進行校驗,這里我們校驗指令集變量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,我們需要優(yōu)先將其轉為成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則優(yōu)先使用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函數(shù)中
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的地址偏移后涵叮,通過各系統(tǒng)的數(shù)據(jù)結構惭蹂,計算出image_dex2oat_enabled_地址偏移即可,這里不再詳細說明割粮。

深坑之Xposed

當你覺得一切很美好的時候盾碗,一個深坑突然冒了出來,Xposed舀瓢!由于Xposed運行期對art進行了hook廷雅,實際使用的是libxposed_art.so而不是libart.so,并且對應數(shù)據(jù)結構存在篡改現(xiàn)象京髓,以5.0-6.0篡改的最為惡劣航缀,其項目地址為 https://github.com/rovo89/android_art

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上朵锣,數(shù)據(jù)結構多了is_recompiling_和is_minimal_framework_谬盐,實際image_dex2oat_enabled_存在向后偏移2字節(jié)的問題;在6.0上诚些,數(shù)據(jù)結構多了is_minimal_framework_飞傀,實際image_dex2oat_enabled_存在向后偏移1字節(jié)的問題;而在Android 7.0及以上诬烹,暫時未存在篡改runtime.h的現(xiàn)象砸烦。因此可在native層判斷是否存在xposed框架,存在則手動校準偏移值绞吁。

判斷是否存在xposed函數(shù)如下

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位動態(tài)庫拟蜻,在兼容性方面绎签,已經遠遠超越Atlas。

在兼容性測試中酝锅,發(fā)現(xiàn)一部分機型runtime數(shù)據(jù)結構存在篡改問題诡必,進一步驗證了Atlas為什么修改image_dex2oat_enabled_變量而不是修改dex2oat_enabled_變量,因為dex2oat_enabled_可能存在向后偏移一字節(jié)的問題(甚至是2字節(jié)屈张,如xposed和一加9.0.2比較新的系統(tǒng)就存在2字節(jié)偏移)擒权,導致尋址錯誤,修改的其實是其原來的地址(即現(xiàn)有真實地址的前一個字節(jié))阁谆,導致禁用失敗碳抄。而通過修改image_dex2oat_enabled_變量,即使dex2oat_enabled_向后偏移一字節(jié)场绿,由于修改的是image_dex2oat_enabled_剖效,所以實際修改的其實就是dex2oat_enabled_現(xiàn)在偏移后的地址,實際上還是達到了禁用的效果焰盗。這里有點繞璧尸,可以細細品味一下。這個操作熬拒,可以兼容大部分機型爷光。

這里貼出一部分數(shù)據(jù)結構存在偏移的機型。


art-address-error1.png
art-address-error2.png
art-address-error3.png

題外話 Dalvik上dex2opt加速

在art上首次加載插件澎粟,會通過禁用dex2oat達到加速效果蛀序,那么在dalvik上首次加載插件,其實也存在類似的問題活烙,dalvik上是通過dexopt進行dex的優(yōu)化操作徐裸,這個操作,也是比較耗時的啸盏,因此在dalvik上重贺,需要一種類似于dex2oat的方式來達到禁用dex2opt的效果。經過驗證后,發(fā)現(xiàn)Atlas是通過禁用verify達到一定的加速气笙,因此我們只需要禁用class verify即可次企。

源碼以Android 4.4.4進行分析,見 https://android.googlesource.com/platform/dalvik/+/android-4.4.4_r2/vm/

在Java層我們加載一個Dex是通過DexFile.loadDex()方法進行加載健民。此方法最終會走到native方法 openDexFileNative抒巢,Android 4.4.4的源碼如下

https://android.googlesource.com/platform/dalvik/+/android-4.4.4_r2/vm/native/dalvik_system_DexFile.cpp#151

最終會調用到dvmRawDexFileOpen或者dvmJarFileOpen

這兩個方法贫贝,最終都會先查找緩存文件是否存在秉犹,如果不存在,最終都會調用到dvmOptimizeDexFile函數(shù)稚晚,見:

https://android.googlesource.com/platform/dalvik/+/android-4.4.4_r2/vm/analysis/DexPrepare.cpp#351

而dvmOptimizeDexFile函數(shù)開頭有這么一段邏輯

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操作都會發(fā)生IOException也搓,導致類加載失敗赏廓,存在crash風險,所以不能修改此值傍妒,看來只能修改class verify為不校驗了幔摸,沒有其他好的方法。事實證明颤练,去掉這一步校驗可以節(jié)約至少1倍的時間既忆。

此外發(fā)現(xiàn)部分4.2.2和4.4.4存在數(shù)據(jù)結構偏移問題,可通過幾個特征數(shù)據(jù)結構進行重試嗦玖,重新定位關鍵數(shù)據(jù)結構進行重試患雇。這里我們通過 dexOptMode,classVerifyMode宇挫,registerMapMode苛吱,executionMode四個特征變量的取值范圍進行重試定位,有興趣自行研究一下器瘪,不再細說翠储。

通過查看源碼發(fā)現(xiàn)gDvm是導出的,見 https://android.googlesource.com/platform/dalvik/+/android-4.4.4_r2/vm/Globals.h#740

extern struct DvmGlobals gDvm;

因此我們只需要借助dlopen和dlsym拿到整個DvmGlobals數(shù)據(jù)結構的起始地址娱局,修改對應的變量的值即可彰亥。不過不幸的是,Android 4.0-4.4這個數(shù)據(jù)結構各版本都不大一致衰齐,需要判斷版本進行適配操作任斋。這里以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十分相似,只需要找到一個變量澈蟆,修改它即可墨辛。

值得注意的是,這里有很多機型趴俘,存在部分數(shù)據(jù)結構向后偏移的問題睹簇,因此,這里得通過幾個特征數(shù)據(jù)結構進行定位寥闪,從而得到目標數(shù)據(jù)結構太惠,這里采用的數(shù)據(jù)結構為

struct DvmGlobalsRetry {
    DexOptimizerMode *dexOptMode;
    DexClassVerifyMode *classVerifyMode;
    RegisterMapMode *registerMapMode;
    ExecutionMode *executionMode;
    /*
     * VM init management.
     */
    bool *initializing;
    bool *optimizing;
};

我們通過變量的范圍值,優(yōu)先找到DexOptimizerMode和DexClassVerifyMode的偏移值疲憋,然后從DexClassVerifyMode之后找到RegisterMapMode的偏移值凿渊,從RegisterMapMode之后找到ExecutionMode的偏移值,最終得到classVerifyMode的偏移值缚柳,經過驗證埃脏,該方法99%+能得到正確的偏移值,從而進行重試秋忙。

部分異常機型數(shù)據(jù)結構偏移如下


dalvik-address-error1.png

dalvik-address-error2.png

思考:是否AOSP中間某一個版本存在數(shù)據(jù)結構偏移? 通過查看AOSP源碼發(fā)現(xiàn)并沒有類似偏移彩掐,因此不得而知為什么這些Android 4.2.2中dexOptMode向后偏移4字節(jié),Android 4.4.4中dexOptMode向后偏移16字節(jié)翰绊。偏移值是如此驚人的一致佩谷,因此可能的確存在一個git提交,該提交中DvmGlobals數(shù)據(jù)結構剛好存在如上偏移導致监嗜。

Android 4.0-Android 4.4.4谐檀,除個別機型偏移值無法計算出來之外,以及dlsym無法獲取導出符號表(基本都是X86的PAD)裁奇,這兩種case不予支持桐猬,其余testin上4.0-4.4機型全部覆蓋,兼容性幾乎100%(部分偏移值錯誤可通過4個特征數(shù)據(jù)結構進行定位刽肠,最終得到正確的偏移值)

總結

至此腕扶,完成了art上dex2oat禁用達到加速以及dalvik上dex2opt禁用class verify達到加速堪旧。

作者簡介

李樟取蛙奖,@WeiDian亮垫,2016年加入微店,目前主要負責微店App的基礎支撐開發(fā)工作躺涝。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末厨钻,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌夯膀,老刑警劉巖诗充,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诱建,居然都是意外死亡蝴蜓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門俺猿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茎匠,“玉大人,你說我怎么就攤上這事辜荠∑В” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵伯病,是天一觀的道長。 經常有香客問我否过,道長午笛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任苗桂,我火速辦了婚禮药磺,結果婚禮上,老公的妹妹穿的比我還像新娘煤伟。我一直安慰自己癌佩,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布便锨。 她就那樣靜靜地躺著围辙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪放案。 梳的紋絲不亂的頭發(fā)上姚建,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音吱殉,去河邊找鬼掸冤。 笑死,一個胖子當著我的面吹牛友雳,可吹牛的內容都是我干的稿湿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼押赊,長吁一口氣:“原來是場噩夢啊……” “哼饺藤!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤策精,失蹤者是張志新(化名)和其女友劉穎舰始,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咽袜,經...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡丸卷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了询刹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谜嫉。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖凹联,靈堂內的尸體忽然破棺而出沐兰,到底是詐尸還是另有隱情,我是刑警寧澤蔽挠,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布住闯,位于F島的核電站,受9級特大地震影響澳淑,放射性物質發(fā)生泄漏比原。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一杠巡、第九天 我趴在偏房一處隱蔽的房頂上張望量窘。 院中可真熱鬧,春花似錦氢拥、人聲如沸蚌铜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冬殃。三九已至,卻和暖如春出革,著一層夾襖步出監(jiān)牢的瞬間造壮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工骂束, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留耳璧,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓展箱,卻偏偏與公主長得像旨枯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子混驰,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內容