1. 程式人生 > >阿里 Andfix 介紹及原理解析

阿里 Andfix 介紹及原理解析

開源專案官方介紹:

AndFix judges the methods should be replaced by java custom annotation and replaces it by hooking it. AndFix has a native method art_replaceMethod in ART or dalvik_replaceMethod in Dalvik.

從以上介紹我們可以知道,它是通過自定義註解來判斷方法是否需要被替換。原理是hook native層的方法來實現方法替換以達到修復功能。由於andfix屬於native方案,因此為了精簡一下篇幅,這裡我們就側重講解native層的原理,至於dex diff 以及補丁載入等Java層的程式碼邏輯,不是很多,大家有興趣可自行下載andfix demo工程去看一看。

替換示意圖:

1.png

方案原理

andfix 方案對於 dalivk 及 art 分別用不同方式去實現方法替換的。原理圖如下:

ART環境:

2.png

Dalivk環境:
3.png

從官方文件及andfix 原始碼我們不難看出,其核心原理在於replaceMethod函式。該方案的替換過程大致如下:

4.png

Andfix是在已經載入了的類中直接在native層替換掉原有方法,它是在原來類的基礎上進行修改的。

原始碼分析

從上面的介紹我們知道其方案核心在於方法替換。紙上談兵多說無益,我們來看一下Andfix的原始碼,分析核心實現邏輯根據介紹成都原理我們大概可推測它的核心大概是replaceMethod方法,這是Java 層到Native 層的入口,因此我們從這個入口開始探索:

原始檔路徑: AndFix/src/com/alipay/euler/andfix/AndFix.java

private static native void replaceMethod(Method dest, Method src);

從程式碼我們可以看出這是一個native方法,它的引數是在Java層通過反射機制得到的Method物件所對應的jobject。src對應的是需要被替換的原有方法,而dest對應的就是新方法,新方法存在於補丁包的新類中,也就是補丁方法。native層對應的方法如下:

static void replaceMethod(JNIEnv* env, jclass clazz,
jobject src, jobject dest) { if (isArt) { art_replaceMethod(env, src, dest); } else { dalvik_replaceMethod(env, src, dest); } }

眾所周知,Android的Java執行環境在4.4以下用的是Dalvik虛擬機器,而在4.4及以上用的是ART虛擬機器。

ART平臺

原始檔: art_method_replace.cpp

extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
		JNIEnv* env, jobject src, jobject dest) {
    if (apilevel > 23) {
        replace_7_0(env, src, dest);
    } else if (apilevel > 22) {
		replace_6_0(env, src, dest);
	} else if (apilevel > 21) {
		replace_5_1(env, src, dest);
	} else if (apilevel > 19) {
		replace_5_0(env, src, dest);
    }else{
        replace_4_4(env, src, dest);
    }
}

Dalvik平臺

原始檔: dalvik_method_replace.cpp

早期版本:
8.png

新版本:
圖片.png

從程式碼可以看出,原來早期的版本Andfix 是參考了 Dexposed 的思想。Dalivk 環境中通過指向公共分發函式然後反射回調來實現。而新版本則統一通過覆蓋方法函式的成員指標引用來實現。

對於不同Android版本的art,底層Java物件的資料結構是不同的,因而會進一步區分不同的替換函式,這裡我們以Android 6.0為例,對應的就是replace_6_0。

每一個Java方法在art中都對應著一個ArtMethod,ArtMethod記錄了這個Java方法的所有資訊,包括所屬類、訪問許可權、程式碼執行地址等等。通過env->FromReflectedMethod,可以由Method物件得到這個方法對應的ArtMethod的真正起始地址。然後就可以把它強轉為ArtMethod指標,從而對其所有成員進行替換。這樣全部替換完之後就完成了熱修復邏輯,以後呼叫這個方法時就會直接走到新方法的實現中了。為什麼把所有成員替換完就可以實現熱修復呢?這需要從虛擬機器呼叫方法的原理說起。

