1. 序言
? android在5.0開(kāi)始正式用art虛擬機(jī)取代了dalvik虛擬機(jī),不同版本的art虛擬機(jī)差別很大,android N開(kāi)始又引入了混合編譯模式。在這里我們只針對(duì)android N之前的art版本進(jìn)行分析,至于art和dalvik的區(qū)別,這里就不多說(shuō)了,最大的區(qū)別是art在安裝時(shí)存在aot過(guò)程,用于生成oat文件,這個(gè)oat文件既包含轉(zhuǎn)化前的dex文件,又包含機(jī)器指令,所以我們的應(yīng)用在運(yùn)行的時(shí)候可以免去[解釋?zhuān)葸@一層而直接加載機(jī)器指令。最后說(shuō)一點(diǎn),art是可以通過(guò)開(kāi)啟解釋模式進(jìn)行解釋執(zhí)行代碼的,此外,有一些情況,比如動(dòng)態(tài)生成的方法等也是需要解釋執(zhí)行的。文末會(huì)有參考文章的鏈接,建議大家對(duì)我略過(guò)的內(nèi)容不太清楚時(shí),可以去參考文章中學(xué)習(xí),畢竟側(cè)重點(diǎn)是不同的。
2. 問(wèn)題的引入
下面開(kāi)始說(shuō)重點(diǎn)。
?其實(shí)對(duì)于classloader熱修復(fù)方案的地址錯(cuò)亂問(wèn)題早有耳聞,最早是在騰訊的一篇文章Android_N混合編譯與對(duì)熱補(bǔ)丁影響解析看到的這個(gè)說(shuō)法,但是作者后續(xù)沒(méi)有進(jìn)行進(jìn)一步的解釋。后來(lái)看了看其他文章基本上是這樣解釋的。假設(shè)app中有一個(gè)Test類(lèi):
public class Test {
public String showTest1(){
return "art address error";
}
public String showText(){
return "I am an showText";
}
}
我們?cè)贛ainActivity中的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文件,說(shuō)明一下,我們接下來(lái)的分析過(guò)程都以android5.1的源碼進(jìn)行,后面會(huì)進(jìn)行匯編代碼的分析,x86指令集比ARM指令集閱讀起來(lái)復(fù)雜的多,如果大家想分析oat文件,最好還是進(jìn)行ARM的手機(jī)進(jìn)行。android5.x默認(rèn)情況下生成的oat文件是放在/data/dalvik-cache 目錄下,如果是ARM指令集的話,是會(huì)在/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é)果。


至于這段匯編的含義先不用管,后面會(huì)介紹。我們只需要關(guān)注標(biāo)紅的字眼,method@21,可以理解為showText()方法在dex中的位置。#96是該方法在ArtMethod中的偏移位置。
接下來(lái)我們給Test類(lèi)添加一個(gè)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文件。


