Dalvik Optimization and Verification With dexopt

Dalvik Optimization and Verification With dexopt

Dalvik 是專門設計用於Android手機平臺的虛擬機。主要目標系統是 內存較小,讀寫存儲速度比較慢,機器性能普片比較差的桌面系統。這些系統通常運行在提供有虛擬內存,進程和線程管理,UID安全機制 的Linux系統之上。

在一些條件限制和特性要求下,我們主要關注以下幾個目標:

  • 類數據,尤其是字節碼, 必須能夠在多個進程間共享,以最大限度地減少系統總內存使用量。
  • 啓動一個新App的開銷必須要儘量少,使設備保持交互的靈敏性
  • 每個類的信息保存在各自獨立的文件中會造成大量的冗餘,尤其是字符串。爲了節省空間,我們需要將這點考慮進去。
  • 在類載入的過程中解析類數據字段會增加不必要的開銷。像C語言一樣直接訪問類數據(例如:數值和字符串)會比較好。
  • 字節碼的校驗是必不可少,但它比較耗時,我們儘可能在App運行以外的時間執行這個步驟。
  • 字節碼的優化(quickened instructions, method pruning)對運行速度和電池續航非常重要。
  • 出於安全的考慮,進程將不允許編輯共享的代碼。

一般的虛擬機實現會從歸檔(zip、jar)文件中解壓出每一個類並將他們保存在堆內存中。這意味着在每個進程中都會有每個類的副本,並且會減慢應用的啓動速度,因爲代碼需要解壓(或者至少也要從存儲上讀取很多的小片)。另一方面,將字節碼保存在本地內存堆中更容易在第一次使用的時候進行指令重寫和執行一系列的優化。

居於以上的目標使我們作出以下的決定:

  • 多個類文件會被整合到唯一的一個“DEX”文件中。
  • DEX文件會被映射成只讀模式並且在進程中共享。
  • 根據本地系統進行直接排序和字對齊操作。
  • 對所有類來說字節碼校驗是必不可少的,但我們想盡可能的進行 預校驗。
  • 需要重寫字節碼的優化步驟要提前完成。

以下的部分將介紹這些決定的結果。

VM Operation

應用代碼是添加在.jar或者.apk中再傳給系統。這些文件都是zip格式的歸檔文件,並添加了一些Meta的文件。Dex 數據文件始終命名爲 classes.dex。
保存在zip文件中的字節碼無法直接進行內存映射和執行,因爲數據是被壓縮的並且文件的開頭不能保證是字對齊的。這些問題可以通過存儲沒有壓縮的classes.dex和填充zip文件來解決, 但這會增加網絡傳輸時包打大小。
我們在使用classes.dex文件時,需要從zip文件中解壓出來。我們需要執行一些步驟(重新對齊,優化,校驗)。然而這又提出了一個新的問題:誰來負責做這些事情,應該將輸出的文件保存在哪裏。

Preparation

至少有3種不同方式可以創建“prepared”的dex文件,也叫作“ODEX”(優化後的DEX文件):

  1. 虛擬機採用準實時(“just in time”)的方式。輸出文件保存在“dalvik-cache”目錄中。這種方式適用於桌面或者開發機器,因爲“dalvik-cache”目錄的權限是不受限的。這在正式發佈的產品上是不允許的。
  2. 系統安裝程序在應用第一次被安裝的時候創建。它具備寫入dalvik-cache”目錄的權限。
  3. 構建系統在構建的時候提前處理。相關的 jar/apk文件仍然存在,但是classes.dex文件被刪除了。優化後的DEX文件保存在和 jar/apk 文件的相同目錄,並非 dalvik-cache中, 是系統鏡像文件的一部分。

“dalvik-cache”目錄準確的講應該是 “$ANDROID_DATA/data/dalvik-cache”。目錄中的文件名稱來源於原始DEX文件的完整路徑。設備上該目錄所有者爲 system/system擁有771的權限(drwxrwx--x system system 2019-09-09 09:50 dalvik-cache), 存儲在該目錄中優化後的DEX文件擁有者爲system和具有0644權限的應用組(-rw-r--r-- system u0_a46 819976 2018-05-22 02:29 data@[email protected]@classes.dex)。DRM-locked程序將使用640的權限來防止其他用戶程序訪問這些文件。最低限度允許你讀取本應用和其他應用的DEX文件,但是你不能創建,修改和刪除。

