Android筆記 - Dalvik的漫談

时间:2022-01-10 16:56:00

Android筆記 - Dalvik的漫談

 

hlchou@mail2000.com.tw
by loda

 Loda's Blog

App BizOrz

Android/Linux Source Code Tags
App BizOrz 
BizOrz.COM 
BizOrz Blog



        由於Dalvik所涉及的範圍不少,JNI介面,Trace-JIT的實作,到最佳化的技巧,筆者在本文只會針對自己挑選的區塊以Android 2.2 Source Code加以說明.同樣的,所有涉及的內容,都會隨著Android程式碼的改版而有所差異,還請以最新取得的Package為主.

 

        在此以引用侯捷曾說過的"源碼之前了無秘密",對有志於深入探究Dalvik運作原理的人而言,Android所釋出的Source Code,就是最好的Handbook.

 

        參考Google的文件,我們知道DalvikGoogleAndroid手機上所提供的ByteCode虛擬器,所預定的目標是要能運作在效能需求不高,較少的記憶體,與使用慢速的內部Flash儲存,作業系統要能支援虛擬記憶體,執行行程與執行緒(Process/Thread),與具備使用者帳號安全管理機制(UID-based security mechanisms),並能透過GNU C編譯器編譯後運作在包括Linux,BSDMac OS XUnix環境下,並支援Little-endianBig-endian處理器執行環境.核心的函式庫主要是承襲Open SourceJava SE實作Apache Harmony而來(網址:http://harmony.apache.org/),並基於Open Source中相關OpenSSL, zlibICUInternational Components for Unicode)的計畫成果.

 

        一般虛擬器的設計,會在應用程式啟動時,動態的從儲存裝置讀取並解壓縮個別的Classes到記憶體中,在沒有經過適度優化的架構下,每個獨立的虛擬器行程,都會包含自己一份的相關Classes記憶體空間,對於程式載入的固定成本與執行效率上,不容易有較好的表現.

 

        因此,參考Google文件,Dalvik設計之初,基於上述的特徵與限制,主要著重在以下的目標,

        1,Class Data尤其是共用的ByteCode,要能跨行程共用,節省系統整體記憶體需求(參考ProcessMemory Map (in /proc/xxxx/maps),目前包括相關的 jar與所包含的Bytecode Dex檔案,都能跨行程在不同的Dalvik行程*用).

        2,減少Dalvik應用程式載入啟動的成本,加速應用程式的反應時間

        3,Class Data儲存在個別的檔案中,導致額外的儲存成本,針對儲存空間的需求需要多加注意.

        4,要從ClassData中讀取出資料數值( 例如:整數或是字串)會增加不必要的成本,評估如何採用C 的形式存取,會是有必要的.

    5,ByteCode的驗證雖耗時但卻是必要的,應該試著在程式執行前完成驗證.

    6,透過快速指令集與機制的優化進行ByteCode的最佳化對於執行效率與電池壽命是相當重要的.

    7,為了安全需求,執行中的行程不能修改共享的程式碼.

 

        基於上述目標,Dalvik在設計時,作了以下的決定

 

        1,可以把多個Classes檔案整合到一個DEX檔案中

        2,DEX檔案會以唯讀方案載入到記憶體中,並跨行程共享

        3,因應支援的系統架構,調整Byte OrderingWord Alignment.

        4,ByteCode驗證對所有的Classes都是必要的,且會儘可能進行事先的驗證加速系統效率.

        5,對於需修改ByteCode的最佳化動作,會在執行前完成.

 

        dalvik上通常會以.apk的方式來提供應用程式的封裝,或以.jar的方式來提供Framework函式庫,這些包裝格式主要是以zip的形式壓縮內容並加上相關的參考資訊,儲存在檔案系統中,並達到節省儲存空間的目的.也因此,在執行前必須要進行解壓縮的動作,DEX檔案載入到記憶體中執行. DEX檔案可包括多個Classes檔案,並以classes.dex檔名結尾.

 

DEX的驗證與最佳化

 

        系統可以在VM進行JIT(Just in time),第一次安裝或在系統編譯時,進行最佳化DEX檔案的動作(ODEX - Optimized DEX),如果是在裝置上進行的最佳化動作,會把最佳化後的檔案存放到dalvik-cache目錄下(例如:/data/dalvik-cache/system@framework@ext.jar@classes.dex),若是在編譯階段的最佳化動作,則是把最佳化後的DEX檔案存放在jar/apk格式的ZIP壓縮檔案中,可成為產品出貨時預設System Image的一部分.

 

        執行時期的dalvik-cache目錄權限為0771並屬於system使用者與system群組(例如:drwxrwx--xsystem  system),最佳化後的DEX檔案權限為0644並屬於system使用者與使用者群組(例如:-rw-r--r-- system  app_29),而有被DRM-locked保護的應用程式最佳化後的DEX檔案權限會設定為0640以避免其他應用程式取得檔案資料.

       

        AndroidSDK中的System.img所包含的init.rc,在啟動過程中會進行如下的配置

 

   # create dalvik-cache and double-check the perms

   mkdir /data/dalvik-cache 0771 system system

   chown system system /data/dalvik-cache

   chmod 0771 /data/dalvik-cache

 

        最佳化DEX的動作會產生一個可供快速載入執行的classes.dex檔案,並會進行包括byte-swapping,structure realigningbasicstructure checks,更新ODEXOptimized DEX)header ,為了確保產生ODEX流程的正確性,Android提供了一個dexopt工具(原始碼在dalvik/dexopt),用來作為一個Dalvik虛擬器的輔助工具,可以在系統啟動時,透過Dalvik虛擬器對載入的DEX檔案執行最佳化的動作.

 

        工具dexoptAndroid系統中有兩種使用的時機,

        1,Dalvik虛擬器來執行,在有多個Dalvik虛擬器執行的環境中,會透過dexopt locks確保同一個DEX檔案只會執行到一次.

        2,在安裝應用程式時,會把檔案從ZIP壓縮中解開,再透過dexopt進行驗證與最佳化

 


       
針對DEX檔案所進行的驗證與最佳化程序內容,如下簡介

        1,驗證:

        會包括DEX檔案中所包括的每個Classes集合以及每個指令集,確保不會在Run-Time階段遇到不適當的指令集,一般而言,Dalvik只有針對被驗證過的Classes進行依據平台的最佳化流程(這條件是可修改的,甚至也可忽略驗證階段只進行最佳化),如果呼叫了一個被驗證失敗的Class所提供的呼叫,就會導致Dalvik虛擬器發生Exception例外, DEX中的Class一旦被驗證過,就會被記錄在ODEX格式的欄位中(包含32-bit check-sum),如此可以避免下次載入時,還要重複進行驗證的工作.

 

        2,最佳化:

        會包括把數值資料的定義,透過指標對應到內部的資料結構,把一定成功或是特定行為的指令流程置換為比較簡單的形式,除了必須要在Run-Time才能取得的資訊外,會把可以先決定的資訊先靜態處理完畢.

        A,vtable Index置換虛擬函式(virtual method calls)Index

        B,變數的存取,會以實際資料的Byte Offset置換,並且會把資料型別為boolean / byte / char / short 等變數,轉為32bits的形式儲存

       C,置換大量調用的函式,例如String.length()透過Inline的置換,直接由Intepreter呼叫到原生函式的實作,減輕函式呼叫的成本,

        D,移除空函式的實作,例如Class物件的init空函式,在每次物件被配置時,都還是會被呼叫,就會以nop指令集(0x00)取代.   

        E,將可以預先計算的資料,進行預處理.

        F,會以Dalvik規格中沒有制定的OpCode指令集來進行最佳化,這部份會交由dexopt根據Dalvik虛擬器的版本去決定哪些部分要置換.

        G.最佳化的流程,也可以看做是跟ELF執行檔與動態函式庫間解決Symbol Resolve的問題,

       

 

        上述的最佳化動作,也會使用到來自其他DEX檔案中的Class,如果所參考到的Class資料的內容或是函式有所變動,就會有相依性的問題要被處理,並且要針對變動的部份重新進行最佳化的動作.

 

       

DEX 相依性.

 

        由於,最佳化時,會把系統相依的問題進行解決,以加速應用程式載入的效率,也因此,這些在最佳化時所仰賴的相關DEX檔案如果有變動的話,就有機會導致原本最佳化的結果可能造成因為版本差異的所導致的系統問題,也因此,最佳化過的ODEXOptimized DEX)檔案中會包括所相依的DEX檔案清單以及其CRC-32,時間資訊,對應到dalvik-cache的完整路徑,SHA-1簽名,Dalvik虛擬器版本編號.基於此,如果啟動目錄下的DEX檔案變動,也就象徵著會導致系統上一次相依到該DEX檔案的驗證與最佳化流程,需要被重新執行.如果使用者自定的Class命名與啟動路目錄下的Class名稱重複,系統會對該Class標註並且該參考將不會被驗證與最佳化流程解析.如果DEX檔案在安裝時就是以ODEX的形式安裝,且他所相依的DEX檔案被更新過,這將會導致dexopt沒有辦法對該DEX檔案依據目前系統的狀況進行最佳化產生ODEX,此時Dalvik虛擬器將會拒絕該DEX檔案安裝.

 

 

