本文摘自人民郵電出版社異步社區《深刻理解Android內核設計思想(第2版)(上下冊)》
html
在分析Android源碼前,首先要學會如何下載和編譯系統。本章將向讀者完整地呈現Android源碼的下載流程、常見問題以及處理方法,並從開發者的角度來理解如何正確地編譯出Android系統(包括原生態系統和定製設備)。java
後面,咱們將在此基礎上深刻到編譯腳本的分析中,以「庖丁解牛」的方式來還原一個龐大而嚴謹的Android編譯系統。python
Git是一種分佈式的版本管理系統,最初被設計用於Linux內核的版本控制。本書工具篇中對Git的使用方法、原理框架有比較詳細的剖析,建議讀者先到相關章節閱讀了解。linux
Git的功能很是強大,速度也很快,是當前不少開源項目的首選工具。不過Git也存在必定的缺點,如相對於圖形界面化的工具沒那麼容易上手、須要對內部原理有必定的瞭解才能很好地運用、不支持斷點續傳等。android
爲此,Google提供了一個專門用於下載Android系統源碼的Python腳本,即Repo。git
在Repo環境下,版本修改與提交流程是:github
因而可知,Repo與咱們在工具篇中討論的Git流程有些許不一樣,差別主要體如今與遠程服務倉庫的交互上;而本地的開發仍然是以原生的Git命令爲主。下面咱們講解Repo的一些經常使用命令,讀者也能夠拿它和Git進行仔細比較。編程
同步操做可讓本地代碼與遠程倉庫保持一致。它有兩種形式。ubuntu
若是是同步當前全部的項目:windows
$ repo sync複製代碼
或者也能夠指定須要同步的某個項目:
$ repo sync [PROJECT1] [PROJECT2]…複製代碼
建立一個分支所需的命令:
$ repo start <BRANCH_NAME>複製代碼
也能夠查看當前有多少分支:
$ repo branches複製代碼
或者:
$ git branch複製代碼
以及切換到指定分支:
$ git checkout <BRANCH_NAME>複製代碼
查詢當前狀態:
$ repo status複製代碼
查詢未提交的修改:
$ repo diff複製代碼
暫存文件:
$git add複製代碼
提交文件:
$git commit複製代碼
若是是提交修改到服務器上,首先須要同步一下:
$repo sync複製代碼
而後執行上傳指令:
$repo upload複製代碼
瞭解了Repo的一些常規操做後,這一小節接着分析Android源碼下載的全過程。這既是剖析Android系統原理的前提,也是讓不少新手感到困惑的地方——源碼下載能夠做爲初學者瞭解Android系統的「Hello World」。
值得一提的是,Android官方建議咱們務必確保編譯系統環境符合如下幾點要求:
在虛擬機上或是其餘不支持的系統(例如Windows)上編譯Android系統也是可能的,事實上Google鼓勵你們去嘗試不一樣的操做系統平臺。不過Google內部針對Android系統的編譯和測試工做大可能是在Ubuntu LTS(14.04)上進行的。於是建議開發人員也都選擇一樣的操做系統版原本開展工做,經驗告訴咱們這樣能夠少走不少彎路。
若是是在虛擬機上運行的Linux系統,那麼理論上至少須要16GB的RAM/Swap纔有可能完成整個Android系統的編譯。
要特別提醒你們的是,如下全部步驟都是在Ubuntu操做系統中完成的(「#」號後面表示註釋內容)。
$ 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複製代碼
在開始下載源碼前,須要對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等信息##########
######複製代碼
完成初始化動做後,就能夠開始下載源碼了。根據上一步的配置,下載到的多是最新版本或者某分支版本的系統源碼。
$ 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),建議你們能夠對比選擇最適合本身的工具。
任何一個項目在編譯前,都首先須要搭建一個完整的編譯環境。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 |
本書所採用的宿主機是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編譯環境已經搭建完成了。
上一小節咱們創建了完整的編譯環境,可謂「萬事俱備,只欠東風」,如今就能夠執行真正的編譯操做了。
下面內容仍然採用分步的形式進行講解。
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]複製代碼
模擬器提供的啓動選項很是豐富,讀者能夠參見本書工具篇中的詳細描述。
上一小節咱們學習了原生態Android系統的編譯步驟,爲你們進一步理解定製設備的編譯流程打下了基礎。Android系統發展到今天,已經在多個產品領域獲得了普遍的應用。相信有一個問題是不少人都想了解的,那就是如何在原生態Android系統中添加本身的定製產品。
仔細觀察整個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中移除。
開發人員在定製一款新設備時,能夠根據實際狀況將本身的配置信息添加到上述幾個組成部分中,以保證設備的正常運行。
不一樣產品的硬件配置每每是有差別的。好比某款手機配備了藍牙芯片,而另外一款則沒有;即使是都內置了藍牙模塊的兩款手機,它們的生產商和型號也極可能不同——這就不可避免地要涉及內核驅動的移植。前面咱們分析的編譯流程只針對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複製代碼
整個內核的編譯相對簡單,讀者能夠自行嘗試。
將編譯生成的可執行文件包經過各類方式寫入硬件設備的過程稱爲燒錄(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模式,使得後者逐步超出了原先的設計範疇,成爲普通消費者執行設備升級操做的首選方式。咱們將在後續小節中對此作更詳細的講解。
早期的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分別用於指定兩種狀況下的應用程序名稱。
經過前面幾個小節的學習,咱們已經按照產品需求編譯出自定製的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 關係圖
理解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的文件結構
各組成部分以下:
存儲內核啓動「頭部」—— 內核啓動參數等信息,佔據一個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實例
內核程序是整個Android系統的基礎,也被「裝入」boot.img中——咱們能夠經過--kernel選項來指定內核映射文件的存儲路徑。其所佔據的大小爲:
n pages=(kernel_size + page_size - 1) / page_size複製代碼
由此能夠看出,boot.img中的各元素必須是頁對齊的。
不只是kernel,boot.img中也包含了ramdisk.img。其所佔據大小爲:
m pages=(ramdisk_size + page_size - 1) / page_size複製代碼
可見也是頁對齊的。
其餘關於ramdisk的詳細描述請參照下一小節,這裏先不作解釋。
這一項是可選的。其佔據大小爲:
o pages= (second_size + page_size - 1) / page_size複製代碼
這個元素一般用於擴展功能,默認狀況下能夠忽略。
不管什麼類型的文件,從計算機存儲的角度來講都只不過是一堆「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系統的啓動過程當中發揮重要做用。
要將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節點中的相關內容。
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還有很是多的技術細節,限於篇幅咱們不作過多討論,但強烈建議讀者自行查閱相關資料作進一步深刻學習。
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所佔的體積更小,於是天然能夠爲系統節省更多的存儲空間。
前面咱們討論了系統包燒錄的幾種傳統方法,而Android系統其實還提供了另外一種全新 的升級方案,即OTA(Over the Air)。OTA很是靈活,它既能夠實現完整的版本升級,也能夠作到增量升級。另外,用戶既能夠選擇經過SD卡來作本地升級,也能夠直接採用網絡在線升級。
不管是哪一種升級形式,均可以總結爲3個階段:
下面咱們來逐一分析這3個階段。
升級包也是由系統編譯生成的,其編譯過程本質上和普通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-24所示,有兩種常見的渠道能夠獲取到OTA升級包,分別是在線升級和本地升級。
▲圖2-24 獲取OTA升級包的兩種方式
開發者將編譯生成的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 在線升級圖例
OTA升級包並不是必定要經過網絡在線的方式才能夠下載到——只要條件容許,就能夠從其餘渠道獲取到升級文件update.zip,並複製到SD卡的根目錄下,而後手動進入升級模式(見下一小節)。
在線升級和本地升級各有利弊,開發商應根據實際狀況來提供最佳的升級方式。
通過前面小節的講解,如今咱們已經準備好系統升級文件了(不管是在線仍是本地升級),接下來就進入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不但負責OTA的升級,並且也是「恢復出廠設置」的實際執行者,如圖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源文件的邏輯層次比較清晰,讀者能夠基於上述流程的描述來對照並閱讀代碼。
目前咱們已經學習了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程序在編譯時都使用了「代碼混淆」技術,反編譯後的結果和原始代碼仍是有必定差距,但不影響咱們理解程序的主體架構。「代碼混淆」能夠有效地保護知識產權,防止某些不法分子惡意剽竊,或者篡改源碼(如添加廣告代碼、植入木馬等),建議你們在實際的項目開發中儘可能採用。
咱們知道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模塊(能夠是靜態或者動態庫,或者可執行程序)。這個腳本中經常使用的變量很少,咱們從中挑選幾個核心的來說解:
指向程序的根目錄。固然,若是你是按照Android系統默認的結構來組織工程文件的話,這個變量是可選的。
用於指示當前是release或者debug版本。前者是默認的值,將會生成優化程度較高的二進制文件;調試模式則會生成未優化的版本,以便保留更多的信息來幫助開發者追蹤問題。在AndroidManifest.xml的<application>標籤中聲明android:debuggable會將默認值變動爲debug,不過APP_OPTIM的優先級更高,能夠重載debuggable的設置。
設置對全體module有效的C/C++編譯標誌。
用於描述一系列連接器標誌,不過只對動態連接庫和可執行程序有效。若是是靜態連接庫的狀況,系統將忽略這個值。
用於指示編譯所針對的目標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消息;
……
除了本章所描述的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/來獲取更多細節詳情。