在 Android/Dalvik 環境引入 precompiled class 的實驗

延續筆者去年的紀錄「當 Compiler 遇上 Mobile」,最近我們又獲得一些進展,是在 Android 的 Dalvik 虛擬機器環境中,引入 pre-compiled class 的實驗,除了可改善執行時期的效能、記憶體使用量外,另外就是避免在「反組譯並修改 Android 應用程式實例」一文可見的資訊保護議題。

其實十幾年前,在 Java 平台中就有相當多團隊提出可行的方案,而世界上第一個開放原始碼的 Kaffe 虛擬機器專案,早在 1999 年即提出實做 "Kaffe/GCJ integration",成功地將 GCJ (GCC for Java) 自 Java class/source 編譯得到的機械碼,當作 pre-compiled shared library,讓 Kaffee VM 讀取,預期可有效改善執行效能並縮減起始時間。GNU Classpath 專案主持人 Mark Wielaard 在 LWN 的文章 "GCJ - past, present, and future" 提到相關的歷史與技術背景,最早可回溯到 Cygnus solutions 時期 (RedHat 尚未併購) 的 GCJ 計畫提案 -- "A Gcc-based Java Implementation"。

為了降低實做的複雜度,筆者最早採用 LLVMIcedTea 作為系統框架,不過一直到今年春節,整體進度還是陷入膠著的狀態。現在則引入 Java2C 搭配 PGO (Profile-Guided Optimization) 與統計模型,嘗試分析運行於 Dalvik VM 的 Android activity / system server,佔用系統資源最頻繁的項目,並預先編譯 (pre-compile) 這些 class/method,居中透過 JNI (Java Native Interface) 讓 Dalvik VM 存取。不過,實務上來說,不是將所有 class/method 都編譯為原生機械碼,就會帶來效能提昇,相反地,後者往往讓系統陷入更差的效能。目前 Eclair 裡面的 Dalvik VM 雖然沒有完整的 JIT compiler,但其 fast interpreter for ARM 的確做了頗多 threaded interpreter 的改進,考量到 I-cache, paging, 與 branch prediction,若我們不思量 Java / Android Dalvik 應用程式的執行時期行為,只是一味作編譯轉換,很可能只是編譯極少被執行,或者完全不會被執行到的程式碼,因此加重執行時期的負擔,而最該被優化的部份,受限於不完整的 source-to-source 轉換,有顧此失彼的疑慮。

於是,找出系統中最該被優化的部份,並考量到正確性與相容度,就是我們的首要研究議題,筆者採用知名的 SciMark 2.0 作為量化分析的指標。以下是在 Qualcomm MSM7x25 硬體平台 (arm1136j-s) 的效能評比,ARM11 的時脈是 528 MHz,ARM Linux 版本為 2.6.29.6-0xlab:
由上可見,在 SciMark 2.0 的各項評比中,Pre-compiled class 執行效能都較 Android Dalvik fast interpreter for ARM 給予一定程度的改良,並且透過 FDO (Feedback Directed Optimization),大多可進一步給予提昇。這僅是初步的整合實驗數據,還需要更多分析與進行實做。

read on
Posted 張貼者: jserv 於 凌晨1:01 on 2010年5月26日 星期三 | 8 意見
Filed under: , , , , , ,
 

反組譯並修改 Android 應用程式實例

為了某個實驗的動機,我們評估反編譯 Android 應用程式的可行性,本文即是筆者的心得與實際的範例,僅供參考。就筆者的認知,目前還沒有針對 Android 的 DEX to Java source 反編譯工具,可實際處理一般的 Android 應用程式,多半要繞幾圈,還會得到不甚理想的結果,不過,smali 這個反組譯工具,已是可用了,只是得對付類似 Jasmin 語法的 Dalvik 組合語言。筆者打包了 smaliFrozen Bubble for Android,作為示範:

