我理解的熱修復(fù)中的ART地址錯亂問題

1. 序言

? android在5.0開始正式用art虛擬機取代了dalvik虛擬機程剥,不同版本的art虛擬機差別很大,android N開始又引入了混合編譯模式崩泡。在這里我們只針對android N之前的art版本進行分析下硕,至于art和dalvik的區(qū)別,這里就不多說了椰于,最大的區(qū)別是art在安裝時存在aot過程,用于生成oat文件仪搔,這個oat文件既包含轉(zhuǎn)化前的dex文件瘾婿,又包含機器指令,所以我們的應(yīng)用在運行的時候可以免去[解釋]這一層而直接加載機器指令烤咧。最后說一點偏陪,art是可以通過開啟解釋模式進行解釋執(zhí)行代碼的,此外煮嫌,有一些情況笛谦,比如動態(tài)生成的方法等也是需要解釋執(zhí)行的。文末會有參考文章的鏈接立膛,建議大家對我略過的內(nèi)容不太清楚時揪罕,可以去參考文章中學習梯码,畢竟側(cè)重點是不同的。

2. 問題的引入

下面開始說重點好啰。
?其實對于classloader熱修復(fù)方案的地址錯亂問題早有耳聞轩娶,最早是在騰訊的一篇文章Android_N混合編譯與對熱補丁影響解析看到的這個說法,但是作者后續(xù)沒有進行進一步的解釋框往。后來看了看其他文章基本上是這樣解釋的鳄抒。假設(shè)app中有一個Test類:

public class Test {
  public String showTest1(){
    return "art address error";
  }
  public String showText(){
    return "I am an showText";
  }
}

我們在MainActivity中的OnCreate方法中去調(diào)用new Test().showText()

public class MainActivity extends Activity {

  @Override
  protected void onCreate( Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final TextView textView=(TextView) findViewById(R.id.mytv);
    textView.setText(new Test().showText());
  }

?我們反編譯生成的oat文件,說明一下椰弊,我們接下來的分析過程都以android5.1的源碼進行,后面會進行匯編代碼的分析许溅,x86指令集比ARM指令集閱讀起來復(fù)雜的多,如果大家想分析oat文件秉版,最好還是進行ARM的手機進行贤重。android5.x默認情況下生成的oat文件是放在/data/dalvik-cache 目錄下,如果是ARM指令集的話清焕,是會在/data/dalvik-cache/arm下的并蝗,名字為
data@app@[package name]-1@base.apk@classes.dex(可能名字上略有差異)。使用oatdump我們可以得到反編譯后的oat文件dump.txt秸妥。

oatdump --file-name=data@app@[package name]-1@base.apk@classes.dex --output=dump.txt

我們看一下結(jié)果滚停。


image.png

text方法調(diào)用機器碼.png

至于這段匯編的含義先不用管,后面會介紹粥惧。我們只需要關(guān)注標紅的字眼键畴,method@21,可以理解為showText()方法在dex中的位置突雪。#96是該方法在ArtMethod中的偏移位置起惕。
接下來我們給Test類添加一個showTest2()方法。

public class Test {

  public String showTest1(){
    Log.i("ljj", "showTest1: ");
    Log.i("ljj", "showTest2: ");
    Log.i("ljj", "showTest3: ");
    return "art address error";
  }