虛擬機器呼叫方法的原理

以Android 6.0 為例,art虛擬機器中ArtMethod的結構是這個樣子的:

 protected:
  // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
  // The class we are a part of.
  GcRoot<mirror::Class> declaring_class_;
  // Access flags; low 16 bits are defined by spec.
  // Getting and setting this flag needs to be atomic when concurrency is
  // possible, e.g. after this method's class is linked. Such as when setting
  // verifier flags and single-implementation flag.
  std::atomic<std::uint32_t> access_flags_;
  /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
  // Offset to the CodeItem.
  uint32_t dex_code_item_offset_;
  // Index into method_ids of the dex file associated with this method.
  uint32_t dex_method_index_;
  /* End of dex file fields. */
  // Entry within a dispatch table for this method. For static/direct methods the index is into
  // the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
  // ifTable.
  uint16_t method_index_;
  // The hotness we measure for this method. Managed by the interpreter. Not atomic, as we allow
  // missing increments: if the method is hot, we will see it eventually.
  uint16_t hotness_count_;
  // Fake padding field gets inserted here.
  // Must be the last fields in the method.
  struct PtrSizedFields {
    // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
    mirror::MethodDexCacheType* dex_cache_resolved_methods_;
    // Pointer to JNI function registered to this method, or a function to resolve the JNI function,
    // or the profiling data for non-native methods, or an ImtConflictTable, or the
    // single-implementation of an abstract/interface method.
    void* data_;
    // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
    // the interpreter.
    void* entry_point_from_quick_compiled_code_;
  } ptr_sized_fields_;

這其中最重要的欄位就是entry_point_from_interprete_和entry_point_from_quick_compiled_code_了,從名字可以看出來,他們就是方法的執行入口。我們知道,Java程式碼在Android中會被編譯為Dex Code。

OAT名詞科普

ART虛擬機器,它的核心是OAT檔案。OAT檔案是一種Android私有ELF檔案格式,它不僅包含有從DEX檔案翻譯而來的本地機器指令,還包含有原來的DEX檔案內容,這使得我們無需重新編譯原有的APK就可以讓它正常地在ART裡面執行。

AOT名詞科普

AOT是”Ahead Of Time”的縮寫,指的就是ART(Anroid RunTime)這種執行方式。

ART中可以採用解釋模式或者AOT模式(本地機器指令執行模式)執行。
解釋模式,就是取出Dex Code,逐條解釋執行就行了。如果方法的呼叫者是以解釋模式執行的,在呼叫這個方法時,就會取得這個方法的entry_point_from_interpreter_,然後跳轉過去執行。
如果是AOT的方式,就會先預編譯好Dex Code對應的機器碼,然後執行期直接執行機器碼就行了,不需要一條條地解釋執行Dex Code。如果方法的呼叫者是以AOT機器碼方式執行的,在呼叫這個方法時,就是跳轉到entry_point_from_quick_compiled_code_執行。

那我們是不是隻需要替換這幾個entry_point_*入口地址就能夠實現方法替換了呢?
並沒有這麼簡單。因為不論是解釋模式或是AOT機器碼模式,在執行期間都還會需要用到ArtMethod裡面的其他成員欄位。
就以AOT機器碼模式為例,雖然Dex Code被編譯成了機器碼。但是機器碼並不是可以脫離虛擬機器而單獨執行的,以這段簡單的程式碼為例:
10.png

編譯為AOT機器碼後,是這樣的:
11.jpeg

這裡面去掉了一些校驗之類的無關程式碼,可以很清楚看到,在呼叫一個方法時,取得了ArtMethod中的dex_cache_resolved_methods_,這是一個存放ArtMethod的指標陣列,通過它就可以訪問到這個Method所在Dex中所有的Method所對應的ArtMethod
Activity.onCreate的方法索引是70,由於是64位系統,因此每個指標的大小為8位元組,又由於ArtMethod*元素是從這個陣列的第0x2個位置開始存放的,因此偏移(70 + 2) * 8 = 576的位置正是Activity.onCreate的ArtMethod指標。