“just in time”和“system installer”在處理DEX文件的方式上主要有3個步驟:
第一,創建 dalvik-cache目錄。這必須要讓具有相應權限的進程來完成,所以 “system installer” 以root的身份運行在 installd進程中。
第二,將classes.dex從zip歸檔文件中解壓出來, 並在文件頭部開始的位置預留一定的空間用於保存ODEX頭部信息。
第三,文件使用內存映射的方式方便訪問並針對當前系統進行調整。這些調整包括字節交換和結構調整,但對DEX文件沒有實質性的修改。同時我們也做一些基本的結構檢查,例如:確保文件的偏移量和數據索引在有效的範圍內。

構建系統使用進程,強制對所有的DEX文件進行優化,然後從dalvik-cache目錄中提取出來。 這麼做的原因是,在進行優化分析的時候會比在桌面運行工具更加容易明白。

當代碼完成字節調整和對齊,我們的準備工作就做好了。我們在ODEX文件的頭部添加一些提前計算好的數據,然後開始執行優化。雖然我們對校驗和優化更很感興趣,但是我們能需要在初始化準備後插入一步。

dexopt

我們打算校驗和優化DEX文件中的所有類。最簡單和安全的方式是將所有的類導入虛擬機中並且全部運行。任何加載失敗的類將不會被校驗和優化。不幸的是,這會導致一些分配的資源難以被回收(例如:已經加載的本地共享庫SO),因此我們不想在運行應用的同一個虛擬機內執行這個步驟。

解決方案是通過運行一個叫做dexopt的程序,它僅是虛擬機的一個後門。它通過執行一個簡化的虛擬機初始化,從系統引導類路徑中加載0個或者幾個DEX文件,然後根據目標DEX文件來設置校驗和優化信息的相關參數。一旦任務完成進程就會退出釋放所有的資源。

可能出現多個虛擬機在同一個時間需要同一個DEX文件。可以通過文件鎖來確保dexop只會執行一次。

Verification

字節碼的校驗過程會掃描每一個類的每一個方法中的所有指令。目的是爲了識別出所有非法的指令,這樣我們就不必在App運行的時候檢查他們。大量的計算會對於精確的垃圾回收來說是必須的。更多相關的詳細信息 Dalvik Bytecode Verifier Notes

由於性能的原因,優化器(將會在下一個章節介紹)會假設校驗的執行時成功的,並且會進行一些潛在不安全的假設。默認情況下,Dalvik虛擬機會仍然會校驗所有的類,僅優化那些被校驗通過的類。如果你想禁用校驗器,你可以通過命令行參數來實現。更多Android應用框架中相關特性的命令Controlling the Embedded VM

上報校驗錯誤是一個相當麻煩的問題。例如,對其他包中方法訪問級別爲僅包內的調用是非法的並且會被校驗器捕獲。我們並不一定要在校驗的過程中進行錯誤上報,實際上我們更希望在方法調用的時候拋出一個異常。檢查每一個方法的訪問權限的代價是
昂貴的。Controlling the Embedded VM 中解決了這個問題。

那些被校驗成功的類文件在ODEX文件中會被設置一個標誌。在加載的時候它們將不會再被校驗。Linux的訪問權限會阻止他們被篡改;如果你能繞過這些訪問機制,安裝錯誤的字節碼遠非最容易的攻擊方式。ODEX文件擁有一個32位的檢查碼,但它僅是爲了快速的校驗文件是否損壞。

Optimization

虛擬機解釋器通常會在代碼被第一次使用的時候執行優化。常量池引用被替換成內部數據結構的指針,總是會執行成功的操作或者經過某些方式能正常工作的會被替換成更簡單的方式。一些替換需要的信息僅在運行時才能獲取,另一些信息則可以通過一定假設進行靜態推斷。

Dalvik 優化器主要執行以下步驟:

  • 對於虛方法的調用,將方法索引替換成vtable方法表中的索引。
  • 對於對象屬性的 get/put,將屬性索引成對象內存地址的字節偏移量。同時將 boolean / byte / chat / short
    合併到同一個32位單元中(解析器中的代碼也少,則CPU緩存中的空間就也大)。
  • 將那些會大量被調用的方法進行內聯,例如: String.length()。這就減少了方法調用所需要的開銷,直接從解釋器切換到native的實現。
  • 剔除空方法的調用。最簡單的例子就是 Object.<init>,這個方法本身不執行任何代碼,但是在任何對象創建的時候都會被調用。
  • 附加預先計算好的數據。例如,虛擬機需要擁有一個查詢類名的哈希表。我們可以現在計算好數據,而不是在DEX文件被加載的時候才做,這樣可以使得每一個加載該DEX文件的虛擬機節省內存空間和計算時間。