在 GNU/Linux 環境中,首先取得 Android SDK,這裡採用 Eclair/2.1,工具執行檔的路徑是 android-sdk-linux_86/tools,將此路徑放入 $PATH 環境變數,如此一來,就可以操作 adb, aapt, apkbuilder 一類的工具程式。筆者提供的套件已包含 Frozen Bubble 執行檔,檔名為 "FrozenBubble-orig.apk"。一旦 Android emulator 啟動後,即可安裝進去執行:(後續的操作也需要 Emulator 持續開啟)
$ adb install -r FrozenBubble-orig.apk
以下是執行畫面:
當然,沒必要讓筆者置喙談如何玩這個經典遊戲,不過我們倒是想更改原本的行為。在進行之前,我們先來複習 Android APK 的建立,參考 "How to build Android application package (.apk) from the command line using the SDK tools + continuously integrated using CruiseControl." 一文,我們可從以下圖表知悉細部的流程:

假設我們完全無法取得原始程式碼,該如何進行呢?沒錯,就透過 smali,簡化繁瑣的流程,筆者包裝為 Makefile,所以只要先解開並反組譯:
$ make extract
這時候會看到兩個目錄:
  • smali-src : 存放反組譯的程式檔輸出
  • workspace : 原本 Frozen Bubble 的 Android resource files
二話不說,當然是觀察反組譯的結果:
smali-src$ find
./org/jfedor/frozenbubble/FrozenBubble.smali
./org/jfedor/frozenbubble/R$id.smali
./org/jfedor/frozenbubble/GameView.smali
./org/jfedor/frozenbubble/SoundManager.smali
./org/jfedor/frozenbubble/LaunchBubbleSprite.smali
./org/jfedor/frozenbubble/Compressor.smali
./org/jfedor/frozenbubble/R$attr.smali
./org/jfedor/frozenbubble/BubbleFont.smali
./org/jfedor/frozenbubble/PenguinSprite.smali
./org/jfedor/frozenbubble/GameView$GameThread.smali
./org/jfedor/frozenbubble/BubbleSprite.smali./org/jfedor/frozenbubble/R$string.smali
./org/jfedor/frozenbubble/R$drawable.smali
./org/jfedor/frozenbubble/ImageSprite.smali
./org/jfedor/frozenbubble/BubbleManager.smali
./org/jfedor/frozenbubble/GameScreen.smali
./org/jfedor/frozenbubble/R.smali
./org/jfedor/frozenbubble/R$layout.smali
./org/jfedor/frozenbubble/BmpWrap.smali./org/jfedor/frozenbubble/FrozenGame.smali
./org/jfedor/frozenbubble/Sprite.smali
./org/jfedor/frozenbubble/LevelManager.smali
./org/jfedor/frozenbubble/R$raw.smali
就忠實地依據 Java package 的方式呈現,檔名結尾是 ".smali"。筆者的修改目標是,讓一開始的關卡 (Level) 從第一關直接跳躍到第五關。在 smali 原始程式碼 (注意:這完全不同於 Java 原始程式碼,而是極為貼近 Dalvik VM 所接受的 DEX 檔案的組合語言形式) 搜尋 "Level" 字眼,可發現主要的分佈就落於兩個檔案:
  • ./org/jfedor/frozenbubble/GameView$GameThread.smali
  • ./org/jfedor/frozenbubble/LevelManager.smali
