本文摘自人民邮电出版社异步社区《深刻理解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/来获取更多细节详情。