以上是一個比較簡單的例子,在實際程式碼中,有許多更為複雜的呼叫情況,很多情況下還需要用到dex_code_item_offset_等欄位。由此可以看出,AOT機器碼的執行過程,還是會有對於虛擬機器以及ArtMethod其他成員欄位的依賴。因此,當把一箇舊方法的所有成員欄位換成都新方法的之後,執行時所有資料才能夠確保可以和新方法的一致。這樣在所有執行到舊方法的地方,會取得新方法的執行入口、所屬class、方法索引號以及所屬dex資訊,然後像呼叫舊方法一樣順滑地執行到新方法的邏輯。

Andfix 的 ArtMethod 對比 Android 原始碼

可以看到,ArtMethod結構裡的各個成員的位置是和AOSP開原始碼裡完全一致的。這是由於Android原始碼是公開的,Andfix裡面的這個ArtMethod自然是遵照android虛擬機器art原始碼裡面的ArtMethod構建的。
但正是因為Android是開源的,各個手機廠商都可以對程式碼進行改造定製。而Andfix裡ArtMethod的結構是根據公開的Android原始碼中的結構寫死的,如果某個廠商對這個ArtMethod結構體進行了修改,就和原先開原始碼裡的結構不一致,那麼在這種修改過結構體的裝置系統上,Andfix的補丁替換機制就會出問題。

比如,在Andfix替換declaring_class_的地方:

 smeth->declaring_class_ = dmeth->declaring_class_;

由於declaring_class_是andfix裡ArtMethod的第一個成員,因此它和以下這行程式碼等價:

*(unit32_t*) (smeth + 0)  =  *(unit32_t*) (dmeth + 0)

如果手機廠商在ArtMethod結構體的declaring_class_前面添加了一個欄位additional_,那麼,additional_就成為了ArtMethod的第一個成員,所以smeth + 0這個位置在這臺裝置上實際就變成了additional_,而不再是declaring_class_欄位。所以這行程式碼的真正含義就變成了:

 smeth->additional_ = dmeth->additional_ ;

這樣就和原先替換declaring_class_的邏輯不一致,從而無法正常執行熱修復邏輯。
這也正是Andfix不支援很多機型的原因,很大的可能,就是因為這些機型修改了底層的虛擬機器結構。

有趣的是,它甚至也不支援相容自家的YunOS…

結語

從本文分析我們瞭解了Andfix的大致實現原理,從它的原理上我們可以知道它的優勢是支援實時修復,不需要重啟即可生效。另外一個優勢是對系統的hook點較少,方案整體的邏輯相對比較簡單。不過它也有諸多不足:

  1. 由於很多廠商定製改造了 Rom,導致該方案相容性比較差。
  2. 使用加固平臺可能會使熱補丁功能失效。
  3. 不支援新增新類和新的欄位,僅支援方法級別的修復。
  4. 不支援資源替換。
  5. 可修復場景限制程度高。由於從實現上直接跳過了類初始化,設定為初始化完畢。所以像靜態函式、靜態成員、建構函式等都無法修復,一改就會出現問題。複雜點的類呼叫Class.forname很可能直接就會掛掉。
  6. 已停止維護,還沒有對7.0之後的版本進行適配。

由於native底層替換方案和ClasssLoader方案各有其優缺點,那麼把他們的優點結合起來,豈不是更好的選擇嗎?
2017年6月,阿里巴巴手淘技術團隊推出了全新的移動熱更新解決方案——Sophix。Sophix的程式碼修復體系汲取 ClassLoader 方案和 Native Hook 思想,同時涵蓋了這兩種方案來實現熱更新機制。

關於Sophix的介紹及原理,我們接下來的這篇文章將會詳細分析。