Dalvik雜談

 

        Dalvik虛擬器支援大約230多個指令集OpCode,其中也包含部分由Dexopt插入到ByteCode執行檔中但目前尚未在Android文件”Bytecode for the Dalvik VM” 說明的OpCode指令.

        主要的Dalvik虛擬器實作,都是基於C Code,並具有平台移植性,除了JIT(Byte-Code Compiler to ARM/x86 Code)JNI(JavaNative Interface) Call Bridge有牽涉到跟平台有關的組語優化實作,這會牽涉到包括如何把Byte-Code針對ARMv5,ARMv7,VFPNEON指令集進行動態的產生與優化,以及由Java端呼叫到Native函式時,要如何根據平台的差異去針對例如C語言的函式參數傳遞進行優化(例如:x86的函式參數傳遞,是由右往左推到Stack,ARM的函式參數傳遞,是由左到右依序放到R0,R1,R2,R3通用暫存器),與函式呼叫與傳回值(例如:x86會根據傳回值的Type決定要用EAX或加上EDX暫存器,或是ARM會決定要用到R0或加上R1暫存器).若你所使用的平台,並不在Dalvik JNI Call Bridge支援中,Google文件中也建議可以參考open-source FFI library (A Portable ForeignFunction Interface Library,網址:http://sourceware.org/libffi/),裡面有關於不同平台的可移植函式呼叫實作.對應到Dalvik中這部分的實作位於Source Codedalvik/vm/arch/目錄下,裡面有關於Java透過JNI機制呼叫到Native Code的實作函式void dvmPlatformInvoke(void* pEnv, ClassObject* clazz, int argInfo,int argc, const u4* argv, const char* signature, void* func, JValue* pReturn) (例如:x86目錄下實作是在Call386ABI.SHints386ABI.c,arm目錄下實作是CallEABI.S,CallOldABI.SHintsEABI.c,generic目錄下的實作Call.cHints.c),其中,也有包括關於SH處理器Call Bridge的實作,這是由HitachiRenesas公司實作後,貢獻到Android計畫中.(....MIPS也應該要這樣子做吧...@_@)

 

        函式dvmPlatformInvoke主要目的是會用來把我們在Java端呼叫JNI函式時所帶入的參數,C函式參數傳遞的原則進行調整,與處理C函式的回傳值對應回Java的世界中.

 

        Dalvik支援兩種Java虛擬器的直譯器,一個是最早的版本,在文件中稱為Portabl Interpreter,所在原始碼路徑為dalvik/vm/mterp/portable,這個all-in-one-functionC實作,可以用來編譯到不同的平台上,並支援Profiling與除錯機制,根據Config的設定,所在編譯環境會納入編譯的Java虛擬器的直譯器實作會把Source Code放到dalvik/vm/mterp/out目錄下.

 

        Dalvik也支援根據不同平台透過組語優化後的直譯器版本,在文件中稱為Fast Interpreter,可以藉由優化後的平台組語實作,得到更好的Java虛擬器執行效能,dalvik/vm/mterp/out目錄下可以看到依據平台不同而命名的InterpC-<arch>.c, InterpAsm-<arch>.S原始碼,並可針對不同平台的差異採用不同的優化指令集,例如:ARM11(ARMv6)架構下可以使用PLD指令,與在ARM7(ARMv4T)架構下要避免使用CLZ指令.

 

        組語版本的優化程式碼,會以64bytes Memory Alignment配置來實作每一個對應的Java指令(也就是說每個Java指令最多可以用16ARM 32bits指令集實作),如果在Dalvik虛擬器啟動時,發現有對應實作的Java指令超過定義的大小,就會由虛擬器產生錯誤(Abort),參考InterpAsm-armv5te.S,如下指令集實作

 

    .balign64

.L_OP_MOVE_16: /* 0x03 */

/* File: armv5te/OP_MOVE_16.S */

    /*for: move/16, move-object/16 */

    /*op vAAAA, vBBBB */

    FETCH(r1,2)                       @ r1<- BBBB

    FETCH(r0,1)                       @ r0<- AAAA

    FETCH_ADVANCE_INST(3)              @ advance rPC, load rINST

    GET_VREG(r2,r1)                   @ r2<- fp[BBBB]

    GET_INST_OPCODE(ip)                @ extract opcode from rINST

    SET_VREG(r2,r0)                   @ fp[AAAA]<- r2

    GOTO_OPCODE(ip)                    @ jump to next instruction

    .balign64

.L_OP_MOVE_WIDE: /* 0x04 */

/* File: armv5te/OP_MOVE_WIDE.S */

    /*move-wide vA, vB */

    /*NOTE: regs can overlap, e.g. "move v6,v7" or "move v7,v6"*/

    mov     r2, rINST, lsr #8          @ r2<- A(+)

    mov     r3, rINST, lsr #12         @ r3<- B

    and     r2, r2, #1

    add     r3, rFP, r3, lsl #2        @ r3<- &fp[B]

    add     r2, rFP, r2, lsl #2        @ r2<- &fp[A]

    ldmia   r3, {r0-r1}                @ r0/r1<- fp[B]

    FETCH_ADVANCE_INST(1)              @ advance rPC, load rINST

    GET_INST_OPCODE(ip)                @ extract opcode from rINST

    stmia   r2, {r0-r1}                @ fp[A]<- r0/r1

    GOTO_OPCODE(ip)                    @ jump to next instruction

 

        也可以參考網頁http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html,有整理好的Dalvik Java OpCode, 對比Sun JavaOpCode (可以參考網頁:http://java.sun.com/docs/books/jvms/second_edition/html/Mnemonics.doc.html 或是比較完整的文件http://java.sun.com/docs/books/jvms/second_edition/Java5-Instructions2.pdf),對比之後可以知道雖然都是透過JDK編譯的程式碼,透過Google Android Dex轉換後,就會變成Dalvik專屬的OpCodeDalvik自己的Java虛擬器上執行.

 

        針對使用者所採用平台的組語優化程式碼,可以透過dalvik/vm/mterp/config-<Arch>相關檔案來決定,其中包括要import哪一部分的實作,也規避掉了ARM在不同架構上支援指令集的差異.dalvik/vm/mterp/config-armv4t為例,擷取其中關於opcode的部分來說明如下

 

# opcode list; argument to op-start isdefault directory

op-start armv5te

    opOP_AGET_WIDE armv4t

    opOP_APUT_WIDE armv4t

    opOP_IGET_WIDE armv4t

    opOP_IGET_WIDE_QUICK armv4t

    opOP_IPUT_WIDE armv4t

    opOP_IPUT_WIDE_QUICK armv4t

    opOP_SGET_WIDE armv4t

    opOP_SPUT_WIDE armv4t

op-end

 

        上述宣告,最後產生的dalvik/vm/mterp/out/InterpAsm-armv4t.S除上述AGET_WIDE,APUT_WIDE,IGET_WIDE,IGET_WIDE_QUICK,IPUT_WIDE,IPUT_WIDE_QUICK,SGET_WIDESPUT_WIDE指令集會用armv4t的實作外,其他指令集會用armv5te的指令集架構實現.如果有修改上述指令集的實作,會需要執行dalvik/vm/mterp/rebuild.sh,重新產生dalvik/vm/mterp/out/下對應到相關處理器架構的Java指令集實作.並重新編譯dalvik目錄,以便產生對應這次修改的libdvm.so .

 

        Fast Interpreter會是預設的Dalvik虛擬器中的直譯器,如果你所在的處理器沒有Dalvik的組語實作支援,可以由使用者在啟動時選擇要用C Code版本的PortableInterpreter,只要在啟動時(例如在init.rc)執行echo dalvik.vm.execution-mode = int:portable >>/data/local.prop “,就可以選擇採用PortableInterpreter.

 

      一般Java虛擬器直譯器的實作,最直覺的寫法就是用一個很大的Switch-Case,依序對應每個進來的指令集OpCode,做出對應OpCode的行為,在每個指令集OpCode執行完畢後,就回到迴圈的頭,重新Fetch下一次的指令,繼續Siwtch-Case的行為. Google文件中提到的另一種改善的技巧就是透過”threadedexecution”,在每個指令集OpCode執行結束後,進行下一個指令集的fetchdispatch,省去要回到迴圈啟始點(Branch)的固定成本.

 

      Dalvik直譯器的實作,採用預先算好的Goto位址,基於每個直譯器處理的指令集OpCode實作,都固定以64bytesMemory Alignment,取代得到一個指令集OpCode,要進行查表的成本,只要取得OpCode後乘上64bytes(等於26次方,可以透過Shift的方式運算),就可以跳到對應指令集OpCode的實作.Android之所以選擇64bytes為每個指令集OpCode實作的最大空間,並沒有特別的理由,主要是這樣的值可以完全適用於目前Android對於ARMx86上實作的結果.

 

      參考Source Code dalvik/libdex/OpCode.h中的DEFINE_GOTO_TABLE的宣告如下,

#defineDEFINE_GOTO_TABLE(_name) /

    static const void*_name[kNumDalvikInstructions] = {                   /

        /* 00..0f */                                                       /

        H(OP_NOP),                                                         /

        H(OP_MOVE),                                                        /

        H(OP_MOVE_FROM16),                                                 /

        H(OP_MOVE_16),                                                     /

        H(OP_MOVE_WIDE),                                                   /

        H(OP_MOVE_WIDE_FROM16),                                            /

        H(OP_MOVE_WIDE_16),                                                /

        H(OP_MOVE_OBJECT),                                                 /

        H(OP_MOVE_OBJECT_FROM16),                                          /

.............................

…....................................etc

 

      再參考Source Code dalvik/vm/mterp/cstubs/entry.c,有如下宣告

 

/*

 * Handler function table, one entry peropcode.

 */

#undefH

#defineH(_op) dvmMterp_##_op

DEFINE_GOTO_TABLE(gDvmMterpHandlers)

                                                                               

#undefH

#defineH(_op) #_op

DEFINE_GOTO_TABLE(gDvmMterpHandlerNames)

 

      定義gDvmMterpHandlers對應到每個OpCode的處理實作,gDvmMterpHandlerNames對應到每個OpCode的名稱.

 

      再來追蹤函式bool dvmMterpStdRun(MterpGlue* glue) ,可以看到如下的Busy Loop

while(true) {

        typedef void (*Handler)(MterpGlue*glue);

                                                                               

        u2 inst = /*glue->*/pc[0];

        Handler handler = (Handler)gDvmMterpHandlers[inst & 0xff];

        LOGVV("handler %p %s/n",

            handler, (const char*)gDvmMterpHandlerNames[inst & 0xff]);

        (*handler)(glue);

    }

      會每次取一個Java指令集OpCode,並透過gDvmMterpHandlers對應0x00-0xff範圍的Java指令集OpCode的對應處理函式.dalvik/vm/mterp/out/InterpAsm-<Arch>.S,配合Fast Interpreter也有對應依據平台差異的函式dvmMterpStdRun組語版本實作,C語言版本實作行為類似,但最大的差異在於,不是透過一個while(true)Busy Loop去逐一抓取給Dalvik虛擬器的指令集,而是先在函式dvmMterpStdRun中執行如下巨集(InterpAsm-armv5te.S實作為例)

 

    FETCH_INST()                       @ load rINST from rPC

    GET_INST_OPCODE(ip)        @ extract opcode from rINST

    GOTO_OPCODE(ip)              @ jump to next instruction

 

      抓取第一個Dalvik OpCode指令並執行,同時在每個指令執行結束後,再透過FETCH_ADVANCE_INSTGET_INST_OPCODE抓取下一個Dalvik OpCode指令,最後再透過GOTO_OPCODE執行該指令集的實作,如此持續運作下去,藉此得到比用C版本Busy Loop更高的執行效率.

 

        如果因為進行記憶體的Garbage Collection,進行除錯或是轉換到Native Code執行,而讓執行中的Dalvik行程處於Suspend狀態,Dalvik直譯器會提供一個安全機制,確保行程可以被正確的回復執行.如下所示,目前Dalvik主要支援Portable/Fast Interpreter,Trace-JIT主要是屬於Fast-Interpreter實作中的一部分.

 

 

Trace-JIT/Compiler

(For Hot Fragments)

Fast Interpreter

(For Cold Fargments)

 

Portable Interpreter
(C Code)

Fast Interpreter (ASM)

Dalvik VM

 

 

        Dalvik上每一個應用程式不管是SDK或是NDK都會基於一個Java Based的應用程式為主體(NDKJava+ELF .so),也因此要了解Android的應用程式Framework,最適當的途徑就是把Dalvik的運作原理與基礎做一個分析,相對這會對於在Android架構下,不管是開發應用程式或是進行系統的效能優化都會有相當的助益.

 

 

Dalvik的控制與範例

 

        如果是透過PC上的系統參數設定操作,可以透過adb設定如下的指令

        #設定系統參數

        adbshell setprop <name> <value>

        #取得系統參數

        adbshell getprop <name>

        相關的Dalvik系統參數設定,是在Zygote行程載入時進行初始化的,一旦相關的系統參數被更動到,就必須要重啟Dalvik Run-Time環境,透過Zygote載入流程讓相關系統參數發揮作用.

        在開發階段,有測試系統參數的需求時,就可以藉由AndroidShell,透過Stop/Start指令,終止與重啟Dalvik Run-Time環境,讓系統參數發揮作用.(Stop會依序設定系統參數ctl.stop=runtimectl.stop=zygote,Start會依序設定系統參數ctl.start=zygotectl.start=runtime)

       

 

 

系統參數

說明

dalvik.vm.stack-trace-file

/data/anr/traces.txt

Stack Dumps
範例: setprop dalvik.vm.stack-trace-file /tmp/stack-traces.txt

 

Dalvik虛擬器在收到Linux Signal SIGQUIT (也可透過kill -3觸發)就會把目標行程所有執行緒的Stack Traces內容(可以知道每個執行緒函式呼叫的Call Stack),根據這個參數指定的路徑把資料寫入,供開發者分析問題時參考之用.

dalvik.vm.dexopt-flags

m=y

Bytecode Verification and Optimization

範例:setprop dalvik.vm.dexopt-flags v=a,o=v

用來決定dexopt所進行執行前的驗證與最佳化動作的行為,在實際的裝置上,dexopt會在Dalvik啟動一個應用或是應用程式安裝時調用(會透過dexlock確保同時只有一個被執行到),

進一步說明參數如下

v=a,o=v =>驗證所有的Dex檔案,並且只最佳化被驗證過的Dex檔案,如果驗證失敗,Dex檔案就不會被執行

v=n,o=v =>關閉驗證Dex的動作,只針對有被驗證過的Dex進行最佳化,沒被驗證過的Dex,就會直接執行(也不會被最佳化)

 

根據Android的文件,第一次執行的Dex檔案,進行驗證的動作,可能會讓執行時間慢了40%左右,但只要該Dex檔案有驗證過,並且對應放在dalvik-cache,只要該檔案沒有變動且所相依的其他Dex檔案也沒有更動過,之後執行就可省去驗證的流程,並可透過最佳化機制,加速應用程式的啟動執行.

Enabling type-precise GC results in larger optimized DEX files. The additional storage requirements for ".odex" files can cause /system to overflow on some devices, so this is configured separately for each product.

dalvik.vm.lockprof.threshold

500

Enable Dalvik lock contention logging for userdebug builds.

dalvik.vm.checkjni

true/false

Extended JNI Checks

範例:setprop dalvik.vm.checkjni true

如果系統沒有設置 dalvik.vm.checkjni,就會以ro.kernel.android.checkjni為主,如果有設置dalvik.vm.checkjni(不論是truefalse),則以dalvik.vm.checkjni為依據來決定CheckJNI的開或關

另一個對應的參數為 ro.kernel.android.checkjni,可在編譯時期透過build/core/main.mk決定.

 

這參數的設置,會讓JNI函式呼叫前,執行相關的稽核動作,其中包括

1,查核NULL Pointer

2,查核函式參數的正確性(jclass is a class object, jfieldID points to field data, jstring is a java.lang.String)

3,確認資料寫入動作與變數宣告一致. (例如: don't store a HashMap in a String field.)

4,確認是否有不允許的例外處理有因為函式呼叫正在等待例外的情況.

5,確認關鍵的Get/Release呼叫沒有不適當的函式存在

6,確認JNIEnv不會被跨執行緒(Thread)分享

7,確認Local變數的參考不會超過該變數所能支援的變數壽命週期.

8,UTF-8字串只會包含有效修改的UTF-8資料.

dalvik.vm.jniopts

forcecopy

Extended JNI Checks

範例:setprop dalvik.vm.jniopts forcecopy

 

 

dalvik.vm.enableassertions

You can provide a class name, a package name (followed by "..."), or the special value "all"

Assertions

範例: setprop dalvik.vm.enableassertions all

 

Dalvik虛擬器預設對於Assertion是關閉的,可以透過設定這個參數來開關DalvikJava程式碼Assertion的動作,

dalvik.vm.execution-mode

int:portable/int:fast/int:jit

Execution Mode

範例:

setprop dalvik.vm.execution-mode int:portable (C版本的Intepreter)

setprop dalvik.vm.execution-mode int:fast (組語版本的Intepreter)

setprop dalvik.vm.execution-mode int:jit (Just-in Time加速)

 

如果有啟動Profiling或是接上Java除錯器,就會切到Debug Mode(我理解Debug Mode是在 C版本的Portable Intepreter中支援的),當除錯階段結束,或是Profiling結束,就會重新回復原本執行的Java模式.

 

如果在AndroidManifest.xml中把android:vmSafeMode設定為true,就會預設關閉JIT選項,開發者可借此釐清應用程式所造成的問題,是否為JIT所導致的.

dalvik.vm.deadlock-predict err

off/warn/err

Deadlock Prediction

範例:setprop dalvik.vm.deadlock-predict err

 

編譯Dalvik時必須要加上WITH_DEADLOCK_PREDICTION便能支援這個功能,參考Android文件,這機制主要用來進行DalvikDeadlock的預測,而非偵測

 

如果設定為off就是關閉,warn是記錄這個問題,但程式繼續執行,如果是err就是在Dalvik OpCpde OP_MONITOR_ENTER(0x1d)指令執行完畢後,立刻觸發Exception,終止Dalvik虛擬器的執行.

dalvik.vm.check-dex-sum

 

DEX File Checksums
範例:setprop dalvik.vm.check-dex-sum true

目前Dalvik虛擬器預設對載入ODEX(Optimized DEX)的流程,是不做Checksum確保的,參考dalvik-cache,跟系統與Framework有關的ODEX,OwnerGroup會是rootsystem,只有一般應用執行的ODEX,會把Group設定為對應的AP Group,基於此,Android預設透過系統存取權限的保護,就可以防止ODEX檔案被修改的可能.

如果所在的儲存媒體不可靠,容易有資料損毀的問題發生,就會有必要加入Checksum的機制(其實以目前很多SmartPhoneMLC NAND+FTL Controller的組合,只要Wear-Leveling有作用,要遇到儲存裝置的不可靠”,對一般使用者而言,應該是很不容易遇到).開發階段,也可以透過dexdump工具,手動的確認DEX檔案的Checksum.

dalvik.vm.jit.profile

true/false

JIT Profile
範例:setprop dalvik.vm.jit.profile true

可以用來查看Dalvik JIT對於熱門執行區塊的統計資訊.

 

 

 

有關Dalvik行程與執行時期介紹

 

        如下所示,Android系統應用程式啟動的父子行程關係,我們可以看到init會把包括Service Manager,Netd,Rild,MediaServer,BootAnimation這些原生行程載入,Dalvik的第一個初始化程式會透過app_process載入後,並命名為Zygote(...Google翻譯查詢是"受精卵"),再由Zygote帶起system_server與後續的Dalvik應用程式.

 

 

Kernel(PID=0)

 

/init(PID=1)

/system/bin/sh(PID=27)

/system/bin/servicemanager(PID=28)

/system/bin/vold(PID=29)

/system/bin/netd(PID=30)

/system/bin/debuggerd(PID=31)

/system/bin/rild(PID=32)

/system/bin/mediaserver(PID=34)

/system/bin/dbus-daemon(PID=35)

/system/bin/installd(PID=36)

/system/bin/keystore(PID=37)

/system/bin/sh(PID=38)

/system/bin/qemud(PID=39)

/sbin/adbd(PID=41)

/system/bin/bootanimation(PID=81)

zygote(PID=33)

system_server (pid:74)
com.android.inputmethod.latin (pid:146)
com.android.phone (pid:150)
android.process.acore (pid:189)
com.android.launcher (pid:191)
com.android.quicksearchbox (pid:229)
android.process.media (pid:259)
com.android.bluetooth (pid:273)
com.android.providers.calendar (pid:280)
com.android.email (pid:292)
com.android.mms (pid:305)
com.android.protips (pid:323)
com.android.music (pid:329)
com.cooliris.media (pid:335)

kthreadd(PID=2)

ksoftirqd/0(PID=3)

events/0(PID=4)

khelper(PID=5)

suspend(PID=6)

kblockd/0(PID=7)

cqueue(PID=8)

kseriod(PID=9)

kmmcd(PID=10)

pdflush(PID=11)

pdflush(PID=12)

kswapd0(PID=13)

aio/0(PID=14)

mtdblockd(PID=22)

kstriped(PID=23)

hid_compat(PID=24)

rpciod/0(PID=25)

mmcqd(PID=26)

 

 

 

        以目前筆者環境,列舉一個Dalvik應用程式CalendarProvider的記憶體Mapping資訊.對應到其他Dalvik應用程式,我們可以看到基於Apriori prelink機制,在共用原生碼動態函式庫.so的部份,不同的Dalvik應用程式,也會對應到同樣的記憶體位置.而在Dalvik OpCpde編碼動態函式庫有共用的.jar (會被解開為classes.dex)部分,在跨不同的Dalvik應用程式,會被配置到同樣的記憶體位址.

 

 

 

記憶體位址

屬性

對應的執行檔或是函式庫名稱

00008000-00009000

r-xp

/system/bin/app_process

00009000-0000a000

rwxp

/system/bin/app_process

0000a000-001f9000

rwxp

[heap]

40000000-40008000

r-xs

/dev/ashmem/system_properties

40008000-40009000

r-xp

 

40009000-4024a000

rwxp

/dev/ashmem/mspace/dalvik-heap/zygote/0

4024a000-41009000

---p

/dev/ashmem/mspace/dalvik-heap/zygote/0

41009000-41038000

r-xs

/system/fonts/DroidSans.ttf

41038000-4104c000

rwxp

 

4104c000-4104d000

---p

/dev/ashmem/dalvik-LinearAlloc

4104d000-41254000

rwxp

/dev/ashmem/dalvik-LinearAlloc

41254000-4154c000

---p

/dev/ashmem/dalvik-LinearAlloc

4154c000-4175b000

r-xs

/system/framework/core.jar

4175b000-41c23000

r-xp

/data/dalvik-cache/system@framework@core.jar@classes.dex

41c23000-41c68000

rwxp

 

41c68000-41ca2000

r-xs

/system/framework/ext.jar

41ca2000-41d2e000

r-xp

/data/dalvik-cache/system@framework@ext.jar@classes.dex

41d2e000-41fc5000

r-xs

/system/framework/framework.jar

41fc5000-425f2000

r-xp

/data/dalvik-cache/system@framework@framework.jar@classes.dex

425f2000-42676000

rwxp

 

42676000-4268b000

r-xs

/system/framework/android.policy.jar

4268b000-426b9000

r-xp

/data/dalvik-cache/system@framework@android.policy.jar@classes.dex

426b9000-42752000

r-xs

/system/framework/services.jar

42752000-428a0000

r-xp

/data/dalvik-cache/system@framework@services.jar@classes.dex

428a0000-428a3000

rwxp

 

428a3000-428d9000

rwxp

/dev/ashmem/dalvik-heap-bitmap/objects

428d9000-428df000

rwxp

 

428df000-428e0000

r-xs

/dev/ashmem/SurfaceFlinger read-only heap

428e0000-42907000

rwxp

 

42907000-4290c000

r-xs

/system/app/CalendarProvider.apk

4291a000-4294b000

rwxp

 

4294b000-42982000

rwxp

/dev/ashmem/dalvik-heap-bitmap/mark/0

42982000-429c2000

rwxp

/dev/ashmem/dalvik-heap-bitmap/mark/1

429c2000-42a16000

r-xs

/system/app/CalendarProvider.apk

42a16000-42a29000

r-xs

/system/framework/android.test.runner.jar

42a29000-42a55000

r-xp

/data/dalvik-cache/system@framework@android.test.runner.jar@classes.dex

42a70000-42e23000

r-xs

/system/framework/framework-res.apk

42e23000-4304e000

r-xs

/system/framework/framework-res.apk

4304e000-4308f000

rwxp

/dev/ashmem/mspace/dalvik-heap/zygote/1

4308f000-43e0e000

---p

/dev/ashmem/mspace/dalvik-heap/zygote/1

43e0e000-43e4f000

rwxp

/dev/ashmem/mspace/dalvik-heap/2

43e4f000-44b8e000

---p

/dev/ashmem/mspace/dalvik-heap/2

44b8e000-44b8f000

---p

 

44b8f000-44c8e000

rwxp

 

44c8e000-44c8f000

---p

 

44c8f000-44d8e000

rwxp

 

44d8e000-44e8c000

r-xp

/dev/binder

44e8c000-44e8d000

---p

 

44e8d000-44f8c000

rwxp

 

44f8c000-44f8d000

---p

 

44f8d000-4508c000

rwxp

 

4508c000-450e0000

r-xs

/system/app/CalendarProvider.apk

450e0000-451ab000

r-xp

/data/dalvik-cache/system@app@CalendarProvider.apk@classes.dex

80000000-801d0000

r-xp

/system/lib/libicudata.so

801d0000-801d1000

rwxp

/system/lib/libicudata.so

80200000-80286000

r-xp

/system/lib/libdvm.so

80286000-80289000

rwxp

/system/lib/libdvm.so

80289000-8028a000

rwxp

 

9d100000-9d139000

r-xp

/system/lib/libstlport.so

9d139000-9d13b000

rwxp

/system/lib/libstlport.so

9d700000-9d736000

r-xp

/system/lib/libjpeg.so

9d736000-9d737000

rwxp

/system/lib/libjpeg.so

9ea00000-9ea08000

r-xp

/system/lib/libdrm1.so

9ea08000-9ea09000

rwxp

/system/lib/libdrm1.so

a2f00000-a2fa9000

r-xp

/system/lib/libstagefright.so

a2fa9000-a2fac000

rwxp

/system/lib/libstagefright.so

a3500000-a3503000

r-xp

/system/lib/libstagefright_color_conversion.so

a3503000-a3504000

rwxp

/system/lib/libstagefright_color_conversion.so

a3600000-a3605000

r-xp

/system/lib/libstagefright_avc_common.so

a3605000-a3606000

rwxp

/system/lib/libstagefright_avc_common.so

a3700000-a370c000

r-xp

/system/lib/libstagefright_amrnb_common.so

a370c000-a370d000

rwxp

/system/lib/libstagefright_amrnb_common.so

a3900000-a39c7000

r-xp

/system/lib/libopencore_common.so

a39c7000-a39cd000

rwxp

/system/lib/libopencore_common.so

a4800000-a48b6000

r-xp

/system/lib/libopencore_player.so

a48b6000-a48be000

rwxp

/system/lib/libopencore_player.so

a5900000-a5917000

r-xp

/system/lib/libomx_amrenc_sharedlibrary.so

a5917000-a5918000

rwxp

/system/lib/libomx_amrenc_sharedlibrary.so

a6800000-a682f000

r-xp

/system/lib/libopencore_net_support.so

a682f000-a6832000

rwxp

/system/lib/libopencore_net_support.so

a6d00000-a6d14000

r-xp

/system/lib/libomx_sharedlibrary.so

a6d14000-a6d15000

rwxp

/system/lib/libomx_sharedlibrary.so

a7500000-a7502000

r-xp

/system/lib/libemoji.so

a7502000-a7503000

rwxp

/system/lib/libemoji.so

a7e00000-a7e05000

r-xp

/system/lib/libhardware_legacy.so

a7e05000-a7e06000

rwxp

/system/lib/libhardware_legacy.so

a7f00000-a7f01000

r-xp

/system/lib/libhardware.so

a7f01000-a7f02000

rwxp

/system/lib/libhardware.so

a8100000-a8124000

r-xp

/system/lib/libutils.so

a8124000-a8125000

rwxp

/system/lib/libutils.so

a8200000-a821f000

r-xp

/system/lib/libbinder.so

a821f000-a8225000

rwxp

/system/lib/libbinder.so

a8300000-a86ec000

r-xp

/system/lib/libwebcore.so

a86ec000-a8747000

rwxp

/system/lib/libwebcore.so

a8747000-a8749000

rwxp

 

a8a00000-a8a14000

r-xp

/system/lib/libexpat.so

a8a14000-a8a16000

rwxp

/system/lib/libexpat.so

a8b00000-a8b4d000

r-xp

/system/lib/libsqlite.so

a8b4d000-a8b4f000

rwxp

/system/lib/libsqlite.so

a9000000-a9051000

r-xp

/system/lib/libmedia.so

a9051000-a905d000

rwxp

/system/lib/libmedia.so

a9300000-a930c000

r-xp

/system/lib/libmedia_jni.so

a930c000-a930d000

rwxp

/system/lib/libmedia_jni.so

a9400000-a941c000

r-xp

/system/lib/libvorbisidec.so

a941c000-a941d000

rwxp

/system/lib/libvorbisidec.so

a9500000-a9552000

r-xp

/system/lib/libsonivox.so

a9552000-a9553000

rwxp

/system/lib/libsonivox.so

a9553000-a9554000

rwxp

 

a9c00000-a9c0a000

r-xp

/system/lib/libskiagl.so

a9c0a000-a9c0b000

rwxp

/system/lib/libskiagl.so

ab100000-ab211000

r-xp

/system/lib/libskia.so

ab211000-ab215000

rwxp

/system/lib/libskia.so

ab215000-ab218000

rwxp

 

ab900000-ab912000

r-xp

/system/lib/libui.so

ab912000-ab914000

rwxp

/system/lib/libui.so

aba80000-aba90000

r-xp

/system/lib/libcamera_client.so

aba90000-aba93000

rwxp

/system/lib/libcamera_client.so

abb00000-abb09000

r-xp

/system/lib/libexif.so

abb09000-abb0a000

rwxp

/system/lib/libexif.so

abb0a000-abb0c000

rwxp

 

abd00000-abd02000

r-xp

/system/lib/libETC1.so

abd02000-abd03000

rwxp

/system/lib/libETC1.so

abe00000-abe08000

r-xp

/system/lib/libEGL.so

abe08000-abe09000

rwxp

/system/lib/libEGL.so

abe09000-abe0b000

rwxp

 

ac100000-ac104000

r-xp

/system/lib/libGLESv2.so

ac104000-ac105000

rwxp

/system/lib/libGLESv2.so

ac200000-ac205000

r-xp

/system/lib/libGLESv1_CM.so

ac205000-ac206000

rwxp

/system/lib/libGLESv1_CM.so

ac700000-ac715000

r-xp

/system/lib/libsurfaceflinger_client.so

ac715000-ac718000

rwxp

/system/lib/libsurfaceflinger_client.so

ac900000-ac919000

r-xp

/system/lib/libpixelflinger.so

ac919000-ac91b000

rwxp

/system/lib/libpixelflinger.so

ad100000-ad12f000

r-xp

/system/lib/libnativehelper.so

ad12f000-ad132000

rwxp

/system/lib/libnativehelper.so

ad300000-ad36d000

r-xp

/system/lib/libandroid_runtime.so

ad36d000-ad375000

rwxp

/system/lib/libandroid_runtime.so

ad375000-ad37a000

rwxp

 

ad900000-ad9e0000

r-xp

/system/lib/libicui18n.so

ad9e0000-ad9e4000

rwxp

/system/lib/libicui18n.so

ad9e4000-ad9e5000

rwxp

 

ade00000-aded0000

r-xp

/system/lib/libicuuc.so

aded0000-aded8000

rwxp

/system/lib/libicuuc.so

aded8000-adeda000

rwxp

 

ae300000-ae304000

r-xp

/system/lib/libnetutils.so

ae304000-ae305000

rwxp

/system/lib/libnetutils.so

ae400000-ae402000

r-xp

/system/lib/libwpa_client.so

ae402000-ae403000

rwxp

/system/lib/libwpa_client.so

af000000-af08d000

r-xp

/system/lib/libcrypto.so

af08d000-af09f000

rwxp

/system/lib/libcrypto.so

af09f000-af0a1000

rwxp

 

af400000-af424000

r-xp

/system/lib/libssl.so

af424000-af427000

rwxp

/system/lib/libssl.so

af700000-af713000

r-xp

/system/lib/libz.so

af713000-af714000

rwxp

/system/lib/libz.so

af900000-af90e000

r-xp

/system/lib/libcutils.so

af90e000-af90f000

rwxp

/system/lib/libcutils.so

af90f000-af91e000

rwxp

 

afa00000-afa03000

r-xp

/system/lib/liblog.so

afa03000-afa04000

rwxp

/system/lib/liblog.so

afb00000-afb20000

r-xp

/system/lib/libm.so

afb20000-afb21000

rwxp

/system/lib/libm.so

afc00000-afc01000

r-xp

/system/lib/libstdc++.so

afc01000-afc02000

rwxp

/system/lib/libstdc++.so

afd00000-afd3f000

r-xp

/system/lib/libc.so

afd3f000-afd42000

rwxp

/system/lib/libc.so

afd42000-afd4d000

rwxp

 

b0001000-b000c000

r-xp

/system/bin/linker

b000c000-b000d000

rwxp

/system/bin/linker

b000d000-b0016000

rwxp

 

bea87000-bea9c000

rwxp

[stack]

 

 

 

 

 

 

0xc0000000---------

 

LINUX KERNEL

 

 

 

 

 

使用 ANT包裝NDK應用程式為APK

 

        Java的應用,通常可以透過Ant來包裝動態函式庫.so檔案與Java應用程式為.apk,Ant是一個Apache計畫的產物,官方網站為http://ant.apache.org/ ,下載JUnit https://github.com/KentBeck/junit/downloads  ( 筆者下載的路徑是http://cloud.github.com/downloads/KentBeck/junit/junit-4.9b2.jar),junit-4.9b2.jar複製到ant解開後的路徑 lib/optional,然後如下設定環境變數

 

(http://ant.apache.org/manual/index.html)

exportANT_HOME=/usr/local/ant

exportJAVA_HOME=/android/jdk1.5.0_22 (如果是Cygwin就設定到JDK安裝的路徑,例如export JAVA_HOME=/cygdrive/c/"ProgramFiles"/Java/jdk1.6.0_23)

export PATH=${PATH}:${ANT_HOME}/bin

NDK=/android-ndk-r5b-windows

接下來,執行./build.sh

就可以產生ANT執行的環境,NDK為例,可以進到範例目錄

透過 Android SDK tools下的android.bat產生讓ANT參考的build.xml

snna@snna-PC/android-ndk-r5b-windows/samples/hello-jni

$ android.batupdate project -p . -s

Updated local.properties

Added fileC:/cygwin/android-ndk-r5b-windows/samples/hello-jni/build.xml

Added fileC:/cygwin/android-ndk-r5b-windows/samples/hello-jni/proguard.cfg

Updated local.properties

Added fileC:/cygwin/android-ndk-r5b-windows/samples/hello-jni/tests/build.xml

Added fileC:/cygwin/android-ndk-r5b-windows/samples/hello-jni/tests/proguard.cfg

透過 ndk-build進行編譯

snna@snna-PC/android-ndk-r5b-windows/samples/hello-jni

$ $NDK/ndk-build

(要編譯Debug版本就是加上NDK_LOG=1NDK_DEBUG=1)

Gdbserver      : [arm-linux-androideabi-4.4.3]libs/armeabi/gdbserver

Gdbsetup       : libs/armeabi/gdb.setup

Install        : libhello-jni.so =>libs/armeabi/libhello-jni.so

透過ANT產生APK

snna@snna-PC/android-ndk-r5b-windows/samples/hello-jni

$ ant debug

Buildfile: C:/cygwin/android-ndk-r5b-windows/samples/hello-jni/build.xml

    [setup]Android SDK Tools Revision 8

    [setup]Project Target: Android 2.2

….......................

debug:

     [echo]Running zip align on final apk...

     [echo]Debug Package: C:/cygwin/android-ndk-r5b-windows/samples/hello-jni/bin/HelloJni-debug.apk

 

BUILD SUCCESSFUL

Total time: 5 seconds

安裝 APKandroid手機環境

 

snna@snna-PC/android-ndk-r5b-windows/samples/hello-jni

$ adb installbin/HelloJni-debug.apk (如果之前有安裝過,可以加上-r,重新安裝)

396 KB/s (79155 bytes in 0.195s)

       pkg: /data/local/tmp/HelloJni-debug.apk

Success

 

安裝完畢後,如果希望透過ndk-gdb除錯時,可以在程式啟動後,立刻停住等待除錯器啟動,不要就直接執行下去,可以修改Java程式碼加入函式呼叫android.os.Debug.waitForDebugger() , 如下所示,修改/android-ndk-r5b-windows/samples/hello-jni/src/com/example/hellojni/HelloJni.java

 

package com.example.hellojni;

 

import android.app.Activity;

import android.widget.TextView;

import android.os.Bundle;

 

 

public class HelloJni extends Activity

{

    /** Called when the activityis first created. */

    @Override

    public void onCreate(BundlesavedInstanceState)

    {

        android.os.Debug.waitForDebugger();

        super.onCreate(savedInstanceState);

 

 

snna@snna-PC/android-ndk-r5b-windows/samples/hello-jni

$ $NDK/ndk-gdb--start

GNU gdb 6.6

Copyright (C) 2006 Free SoftwareFoundation, Inc.

GDB is free software, covered by the GNUGeneral Public License, and you are

welcome to change it and/or distributecopies of it under certain conditions.

Type "show copying" to see theconditions.

There is absolutely no warranty forGDB. Type "show warranty" fordetails.

This GDB was configured as"--host=i586-mingw32msvc --target=arm-elf-linux".

(no debugging symbols found)

 

 

Android Runtime

 

        包括工具dexopt,或是app_process(zygote)dalvikvm這些在Android目錄下可以使用的執行檔,其實最主要的實作都是基於/system/lib/libdvm.so,並且透過JNI串起原生程式碼與Java程式碼的介面,這些工具主要是把要傳遞給libdvm.so處理的資料,進行前段的處理,如下以app_process為例說明Zygote程序的初始化流程.

 

        Dalvik第一個Java應用程式Zygote,就由此而來,app_process Source Code所在路徑為frameworks/base/cmds/app_process.系統在初始化Zygote,會帶入如下的參數

/system/bin/app_process -Xzygote/system/bin --zygote –start-system-server

 

        由於最終實作的內容是在Java Framework中的,com.android.internal.os.ZygoteInit (原始碼路徑:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java),app_processC實作中,主要是初始化Dalvik虛擬器,並且把要帶入Java的資訊加以處理,我們簡要說明如下.

 

        1,C程式碼中產生AndroidRuntime (AndroidRuntime實作在frameworks/base/core/jni/AndroidRuntime.cpp)

        2,呼叫AndroidRuntime::addVmArgumentsC參數的個數與內容扣掉屬於app_process的部份(個數減一與字串往前移動一).傳給AndroidRuntime

        3,如果參數等於--zygote與隨後參數等於--start-system-server就把變數startSystemServer設為true,並呼叫set_process_name設定行程名稱為zygote,再來執行AndroidRuntime::start(constchar* className, const bool startSystemServer)函式,該函式會初始化Dalvik虛擬器(AndroidRuntime::startVm呼叫函式JNI_CreateJavaVM(原始碼在:dalvik/vm/Jni.c)),並由C透過JNI介面(FindClass,GetStaticMethodID and CallStaticVoidMethod)執行Javacom.android.internal.os.ZygoteInit Class(原始碼路徑:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java)ZygoteInit::main函式,執行如下行為(只列舉筆者覺得重要的)

        3.a,註冊ZygoteSocket

        3.b,Preload所需的DEX/Class檔案,例如:會透過dvmJarFileOpen(原始碼在:dalvik/vm/JarFile.c),解開ZIP壓縮的JAR檔案,先確認是否有odex結尾的檔案(表示已經做過最佳化的動作),如果有,就會確認該odex檔案的Class相依性(如果有相依的Class被更新,就要重新進行最佳化),接下來搜尋是否有classes.dex結尾的檔案(尚未被最佳化過),並呼叫dvmOptimizeDexFile(原始碼在:dalvik/vm/analysis/DexOptimize.c)進行驗證與最佳化DEX的流程.

        3.c,進行初始化後的記憶體回收動作(GC,Memory Garbage Collection)

        3.d,如果startSystemServertrue,呼叫startSystemServer產生System Server行程.

        3.e,Zygote進入一個Socket等待Client連結的無窮迴圈.至此即完成zygote行程的初始化動作.

        4,如果參數不為--zygote,就會把Class名稱與C參數的個數與內容扣掉屬於app_process的部份(個數減一與字串往前移動一).傳給AndroidRuntime.並呼叫set_process_name設定行程為該Class名稱,再來執行AndroidRuntime::start(const char* className, const boolstartSystemServer)函式,該函式會初始化Dalvik虛擬器,並由C透過JNI介面執行Javacom.android.internal.os.RuntimeInit Class(原始碼路徑:frameworks/base/core/java/com/android/internal/os/RuntimeInit.java)RuntimeInit::main函式

 

 

JNI (JavaNative Interface)

 

        JNI的介面是原本Java環境中就有的機制,Java的應用程式可以跟其他語言實作的動態函式庫進行互通,更進一步來說,就是可以透過Native Code執行的高效率,優化Java應用程式執行上表現.

 

        有兩份關於JNI的文件,Google推薦開發者可以參考的

        1,Introductionand Tutorial
                (http://java.sun.com/docs/books/jni/html/jniTOC.html)

        2,Java Native Interface Specification for J2SE 1.6
                (http://download.oracle.com/javase/6/docs/technotes/guides/jni/spec/jniTOC.html)



        JNIC函式可以存取"JavaVM""JNIEnv"的資料結構,一個JNI C函式的第一個函式參數會是JNIEnv (例如:HelloWorld(JNIEnv * env, jobject jobj)),並且以Dalvik的設計而言,是以一個JavaAP對應到一個Linux Process並對應到一個Dalvik VM的實體,也就是說,Dalvik VM本身並不會進行MultiTask Java AP的動作,同時,每個Java AP與其對應的JNI C動態函式庫,就會以一個Linux Process行程空間(每個都是一個獨立的4GB記憶體空間)為單位來運作.

 

        JNI可以透過FindClass取得Java世界所宣告的Class,透過GetFieldID取得Java世界的變數(並且也可以改變,設定值與使用該變數),與可以透過GetMethodID取得Java世界的Class函式呼叫.有關字元部分需要注意的是,Java世界採用的字元是以Unicode(UTF-16 LE)編碼,而在JNIC世界中是採用UTF-8的方式編碼.

 

        c為例,在實作JNI的函式時,所面對的資料型態,主要是以j開頭加上對應的Java端的資料型態,例如

 

Java中的型態

JNI 中的型態

對應到C實作中的型態

boolean

jboolean

unsigned char  ( unsigned 8 bits)

byte

jbyte

signed char (signed 8 bits)

char

jchar

unsigned short  (unsigned 16 bits)

short

jshort

short (signed 16 bits)

int

jint

int (signed 32 bits)

long

jlong

long long (signed 64 bits)

float

jfloat

float (32-bit IEEE 754)

double

jdouble

double ( 64-bit IEEE 754)

size

jsize

int (signed 32 bits)

void

void

void

String

jstring

void *

object

jobject

void *

class

jclass

void *

array

jarray

void *

 

jobjectArray/jbooleanArray/jbyteArray/jcharArray/jshortArray/jintArray/jlongArray/jfloatArray/jdoubleArray/jthrowable/jweak

void *

 

        需要注意的是,Java中字元是以Unicode的方式編碼,所以jchar對應一個Java字元,才會等於一個unsignedshort 16bits字元,但是在JNI這邊C語言的實作則是以UTF-8的方式來實現,也就是說ASCII字元固定為1bytes,但中文或其他語系的編碼就會有2-6bytes不等的編碼長度(可以參考http://en.wikipedia.org/wiki/UTF-8),中文等亞洲語系通常為3bytes.

 

        如下,提供一個簡單沒有傳入參數與傳回值的JNI範例libtest8.c

#include <jni.h>

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#define LOG_TAG "C LOG DEMO"

#undef LOG

#include <utils/Log.h>

                                                                               

JNIEXPORT void JNICALLJava_loda_Test8_Test8_HelloWorld(JNIEnv * env, jobject jobj)

{

       printf("Hello World in Console./n");

       LOGV("Hello World! This color is for Verbose.");

       LOGD("Hello World! This color is for Debug.");

       LOGI("Hello World! This color is for Information");

       LOGW("Hello World! This color is for Warnning.");

       LOGE("Hello World! This color is for Error.");

}

 

透過如下內容的Android.mk

LOCAL_PATH := $(my-dir)

include $(CLEAR_VARS)

LOCAL_SRC_FILES := libtest8.c

LOCAL_SHARED_LIBRARIES := liblog

LOCAL_MODULE := libtest8

LOCAL_PRELINK_MODULE := false

include $(BUILD_SHARED_LIBRARY)

                                     

之後執行

[root@localhost froyo]# make libtest8

============================================

PLATFORM_VERSION_CODENAME=REL

PLATFORM_VERSION=2.2.1

TARGET_PRODUCT=generic

TARGET_BUILD_VARIANT=eng

TARGET_SIMULATOR=

TARGET_BUILD_TYPE=release

TARGET_BUILD_APPS=

TARGET_ARCH=arm

HOST_ARCH=x86

HOST_OS=linux

HOST_BUILD_TYPE=release

BUILD_ID=MASTER

============================================

target thumb C: libtest8 <=system/core/libtest8/libtest8.c

target SharedLib: libtest8(out/target/product/generic/obj/SHARED_LIBRARIES/libt

est8_intermediates/LINKED/libtest8.so)

target Non-prelinked: libtest8(out/target/product/generic/symbols/system/lib/li

btest8.so)

target Strip: libtest8(out/target/product/generic/obj/lib/libtest8.so)

Install:out/target/product/generic/system/lib/libtest8.so

 

 

之後,筆者把libtest.so放到Windows環境下,首先原本的system.img载入是唯讀的,必須要透過

adb reomunt

system.img可以寫入資料(/dev/block/mtdblock0 /system yaffs2 rw 0 0)

之後,透過adb push libtest8.so /system/lib

616 KB/s (25248 bytes in 0.040s)

把函式庫放到目標主機上,然後透過Eclipse執行下面的程式碼

package loda.Test8;


import android.app.Activity;
import android.util.Log;
import android.os.Bundle;
import java.io.IOException;
import java.io.InputStream;
import java.lang.IllegalStateException;

public class Test8 extends Activity {
    private native voidHelloWorld();    
    static {
            System.loadLibrary("test8");
    }
   
    @Override
    public void onCreate(BundlesavedInstanceState) {
       super.onCreate(savedInstanceState);        
        Log.i("loda","libtest8 Before Call JNI HelloWorld");
        HelloWorld(); 
        Log.i("loda","libtest8 After Call JNI HelloWorld");        
        setContentView(R.layout.main);
    }
  
}

 

就可以看到在Eclipse的環境中,logcat有來自Java程式透過JNI介面呼叫HelloWorld C函式庫的結果了.

 

        接下來,我們把這個JNI範例加上傳入參數與傳回值,如下範例C程式

 

#include <jni.h>

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#define LOG_TAG "C LOG DEMO"

#undef LOG

#include <utils/Log.h>

                                                                               

char gTest8Buffer[]="Unicode basedGlobal Sring Reference from JNI-C";

                                                                               

JNIEXPORT jstring JNICALLJava_loda_Test8d_Test8d_HelloWorld(JNIEnv * env, jobject jobj,jinttest_int,jstring test_string,jdouble test_double)

{

       const jbyte *jni_string;

       jni_string=(*env)->GetStringUTFChars(env,test_string,0);

       if(jni_string==NULL)

       {

                return NULL;

       }

       printf("int:%d string:%sdouble:%f/n",test_int,jni_string,test_double);

       LOGI("int:%d string:%s double:%f/n",test_int,jni_string,test_double);

       (*env)->ReleaseStringUTFChars(env,test_string,jni_string);

       return (*env)->NewStringUTF(env, gTest8Buffer);

}

 

我們額外加入, int,Stringdouble型態的參數,Java端傳入給JNIC程式,編譯後,同樣的透過adb push libtest8d.so /system/lib

61 KB/s (5164 bytes in 0.082s)

傳到Android Target,然後再Eclipse上編譯如下的Java程式

 

package loda.Test8d;

import android.app.Activity;
import android.util.Log;
import android.os.Bundle;

public class Test8d extends Activity {

         private native String HelloWorld(int a,Stringb,double c);     
         
         static {
                   System.loadLibrary("test8d");
           }
           
    @Override
    public void onCreate(BundlesavedInstanceState) {
       super.onCreate(savedInstanceState);
        Log.i("loda","libtest8d Before Call JNI HelloWorld");
        String vStr="It is a stringfrom Java";
        String vRet=HelloWorld(5799232,vStr,123.456); 
        Log.i("loda",vRet);   
        Log.i("loda","libtest8d After Call JNI HelloWorld");        
        setContentView(R.layout.main);
    }
}

 

        就可以示範如何從Java端傳遞整數,字串與浮點數給JNI C程式,然後再從C程式中傳遞結果字串給Java應用程式.

 

        接下來,我們再舉另一個例子,示範如何從JNI C程式中,去呼叫Java端的應用函式,如此我們就可以達成CJava端彼此可以呼叫的結果,並且可以驗證這些JNI的行為在Android架構下確實可以運作無誤.

(參考:http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/method.html)

 

 

Signature

Java Type

Z

boolean

B

byte

C

char

S

short

I

int

J

long

F

float

D

double

L

fully-qualified-class;

[ type

type[]

( arg-types ) ret-type

method type



 

如下所示,我們在JNI C函式中透過GetMethodID取得Java函式String CallFromJNI(int a,String b,double c)c函式與參數宣告 “(ILjava/lang/String;D)Ljava/lang/String;”,

 

#include <jni.h>

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#define LOG_TAG "C LOG DEMO"

#undef LOG

#include <utils/Log.h>

                                                                               

char gTest8Buffer[]="Unicode basedGlobal Sring Reference from JNI-C";

                                                                               

JNIEXPORT jstring JNICALLJava_loda_Test8d_Test8d_HelloWorld(JNIEnv * env, jobject jobj,jinttest_int,jstring test_string,jdouble test_double)

{

       jclass vClass=(*env)->GetObjectClass(env,jobj);

       jmethodID vMethodID=(*env)->GetMethodID(env,vClass,"CallFromJNI","(ILjava/lang/String;D)Ljava/lang/String;");

       if(vMethodID==NULL)

       {

                LOGI("Falied to load JavaFunc CallFromJNI");

                return NULL;

       }

       jstring vStr=(*env)->NewStringUTF(env, gTest8Buffer);

        jstringvRetStr=(*env)->CallObjectMethod(env,jobj,vMethodID,123,vStr,4321.1234);

       const jbyte *jni_string=(*env)->GetStringUTFChars(env,vRetStr,0);

       if(jni_string==NULL)

       {

              return NULL;

       }

       LOGI("Java Method Return String:%s",jni_string);

       (*env)->ReleaseStringUTFChars(env,vRetStr,jni_string);

       return vStr;

 

透過如下命令傳到手機上

adb push libtest8d.so /system/lib

419 KB/s (5160 bytes in 0.012s)

 

再透過如下的Java 程式碼,就可以完成從Java呼叫JNI C函式,到由JNI C函式呼叫回Java函式的兩者互通的函式呼叫機制,並可在Android環境中驗證無誤,如下範例程式碼

 

package loda.Test8d;

import android.app.Activity;
import android.util.Log;
import android.os.Bundle;

public class Test8d extends Activity {

         private native String HelloWorld(int a,Stringb,double c);     
         
         static {
                   System.loadLibrary("test8d");
           }
         private String CallFromJNI(int a,Stringb,double c)
         {
                 Log.i("loda","int:"+a+"_String:"+b+"_double:"+c); 
                 return "From CallFromJNI in Java";
         }
    @Override
    public void onCreate(Bundle savedInstanceState){
       super.onCreate(savedInstanceState);
        Log.i("loda","libtest8d Before Call JNI HelloWorld");
        String vStr="It is a stringfrom Java";
        String vRet=HelloWorld(5799232,vStr,123.456); 
        Log.i("loda", vRet);   
        Log.i("loda","libtest8d After Call JNI HelloWorld");        
        setContentView(R.layout.main);
    }
}

 

        最後,再示範如何透過JNI C函式,去取得JavaClass變數的機制,雖然一般模組化的設計,跨模塊的變數存取最好都是透過函式實作來達成,直接去存取變數並不是一個好的實作方式,但本文的目的主要是演示這些JNI操作的實例作為相關設計的參考之用,所以,還請僅供各位參考,實作上還是建議可以透過函式來使用跨模組的變數,而不是直接自行存取造成後續維護或是問題收斂上的困難.

 

        如下為JNI C程式碼的範例,C程式碼去參考Java程式碼中宣告的int,Stringdouble變數

#include <jni.h>  

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#define LOG_TAG "C LOG DEMO"

#undef LOG

#include <utils/Log.h>

 

char gTest8Buffer[]="Sring fromJNI-C";

 

JNIEXPORT jstring JNICALLJava_loda_Test8e_Test8e_HelloWorld(JNIEnv * env, jobject jobj)

{

        jfieldIDvFieldID;

        jintjInt;

        jstringjStr;

        jdoublejDouble;

        constchar *vpTemp;

       

        jclassvClass=(*env)->GetObjectClass(env,jobj);

        /////////////////////////////////////////////

        //Getand Set Integer from Java

        //

        vFieldID=(*env)->GetFieldID(env,vClass,"IntInJava","I");

        if(vFieldID==NULL)

        {

                LOGI("Faliedto load Java Integer Variable");

                returnNULL;

        }

        jInt=(*env)->GetIntField(env,jobj,vFieldID);

        if(jInt==0)

  {

    LOGI("Faliedto get jInt Object");

    returnNULL;

  }

        LOGI("jInt:%d",jInt);

        jInt=987654321;

        (*env)->SetIntField(env,jobj,vFieldID,jInt);

        /////////////////////////////////////////////

        /////////////////////////////////////////////

        //Getand Set String from Java

        //

        vFieldID=(*env)->GetFieldID(env,vClass,"StringInJava","Ljava/lang/String;");

        if(vFieldID==NULL)

        {

                LOGI("Faliedto load Java String Variable");

                returnNULL;

        }

        jStr=(*env)->GetObjectField(env,jobj,vFieldID);

        if(jStr==NULL)

  {

    LOGI("Faliedto get jStr Object");

    returnNULL;

  }

        vpTemp=(*env)->GetStringUTFChars(env,jStr,NULL);

        if(vpTemp==NULL)

        {

                LOGI("Faliedto get UTF8 String Variable");

                returnNULL;

        }     

        LOGI("jStr:%s",vpTemp);

        (*env)->ReleaseStringUTFChars(env,jStr,vpTemp);

        jStr=(*env)->NewStringUTF(env,"JNIC Copy to Java String Variable");

        if(jStr==NULL)

        {

                LOGI("Faliedto allocate UTF8 String Variable"); 

                returnNULL;

        }

        (*env)->SetObjectField(env,jobj,vFieldID,jStr);

        ////////////////////////////////////////////////

        /////////////////////////////////////////////

        //Getand Set Double from Java

        //

        vFieldID=(*env)->GetFieldID(env,vClass,"DoubleInJava","D");

        if(vFieldID==NULL)

        {

                LOGI("Faliedto load Java Double Variable");

                returnNULL;

        }

        jDouble=(*env)->GetDoubleField(env,jobj,vFieldID);

        if(jDouble==0)

  {

    LOGI("Faliedto get jDouble Object");

    returnNULL;

  }

        LOGI("jDouble:%f",jDouble);

        jDouble=9876.5432;

        (*env)->SetDoubleField(env,jobj,vFieldID,jDouble);

        /////////////////////////////////////////////

        jstringvStr=(*env)->NewStringUTF(env, gTest8Buffer);   

        returnvStr;

}

 

透過如下命令傳到手機上

adb push libtest8e.so /system/lib

334 KB/s (5136 bytes in 0.015s)

 

如下所示,為搭配上述JNI C程式碼參考到Java中變數,所配套的Java程式碼

package loda.Test8e;

import android.app.Activity;
import android.util.Log;
import android.os.Bundle;

public class Test8e extends Activity {

         private native String HelloWorld();       
         private int IntInJava;
         private String StringInJava;
         private double DoubleInJava;
         
         static {
                   System.loadLibrary("test8e");
           }     
         
    @Override
    public void onCreate(BundlesavedInstanceState) {
       super.onCreate(savedInstanceState);
        IntInJava=99999;
        StringInJava="String fromJava";
        DoubleInJava=7777.6666;
        Log.i("loda","libtest8e Before Call JNI HelloWorld");
        String vRet= HelloWorld(); 
        Log.i("loda:","IntInJava:" + IntInJava);  
       Log.i("loda:","StringInJava:" + StringInJava);  
       Log.i("loda:","DoubleInJava:" + DoubleInJava); 
        Log.i("Return String:",vRet);   
        Log.i("loda","libtest8e After Call JNI HelloWorld");        
        setContentView(R.layout.main);
    }
}

        接下來,讓我們示範由JNI C程式碼中,透過初始化要使用的Java Class與使用所包含的Method

#include <jni.h>  

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#define LOG_TAG "C LOG DEMO"

#undef LOG

#include <utils/Log.h>

 

char gTest8Buffer[]="Sring fromJNI-C";

 

JNIEXPORT jstring JNICALLJava_loda_Test8f_Test8f_HelloWorld(JNIEnv * env, jobject jobj)

{

        jmethodIDvMethodID;

        jobjectvNewObj;

       

        jclassvClass=(*env)->FindClass(env,"loda/Test8f/MyJavaClass");

        if(vClass==NULL)

        {

                LOGI("Faliedto FindClass MyJavaClass");

                returnNULL;

        }             

        /////

        vMethodID=(*env)->GetMethodID(env,vClass,"<init>","()V");

  if(vMethodID==NULL)

  {

      LOGI("Falied to Get init ofMyJavaClass");

    returnNULL;

  }

  vNewObj=(*env)->NewObject(env,vClass,vMethodID, NULL);

        /////

        vMethodID=(*env)->GetMethodID(env,vClass,"MyRun","(Ljava/lang/String;)Ljava/lang/String;");

        if(vMethodID==NULL)

        {

                LOGI("Faliedto Get MyRun of MyJavaClass");

                returnNULL;

        }

        LOGI("GetMyJavaClass Method MyRun");

        jstringvStr=(*env)->NewStringUTF(env, gTest8Buffer);

        jstringvRetStr=(*env)->CallObjectMethod(env,vNewObj,vMethodID,vStr);

        constjbyte *jni_string=(*env)->GetStringUTFChars(env,vRetStr,0);

  if(jni_string==NULL)

  {

      LOGI("Falied toGetStringUTFChars");

    returnNULL;

  }

  LOGI("JavaMethod Return String:%s",jni_string);

  (*env)->ReleaseStringUTFChars(env,vRetStr,jni_string); 

  (*env)->DeleteLocalRef(env,vClass);

        returnvStr;

}

 

透過如下命令傳到手機上

adb push libtest8f.so /system/lib

417 KB/s (5132 bytes in 0.012s)

 

如下所示,為搭配上述JNI C程式碼參考到Java中變數,所配套的Java程式碼

package loda.Test8f;

 

import android.app.Activity;

import android.util.Log;

import android.os.Bundle;

 

public class Test8f extends Activity {

 

        privatenative String HelloWorld();        

        static{

            System.loadLibrary("test8f");

    }     

       

    @Override

    publicvoid onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       String vRet= HelloWorld(); 

       Log.i("Return String:", vRet); 

       setContentView(R.layout.main);

    }

}

 

class MyJavaClass implements Runnable

{

        publicvoid run()

        {

       

        }

        publicString MyRun(String StringFromJNIC)

        {

                try

                {

                        Log.i("loda:","in MyJavaClass:" + StringFromJNIC);  

                }     

                catch(Exceptione)

                {     

                        e.printStackTrace();

                }

                Log.i("loda:","Endof MyRun");

                return"From MyJavaClass/MyRun in Java";

        }

}

 

        透過C程式要啟動Dalvik虛擬器,會透過呼叫函式JNI_CreateJavaVM (實作在dalvik/vm/Jni.c),之後透過函式dvmStartup(實作在dalvik/vm/Init.c),再呼叫函式dvmPrepMainThread(實作在dalvik/vm/Thread.c)產生Dalvik虛擬器的Main Thread.

 

        Google2.2Foryo開始釋出NDK r4的版本,支援JNI單步除錯的機制.除了可以直接透過JNI介面開發JavaNative Code的整合應用外,Google當然也提供了NDKFramework,讓有這需求的開發者可以有一定程度的規範.

       

        接下來,我們就針對JNI的介面進一步的追蹤系統運作的原理,

 

        hello-jni範例提供的JNI C函式Java_com_example_hellojni_HelloJni_stringFromJNI為例,進入到Java_com_example_hellojni_HelloJni_stringFromJNI函式後,此時的Call Stack

(gdb) bt full

#0  Java_com_example_hellojni_HelloJni_stringFromJNI(env=0xaa50,thiz=0x43e1d078)

    atC:/cygwin/android-ndk-r5b-windows/samples/hello-jni/jni/hello-jni.c:30

No locals.

#1  0x80213978 in ?? ()

確認Registers LRPC

 

lr             0x80213978      2149661048

pc             0x803002e4      0x803002e4<Java_com_example_hellojni_HelloJni_stringFromJNI+8>

 

進一步確認上一層記憶體0x80213974中的程式邏輯

0x8021396c:     ldmia  r9, {r2, r3}

0x80213970:     ldr    r12, [r4, #8]

0x80213974:    blx    r12 =>由此跳到Java_com_example_hellojni_HelloJni_stringFromJNI中執行C函式庫

0x80213978:     cmp    r5, #0 ; 0x0

0x8021397c:     ldrne  r12, [r4, #12]

0x80213980:     stmneia r12,{r0, r1}

0x80213984:     ldmdb  r4, {r4, r5, r6, r7, r8, r9, sp, pc}

0x80213988:     mov    r5, r2, lsr #28

0x8021398c:     ldr     r6,[r4, #4]

0x80213990:     mov    r2, #0 ; 0x0

0x80213994:     ldrb   r12, [r6], #1

0x80213998:     cmp    r12, #0 ; 0x0

0x8021399c:     beq    0x802139c0

0x802139a0:     add    r2, r2, #1     ; 0x1

0x802139a4:     cmp    r12, #68       ; 0x44

0x802139a8:     cmpne  r12, #74       ; 0x4a

0x802139ac:     bne    0x80213994

 

由上述的程式碼,我們可以對應到JNI呼叫到Native Code中的實作代碼是在dalvik/vm/arch/arm/CallEABI.S原始碼中的Lcopy_done程式碼,負責由Java World呼叫到JNI Native Code

 

.Lcopy_done:

    ...................

    ldmia   r9, {r2-r3}                @ r2/r3<- argv[0]/argv[1]

    @ call the method

    ldr     ip, [r4, #8]               @ func

#ifdef __ARM_HAVE_BLX

    blx     ip

#else

    mov     lr, pc

    bx      ip

#endif

                                                                               

            由此溯源,我們得知函式dvmPlatformInvoke是在Dalvik Vm中負責將Java WorldNative World做一個呼叫轉換的函式.參考dalvik/vm/arch中的實作,Android本身有提供對arm/sh/x86/x86-atom JNI Calling Convention的組語優化,如果不是上述平台,就會透過FFI (Foreign Function Interface)(網址:http://sourceware.org/libffi/ )函式庫來實作函式dvmPlatformInvoke,達成JNI介面在不同平台上的支援. FFI機制,提供一個完整的介面,讓從Interpreter端可以呼叫到原生碼的實作,包括參數的傳遞,與對不同平台的支持,參考網頁,除了Dalvik JNI介面有採用外,包括FireFox 3.6/Mac OSX/iPhone JavaScript,Ruby,Python ..etc都有使用這個介面達成由Interpreter呼叫到原生碼的機制.

 

 

 

Just-In-TimeCompiler

 

        有關JITByteCode轉成原生指令集的概念,前兩天也聽到同事在聊LLVM(Low level virtual machine, 網址http://llvm.org/ ),這概念是把C/C++/Fortran的程式語言透過llvm-gcc轉成中介語言,之後透過LLVM轉換成執行平台原生的指令集,有關程式碼與中介語言的產生,可以透過該網站的Demo網頁http://llvm.org/demo/index.cgi實際的操刀體驗看看.

 

        Dalvik系統除了有PortableFast Intepreter實作外,還可以透過JIT的機制,實現把Dalvik OpCode轉成原生代碼的機制,讓運作的效率可以變得更高,參考dvmMterpStdRun的實作,我們可以知道JITDalvik中的入口是透過Fast Interpreter,如果你所在平台用的是Portable Interpreter,JIT的機制也就沒有作用了.(也是啦,,,,Fast Interpreter就是Dalvik用組語去刻出來的加速Dalvik OpCode直譯器,如果你不是用ARM或是x86的指令集平台,或其他第三方有支援Fast Interpreter組語實作的Dalvik版本,只能採用C版本的實作,自然也不可能會有JIT機制的實作了.).

 

        可以參考如下的新聞Android 2.2 Froyo 450% Faster Than Eclair(網頁:http://www.mobiletopsoft.com/board/7646/android-2-2-froyo-450-faster-than-eclair.html),在有開啟JITAndroid 2.2 Froyo版本中,可以得到比沒有開啟JIT2.1 Eclair大約4.5倍的Dalvik應用執行效率.(分別為37.5 MFLOPS 6.5-7 MFLOPS).

 

        參考Google的文件,JIT的技術,有很多種施行的方式,包括在載入時期編譯,安裝應用程式時編譯,函式被呼叫時編譯整個函式,或是Byte Code指令在被擷取時編譯,而編譯時,可以選擇把整個應用程式編譯,動態函式庫,針對ClassMethod編譯,Trace-JIT(選擇熱點編譯),或一次只編譯一個Byte Code指令.Android在選擇JIT技術時,主要希望以符合移動,透過電池供電,最小的記憶體需求(可以參考Jazelle網頁http://www.arm.com/products/processors/technologies/jazelle.php,JavaByte Code to Native Code記憶體會膨脹4-8),要跟既有的Dalvik安全機制共存,快速達到效能的提升,能在既有的直譯器(Intepreter)JIT技術間平順的過渡.一般常見的Java虛擬器(例如SUN Java HotSpot Virtual Machine)採用的是Method-JIT,Dalvik採用的是Trace-JIT,兩者的差別如下

 

 

 

 

 

Method JIT

1,目前最常見的JIT機制

2,透過直譯器(Interpreter)去統計出熱門的Class Method

3,編譯與最佳化以Java Class Method為單位

4,有關虛擬器對應到直譯器的資訊,只需要考慮到所走過的Method為主.

5,缺點在於,所最佳化的Mehtod,可能會包含幾乎用不到的程式碼片段,由於優化的範圍大,因此對記憶體需求較高,在函式一開始執行時,呼叫到熱門Mehtod,需要有比較長的延遲時間(等待最佳化為機械碼)

Trace JIT

1,透過直譯器去統計熱門的執行路徑(Execution Path)

2,會把編譯的程式碼片段串(Fragment Chain)放到轉譯的Cache(Dalvik-jit-code-cache)

3,只有最熱門的程式碼執行路徑(片段)會被編譯到,可以節省記憶體的需求,並且可以跟直譯器更緊密的結合,擁有較快速的編譯與執行效能(因為不用根據整個Method去編譯)

4,每個Dalvik行程都有獨立的Dalvik虛擬器,並擁有一個Translation Cache(用來儲存編譯為機械碼的Trace段落).

5,缺點在於,優化的範圍較小,會跟直譯器互動的頻率高,並且,較難以把最佳化過的程式碼片段跨行程分享.(由於不是以Method為單位,只根據每個Mehtod中的程式碼片段優化,每個應用程式的行為差異不小,不過這有一篇交大碩士的論文http://www.cis.nctu.edu.tw/~wuuyang/papers/File-Based%20Sharing%20For%20Dynamically%20Compiled%20Code%20On%20Dalvik%20Virtual%20Machine.pdf ,有提到如何在Dalvik中跨行程分享這優化過的程式碼片段)

6,Android建議把JIT視為直譯器能力的延伸

7,確保Trace段落,只會包含有成功執行過的Byte Code

8,會根據使用的平台(通常為ARM),優化暫存器的使用,Load/Store指令的使用(減少需要去外部記憶體存取的成本),消除不必要的Null-Check.

9,優化Byte CodeLoop迴圈的執行.

10,通常會以一個Branch的目標位址作為一個Trace區塊的頭.(以此為Trace區塊也合理,便於在Translation CacheInterpreter之間進行跳躍).

 

 

        參考這份Android文件http://www.android-app-developer.co.uk/android-app-development-docs/android-jit-compiler-androids-dalvik-vm.pdf,Android Dalvik基於記憶體需求,功耗(For移動裝置)與應用程式反應時間,選擇了Trace-JIT為實作的技術,運作的機制為,應用程式先透過Fast Interpreter載入,依據執行的位置,更新程式執行統計表格,,如果該位置沒有達到Threshold,直譯器繼續執行,直到到達下一個可能的Trace段落,再度確認統計表格,是否有達到Threshold,若達到就確認該Trace段落,是否已經有對應編譯的結果,如果有就直接跳過去執行,如果沒有就編譯該Trace段落,並加入到JIT轉譯快取(Translation Cache). 位於Translation中的Trace段落編譯結果,可以依據應用程式的行為彼此呼叫,如果該編譯段落的下一段Trace Fragment並不在Translation Cache,就直接呼叫回直譯器執行該Trace Fragment,繼續Dalvik應用程式的執行.

 

        ARMv5te架構下gDvmJit.threshold = 200 (in Sorce Codecompiler/codegen/arm/armv5te/ArchVariant.c),ARMv7a架構下gDvmJit.threshold = 40 (compiler/codegen/arm/armv7-a/ArchVariant.c),以此來看,處理器的時脈越高,Android對於Trace區塊Threshold的要求就越低,也就是會盡可能都透過JIT編譯讓運作環境更快.(當然也表示對同一Android區塊,JIT的編譯時間越短).

 

        參考Android文件(抱歉,,我就不自己OProfile...@_@),Dalvik JITTrace段落熱區的統計,可以達到有97%是落在Translation Cache(Dalvik-jit-code-cache)(Cache中的Trace段落可以彼此呼叫,直到遇到不在Cache中的Trace段落才需要回到Interpreter),只有3%需要透過Fast Interpreter (Libdvm.so)執行.

 

        承上,system_server為例,在開機後20分鐘,大約有9898Trace段落被編譯,總共佔Byte Code大小約103966bytes(平均每個Trace段落Byte Code11bytes),編譯需要時間約6.024,編譯後的機械碼大小為796264 bytes (平均每個Trace段落轉成機械碼約80bytes),所涉及的Method Byte Code程式碼大小為396230 bytes(如果用Trace-JIT可以省去編譯2.8倍的ByteCode範圍),相對於省去的JIT機械碼大小就更可觀了(Method Byte Code程式碼大小:396230 bytes 乘上7(80/11)=2773610bytes,約可省下編譯後約1.977364MB). 

 

        Dalvik在選擇JIT Trace區塊的時候,會在以下OpCode中進行Profile的統計,找出熱門的執行Trace區塊(參考dalvik/vm/mterp/out/InterpAsm-armv5te.S實作),作為Trace區塊的Head.

       

指令集名稱

編碼

OP_GOTO

0x28

OP_GOTO_16

0x29

OP_GOTO_32

0x2a

OP_PACKED_SWITCH

0x2b

OP_SPARSE_SWITCH

0x2c

OP_IF_EQ

0x32

OP_IF_NE

0x33

OP_IF_LT

0x34

OP_IF_GE

0x35

OP_IF_GT

0x36

OP_IF_LE

0x37

OP_IF_EQZ

0x38

OP_IF_NEZ

0x39

OP_IF_LTZ

0x3a

OP_IF_GEZ

0x3b

OP_IF_GTZ

0x3c

OP_IF_LEZ

0x3d

 

 

        我們舉實際的例子來說明,不過首先定義一下,

ARM EABI GCC編譯器中,對於ARM暫存器的識別名稱

 

暫存器

識別名稱

說明

r10

sl

seems to be generally available

r11

fp

is used by gcc (unless -fomit-frame-pointer is set)

r12

ip

is scratch -- not preserved across method calls

r13

sp

should be managed carefully in case a signal arrives

r14

lr

must be preserved

r15

pc

can be tinkered with directly

 

Fast Interpreter,對於ARM暫存器的使用與識別名稱

 

暫存器

識別名稱

說明

r4

rPC

interpreted program counter, used for fetching instructions

r5

rFP

interpreted frame pointer, used for accessing locals and args

r6

rGLUE

MterpGlue pointer

r7

rINST

first 16-bit code unit of current instruction

r8

rIBASE

interpreted instruction base pointer, used for computed goto

 

        還有參考dalvik/vm/Globals.h中對於gDvmJit的宣告 “extern struct DvmJitGlobals gDvmJit;”,其中DvmJitGlobals定義了JIT運作時,所需的相關變數與狀態資訊.以及dalvik/vm/Thread.h中對於Thread的宣告,建議可以一併理解.

 

        參考dalvik/vm/mterp/common/asm-constants.h,關於組語中會用到的變數定義,同時參考dalvik/vm/mterp/Mterp.h中的宣告"typedef InterpState MterpGlue;”與在原始碼dalvik/vm/interp/InterpDefs.h中關於 “typedefstruct InterpState”的原型,我定義幾個在後續說明時,會用到的MterpGlue struct參數名稱與說明

 

 

ARM 組語中的名稱

C程式碼中的變數名稱

說明

offGlue_pJitProfTable

pJitProfTable

Array of profile threshold counters

offGlue_jitThreshold

jitThreshold

為判斷該目標位置,是否已達可進行JIT編譯為原生碼的要求,以筆者手中的版本來說,armv5te-vfp/armv5te定義為200,armv7-a-neon/armv7-a定義為40即達可進行JIT編譯的門檻.

 

pJitEntryTable

會透過JIT Hash機制,用來儲存在Fast Interpret中有被編譯為原生碼的目標位置Trace段落.

offThread_inJitCodeCache

inJitCodeCache

相對於每個Dalvik Thread,用來儲存對應的Trace區塊編譯後的原生碼記憶體位址

offGlue_jitState

jitState

設定Debug或要取Trace區塊時,Interpreter所在的狀態,可以包括以下的屬性

kJitNot = 0,               // Non-JIT related reasons */

kJitTSelectRequest = 1,    // Request a trace (subject to filtering)

kJitTSelectRequestHot = 2, // Request a hot trace (bypass the filter)

kJitSelfVerification = 3,  // Self Verification Mode

                                                                               

/* Operational states in the debug interpreter */

kJitTSelect = 4,           // Actively selecting a trace

kJitTSelectEnd = 5,        // Done with the trace - wrap it up

kJitSingleStep = 6,        // Single step interpretation

kJitSingleStepEnd = 7,     // Done with single step, ready return to mterp

kJitDone = 8,              // Ready to leave the debug interpreter

 

InterpEntry

組語的宣告

MTERP_CONSTANT(kInterpEntryInstr,   0)

MTERP_CONSTANT(kInterpEntryReturn,  1)

MTERP_CONSTANT(kInterpEntryThrow,   2)

MTERP_CONSTANT(kInterpEntryResume,  3)

C的宣告

kInterpEntryInstr = 0,      // continue to next instruction

kInterpEntryReturn = 1,     // jump to method return

kInterpEntryThrow = 2,      // jump to exception throw

kInterpEntryResume = 3,     // Resume after single-step

offGlue_entryPoint

entryPoint

用來儲存所要去的Interpreter

 

 

 

        接下來,進行流程上的說明,OP_GOTO,如果有加入JIT的支援就會,加入這段Code (我會把認為流程上可以忽略的Code “....”)

 

    mov     r0, rINST, lsl #16         @ r0<- AAxx0000

    movs    r9, r0, asr #24            @ r9<- ssssssAA (sign-extended)

    mov     r9, r9, lsl #1             @ r9<- byte offset=>r9儲存Goto要跳去的Offset

    =>如果r9為負數,表示會往低位址跳,會跳到函式common_backwardBranch中執行

    bmi     common_backwardBranch      @ backward branch, do periodic checks

    =>如果r9為正數,就繼續執行

#if defined(WITH_JIT)

    GET_JIT_PROF_TABLE(r0)

    FETCH_ADVANCE_INST_RB(r9)          @ update rPC, load rINST

    cmp     r0,#0

    bne     common_updateProfile

    GET_INST_OPCODE(ip)                @ extract opcode from rINST

    GOTO_OPCODE(ip)                    @ jump to next instruction

#else

    …...

#endif

 

        其中函式common_backwardBranch的實作如下,

 

common_backwardBranch:

    mov     r0, #kInterpEntryInstr

    …...

#if defined(WITH_JIT)

    GET_JIT_PROF_TABLE(r0)

    FETCH_ADVANCE_INST_RB(r9)          @ update rPC, load rINST

    cmp     r0,#0

    bne     common_updateProfile

    GET_INST_OPCODE(ip)

    GOTO_OPCODE(ip)

#else

    …...

#endif

 

 

        巨集FETCH_ADVANCE_INST_RB(宣告為#defineFETCH_ADVANCE_INST_RB(_reg) ldrh   rINST, [rPC, _reg]!),會把帶入的GOTO目標偏移植(Offset)當做rPC(rPCFast Interpreter中的Program Counter)的偏移植,並把指令存在rINST,與更新下一個指令位置到rPC.巨集GET_JIT_PROF_TABLE (宣告為#define GET_JIT_PROF_TABLE(_reg)   ldr    _reg,[rGLUE,#offGlue_pJitProfTable]),會傳回目前ProfileThreshold Counters表格的記憶體位置,如果該值為0,表示目前並沒有啟動指令Profiling Threshold的機制,就執行原本Fast Interpreter的流程,如果該值不為0,就表示JITProfiling Threshold機制有開啟,就跳到函式common_updateProfile進行處理.如下所示    

 

common_updateProfile:

    =>rPC儲存的是GOTO要去執行的目標位置

    eor     r3,rPC,rPC,lsr #12 @ cheap, but fast hashfunction

    =>在筆者ARM_ARCH_5TE組態中,JIT_PROF_SIZE_LOG_2=9,其它平台為11

    lsl     r3,r3,#(32 - JIT_PROF_SIZE_LOG_2)         @ shift out excess bits

    =>以在Fast Interpreter中的ProgramCounterProfile Threshold Counters表格(位置在r0)Hash,然後把目前目標位置的Threshold,放到r1

    ldrb    r1,[r0,r3,lsr #(32 - JIT_PROF_SIZE_LOG_2)]@ get counter

    =>rINST & 0xff存到r12,成為一個Dalvik OpCode

    GET_INST_OPCODE(ip)

    subs    r1,r1,#1          @ decrement counter

    strb    r1,[r0,r3,lsr #(32 - JIT_PROF_SIZE_LOG_2)]@ and store it

    =>如果r1不為0,就直接跳到該Dalvik OpCode所在的位址,表示該目標位置尚未達Threshold的要求,還是執行Fast Interpreter的原本機制,不編譯為原生碼.

    GOTO_OPCODE_IFNE(ip)      @ if not threshold, fallthroughotherwise */ (CPSR.Z=0)

 


=>
如果r10,就繼續執行,根據Trace區塊,進行編譯的流程.
…..........................

 

    =>取得目前平台預設的Threshold數值

   GET_JIT_THRESHOLD(r1)

   ldr     r10, [rGLUE,#offGlue_self] @ callee saved r10 <- glue->self

    =>重設該目標位置的Threshold Counter,ARMv5預設是200,ARMv7預設是40

   strb    r1,[r0,r3,lsr #(32 -JIT_PROF_SIZE_LOG_2)] @ reset counter

    =>Fast Interpreter要去執行的目標位置,存到Interpreted Frame(Stack)

   EXPORT_PC()

   mov     r0,rPC

   bl      dvmJitGetCodeAddr          @ r0<- dvmJitGetCodeAddr(rPC)

    =>呼叫函式dvmJitGetCodeAddr ,如果位於FastInterpreterProgram Counter已經有被編譯過,並儲存在Translation Cache,就傳回對應的記憶體位置

   str     r0, [r10,#offThread_inJitCodeCache] @ set the inJitCodeCache flag

    =>把編譯過的Code Cache記憶體位址儲存在#offGlue_self中的#offThread_inJitCodeCache欄位中

   mov     r1, rPC                    @ arg1 of translation mayneed this

   mov     lr, #0                     @ in case target is HANDLER_INTERPRET

   cmp     r0,#0

    =>如果r00,表示尚未編譯過,反之表示編譯過,存在Translation Cache

#if !defined(WITH_SELF_VERIFICATION)

   ...............

#else

    =>如果目前Fast Interpreter要去的ProgramCounter尚未編譯過,就設定r2selectTrace Request然後呼叫函式common_selectTrace

   moveq  r2,#kJitTSelectRequest     @ askfor trace selection

   beq     common_selectTrace

    /*

    * At this point, we have a target translation. However, if

    * that translation is actually the interpret-only pseudo-translation

    * we want to treat it the same as no translation.

    */

   mov     r10, r0                    @ save target

   bl     dvmCompilerGetInterpretTemplate

   cmp     r0, r10                    @ special case?

   bne     jitSVShadowRunStart        @ set up self verification shadowspace

   GET_INST_OPCODE(ip)

   GOTO_OPCODE(ip)

   /* no return */

#endif

 

         巨集GET_JIT_THRESHOLD (宣告為#define GET_JIT_THRESHOLD(_reg)    ldr    _reg,[rGLUE,#offGlue_jitThreshold]),巨集EXPORT_PC(宣告為#define EXPORT_PC()  str    rPC, [rFP, #(-sizeofStackSaveArea + offStackSaveArea_currentPc)]),由函式common_selectTrace會先回到函式dvmMterpStd(為標準的mterp進入點,實作位置在dalvik/vm/mterp/Mterp.c),並設定glue->nextMode= INTERP_DBG與傳回true,之後回到函式dvmInterpret(實作的位置在dalvik/vm/interp/Interp.c),因為選擇了Debug版本的Interpreter,呼叫進入dvmInterpretDbg(定義為#define INTERP_FUNC_NAME dvmInterpretDbg ),在函式INTERP_FUNC_NAME(實作在dalvik/vm/mterp/out/InterpC-portdbg.c),再呼叫函式dvmJitCheckTraceRequest(實作在dalvik/vm/interp/Jit.c),再透過函式setTraceConstruction進行Trace的建置,最後呼叫函式dvmCheckJit(實作在dalvik/vm/interp/Jit.c)會一次Interpreter切換週期(Fast Interpreter切到Debug Interpreter),增加一個指令集的方式進行Trace流程的追蹤,目前一個Trace流程最多可以有64TraceRun(MAX_JIT_RUN_LEN=64),也就是說如果你是在函式內GOTO到不同位置的話,這些路徑就會被當做個別的TraceRun,最後,放到同一個Trace流程中.如果是在有Switch操作的函式中,TraceRun的選擇會以一個Switch內的處理指令流程為主,如果離開所在的Switch判斷,就會作為一個Trace流程的結束.dvmCheckJit內部會有以下的判斷依據

 

        1,如果目前已執行Trace指令個數長度不為0,且上一次處理的指令為OP_PACKED_SWITCHOP_SPARSE_SWITCH,就會將Trace流程結束.

        2,如果上一個指令不是GOTO,也不為INVOKE_DIRECT_EMPTY ,而屬於像是IF/Switch/Return/Invoke類的指令,也會作為一個Trace流程的結束.

        3,如果上一個指令是一個Exception或是一個Self-Loop,也會作為一個Trace Run的結束.

        4,如果Trace的指令個數超過100(JIT_MAX_TRACE_LEN=100),也會當做一個Trace流程的結束.

        5,如果前一個指令為Return,也會當做一個Trace流程的結束.

 

        當上述Trace流程結束就會把interpState->jitState設定為kJitTSelectEnd,或是直接進入到switch kJitTSelectEnd,結束Trace流程.

 

        在函式dvmCheckJit中會判斷,如果是第一個Trace區塊的指令,會把當前的Program Counter記錄在interpState->lastPC,然後,直接結束.第二次進來時,會呼叫dexDecodeInstruction(實作在dalvik/libdex/InstrUtils.c)把上一次的ProgramCounter進行指令集的Decode,如果解碼後的指令為OP_PACKED_SWITCH或是OP_SPARSE_SWITCH,就會設定interpState->jitState = kJitTSelectEndTrace區塊做一個結束.若繼續執行,就會透過函式dexGetInstrFlags(Inline函式宣告在libdex/InstrUtils.h),並依據指令集的OpCode決定Flags為以下的值之一

 

 

Dalvik 指令

對應到的Flags狀態

OP_NOP,OP_MOVE,OP_MOVE_FROM16,OP_MOVE_16,OP_MOVE_WIDE,

OP_MOVE_WIDE_FROM16,OP_MOVE_WIDE_16,OP_MOVE_OBJECT,

OP_MOVE_OBJECT_FROM16,OP_MOVE_OBJECT_16,OP_MOVE_RESULT,

OP_MOVE_RESULT_WIDE,OP_MOVE_RESULT_OBJECT,

OP_MOVE_EXCEPTION,OP_CONST_4,OP_CONST_16,OP_CONST,

OP_CONST_HIGH16,OP_CONST_WIDE_16,OP_CONST_WIDE_32,

OP_CONST_WIDE,OP_CONST_WIDE_HIGH16,OP_FILL_ARRAY_DATA,

OP_CMPL_FLOAT,OP_CMPG_FLOAT,OP_CMPL_DOUBLE,

OP_CMPG_DOUBLE,OP_CMP_LONG,OP_NEG_INT,OP_NOT_INT,

OP_NEG_LONG,OP_NOT_LONG,OP_NEG_FLOAT,OP_NEG_DOUBLE,

OP_INT_TO_LONG,OP_INT_TO_FLOAT,OP_INT_TO_DOUBLE,

OP_LONG_TO_INT,OP_LONG_TO_FLOAT,OP_LONG_TO_DOUBLE,

OP_FLOAT_TO_INT,OP_FLOAT_TO_LONG,OP_FLOAT_TO_DOUBLE,

OP_DOUBLE_TO_INT,OP_DOUBLE_TO_LONG,OP_DOUBLE_TO_FLOAT,

OP_INT_TO_BYTE,OP_INT_TO_CHAR,OP_INT_TO_SHORT,OP_ADD_INT,

OP_SUB_INT,OP_MUL_INT,OP_AND_INT,OP_OR_INT,OP_XOR_INT,

OP_SHL_INT,OP_SHR_INT,OP_USHR_INT,OP_ADD_LONG,OP_SUB_LONG,

OP_MUL_LONG,OP_AND_LONG,OP_OR_LONG,OP_XOR_LONG,

OP_SHL_LONG,OP_SHR_LONG,OP_USHR_LONG,OP_ADD_FLOAT,

OP_SUB_FLOAT,OP_MUL_FLOAT,OP_DIV_FLOAT,OP_REM_FLOAT

,OP_ADD_DOUBLE,OP_SUB_DOUBLE,OP_MUL_DOUBLE,OP_DIV_DOUBLE,

OP_REM_DOUBLE,OP_ADD_INT_2ADDR,OP_SUB_INT_2ADDR,

OP_MUL_INT_2ADDR,OP_AND_INT_2ADDR,OP_OR_INT_2ADDR,

OP_XOR_INT_2ADDR,OP_SHL_INT_2ADDR,OP_SHR_INT_2ADDR,

OP_USHR_INT_2ADDR,OP_ADD_LONG_2ADDR,OP_SUB_LONG_2ADDR,

OP_MUL_LONG_2ADDR,OP_AND_LONG_2ADDR,OP_OR_LONG_2ADDR,

OP_XOR_LONG_2ADDR,OP_SHL_LONG_2ADDR,OP_SHR_LONG_2ADDR,

OP_USHR_LONG_2ADDR,OP_ADD_FLOAT_2ADDR,OP_SUB_FLOAT_2ADDR,

OP_MUL_FLOAT_2ADDR,OP_DIV_FLOAT_2ADDR,OP_REM_FLOAT_2ADDR,

OP_ADD_DOUBLE_2ADDR,OP_SUB_DOUBLE_2ADDR,

OP_MUL_DOUBLE_2ADDR,OP_DIV_DOUBLE_2ADDR,

OP_REM_DOUBLE_2ADDR,OP_ADD_INT_LIT16,OP_RSUB_INT,

OP_MUL_INT_LIT16,OP_AND_INT_LIT16,OP_OR_INT_LIT16,

OP_XOR_INT_LIT16,OP_ADD_INT_LIT8,OP_RSUB_INT_LIT8,

OP_MUL_INT_LIT8,OP_AND_INT_LIT8,OP_OR_INT_LIT8,OP_XOR_INT_LIT8,

OP_SHL_INT_LIT8,OP_SHR_INT_LIT8,OP_USHR_INT_LIT8      

flags = kInstrCanContinue; 

表示這類指令會加入Trace區塊,並不會造成Exception

OP_CONST_STRING,OP_CONST_STRING_JUMBO,OP_CONST_CLASS,

OP_MONITOR_ENTER,OP_MONITOR_EXIT,OP_CHECK_CAST,

OP_INSTANCE_OF,OP_ARRAY_LENGTH,OP_NEW_INSTANCE,

OP_NEW_ARRAY,OP_FILLED_NEW_ARRAY,OP_FILLED_NEW_ARRAY_RANGE

,OP_AGET,OP_AGET_BOOLEAN,OP_AGET_BYTE,OP_AGET_CHAR,

OP_AGET_SHORT,OP_AGET_WIDE,OP_AGET_OBJECT,OP_APUT,

OP_APUT_BOOLEAN,OP_APUT_BYTE,OP_APUT_CHAR,OP_APUT_SHORT,

OP_APUT_WIDE,OP_APUT_OBJECT,OP_IGET,OP_IGET_BOOLEAN,

OP_IGET_BYTE,OP_IGET_CHAR,OP_IGET_SHORT,OP_IGET_WIDE,

OP_IGET_OBJECT,OP_IPUT,OP_IPUT_BOOLEAN,OP_IPUT_BYTE,

OP_IPUT_CHAR,OP_IPUT_SHORT,OP_IPUT_WIDE,OP_IPUT_OBJECT,

OP_SGET,OP_SGET_BOOLEAN,OP_SGET_BYTE,OP_SGET_CHAR,

OP_SGET_SHORT,OP_SGET_WIDE,OP_SGET_OBJECT,OP_SPUT,

OP_SPUT_BOOLEAN,OP_SPUT_BYTE,OP_SPUT_CHAR,OP_SPUT_SHORT,

OP_SPUT_WIDE,OP_SPUT_OBJECT,OP_DIV_INT,OP_REM_INT,

OP_DIV_LONG,OP_REM_LONG,OP_DIV_INT_2ADDR,OP_REM_INT_2ADDR,

OP_DIV_LONG_2ADDR,OP_REM_LONG_2ADDR,OP_DIV_INT_LIT16,

OP_REM_INT_LIT16,OP_DIV_INT_LIT8,OP_REM_INT_LIT8,

OP_EXECUTE_INLINE,OP_EXECUTE_INLINE_RANGE,OP_IGET_QUICK,

OP_IGET_WIDE_QUICK,OP_IGET_OBJECT_QUICK,OP_IPUT_QUICK,

OP_IPUT_WIDE_QUICK,OP_IPUT_OBJECT_QUICK      

flags = kInstrCanContinue | kInstrCanThrow;

表示這類指令會加入Trace區塊,但有可能造成Exception

 

 

 

OP_INVOKE_VIRTUAL,OP_INVOKE_VIRTUAL_RANGE,OP_INVOKE_SUPER,

OP_INVOKE_SUPER_RANGE,OP_INVOKE_DIRECT,

OP_INVOKE_DIRECT_RANGE,OP_INVOKE_STATIC,

OP_INVOKE_STATIC_RANGE,OP_INVOKE_INTERFACE,

OP_INVOKE_INTERFACE_RANGE,OP_INVOKE_VIRTUAL_QUICK,

OP_INVOKE_VIRTUAL_QUICK_RANGE,OP_INVOKE_SUPER_QUICK,

OP_INVOKE_SUPER_QUICK_RANGE,OP_INVOKE_DIRECT_EMPTY

flags = kInstrCanContinue | kInstrCanThrow | kInstrInvoke; 

表示這類指令會加入Trace區塊,有機會發生Exception或是進行外部呼叫

OP_RETURN_VOID,OP_RETURN,OP_RETURN_WIDE,OP_RETURN_OBJECT

flags = kInstrCanReturn;

表示為Return指令

OP_THROW,OP_THROW_VERIFICATION_ERROR

flags = kInstrCanThrow;

會觸發Exception

OP_GOTO,OP_GOTO_16,OP_GOTO_32

flags = kInstrCanBranch | kInstrUnconditional;

表示一個無條件的Branch動作

OP_IF_EQ,OP_IF_NE,OP_IF_LT,OP_IF_GE,OP_IF_GT,OP_IF_LE,OP_IF_EQZ,

OP_IF_NEZ,OP_IF_LTZ,OP_IF_GEZ,OP_IF_GTZ,OP_IF_LEZ      

flags = kInstrCanBranch | kInstrCanContinue;

表示一個搭配條件判斷的Branch動作

OP_PACKED_SWITCH,OP_SPARSE_SWITCH

flags = kInstrCanSwitch | kInstrCanContinue;

表示一個Switch判斷,如果該值不在這Switch,會加入Trace區塊

OP_UNUSED_3E,OP_UNUSED_3F,OP_UNUSED_40,OP_UNUSED_41,

OP_UNUSED_42,OP_UNUSED_43,OP_UNUSED_73,OP_UNUSED_79,

OP_UNUSED_7A,OP_UNUSED_E3,OP_UNUSED_E4,OP_UNUSED_E5,

OP_UNUSED_E6,OP_UNUSED_E7,OP_UNUSED_E8,OP_UNUSED_E9,

OP_UNUSED_EA,OP_UNUSED_EB,OP_BREAKPOINT,OP_UNUSED_F1,

OP_UNUSED_FC,OP_UNUSED_FD,OP_UNUSED_FE,OP_UNUSED_FF

these should never appear when scanning code

flags=0

 

        後續,呼叫dexGetInstrOrTableWidthAbs(實作在libdex/InstrUtils.c)判斷所在位置指令或是資料(PackedSwitchSignature/SparseSwitchSignature/ArrayDataSignature)的長度(指令長度的Table可以透過函式dexCreateInstrWidthTable建立),以及距離所在Class Method起始點的位置(lastPC – interpState->method->insns),如果超過該ClassMethod的實際實作的大小,就會觸發Assertion.

               

        等到完成一個Trace流程段落,會呼叫函式dvmCompilerWorkEnqueue,把這個Trace流程段落放到JIT編譯器的Queue,之後就會由Compilerthread透過函式dvmCompileTrace(實作在dalvik/vm/compiler/Frontend.c)進行編譯為原生碼的動作.並重新回到函式dvmMterpStd,繼續下一個指令集的抓取與執行,直到又有指令Threshold被滿足,而進行下一次的Trace追蹤的流程.在進行Trace追蹤的流程時,相關指令都還是會以Fast Interpreter的實作被執行.     

 

        要在Android環境中支援JIT機制,必須在Dalvik編譯時加入WITH_JIT參數,可以參考dalvik/vm/Android.mk,如果所選擇的架構是armv5te (TARGET_ARCH_VARIANT:=armv5te),WITH_JIT預設會是關掉的.然後修改build/target/board/generic/system.prop加入dalvik.vm.execution-mode=int:jit(這個系統參數設定會被加入到執行時期的檔案/system/build.prop), ,就可以開始編譯支援Dalvik JIT的虛擬器.

 

        支援JITDalvik環境除了會編譯出libdvm.so,參考dalvik/vm/Android.mk,在編譯Dalvik函式庫時,還會額外產生以下三個函式庫

        libdvm_sv.so(開啟Assertionself-verification)

        libdvm_assert.so(開啟AssertionJIT Tuning)

        libdvm_interp.so(這是把WITH_JIT關閉的版本)

 

        可根據自己開發上的除錯,決定要採用的版本.

 

        要確認所編譯的Dalvik虛擬器有無啟動JIT機制,可以在啟動後,執行dalvikvm -h 查看是否有下列參數設定

 

  -Xjitop:hexopvalue[-endvalue][,hexopvalue[-endvalue]]*

  -Xincludeselectedmethod

  -Xjitthreshold:decimalvalue

  -Xjitblocking

  -Xjitmethod:signature[,signature]*(eg Ljava/lang/String/;replace)

  -Xjitcheckcg

  -Xjitverbose

  -Xjitprofile

  -Xjitdisableopt 

 

 

簡述ADB,EmulatorNDK-GDB運作的機制

 

        在非直接使用實體手機裝置透過USB開發的環境下,我們可以透過電腦上的模擬器達到便利開發的目的,根據上述的流程,我們可以看到包括abd或是ndk-gdb這些輔助開發,安裝與除錯的工具,都可以跟Emulator互通.目前ADB,Emulator或是NDK-GDB都是透過網路TCP連線的方式進行互通.如果使用者使用Eclipse加上ADT的環境,一開始會產生一個ADBDaemon,監聽port 5037,Eclipse上的ADT套件,會產生連線到ADTport 5037. 如果有執行Emulator的話,就會由ADT連到Emulator所監聽的Port5555,Eclipse連到Emulator所監聽的Port 5554. EmulatorPort訊息也會透過ADB Port 5037交換,如下所示

 

Receive Side Port:5037

==>Return Code: 0x00000000

     001Chost:transport:emulator-5554

 

Send Size Port:40280

==> Return Code: 0x00000000

      OKAY

 

        如果使用者透過ndk-gdb - -start啟動對JNI應用除錯機制的話,就會由arm-linux-androideabi-gdb.連到adb所監聽的Port 5039進行GDB的操作動作.

 

 

(Receive Port:5037 Send Port:49288)

Receive: Return Code: 0x00000000

0052host:forward:tcp:5039;localfilesystem:/data/data/com.example.hellojni/debug-socket

 

Send: Return Code: 0x00000000

OKAYOKAY

 

 

(Receive Port:5037 Send Port:49290)

 

Receive: Return Code: 0x00000000

0012host:transport-any

 

Send: Return Code: 0x00000000

OKAY

 

Receive: Return Code: 0x00000000

004ashell:run-as com.example.hellojnilib/gdbserver +debug-socket --attach 348

 

Send: Return Code: 0x00000000

OKAY

 

Send: Return Code: 0x00000000

Attached; pid = 348

 

 

Send: Return Code: 0x00000000

Listening on sockaddr socket debug-socket

 

(Receive Port:5039 Send Port:49295)

Receive: Return Code: 0x00000000

$qSupported#37

 

Send: Return Code: 0x00000000

+$PacketSize=7cf;qXfer:auxv:read+#70

 

Receive: Return Code: 0x00000000

++$Hc-1#09

 

Send: Return Code: 0x00000000

+$E01#a6

 

Receive: Return Code: 0x00000000

+$qC#b4

 

Send: Return Code: 0x00000000

+$#00

 

Receive: Return Code: 0x00000000

+$qOffsets#4b

 

Send: Return Code: 0x00000000

+$#00

 

Receive: Return Code: 0x00000000

+$?#3f

 

Send: Return Code: 0x00000000

+$T050b:0*"00;0d:9838a0be;0f:08ebd0af;#6e

 

Receive: Return Code: 0x00000000

+$m9040,108#ff

 

Send: Return Code: 0x00000000

+$010*"c4ca00b0010*"24cf00b0010*"3cd000b0010*"6cd200b0010*"84d300b0010*"dccb00b0010*"f4cc00b0010*"0cce00b020*%90*!210*"080*"190*"0890*!1b0*"080*"1a0*"1090*!1c0*"080*"040*"08810*!50*"ec840*!60*"4c820*!a0*"e5030*!b0*"10*"0150*"a8c400b0030*"48910*!20*"b0*"0140*"110*"170*"fc880*110*"d4880* 120*"280*"130*"080*}0*!#a8

 

Receive: Return Code: 0x00000000

+$mb000c4ac,4#1a

 

Send: Return Code: 0x00000000

+$b0ca00b0#48

 

Receive: Return Code: 0x00000000

+$mb000cab0,14#46

 

Send: Return Code: 0x00000000

+$0*"00db3da0be0*"00e04901b0*%#b1

 

Receive: Return Code: 0x00000000

+$mb00149e0,14#f3

 

Send: Return Code: 0x00000000

+$0*"00dc4801b0*%e0cc00b0b0ca00b0#e6

 

 

結語

 

        本文謹涉及Trace JIT,JNI與有關Dalvik基礎的知識,後續,會視時間從系統運作的角度,來分析Dalvik在效能加速上的議題. 

 

        雖盡可能確保資訊的正確性,然若仍有所遺漏,還請不吝告知.感謝!!