編輯:關於android開發
這個選題很大,但並不是一開始就有這麼高大上的追求。最初之時,只是源於對Xposed的好奇。Xposed幾乎是定制ROM的神器軟件技術架構或者說方法了。它到底是怎麼實現呢?我本意就是想搞明白Xposed的實現原理,但隨著代碼研究的深入,我發現如果不了解虛擬機的實現,而僅簡單停留在Xposed的調用流程之上,那真是對Xposed最大的不敬了。另外,歪果仁為什麼能寫出Xposed?Android上的Java虛擬機對他們來說應該也是相對陌生的,何以他們能做而我們沒有人搞出這樣的東西?
所以,在研究Xposed之後,我決定把虛擬機方面的東西也來研究一番。誠如我在很多場合中提到的關於Android學習的三個終極問題(其實對其他各科學習也適用):學什麼?怎麼學?學到什麼程度為止?關於這三個問題,以本次研究的情況來看,回答如下:
除了這三個問題,其實還有一個隱含的疑問,學完之後有什麼用呢?
言歸正傳,現在開始正式介紹dalvik,請牢記關於它的學習目標和學習程度。
你也可以下載本專題對應的demo代碼用於學習。
Class文件是理解Vm實現的關鍵。關於Class文件的結構,這裡介紹的內容直接參考JVM規范,因為它是最權威的資料。
Oracle的JVM SE7官方規范:https://docs.oracle.com/javase/specs/jvms/se7/html/
還算很有良心,純網頁版的,也可以下載PDF版。另外,周志明老師曾經翻譯過中文版的JVM規范,網上可搜索到。
作為分析Class文件的入口,我在Demo示例中提供了一個特別簡單的例子,代碼如圖1所示:
TestMain類的代碼簡單到不行,此處也不擬多說,因為沒有特殊之處。
當我們用eclipse編譯這個類後,將得到bin/com/test/TestMain.class。這個TestMain.class就是我們要分析的Class文件了。
Class文件到底是什麼東西?我覺得一種通俗易懂的解釋就是:
在某種哲學意義上看,java源文件和處理得到的class文件是同一種東西......
那麼,這個給VM使用的class文件,其內部結構是怎樣的呢?Jvm規范很聰明,它通過一個C的數據結構表達了class文件結構。這個數據結構如圖2所示:
請大家務必駐足停留片刻,因為搞清楚圖2的內容對後續的學習非常關鍵。圖2的ClassFile這個數據結構真得是太容易理解了。相比那些native的二進制程序而言,ClassFile的組織結構和Java源碼的組織結構匹配度非常高,以致於我第一眼看到這個結構體時,我覺得自己差不多就理解了它:
Class文件用javap工具可以很好得解析成圖2那樣的格式,我這裡替大家解析了一把,結果如圖3所示(先顯示部分內容):
注意,解析方法為:javap -verbose xxxx.class
先來看看常量池。
常量池看起來陌生,其實簡單得要死。注意,count_pool_count是常量池數組長度+1。比如,假設某個Class文件常量池只有4個元素,那麼count_pool_count=5)。
javap解析class文件的時候,常量池的索引從1算起,0默認是給VM自己用得,一般不顯示0這一項。這也是為什麼圖3中常量池第一個元素以#1開頭。所以,如果count_pool_count=5的話,真正有用的元素是從count_pool[1]到count_pool[4]。
常量池數組的元素類型由下面的代碼表示:
cp_info { //特別注意,這是介紹的cp_info是相關元素類型的通用表達。
u1 tag; //tag為1個字節長。不論cp_info具體是哪種,第一個字節一定代表tag
u1 info[]; //其他信息,長度隨tag不同而不同
}
//tag取值,先列幾個簡單的:
tag=7 <==info代表這個cp_info是CONSTANT_Class_info結構體
tag=9<==info代表CONSTANT_Fieldrefs_info結構體
tag=10<==info代表CONSTANT_Methodrefs_info結構體
tag=8<==info代表CONSTANT_String_info結構體
tag=1<==info代表CONSTANT_Utf8_info結構體
在JVM規范中,真正代表字符串的數據結構是CONSTANT_Utf8_info結構體,它的結構如下代碼所示:
CONSTANT_Utf8_info {
u1 tag;
u2 length; //下面就是存儲UTF8字符串的地方了
u1 bytes[length];
}
大家看圖3中常量池的內容,比如#2=Utf8 com/test/TestMain 這行表示:
數組第二個元素的類型是CONSTANT_Utf8_info,字符串為“com/test/TestMain”
下面我們看幾個常用的常量池元素類型
這個類型是用於描述類信息的,此處的類信息很簡單,就是類名(也就是代表類名的字符串)
CONSTANT_Class_info {
u1 tag; //tag取值為7,代表CONSTANT_Class_info
u2 name_index; //name_index表示代表自己類名的字符串信息位於於常量池數組中哪一個,也就是索引
}
唉,夠懶的,name_index對應的那個常量池元素必須是CONSTANT_Utf8_info,也就是字符串。圖3中的例子,咱們再看看:
#1 = Class #2 //com/test/TestMain
#2 = Utf8 com/test/TestMain
這說明:
這個結構也是常量池數據結構中中比較重要的一個,干什麼用得呢?恩,它用來描述方法/成員名以及類型信息的。有點JNI基礎的童鞋相信不難明白,在JNI中,一個類的成員函數或成員變量都可以由這個類名字符串+函數名字符串+參數類型字符串+返回值類型來確定(如果是成員變量,就是類名字符串+變量名字符串+類型字符串)來表達。既然是字符串,那麼NameAndType_Info也就是存儲了對應字符串在常量池數組中的索引:
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index; //方法名或域名對應的字符串索引
u2 descriptor_index; //方法信息(參數+返回值),或者成員變量的信息(類型)對應的字符串索引
}
//還是來看圖3中的例子吧
#13 = Utf8 ()V
#15 = NameAnType #16.#13 //合起來就是test.()V 函數名是test,參數和返回值是()V
#16=Utf8 test
太簡單了,都不惜得說...,請大家自行解析#25這個常量池元素的內容,一定要做喔!
注意,對於構造函數和類初始化函數來說,JVM要求函數名必須是
Methodref_Info還有兩個兄弟,分別是Fieldref_Info,InterfaceMethodref_Info,他們三用於描述方法、成員變量和接口信息。剛才的NameAndType_Info其實已經描述了方法和成員變量信息的一部分,唯一還缺的就是沒有地方描述它們屬於哪個類。而咱這三兄弟就補全了這些信息。他們三的數據結構如圖4所示:
如此直白簡單,不解釋了。不放心的童鞋們請對照圖3的例子自行玩耍!
常量池先介紹到這,它還有一些有用的信息,不過要等到後面我們碰到具體問題時再分析
剛才在常量池介紹中有提到Methodref_Info和Fieldref_Info,不過這兩個Info無非是描述了函數或成員變量的名字,參數,類型等信息。但是真正的方法、成員變量信息還包括比如訪問權限,注解,源代碼位置等。對於方法來說,更重要的還包括其函數功能(即這個函數對應的字節碼)。
在Java VM中,方法和成員變量的完整描述由如圖5所示的數據結構來表達的:
attribute_info結構體很簡單,如下代碼所示:
attribute_info {//特別注意,這裡描述的attribute_info結構體也是具體屬性數據結構的通用表達
u2 attribute_name_index; //attribute_info的描述,指向常量池的字符串
u4 attribute_length; //具體的內容由info數組描述
u1 info[attribute_length];
}
Java VM規范中,attribute類型比較多,我們重點介紹幾個,先來看代表一個函數實際內容的Code屬性。
代表Code屬性的數據結構如圖6所示:
來看個實際例子吧,如圖7所示(接著圖3的例子):
圖7中:
請大家自行解析圖7中最後一行,看看能搞明白LocalVariableTable的含義不...
另外,Android SDK build Tools中的dx工具dump class文件得到的信息更全,大家可以試試。
使用方法是:dx --dump --debug xxx.class。
Class文件先介紹到這,下面我們來看看Android平台上的dex文件。
Android平台中沒有直接使用Class文件格式,因為早期的Anrdroid手機內存,存儲都比較小,而Class文件顯然有很多可以優化的地方,比如每個Class文件都有一個常量池,裡邊存儲了一些字符串。一串內容完全相同的字符串很有可能在不同的Class文件的常量池中存在,這就是一個可以優化的地方。當然,Dex文件結構和Class文件結構差異的地方還很多,但是從攜帶的信息上來看,Dex和Class文件是一致的。所以,你了解了Class文件(作為Java VM官方Spec的標准),Dex文件結構只不過是一個變種罷了(從學習到什麼程度為止的問題來看,如果不是要自己來解析Dex文件,或者反編譯/修改dex文件,我覺得大致了解下Dex文件結構的情況就可以了)。圖8所示為Dex文件結構的概貌:
有一點需要說明:傳統Class文件是一個Java源碼文件會生成一個.Class文件,而Android是把所有Class文件進行合並,優化,然後生成一個最終的class.dex,如此,多個Class文件裡如果有重復的字符串,當把它們都放到一個dex文件的時候,只要一份就可以了嘛。
dex頭部信息中的magic取值為“dex\n035\0”
proto_ids:描述函數原型信息,包括返回值,參數信息。比如“test:()V”
methods_ids:函數信息,包括所屬類及對應的proto信息。比如
"Lcom.test.TestMain. test:()V",.前面是類信息,後面屬於proto信息
下面我們將示例TestMain.class轉換成dex文件,然後再用dexdump工具看看它的結果,如圖9所示:
具體方法:
圖9中的dexdump結果其實比圖3還要清晰易懂。我們重點關注code段的內容(圖中紅框的部分):
Android官方文檔:https://source.android.com/devices/tech/dalvik/dex-format.html
說實話,寫完這一小節的時候,我又反復看了官方文檔還有其他一些參考文檔。很痛苦,主要是東西太多,而我們目前又沒有實際的問題,所以基本上是一邊看一邊忘!
恩。至少在這個階段,先了解到這個程度就好。後面會隨著學習的深入,有更多的深入知識,到時候根據需求再加進來。
再來看odex。odex是Optimized dex的簡寫,也就是優化後的dex文件。為什麼要優化呢?主要還是為了提高Dalvik虛擬機的運行速度。但是odex不是簡單的、通用的優化,而是在其優化過程中,依賴系統已經編譯好的其他模塊,簡單點說:
圖10給出了圖1所示示例代碼得到的test.dex,然後利用dexopt得到test.odex,接著利用dexdump得到其內容,最後利用Beyond Compare比較這兩個文件的差異。
圖10中,綠色框中是test.dex的內容,紅色框中是test.odex的內容,這也是兩個文件的差異內容:
vtable是虛表的意思,一般在OOP實現中用得很多。vtable一定比methodtable快麼?那倒是有可能。我個人猜測:
1 http://mylifewithandroid.blogspot.com/2009/05/about-quick-method-invocation.html介紹了vtable的生成,大家可以看看
2 http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html 詳細描述了dex/odex指令的格式,大家有興趣可以做參考。
前面曾經提到過,odex文件的生成依賴於BOOTCLASSPATH提供的系統核心庫。以我們這個簡單的例子而言,core.jar是必須的(java基礎類大部分封裝在core.jar中)。另外,core.jar對應的core.odex文件也需要。所有這些文件我都已經上傳到示例代碼倉庫的javavmtest/odex-test目錄下。然後執行dextest.sh腳本。此腳本內容如下:
#!/bin/sh
#在根目錄下建立/data/dalvik-cache目錄,這是因為odex往往是在機器上生成的,所有這些目錄都是
#設備上才有。我們模擬一下罷了
sudo mkdir -p /data/dalvik-cache/
#core.dex文件名:這也是模擬了機器上的情況。系統將dex文件的絕對路徑名換成了@來唯一標示
#一個dex文件。由於我在制作core.dex的時候,該core.jar包放在了/home/innost/workspace/my-projects/
#javavmtest/odex-test下,生成的core.dex就應該命名為home@innost@workspace@my-projects@javavmtest@odex-test@[email protected]
CORE_TARGET_DEX="home@innost@workspace@my-projects@javavmtest@[email protected]@"
CURRENT_PATH=`pwd`
#為了減少麻煩,我這裡做了一個鏈接,將需要的dex文件鏈接到此目錄下的core.dex
sudo ln -sf ${CURRENT_PATH}/core.dex /data/dalvik-cache/${CORE_TARGET_DEX}classes.dex
rm test.odex
#設置BOOTCLASSPATH變量
export BOOTCLASSPATH=${CURRENT_PATH}/core.jar
/home/innost/workspace/android-4.4.4/out/host/linux-x86/bin/dexopt --preopt ${CURRENT_PATH}/test.jar test.odex "m=y u=n"
#刪掉/data目錄
sudo rm -rf /data
odex文件由dexopt生成,這個工具在SDK裡沒有,只能由源碼生成。odex文件的生成有三種方式:
實際上dex轉odex是利用了dalvik vm,裡邊也會運行dalvik vm的相關方法。
本節主要介紹了Class文件,以及在Android平台上的變種dex和odex文件。以標准角度來看,Class文件是由Java VM規范定義的,所以通用性更廣。dex或者是odex只不過是規范在Android平台上的一種具體實現罷了,而且dex/odex在很多地方也需要遵守規范。因為dex文件的來源其實還是Class文件。
對於初學者而言,我建議了解Class文件的結構為主。另外,關於dex/odex的文件結構,除非有明確需求(比如要自己修改字節碼等),否則以了解原理就可以。而且,將來我們看到dalvik vm的實際代碼後,你會發現dex的文件內容還是會轉換成代碼裡的那些你很熟悉的類型,數據結構。比如dex存儲字符串是一種優化後的方法,但是到vm代碼中,還不是只能用字符串來表示嗎?
另外,你還會發現,Class、dex還是odex文件都存儲了很多源碼中的信息,比如類名、函數名、參數信息、成員變量信息等,而且直接用得是字符串。這和Native的二進制比起來,就容易看懂多了。
下面我們來講講字節碼的執行。很多人對Java字節碼到底是怎麼運行的比較好奇。Java字節碼的運行和操作系統上(比如Linux)一個進程是如何執行其代碼,從理論上說是一致的。只不過Java字節碼的執行是JVM,而操作系統上一個進程其代碼的執行是由CPU來完成。當然,現在JVM也可以把Java字節碼直接轉成機器碼,然後交給CPU來執行。這樣可以顯著提高運行速度。
本節我們將介紹Android平台上Java字節碼的執行。當然,我並不會具體分析每一行代碼都是怎麼執行的(比如函數參數的入棧,寄存器的使用),而只是想向大家介紹大體的流程,滿足大家的好奇心。如果有更深次的學習需求,你就可以在本節基礎上自行開展了!
下面所講內容的源碼全部位於AOSP源碼/dalvik/vm/mterp/out目錄下
mterp/out目錄下有好些個源碼文件,如圖11所示:
這個目錄中的文件就是不同平台上,Java字節碼處理的代碼。每一個平台包含一個匯編文件和一個C文件。
下面我們看對於new操作,portable、arm平台的處理。
在InterpC-portable.cpp中,有幾處關鍵代碼,先來看圖12:
在這段代碼中:
那麼,handlerTable是怎麼定義的呢?來看圖13:
圖13中:
那麼,new操作符對應的goto label在哪裡呢?來看圖14:
你看,portable.cpp中通過HANDLE_OPCODE(OP_NEW_INSTANCE)定義了new操作符的處理邏輯。這段邏輯中,真正分配內存的操作是由紅框的dvmAllocObject來處理的。
看到這裡,你會發現JVM執行Java字節碼還是比較容易理解的。其實對於arm等平台也是這樣。
和portable下dvmInterpretPortable函數(Java字節碼執行的入口函數)相對應的,其他模式下的入口函數是dvmMterpStd,其代碼如圖15所示:
dvmMterpStd中最重要的是dvmMterpStdRun,這個函數是由各平台對應的xxx.S匯編文件定義的。InterpAsm-armv7-a-neon.S對應的dvmMterpStdRun函數以及對new的處理邏輯如圖16所示:
圖16中:
這一節我們介紹了JVM是怎麼執行Java字節碼的,主要以揭秘性質為主,大家也以掌握原理為首要任務。其中,portable模式下,操作碼是一條一條解釋執行的。而具體CPU平台上,則是由相關匯編代碼來處理。二者實際上大同小異。但是由CPU來執行,顯然處理要快,比如對於+這種操作,用portable的解釋執行當然比直接轉換成機器指令來執行要慢很多。
到此,我們了解了Class文件結構,以及Java字節碼到底是怎麼執行的。下一步,我們就開始正式分析Dalvik虛擬機了。
Android平台中,第一個虛擬機是通過app_process進程啟動的,這個進程也就是大名鼎鼎的Zygote(含義是受精卵)。Zygote的啟動我在《深入理解Android卷I》第四章深入理解Zygote中有詳細分析,這裡我們簡單回顧下。圖17所示為zygote啟動的觸發機制:
上述代碼是位於init.rc中,當Linux天字號第一進程init啟動後,將執行init.rc中的內容。此處的zygote的一個Service,對應的進程是/system/bin/app_process,後面的--zygote...等是該進程的參數。
zygote,也就是app_process,其源碼位於frameworks/base/cmds/app_process裡,源碼比較少,主要是一個App_main.cpp。其main函數如下:
int main(int argc, char* const argv[])
{
.......
AppRuntime runtime; //AppRuntime是關鍵數據結構
const char* argv0 = argv[0];
int i = runtime.addVmArguments(argc, argv);//添加參數,不重要
// Parse runtime arguments. Stop at first unrecognized option.
.......
if (zygote) {//我是zygote
runtime.start("com.android.internal.os.ZygoteInit",
startSystemServer ? "start-system-server" : "");
} ......
}
runtime是核心對象,其類型是AppRuntime,是定義在app_process中的一個Class,它從AndroidRuntime派生。start函數就是AndroidRuntime中的,用於啟動VM的入口。
start函數我們分兩部分講,第一部分如圖18所示:
第一部分包含三個主要函數:
該函數內容如圖19所示:
該函數:
所以,以後調用比如JNI_CreateVM_函數的時候,我們知道它的真實實現其實是位於libdvm.so中的JNI_CreateVM就好。
比較簡單,Nothing more....
startVM屬於Android Runtime start函數的第一部分,不過該函數內容比較多,我們單獨搞一大節來講它!
startVM此函數前面一大段都是參數處理,所以對本文有意義的內容其實只有圖20所示的部分:
核心內容還是在libdvm.so中的JNI_CreateVM函數中,這個函數定義在dalvik/vm/jni.cpp中。來看它!
圖21所示為此函數的主要代碼:
圖21中,首先撲面而來的就是Dalvik VM中的幾個重量級數據結構:
圖22所示為JavaVMExt和JNIEnvExt的內容:
圖22中可知:
再來看gDvm的內容,它自己其實就是一大倉庫,裡邊有很多成員變量,每個成員變量都有各自的用途。其內部如圖23所示:
圖23中:
這裡要特別說明虛擬機中對類唯一性的確定方法:
1 對我們而言,類的唯一性由包名+類名表示,比如java.lang.Class這個類,就是唯一的。但實際上,根據Java VM規范,類的唯一性由全路徑類名+定義它的ClassLoader兩者唯一確定。
2 對一個類的加載而言,ClassLoader有兩種情況。一種是直接創建目標類,這種loader叫Define Loader(定義加載器)。另外一種情況是一個ClassLoader創建了Class,但它可以自己直接創建,也可以是委托給比如父加載器創建的,這種Loader叫Initiating Loader(初始加載器)。
3 類的唯一性是由全路徑類名+定義加載器唯一決定。
下面來看JNIEnvExt的創建,這是由圖21中的dvmCreateJNIEnv函數完成的。
圖21中的調用方法如下:
JNIEnvExt* pEnv = (JNIEnvExt*) dvmCreateJNIEnv(NULL);
該函數的相關代碼如圖24所示:
圖24中,Dalvik虛擬機裡JNI的所有函數都封裝在gNativeInterface中。這個結構體包含了JNI定義的所有函數。注意,在使用sourceInsight的時候會有一些函數無法被解析。因為這些函數使用了類似圖右下角的CALL_VIRTUAL宏方式定義。
我確認了下,應該所有函數的定義其實都在jni.cpp這一個文件裡。
到此,我們為主線程創建和初始化了gDvm和JNI環境。下面來看dvmStartup。
去掉dvmStartup函數中一些判斷代碼後,該函數整個執行流程可由圖25表示:
圖25中,dvmStartup的執行從左到右。由於本章我只是想討論dalvik是怎麼執行的Java代碼的,所以這裡有一些函數(比如GC相關的,就不擬討論)。
dvmStartup首先是解析參數,這些參數信息可能會傳給gDvm相關的成員變量。解析參數是由setCommandLineDefaults和processOptions來完成的。具體代碼就不看了,最終設置的幾個重要的參數是:
圖26為Nexus 7 Wi-Fi版4.4.4的BOOTCLASSPATH值:
圖26可知,system/framework下幾乎所有的jar包都被放在了BOOT CLASSPATH裡。這意味這zygote進程加載了所有framework的包,這進一步意味著App也加載了所有framework的包.....。
下面來分析幾個和本章目標相關的函數:
圖27所示為dvmThreadStartup的一些關鍵代碼和解釋:
Thread是Dalvik中代表和管理一個線程的重要結構。注意,這裡的Thread不簡單是我們在Java層中的線程。在那裡,我們只需要在線程裡執行要干得活就可以了。而這裡的Thread幾乎模擬了一個CPU(或者說CPU上的一個核)是怎麼執行代碼的。比如Thread中為函數調用要設置和維護一個棧,還要要有一個變量指向當前正在執行的指令(大名鼎鼎的PC)。這一塊我不想浪費時間介紹,有興趣的童鞋們可以此為契機進行深入研究。
dvmInlineNativeStartup主要是將一些常用的函數搞成inline似的。這裡的inline,其實就是將某些Java函數搞成JNI。比如String類的charAt、compareTo函數等。相關代碼如圖28所示:
注意,在上面函數中,gDvm.inlineMethods只不過是分配了一個內存空間,該空間大小和gDvmInlineOpsTable一樣。而gDvm.inlineMethods數組元素並未和gDvmInlineOpsTable掛上鉤。當然,最終是會掛上的,但是不在這裡。此處暫且不表。
下面我們跳到dvmClassStartup,這個函數很重要。圖29是其代碼:
圖29中:
下面來看processClassPath這個函數,它要加載所有的Boot Class,由於它涉及到類的加載,所以它也是本文的重點內容。先來看圖30:
processClassPath主要是處理BOOTCLASSPATH,也就是圖26中的那些位於system/framework/下的jar包。圖31展示了prepareCpe的代碼,該函數處理一個一個的文件:
prepareCpe倒是很簡單:
這裡我們看dvmJarFileOpen函數,如圖32所示:
圖32介紹了dvmJarFileOpen的主要內容,其中:
到此dvmClassStartup就介紹完了。下面來看一個重要函數,dvmFindRequiredClassesAndMembers。
dvmFindRequiredClassesAndMembers初始化一些重要類和函數。其代碼如圖33所示:
dvmFindRequiredClassesAndMembers就是初始化一些類,函數,虛函數等等。我們重點關注它是怎麼初始化的。一共有三個重要函數:
重點是findClassNoInit,代碼如圖34所示:
圖34中,有幾個關鍵點:
注意:我們在編寫代碼的時候,對於類的唯一性往往只知道全路徑類名,很少關注ClassLoader的重要性。實際上,我之前曾經碰到過一個問題:通過兩個不同ClassLoader加載的相同的Class居然不相等。當時很不明白為什麼要這麼設計, 直到我碰到一個真實事情:有一天我在等車,聽見一個路人大聲叫著“李志剛,李志剛”。我回頭一看,以為他是在找人,結果發現他的寵物狗跑了出來。原來他的 寵物狗就叫李志剛。這就說明,兩個具有相同名字的東西,實際上很能是完全不同的事物。所以,簡單得以兩個類是否同名來判斷唯一性肯定是不行得了。
下面來看最重要的loadClassFromDex,這個函數其實就是把odex文件中的信息轉換成ClassObject。我們來看它:loadClassFromDex代碼如圖34所示:
其中主要的加載函數是loadClassFromDex0,其代碼如圖35所示:
以上是loadClassFromDex0的第一部分內容,這這一塊比較簡單,也就是設置一些東西。下面看圖36
圖36中:
其實loadClassFromDex0後面的工作也類似,比如解析成員函數信息,成員變量信息等。我們直接看相關函數吧:
圖37展示了解析成員變量和解析函數用的兩個函數。
注意native函數的處理,此處是先用dvmResolveNativeMethod頂著。我們以後分析JNI的時候再來討論它。
上面的findClassNoInit是用於搜索Class的,下面我們來看dvmFindDirectMethodByDescriptor函數,它是用來搜索方法的,代碼如圖38所示:
對compareMethodHelper好奇的讀者,我在圖40裡展示了如何從dex文件中獲取一個函數的返回值信息。
好像感覺我們一直和字符串在玩耍。
說實話,講到現在,其實虛擬機啟動的流程差不多就完了。當然,本節所說的這個流程是很粗犷的,主要內容還是集中在Class的加載上,然後浮光掠影看了下一些重要的數據結構。Anyway,上述流程,我建議讀者結合代碼反復走幾個來回。下面我們將開始介紹一些細節性的內容:
JVM中,一個Class首先被使用的時候會調用它的
先來看一段示例代碼,如圖41所示:
示例代碼中:
問題來了:TestAnother的
要確認這一點,只需要看dexdump的結果,如圖42所示:
圖42中:
當然,根據圖41的日志輸出,我們知道
我們在3.1節portable的純解釋執行一節中提到過new-instance,下面我們將以portable為主要講解對象來介紹。
其實,不管是portable還是arm、x86方式,最終都會變成機器指令來執行。相對arm、x86的匯編代碼,portable是以C語言實現的Java字節碼解釋器,非常方便我們理解。
圖43為new-instance指令對應的代碼:
第六節會介紹portable模式下Java函數是如何執行的,所以這裡大家先不用管HANDLE_OPCODE這樣的宏是干什麼用的。圖43中:
我們重點介紹dvmResolveClass和dvmInitClass。
圖44是dvmResolveClass的代碼:
圖44中:
圖45是findClassFromLoaderNoInit的代碼,出奇的簡單:
代碼真是簡潔啊,居然調用java/lang/ClassLoader的loadClass函數來加載類。當然,dalvik中調用Java函數是通過dvmCallMethod來實現的。這個函數我們下一節再介紹。然後,我們把loader存儲到目標clazz的初始加載loader鏈表中。初始加載鏈表在決定類唯一性的時候很有幫助(不記得初始加載器和定義加載器的同學們,請回顧圖23後的說明和圖33)。
Anyway,到此,目標類就算加載成功了。類加載成功到底意味這什麼?前面講過loadClassFromDex等函數,類加載成功意味著dalvik虛擬機從dex字節碼文件中成功得到了一個代表該類的ClassObject對象,裡邊該填的信息在這裡都填好了!
加載成功,下一步工作是初始化,來看下一節:
圖46為dvmInitClass的代碼:
終於,在dvmInitClass中,我們看到了
再次強調,本章是整個虛擬機旅程中一次浮光掠影般的介紹,先讓大家,包括我自己看看虛擬機是個什麼樣子,有一個粗略的認識即可。後續有打算搞一個完整的,嚴謹的,基於ART的虛擬機分析系列。
JVM規范定義了JVM應該怎麼執行一個函數,東西較碎,但和其他語言一樣,無非是如下幾個要點:
函數執行肯定是在一個線程裡來做的,棧幀則理所當然就會和某個線程相關聯。我們先來看dalvik是怎麼創建線程及對應棧的。
Dalvik中,allocThread用於創建代表一個線程的線程對象,其代碼如圖47所示:
圖47是dalvik虛擬機為一個線程創建代表對象的處理代碼,其中,它為每個線程都創建了一個線程棧。線程棧大小默認為16KB,並設置了相關的棧頂和棧底指針,如圖中右下角所示:
每個線程都分配16KB,會不會耗費內存呢?不會,這是因為mmap只是在內核裡建立了一個內存映射項,這個項覆蓋16KB內存。注意,它只是告訴kernel,這塊區域最大能覆蓋16KB內存。如果一直沒有使用這塊內存的話,那麼內存並不會真正分配。所以,只有我們真正操作了這塊內存,系統才會為它分配內存。
dalvik中,如果需要調用某個函數,則會調用dvmCallMethod(嗯嗯?不對吧,Java字節碼裡的invoke-direct指令難道也是調用這個麼?別急,待會再說invoke-direct的實現。)
dvmCallMethod第一步主要是調用callPrep准備棧幀,這是函數調用的關鍵一步,馬上來看:
當調用一個Java函數時,JVM需要為它搞一個新的棧幀,圖49展示了dvmPushInterpFrame的代碼
圖49中:
1 注意:registersSize包括函數輸入參數和函數內部本地變量的個數
2 dvmPushJNIFrame,這個函數是當Java要調用JNI函數時的壓棧處理,該函數和dvmPushInterpFrame幾乎一樣,只是在計算所需棧空間時,沒有加上outsSize*4,因為native函數所需棧是由Native自己控制的。此函數代碼很簡單,請童鞋們自己學習
好了,棧已經准備好了,我們看看函數到底怎麼執行。
圖48中dvmCallMethodV調用callPrep之後,有一段代碼我們還沒來得及展示,如圖50所示:
參數入棧,您看明白了嗎?
接著看dvmCallMethodV調用函數部分,如圖51所示
對於java函數,其處理邏輯由dvmInterpret完成,對於Native函數,則由對應的nativeFunc完成。JNI我們放到後面講,先來處理dvmInterpret。如圖52所示:
圖52中:
下面我們來看dvmInterpretPortable的處理:
dvmInterpretPortable位於dalvik/vm/mterp/out/InterpC-portable.cpp裡,這個InterpC-portable.cpp是用工具生成的,將分散在其他地方的函數合並到最終這一個文件裡。我們先來看該函數的第一段內容,如圖53所示:
第一部分中,我們發現dvmInterpretPortable通過DEFINE_GOTO_TABLE定義了一個handlerTable[kNumPackedOpcodes]數組,這個數組裡的元素通過H宏定義。H宏使用了&&操作符來獲取某個goto label的位置。比如圖中的H(OP_RETURN_VOID),展開這個宏後得到&&op_OP_RETURN_VOID,這表示op_OP_RETURN_VOID的位置。
那麼,這個op_OP_RETURN_VOID標簽是誰定義的呢?恩,圖中的HANDLE_OPCODE宏定義的,展開後得到op_OP_RETURN_VOID:。
最後:
來看portable模式下Java字節碼的處理,這也是最精妙的一部分,如圖54所示:
請先認真看圖54的內容,然後再看下面的總結,portable模式下:
好了,portable模式下dalvik如何運行java指令就是這樣的,就是這麼任性,就是這麼簡單。下面,我們來看Invoke-direct指令又是如何被解析然後執行的。
剛才你看到了portable模式下指令的執行,就是解析指令的操作碼然後跳轉到對應的label。假設我們現在碰到了invoke-direct指令,這是用來調用函數的。我們看看dvmInterpretPortable怎麼處理它。一個圖就可以了,如圖55所示:
就是跳來跳去麻煩點,其實和dvmCallMethod一樣一樣。
一切盡在圖56。
函數返回後,還需要pop棧幀,代碼在stack.cpp的dvmPopFrame中。此處略過不討論了。
這一節你真得要好好思考,函數調用,不論是Java、C/C++,python等等,都有這類似的處理:
這好像是程序設計的基礎知識,這回你真正明白了嗎?
關於JNI,我打算介紹下面幾個內容:
native庫中,如果某個線程需要調用java函數,它會先創建一個JNIEnv環境,然後callXXMethod來調用Java層函數。這部分內容請大家自行研究吧....
把這幾個步驟講清楚的話,JNI內容就差不多了。
APP中,如果要使用JNI的話,native函數必須封裝在動態庫裡,Windows平台叫DLL,Linux平台叫so。然後,我們要在APP中通過System.loadLibrary方法把這個so加載進來。所以,入口是System的loadLibrary函數。相關代碼如圖57所示:
圖57是System.loadLibrary的相關代碼。這裡主要介紹了so加載路徑的問題:
這裡再明確解釋下,loadLibrary只是指定了so文件的名字,而沒有指定絕對路徑。所以虛擬機得知道去哪個目錄搜索這個文件。傳統做法是搜索LD_LIBRARY_PATH環境變量所表明的文件夾(AOSP默認是/vendor/lib和/system/lib)這兩個目錄。但是我剛才講,如果使用傳統方法,APP A有so要加載的話,得把自己的路徑加到LD_LIBRARY_PATH裡去。比如LD_LIBRARY_PATH=/vendor/lib:/system/lib:/data/data/pkg-of-app-A/libs,這種方法將導致任何APP都可以加載A的so。
真正的加載由doLoad函數完成。這個函數相關的代碼如圖58所示:
沒什麼太多可說的,無非就是dlopen對應的so,然後調用JNI_OnLoad(如果該so定義了這個函數的話)。另外,dalvik虛擬機會保存自己加載的so項。
注意,圖58裡左邊有兩個笑臉,當然是很“陰險”的笑臉。什麼意思呢?請童鞋們看看nativeLoad和它對應的Dalvik_java_lang_Runtime_nativeLoad函數。你會發現Runtime_nativeLoad的函數參數聲明好奇怪,完全不符合JNI規范。並且,Runtime_nativeLoad的函數返回是void,但是Java中的nativeLoad卻是有返回值的。怎麼回事???此處不表,下文接著說。
我們在JNI裡,往往會自行注冊java中native函數和native層對應函數的關系。這樣,Java層調用native函數時候就會轉到native層對應函數來執行。注冊,是通過JNIEnv的RegisterNatives函數來完成的。我們來看看它的實現。如圖59所示:
RegisterNatives裡有幾個比較重要的點:
被動注冊,也就是JNI裡不調用RegisterNatives函數,而是讓虛擬機根據一定規則來查找native函數的實現。一般的JNI教科書都是介紹被動注冊,不過我從《深入理解Android卷1》開始就建議直接上主動注冊方法。
dalvik中,當最開始加載類並解析其中的函數時,如果標記為native函數,則會把Method->nativeFunc設置為dvmResolveNativeMethod(請回頭看圖37)。我們來看這個函數的內容,如圖60所示:
被動注冊的方式是在該native函數第一次調用的時候被處理。童鞋們主要注意native函數的匹配規則。Anyway,不建議使用被動注冊的方法,因為native層設置的函數名太長,搞起來很不方便。
6.2節專門講過如何調用java函數,故事還得從dvmCallMethodV說起,如圖61所示:
整個流程如下:
圖62是X86平台上關於dvmPlatformInvoke注釋:
也就是解析參數嘛,不多說了。和前面講的Java准備棧幀類似,無非是用匯編寫得罷了。
fastJni,唉,可惜代碼裡有這個,但是好像沒地方用。干啥的呢?還記得我們前面圖58裡的兩個笑臉嗎?
實話告訴大家,fastJni如果真正實現的話,可以加快JNI層函數的調用。為什麼?我先給你看個東西,如圖63所示:
圖63需要好好解釋下:
這種做法會造成什麼後果呢?
注意喔,這兩個函數的參數一個是四個參數,一個是兩個參數。不過注釋中說了,給一個只有兩個參數的函數傳4個參數沒有問題.....
等等,這麼做的好處是什麼?
當然,fastJni模式是有要求的,比如是靜態,而且非synchronized函數。Anyway,目前這麼高級的功能還是只有虛擬機自己用,沒放開給應用層。
本篇是我第一次細致觀察Android上Java虛擬機的實現,起因是想知道xposed的原理。我們下一篇會分析xposed的原理,其實蠻簡單。因為xposed只涉及到了函數調用,hook之類的東西,沒有虛擬機裡什麼內存管理,線程管理之類的。所以,我們這兩篇文章都不會涉及內存管理,線程管理之類的高級玩意兒。
簡單點說,本章介紹得和dalvik相關的內容還是比較好理解。希望各位先看看,有個感性認識,為將來我們搞更深入的研究而打點基礎。
了解Activity 依照郭霖老師的《第一行代碼Android》,今天我要來學習Activity,首先來初步了解Activity,基本上就是照葫蘆畫瓢的模式,有點回到當初
Android--音樂播放器 1、什麼是Open Core? Open Core是 Android 多媒體框架的核心,所有 Android平台的音頻、視頻的采用以及播
安卓動態調試七種武器之孔雀翎 – Ida Pro,安卓ida安卓動態調試七種武器之孔雀翎 – Ida Pro 作者:蒸米@阿裡聚安全 0x00
Android中MVP模式與MVC模式比較(含示例) MVP 介紹 MVP模式(Model-View-Presenter)是MVC模式的一個衍生。主要目的是為了解耦,使項