發(fā)現(xiàn)同樣調(diào)用的showText(),方法id已經(jīng)變成了method@22,偏移也變成了#100.那問(wèn)題就來(lái)了。我們簡(jiǎn)單分析下:
假設(shè)Test類(lèi)需要修復(fù),我們打補(bǔ)丁時(shí)在Test類(lèi)中增加了一個(gè)showTest2方法,導(dǎo)致showText的方法的偏移由#96變成了#100,而此時(shí)#96指向的是showTest2方法,那么當(dāng)我們調(diào)用到showtext方法時(shí)會(huì)調(diào)用到showTest2方法,從而導(dǎo)致了地址錯(cuò)亂,這是網(wǎng)上大家關(guān)于地址錯(cuò)亂的解釋?zhuān)恢雷x到這里大家有沒(méi)有發(fā)現(xiàn)問(wèn)題?
?我們仔細(xì)來(lái)梳理一下,看看有沒(méi)有問(wèn)題。首先當(dāng)我們安裝app時(shí),會(huì)生成一個(gè)oat文件,我們稱(chēng)為host.oat。這個(gè)oat文件中有一個(gè)Test類(lèi),是待修復(fù)的,在這個(gè)oat文件中,showText()的方法是method@21,調(diào)用處的偏移是#96。接著我們下發(fā)補(bǔ)丁,通過(guò)DexClassLoader加載的patch.dex,在動(dòng)態(tài)加載的過(guò)程中,patch.dex也會(huì)生成一個(gè)oat文件,這里我們?yōu)榱藚^(qū)分,稱(chēng)為patch.oat。在patch.oat中方法的編號(hào)顯然是和host.oat沒(méi)有一點(diǎn)關(guān)系的。而我們加載Test類(lèi)時(shí),毫無(wú)疑問(wèn)加載到的是patch.oat中的Test類(lèi),否則熱修復(fù)就不會(huì)成功了,那么怎么可能存在#100的問(wèn)題,這種說(shuō)法明顯是在同一個(gè)dex中進(jìn)行調(diào)用考慮的,和熱修復(fù)(不考慮全量合成)是不符合的,因?yàn)闊嵝迯?fù)生效的時(shí)候,運(yùn)行起來(lái)是跨oat或者說(shuō)是跨dex的。如果真是按照機(jī)器碼執(zhí)行時(shí)寫(xiě)入的地址進(jìn)行跨dex調(diào)用,感覺(jué)很容易跑飛啊,即使我不插入showTest2方法,兩個(gè)oat文件的類(lèi)和方法的偏移都不是同一個(gè)基準(zhǔn)的,按說(shuō)機(jī)器碼應(yīng)該都不能找到patch.dex中的showText方法。到底怎么回事呢?
3. 探索答案
我們分以下幾個(gè)步驟來(lái)探索。
- art下調(diào)用者是怎么通過(guò)機(jī)器碼找到我們patch中的方法的(art下跨dex方法調(diào)用的實(shí)現(xiàn))
- art下像上文中增加方法,真的會(huì)地址錯(cuò)亂嗎?
我們先來(lái)看一下第一個(gè)問(wèn)題,這里說(shuō)明一下,因?yàn)閷?duì)匯編等底層的知識(shí)不太了解,以下的解釋可能有些地方表述不到位,甚至可能有錯(cuò)誤,如果有問(wèn)題,希望大家一起探討學(xué)習(xí)。
3.1 如何找到的Test類(lèi)(跨dex查找類(lèi))
我們首先看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
省略了無(wú)關(guān)的機(jī)器碼,這段匯編主要是new Test().showText()相關(guān)的代碼。這里只針對(duì)關(guān)鍵的指令進(jìn)行分析,把握住我們的主線,如果擴(kuò)展開(kāi)來(lái),一篇文章肯定是寫(xiě)不下的,大家可以參考文末老羅的相關(guān)文章,我也是參照老羅的文章來(lái)查看的。
首先看第一行,pAllocObject很明顯是在創(chuàng)建對(duì)象,在art虛擬機(jī)啟動(dòng)進(jìn)行初始化的過(guò)程中,會(huì)注冊(cè)很多的Trampoline,稱(chēng)為跳轉(zhuǎn)表,pAllocObject就是其中的一個(gè),Trampoline指向的是一段匯編代碼,在匯編中會(huì)去執(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
接著會(huì)調(diào)用artAllocObjectFromCode方法來(lái)創(chuàng)建對(duì)象。這里需要知道,每個(gè)dex都會(huì)生成一個(gè)DexCache,會(huì)緩存對(duì)應(yīng)dex中加載解析過(guò)的所有類(lèi)和方法,每個(gè)類(lèi)會(huì)被分配一個(gè)typeId,每個(gè)方法會(huì)被分配一個(gè)methodId。這個(gè)方法首先會(huì)根據(jù)typeId在調(diào)用者的DexCache中查看該類(lèi)是否加載過(guò),如果沒(méi)加載過(guò),則會(huì)調(diào)用ClassLinker的ResolveType方法進(jìn)行類(lèi)的查找解析。
代碼位置: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;
}
我們針對(duì)這段代碼進(jìn)行詳細(xì)分析,首先dex_cache是通過(guò)referrer->GetDexCache()拿到的,這個(gè)referrer是caller,也就是調(diào)用者所在的類(lèi),在我們的例子中就是指的MainActivity,所以這個(gè)dex_cache我們可以認(rèn)為是主dex的cache。dex_file相當(dāng)于是主dex。在主dex的cache中根據(jù)type_id去查找,因?yàn)槭鞘状渭虞d,所以
mirror::Class* resolved = dex_cache->GetResolvedType(type_idx);
返回nullptr。接著會(huì)從主dex中根據(jù)type_id拿到類(lèi)的名字,繼續(xù)調(diào)用FindClass去class_loader中查找類(lèi),這塊就涉及到類(lèi)的加載機(jī)制了,我們就不多說(shuō)了,會(huì)優(yōu)先找到我們patch.dex中的Test類(lèi)。類(lèi)加載及方法調(diào)用的調(diào)用鏈為:
FindClass->FindInClassPath->DefineClass->LoadClass->LoadClassMembers->LoadMethod->LinkCode
加載的每個(gè)類(lèi)對(duì)應(yīng)一個(gè)oatClass,class的每個(gè)field會(huì)用一個(gè)ArtField表示,每個(gè)method都會(huì)對(duì)應(yīng)一個(gè)ArtMethod對(duì)象。loadMethod方法會(huì)對(duì)創(chuàng)建的ArtMethod進(jìn)行賦值。這里我們只需要知道ArtMethod的dex_cache_resolved_methods_數(shù)組指向的是所在class對(duì)應(yīng)的DexCache中被resolved了的方法。在這里也就是在patch.dex中被resolved的方法,和主dexCache沒(méi)任何關(guān)系。