  public String showTest2(){
   return "art address error"; 
  }
  public String showText(){
    return "I am an showText";
  }
}

再次查看oat文件挂签。


image.png

image.png

發(fā)現(xiàn)同樣調(diào)用的showText()疤祭,方法id已經(jīng)變成了method@22盼产,偏移也變成了#100.那問題就來了饵婆。我們簡單分析下:
假設(shè)Test類需要修復(fù),我們打補丁時在Test類中增加了一個showTest2方法戏售,導致showText的方法的偏移由#96變成了#100侨核,而此時#96指向的是showTest2方法,那么當我們調(diào)用到showtext方法時會調(diào)用到showTest2方法灌灾,從而導致了地址錯亂搓译,這是網(wǎng)上大家關(guān)于地址錯亂的解釋,不知道讀到這里大家有沒有發(fā)現(xiàn)問題锋喜?
?我們仔細來梳理一下些己,看看有沒有問題豌鸡。首先當我們安裝app時,會生成一個oat文件段标,我們稱為host.oat涯冠。這個oat文件中有一個Test類,是待修復(fù)的逼庞,在這個oat文件中蛇更,showText()的方法是method@21,調(diào)用處的偏移是#96赛糟。接著我們下發(fā)補丁派任,通過DexClassLoader加載的patch.dex,在動態(tài)加載的過程中璧南,patch.dex也會生成一個oat文件掌逛,這里我們?yōu)榱藚^(qū)分,稱為patch.oat司倚。在patch.oat中方法的編號顯然是和host.oat沒有一點關(guān)系的颤诀。而我們加載Test類時,毫無疑問加載到的是patch.oat中的Test類对湃,否則熱修復(fù)就不會成功了崖叫,那么怎么可能存在#100的問題,這種說法明顯是在同一個dex中進行調(diào)用考慮的拍柒,和熱修復(fù)(不考慮全量合成)是不符合的心傀,因為熱修復(fù)生效的時候,運行起來是跨oat或者說是跨dex的拆讯。如果真是按照機器碼執(zhí)行時寫入的地址進行跨dex調(diào)用脂男,感覺很容易跑飛啊,即使我不插入showTest2方法种呐,兩個oat文件的類和方法的偏移都不是同一個基準的嗅榕,按說機器碼應(yīng)該都不能找到patch.dex中的showText方法。到底怎么回事呢斋枢?

3. 探索答案

我們分以下幾個步驟來探索已维。

  • art下調(diào)用者是怎么通過機器碼找到我們patch中的方法的(art下跨dex方法調(diào)用的實現(xiàn))
  • art下像上文中增加方法,真的會地址錯亂嗎阔墩?

我們先來看一下第一個問題嘿架,這里說明一下,因為對匯編等底層的知識不太了解啸箫,以下的解釋可能有些地方表述不到位耸彪,甚至可能有錯誤,如果有問題忘苛,希望大家一起探討學習蝉娜。

3.1 如何找到的Test類(跨dex查找類)

我們首先看MainActivity的調(diào)用處

