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é)果滚停。
至于這段匯編的含義先不用管,后面會介紹粥惧。我們只需要關(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文件挂签。
發(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)系。
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文件查看一下:
由于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上的問題