LinkCode過(guò)程會(huì)對(duì)ArtMethod的執(zhí)行入口進(jìn)行設(shè)置,是compiled_code方式還是interpreter解釋執(zhí)行,不想扯的太遠(yuǎn)?;氐絉esolveType方法中,注意這一句很關(guān)鍵,當(dāng)我們通過(guò)FindClass方法找到了對(duì)應(yīng)的class后,此時(shí)的dex_cache是主dex的,也就是從patch.dex中拿到了class后,同時(shí)填充到了主dex的dexcache中對(duì)應(yīng)的位置上了。
dex_cache->SetResolvedType(type_idx, resolved);
好了至此我們分析完了pAllocObject的過(guò)程,完成了Test類(lèi)的加載解析。到此我們了解了,這段匯編執(zhí)行過(guò)后是通過(guò)類(lèi)的簽名在patch.dex中拿到的Test類(lèi),同時(shí)緩存到了自己的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查找類(lèi)方法)
接下來(lái)更關(guān)鍵,我們已經(jīng)找到了Test類(lèi),我們?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
我們直接來(lái)分析最后一段匯編代碼,也就是對(duì)應(yīng)的showText調(diào)用部分。r
0x00002112: 1c28 mov r0, r5
5寄存器一開(kāi)始是由r0寄存器賦值過(guò)來(lái)的,而調(diào)用時(shí)r0指向的就是調(diào)用者的ArtMethod地址,也就是MainActivity 的onCreate方法對(duì)應(yīng)的ArtMethod。所以下面代碼就是將r0指向調(diào)用者的ArtMethod。
0x00002114: 68c0 ldr r0, [r0, #12]
這個(gè)#12是什么意思呢?在/art/runtime/asm_support.h中定義了ArtMethod的相關(guān)結(jié)構(gòu)地址跳轉(zhuǎn)的常量。不同版本這個(gè)值是不一樣的,android 5.1對(duì)應(yīng)的是12,6.0對(duì)應(yīng)的4。那么這條指令的意思就是將r0指向ArtMethod的dex_cache_resolved_methods_位置。
#define METHOD_DEX_CACHE_METHODS_OFFSET 12
r6寄存器的值是由 movs r0, #16賦值給r0寄存器,然后由r0賦值給r6的,Test類(lèi)的type是16,這行的意思就是將this參數(shù)賦值給r1。
0x00002116: 1c31 mov r1, r6
這行不太好分析。首先要知道此時(shí)r0指向的artMethod的dex_cache_resolved_methods_,那么#100是什么呢,通過(guò)不斷的觀察,發(fā)現(xiàn)每個(gè)方法調(diào)用都是從dex_cache_resolved_methods_的第三個(gè)位置開(kāi)始計(jì)算的,阿里的深入探索android熱修復(fù)技術(shù)中寫(xiě)到查找都是從數(shù)組的0x2開(kāi)始的,這點(diǎn)我比較疑惑,我編譯出來(lái)的機(jī)器碼都是從0x3開(kāi)始的,可能不同的版本不一樣吧,我在源碼中也沒(méi)有找到相關(guān)的定義。showText的methodId是22,這里的oat文件是在32位的機(jī)器上編譯的,每個(gè)指針占4個(gè)字節(jié),(22+3)*4=100。所以#100指向的就是showText所對(duì)應(yīng)的ArtMethod。
0x00002118: 6e40 ldr r0, [r0, #100]
44是什么意思呢?這個(gè)同樣是是在/art/runtime/asm_support.h中定義的。對(duì)應(yīng)于ArtMethod的entry_point_from_quick_compiled_code_字段。
#define METHOD_QUICK_CODE_OFFSET_32 44
這行代碼的意思就是找到showText的機(jī)器碼執(zhí)行入口。準(zhǔn)備執(zhí)行。
0x0000211a: f8d0e02c ldr.w lr, [r0, #44]
好了,到此我們分析完了這段匯編代碼的含義,可能有人會(huì)有疑問(wèn),#100上放的是showText對(duì)應(yīng)的ArtMethod的嗎?什么時(shí)候放上去的呢?
這里大致說(shuō)一下Art下方法調(diào)用的過(guò)程。
- 當(dāng)一個(gè)dex的Dex_cache被初始化的時(shí)候,resolved_methods數(shù)組里面的ArtMethod都是指向同一個(gè)名為Resolution Method,這個(gè)ArtMethod的特點(diǎn)是index為kDexNoIndex,表明它不代表任何的類(lèi)方法。
- 啟動(dòng)OAT文件的OAT頭部包含有一個(gè)quick_resolution_trampoline_offset_字段。這個(gè)quick_resolution_trampoline_offset_字段指向一小段Trampoline代碼。這一小段Trampoline代碼的作用是找到當(dāng)前線程類(lèi)型為Quick的函數(shù)跳轉(zhuǎn)表中的pQuickResolutionTrampoline項(xiàng),并且跳到這個(gè)pQuickResolutionTrampoline項(xiàng)指向的函數(shù)art_quick_resolution_trampoline去執(zhí)行。
- 當(dāng)方法首次加載時(shí),如果判斷出來(lái)方法的index是kDexNoIndex,則表明是一個(gè)運(yùn)行時(shí)方法,就會(huì)執(zhí)行蹦床函數(shù)執(zhí)行去查找真正的方法,找到后會(huì)填充到DexCache中對(duì)應(yīng)的ArtMethod中。下次運(yùn)行時(shí)就可以直接從DexCache中找到該方法了。
蹦床函數(shù)art_quick_resolution_trampoline會(huì)調(diào)用到artQuickResolutionTrampoline,此時(shí)如果發(fā)現(xiàn)是Runtime方法,會(huì)觸發(fā)classLinker的resolveMethod方法去查找。ok,終于繞出來(lái)了,我們?nèi)タ纯磖esolveMethod的實(shí)現(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;
}
}
....
}
}
按照代碼中步驟標(biāo)示來(lái)解釋?zhuān)?/p>
- 第一步:注意此時(shí)dex_cache是主dex,先去dex_cache中去查找,很顯然,查找不到,因?yàn)閞esolveType時(shí)將方法保存在了patch.dex的dex_cache中了,主dex_cache是找不到的。
- 第二步:會(huì)根據(jù)method_id.class_idx去主dex_cache中查找class,很明顯是可以找到的,還記得上面加載類(lèi)時(shí)在resolveType時(shí)將class也放到了主dex中了。
- 第三步:找到了class后,會(huì)在class的directMethods數(shù)組中查找,按道理是可以找到的,但是在class.cc中會(huì)進(jìn)行dexCache驗(yàn)證,在/art/runtime/mirror/class.cc的FindDeclaredDirectMethod方法。而此時(shí)GetDexCache其實(shí)獲取的是patch.dex的dexCache,而傳入的dex_cache是主dex_cache,所以依然獲取不到,進(jìn)入第四步。
if (GetDexCache() == dex_cache) {
....
}
- 第四步我們也看到了源碼中注釋?zhuān)癝earch by name, which works across dex files.” 到此,徹底明白了,跨dex是通過(guò)name和簽名來(lái)調(diào)用方法的。既然跨dex是通過(guò)name和簽名來(lái)進(jìn)行查找的,那么在patch.dex中增加一個(gè)showTest2()
按道理來(lái)講是不會(huì)找錯(cuò)方法的。
4.結(jié)論驗(yàn)證
這里先說(shuō)一下結(jié)論:當(dāng)我們?cè)赼pp中調(diào)用patch中的方法,是通過(guò)name和簽名去查找的,如果patch增加方法改變了類(lèi)結(jié)構(gòu),是不會(huì)出現(xiàn)地址錯(cuò)亂的。
下面進(jìn)行驗(yàn)證:在android5.1,android6.0上進(jìn)行驗(yàn)證,發(fā)現(xiàn)果然沒(méi)問(wèn)題,隨意增加方法,都能打patch成功,然而android7.0卻會(huì)找錯(cuò)方法,繼續(xù)研究一下。
dump出android7.0的oat文件查看一下:

