android odex版本调试_Android可执行文件之谜 - DEX与ODEX, OAT与ELF
前言
米娜桑,是時候揭開DEX的面紗了!我們都知道multidex,都知道65535方法數超標,那DEX到底是個什么東西呢?或許又有些同學知道DEX會優化為ODEX,那ODEX又是什么鬼,優化了什么呢?為什么ClassLoader熱補丁方案插入構造函數導致CLASS_ISPREVERIFIED為false后,會對性能造成影響,和ODEX又有什么關系呢?
我們又知道5.0以上Android虛擬機變成了Art,那DEX在art上變成了什么呢?為什么安裝特別耗時間?有時候我看著我的Nexus6安裝一個應用在那進度條讀啊讀的好像卡住了,有一種想砸了它的想法,所以當我拿到Nexus 5測試機的時候,第一件事就是刷到4.4,不然每次安裝的效率實在不能忍(捂臉)。
DEX是什么
直接把apk當成zip打開后,第一級目錄你就會看見有classes.dex,這就是我們要揭開面紗的東西了。
Why DEX
為什么需要DEX,jar不行嗎?相應地,為什么需要Dalvik虛擬機,JVM不行嗎?
Dalvik虛擬機是專門為了Android移動平臺設計的。目標系統的RAM有限,數據存儲在緩慢的內部閃存上,而且性能和上個世紀的周免系統相當。它們運行Linux,來提供虛擬內存,進程和線程,以及基于UID的安全機制。
這些特征和限制使我們聚焦在這些目標上:類數據,尤其是字節碼,必須被多個進程共享,以最小化系統內存使用。
啟動一個新app的開銷必須最小化,來保證設備的可響應。
在獨立的文件存儲類數據可能導致很多冗余,尤其是字符串。為了保證磁盤空間,我們需要把這些因子提出來。
解析類數據的fields在類加載的時候增加了很多不必要的開銷。把數據值直接當成C類型(比如整數或字符串)使用會更好。
字節碼驗證是必要的,卻也是緩慢的。所以我們想在app執行外盡量驗證更多,以便不要影響app本身體驗。
字節碼優化(加速指令,精簡方法)對速度和電池生命很重要。
為了安全原因,進程不能編輯共享代碼。
典型的虛擬機執行從壓縮文件解壓獨立的類,然后把它們存到heap上。這就導致了每個類可能在每個進程有獨立的拷貝,從而使得應用啟動變慢,因為代碼必須被解壓(或者至少需要從磁盤的很多小片段去讀取)。另一方面,在本地heap放置字節碼簡化了首次使用時的指令重寫,從而可能導致一些不同的優化。
這些目標指引了一些基本決定:多個類被聚集到一個單個的DEX文件。
DEX文件被映射為只讀,并且在進程間共享。
針對本地系統調整字節碼順序和詞對齊。
字節碼驗證對所有類都是強制的,但我們想要對一切可能的進行”預驗證(pre-verify)”。
需要重寫字節碼的優化必須提前執行。
而Dalvik虛擬機和DEX也就應運而生。
Hello DEX
讓我們手動來生成一個java,編譯成javac,然后轉換為dex看看:
1
2
3
4
5
6
7echo?'class?Foo?{'\
'public?static?void?main(String[]?args)?{'\
'System.out.println("Hello,?world");?}}'?>?Foo.java
javac?Foo.java
dx?--dex?--output=foo.jar?Foo.class
adb?push?foo.jar?/sdcard/
adb?shell?dalvikvm?-cp?/sdcard/foo.jar?Foo
當我們在dx命令的output中指定輸出文件后綴為.jar,.zip,或者.apk,名為classes.dex的文件就會被創建并保存在壓縮包內。解開Foo.jar你就會看到classes.dex和META-INF文件夾(里面只有一個MANIFEST.MF文件)。
我們創建完該jar后直接push到設備上,并通過shell直接讓dalvik虛擬機去運行它,如果操作無誤,會看到命令行的反饋 - Hello, world。
DEX in file system
這次我打算多畫點圖,所以看圖說話吧:
DEX in memory
為什么DEX不能被內存映射,或者說,不能直接從zip去執行呢?因為數據是壓縮的,文件頭也不保證是詞對齊的。這些問題可以通過不壓縮直接保存為classes.dex和填充zip文件來解決,但會導致數據網絡間傳輸的包體積變大。
我們需要在使用前把zip包里的classes.dex解壓。當我們拿到文件的時候,我們可能還會做些之前提到的其他操作(對齊、優化、驗證)。這又引出了另一個問題:誰去負責做這些,我們又該把輸出放在哪兒?
ODEX是什么
ODEX,全名Optimized DEX,即優化過的DEX。
有至少3種方法去創建一個“準備好的”DEX文件,即ODEX:虛擬機“即時(just in time)”執行。輸出會跑到一個特殊的dalvik-cache目錄。這只在一些特殊的桌面和工程機的設備上使用(這些機器的build中,dalvik-cache目錄的權限不是嚴格的)。在生產機器上這是不被允許的。
系統的安裝器在程序首次安裝時候執行,它有寫dalvik-cache的權限。
構建(build)系統預先執行。相關的 jar / apk 文件還在,但classes.dex被剝離出來了。ODEX和原來的zip包保存在一起,不在dalvik-cache,而是系統鏡像的一部分。
dalvik-cache目錄更準確地說是$ANDROID_DATA/data/dalvik-cache。里面的文件的名字來源于源DEX的完整路徑。在設備上該目錄被system所擁有,而system擁有0771權限,保存在那里的ODEX被系統和應用的組所擁有,權限為0644。數字權限保護的應用會使用640權限來防止其他應用去檢測它們。底線是你可以讀取自己的與其他大部分應用的DEX文件,但你不能創建、修改,或刪除它們。
前兩種方法的執行分為以下三個步驟:
首先,dalvik-cache文件被創建。這必須在一個有恰當權限的進程進行,所以在“系統安裝器”的場景,是在運行為root的installd進程執行的。
接著,classes.dex從zip包中解壓出來。文件頭部留出一小塊空間給ODEX header。
最后,文件被內存映射以便訪問,并被為當前系統使用進行調整。這包括了字節交換(byte-swapping),結構重新排列(structure realigning),但并沒有對DEX文件做有意義的改變。還做了一些其他的基本結構檢查,比如確保文件偏移量和數據索引落在有效范圍內。
構建系統不在桌面上運行工具,而寧愿去啟動模擬器,強制所有相關DEX文件的即時優化,然后從dalvik-cache把結果提取出來。這樣做的原因,在解釋完優化后會變得更顯而易見。
一旦代碼被字節替換和對齊,我們就可以繼續了。我們添加了一些預計算的數據,在文件頭填寫ODEX header,然后開始執行。然而,如果我們對驗證和優化有興趣,就需要在初始準備后再插入一個步驟。
dexopt的魔法
在Android 2.3版本以前,系統源碼中提供了生成odex的工具dexopt-wrapper,位于Android 2.2系統源碼的 build/tools/dexpreopt/dexopt-wrapper/ 目錄下,查看DexOptWrapper.cpp文件會發現實際調用的是 /system/bin/dexopt 程序。在5.0及以上版本的設備上,你可能已經再也找不到dexopt了,取而代之的是dex2oat。
我們想要驗證和優化DEX文件里的所有類。最簡單和安全的方法就是把所有類加載到虛擬機,然后跑一遍。任何加載失敗的就是驗證/優化失敗的。不幸的是,這可能導致一些資源的分配難以釋放(比如native共享庫的加載),所以我們不想執行在應用運行的虛擬機里。
解決方案就是起一個叫做dexopt的程序(事實上就是虛擬機的后門)。它會執行一個簡短的虛擬機初始化,從引導的類路徑加載0個或多個DEX文件,然后開始做一切從目標DEX可以做的驗證和優化。結束后,進程退出,釋放所有資源。
因為多個虛擬機可能同時需求同一個DEX文件,文件鎖被用來確保dexopt僅被執行一次。
驗證
字節碼驗證過程包含了掃描DEX文件中每一個類每個方法的指令。目的是為了識別非法指令序列以便不會在運行時才發現它們。涉及到的很多運算對“準確的”GC也是必要的。更多信息見Dalvik字節碼驗證器筆記。
為了性能原因,(下節描述的)優化器假設驗證器已經運行成功,還會做一些其他可能不安全的假設。默認地,Dalvik會堅持驗證所有類,并只優化那些被驗證過的類。可以使用命令行flags去禁用驗證器。怎么在Android應用框架中控制這些功能的指令見控制嵌入式虛擬機。
驗證失敗的報告是一個復雜的問題。例如,在不同的package中,調用一個package內可見的方法是非法的,會被驗證器捕捉到。但我們未必想要在驗證期報告它 —— 事實上我們想要在試圖調用方法的時候拋出異常。在每個方法調用上檢查這些訪問flags也是很昂貴的,Dalvik字節碼驗證器筆記提到了這個問題。
成功被驗證的類在ODEX有一個flag被設置了,在加載的時候就不會被重新驗證。ODEX文件有一個32位的checksum,但那是主要是用來快速檢查數據損壞的。
優化
虛擬機解釋器通常會在一段代碼被首次使用的時候執行某些優化。常量池引用被指向內部數據結構的指針所替代,總是成功的操作或是那些總會以某種方式工作的,會被更簡單的形式所替代。這些的一部分需要僅在運行時可用的信息,另一部分在某些特定假設下可以被靜態推論出。
Dalvik優化器做了這些:對于虛方法調用,把方法索引替換為vtable索引。
對于實例變量(field)的get/put,把變量索引替換為字節偏移。另外,把 boolean / byte / char / short 基本變量(variants)合并到單個的32位形式(解釋器里更少的代碼意味著CPU I-cache里更少的空間)。
替換一些高頻次調用,比如把 String.length() 替換成”內聯“的。這可以跳過一些常見的方法調用消耗,直接從解釋器切換到native實現。
刪除空方法。最簡單的例子就是Object.,啥都沒干,但卻必須在任何對象被分配的時候執行。指令會被替換為一個新版本的空指令(no-op)形式,除非調試器被attach上去了。
附加預計算數據。例如,虛擬機想要一個類名的哈希表以便查找。不同于在加載DEX文件時候去計算這個,我們可以先計算,以節省堆(heap)空間和所有加載該DEX文件的虛擬機的計算時間。
大部分的優化顯然都會更好。
Hello ODEX
我們繼續玩耍之前生成的dex,來做一個odex:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20adb?push?dexopt-wrapper?/sdcard/
adb?shell
#?不然沒權限去/data/local
su
chmod?777?dexopt-wrapper
#?直接在sdcard執行會提示權限錯誤
cp?dexopt-wrapper?/data/local/
cp?foo.jar?/data/local/
cd?/data/local
/dexopt-wrapper?foo.jar?foo.odex
---?BEGIN?'foo.jar'?(bootstrap=0)?---
---?waiting?for?verify+opt,?pid=5220
---?would?reduce?privs?here
---?END?'foo.jar'?(success)?---
cp?foo.odex?/sdcard
exit
exit
adb?pull?/sdcard/foo.odex?.
這樣子就拿到了優化后的odex,趕緊把手機還給同事。
打開App,閱讀手記
總結
以上是生活随笔為你收集整理的android odex版本调试_Android可执行文件之谜 - DEX与ODEX, OAT与ELF的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 百度文心一言问答测试为视频演示 李彦宏:
- 下一篇: 百度“文心一言”首款量产车 长安逸达本周