  textView.setText(new Test().showText());
0x000020f8: f8d9e124    ldr.w   lr, [r9, #292]  ; pAllocObject
      0x000020fc: 1c29      mov     r1, r5
      0x000020fe: 2010      movs    r0, #16
      0x00002100: 47f0      blx     lr
      suspend point dex PC: 0x0003
      GC map objects:  v1 (r7), v2 (r8)
      0x00002102: 1c06      mov     r6, r0
      0x00002104: 1c28      mov     r0, r5
      0x00002106: 68c0      ldr     r0, [r0, #12]
      0x00002108: 1c31      mov     r1, r6
      0x0000210a: 6d80      ldr     r0, [r0, #88]
      0x0000210c: f8d0e02c  ldr.w   lr, [r0, #44]
      0x00002110: 47f0      blx     lr
      suspend point dex PC: 0x0005
      GC map objects:  v0 (r6), v1 (r7), v2 (r8)
      0x00002112: 1c28      mov     r0, r5
      0x00002114: 68c0      ldr     r0, [r0, #12]
      0x00002116: 1c31      mov     r1, r6
      0x00002118: 6e40      ldr     r0, [r0, #100]
      0x0000211a: f8d0e02c  ldr.w   lr, [r0, #44]
      0x0000211e: 47f0      blx     lr
      suspend point dex PC: 0x0008

省略了無關(guān)的機器碼唱较,這段匯編主要是new Test().showText()相關(guān)的代碼。這里只針對關(guān)鍵的指令進行分析召川,把握住我們的主線绊汹,如果擴展開來,一篇文章肯定是寫不下的扮宠,大家可以參考文末老羅的相關(guān)文章西乖,我也是參照老羅的文章來查看的。
首先看第一行坛增,pAllocObject很明顯是在創(chuàng)建對象获雕,在art虛擬機啟動進行初始化的過程中,會注冊很多的Trampoline收捣,稱為跳轉(zhuǎn)表届案,pAllocObject就是其中的一個,Trampoline指向的是一段匯編代碼罢艾,在匯編中會去執(zhí)行相關(guān)的方法調(diào)用楣颠。
代碼位置:art/runtime/thread.cc

void InitEntryPoints(InterpreterEntryPoints* ipoints, JniEntryPoints* jpoints,
                     PortableEntryPoints* ppoints, QuickEntryPoints* qpoints) {
    // Interpreter
    ipoints->pInterpreterToInterpreterBridge = artInterpreterToInterpreterBridge;
    ipoints->pInterpreterToCompiledCodeBridge = artInterpreterToCompiledCodeBridge;

    // JNI
    jpoints->pDlsymLookup = art_jni_dlsym_lookup_stub;

    // Portable
    ppoints->pPortableResolutionTrampoline = art_portable_resolution_trampoline;
    ppoints->pPortableToInterpreterBridge = art_portable_to_interpreter_bridge;

    // Alloc
    qpoints->pAllocArray = art_quick_alloc_array;
    qpoints->pAllocArrayWithAccessCheck = art_quick_alloc_array_with_access_check;
    qpoints->pAllocObject = art_quick_alloc_object;
    qpoints->pAllocObjectWithAccessCheck = art_quick_alloc_object_with_access_check;
    qpoints->pCheckAndAllocArray = art_quick_check_and_alloc_array;
    qpoints->pCheckAndAllocArrayWithAccessCheck = art_quick_check_and_alloc_array_with_access_check;
 ....
};

位置:art/runtime/arch/arm/quick_entrypoints_arm.S

    /* 
     * Called by managed code to allocate an object 
     */  
    .extern artAllocObjectFromCode  
ENTRY art_quick_alloc_object  
    SETUP_REF_ONLY_CALLEE_SAVE_FRAME  @ save callee saves in case of GC  
    mov    r2, r9                     @ pass Thread::Current  
    mov    r3, sp                     @ pass SP  
    bl     artAllocObjectFromCode     @ (uint32_t type_idx, Method* method, Thread*, SP)  
    RESTORE_REF_ONLY_CALLEE_SAVE_FRAME  
    RETURN_IF_RESULT_IS_NON_ZERO  
    DELIVER_PENDING_EXCEPTION  
END art_quick_alloc_object  

接著會調(diào)用artAllocObjectFromCode方法來創(chuàng)建對象。這里需要知道咐蚯,每個dex都會生成一個DexCache童漩,會緩存對應(yīng)dex中加載解析過的所有類和方法,每個類會被分配一個typeId春锋,每個方法會被分配一個methodId矫膨。這個方法首先會根據(jù)typeId在調(diào)用者的DexCache中查看該類是否加載過,如果沒加載過期奔,則會調(diào)用ClassLinker的ResolveType方法進行類的查找解析侧馅。
代碼位置:art/runtime/class_linker.cc

mirror::Class* ClassLinker::ResolveType(const DexFile& dex_file, uint16_t type_idx,
                                       mirror::Class* referrer) {
 StackHandleScope<2> hs(Thread::Current());
 Handle<mirror::DexCache> dex_cache(hs.NewHandle(referrer->GetDexCache()));
 Handle<mirror::ClassLoader> class_loader(hs.NewHandle(referrer->GetClassLoader()));
 return ResolveType(dex_file, type_idx, dex_cache, class_loader);
}

mirror::Class* ClassLinker::ResolveType(const DexFile& dex_file, uint16_t type_idx,
                                       Handle<mirror::DexCache> dex_cache,
                                       Handle<mirror::ClassLoader> class_loader) {
 DCHECK(dex_cache.Get() != nullptr);
 mirror::Class* resolved = dex_cache->GetResolvedType(type_idx);
 if (resolved == nullptr) {
   Thread* self = Thread::Current();
   const char* descriptor = dex_file.StringByTypeIdx(type_idx);
   resolved = FindClass(self, descriptor, class_loader);
   if (resolved != nullptr) {
     dex_cache->SetResolvedType(type_idx, resolved);
   } else {
     CHECK(self->IsExceptionPending())
         << "Expected pending exception for failed resolution of: " << descriptor;
     // Convert a ClassNotFoundException to a NoClassDefFoundError.
     StackHandleScope<1> hs(self);
     Handle<mirror::Throwable> cause(hs.NewHandle(self->GetException(nullptr)));
     if (cause->InstanceOf(GetClassRoot(kJavaLangClassNotFoundException))) {
       DCHECK(resolved == nullptr);  // No Handle needed to preserve resolved.
       self->ClearException();
       ThrowNoClassDefFoundError("Failed resolution of: %s", descriptor);
       self->GetException(nullptr)->SetCause(cause.Get());
     }
   }
 }
 DCHECK((resolved == nullptr) || resolved->IsResolved() || resolved->IsErroneous())
         << PrettyDescriptor(resolved) << " " << resolved->GetStatus();
 return resolved;
}

我們針對這段代碼進行詳細分析,首先dex_cache是通過referrer->GetDexCache()拿到的呐萌,這個referrer是caller馁痴,也就是調(diào)用者所在的類,在我們的例子中就是指的MainActivity肺孤,所以這個dex_cache我們可以認為是主dex的cache罗晕。dex_file相當于是主dex。在主dex的cache中根據(jù)type_id去查找渠旁,因為是首次加載攀例,所以

 mirror::Class* resolved = dex_cache->GetResolvedType(type_idx);

返回nullptr船逮。接著會從主dex中根據(jù)type_id拿到類的名字顾腊,繼續(xù)調(diào)用FindClass去class_loader中查找類,這塊就涉及到類的加載機制了挖胃,我們就不多說了杂靶,會優(yōu)先找到我們patch.dex中的Test類梆惯。類加載及方法調(diào)用的調(diào)用鏈為:

FindClass->FindInClassPath->DefineClass->LoadClass->LoadClassMembers->LoadMethod->LinkCode

加載的每個類對應(yīng)一個oatClass,class的每個field會用一個ArtField表示吗垮,每個method都會對應(yīng)一個ArtMethod對象垛吗。loadMethod方法會對創(chuàng)建的ArtMethod進行賦值。這里我們只需要知道ArtMethod的dex_cache_resolved_methods_數(shù)組指向的是所在class對應(yīng)的DexCache中被resolved了的方法烁登。在這里也就是在patch.dex中被resolved的方法怯屉,和主dexCache沒任何關(guān)系。


image.png

LinkCode過程會對ArtMethod的執(zhí)行入口進行設(shè)置饵沧,是compiled_code方式還是interpreter解釋執(zhí)行锨络,不想扯的太遠±俏回到ResolveType方法中羡儿,注意這一句很關(guān)鍵,當我們通過FindClass方法找到了對應(yīng)的class后是钥,此時的dex_cache是主dex的掠归,也就是從patch.dex中拿到了class后,同時填充到了主dex的dexcache中對應(yīng)的位置上了悄泥。

  dex_cache->SetResolvedType(type_idx, resolved);

好了至此我們分析完了pAllocObject的過程虏冻,完成了Test類的加載解析。到此我們了解了弹囚,這段匯編執(zhí)行過后是通過類的簽名在patch.dex中拿到的Test類兄旬,同時緩存到了自己的dexCache中。

0x000020f8: f8d9e124    ldr.w   lr, [r9, #292]  ; pAllocObject
      0x000020fc: 1c29      mov     r1, r5
      0x000020fe: 2010      movs    r0, #16
      0x00002100: 47f0      blx     lr
3.2 如何找到的showText方法(跨dex查找類方法)

接下來更關(guān)鍵余寥,我們已經(jīng)找到了Test類领铐,我們?nèi)绾尾檎襰howText方法呢?

 0x0003: new-instance v0, com.ljj.fixtest.Test // type@16
 0x0005: invoke-direct {v0}, void com.ljj.fixtest.Test.<init>() // method@19
 0x0008: invoke-virtual {v0}, java.lang.String com.ljj.fixtest.Test.showText() // method@22
0x000020f8: f8d9e124    ldr.w   lr, [r9, #292]  ; pAllocObject
      0x000020fc: 1c29      mov     r1, r5
      0x000020fe: 2010      movs    r0, #16
      0x00002100: 47f0      blx     lr
      suspend point dex PC: 0x0003
      GC map objects:  v1 (r7), v2 (r8)
      0x00002102: 1c06      mov     r6, r0
      0x00002104: 1c28      mov     r0, r5
      0x00002106: 68c0      ldr     r0, [r0, #12]
      0x00002108: 1c31      mov     r1, r6
      0x0000210a: 6d80      ldr     r0, [r0, #88]
      0x0000210c: f8d0e02c  ldr.w   lr, [r0, #44]
      0x00002110: 47f0      blx     lr
      suspend point dex PC: 0x0005
      GC map objects:  v0 (r6), v1 (r7), v2 (r8)
      0x00002112: 1c28      mov     r0, r5
      0x00002114: 68c0      ldr     r0, [r0, #12]
      0x00002116: 1c31      mov     r1, r6
      0x00002118: 6e40      ldr     r0, [r0, #100]
      0x0000211a: f8d0e02c  ldr.w   lr, [r0, #44]
      0x0000211e: 47f0      blx     lr
      suspend point dex PC: 0x0008

我們直接來分析最后一段匯編代碼宋舷,也就是對應(yīng)的showText調(diào)用部分绪撵。r

0x00002112: 1c28        mov     r0, r5

5寄存器一開始是由r0寄存器賦值過來的,而調(diào)用時r0指向的就是調(diào)用者的ArtMethod地址祝蝠,也就是MainActivity 的onCreate方法對應(yīng)的ArtMethod音诈。所以下面代碼就是將r0指向調(diào)用者的ArtMethod。

 0x00002114: 68c0      ldr     r0, [r0, #12]

這個#12是什么意思呢绎狭?在/art/runtime/asm_support.h中定義了ArtMethod的相關(guān)結(jié)構(gòu)地址跳轉(zhuǎn)的常量细溅。不同版本這個值是不一樣的,android 5.1對應(yīng)的是12儡嘶,6.0對應(yīng)的4喇聊。那么這條指令的意思就是將r0指向ArtMethod的dex_cache_resolved_methods_位置。

#define METHOD_DEX_CACHE_METHODS_OFFSET 12

r6寄存器的值是由 movs r0, #16賦值給r0寄存器蹦狂,然后由r0賦值給r6的誓篱,Test類的type是16朋贬,這行的意思就是將this參數(shù)賦值給r1。

 0x00002116: 1c31      mov     r1, r6

這行不太好分析窜骄。首先要知道此時r0指向的artMethod的dex_cache_resolved_methods_锦募,那么#100是什么呢,通過不斷的觀察邻遏,發(fā)現(xiàn)每個方法調(diào)用都是從dex_cache_resolved_methods_的第三個位置開始計算的糠亩,阿里的深入探索android熱修復(fù)技術(shù)中寫到查找都是從數(shù)組的0x2開始的,這點我比較疑惑准验,我編譯出來的機器碼都是從0x3開始的削解,可能不同的版本不一樣吧,我在源碼中也沒有找到相關(guān)的定義沟娱。showText的methodId是22氛驮,這里的oat文件是在32位的機器上編譯的,每個指針占4個字節(jié)济似,(22+3)*4=100矫废。所以#100指向的就是showText所對應(yīng)的ArtMethod。

 0x00002118: 6e40      ldr     r0, [r0, #100]

44是什么意思呢砰蠢?這個同樣是是在/art/runtime/asm_support.h中定義的蓖扑。對應(yīng)于ArtMethod的entry_point_from_quick_compiled_code_字段。

#define METHOD_QUICK_CODE_OFFSET_32 44

這行代碼的意思就是找到showText的機器碼執(zhí)行入口台舱。準備執(zhí)行律杠。

0x0000211a: f8d0e02c  ldr.w   lr, [r0, #44]

好了,到此我們分析完了這段匯編代碼的含義竞惋,可能有人會有疑問柜去,#100上放的是showText對應(yīng)的ArtMethod的嗎?什么時候放上去的呢拆宛?
這里大致說一下Art下方法調(diào)用的過程嗓奢。

  • 當一個dex的Dex_cache被初始化的時候,resolved_methods數(shù)組里面的ArtMethod都是指向同一個名為Resolution Method浑厚,這個ArtMethod的特點是index為kDexNoIndex股耽,表明它不代表任何的類方法。
  • 啟動OAT文件的OAT頭部包含有一個quick_resolution_trampoline_offset_字段钳幅。這個quick_resolution_trampoline_offset_字段指向一小段Trampoline代碼物蝙。這一小段Trampoline代碼的作用是找到當前線程類型為Quick的函數(shù)跳轉(zhuǎn)表中的pQuickResolutionTrampoline項,并且跳到這個pQuickResolutionTrampoline項指向的函數(shù)art_quick_resolution_trampoline去執(zhí)行敢艰。
  • 當方法首次加載時诬乞,如果判斷出來方法的index是kDexNoIndex,則表明是一個運行時方法,就會執(zhí)行蹦床函數(shù)執(zhí)行去查找真正的方法丽惭,找到后會填充到DexCache中對應(yīng)的ArtMethod中击奶。下次運行時就可以直接從DexCache中找到該方法了辈双。

蹦床函數(shù)art_quick_resolution_trampoline會調(diào)用到artQuickResolutionTrampoline责掏,此時如果發(fā)現(xiàn)是Runtime方法,會觸發(fā)classLinker的resolveMethod方法去查找湃望。ok换衬,終于繞出來了,我們?nèi)タ纯磖esolveMethod的實現(xiàn):

mirror::ArtMethod* ClassLinker::ResolveMethod(const DexFile& dex_file, uint32_t method_idx,
                                              Handle<mirror::DexCache> dex_cache,
                                              Handle<mirror::ClassLoader> class_loader,
                                              Handle<mirror::ArtMethod> referrer,
                                              InvokeType type) {
  //1. 第一步
  mirror::ArtMethod* resolved = dex_cache->GetResolvedMethod(method_idx);
  if (resolved != nullptr && !resolved->IsRuntimeMethod()) {
    return resolved;
  }
 
  const DexFile::MethodId& method_id = dex_file.GetMethodId(method_idx);
  //2. 第二步
  mirror::Class* klass = ResolveType(dex_file, method_id.class_idx_, dex_cache, class_loader);
  if (klass == nullptr) {
    DCHECK(Thread::Current()->IsExceptionPending());
    return nullptr;
  }
  switch (type) {
    case kDirect:  // Fall-through.
    case kStatic:
     // 第三步
      resolved = klass->FindDirectMethod(dex_cache.Get(), method_idx);
      break;
    case kInterface:
      resolved = klass->FindInterfaceMethod(dex_cache.Get(), method_idx);
      DCHECK(resolved == nullptr || resolved->GetDeclaringClass()->IsInterface());
      break;
    case kSuper:  // Fall-through.
    case kVirtual:
      resolved = klass->FindVirtualMethod(dex_cache.Get(), method_idx);
      break;
    default:
      LOG(FATAL) << "Unreachable - invocation type: " << type;
  }
  if (resolved == nullptr) {
    // Search by name, which works across dex files.
   //第四步
    const char* name = dex_file.StringDataByIdx(method_id.name_idx_);
    const Signature signature = dex_file.GetMethodSignature(method_id);
    switch (type) {
      case kDirect:  // Fall-through.
      case kStatic:
        resolved = klass->FindDirectMethod(name, signature);
        break;
      case kInterface:
        resolved = klass->FindInterfaceMethod(name, signature);
        DCHECK(resolved == nullptr || resolved->GetDeclaringClass()->IsInterface());
        break;
      case kSuper:  // Fall-through.
      case kVirtual:
        resolved = klass->FindVirtualMethod(name, signature);
        break;
    }
  }
 ....
  }
}

按照代碼中步驟標示來解釋:

  • 第一步:注意此時dex_cache是主dex证芭,先去dex_cache中去查找瞳浦,很顯然,查找不到废士,因為resolveType時將方法保存在了patch.dex的dex_cache中了叫潦,主dex_cache是找不到的。
  • 第二步:會根據(jù)method_id.class_idx去主dex_cache中查找class官硝,很明顯是可以找到的矗蕊,還記得上面加載類時在resolveType時將class也放到了主dex中了。
  • 第三步:找到了class后氢架,會在class的directMethods數(shù)組中查找傻咖,按道理是可以找到的,但是在class.cc中會進行dexCache驗證岖研,在/art/runtime/mirror/class.cc的FindDeclaredDirectMethod方法卿操。而此時GetDexCache其實獲取的是patch.dex的dexCache,而傳入的dex_cache是主dex_cache,所以依然獲取不到孙援,進入第四步害淤。
if (GetDexCache() == dex_cache) {
        .... 
}
  • 第四步我們也看到了源碼中注釋,“Search by name, which works across dex files.” 到此拓售,徹底明白了筝家,跨dex是通過name和簽名來調(diào)用方法的。既然跨dex是通過name和簽名來進行查找的邻辉,那么在patch.dex中增加一個showTest2()
    按道理來講是不會找錯方法的溪王。
4.結(jié)論驗證

這里先說一下結(jié)論:當我們在app中調(diào)用patch中的方法,是通過name和簽名去查找的值骇,如果patch增加方法改變了類結(jié)構(gòu)莹菱,是不會出現(xiàn)地址錯亂的

下面進行驗證:在android5.1吱瘩,android6.0上進行驗證道伟,發(fā)現(xiàn)果然沒問題,隨意增加方法,都能打patch成功蜜徽,然而android7.0卻會找錯方法祝懂,繼續(xù)研究一下。
dump出android7.0的oat文件查看一下:


image.png

由于android7.0引入了混合編譯模式拘鞋,oat文件中默認并不會生成機器碼砚蓬,但是進行了指令優(yōu)化,invoke-virtual已經(jīng)被優(yōu)化成了invoke-virtual-quick指令盆色。后面直接跟上了vtable索引號灰蛙,兩個指令有什么區(qū)別呢?具體源碼定義在
/art/runtime/interpreter/interpreter_switch_impl.cc中隔躲,具體實現(xiàn)在/art/runtime/interpreter/interpreter_common.h中

  • 對于invoke-virtual指令
static inline bool DoInvoke(Thread* self, ShadowFrame& shadow_frame, const Instruction* inst,
                            uint16_t inst_data, JValue* result) {
  ArtMethod* const method = FindMethodFromCode<type, do_access_check>(
      method_idx, &receiver, &sf_method, self);
}

會進行方法的查找摩梧,F(xiàn)indMethodFromCode會進入resolveMethod方法去查找真正的方法。

  • invoke-virtual-quick指令
static inline bool DoInvokeVirtualQuick(Thread* self, ShadowFrame& shadow_frame,
                                        const Instruction* inst, uint16_t inst_data,
                                        JValue* result) {

  ArtMethod* const method = receiver->GetClass()->GetEmbeddedVTableEntry(vtable_idx);
 
  }
}

可以看到宣旱,quick指令是直接在class的vtable中按照index查找仅父,那問題就來了,安裝時方法的index是針對class的浑吟,即在class的vtable中是寫死的笙纤,此時我下發(fā)patch時增加了showTest2方法,那么很明顯showTest2會占用原來的showText的位置买置,從而出現(xiàn)地址錯亂粪糙。

至此,我們徹底分析完了所有的問題忿项,可能內(nèi)容有點多蓉冈,這里簡單的總結(jié)下:

結(jié)論一:

art(android N之前)下本地機器碼跨dex進行方法調(diào)用是通過方法的name和簽名進行的,在patch中通過增加方法改變類的結(jié)構(gòu)并不會導致地址錯亂轩触。需要說明兩點:

  • 第一寞酿,art生成oat文件可以指定多種方式。
  • 第二脱柱,我只是針對在修復(fù)方法前增加方法等改變伐弹,注意其他情形沒有研究和驗證。
  • 第三榨为,只分析了主dex跨dex訪問補丁dex的情形惨好,至于補丁中訪問到主dex情形沒有深入研究,但是從patch.dexde的機器碼中可以看出是通過pAllocObjectWithAccessCheck等Alloc類蹦床函數(shù)來回調(diào)的随闺。
結(jié)論二:
  • android N增加方法后導致地址錯亂是因為解釋執(zhí)行時進行指令優(yōu)化導致的日川,和本地機器碼沒什么大的關(guān)系。其實在dalvik上應(yīng)該也存在此問題矩乐,只是我們禁止了進行dexopt龄句,所以指令沒有優(yōu)化回论,從而屏蔽了該問題而已。

?PS:我只是利用熱修復(fù)來進行學習分歇,并沒有深入的做熱修復(fù)的相關(guān)工作傀蓉,只是出于對art虛擬機下地址錯亂問題的好奇而進行的,可能有很多地方解釋的不到位或者有誤职抡,歡迎指正葬燎。感覺這篇文章需要點基礎(chǔ),如果直接看本文的話繁调,不一定看得懂萨蚕,可以參考我之前寫的熱修復(fù)相關(guān)的文章結(jié)合文末的參考文章來看靶草,參考文章都是比較有價值的蹄胰,但是針對熱修復(fù)下art的跨dex調(diào)用問題都沒有闡述的很清楚,有問題歡迎留言討論奕翔。

參考文章:
1.老羅的Android運行時ART加載OAT文件的過程分析
2.老羅的Android運行時ART加載類和方法的過程分析
3.老羅的Android運行時ART執(zhí)行類方法的過程分析
4.老羅的ART運行時為新創(chuàng)建對象分配內(nèi)存的過程分析
5.滴滴Android熱修復(fù)探索
6.蘑菇街Android熱修復(fù)探索之路
7.Android中的類加載-查找和在hotpatch上的問題

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末裕寨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子派继,更是在濱河造成了極大的恐慌宾袜,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驾窟,死亡現(xiàn)場離奇詭異庆猫,居然都是意外死亡,警方通過查閱死者的電腦和手機绅络,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門月培,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恩急,你說我怎么就攤上這事杉畜。” “怎么了衷恭?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵此叠,是天一觀的道長。 經(jīng)常有香客問我随珠,道長灭袁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任窗看,我火速辦了婚禮茸歧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘烤芦。我一直安慰自己举娩,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著铜涉,像睡著了一般智玻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芙代,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天吊奢,我揣著相機與錄音,去河邊找鬼纹烹。 笑死页滚,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的铺呵。 我是一名探鬼主播裹驰,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼片挂!你這毒婦竟也來了幻林?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤音念,失蹤者是張志新(化名)和其女友劉穎沪饺,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闷愤,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡整葡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了讥脐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遭居。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖攘烛,靈堂內(nèi)的尸體忽然破棺而出魏滚,到底是詐尸還是另有隱情,我是刑警寧澤坟漱,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布鼠次,位于F島的核電站,受9級特大地震影響芋齿,放射性物質(zhì)發(fā)生泄漏腥寇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一觅捆、第九天 我趴在偏房一處隱蔽的房頂上張望赦役。 院中可真熱鬧,春花似錦栅炒、人聲如沸掂摔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乙漓。三九已至级历,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間叭披,已是汗流浹背寥殖。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留涩蜘,地道東北人嚼贡。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像同诫,于是被迫代替她去往敵國和親粤策。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355