由于android7.0引入了混合編譯模式,oat文件中默認(rèn)并不會(huì)生成機(jī)器碼,但是進(jìn)行了指令優(yōu)化,invoke-virtual已經(jīng)被優(yōu)化成了invoke-virtual-quick指令。后面直接跟上了vtable索引號(hào),兩個(gè)指令有什么區(qū)別呢?具體源碼定義在
/art/runtime/interpreter/interpreter_switch_impl.cc中,具體實(shí)現(xiàn)在/art/runtime/interpreter/interpreter_common.h中
- 對(duì)于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);
}
會(huì)進(jìn)行方法的查找,F(xiàn)indMethodFromCode會(huì)進(jìn)入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查找,那問(wèn)題就來(lái)了,安裝時(shí)方法的index是針對(duì)class的,即在class的vtable中是寫(xiě)死的,此時(shí)我下發(fā)patch時(shí)增加了showTest2方法,那么很明顯showTest2會(huì)占用原來(lái)的showText的位置,從而出現(xiàn)地址錯(cuò)亂。
至此,我們徹底分析完了所有的問(wèn)題,可能內(nèi)容有點(diǎn)多,這里簡(jiǎn)單的總結(jié)下:
結(jié)論一:
art(android N之前)下本地機(jī)器碼跨dex進(jìn)行方法調(diào)用是通過(guò)方法的name和簽名進(jìn)行的,在patch中通過(guò)增加方法改變類(lèi)的結(jié)構(gòu)并不會(huì)導(dǎo)致地址錯(cuò)亂。需要說(shuō)明兩點(diǎn):
- 第一,art生成oat文件可以指定多種方式。
- 第二,我只是針對(duì)在修復(fù)方法前增加方法等改變,注意其他情形沒(méi)有研究和驗(yàn)證。
- 第三,只分析了主dex跨dex訪問(wèn)補(bǔ)丁dex的情形,至于補(bǔ)丁中訪問(wèn)到主dex情形沒(méi)有深入研究,但是從patch.dexde的機(jī)器碼中可以看出是通過(guò)pAllocObjectWithAccessCheck等Alloc類(lèi)蹦床函數(shù)來(lái)回調(diào)的。
結(jié)論二:
- android N增加方法后導(dǎo)致地址錯(cuò)亂是因?yàn)榻忉寛?zhí)行時(shí)進(jìn)行指令優(yōu)化導(dǎo)致的,和本地機(jī)器碼沒(méi)什么大的關(guān)系。其實(shí)在dalvik上應(yīng)該也存在此問(wèn)題,只是我們禁止了進(jìn)行dexopt,所以指令沒(méi)有優(yōu)化,從而屏蔽了該問(wèn)題而已。
?PS:我只是利用熱修復(fù)來(lái)進(jìn)行學(xué)習(xí),并沒(méi)有深入的做熱修復(fù)的相關(guān)工作,只是出于對(duì)art虛擬機(jī)下地址錯(cuò)亂問(wèn)題的好奇而進(jìn)行的,可能有很多地方解釋的不到位或者有誤,歡迎指正。感覺(jué)這篇文章需要點(diǎn)基礎(chǔ),如果直接看本文的話,不一定看得懂,可以參考我之前寫(xiě)的熱修復(fù)相關(guān)的文章結(jié)合文末的參考文章來(lái)看,參考文章都是比較有價(jià)值的,但是針對(duì)熱修復(fù)下art的跨dex調(diào)用問(wèn)題都沒(méi)有闡述的很清楚,有問(wèn)題歡迎留言討論。
參考文章:
1.老羅的Android運(yùn)行時(shí)ART加載OAT文件的過(guò)程分析
2.老羅的Android運(yùn)行時(shí)ART加載類(lèi)和方法的過(guò)程分析
3.老羅的Android運(yùn)行時(shí)ART執(zhí)行類(lèi)方法的過(guò)程分析
4.老羅的ART運(yùn)行時(shí)為新創(chuàng)建對(duì)象分配內(nèi)存的過(guò)程分析
5.滴滴Android熱修復(fù)探索
6.蘑菇街Android熱修復(fù)探索之路
7.Android中的類(lèi)加載-查找和在hotpatch上的問(wèn)題