本文摘自人民郵電出版社非同步社群《深入理解Android核心設計思想(第2版)(上下冊)》
購書地址:item.jd.com/12212640.ht…
試讀地址:www.epubit.com.cn/book/detail…
第2章 Android原始碼下載及編譯
在分析Android原始碼前,首先要學會如何下載和編譯系統。本章將向讀者完整地呈現Android原始碼的下載流程、常見問題以及處理方法,並從開發者的角度來理解如何正確地編譯出Android系統(包括原生態系統和定製裝置)。
後面,我們將在此基礎上深入到編譯指令碼的分析中,以“庖丁解牛”的方式來還原一個龐大而嚴謹的Android編譯系統。
2.1 Android原始碼下載指南
2.1.1 基於Repo和Git的版本管理
Git是一種分散式的版本管理系統,最初被設計用於Linux核心的版本控制。本書工具篇中對Git的使用方法、原理框架有比較詳細的剖析,建議讀者先到相關章節閱讀了解。
Git的功能非常強大,速度也很快,是當前很多開源專案的首選工具。不過Git也存在一定的缺點,如相對於圖形介面化的工具沒那麼容易上手、需要對內部原理有一定的瞭解才能很好地運用、不支援斷點續傳等。
為此,Google提供了一個專門用於下載Android系統原始碼的Python指令碼,即Repo。
在Repo環境下,版本修改與提交流程是:
由此可見,Repo與我們在工具篇中討論的Git流程有些許不同,差異主要體現在與遠端服務倉庫的互動上;而本地的開發仍然是以原生的Git命令為主。下面我們講解Repo的一些常用命令,讀者也可以拿它和Git進行仔細比較。
1.同步
同步操作可以讓原生程式碼與遠端倉庫保持一致。它有兩種形式。
如果是同步當前所有的專案:
$ repo sync複製程式碼
或者也可以指定需要同步的某個專案:
$ repo sync [PROJECT1] [PROJECT2]…複製程式碼
2.分支操作
建立一個分支所需的命令:
$ repo start <BRANCH_NAME>複製程式碼
也可以檢視當前有多少分支:
$ repo branches複製程式碼
或者:
$ git branch複製程式碼
以及切換到指定分支:
$ git checkout <BRANCH_NAME>複製程式碼
3.查詢操作
查詢當前狀態:
$ repo status複製程式碼
查詢未提交的修改:
$ repo diff複製程式碼
4.版本管理操作
暫存檔案:
$git add複製程式碼
提交檔案:
$git commit複製程式碼
如果是提交修改到伺服器上,首先需要同步一下:
$repo sync複製程式碼
然後執行上傳指令:
$repo upload複製程式碼
2.1.2 Android原始碼下載流程
瞭解了Repo的一些常規操作後,這一小節接著分析Android原始碼下載的全過程。這既是剖析Android系統原理的前提,也是讓很多新手感到困惑的地方——原始碼下載可以作為初學者瞭解Android系統的“Hello World”。
值得一提的是,Android官方建議我們務必確保編譯系統環境符合以下幾點要求:
在虛擬機器上或是其他不支援的系統(例如Windows)上編譯Android系統也是可能的,事實上Google鼓勵大家去嘗試不同的作業系統平臺。不過Google內部針對Android系統的編譯和測試工作大多是在Ubuntu LTS(14.04)上進行的。因而建議開發人員也都選擇同樣的作業系統版本來開展工作,經驗告訴我們這樣可以少走很多彎路。
如果是在虛擬機器上執行的Linux系統,那麼理論上至少需要16GB的RAM/Swap才有可能完成整個Android系統的編譯。
要特別提醒大家的是,以下所有步驟都是在Ubuntu作業系統中完成的(“#”號後面表示註釋內容)。
1.下載Repo
$ cd ~ #進入home目錄
$ mkdir bin #建立bin目錄用於存放Repo指令碼
$ PATH=~/bin:$PATH #將bin目錄加入系統路徑中
$ curl storage.googleapis.com/git-repo-do… > ~/bin/repo #curl
#是一個基於命令列的檔案傳輸工具,它支援非常多的協議。這裡我們利用curl來將repo儲存到相應目錄下
$ chmod a+x ~/bin/repo複製程式碼
注:網上有很多開發者(中國大陸地區)反映上面的地址經常無法成功訪問。如果讀者也有類似困擾,可以試試下面這個:
$curl android.googlesource.com/repo > ~/bin/repo複製程式碼
另外,國內不少組織(特別是教育機構)也對Android做了映象,如清華大學提供的開源專案(TUNA)的mirror地址如下:
aosp.tuna.tsinghua.edu.cn/複製程式碼
下面是TUNA官方對Android程式碼庫的使用幫助節選:
Android映象使用幫助
參考Google教程source.android.com/source/down… source.com/全部使用git://aosp.tuna.tsinghua.edu.cn/android/代替即可。
本站資源有限,每個IP限制併發數為4,請勿使用repo sync-j8這樣的方式同步。
替換已有的AOSP原始碼的remote。
如果你之前已經通過某種途徑獲得了AOSP的原始碼(或者你只是init這一步完成後),你希望以後通過TUNA同步AOSP部分的程式碼,只需要將.repo/manifest.xml把其中的AOSP這個remote的fetch從https://android. googlesource.com改為git://aosp.tuna.tsinghua.edu.cn/android/。
<manifest>
<remote name="aosp"
- fetch="android.googlesource.com"
+ fetch="git://aosp.tuna.tsinghua.edu.cn/android/"
review="android-review.googlesource.com" />
<remote name="github"
這個方法也可以用來在同步Cyanogenmod程式碼的時候從TUNA同步部分程式碼複製程式碼
下載repo後,最好進行一下校驗,各版本的校驗碼如下所示:
對於 版本 1.17, SHA-1 checksum是:ddd79b6d5a7807e911b524cb223bc3544b661c28
對於 版本 1.19, SHA-1 checksum是:92cbad8c880f697b58ed83e348d06619f8098e6c
對於 版本 1.20, SHA-1 checksum 是:e197cb48ff4ddda4d11f23940d316e323b29671c
對於 版本 1.21, SHA-1 checksum 是:b8bd1804f432ecf1bab730949c82b93b0fc5fede複製程式碼
2.Repo配置
在開始下載原始碼前,需要對Repo進行必要的配置。
如下所示:
$ mkdir source #用於存放整個專案原始碼
$ cd source
$ repo init -u android.googlesource.com/platform/ma…
############以下為註釋部分########
init命令用於初始化repo並得到近期的版本更新資訊。如果你想獲取某個非master分支的程式碼,需要在命令最後加上-b選項。如:
$ repo init -u android.googlesource.com/platform/ma… -b android-4.0.1_r1
完成配置後,repo會有如下提示:
repo initialized in /home/android
這時在你的機器home目錄下會有一個.repo目錄,用於記錄manifest等資訊##########
######複製程式碼
3.下載原始碼
完成初始化動作後,就可以開始下載原始碼了。根據上一步的配置,下載到的可能是最新版本或者某分支版本的系統原始碼。
$ repo sync複製程式碼
由於整個Android原始碼專案非常大,再加上網路等不確定因素,運氣好的話可能1~2個小時就能品嚐到“Android盛宴”;運氣不好的話,估計一個禮拜也未必能完成這一步——如果下載一直失敗的話,讀者也可以嘗試到網上搜尋別人已經下載完成的原始碼包,因為通常在新版本釋出後的第一時間就有熱心人把它上傳到網上了。
可以看到在Repo的幫助下,整個下載過程還是相當簡單直觀的。
提示:如果你在下載過程中出現暫時性的問題(如下載意外中斷),可以多試幾次。如果一直存在問題,則很可能是代理、閘道器等原因造成的。更多常見問題的描述與解決方法,可以參見下面這個網址。
source.android.com/source/know…複製程式碼
典型的repo下載介面如圖2-1所示。
▲圖2-1 原生Android工程的典型下載介面
Android系統本身是由非常多的子專案組成的,這也是為什麼我們需要repo來統一管理AOSP原始碼的一個重要原因,如圖2-2所示(部分)。
▲圖2-2 子專案
另外,不同子專案之間的branches和tags的區別如圖2-3所示。
▲圖2-3 Android各子專案的分支和標籤
(左:frameworks/base,中:frameworks/native,右:/platform/libcore)
當我們使用repo init命令初始化AOSP工程時,會在當前目錄下生成一個repo資料夾,如圖2-4所示。
▲圖2-4 repo檔案
其中manifests本身也是一個Git專案,它提供的唯一檔名為default.xml,用於管理AOSP中的所有子專案(每個子專案都由一個project標籤表示):
另外,default.xml中記錄了我們在初始化時通過-b選項指定的分支版本,例如“android-n-preview-2”:
這樣當執行repo sync命令時,系統就可以根據我們的要求去獲取正確的原始碼版本了。
友情提示:經常有讀者詢問閱讀Android原始碼可以使用哪些工具。除了著名的Source Insight外,另外還有一個名為SlickEdit的IDE也是相當不錯的(支援Windows、Linux和Mac),建議大家可以對比選擇最適合自己的工具。
2.2 原生Android系統編譯指南
任何一個專案在編譯前,都首先需要搭建一個完整的編譯環境。Android系統通常是執行於類似Arm這樣的嵌入式平臺上,所以很可能涉及交叉編譯。
什麼是交叉編譯呢?
簡單來說,如果目標平臺沒有辦法安裝編譯器,或者由於資源有限等無法完成正常的編譯過程,那就需要另一個平臺來輔助生成可執行檔案。如很多情況下我們是在PC平臺上進行Android系統的研發工作,這時就需要通過交叉編譯器來生成可執行於Arm平臺上的系統包。需要特別提出的是,“平臺”這個概念是指硬體平臺和作業系統環境的綜合。
交叉編譯主要包含以下幾個物件。
宿主機(Host):指的是我們開發和編譯程式碼所在的平臺。目前不少公司的開發平臺都是基於X86架構的PC,作業系統環境以Windows和Linux為主。
目標機(Target):相對於宿主機的就是目標機。這是編譯生成的系統包的目標平臺。
交叉編譯器(Cross Compiler):本身執行於宿主機上,用於產生目標機可執行檔案的編譯器。
針對具體的專案需求,可以自行配置不同的交叉編譯器。不過我們建議開發者儘可能直接採用國際權威組織推薦的經典交叉編譯器。因為它們在release之前就已經在多個專案上測試過,可以為接下來的產品開發節約寶貴的時間。表2-1所示給出了一些常見的交叉編譯器及它們的應用環境。
表2-1 常用交叉編譯器及應用環境
交叉編譯器 | 宿 主 機 | 目 標 機 |
---|---|---|
armcc | X86PC(windows),ADS開發環境 | Arm |
arm-elf-gcc | X86PC(windows),Cygwin開發環境 | Arm |
arm-linux-gcc | X86PC(Linux) | Arm |
2.2.1 建立編譯環境
本書所採用的宿主機是X86PC(Linux),通過表2-1可知在編譯過程中需要用到arm-linux-gcc交叉編譯器(注:Android系統工程中自帶了交叉編譯工具,只要在編譯時做好相應的配置即可)。
接下來我們分步驟來搭建完整的編譯環境,並完成必要的配置。所選取的宿主機作業系統是Ubuntu的14.04版本LTS(這也是Android官方推薦的)。為了不至於在編譯過程中出現各種意想不到的問題,建議大家也採用同樣的作業系統環境來執行編譯過程。
Step1. 通用工具的安裝
表2-2給出了所有需要安裝的通用工具及它們的下載地址。
表2-2 通用編譯工具的安裝及下載地址
通 用 工 具 | 安裝地址、指南 | |
Python 2.X | www.python.org/download/ | |
GNU Make 3.81 -- 3.82 | ftp.gnu.org/gnu/make/ | |
JDK | Java 87 針對Kitkat以上版本 | 最新的Android工程已經改用OpenJDK,並要求為Java 87及以上版本。這點大家應該特別注意,否則可能在編譯過程中遇到各種問題。具體安裝方式見下面的描述 |
JDK 6 針對Gingerbread到Kitkat之間的版本 | java.sun.com/javase/down… | |
JDK 5 針對Cupcake到Froyo之間版本 | ||
Git 1.7以上版本 | git-scm.com/download |
對於開發人員來說,他們習慣於通過以下方法安裝JDK(如果處於Ubuntu系統下):
Java 6:
$ sudo add-apt-repository "deb archive.canonical.com/ lucid partner"
$ sudo apt-get update
$ sudo apt-get install sun-java6-jdk複製程式碼
Java 5:
$ sudo add-apt-repository "deb archive.ubuntu.com/ubuntu hardy main multiverse"
$sudo add-apt-repository "deb archive.ubuntu.com/ubuntu hardy-updates main
multiverse"
$ sudo apt-get update
$ sudo apt-get install sun-java5-jdk複製程式碼
但是隨著Java的版本變遷及Sun(已被Oracle收購)公司態度的轉變,目前獲取Java的方式也發生了很大變化。基於版權方面的考慮(大家應該已經聽說了Oracle和Google之間的官司恩怨),Android系統已經將Java環境切換到了OpenJDK,安裝步驟如下所示:
$ sudo apt-get update
$ sudo apt-get install openjdk-8-jdk複製程式碼
首先通過上述命令install OpenJDK 8,成功後再進行如下配置:
$ sudo update-alternatives --config java
$ sudo update-alternatives --config javac複製程式碼
如果出現Java版本錯誤的問題,make系統會有如下提示:
**
You are attempting to build with the incorrect version
of java.
Your version is: WRONG_VERSION.
The correct version is: RIGHT_VERSION.
Please follow the machine setup instructions at
source.android.com/source/down…
**複製程式碼
Step2. Ubuntu下特定工具的安裝
注意,這一步中描述的安裝過程是針對Ubuntu而言的。如果你是在其他作業系統下執行的編譯,請參閱官方文件進行正確配置;如果你是在虛擬機器上執行的Ubuntu系統,那麼請至少保留16GB的RAM/SWAP和100GB以上的磁碟空間,這是完成編譯的基本要求。
$ sudo apt-get install bison g++-multilib git gperf libxml2-utils make zlib1g-
dev:i386 zip複製程式碼
所需的命令如下:
$ sudo apt-get install git gnupg flex bison gperf build-essential \
zip curl libc6-dev libncurses5-dev:i386 x11proto-core-dev \
libx11-dev:i386 libreadline6-dev:i386 libgl1-mesa-glx:i386 \
libgl1-mesa-dev g++-multilib mingw32 tofrodos \
python-markdown libxml2-utils xsltproc zlib1g-dev:i386
$ sudo ln -s /usr/lib/i386-linux-gnu/mesa/libGL.so.1 /usr/lib/i386-linux-gnu/libGL.so複製程式碼
需要安裝的程式比較多,不過我們還是可以通過apt-get來輕鬆完成。
具體命令如下:
$ sudo apt-get install git-core gnupg flex bison gperf build-essential \
zip curl zlib1g-dev libc6-dev lib32ncurses5-dev ia32-libs \
x11proto-core-dev libx11-dev lib32readline5-dev lib32z-dev \
libgl1-mesa-dev g++-multilib mingw32 tofrodos python-markdown \
libxml2-utils xsltproc複製程式碼
注意,如果以上命令中存在某些包找不到的情況,可以試試以下命令:
$ sudo apt-get install git-core gnupg flex bison gperf libsdl-dev libesd0-dev libwxgtk2.6-dev build-essential zip curl libncurses5-dev zlib1g-dev openjdk-6-jdk ant gcc-multilib g++-multilib複製程式碼
如果你的作業系統剛好是Ubuntu 10.10,那麼還需要:
$ sudo ln -s /usr/lib32/mesa/libGL.so.1 /usr/lib32/mesa/libGL.so複製程式碼
如果你的作業系統剛好是Ubuntu 11.10,那麼還需要:
$ sudo apt-get install libx11-dev:i386複製程式碼
Step3. 設立ccache(可選)
如果你經常執行“make clean”,或者需要經常編譯不同的產品類別,那麼ccache還是有用的。它可以作為編譯時的緩衝,從而加快重新編譯的速度。
首先,需要在.bashrc中加入如下命令。
export USE_CCACHE=1複製程式碼
如果你的home目錄是非本地的檔案系統(如NFS),那麼需要特別指定(預設情況下它存放於~/.ccache):
export CCACHE_DIR=<path-to-your-cache-directory>複製程式碼
在原始碼下載完成後,必須在原始碼中找到如下路徑並執行命令:
prebuilt/linux-x86/ccache/ccache -M 50G
#推薦的值為50-100GB,你可以根據實際情況進行設定複製程式碼
Step4. 配置USB訪問許可權
USB的訪問許可權在我們對實際裝置進行操作時是必不可少的(如下載系統程式包到裝置上)。在Ubuntu系統中,這一許可權通常需要特別的配置才能獲得。
可以通過修改/etc/udev/rules.d/51-android.rules來達到目的。
例如,在這個檔案中加入以下命令內容:
# adb protocol on passion (Nexus One)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e12", MODE="0600", OWNER
="<username>"
# fastboot protocol on passion (Nexus One)
SUBSYSTEM=="usb", ATTR{idVendor}=="0bb4", ATTR{idProduct}=="0fff", MODE="0600", OWNER
="<username>"
# adb protocol on crespo/crespo4g (Nexus S)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e22", MODE="0600", OWNER
="<username>"
# fastboot protocol on crespo/crespo4g (Nexus S)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e20", MODE="0600", OWNER
="<username>"
# adb protocol on stingray/wingray (Xoom)
SUBSYSTEM=="usb", ATTR{idVendor}=="22b8", ATTR{idProduct}=="70a9", MODE="0600", OWNER
="<username>"
# fastboot protocol on stingray/wingray (Xoom)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="708c", MODE="0600", OWNER
="<username>"
# adb protocol on maguro/toro (Galaxy Nexus)
SUBSYSTEM=="usb", ATTR{idVendor}=="04e8", ATTR{idProduct}=="6860", MODE="0600", OWNER
="<username>"
# fastboot protocol on maguro/toro (Galaxy Nexus)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e30", MODE="0600", OWNER
="<username>"
# adb protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d101", MODE="0600", OWNER
="<username>"
# fastboot protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d022", MODE="0600", OWNER
="<username>"
# usbboot protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d00f", MODE="0600", OWNER
="<username>"
# usbboot protocol on panda (PandaBoard ES)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d010", MODE="0600", OWNER
="<username>"複製程式碼
如果嚴格按照上述4個步驟來執行,並且沒有任何錯誤——那麼恭喜你,一個完整的Android編譯環境已經搭建完成了。
2.2.2 編譯流程
上一小節我們建立了完整的編譯環境,可謂“萬事俱備,只欠東風”,現在就可以執行真正的編譯操作了。
下面內容仍然採用分步的形式進行講解。
Step1. 執行envsetup指令碼
指令碼檔案envsetup.sh記錄著編譯過程中所需的各種函式實現,如lunch、m、mm等。你可以根據需求進行一定的修改,然後執行以下命令:
$ source ./build/envsetup.sh複製程式碼
也可以用點號代替source:
$ . ./build/envsetup.sh複製程式碼
Step2. 選擇編譯目標
編譯目標由兩部分組成,即BUILD和BUILDTYPE。表2-3和表2-4給出了詳細的解釋。
表2-3 BUILD引數詳解
BUILD | 設 備 | 備 注 |
---|---|---|
Full | 模擬器 | 全編譯,即包括所有的語言、應用程式、輸入法等 |
full_maguro | maguro | 全編譯,並且執行於 Galaxy Nexus GSM/HSPA+ ("maguro") |
full_panda | panda | 全編譯,並且執行於 PandaBoard ("panda") |
可見BUILD可用於描述不同的目標裝置。
表2-4 BUILDTYPE引數詳解
BUILDTYPE | 備 注 |
---|---|
User | 編譯出的系統有一定的許可權限制,通常用來發布最終的上市版本 |
userdebug | 編譯出的系統擁有root許可權,通常用於除錯目的 |
Eng | 即engineering版本 |
可見BUILDTYPE可用於描述各種不同的編譯場景。
選擇不同的編譯目標,可以使用以下命令:
$ lunch BUILD-BUILDTYPE複製程式碼
如我們執行命令“lunch full-eng”,就相當於編譯生成一個用於工程開發目的,且執行於模擬器的系統。
如果不知道有哪些產品型別可選,也可以只敲入“lunch”命令,這時會有一個列表顯示出當前工程中已經配置過的所有產品型別(後續小節會講解如何新增一款新產品);然後可以根據提示進行選擇,如圖2-5所示。
▲圖2-5 使用“lunch”來顯示所有產品
Step3. 執行編譯命令
最直接的就是輸入如下命令:
$ make複製程式碼
對於2.3以下的版本,整個編譯過程在一臺普通計算機上需要3小時以上的時間。而對於JellyBean以上的專案,很可能會花費5小時以上的時間(這取決於你的宿主機配置)。
如果希望充分利用CPU資源,也可以使用make選項“-jN”。N的值取決於開發機器的CPU數、每顆CPU的核心數以及每個核心的執行緒數。
例如,你可以使用以下命令來加快編譯速度:
$ make –j4複製程式碼
有個小技巧可以為這次編譯輕鬆地打上Build Number標籤,而不需要特別更改指令碼檔案,即在make之前輸入如下命令:
$ export BUILD_NUMBER=${USER}-'date +%Y%m%d-%H%M%S'複製程式碼
在定義BUILD_NUMBER變數值時要特別注意容易引起錯誤的符號,如“$”“&”“:”“/”“\”“<”“>”等。
這樣我們就成功編譯出Android原生態系統了——當然,上面的“make”指令只是選擇預設的產品進行編譯。假如你希望針對某個特定的產品來執行,還需要先通過上一小節中的“lunch”進行相應的選擇。
接下來看看如何編譯出SDK。這是很多開發者,特別是應用程式研發人員所關心的。因為很多時候通過SDK所帶的模擬器來除錯APK應用,比在真機上操作要來得高效且便捷;而且模擬器可以配置出各種不同的螢幕引數,用以驗證應用程式的“適配”能力。
SDK是執行於Host機之上的,因而編譯過程根據宿主作業系統的不同會有所區別。詳細步驟如下:
Mac OS和Linux
(1)下載原始碼,和前面已經講過的原始碼下載過程沒有任何區別。
(2)執行envsetup.sh。
(3)選擇SDK對應的產品。
$ lunch sdk-eng複製程式碼
提示:如果通過“lunch”沒有出現“sdk”這個種類的產品也沒有關係,可以直接輸入上面的命令。
(4)最後,使用以下命令進行SDK編譯:
$ make sdk複製程式碼
Windows
執行於Windows環境下的SDK編譯需要基於上面Linux的編譯結果(注意只能是Linux環境下生成的結果,而不支援MacOS)。
(1)執行Linux下SDK編譯的所有步驟,生成Linux版的SDK。
(2)安裝額外的支援包。
$ sudo apt-get install mingw32 tofrodos複製程式碼
(3)再次執行編譯命令,即:
$ . ./build/envsetup.sh
$ lunch sdk-eng
$ make win_sdk複製程式碼
這樣我們就完成Windows版本SDK的編譯了。
當然上面編譯SDK的過程也同樣可以利用多核心CPU的優勢。例如:
$ make -j4 sdk複製程式碼
面向Host和Target的編譯結果都存放在原始碼工程out目錄下,分為兩個子目錄。
host:SDK生成的檔案存放在這裡。例如:
MacOS
out/host/darwin-x86/sdk/android-sdk_eng.<build-id>_mac-x86.zip
Windows
out/host/windows/sdk/android-sdk_eng.${USER}_windows/
target:通過make命令生成的檔案存放在這裡。
另外,啟動一個模擬器可以使用以下命令。
$ emulator [OPTIONS]複製程式碼
模擬器提供的啟動選項非常豐富,讀者可以參見本書工具篇中的詳細描述。
2.3 定製產品的編譯與燒錄
上一小節我們學習了原生態Android系統的編譯步驟,為大家進一步理解定製裝置的編譯流程打下了基礎。Android系統發展到今天,已經在多個產品領域得到了廣泛的應用。相信有一個問題是很多人都想了解的,那就是如何在原生態Android系統中新增自己的定製產品。
2.3.1 定製新產品
仔細觀察整個Android原始碼專案可以發現,它的根目錄下有一個device資料夾,其中又包含了諸如samsung、moto、google等廠商名錄,如圖2-6所示。
▲圖2-6 device資料夾下的廠商目錄
在Android編譯系統中新增一款裝置的過程如下。
Step 1. 和圖2-6所列的各廠商一樣,我們也最好先在device目錄下新增一個以公司命名的資料夾。當然,Android系統本身並沒有強制這樣做(後面會看到vendor目錄也是可以的),只不過規範的做法有利於專案的統一管理。
然後在這個公司名目錄下為各產品分別建立對應的子資料夾。以samsung為例,其資料夾中包含的產品如圖2-7所示。
▲圖2-7 一個廠商通常有多種產品
完成產品目錄的新增後,和此專案相關的所有特定檔案都應該優先放置到這裡。一般的組織結構如圖2-8所示。
▲圖2-8 device目錄的組織架構
由圖2-8最後一行可以看出,一款新產品的編譯需要多個配置檔案(sh、mk等)的支援。我們按照這些檔案所處的層級進行一個系統的分類,如表2-5所示。
表2-5 定製新裝置所需的配置檔案分類
層 級 | 作 用 |
---|---|
晶片架構層(Architecture) | 產品所採用的硬體架構,如ARM、X86等 |
核心板層(Board) | 硬體電路的核心板層配置 |
裝置層(Device) | 外圍裝置的配置,如有沒有鍵盤 |
產品層(Product) | 最終生成的系統需要包含的軟體模組和配置,如是否有攝像頭應用程式、預設的國家或地區語言等 |
也就是說,一款產品由底層往上的構建順序是:晶片架構→核心板→裝置→產品。這樣講可能有點抽象,給大家舉個具體的例子。我們知道,當前嵌入式領域市場佔有率最高的當屬ARM系列晶片。但是首先,ARM公司本身並不生產具體的晶片,而只授權其他合作伙伴來生產和銷售半導體晶片。ARM架構就是屬於最底層的硬體體系,需要在編譯時配置。其次,很多晶片設計商(如三星)在獲得授權後,可以在ARM架構的基礎上設計出具體的核心板,如S5PV210。接下來,三星會將其產品進一步銷售給有需要的下一級廠商,如某手機生產商。此時就要考慮整個裝置的硬體配置了,如這款手機是否要帶有按鍵、觸控式螢幕等。最後,在確認了以上3個層次的硬體設計後,我們還可以指定產品的一些具體屬性,如預設的國家或地區語言、是否帶有某些應用程式等。
後續的步驟中我們將分別講解與這幾個層次相關的一些重要的指令碼檔案。
Step 2. vendorsetup.sh
雖然我們已經為新產品建立了目錄,但Android系統並不知道它的存在——所以需要主動告知Android系統新增了一個“家庭成員”。以三星toro為例,為了讓它能被正確新增到編譯系統中,首先就要在其目錄下新建一個vendorsetup.sh指令碼。這個指令碼通常只需要一個語句。具體範例如下:
add_lunch_combo full_toro-userdebug複製程式碼
大家應該還記得前一小節編譯原生態系統的第一步是執行envsetup.sh,函式add_lunch_combo就是在這個檔案中定義的。此函式的作用是將其引數所描述的產品(如full_toro-userdebug)新增到系統相關變數中——後續lunch提供的選單即基於這些變數產生的。
那麼,vendorsetup.sh在什麼時候會被呼叫呢?
答案也是envsetup.sh。這個指令碼的大部分內容是對各種函式進行定義與實現,末尾則會通過一個for迴圈來掃描工程中所有可用的vendorsetup.sh,並執行它們。具體原始碼如下:
# Execute the contents of any vendorsetup.sh files we can find.
for f in 'test -d device && find device -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null'
\
'test -d vendor && find vendor -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null'
do
echo "including $f"
. $f
Done
unset f複製程式碼
可見,預設情況下編譯系統會掃描如下路徑來查詢vendorsetup.sh:
/vendor/
/device/複製程式碼
注:vendor這個目錄在4.3版本的Android工程中已經不存在了,建議開發者將產品目錄統一放在device中。
打一個比方,上述步驟有點類似於超市的工作流程:工作人員(編譯系統)首先要掃描倉庫(vendor和device目錄),統計出有哪些商品(由vendorsetup.sh負責記錄),並通過一定的方式(add_lunch_combo@envsetup.sh)將物品上架,然後消費者才能在貨架上挑選(lunch)自己想要的商品。
Step 3. 新增AndroidProducts.mk。消費者在貨架上選擇(lunch)了某樣“商品”後,工作人員的後續操作(如結賬、售後等)就完全基於這個特定商品來展開。編譯系統會先在商品所在目錄下尋找AndroidProducts.mk檔案,這裡記錄著針對該款商品的一些具體屬性。不過,通常我們只在這個檔案中做一個“轉向”。如:
/device/samsung/toro/AndroidProducts.mk/
PRODUCT_MAKEFILES := \
$(LOCAL_DIR)/aosp_toro.mk \
$(LOCAL_DIR)/full_toro.mk複製程式碼
因為AndroidProducts.mk對於每款產品都是通用的,不利於維護管理,所以可另外新增一個或者多個以該產品命名的makefile(如full_toro.mk和aosp_toro.mk),再讓前者通過PRODUCT_MAKEFILES“指向”它們。
Step4. 實現上一步所提到的某產品專用的makefile檔案(如full_toro.mk和aosp_toro.mk)。可以充分利用編譯系統已有的全域性變數或者函式來完成任何需要的功能。例如,指定編譯結束後需要複製到裝置系統中的各種檔案、設定系統屬性(系統屬性最終會寫入裝置/system目錄下的build.prop檔案中)等。以full_toro.mk為例:
/device/samsung/toro/full_toro.mk/
#將apns等配置檔案複製到裝置的指定目錄中
PRODUCT_COPY_FILES += \
device/samsung/toro/bcmdhd.cal:system/etc/wifi/bcmdhd.cal \
device/sample/etc/apns-conf_verizon.xml:system/etc/apns-conf.xml \
…
# 繼承下面兩個mk檔案
$(call inherit-product, $(SRC_TARGET_DIR)/product/aosp_base_telephony.mk)
$(call inherit-product, device/samsung/toro/device_vzw.mk)
# 下面過載編譯系統中已經定義的變數
PRODUCT_NAME :=full_toro #產品名稱
PRODUCT_DEVICE := toro #裝置名稱
PRODUCTBRAND := Android #品牌名稱
…複製程式碼
這部分的變數基本上以“PRODUCT”開頭,我們在表2-6中對其中常用的一些變數做統一講解。
表2-6 PRODUCT相關變數
變 量 | 描 述 |
---|---|
PRODUCT_NAME | 產品名稱,最終會顯示在系統設定中的“關於裝置”選項卡中 |
PRODUCT_DEVICE | 裝置名稱 |
PRODUCT_BRAND | 產品所屬品牌 |
PRODUCT_MANUFACTURER | 產品生產商 |
PRODUCT_MODEL | 產品型號 |
PRODUCT_PACKAGES | 系統需要預裝的一系列程式,如APKs |
PRODUCT_LOCALES | 所支援的國家語言。格式如下: |
PRODUCT_POLICY | 本產品遵循的“策略”,如: |
PRODUCT_TAGS | 一系列以空格分隔的產品標籤描述 |
PRODUCT_PROPERTY_OVERRIDES | 用於過載系統屬性。 |
Step 5. 新增BoardConfig.mk檔案。這個檔案用於填寫目標架構、硬體裝置屬性、編譯器的條件標誌、分割槽佈局、boot地址、ramdisk大小等一系列引數(參見下一小節對系統映像檔案的講解)。下面是一個範例(因為toro中的BoardConfig主要引用了tuna的BoardConfig實現,所以我們直接講解後者的實現):
#/device/samsung/tuna/BoardConfig.mk/
TARGET_CPU_ABI := armeabi-v7a ## eabi即Embedded application binary interface
TARGET_CPU_ABI2 := armeabi
…
TARGET_NO_BOOTLOADER := true ##不編譯bootloader
…
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 685768704#system.img分割槽大小
BOARD_USERDATAIMAGE_PARTITION_SIZE := 14539537408#userdata.img的分割槽大小
BOARD_FLASH_BLOCK_SIZE := 4096 #flash塊大小
…
BOARD_WLANDEVICE := bcmdhd #wifi裝置複製程式碼
可以看到,這個makefile檔案中涉及的變數大部分以“TARGET”和“BOARD_”開頭,且數量眾多。相信對於第一次編寫BoardConfig.mk的開發者來說,這是一個不小的挑戰。那麼,有沒有一些小技巧來加速學習呢?
答案是肯定的。
各大廠商在自己產品目錄下存放的BoardConfig.mk樣本就是我們學習的絕佳材料。通過比較可發現,這些檔案大部分都是雷同的。所以我們完全可以先從中複製一份(最好選擇架構、主晶片與自己專案相當的),然後根據產品的具體需求進行修改。
Step 6. 新增Android.mk。這是Android系統下編譯某個模組的標準makefile。有些讀者可能分不清楚這個檔案與前面幾個步驟中的makefile有何區別。我們舉例說明,如果Step1-Step5中的檔案用於決定一個產品的屬性,那麼Android.mk就是生產這個“產品”某個“零件”的“生產工序”。——要特別注意,只是某個“零件”而已。整個產品是需要由很多Android.mk生產出的“零件”組合而成的。
Step7. 完成前面6個步驟後,我們就成功地將一款新裝置定製到編譯系統中了。接下來的編譯流程和原生態系統是完全一致的,這裡不再贅述。
值得一提的是,/system/build.prop這個檔案的生成過程也是由編譯系統控制的。具體處理過程在/build/core/Makefile中,它主要由以下幾個部分組成:
這個指令碼用於向build.prop中輸出各種<key> <value>組合,實現方式也很簡單。下面是其中的兩行節選:
echo "ro.build.id=$BUILD_ID"
echo "ro.build.display.id=$BUILD_DISPLAY_ID"
清理工作,將黑名單中的專案從最終的build.prop中移除。
開發人員在定製一款新裝置時,可以根據實際情況將自己的配置資訊新增到上述幾個組成部分中,以保證裝置的正常執行。
2.3.2 Linux核心編譯
不同產品的硬體配置往往是有差異的。比如某款手機配備了藍芽晶片,而另一款則沒有;即便是都內建了藍芽模組的兩款手機,它們的生產商和型號也很可能不一樣——這就不可避免地要涉及核心驅動的移植。前面我們分析的編譯流程只針對Android系統本身,而Linux核心和Android的編譯是獨立的。因此對於裝置開發商來說,還需要下載、修改和編譯核心版本。
接下來以Android官方提供的例子來講解如何下載合適的核心版本。
這個範例基於Google的Panda裝置,具體步驟如下。
Step1. 首先通過以下命令來獲取到git log:
$ git clone android.googlesource.com/device/ti/p…
$ cd panda
$ git log --max-count=1 kernel複製程式碼
這樣就得到了panda kernel的提交值,在後續步驟中會用到。
Step2. Google針對Android系統提供了以下可用的核心版本:
$ git clone android.googlesource.com/kernel/comm…
$ git clone android.googlesource.com/kernel/exyn…
$ git clone android.googlesource.com/kernel/gold…
$ git clone android.googlesource.com/kernel/msm.…
$ git clone android.googlesource.com/kernel/omap…
$ git clone android.googlesource.com/kernel/sams…
$ git clone android.googlesource.com/kernel/tegr…複製程式碼
上述命令的每一行都代表了一個可用的核心版本。
那麼,它們之間有何區別呢?
由此可見,與Panda裝置相匹配的是omap.git這個版本的核心。
Step3. 除了Linux核心,我們還需要下載prebuilt。具體命令如下:
$ git clone android.googlesource.com/platform/pr…
$ export PATH=$(pwd)/prebuilt/linux-x86/toolchain/arm-eabi-4.4.3/bin:$PATH複製程式碼
Step4. 完成以上步驟後,就可以進行Panda核心的編譯了:
$ export ARCH=arm
$ export SUBARCH=arm
$ export CROSS_COMPILE=arm-eabi-
$ cd omap
$ git checkout <第一步獲取到的值>
$ make panda_defconfig
$ make複製程式碼
整個核心的編譯相對簡單,讀者可以自行嘗試。
2.3.3 燒錄/升級系統
將編譯生成的可執行檔案包通過各種方式寫入硬體裝置的過程稱為燒錄(flash)。燒錄的方式有很多,各廠商可以根據實際的需求自行選擇。常見的有以下幾種。
(1)SD卡工廠燒錄方式
當前市面上的CPU主晶片通常會提供多種跳線方式,來支援嵌入式裝置從不同的儲存介質(如Flash、SD Card等)中載入載入程式並啟動系統。這樣的設計顯然會給裝置開發商帶來更多的便利。研發人員只需要將燒錄檔案按一定規則先寫入SD卡,然後將裝置配置為SD卡啟動。一旦裝置成功啟動後,處於燒寫模式下的BootLoader就會將各檔案按照要求寫入產品儲存裝置(通常是FLASH晶片)的指定地址中。
由此可見Bootloader的主要作用有兩個:其一是提供下載模式,將組成系統的各個Image寫入到裝置的永久儲存介質中;其二才是在裝置開機過程中完成引導系統正常啟動的重任。
一個完整的Android燒錄包至少需要由3部分內容(即Boot Loader,Linux Kernel和Android System)組成。我們可以利用某種方式對它們先進行打包處理,然後統一寫入裝置中。一般情況下,晶片廠商(如Samsung)會針對某款或某系列晶片提供專門的燒錄工具給開發人員使用;否則各產品開發商需要根據實際情況自行研發合適的工具。
總的來說,SD卡的燒錄手法以其操作簡便、不需要PC支援等優點被廣泛應用於工廠生產中。
(2)USB方式
這種方式需要在PC的配合下完成。裝置首先與PC通過USB進行連線,然後執行於PC上的客戶端程式將輔助Android裝置來完成檔案燒錄。
(3)專用的燒寫工具
比如使用J-Tag進行系統燒錄。
(4)網路連線方式
這種方式比較少見,因為它要求裝置本身能接入網路(區域網、網際網路),這對於很多嵌入式裝置來說過於苛刻。
(5)裝置Bootloader+fastboot的模式
這也就是我們俗稱的“線刷”。需要特別注意的是,能夠使用這種升級模式的一個前提是裝置中已經存在可用的Bootloader,因而它不能被運用於工廠燒錄中(此時裝置中還未有任何有效的系統程式)。
當然,各大廠商通常還會在這種模式上做一些“易用性的封裝”(譬如提供帶GUI介面的工具),從而在一定程度上降低使用者的使用門檻。
迫使Android裝置進入Bootloader模式的方法基本上大同小異,下面這兩種是最常見的:
通過“fastboot reboot-bootloader”命令來重啟裝置並進入Bootloader模式;
在關機狀態下,同時按住裝置的“音量減”和電源鍵進入Bootloader模式。
(6)Recovery模式
和前一種方式類似,Recovery模式同樣不適用於裝置首次燒錄的場景。“Recovery”的字面意思是“還原”,這也從側面反映出它的初衷是幫助那些出現異常的系統進行快速修復。由於OTA這種得到大規模應用的升級方式同樣需要藉助於Recovery模式,使得後者逐步超出了原先的設計範疇,成為普通消費者執行裝置升級操作的首選方式。我們將在後續小節中對此做更詳細的講解。
2.4 Android Multilib Build
早期的Android系統只支援32位CPU架構的編譯,但隨著越來越多的64位硬體平臺的出現,這種編譯系統的侷限性就突顯出來了。因而Android系統推出了一種新的編譯方式,即Multilib build。可想而知,這種編譯系統上的改進需要至少滿足兩個條件:
64位和32位平臺在很長一段時間內都需要“和諧共處”,因而編譯系統必須保證以下幾個場景。
Case1:支援只編譯64-bit系統。
Case2:支援只編譯32-bit系統。
Case3:支援編譯64和32bit系統,64位系統優先。
Case4:支援編譯32和64位系統,32位系統優先。
事實上Multilib Build提供了比較簡便的方式來滿足以上兩個條件,我們將在下面內容中學習到它的具體做法。
(1)平臺配置
BoardConfig.mk用於指定目標平臺相關的很多屬性,我們可以在這個指令碼中同時指定Primary和Secondary的CPU Arch和ABI:
與Primary Arch相關的變數有TARGET_ARCH、TARGET_ARCH_VARIANT、TARGET_CPU_VARIANT等,具體範例如下:
TARGET_ARCH := arm64
TARGET_ARCH_VARIANT := armv8-a
TARGET_CPU_VARIANT := generic
TARGET_CPU_ABI := arm64-v8a複製程式碼
與Secondary Arch相關的變數有TARGET_2ND_ARCH、TARGET_2ND_ARCH_VARIANT、TARGET_2ND_CPU_VARIANT等,具體範例如下:
TARGET_2ND_ARCH := arm
TARGET_2ND_ARCH_VARIANT := armv7-a-neon
TARGET_2ND_CPU_VARIANT := cortex-a15
TARGET_2ND_CPU_ABI := armeabi-v7a
TARGET_2ND_CPU_ABI2 := armeabi複製程式碼
如果希望預設編譯32-bit的可執行程式,可以設定:
TARGET_PREFER_32_BIT := true複製程式碼
通常lunch列表中會針對不同平臺提供相應的選項,如圖2-9所示。
▲圖2-9 相應的選項
當開發者選擇不同平臺時,會直接影響到TARGET_2ND_ARCH等變數的賦值,從而有效控制編譯流程。比如圖2-10中左、右兩側分別對應我們使用lunch 1和lunch 2所產生的結果,大家可以對比下其中的差異。
▲圖2-10 控制編譯流程
另外,還可以設定TARGET_SUPPORTS_32_BIT_APPS和TARGET_SUPPORTS_64_BIT_APPS來指明需要為應用程式編譯什麼版本的本地庫。此時需要特別注意:
那麼在支援不同位數的編譯時,所採用的Tool Chain是否有區別?答案是肯定的。
如果你希望使用通用的GCC工具鏈來同時處理兩種Arch架構,那麼可以使用TARGET_GCC_VERSION_EXP;反之你可以使用TARGET_TOOLCHAIN_ROOT和2ND_TARGET_TOOLCHAIN_ROOT來為64和32位編譯分別指定不同的工具鏈。
(2)單模組配置
我們當然也可以針對單個模組來配置Multilib。
需要特別注意的是,在make命令中直接指定的目標物件只會產生64位的編譯。舉一個例子來說,“lunch aosp_arm64-eng”→“make libc”只會編譯64-bit的libc。如果你想編譯32位的版本,需要執行“make libc_32”。
描述單模組編譯的核心指令碼是Android.mk,在這個檔案裡我們可以通過指定LOCAL_MULTILIB來改變預設規則。各種取值和釋義如下所示:
只考慮Primary Arch的情況
同時編譯32和64位版本
只編譯32位版本
只編譯64位版本
這是預設值。編譯系統會根據其他配置來決定需要怎麼做,如LOCAL_MODULE_TARGET_ARCH,LOCAL_32_BIT_ONLY等。
如果你需要針對某些特定的架構來做些調整,那麼以下幾個變數可能會幫到你:
可以指定一個Arch列表,例如“arm x86 arm64”等。這個列表用於指定你的模組所支援的arch範圍,換句話說,如果當前正在編譯的arch不在列表中將導致本模組不被編譯:
如其名所示,這個變數起到和上述變數相反的作用。
這兩個變數的末尾多了個“WARN”,意思就是如果當前模組在編譯時被忽略,那麼會有warning列印出來。
各種編譯標誌也可以打上與Arch相應的標籤,如以下幾個例子:
我們再來看一下安裝路徑的設定。對於庫檔案來說,可以使用LOCAL_MODULE_RELATIVE_PATH來指定一個不同於預設路徑的值,這樣32位和64位的庫都會被放置到這裡。對於可執行檔案來說,可以分別使用以下兩類變數來指定檔名和安裝路徑:
分別指定32位和64位下的可執行檔名稱。
分別指定32位和64位下的可執行檔案安裝路徑。
(3)Zygote
支援Multilib Build還需要考慮一個重要的應用場合,即Zygote。可想而知,Multilib編譯會產生兩個版本的Zygote來支援不同位數的應用程式,即Zygote64和Zygote32。早期的Android系統中,Zygote的啟動指令碼被直接書寫在init.rc中。但從Lollipop開始,這種情況一去不復返了。我們來看一下其中的變化:
/system/core/rootdir/init.rc/
import /init.${ro.hardware}.rc
import /init.${ro.zygote}.rc複製程式碼
根據系統屬性ro.zygote的不同,init程式會呼叫不同的zygote描述指令碼,從而啟動不同版本的“孵化器”。以ro.zygote為“zygote64_32”為例,具體指令碼如下:
/system/core/rootdir/init.zygote64_32.rc/
service zygote /system/bin/<strong>app_process64</strong> -Xzygote /system/bin --zygote --start-system
-server --socket-name=zygote
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
service zygote_secondary /system/bin/<strong>app_process32</strong> -Xzygote /system/bin --zygote --
socket-name=zygote_secondary
class main
socket zygote_secondary stream 660 root system
onrestart restart zygote複製程式碼
這個指令碼描述的是Primary Arch為64,Secondary Arch為32位時的情況。因為zygote的承載程式是app_process,所以我們可以看到系統同時啟動了兩個Service,即app_process64和app_process32。關於zygote啟動過程中的更多細節,讀者可以參考本書的系統啟動章節,我們這裡先不進行深入分析。
因為系統需要有兩個不同版本的zygote同時存在,根據前面內容的學習我們可以斷定,zygote的Android.mk中一定做了同時編譯32位和64位程式的配置:
/frameworks/base/cmds/app_process/Android.mk/
LOCAL_SHARED_LIBRARIES := \
libcutils \
libutils \
liblog \
libbinder \
libandroid_runtime
LOCAL_MODULE:= app_process
LOCAL_MULTILIB := <strong>both</strong>
LOCAL_MODULE_STEM_32 := app_process32
LOCAL_MODULE_STEM_64 := app_process64
include $(BUILD_EXECUTABLE)複製程式碼
上面這個指令碼可以作為需要支援Multilib build的模組的一個範例。其中LOCAL_MULTILIB告訴系統,需要為zygote生成兩種型別的應用程式;而LOCAL_MODULE_STEM_32和LOCAL_MODULE_STEM_64分別用於指定兩種情況下的應用程式名稱。
2.5 Android系統映像檔案
通過前面幾個小節的學習,我們已經按照產品需求編譯出自定製的Android版本了。編譯成功後,會在out/target/product/[YOUR_PRODUCT_NAME]/目錄下生成最終要燒錄到裝置中的映像檔案,包括system.img,userdata.img,recovery.img,ramdisk.img等。初次看到這些檔案的讀者一定想知道為什麼會生成這麼多的映像、它們各自都將完成什麼功能。
這是本小節所要回答的問題。
Android中常見image檔案包的解釋如表2-7所示。
表2-7 Android系統常見image釋義
Image | Description |
---|---|
boot.img | 包含核心啟動引數、核心等多個元素(詳見後面小節的描述) |
ramdisk.img | 一個小型的檔案系統,是Android系統啟動的關鍵 |
system.img | Android系統的執行程式包(framework就在這裡),將被掛載到裝置中的/system節點下 |
userdata.img | 各程式的資料儲存所在,將被掛載到/data目錄下 |
recovery.img | 裝置進入“恢復模式”時所需要的映像包 |
misc.img | 即“miscellaneous”,包含各種雜項資源 |
cache.img | 緩衝區,將被掛載到/cache節點中 |
它們的關係可以用圖2-11來表示。
接下來對boot、ramdisk、system三個重要的系統image進行深入解析。
▲圖2-11 關係圖
2.5.1 boot.img
理解boot.img的最好方法就是學習它的製作工具—— mkbootimg,原始碼路徑在system/core/ mkbootimg中。這個工具的語法規則如下:
mkbootimg --kernel <filename> --ramdisk <filename>
[ --second <2ndbootloader-filename>] [ --cmdline <kernel-commandline> ]
[ --board <boardname> ] [ --base <address> ]
[ --pagesize <pagesize> ] -o|--output <filename>複製程式碼
--kernel:指定核心程式包(如zImage)的存放路徑;
--ramdisk:指定ramdisk.img(下一小節有詳細分析)的存放路徑;
--second:可選,指第二階段檔案;
--cmdline:可選,核心啟動引數;
--board:可選,板名稱;
--base:可選,核心啟動基地址;
--pagesize:可選,頁大小;
--output:輸出名稱。
那麼,編譯系統是在什麼地方呼叫mkbootimg的呢?
其一就是droidcore的依賴中,INSTALLED_BOOTI MAGE_TARGET,如圖2-12所示。
▲圖2-12 droidcore的依賴
其二就是生成INSTALLED_BOOTIMAGE_TARGET的地方(build/core/Makefile),如圖2-13所示。
▲圖2-13 生成INSTALLED_BOOTIMAGE_TARGET的地方
可見mkbootimg程式的各引數是由INTERNAL_BOOTIMAGE_ARGS和BOARD_MKBOOTIMG_ARGS來指定的,而這兩者又分別取決於其他makefile中的定義。如BoardConfig.mk中定義的BOARD_KERNEL_CMDLINE在預設情況下會作為--cmdline引數傳給mkbootimg;BOARD_KERNEL_BASE則作為--base引數傳給mkbootimg。
按照Bootimg.h中的描述,boot.img的檔案結構如圖2-14所示。
▲圖2-14 boot.img的檔案結構
各組成部分如下:
1.boot header
儲存核心啟動“頭部”—— 核心啟動引數等資訊,佔據一個page空間,即4KB大小。Header中包含的具體內容可以通過分析Mkbootimg.c中的main函式來獲知,它實際上對應boot_img_hdr這個結構體:
/system/core/mkbootimg/Bootimg.h/
struct boot_img_hdr
{
unsigned char magic[BOOT_MAGIC_SIZE];
unsigned kernel_size; / size in bytes /
unsigned kernel_addr; / physical load addr /
unsigned ramdisk_size; / size in bytes /
unsigned ramdisk_addr; / physical load addr /
unsigned second_size; / size in bytes /
unsigned second_addr; / physical load addr /
unsigned tags_addr; / physical addr for kernel tags /
unsigned page_size; / flash page size we assume /
unsigned unused[2]; / future expansion: should be 0 /
unsigned char name[BOOT_NAME_SIZE]; / asciiz product name /
unsigned char cmdline[BOOT_ARGS_SIZE];
unsigned id[8]; / timestamp / checksum / sha1 / etc /
};複製程式碼
這樣講有點抽象,下面舉個實際的boot.img例子,我們可以用UltraEditor或者WinHex把它開啟,如圖2-15所示。
可以看到,檔案最起始的8個位元組是“ANDROID!”,也稱為BOOT_MAGIC;後續的內容則包括kernel_size,kernel_addr等,與上述的boot_img_hdr結構體完全吻合。
▲圖2-15 boot header例項
2.kernel
核心程式是整個Android系統的基礎,也被“裝入”boot.img中——我們可以通過--kernel選項來指定核心對映檔案的儲存路徑。其所佔據的大小為:
n pages=(kernel_size + page_size - 1) / page_size複製程式碼
由此可以看出,boot.img中的各元素必須是頁對齊的。
3.ramdisk
不僅是kernel,boot.img中也包含了ramdisk.img。其所佔據大小為:
m pages=(ramdisk_size + page_size - 1) / page_size複製程式碼
可見也是頁對齊的。
其他關於ramdisk的詳細描述請參照下一小節,這裡先不做解釋。
4.second stage
這一項是可選的。其佔據大小為:
o pages= (second_size + page_size - 1) / page_size複製程式碼
這個元素通常用於擴充套件功能,預設情況下可以忽略。
2.5.2 ramdisk.img
無論什麼型別的檔案,從計算機儲存的角度來說都只不過是一堆“0”“1”數字的集合—— 它們只有在特定處理規則的解釋下才能表現出意義。如txt文字用Ultra Editor開啟就可以顯示出裡面的文字;jpg影象檔案在Photoshop工具的輔助下可以讓使用者看到其所包含的內容。而文字與jpeg影象檔案本質上並沒有區別,只不過儲存與讀取這一檔案的“規則”發生了變化—— 正是這些“五花八門”的“規則”才創造出成千上萬的檔案型別。
另外,檔案字尾名也並不是必需的,除非作業系統用它來鑑別檔案的型別。而更多情況下,字尾名的存在只是為了讓使用者有個直觀的認識。如我們會認為“.txt”是文字文件、“.jpg”是圖片等。
Android的系統檔案以“.img”為字尾名,這種型別的檔案最初用來表示某個disk的完整複製。在從原理的層面講解這些系統映像之前,可以通過一種方式來讓讀者對這些檔案有個初步的感性認識(下面的操作以ramdisk.img為例,其他映像檔案也是類似的)。
首先對ramdisk.img執行file命令,得到如下結果:
$file ramdisk.img
ramdisk.img: gzip compressed data, from Unix複製程式碼
這說明它是一個gZip的壓縮檔案。我們將其改名為ramdisk.img.gz,再進行解壓。具體命令如下:
$gzip –d ramdisk.img.gz複製程式碼
這時會得到另一個名為ramdisk.img的檔案,不過檔案型別變了:
$file ramdisk.img
ramdisk.img: ASCII cpio archive (SVR4 with no CRC)複製程式碼
由此可知,這時的ramdisk.img是CPIO檔案了。
再來執行以下操作:
$cpio -i -F ramdisk.img
3544 blocks複製程式碼
這樣就解壓出了各種檔案和資料夾,範例如圖2-16所示。
▲圖2-16 範例
可以清楚地看到,常用的system目錄、data目錄以及init程式(系統啟動過程中執行的第一個程式)等檔案都包含在ramdisk.img中。
這樣我們可以得出一個大致的結論,ramdisk.img中存放的是root根目錄的映象(編譯後可以在out/target/product/[YOUR_PRODUCT_NAME]/root目錄下找到)。它將在Android系統的啟動過程中發揮重要作用。
2.5.3 system.img
要將system.img像ramdisk.img一樣解壓出來會相對麻煩一些。不過方法比較多,除了以下提到的方式,讀者還可以嘗試使用unyaffs(參考code.google.com/p/unyaffs/或…. google.com/p/yaffs2utils/)來實現。
這裡我們採取mount的方法,這是目前最省時省力的解決方式。
步驟如下:
編譯成功後,這個工具的可執行檔案在out/host/linux-x86/bin中。
原始碼目錄 system/extras/ext4_utils。
將此工具複製到與system.img同一目錄下。
執行如下命令可以查詢simg2img的用法:
$ ./simg2img --h
Usage: simg2img <sparse_image_file><raw_image_file>複製程式碼
對system.img執行:
$ ./simg2img system.img system.img.step1複製程式碼
將上一步得到的檔案通過以下操作掛載到system_extracted中:
$ mkdir system_extracted
$ sudo mount -o loop system.img.step1 system_extracted複製程式碼
最終我們得到如圖2-17所示的結果。
▲圖2-17 結果圖
這說明該image檔案包含了裝置/system節點中的相關內容。
2.5.4 Verified Boot
Android領域的開放性催生了很多第三方ROM的繁榮(例如市面上“五花八門”的Recovery、定製的Boot Image、System Image等),同時也給系統本身的安全性帶來了挑戰。
從4.4版本開始,Android結合Kernel的dm-verity驅動能力實現了一個名為“Verified Boot”的安全特性,以期更好地保護系統本身免受惡意程式的侵害。我們在本小節將向大家講解這一特性的基本原理,以便讀者們在無法成功利用fastboot寫入image時可以清楚地知道隱藏在背後的真正原因。
我們先來熟悉表2-8所示的術語。
當裝置開機以後,根據Boot State和Device State的狀態值不同,有如圖2-18所示幾種可能性。
表2-8 Verified Boot相關術語
術 語 | 釋 義 |
---|---|
dm-verity | Linux kernel的一個驅動,用於在執行時態驗證檔案系統分割槽的完整性(判斷依據是Hash Tree和Signed metadata) |
Boot State | 保護等級,分為GREEN、YELLOW、ORANGE和RED四種 |
Device State | 表明裝置接受軟體刷寫的程度,通常有LOCKED和UNLOCKED兩種狀態 |
Keystore | 公鑰合集 |
OEM key | Bootloader用於驗證boot image的key |
▲圖2-18 Verified Boot總體流程
(引用自Android官方文件)
最下方的4個圓圈顏色分別為:GREEN、YELLOW、RED和ORANGE。例如當前裝置的Device State是LOCKED,那麼就首先需要經歷OEM KEY Verification——如果通過的話Boot State是GREEN,表示系統是安全的;否則需要進入下一輪的Signature Verification,其結果決定了Boot State是YELLOW或者是RED(比較危險)。當然,如果當前裝置本身就是UNLOCKED的,那就不用經過任何檢驗——不過它和YELLOW、RED一樣的地方是,都會在螢幕上顯式地告誡使用者潛在的各種風險。部分Android裝置還會要求使用者主動做出選擇後才能正常啟動,如圖2-19所示典型示例。
如果裝置的Device State發生切換的話(fastboot就提供了類似的命令,只不過大部分裝置都需要解鎖碼才能完成),那麼系統中的data分割槽將會被擦除,以保證使用者資料的安全。
▲圖2-19 典型示例
我們知道,Android系統在啟動過程中要經過Bootloader->Kernel->Android三個階段,因而在Verified Boot的設計中,它對分割槽的看護也是環環相扣的。具體來說,Bootloader承擔boot和recovery分割槽的完整性校驗職責;而Boot Partition則需要保證後續的分割槽,如system的安全性。另外,Recovery的工作和Boot是基本類似的。
不過,由於分割槽檔案大小有差異,具體的檢驗手段也是不同的。結合前面小節對boot.img的描述,其在增加了verified boot後的檔案結構變化如圖2-20所示。
▲圖2-20 檔案結構變化
除了mkbootimg來生成原始的boot.img外,編譯系統還會呼叫另一個新工具,即boot_signer(對應原始碼目錄system/extras/verity)來在boot.img的尾部附加一個signature段。這個簽名是針對boot.img的Hash結果展開的,預設使用的key在/build/target/product/security目錄下。
而對於某些大塊分割槽(如System Image),則需要通過dm-verity來驗證它們的完整性。關於dm-verity還有非常多的技術細節,限於篇幅我們不做過多討論,但強烈建議讀者自行查閱相關資料做進一步深入學習。
2.6 ODEX流程
ODEX是Android舊系統的一個優化機制。對於很多開發人員來說,ODEX可以說是既熟悉又陌生。熟悉的原因在於目前很多手機系統,或者APK中的檔案都從以前的格式變成了如圖2-21和圖2-22所示的樣子。
而陌生的原因在於有關ODEX的資料並不是很多,不少開發人員對於ODEX是什麼,能做什麼以及它的應用流程並不清楚——這也是我們本小節所要向大家闡述的內容。
▲圖2-21 系統目錄system/framework下的檔案列表
ODEX是Optimized Dalvik Executable的縮寫,從字面意思上理解,就是經過優化的Dalvik可執行檔案。Dalvik是Android系統(目前已經切換到Art虛擬機器)中採用的一種虛擬機器,因而經過優化的ODEX檔案讓我們很自然地想到可以為虛擬機器的執行帶來好處。
事實上也的確如此——ODEX是Google為了提高Android執行效率做出努力的成果之一。我們知道,Android系統中不少程式碼是使用Java語言編寫的。編譯系統首先會將一個Java檔案編譯成class的形式,進而再通過一個名為dx的工具來轉換成dex檔案,最後將dex和資源等檔案壓縮成zip格式的APK檔案。換句話說,一個典型的Android APK的組成結構如圖2-23所示。
▲圖2-22 系統目錄/system/app下的檔案列表
▲圖2-23 APK的組成結構
本書的Android應用程式編譯和打包章節將做更為詳細介紹。現在大家只要知道APK中有哪些組成元素就可以了。當應用程式啟動時,系統需要提取圖2-23中的dex(如果之前沒有做過ODEX優化的話,或者/data/dalvik-cache中沒有對應的ODEX快取),然後才能執行載入動作。而ODEX則是預先將DEX提取出來,並針對當前具體裝置做了優化工作後的產物,這樣做除了能提高載入速度外,還有如下幾個優勢:
ODEX是在dex基礎上針對當前具體裝置所做的優化,因而它和生成時所處的具體裝置有很大關聯。換句話說,除非破解者能提供與ODEX生成時相匹配的環境檔案(比如core.jar、ext.jar、framework.jar、services.jar等),否則很難完成破解工作。這就在無形中提高了系統的安全性。
按照Android系統以前的做法,不僅APK中需要存放一個dex檔案,而且/data/dalvik-cache目錄下也會有一個dex檔案,這樣顯然會浪費一定的儲存空間。相比之下,ODEX只有一份,而且它比dex所佔的體積更小,因而自然可以為系統節省更多的儲存空間。
2.7 OTA系統升級
前面我們討論了系統包燒錄的幾種傳統方法,而Android系統其實還提供了另一種全新 的升級方案,即OTA(Over the Air)。OTA非常靈活,它既可以實現完整的版本升級,也可以做到增量升級。另外,使用者既可以選擇通過SD卡來做本地升級,也可以直接採用網路線上升級。
不論是哪種升級形式,都可以總結為3個階段:
下面我們來逐一分析這3個階段。
2.7.1 生成升級包
升級包也是由系統編譯生成的,其編譯過程本質上和普通Android系統編譯並沒有太大區別。如果想生成完整的升級包,具體命令如下:
$make otapackage複製程式碼
注意
生成OTA包的前提是,我們已經成功編譯生成了系統映像檔案(system.img等)。
最終將生成以下檔案:
out/target/product/[YOUR_PRODUCT_NAME]/[YOUR_PRODUCT_NAME]-ota-eng.[UID].zip複製程式碼
而生成差分包的過程相對麻煩一些,不過方法也很多。以下給出一種常用的方式:
將上一次生成的完整升級包複製並更名到某個目錄下,如~/OTA_DIFF/old_target_file.zip;
對原始檔進行修改後,用make otapackage編譯出一個新的OTA版本;
將本次生成的OTA包更名後複製到和上一個升級包相同的目錄下,如~/OTA_DIFF/ new_target_file.zip;
呼叫ota_from_target_files指令碼來生成最終的差分包。
這個指令碼位於:
build/tools/releasetools/ota_from_target_files複製程式碼
值得一提的是,完整升級包的生成過程其實也使用了這一指令碼。區分的關鍵就在於使用時是否提供了-i引數。
其具體語法格式是:
ota_from_target_files [Flags] input_target_files output_ota_package複製程式碼
所有Flags引數釋義如表2-9所示。
表2-9 ota_from_target_files引數
參 數 | 說 明 |
---|---|
-b (--board_config) <file> | 在新版本中已經無效 |
-k (--package_key) <key> | <key>用於包的簽名預設使用input_target-files中的META/misc_info.txt檔案如果此檔案不存在,則使用build/target/product/security/testkey |
-i (--incremental_from) <file> | 該選項用於生成差分包 |
-w (--wipe_user_data) | 由此生成的OTA包在安裝時會自動擦除user data 分割槽 |
-n (--no_prereq) | 忽略時間戳檢查 |
-e (--extra_script) <file> | 將<file>內容插入update指令碼的尾部 |
-a (--aslr_mode) <on|off> | 是否開啟ASLR技術預設為開 |
在這個例子中,我們可以採用以下命令生成一個OTA差分包:
./build/tools/releasetools/ota_from_target_files-i ~/OTA_DIFF/old_target_file.zip~/OTA_DIFF/new_target_file.zip複製程式碼
這樣生成的update.zip就是最終可用的差分升級包。一方面,差分升級包體積較小,傳輸方便;但另一方面,它對升級的裝置有嚴格要求,即必須是安裝了上一升級包版本的那些裝置才能正常使用本次的OTA差分包。
2.7.2 獲取升級包
如圖2-24所示,有兩種常見的渠道可以獲取到OTA升級包,分別是線上升級和本地升級。
▲圖2-24 獲取OTA升級包的兩種方式
1.線上升級
開發者將編譯生成的OTA包上傳至網路儲存伺服器上,然後使用者可以直接通過終端訪問和下載升級檔案。通常我們把下載到的OTA包儲存在裝置的SD卡中。
線上升級的方式涉及兩個核心因素。
裝置廠商需要架構伺服器來存放、管理OTA包,併為客戶端提供包括查詢在內的多項服務。
客戶終端如何與伺服器進行互動,是否需要認證,OTA包如何傳輸等都是需要考慮的。
由此可見,線上升級方式要求廠商提供較好的硬體環境來解決使用者大規模升級時可能引發的問題,因而成本較高。不過這種方式對消費者來說比較方便,而且可以實時掌握版本的最新動態,所以對凝聚客戶有很大幫助。目前很多主流裝置生產商(如HTC)和第三方的ROM開發商(如MIUI)都提供了線上升級模式。
伺服器和客戶端的一種理論互動方案可以參見圖2-25所示的圖例。
步驟如下:
在手動升級的情況下,由使用者發出升級的指令;而在自動升級的情況下,則由程式根據一定的預設條件來啟動升級流程。比如設定了開機自動檢查是否有可用的更新,那麼每次機器啟動後都會去伺服器取得最新的版本資訊。
無論是手動還是自動升級,都必須通過伺服器查詢資訊。與伺服器的連線方式是多種多樣的,由開發人員自行決定。在必要的情況下,還應該使用加密連線。
如果一切順利,我們就得到了伺服器上最新升級檔案的版本號。接下來需要將這個版本號與本地安裝的系統版本號進行比較,決定是否進入下一步操作。
如果伺服器上的升級檔案要比本地系統新(在制定版本號規則時,應儘量考慮如何可以保證新舊版本的快速比較),那麼升級繼續;否則中止升級流程——且若是手動升級的情況,一定要提示使用者中止的原因,避免造成不好的使用者體驗。
升級檔案一般都比較大(Android系統檔案可能達到幾百MB)。這麼大的資料量,如果是通過行動通訊網路(GSM\WCDMA\CDMA\TD-SCDMA等)來下載,往往不現實。因此如果沒有事先知會使用者而自動下載的話,很可能會引起使用者的不滿。“提示框”的設計也要儘可能便利,如可以讓使用者快捷地啟用Wi-Fi通道進行下載。
下載後的升級檔案需要儲存在本地裝置中才能進入下一步的升級。通常這一檔案會直接被放置在SD卡的根目錄下,命名為update.zip。
接下來系統將自動重啟,並進入RecoveryMode進行升級。
▲圖2-25 線上升級圖例
2.本地升級
OTA升級包並非一定要通過網路線上的方式才可以下載到——只要條件允許,就可以從其他渠道獲取到升級檔案update.zip,並複製到SD卡的根目錄下,然後手動進入升級模式(見下一小節)。
線上升級和本地升級各有利弊,開發商應根據實際情況來提供最佳的升級方式。
2.7.3 OTA升級——Recovery模式
經過前面小節的講解,現在我們已經準備好系統升級檔案了(不論是線上還是本地升級),接下來就進入OTA升級最關鍵的階段——Recovery模式,也就是大家俗稱的“卡刷”。
Recovery相關的原始碼主要在工程專案的如下目錄中:
\bootable\recovery
因為涉及的模組比較多,這個資料夾顯得有點雜亂。我們只挑選與Recovery刷機有關聯的部分來進行重點分析。
▲圖2-26 進入RecoveryMode的流程
圖2-26所示是Android系統進入RecoveryMode的判斷流程,可見在如下兩種情況下裝置會進入還原模式。
很多Android裝置的RecoveryKey都是電源和Volume+的組合鍵,因為這兩個按鍵在大部分裝置上都是存在的。
系統在某些情況下會主動要求進入還原模式,如我們前面討論的“線上升級”方式——當OTA包下載完成後,系統需要重啟然後進入RecoveryMode進行檔案的刷寫。
當進入RecoveryMode後,裝置會執行一個名為“Recovery”的程式。這個程式對應的主要原始碼檔案是/bootable/recovery/ recovery.cpp,並且通過如下幾個檔案與Android主系統進行溝通。
(1)/cache/recovery/command INPUT
Android系統傳送給recovery的命令列檔案,具體命令格式見後面的表格。
(2)/cache/recovery/log OUTPUT
recovery程式輸出的log檔案。
(3)/cache/recovery/intent OUTPUT
recovery傳遞給Android的intent。
當Android系統希望開機進入還原模式時,它會在/cache/recovery/command中描述需要由Recovery程式完成的“任務”。後續Recovery程式通過解析這個檔案就可以知道系統的“意圖”,如表2-10所示。
表2-10 CommandLine引數釋義
Command Line | Description |
---|---|
--send_intent=anystring | 將text輸出到recovery.intent中 |
--update_package=path | 安裝OTA包 |
--wipe_data | 擦除user data,然後重啟 |
--wipe_cache | 擦除cache(不包括user data),然後重啟 |
--set_encrypted_filesystem=on|off | enable/disable加密檔案系統 |
--just_exit | 直接退出,然後重啟 |
由表格所示的引數可以知道Recovery不但負責O他的升級,而且也是“恢復出廠設定”的實際執行者,如圖2-27所示。
▲圖2-27 系統設定中的“恢復出廠設定”
接下來分別講解這兩個功能在Recovery程式中的處理流程。
恢復出廠設定。
(1)使用者在系統設定中選擇了“恢復出廠設定”。
(2)Android系統在/cache/recovery/command中寫入“--wipe_data”。
(3)裝置重啟後發現了command命令,於是進入recovery。
(4)recovery將在BCB(bootloader control block)中寫入“boot-recovery”和“--wipe_data”,具體是在get_args()函式中——這樣即便裝置此時重啟,也會再進入erase流程。
(5)通過erase_volume來重新格式化/data。
(6)通過erase_volume來重新格式化/cache。
(7)finish_recovery將擦除BCB,這樣裝置重啟後就能進入正常的開機流程了。
(8)main函式呼叫reboot來重啟。
上述過程中的BCB是專門用於recovery和bootloader間互相通訊的一個flash塊,包含了如下資訊:
struct bootloader_message {
char command[32];
char status[32];
char recovery[1024];
};複製程式碼
依據前面對Android系統幾大分割槽的講解,BCB資料應該存放在哪個image中呢?沒錯,是misc。
OTA升級具體如下。
(1)OTA包的下載過程參見前一小節的介紹。假設包名是update.zip,儲存在SDCard中。
(2)系統在/cache/recovery/command中寫入"--update_package=[路徑名]"。
(3)系統重啟後檢測到command命令,因而進入recovery。
(4)get_args將在BCB中寫入"boot-recovery" 和 "--update_package=..." —— 這樣即便此時裝置重啟,也會嘗試重新安裝OTA升級包。
(5)install_package開始安裝OTA升級包。
(6)finish_recovery擦除BCB,這樣裝置重啟後就可以進入正常的開機流程了。
(7)如果install失敗的話:
(8)main呼叫maybe_install_firmware_update,OTA包中還可能包含radio/hboot firmware的更新,具體過程略。
(9)main呼叫reboot重啟系統。
總體來說,整個Recovery.cpp原始檔的邏輯層次比較清晰,讀者可以基於上述流程的描述來對照並閱讀程式碼。
2.8 Android反編譯
目前我們已經學習了Android原生態系統及定製產品的編譯和燒錄過程。和編譯相對的,卻同樣重要的是反編譯。比如,一個優秀的“用毒”高手往往也會是卓越的“解毒”大師,反之亦然。大自然的一個奇妙之處即萬事萬物都是“相生相剋”的,只有在競爭中才能不斷地進步和發展。
首先要糾正不少讀者可能會持有的觀點——“反編譯”就是去“破解”軟體。應該說,破解一款軟體的確需要用到很多反編譯的知識,不過這並不是它的全部用途。比如筆者就曾經在開發過程中利用反編譯輔助解決了一個bug,在這裡和讀者分享一下。
問題是這樣的:開發人員A修改了framework中的某個檔案,然後通過正常的編譯過程生成了image,再將其燒錄到了機器上。但奇怪的是,檔案的修改並沒有體現出來(連新加的log也沒有列印出來)。顯然,出現問題的可能是下列步驟中的任何一個,如圖2-28所示。
▲圖2-28 可能出現問題的幾個步驟
可疑點為:
因為加log的那個函式是系統會頻繁呼叫到的,而且log就放在函式開頭沒有加任何判斷,所以這個可能性被排除。
列印log所用的方法與此檔案中其他地方所用的方法完全一致,而且其他地方的log確實成功輸出了,所以也排除這一可能性。
雖然Android的編譯系統非常強大,但是難免會有bug,因而這個可能性還是存在的。那麼如何確定我們修改的檔案真的被編譯到了呢?此時反編譯就有了用武之地了。
這並不是空穴來風,確實發生過開發人員因為粗心大意燒錯版本的“事故”(對於某些細微修改,編譯系統不會主動產生新的版本號)。通過反編譯機器上的程式,然後和原始檔案進行比較,我們可以清楚地確認機器中執行的程式是不是預期的版本。
由上述分析可知,反編譯是確定該問題最直接的方式。
Android反編譯過程按照目標的不同分為如下兩類(都是基於Java語言的情況)。
不論針對哪種目標物件,它們的步驟都可以歸納為如圖2-29所示。
APK應用安裝包實際上是一個Zip壓縮包,使用Zip或WinRAR等軟體開啟后里面有一個“classes.dex”檔案—— 這是Dalvik JVM虛擬機器支援的可執行檔案(Dalvik Executable)。關於這個檔案的生成過程,可以參見本書應用篇中對APK編譯過程的介紹。換句話說,classes.dex這個檔案包含了所有的可執行程式碼。
▲圖2-29 反編譯的一般流程
由前面小節的學習我們知道,odex是classes.dex經過dex優化(optimize)後產生的。一方面,Dalvik虛擬機器會根據執行需求對程式進行合理優化,並快取結果;另一方面,因為可以直接訪問到程式的odex,而不是通過解壓縮包去獲取資料,所以無形中加快了系統開機及程式的執行速度。
針對反編譯過程,我們首先是要取得程式的dex或者odex檔案。如果是APK應用程式,只需要使用Zip工具解壓縮出其中的classes.dex即可(有的APK原始的classes.dex會被刪除,只保留對應的odex檔案);而如果是包含在系統image中的系統包(如framework就是在system.img中),就需要通過其他方法間接地將其原始檔案還原出來。具體步驟可以參見前一小節的介紹。
取得dex/odex檔案後,我們將它轉化成Jar檔案。
目前已經有不少研究專案在分析Android的逆向工程,其中最著名的就是smali/baksmali。可以在這裡下載到它的最新版本:
code.google.com/p/smali/dow…複製程式碼
“smali”和“baksmali”分別對應冰島語中“assembler”和“disassembler”。為什麼要用冰島語命名呢?答案就是Dalvik這個名字實際上是冰島的一個小漁村。
如果是odex,需要先用baksmali將其轉換成dex。具體語法如下:
$ baksmali -a <api_level> -x <odex_file> -d <framework_dir>複製程式碼
-a指定了API Level,-x表示目標odex檔案,-d指明瞭framework路徑。因為這個工具需要用到諸如core.jar,ext.jar,framework.jar等一系列framework包,所以建議讀者直接在Android原始碼工程中out目錄下的system/framework中進行操作,或者把所需檔案統一複製到同一個目錄下。
範例如下(1.4.1版本):
$ java -jar baksmali-1.4.1.jar -a 16 -x example.odex複製程式碼
如果是要反編譯系統包中的odex(如services.odex),請參考以下命令:
$java -Xmx512m -jar baksmali-1.4.1.jar -a 16 -c:core.jar:bouncycastle.jar:ext.jar:framework.
jar:android.policy.jar:services.jar:core-junit.jar -d framework/ -x services.odex複製程式碼
更多語法規則可以通過以下命令獲取:
$ java -jar baksmali-1.4.1.jar --help複製程式碼
執行結果會被儲存在一個out目錄中,裡面包含了與odex相應的所有原始碼,只不過由smali語法描述。讀者如果有興趣的話,可以閱讀以下文件來了解smali語法:
code.google.com/p/smali/wik…複製程式碼
當然對於大部分開發人員來說,還是希望能反編譯出最原始的Java語言檔案。此時就要再將smali檔案轉化成dex檔案。具體命令如下:
$ java -jar smali-1.4.1.jar out/ -o services.dex複製程式碼
於是接下來的流程就是dex→Java,請參考下面的說明。
前面我們已經成功將odex“去優化”成dex了,離勝利還有一步之遙——將dex轉化成jar檔案。目前比較流行的工具是dex2jar,可以在這裡下載到它的最新版本:
使用方法也很簡單,具體範例如下:
$ ./dex2jar.sh services.dex複製程式碼
上面的命令將生成services_dex2jar.jar,這個Jar包中包含的就是我們想要的原始Java檔案。那麼,選擇什麼工具來閱讀Jar中的內容呢?在本例中,我們只是希望確定所加的log是否被正確編譯進目標檔案中,因而可以使用任何常用的文字編輯器查閱程式碼。而如果希望能更方便地閱讀程式碼,推薦使用jd-gui,它是一款圖形化的反編譯程式碼閱讀工具。
這樣,整個反編譯過程就完成了。
順便提一下,目前,幾乎所有的Android程式在編譯時都使用了“程式碼混淆”技術,反編譯後的結果和原始程式碼還是有一定差距,但不影響我們理解程式的主體架構。“程式碼混淆”可以有效地保護智慧財產權,防止某些不法分子惡意剽竊,或者篡改原始碼(如新增廣告程式碼、植入木馬等),建議大家在實際的專案開發中儘量採用。
2.9 NDK Build
我們知道Android系統下的應用程式主要是由Java語言開發的,但這並不代表它不支援其他語言,比如C++和C。事實上,不同型別的應用程式對程式語言的訴求是有區別的——普通Application的UI介面基本上是靜態的,所以,利用Java開發更有優勢;而遊戲程式,以及其他需要基於OpenGL(或基於各種Game Engine)來繪製動態介面的應用程式則更適合採用C或者C++語言。
伴隨著Android系統的不斷髮展,開發者對於C/C++語言的需求越來越多,也使得Google需要不斷完善它所提供的NDK工具鏈。NDK的全稱是Native Development Kit,可以有效支撐Android系統中使用C/C++等Native語言進行開發,從而讓開發者可以:
完成同樣的功能,Java虛擬機器理論上來說比C/C++要耗費更多的系統資源。因而,如果程式本身對執行效能要求很高的話,建議利用NDK進行開發。
好處是顯而易見,即最大程度地避免重複性開發。
NDK的官方網址是:
developer.android.com/ndk/index.h…複製程式碼
它的安裝很簡單,在Windows下只要下載一個幾百MB的自解壓包然後雙擊開啟它就可以了。NDK資料夾可以被放置到磁碟中的任何位置,不過為了操作方便,建議開發者可以設定系統環境變數來指向其中的關鍵程式。NDK既支援Java和C/C++混合程式設計的模式,也允許我們只開發純Native實現的程式。前者需要用到JNI技術(即Java Native Interface),它的神奇之處在於可以讓兩種看似沒有瓜葛的語言間進行無縫的呼叫。例如下面是一個JNI的例項:
public class MyActivity extends Activity {
/*
Native method implemented in C/C++
*/
public <strong>native</strong> void jniMethodExample();
}複製程式碼
MyActivity是一個Java類,它的內部包含一個宣告為Native的成員變數,即jniMethodExample。這個函式的實現是通過C/C++完成的,並被編譯成so庫來供程式載入使用。更多JNI的細節,我們將在後續章節進行詳細介紹。
本小節我們將通過一個具體例項來著重講解如何利用NDK來為應用程式執行C/C++的編譯。
在此之前,請確保你已經下載並解壓了NDK包,併為它設定了正確的系統環境變數。這個例子中將包含如下幾個檔案,我們統一放在一個JNI資料夾中:
Android.mk用於描述一個Android的模組,包括應用程式、動態庫、靜態庫等。它和我們本章節講解的用法基本一致,因而不再贅述。
Application.mk用於描述你的程式中所用到的各個Native模組(可以是靜態或者動態庫,或者可執行程式)。這個指令碼中常用的變數不多,我們從中挑選幾個核心的來講解:
1.APP_PROJECT_PATH
指向程式的根目錄。當然,如果你是按照Android系統預設的結構來組織工程檔案的話,這個變數是可選的。
2.APP_OPTIM
用於指示當前是release或者debug版本。前者是預設的值,將會生成優化程度較高的二進位制檔案;除錯模式則會生成未優化的版本,以便保留更多的資訊來幫助開發者追蹤問題。在AndroidManifest.xml的<application>標籤中宣告android:debuggable會將預設值變更為debug,不過APP_OPTIM的優先順序更高,可以過載debuggable的設定。
3.APP_CFLAGS
設定對全體module有效的C/C++編譯標誌。
4.APP_LDFLAGS
用於描述一系列連結器標誌,不過只對動態連結庫和可執行程式有效。如果是靜態連結庫的情況,系統將忽略這個值。
5.APP_ABI
用於指示編譯所針對的目標Application Binary Interface,預設值是armeabi。可選值如表2-11所示。
表2-11 可選值
指 令 集 | ABI值 |
---|---|
Hardware FPU instructions on ARMv7 based devices | APP_ABI := armeabi-v7a |
ARMv8 AArch64 | APP_ABI := arm64-v8a |
IA-32 | APP_ABI := x86 |
Intel64 | APP_ABI := x86_64 |
MIPS32 | APP_ABI := mips |
MIPS64 (r6) | APP_ABI := mips64 |
All supported instruction sets | APP_ABI := all |
檔案testNative.cpp中的內容就是程式的原始碼實現,對此NDK官方提供了較為完整的Samples供大家參考,涵蓋了OpenGL、Audio、Std等多個方面,有興趣的讀者可以自行下載分析。
那麼有了這些檔案後,如何利用NDK把它們編譯成最終產物呢?
最簡單的方式就是採用如下的命令:
cd <project>
$ <ndk>/ndk-build複製程式碼
其中ndk-build是一個指令碼,等價於:
$GNUMAKE -f <ndk>/build/core/build-local.mk
<parameters>複製程式碼
<ndk>指的是NDK的安裝路徑。
可見使用NDK來編譯還是相當簡單的。另外,除了常規的編譯外,ndk-build還支援多種選項,譬如:
“clean”表示清理掉之前編譯所產生的各種中間檔案;
“-B”會強制發起一次完整的編譯流程;
“NDK_LOG=1”用於開啟NDK的內部log訊息;
……
2.10 第三方ROM的移植
除了本章所描述的Android原生程式碼外,開發人員也可以選擇一些知名的第三方開源ROM來進行學習,譬如CyanogenMod。
CyanogenMod(簡稱CM)的官方網址如下:
它目前的最新版本是基於Android 6.0的CM 13,並同時支援Google Nexus、HTC、Huawei、LG等多個品牌的眾多裝置。CyanogenMod的初衷是將Android系統移植到更多的沒有得到Google官方支援的裝置中,所以有的時候CM針對某特定裝置的版本更新時間可能比裝置廠商來得還要早。
那麼CyanogenMod是如何做到針對多種裝置的移植和適配工作的呢?我們將在接下來的內容中為大家揭開這個問題的答案。圖2-30是CM的整體描述圖。
▲圖2-30 CM的整體描述
下面我們分步驟進行講解。
Step1. 前期準備
在做Porting之前,有一些準備工作需要我們去完成。
(1)獲取裝置的Product Name、Code Name、Platform Architecture、Memory Size、Internal Storage Size等資訊
這些資料有很多可以從/system/build.prop檔案中獲得,不過前提條件是手機需要被root。
(2)收集裝置對應的核心原始碼
根據GPL開源協議的規定,Android廠商必須公佈受GPL協議保護的內容,包括核心原始碼。因而實現這一步是可行的,只是可能會費些周折。
(3)獲取裝置的分割槽資訊
Step2. 建立3個核心資料夾
分別是:
裝置特有的配置和程式碼將儲存在這個路徑下。
這個資料夾中的內容是從原始裝置中拉取出來的,由此可見主要是那些沒有原始碼可以生成的部分,例如一些二進位制檔案。
專門用於儲存核心版本原始碼的地方。
CM提供了一個名為mkvendor.sh的指令碼來幫助建立上述資料夾的“雛形”,有興趣的讀者可以參見build/tools/device/mkvendor.sh檔案。不過很多情況下還需要開發者手工修改其中的部分檔案,例如device目錄下的BoardConfig.mk、device_[codename].mk、cm.mk、recovery.fstab等核心檔案。
Step3. 編譯一個用於測試的recovery image
編譯過程和普通CM編譯的最大區別在於選擇make recoveryimage。如果在recovery模式下發現Android裝置的硬體按鍵無法使用,那麼可以嘗試修改/device/[vendor]/[codename]/recovery/ recovery_ui.cpp中的GPIO配置。
Step4. 為上述的device目錄建立github倉庫,以便其他人可以訪問到。
Step5. 填充vendor目錄
可以參考CM官網上成熟的裝置範例提供的extract-files.sh和setup-makefiles.sh,並據此完成適合自己的這兩個指令碼。
Step6. 通過CM提供的編譯命令最終編譯出ROM升級包,並利用前面生成的recovery來將其刷入到裝置中。這個過程很可能不是“一蹴而就”的,需要不斷除錯和修改,直至成功。
當然,限於篇幅我們在本小節只是講解了CM升級包的核心製作過程,讀者如果有興趣的話可以查閱www.cyanogenmod.org/來獲取更多細節詳情。