所有的指令修改都會涉及到替換成一個沒有在定義Dalivk虛擬機規範中定義的操作碼。這允許我們可以自由的組合優化的指令和沒有優化的指令。這些優化指令的具體表現是和虛擬機版本緊密相關的。

大多數的指令優化是顯然是有益的。使用原生的索引和直接偏移量不僅使得運行更加快速,並且我們可以跳過初始化符號解析。預先計算數據需要佔用存儲空間,因此我需要適度的進行。

這些指令優化也是很多潛在麻煩的根源。
第一,vtable表索引和字節偏移量在虛擬機更新的時候可能會發生變化。
第二,如果父類是在不同的DEX文件中,當父類的DEX文件更新的時候,我們需要確保我們優化的索引和字節偏移量也正常的更新。當用戶使用自定義的類加載器的時候會引起一個相似但更加棘手的問題:實際我們調用的類可能並不是我們期望的。

以上的問題可以通過來增加 依賴列表 和 優化限制 來解決。

Dependencies and Limitations

優化的DEX文件中包含對其他DEX文件依賴的列表,以及原始歸檔文件中classes.dex節點的CRC-32和文件的最後修改時間。依賴列表包含dalvik-cache目錄中文件的完整路徑和文件的 SHA-1摘要。在某些設備上,文件的時間戳是不可靠,不能使用的。依賴信息還包含了虛擬機的版本號。

一個優化的DEX會依賴引導類路徑上所有的DEX文件。而引導類路徑的DEX文件又會依賴更底層的DEX文件。爲了確保依賴列表以外的DEX文件是不可用的,dexopt僅加載引導類路徑上的類。對其他DEX文件裏面的類引用失敗,會導致類加載和建議的失敗,並且類引用的外部依賴是沒有被優化的。

這意味着將代碼拆分到多個DEX文件中會有一個缺點:對非引導類路徑上DEX文件的虛方法調用和實例屬性查找將不會給優化。由於校驗的成功或者失敗是類級別的,因此類中的方法依賴了外部DEX文件中的類將不會被優化。這可能有點過重,但這是確保當外部依賴文件更新的時候不會出錯的唯一方式。

另一個不好的結果:任何一個引導類路徑上的DEX文件變更將會導致所有優化後的DEX文件失效。這使得難以保持系統的小更新。

儘管我們很謹慎,但由於用戶自定義類加器加載的DEX文件中的類仍有可能請求加載引導類路徑的類(例如:String),然後返回一個相同名字的不同於引導類路徑下的類實例。如果一個正在被處理DEX文件中的類和引導類路徑下的DEX裏面的類擁有相同的名字,這個類會被被標記成有歧義的,並且引用這個類的其他類在校驗和優化階段將不會被解析。這個類在虛擬機鏈接代碼時會額外的檢查,詳細信息可以查看VM源中的詳細描述(vm/oo/Class.c)。

如果其中一個依賴的DEX文件更新了,我們需要重新校驗和優化DEX文件。如果我們可以實時調用dexopt優化,那將會變得很簡單。如果我們不得不依賴安裝服務或者DEX文件僅在ODEX文件中提供,那虛擬機將不得不拒絕這個文件。

dexopt的輸出文件是根據主機信息進行字節調整和結構對齊的,並且包含了高度虛擬機(版本和平臺)定製的索引和偏移量。編寫一個能夠在桌面運行生成適合所有機器的dexopt版本是很困難的。最安全的方式是在目標機器或者在設備的模擬器上運行。

Generated DEX

一些語言和框架依賴於生成字節碼並執行它的能力。繁重的dexopt校驗和優化模式使得這些語言和框架難以正常工作。

我們打算在將來的版本中提供支持,但是具體的實現方法還未確定。我們可能允許添加獨立的類或者整個DEX文件;可能允許指令中含有Java字節碼或者Dalvik字節碼;可能會執行通用的優化或者使用獨立的解釋器在代碼第一次執行時進行優化(這些優化將不會被映射成read-only, 因爲它是一種本地定義)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章