前者就是 org/jfedor/frozenbubble/GameView.java 的編譯輸出,因為是 inner class,獨立編譯出 org/jfedor/frozenbubble/GameView.class$GameThread.smali,由這個 class 命名方式大概就可猜出其重要性,基本上只要能夠控制此 class 的實做,就掌握此 Android 應用程式的行為。而 class LevelManager 顧名思義,看來掌握了遊戲進行的程序控制。先觀察其 method 列表:
smali-src$ grep "\.method" org/jfedor/frozenbubble/LevelManager.smali
.method public constructor <init>([BI)V
.method private getLevel(Ljava/lang/String;)[[B
.method public getCurrentLevel()[[B
.method public getLevelIndex()I
.method public goToFirstLevel()V
.method public goToNextLevel()V.method public restoreState(Landroid/os/Bundle;)V
.method public saveState(Landroid/os/Bundle;)V
倘若我們以 "goToFirstLevel" 一類的關鍵字,在 org/jfedor/frozenbubble/GameView$GameThread.smali 檔案中搜尋,可找出有具體的呼叫行為:
smali-src$ grep -r goToFirstLevel *
org/jfedor/frozenbubble/GameView$GameThread.smali: invoke-virtual {v2}, Lorg/jfedor/frozenbubble/LevelManager;->goToFirstLevel()V
org/jfedor/frozenbubble/LevelManager.smali:.method public goToFirstLevel()V
由此更確定我們之前的猜想。其中組合語言寫為以下:
move-object/from16 v0, p0

iget-object v0, v0, Lorg/jfedor/frozenbubble/GameView$GameThread;->mLevelManager:Lorg/jfedor/frozenbubble/LevelManager;

move-object v2, v0

invoke-virtual {v2}, Lorg/jfedor/frozenbubble/LevelManager;->goToFirstLevel()V
不要被貌似複雜的語法嚇到了,基本上掌握 Java 程式語言的原則 "Everything is Object" (不過仍有提供 primitive type),組合語言仍會作 Java Object 的實體化 (instantialization),Dalvik 本身是 Register-based Virtual Machine,而扣除 static/class method 外,Java 中所有的 method invocation 多為 virtual function (對應於 C++ 的觀點,才能更具體用機械方式思考),所以組合語言的指令為 "invoke-virtual" (注意有連字號,此與 Java bytecode 不同),"{v2}" 表示第一受者的 Register,此為 Object 本體。接著,與 Java bytecode 一樣,"Lorg/jfedor/frozenbubble/LevelManager;" 就表示 Java 層面的 class "org.jfedor.frozenbubble.LevelManager",字母 "L" 即為 class 的識別,而 "->" 就很直觀了,自然是 method invocation,所以連貫來看,這一段組合語言的 Java 意思為:
objectLevelManager.goToFirstLevel();
其中 objectLevelManager 是一個 class LevelManager 的實例/實體 (instance)。倘若需要在 method invocation 時,帶入參數,那麼前述的 "{v2}" 一般會被替換為 "{v0, v1, v2, ...} 的 register 列表。關於詳細的狀況,可參考 Dalvik 非官方說明,另外 smali 的 wiki 也提供一些範例,可多加利用。

回到筆者剛剛設定的目標,我們既然知道 class org.jfedor.frozenbubble.GameView$GameThread 掌控了程式處理邏輯,自然一堆變數的傳遞、method 呼叫,都在此進行,那我們先試著用 "level" 字串去搜尋,想辦法找出常數定義,後者在 Dalvik 中,會集中保存於 constant pool 中,而 smali 的組合語言寫法大致是 "const" 開頭的宣告,端看其類型而定。以程式追蹤的目的來說,我們專注於以下兩種:
  • const-string : primitive string (不同於 java.lang.String) 表示
  • const/4 : 長度為 4 bytes (32 bit) 的整數表示
字串何其多,依據之前的推測來說,我們要找接近 "LevelManager" 字眼的組合語言程式碼,道理很明顯,從一般 Java programmer 的寫作邏輯去反推。經過一番搜尋,我們找到以下的反組譯程式碼: (位於檔案 org/jfedor/frozenbubble/GameView$GameThread.smali中)
const-string v3, "level"
const/4 v4, 0x0
move-object/from16 v0, v25 move-object v1, v3
move v2, v4
invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getInt(Ljava/lang/String;I)I
move-result p4
new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;
move-object v0, v3
move-object/from16 v1, v22
move/from16 v2, p4 invoke-direct {v0, v1, v2}, Lorg/jfedor/frozenbubble/LevelManager;-><init>([BI)V
在上述程式碼列表中,"Lorg/jfedor/frozenbubble/LevelManager;-><init>" 表示呼叫 class LevelManager 的 constructor,也就是 "<init>"。注意到 method invocation 方式就不同了,是 "invoke-direct",表示 class constructor,而這之前要有 "new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;" 的組合語言指令宣告。

前面談過「倘若需要在 method invocation 時,帶入參數,一般會被替換為 "{v0, v1, v2, ...} 的 register 列表」這樣的概念,我們可推知,Register v1 與 v2 就是實際上 class org.jfedor.frozenbubble.LevelManager 的 constructor 參數。就程式設計的邏輯來看,class GameView 就是依據某個流程,要求 LevelManager 去改變狀態,所以這裡的兩個參數,其實就是初始值,非常的重要。

與 Register v1 相關的組合語言指令有這幾行:(用粗體字標示)
const-string v3, "level"
const/4 v4, 0x0
move-object/from16 v0, v25
move-object v1, v3
move v2, v4
invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getInt(Ljava/lang/String;I)I
move-result p4
new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;
move-object v0, v3
move-object/from16 v1, v22
move/from16 v2, p4
invoke-direct {v0, v1, v2}, Lorg/jfedor/frozenbubble/LevelManager;-><init>([BI)V
顯然,Register v1 還被帶入到 android.content.Shared.Preference.getInt() method,而更早以前,其內含值被設定為 Register v3 的值,也就是常數字串 (const-string) "level",這好像與我們的焦點不同。另外,像是 Register v22 這個編號較大的 register,表示 local variable,這點需要多留意,因為 Java 程式設計的規範來說,往往會將程式切割為若干 method,而 method 實做體中,又有極多的 local variable,於是往往可從組合語言反推 Java 原始碼的型態。

那麼,看看 Register v2 吧,同樣用粗體字標示相關的指令:
const-string v3, "level"
const/4 v4, 0x0
move-object/from16 v0, v25
move-object v1, v3
move v2, v4
invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getInt(Ljava/lang/String;I)I
move-result p4
new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;
move-object v0, v3
move-object/from16 v1, v22
move/from16 v2, p4
invoke-direct {v0, v1, v2}, Lorg/jfedor/frozenbubble/LevelManager;-><init>([BI)V
這個 Register v4 的內含值 "0x0" 會指派到 Register v2 中,而讓我們似乎找到方向了,回頭看看 class org.jfedor.frozenbubble.LevelManager 的 constructor 宣告方式: (之前 grep 結果的第一行)
smali-src$ grep "\.method" org/jfedor/frozenbubble/LevelManager.smali
.method public constructor <init>([BI)V
其中 "public" 是 ACL (存取權限) 的宣告,而 constructor 的符號規範為 "<init>",注意到括號 "(" 與 ")" 裡面的兩個大寫字母,表示接受兩個參數,對應的型態為:
  • B : byte
  • I : int
在 Java 與 Dalvik virtual machine 皆以同樣的形式作宣告,看到這裡,我們實在忍不住要動手修改了,當然,一切都是實驗性質。既然一開啟遊戲,畫面就顯示 Level 1,表示 LevelManager 一開始接受的 level 參數會是 "0",然後依據 GameView 的邏輯,逐一升級或中斷遊戲進行,那麼,如果要讓遊戲一起動就是 Level 5,是不是要把內含值改為 0x4 即可?也就是 "invoke-direct {v0, v1, v2}, Lorg/jfedor/frozenbubble/LevelManager;-><init>([BI)V" 的 Register v2 當時內含值該為 0x4,不就任務達成了嗎?想到此,不禁會心一笑。

不過,回顧稍早 Register v2 的相關程式碼輸出,其中有兩行需要留意 (以粗體字為主):
invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getInt(Ljava/lang/String;I)I
move-result p4
new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;
move-object v0, v3
move-object/from16 v1, v22
move/from16 v2, p4
"p4" 用以保存 method invocation 之後的回傳值,顯然,Register v2 受到 p4 的指派,也就是被更動為 android.content.Shared.Preference.getInt() method 的回傳值,這存在不確定性,於是,我們乾脆一口氣改掉: (修改的部份會用井字號 "#" 作註解)
# Modified from 0x0 to 0x4"
const/4 v4, 0x4
move-object/from16 v0, v25
move-object v1, v3
move v2, v4
# Modified: removed the following 2 lines
# invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getInt(Ljava/lang/String;I)I
# move-result p4
new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;
move-object v0, v3
move-object/from16 v1, v22
# Modified: removed the following 1 line
# move/from16 v2, p4
invoke-direct {v0, v1, v2}, Lorg/jfedor/frozenbubble/LevelManager;-><init>([BI)V
改好程式,當然要驗證,回到上一層目錄,透過 smali 提供的組譯器,重新產生 Dalvik DEX 輸出,為了簡化流程,筆者把 smali, apkbuilder, aapt, adb install 都一次整合進去,所以會直接讓 Android Emulator 生效,來看看我們的戰果吧:
注意到左下角,這表示我們成功了,完全不用取得 Java 原始程式碼,就可以作反組譯並且修改的動作。

read on
Posted 張貼者: jserv 於 晚上10:08 on 2010年5月21日 星期五 | 4 意見
Filed under: , , , , , ,
 

Android Toolchain 原始程式碼尋寶

很多人從事 Android 移植,但連 Android Toolchain 都自行維護者,相對就少多了。筆者過去有幸在「台灣心」計畫中,從事建構於台灣自主 CPU 的軟體開發,嘗試從零到有,將 Android 移植到 Andes NDS32 架構,就涉及到 Toolchain 的更動。而維護 0xdroid 與對應 0xlab 的 GNU Toolchain,不免會迷失於眾多的原始程式碼,只好靜下心慢慢探索,沒想到因此挖掘到頗多「寶物」,本文簡記備忘使用。

Google 工程師 Jing Yu 於 Jan 18, 2010 提交一份修改 "Bring gcc-4.4.0 to up-to-date",讓人摸不著頭緒,不過其對於 GCC 的 architecture 更動,追加了新項目,可參見檔案 gcc-4.4.0/gcc/config/linux-grtev1.h 。以下節錄開頭註解:

/* Definitions for Linux-based GRTE (Google RunTime Environment) version 1.
Copyright (C) 2009 Free Software Foundation, Inc.
Contributed by Chris Demetriou.

This file is part of GCC.
這個以 Linux 為基礎的 GRTE 到底是什麼呢?沒獲得解答,只好在其 gcc spec 檔中探索,以下是相關的描述:
/* When GRTE links statically, it needs its NSS and resolver libraries
linked in as well. Note that when linking statically, these are
enclosed in a group by LINK_GCC_C_SEQUENCE_SPEC. */
#undef LINUX_GRTE_EXTRA_SPECS
#define LINUX_GRTE_EXTRA_SPECS \
{ "libc", "%{static:%(libc_static);:-lc}" }, \
{ "libc_p", "%{static:%(libc_p_static);:-lc_p}" }, \
{ "libc_static", \
"-lc -lnss_borg -lnss_cache -lnss_dns -lnss_files -lresolv" }, \
{ "libc_p_static", \
"-lc_p -lnss_borg_p -lnss_cache_p -lnss_dns_p -lnss_files_p -lresolv_p" },
這裡的 libc 看來是獨立的 C Library 實做,比較有趣的是 libnss 的部份。libnss 可透過外部的 module 作擴充,比方說 libnss-ldap 就是 "NSS module for using LDAP as a naming service",而前述 gcc spec 提及當進行靜態編譯時,需要將所需的 module 一併連結,那麼,這系列 libnss_ 開頭的函式庫,顯然就是 GRTE (Google RunTime Environment) 所需的基礎建設。來看看有哪些特別的:
  • libnss_borg
  • libnss_cache
或許後者可從名稱猜出一些端倪,但這個 "libnss_borg" 實在是太詭異 (Borg in Star Trek?),跟 GRTE 一樣神秘,我們只好等待 Google 大神給我們一些驚喜。

另外,Android Benchmark Suite 2.0.0 已公開釋出,可參見 benchmark.git,值得一書的是,內建了 FDO (Feedback Directed Optimization) 的編譯、測試,與效能評估機制。

read on
Posted 張貼者: jserv 於 上午11:21 on 2010年5月4日 星期二 | 0 意見
Filed under: , , , ,
 
Developer at 0xlab.