[TOC]
概述
这是一篇未完成的实验报告,因为在写报告之时,笔者的实验并没有成功,后续还有很多工作需要完成。而笔者所做的项目在之前几乎没有任何可参考的先例,只有适合于其他设备方案的资料参照,可谓“摸着石头过河”。在毕业论文截止之际,笔者先将截至2018年9月初所得到的研究成果记录下来,暂且作为本次毕业论文的内容。
华为Ascend P6(以下简称P6),是华为于2013年推出的一款旗舰手机,属P系列旗舰的第六款,也是华为海思(HiSillicon)处理器应用的里程碑,具有重要的战略和纪念意义。然而,就是这样一款里程碑旗舰,却一直被广大Android开发者忽视,以至于推出5年多,都没有开发者为其适配历代最新的Android系统[^Ref_HD2]。
而Android系统的适配,本身更是一项需要大量精力投入的工作,涉及到配置文件的收集、编写,长期多方面的调试,以及潜在问题的跟踪,其中的调试环节更是需要细致耐心,因为在一款完全没有运行对应版本Android系统的设备上,随时都会出现连原厂开发者均难以发现的问题。
笔者愿意为这项漫长的工作而付出,这不仅是因为P6在国产芯片发展史上的重大实践意义,也是为了以此来锻炼自己的科研能力。这些工作最终的意义,于Android开发者界,于笔者本人,都是有意义的。
注:正文部分关于shell的代码块,若将命令和输出放在一起,则命令前加上提示符
$
,否则不加。
一、背景介绍
(1)华为Ascend P6
华为Ascend P6是华为P系列旗舰家族的成员,于2013年推出。它是华为首款搭载海思处理器的旗舰机,在华为乃至国产手机芯片的发展史上拥有里程碑的重要意义。配置如下所示:
要素 | 配置情况 |
---|---|
型号与网络 | P6-T00:移动版,支持TD-SCDMA、GSM P6-C00:电信版,支持CDMA、GSM P6-U06:联通版,支持WCDMA、GSM |
CPU | HiSillicon K3V2 |
架构 | ARM Cortex-a7 32位 |
RAM | 2.0GB LPDDR3 |
ROM | 16GB eMMC |
屏幕 | 4.8英寸LCD 1280x720 |
操作系统 | 预装 Android 4.2.2,采用华为EMUI 2.0界面 官方可更新至Android 4.4(EMUI 3.0)[^stopped_support] |
内核版本 | Linux 3.0.8 |
移动网络 | 支持三大运营商3G、2G网络(运营商定制型号,非全网通) 双卡,副卡只支持GSM |
无线通信 | WLAN、蓝牙 |
笔者所持有的P6为电信版本P6-C00。
(2)Android系统开放性及其弊端
Android是谷歌推出的一款开放源代码的移动操作系统,遵循Apache 2.0协议。任何开发者,包括厂商和个人开发者,均可从Android系统的官方网站(source.android.com [^android_website_in_China])获取源代码。它与生俱来的这一开放性,使得大量的开发者在其基础上开发了各种定制版本,真正将其“为我所用”。2018年,最新的正式版是Android 8.1 Oreo。
现阶段,Android系统的定制版本分为两大方向:
一是厂商进行定制,发布到自家的产品上,我们能购买到的设备产品(如手机、平板电脑、智能手表等)所搭载的Android系统均属于该类型。这里的定制,不仅仅包括在Android官方源代码的基础上进行功能的定制,还包括各种二次开发。最著名的例子就是小米的MIUI、华为的EMUI、魅族的Flyme OS,以及锤子科技的Smartisan OS。一般而言,厂商多不会将自己的定制系统进行开源,这就意味着只有官方的设备才能使用,并获得长期的技术支持。
二是定制后保持开源。这样的Android定制版本多在原生Android的基础上做出一些功能上的改进,推出新功能,从而成为更具易用性的系统。它们延续Android官方源代码的策略,继续保持开源,这使得广大开发者可以适配出适合于自己设备的Android系统。著名的有魔趣(MoKee)、LineageOS、OmniROM、ResurrectionRemix等。
然而,Android系统的开放性,造成了一个不可忽视的弊端,即碎片化。在开放性的背景下,Android系统版本的升级由厂商负责,而不是谷歌官方,但并不是所有的厂商都能及时响应Android版本的更新,从而导致一个新版本Android系统推出后,仍有大量的设备在使用旧版本的系统,一些设备最新的系统版本甚至定格在了4.4以下版本,甚至古老的2.3。如此现状导致了双重后果:安全性掉队(谷歌每推出一个Android更新,都会带来大量的安全性改进),以及用户体验落后。
本报告中的目标设备P6,就是一个典型的例子。官方在推出它的第二年后,就放弃了对其系统更新的支持,这使得它能使用的最新Android版本永远地定格在了4.4。即使2016年Android 6.0推出,2017年Android 7.0推出,官方也不为所动。如此态度,对于一款昔日的旗舰,着实不公。
二、正式适配计划与准备
(一)笔者的工作环境
笔者计划为P6适配的系统为Android 8.1 Oreo,选用开源定制版本OmniROM。在Android开源定制版本中,OmniROM技术成熟,适配过程相对简便,特选择之。用于编译适配的计算机为笔者的惠普Pavilion笔记本,操作系统为Ubuntu 18.04 64-bit。
计算机已配置好编译环境,并下载了源代码。
笔者计算机的配置如下:
要素 | 配置情况 | 备注 |
---|---|---|
型号 | HP Pavilion Notebook | 惠普畅游人笔记本电脑,购买于2015年 |
CPU | Intel Core i7-5500U 2.40GHz(睿频≈2.85Ghz) | 第五代酷睿i7处理器,低压移动版 |
GPU | Intel HD Graphics NVidia Geforce 940M |
双显卡,但在Linux中只能使用核显 |
内存 | 8GB LPDDR3 | |
固态硬盘 | ADATA SP550 240GB | 操作系统与虚拟内存安装于固态硬盘上 |
机械硬盘 | WD 1TB 5400转 | Android源代码搭载于机械硬盘上 |
操作系统 | Ubuntu 18.04 LTS 64-bit |
(二)准备设备配置文件
Android是一个与具体硬件密切相关的系统。要想使系统在具体的设备上运行,必须要准备好合适的配置文件,以此为编译系统的依据。对于P6,需要准备以下配置文件:
配置文件 | Android源码的目标路径 | 作用 |
---|---|---|
设备参数文件 | device/huawei/hwp6_u06 |
配置文件的核心,用以在Android源码中声明设备,并指定构建系统所需的一系列参数 |
内核源代码 | kernel/huawei/hwp6_u06 |
Linux内核的源代码,是运行Android的基石 |
厂商配置文件 | vendor/huawei |
存放厂商提供的专有文件,设备正常运行必需 |
Android目标设备的多样性,使得编写配置文件需要庞大的工作量。幸运的是,在此前,GitHub上有开发者已经做好了第三方Android的适配,因此可以在其中找到现成的配置文件。依次以device huawei p6
、kernel huawei p6
与vendor huawei p6
为关键字搜索即可。
笔者选用了以下配置文件:
- 设备参数文件(由marlintoe提供):https://github.com/marlontoe/device_huawei_hwp6_u06.git
- 内核源代码(由surdupetru提供):https://github.com/surdupetru/huawei-p6.git
- 厂商配置文件(由marlintoe提供)
(三)设备配置文件介绍
值得注意的是,以上设备配置文件并非“开箱即用”的,在将这些文件复制到对应位置后,并不能马上开始编译。这是因为上述作者编写的配置文件仅适用于旧版本的OmniROM,具体为Android 4.4。然而时过境迁,新版本的Android编译系统与原先4.4时期有很大不同,并且原作者在编写时并未考虑到将来出现的Android系统版本。若直接使用这些基于旧版本的配置文件进行编译,则必定会出现各种错误。
为了让这些配置文件能够适应笔者拥有的新版本编译系统,须根据新版本源码和设备的具体情况,对原始配置文件进行修改。以下简要介绍配置文件的构成,后续会涉及到修改的过程。
1. 设备参数文件
P6的设备参数文件(device/huawei/hwp6_u06
)核心构成如下表所示(只列出最基本的文件)。其中.mk
后缀的是Makefile文件,.sh
为Shell脚本:
文件名/目录名 (结尾带“ / ”的为目录) |
作用 |
---|---|
Android.mk |
入口文件,编译系统包含(include)它,以将设备参数文件纳入编译系统中 |
AndroidProducts.mk |
设备参数文件的核心,编译系统会在读取参数文件时先读取它 该文件会自动包含 omni_hwp6_u06.mk 与full_hwp6_u06.mk |
AndroidBoard.mk |
类似于AndroidProducts.mk ,让编译系统包含一些附加的编译目标(如预编译的内核)。这里不需要。 |
BoardConfig.mk |
以<变量名> := <值> 的格式,集中设定各种与设备特性有关的参数,以及一些组件的位置(如内核源码路径、内核配置文件名) |
omni_hwp6_u06.mk |
OmniROM配置的入口,在其中定义了设备的基本信息。是OmniROM编译系统识别一套设备参数文件不可或缺的依据。 |
device_hwp6_u06.mk |
omni_hwp6_u06.mk 的补充,被其包含,用于列出预编译好的、需要复制到系统中的文件 |
full_hwp6_u06.mk |
AOSP配置的入口,从谷歌原生的Android源码承继而来,用不到。 |
overlay/ |
叠加层文件,定义对Android自带系统程序(如“设置”)界面的调整,如添加/删除设置项、调整布局 |
prebuilt/ |
存放预编译好的、需要安装于设备中的文件 |
recovery/ |
需要放置于Recovery根文件系统(rootfs)的额外文件,通常是厂商专用的文件 |
root/ |
需要放置于根文件系统(rootfs)的额外文件,通常是厂商专用的文件 |
sepolicy/ |
存放设备专用的SELinux策略文件 |
vendorsetup.sh |
将当前设备的编译目标添加到编译系统中,添加后用户可在编译系统中选择编译该设备 会被编译系统的初始化脚本 build/envsetup.sh 自动包含 |
2. 内核源代码
P6的内核源代码存放于kernel/huawei/hwp6_u06
,文件构成与一般的Linux内核源码一致。区别仅在于:
- 内核源码根目录中有一个
Android.mk
作为入口文件,指定编译内核的一系列Makefile规则。 - 内核源码架构目录
arch/arm/configs
下有P6专用的配置文件cm_k3v2oem1_defconfig
。
3. 构建变量和产品变量
用户在编译时可以通过指定构建变量(build variable),来选择编译Android系统的类型。不同编译类型面向不同类型的用户,它们的区别在于对调试功能的开放程度。可选的构建变量如下:
构建变量 | 说明 |
---|---|
release |
正式版本,供最终用户使用。我们购买的手机、平板电脑等设备即属于此类型。它默认禁用了各种调试功能,旨在提供一个稳定的系统 |
userdebug |
调试版本,是用户自行编译源码的一般选择。提供一些基本的调试功能,如默认打开ADB、允许禁用SELinux |
eng |
工程版本,由厂商使用。在userdebug 的基础上提供了更多的调试选项,开放程度为三者之最,但更不适合最终用户使用 |
面对日益纷繁复杂的Android源码,从Android 7.0起,谷歌开发了一套全新的编译系统Soong,旨在改善构建Android系统的体验。在Soong体系中,引入了一个新的概念——产品变量(product variable),从产品的视角来看待编译选项的设置。它的本质与构建变量相同,只是将userdebug
与eng
归为debuggable
。但亦允许用户在专用的配置文件Android.bp
中,于debuggable
基础上再为这两种构建变量指定专用的参数。
三、可行性测试:为P6编译TWRP Recovery 3.1.0.0
确定一台设备有没有适配新系统的可能性,最优的起步点即为为该设备编译一个TWRP Recovery。Recovery是Android系统的恢复环境,支持在设备上进行刷机与基于系统底层的管理;TWRP则是一款专业的触屏Recovery环境。TWRP能正常运行,就说明接下来的努力就有了实现的空间,手上的这台P6自然能焕发第二春。本质上,TWRP Recovery是一个特制的启动映像(boot image),文件结构包括文件头(含内核启动参数)、Linux内核、RAMdisk与DTB(Device Tree Binary,编译成二进制的设备树。华为P6不需要),其中的RAMdisk包含了TWRP的主程序和运行环境。它的结构与Android本身的启动镜像完全相同,因此在正式测试Android系统的启动过程之前,可以以TWRP作为参照。
(一)准备工作
删除OmniROM自带的谷歌官方Recovery环境,位于bootable/recovery
。然后用git
,将TWRP的源码下载到这个目录中:
1 |
|
(二)修改设备参数文件
设备参数文件目录device/huawei/hwp6_u06
中的BoardConfig.mk
中记录了设备相关的参数,TWRP编译时所使用的参数也列于其中。
编译前,确保下面的参数设置正确:
TW_THEME
- TWRP必填参数,设定Recovery所用的主题。根据设备分辨率和屏幕方向的不同,可选的值为
landscape_hdpi
、landscape_mdpi
、portrait_hdpi
、portrait_mdpi
、watch_mdpi
。华为P6选用portrait_hdpi
。
- TWRP必填参数,设定Recovery所用的主题。根据设备分辨率和屏幕方向的不同,可选的值为
TARGET_KERNEL_SOURCE
- 内核源码所在位置。设置为
kernel/huawei/hwp6_u06
。
- 内核源码所在位置。设置为
TARGET_KERNEL_CONFIG
- 内核配置文件,指定编译内核的参数。华为P6选用
cm_k3v2oem1_defconfig
。
- 内核配置文件,指定编译内核的参数。华为P6选用
启动镜像的参数
启动镜像的参数因设备而异,原作者已经配置好。这些参数如下所示:
1
2
3
4
5
6
7
8
9
10
11# 内核命令参数
BOARD_KERNEL_CMDLINE := vmalloc=512M k3v2_pmem=1 mmcparts=mmcblk0:p1(xloader),p3(nvme),p4(misc),p5(splash),p6(oeminfo),p7(reserved1),p8(reserved2),p9(splash2),p10(recovery2),p11(recovery),p12(boot),p13(modemimage),p14(modemnvm1),p15(modemnvm2),p16(system),p17(cache),p18(cust),p19(userdata);mmcblk1:p1(ext_sdcard)
# 内核偏移
BOARD_KERNEL_BASE := 0x00000000
# 内核的页大小
BOARD_KERNEL_PAGESIZE := 2048
# 启动映像生成工具(mkbootimg)传入的参数
# 指定的--ramdisk-offset为RAMDisk在映像文件中的偏移
BOARD_MKBOOTIMG_ARGS += --ramdisk_offset 0x01400000
# 内核文件名。文件名决定了内核被压缩而成的格式
BOARD_KERNEL_IMAGE_NAME := zImage
禁用以下选项[^how_to_disable_them],否则会导致编译出的Recovery出现问题:
×TARGET_PREBUILT_KERNEL
- 使用预编译的内核而不是从源码编译。原作者已经提供了一个内核,本用于编译旧版本Android 4.4系统与2.x系列的TWRP,但笔者实验证明,旧内核缺少新版本Android所必需的功能,导致Recovery无法启动。这一点会在后面的实验中证明。
×TARGET_PREBUILT_RECOVERY_KERNEL
- 编译Recovery时使用预编译的内核而不是从源码编译。原因同上。
×TARGET_RECOVERY_INITRC
- 使用用户提供的
init.rc
。init.rc
是Android初始化程序init
解析的主脚本,使用Android初始化语言(Android init language)编写,指定了系统启动时需要执行的一些工作。原作者编写了一个init.rc
文件,位于device/huawei/hwp6_u06/recovery/init.rc
,但是该文件是为旧版本Android编写的,其中的很多配置与新版本的Android系统不兼容;且其中混合了Recovery启动时必需的代码与设备特定的代码,前者已由TWRP配备,直接照搬原作者提供的文件必会导致冲突。
- 使用用户提供的
(三)修改内核源码Android.mk
文件
Android编译系统中的Android.mk
,是每一个编译目标的入口文件,包含目标编译所需的规则。编译系统通过扫描并包含这些文件,来得出Android系统的编译目标。对于内核,若要在编译系统时一并编译内核源码以得到一个新内核,则须在内核源码目录中编写Android.mk
。
原作者提供的内核源码中已经包含了Android.mk
,但是它也是为旧版本Android系统编写的,其中的编译规则并不适用于Android 8.1,甚至还会使Android 8.1的编译系统报错。因此必须修改。
以下的diff
补丁显示了笔者修改的内容,其中用一连串#
号包围的行作为修改说明,遵循Makefile的语法规则。
1 |
|
(四) 尝试编译并刷入Recovery启动映像
以上准备工作完成后,笔者尝试编译Recovery映像,并将其刷入设备中尝试启动。
Android的启动映像存储于boot
和recovery
两个分区中,前者用于启动Android系统,后者则存放Recovery映像。为调试方便,笔者将“ATX”团队的一款ClockworkMod Recovery做了精简(下文简称“ATX”),刷入boot
分区中,让手机正常启动时能够进入该款Recovery中[^why_prune]。但是,实际使用时,由于精简过度,进入ATX后手机黑屏不能操作,只能连接电脑使用ADB(Android Debug Bridge)管理手机。精简后只保留Busybox与adbd
(Android Debug Bridge Daemon)功能,用于连接电脑进行调试。
回到OmniROM源码根目录,依次运行以下命令开始编译:
1 |
|
编译输出的结果在out/target/product/hwp6_u06
中,其中生成的Recovery映像文件名为recovery.img
。需要进入设备的引导模式(bootloader),并使用Android官方的刷机工具fastboot
进行刷入。将设备打开电源,连接电脑,正常进入ATX ,并在终端中运行如下命令:
1 |
|
从ATX进入Bootloader模式的标志是屏幕短暂黑屏,随后长时间定格在开机第一屏上;而从Bootloader模式重启回到ATX模式的标志,则是在显示开机画面一段时间后屏幕关闭。可靠起见,使用下面两个命令判断设备是否与电脑正确连接,若连接成功则会输出设备的ID:
1 |
|
(五)问题一:开机画面反复循环
进入Recovery模式之后,机器一直未进入Recovery,而是显示开机画面一段时间后,立即黑屏重启,再度显示开机画面,又再度重启,如此不断循环。而强制按电源键重启后,又能正常回到ATX模式。笔者怀疑,问题就集中在刚编译的Recovery映像上。
根据启动映像制作工具mkbootimg
的源码说明(位于system/core/mkbootimg/bootimg.h
),启动映像由以下四个部分组成:
要素 | 说明 | 占用页数 |
---|---|---|
boot header | 启动映像的文件头 | 1 |
kernel | Linux内核映像 | n |
ramdisk | 启动内存盘镜像 | m |
second stage | 第二阶段部分,即附加的部分(如DTB) | o |
页数以字节为单位,海思K3V2平台指定为2048
。三个量n、m、o之间的关系如下:
n = (kernel_size + page_size - 1) / page_size
m = (ramdisk_size + page_size - 1) / page_size
o = (second_size + page_size - 1) / page_size
K3V2平台不需要second stage,因此问题只能出现在剩下的三个部分中,笔者计划从第一个部分boot header开始检验。boot header由启动映像制作工具mkbootimg
生成,不同的工具可能会生成不同的header,因此笔者决定,用若干个相关工具对Recovery映像进行重新打包,再刷入测试。
用于测试的工具如下所示(假设这些工具均已位于$PATH
中):
bootimg.py
- 由中国开发者Liu DongMiao(liudongmiao@gmail.com)开发,使用Python 2.7编写,支持快速解包/打包启动映像。默认存储文件名固定,可以不需指定额外参数
Android官方提供的
mkbootimg
工具源码位于Android源码目录的
system/core/mkbootimg
中选用下列Android源码所附带的版本。其中除Omni 8.1自带者用Python编写外,其余均用C语言编写:
- Omni 4.3
- Omni 4.4
- Omni 6.0
- Omni 8.1
对于C语言编写者,在所在目录下用以下命令进行编译。编译所得的文件名加上与Android版本对应的后缀,如
mkbootimg-4.3
:1
2gcc mkbootimg.c ../libmincrypt/sha.c -I../include -o mkbootimg
gcc unpackbootimg.c ../libmincrypt/sha.c -I../include -o unpackbootimg
测试的具体步骤如下:
将
recoveryimage.img
移动到/tmp/recimg_modify
目录中,重命名为boot.img
。这是bootimg.py
的要求。进入
/tmp/recimg_modify
,使用bootimg.py
进行解包。解包过程会输出boot.img
的一些信息:1
2
3
4
5
6
7
8
9
10
11
12$ bootimg.py --unpack-bootimg
arguments: [bootimg file]
bootimg file: boot.img
output: kernel[.gz] ramdisk[.gz] second[.gz]
kernel_addr=0x8000
ramdisk_addr=0x1400000
second_addr=0xf00000
tags_addr=0x100
page_size=2048
name=""
cmdline="vmalloc=512M k3v2_pmem=1 mmcparts=mmcblk0:p1(xloader),p3(nvme),p4(misc),p5(splash),p6(oeminfo),p7(reserved1),p8(reserved2),p9(splash2),p10(recovery2),p11(recovery),p12(boot),p13(modemimage),p14(modemnvm1),p15(modemnvm2),p16(system),p17(cache),p18(cust),p19(userdata);mmcblk1:p1(ext_sdcard) buildvariant=userdebug"
padding_size=2048解包后,得到
kernel
、ramdisk.gz
和bootimg.json
三个文件。其中bootimg.json
记录了启动镜像的参数。使用
bootimg.py
回打包。由于回打包会覆盖原始文件boot.img
,故需将原始文件改名为boot-old.img
,然后再运行打包命令:1
bootimg.py --repack-bootimg
使用
mkbootimg-6.0
回打包:1
2
3
4
5mkbootimg-6.0 \
--kernel kernel --ramdisk ramdisk.gz \
--cmdline "vmalloc=512M k3v2_pmem=1 mmcparts=mmcblk0:p1(xloader),p3(nvme),p4(misc),p5(splash),p6(oeminfo),p7(reserved1),p8(reserved2),p9(splash2),p10(recovery2),p11(recovery),p12(boot),p13(modemimage),p14(modemnvm1),p15(modemnvm2),p16(system),p17(cache),p18(cust),p19(userdata);mmcblk1:p1(ext_sdcard) buildvariant=userdebug" \
--base 0x0000 --ramdisk_offset 0x1400000 --kernel_offset 0x8000 --pagesize 2048 \
-o bootimg-6.0.img依次再使用
mkbootimg-4.4
、mkbootimg-4.3
和mkbootimg-8.1
打包。命令行与mkbootimg-6.0
相同,只是程序名和输出文件名表示系统版本的后缀要作相应修改,最终得到bootimg-4.4.img
与bootimg-4.3.img
。最后依次将上述步骤所得到的启动映像刷入设备中,一一进行测试,并记录结果。
测试的结果如下所示:
映像文件名 | 生成工具 | 启动结果 |
---|---|---|
boot.img |
bootimg.py |
不再开机画面无限重启 |
bootimg-6.0.img |
mkbootimg-6.0 |
仍然无限重启 |
bootimg-4.4.img |
mkbootimg-4.4 |
仍然无限重启 |
bootimg-4.3.img |
mkbootimg-4.3 |
不再开机画面无限重启 |
bootimg-8.1.img |
mkbootimg-8.1 |
不再开机画面无限重启 |
理论上,上述所有工具均使用同样的kernel
与ramdisk
进行打包,最终所得映像的相应部分会完全相同。而不同的地方,则正在于除此之外的部分,亦即boot header。使用十六进制比较工具(如Beyond Compare、hexdiff
)对比能启动的映像文件和不能启动的映像文件,可以发现文件头有明显的不同,表现为并不是所有的字节都对齐。使用Beyond Compare比较发现,同样的字节位置,在其中一个文件里有数值,而另一个文件却为空,二者产生差异的部分观感类似于缺粒的玉米。
而上述差异,来自mkbootimg
程序源码中对boot header文件头定义的不同。要弄清文件头差异的根源,还需从源代码中寻找答案。启动映像的文件头存放着该映像的元数据,它的定义位于system/core/mkbootimg/bootimg.h
中,为结构体boot_img_hdr
。文件头根据结构体的定义存放,每一个要素的大小即C代码中结构体元素的大小。
OmniROM 8.1中的boot_img_hdr
如下:
1 |
|
对比旧版本OmniROM 6.0的boot_img_hdr
如下:
1 |
|
更老的版本OmniROM 4.3中,boot_img_hdr
则如下:
1 |
|
对这三处代码进行三路比较,统一变量类型(unsigned char
等价于uint8_t
,unsigned
等价于uint32_t
)后,结果如下图。从中可知,8.1的数据结构与4.3基本一致,但是6.0相比前两者,则在page_size
和unused
两项之间加入了一个dt_size
,导致三者的数据结构不对齐。
至此可知,boot header格式不正确,会导致K3V2平台的Bootloader无法识别,从而无限重启。
(六)问题二:开机后定格在开机画面
以上能够突破”问题一“难关的两个Recovery映像,虽然因boot header正确而被Bootloader成功引导,但仍然并未成功,而是出现了第二个问题:定格在开机第一屏。
画面停留在开机第一屏,首先就要判断设备是否重启进入了Bootloader模式。但是,当笔者连接电脑,并使用fastboot devices
检测连接状况时,却发现该命令没有任何输出(若有设备连接则会显示设备的ID)。这说明当前设备所处在的状态并不是Bootloader,而是卡在了Recovery的引导过程中。
笔者尝试用adb devices
检测设备状态,结果adb
亦未输出设备的ID,说明设备并未在线。而电脑端adb
访问设备端的前提是设备端需要开启adbd
服务(TWRP等第三方Recovery默认启用adbd
),而adbd
服务只有在引导进入系统后才由ramdisk中的初始化程序init
进程启动。Linux内核加载后才启动init
。由上述推理,显然得知,P6的引导过程卡在了Linux内核的加载过程中。
查清故障的首要方法即为获取内核日志,但在设备无法连接电脑的情况下,很难获取日志。华为的技术人员还可以通过串口连接电脑,对内核进行底层调试,但笔者并无此条件。
(1)尝试之一:借助kmesg
启动内核
对于诊断内核问题,笔者首先想到的思路,即为借助kmesg
启动内核。kmesg
是Linux官方提供的工具,允许在系统启动后的用户态环境下,如运行程序般运行一个内核。著名的Android多系统管理器MultiROM就是用该工具来实现多系统启动的。
可以从Linux官方网站上找到该工具的源代码(https://git.kernel.org/pub/scm/utils/kernel/kexec/kexec-tools.git),并编译成适用于ARM平台的版本。笔者选用Linux提供的最新稳定版(截至2018年7月)。编译器工具链选择`gcc-arm-linux-gnueabi`的4.9版本。
具体编译的步骤如下。这里是在X86_64平台上编译ARM平台的程序,因此为交叉编译:
将源码克隆到
/tmp/kexec-tools
目录中,并切换到其中。1
2git clone https://git.kernel.org/pub/scm/utils/kernel/kexec/kexec-tools.git /tmp/kexec-tools
cd /tmp/kexec-tools初始化编译系统,并配置编译参数。
TODO:现在手上只有4.4.3的编译器!!!!
1
2
3
4
5
6
7
8
9./bootstrap # 编译配置程序使用automake编写,运行该命令以生成编译环境
# 运行编译配置程序,其中:
# --host=arm
# 交叉编译必选项,表明主机上运行所运行编译器的目标平台
# CC=arm-linux-gcc
# 指定交叉编译为目标设备编译时使用的编译器。
# LDFLAGS=-static
# 传递给链接器(linker)的参数,-static表示将程序编译为静态可执行文件
./configure --host=arm CC=arm-linux-gcc LDFLAGS=-static开始编译。
1
make -j8
编译所得的可执行文件位于子目录
build/sbin
中。必须静态编译,否则会因缺少依赖而无法在P6上独立运行。启动P6进入ATX,将
kexec
与内核传送到根目录/
。在Android编译系统中,编译好的内核位于
out/target/product/hwp6_u06/kernel
。一并将kexec
与内核复制到/tmp
,然后在/tmp
中运行:1
2adb push kexec /
adb push kernel /运行测试。执行
adb shell
,连接到设备上的终端(terminal),然后尝试运行kexec
。结果却出现了这样的错误提示:
1 |
|
查阅Quora得知,运行静态编译的可执行文件会出现这种现象,原因在于交叉编译器限制了最低内核的版本[^quora]。静态编译的可执行文件会包含运行库glibc
,而glibc
本身有内核版本的限制。使用file
工具查看kexec
的基本信息时(file build/sbin/kexec
),可以得知该可执行文件运行时所需的最低内核版本为3.2.0
(注意“for GNU/Linux`”处):
1 |
|
解决这一问题只有两种思路:升级内核,或降低编译器版本(低版本的编译器所作的版本限制会低一些)。显然,最简便的方法即为降低编译器版本,其中包括使用./configure --enable-kernel=<低于3.0.8的版本号>
重新配置并编译交叉编译器(GCC编译器本身也是有源码、可以被编译的),也包括直接使用现成的低版本编译器。编译编译器的工作比较繁琐,在有限的时间内不现实;而笔者手中正好拥有一套由FriendlyARM组织于2011年推出的gcc-arm-none-linux-gnueabi 4.4.3
编译器。
因此,笔者将编译器安装到系统中,重新进行以上步骤:
1 |
|
再次检查所生成可执行文件的信息,可以发现最低内核版本降低了不少:
1 |
|
P6 ATX的内核版本亦为3.0.8
,该版本的kexec
已经可以正常运行了。下一步,即为尝试从ATX启动新编译的内核。运行如下命令:
1 |
|
然而,对于ARM平台,kexec
还需一个参数,为--dtb
,指定设备树二进制文件。事实上,设备树并不是ARM平台的必需,没有设备树,设备仍然能够启动——运行官方Android 4.2.2~4.4系统的P6正是一个典型的例证。K3V2平台的内核源码并未提供任何与设备树有关的源文件与目标文件(以device*tree
、dts
(device tree source)为关键字检索),因此这个--dtb
参数无法满足。不过,尝试忽略它,kexec
却无法继续了,仍然强制要求指定一个设备树二进制文件:
1 |
|
至此,使用kexec
启动内核的方法得证不适用于P6。
(2)尝试之二:更换内核编译器
笔者编译运行kexec
的尝试虽然失败,但尝试过程中出现的“Kernel too old
”提示却给了我一个启发:GCC编译器版本能够影响可执行文件是否能运行,那么对于同样使用GCC编译的内核,它能否正常运行,也会与编译器有关。带着这个设想,笔者尝试亦用旧版本gcc-arm-none-linux-gnueabi 4.4.3
编译器来编译内核。
具体编译与测试的步骤如下:
切换到内核源码所在的目录下。位于Android源码目录的
kernel/huawei/hwp6_u06
。设置编译环境相关变量。
1
2
3
ARCH=arm # 目标架构
SUBARCH=arm # 第二目标架构
CROSS_COMPILE=arm-none-linux-gnueabi- # 交叉编译器前缀
- 初始化编译环境并开始编译。
1
2
3
make distclean # 清除所有临时文件
make cm_k3v2oem1_defconfig # 加载K3V2平台的配置文件
make -j8 # 开始编译
编译完成后,生成的内核镜像zImage为`arch/arm/boot/zImage`。
- 将新的内核重新打包到由
bootimg.py
解包的Recovery启动镜像中。
1
2
3
4
5
6
# 用`zImage`覆盖`/tmp/recimg_modify`下的`kernel`文件
cp arch/arm/boot/zImage /tmp/recimg_modify/kernel
# 重新打包内核
cd /tmp/recimg_modify
bootimg.py --repack-bootimg
- 将新的内核映像刷入设备。
有别于“问题一”,测试结果发生了新的变化。设备不再卡滞于开机画面中,而是在开机画面停留一段时间后自动重启,重启后方才再度长时间定格在开机第一屏。使用 fastboot devices
检测可知,该重启进入了Bootloader模式。
为了排除偶然因素,笔者重现上述步骤,继续进行如下的补充实验:
- 使用新版本GCC编译器重新编译。将
CROSS_COMPILE
的值改为arm-linux-
。 - 再次使用
4.4.3
版本编译器编译。
结果如下表所示:
补充实验 | 结果 |
---|---|
使用新版本编译器重新编译 | 与“问题一”的故障相同,仍然长时间卡滞于开机第一屏 |
再次用旧版本编译器编译 | 不再卡滞,而是重启进入Bootloader |
至此,更换旧版本编译器的方法得证适用于P6,可以用此方法解决开机画面定格的问题。P6所使用的3.0.8
版本内核过旧,新版本编译器可能不再兼容。
(3)让编译系统默认使用4.4.3
编译器
以上实验证明,P6在编译内核上必须使用专门的编译器,而不能使用Android源码自带的版本。为此,须修改设备参数文件,使得编译系统在编译内核时,自动使用4.4.3
版本编译器,避免每次遵照上述步骤手动编译内核的不便。
在device/huawei/hwp6_u06
下新建文件夹toolchain
,然后将整套4.4.3
编译器置入其中。并修改BoardConfig.mk
,加上如下参数,下次编译生效:
1 |
|
(七)调试准备:了解K3V2平台的panic日志转存机制
获取Android系统的Linux内核日志,通常的做法是在系统中运行dmesg
打印出一段时间内的日志,或cat /proc/kmsg
实时输出日志。而在未进入Android的情况下,以上的方法就失效了,取而代之的只有获取panic日志[^unavailable_kernel_debug]。panic,是Linux开发者对内核崩溃的形象称呼。
幸运的是,K3V2平台提供了一个获取panic日志的有效机制——APANIC_MMC
:在发生崩溃时,将panic日志转存到存储器的某一个分区中。用户只需使用cat
命令读取该分区的内容即可。该机制是Android内核崩溃记录机制APANIC
的延伸,作为一个驱动程序加载。
内核配置文件中的以下参数用于控制APANIC_MMC
:
参数 | 类型 | 说明 |
---|---|---|
CONFIG_APANIC |
布尔值 | 启用/关闭Android内核崩溃记录机制 |
CONFIG_ANDROID_RAM_CONSOLE |
布尔值 | 启用/关闭Android内存控制台 该功能可能也为 APANIC 提供支持,笔者不能证明二者相关性,但仍然启用 |
CONFIG_APANIC_MMC |
布尔值 | 启用/关闭崩溃日志转存至存储器 |
CONFIG_APANIC_PLABEL |
字符串 | 设置转存崩溃日志的目标分区名字 这里的名字是对分区的命名,而不是设备路径(如 /dev/block/mmcblk0p5 ) |
CONFIG_APANIC_MMC_MEMDUMP |
布尔值 | 启用/关闭崩溃时内存转储。一般用不到 |
笔者启用该功能,按如下设置之后重新编译内核。其中splash
为eMMC存储器的5号分区(/dev/block/mmcblk0p5
)。
1 |
|
四、升级代码:更新SELinux
不积跬步,无以至千里。上一部分解决了内核启动的问题,相当于向前迈进了一大步,内核启动成功也就意味着接下来若遇到问题即可获取并分析内核日志了。然而,真正具有难度的问题,随着实践的深入,正接踵而至。
(一)启用SELinux
SELinux是Linux重要的安全机制,由NSA(National Security Agency,美国国家安全局)开发并负责维护,其源码位于security/selinux
。
使用新的init
启动Recovery之后,设备正如预期,产生了panic,随后重启,正常引导进入了ATX。果然如我所愿,我成功抓取到了一个panic日志。其中,init
部分的输出,即init
程序产生的输出,如下所示:
1 |
|
从日志中可知,SELinux若在内核中启用,就会建立一个虚拟文件系统selinuxfs
,并在/sys/fs/selinux
中建立SELinux的设备节点文件(类似于/dev
,但后者是由init
生成的,这与init
加载前就生成的SELinux设备节点有本质区别)。日志中明确指出的文件/sys/fs/selinux/null
起到空设备/dev/null
的作用。为什么不用/dev/null
?这是因为init
启动伊始,并没有建立/dev
下的各个节点,init
反而负责建立它们。可见,init
的启动有赖于SELinux的支持。
事实上,从Android 7.0起,谷歌对Android的设计要求愈发严格,SELinux也就成为了任何Android厂商必须支持的标准,到Android 8.0则开始成为强制性要求。Android的SELinux要求“里应外合”,“外”则为Android的SELinux组件,“内”即为Linux内核中的SELinux机制。实现SELinux的根本是内核中的SELinux机制,Android的SELinux组件主要是调用内核中的API,以此保证系统内外的安全性。正因如此,内核必须支持SELinux。
所幸,早在2.6.35
版本中[^follow_my_collections],Linux就已经引入了SELinux机制。但默认情况下它并未启用,也并未作为Linux默认的安全机制,故用户必须手动启用。在内核配置文件的“# Security Options
”部分中加入以下配置:
1 |
|
SELinux还依赖其他的设置项,因此也要确保下面的项目一并配置正确:
1 |
|
重新编译内核并应用之,下次panic时即可发现,init
不再于此处出错了,而是顺利地开始了后面的进程。
(二)SELinux PolicyDB版本过旧引发的问题
(1)问题描述
原本想,笔者拥有的3.0.8
版本内核已装备了SELinux,加之已将其打开,init
的启动应当能够继续。然而殊不知,一个让笔者意想不到的问题发生了。启动未过多久,设备再度panic。获取内核日志,发现init
遇到了这样一个错误,使其无法继续:
1 |
|
这里的PolicyDB,即存放SELinux策略的数据库,为一个二进制文件,由SELinux规则的定义代码(以.te
为扩展名)通过checkpolicy
工具编译而来。它的版本问题,竟然会影响到SELinux机制的工作,可谓问题重大。
带着疑问,我在Android的SELinux模块(external/selinux
)中,以上述错误文本为关键字进行grep
搜索。值得注意的是,程序员在编写这些用于日志输出的错误文本时,为了排版美观,作了断行处理;加之日志中的30
、15
和26
一般都是不确定的值:如此使得检索一整段关键字是找不到结果的。故笔者尝试截取其中一部分,如“does not match
”。
果然有了结果:
1 |
|
显然policydb.c
的那一项最匹配。打开该源码,定位到相应的位置,发现以下代码:
1 |
|
此处的条件判断,正是用于在读取PolicyDB时检测PolicyDB的版本,若不符合版本要求,立刻报错返回。其中的两个宏——POLICYDB_VERSION_MAX
与POLICYDB_VERSION_MIN
,指定了要求的版本范围,它们的定义在头文件libsepol/include/sepol/policydb/policydb.h
之中。
(2)Android SELinux模块中的PolicyDB版本
Android SEPolicy模块中头文件libsepol/include/sepol/policydb/policydb.h
包含了所有可能的PolicyDB版本。从每个版本的名字可以猜测,它们的“版本”,实际上指的是API的等级,表示它们所支持的功能。最低为POLICYDB_VERSION_BASE
(15
),最高为POLICYDB_VERSION_XPERMS_IOCTL
(30
)[^another_highest_poldb_ver]。
1 |
|
最高版本POLICYDB_VERSION_XPERMS_IOCTL
是自4.4
版内核引入的新PolicyDB版本,XPERMS
为extended permissions的缩写,即“扩展权限”。Android 8.0开始的一些安全特性需要用到扩展权限的API,因此对于PolicyDB版本的最低要求升到了30。从Android的SEPolicy配置档案(system/sepolicy
)的Android.mk
中,我们不难窥知这一点:
1 |
|
然而,SELinux归根到底是由内核实现的功能,Android提供的SELinux模块仅仅是用于调用内核中之SELinux机制的,相当于客户端之于服务器的关系。解铃还须系铃人,要想抓住PolicyDB版本定义的本质,仍须从根本之所在——内核,进行分析。
(3)内核SELinux定义的PolicyDB版本
内核的PolicyDB版本定义在security/selinux/include/security.h
中,如下所示。这里定义的最低版本为POLICYDB_VERSION_BASE
(15
),最高为POLICYDB_VERSION_ROLETRANS
(26
)
1 |
|
不难发现,内核SELinux中定义的最高版本,明显要低于Android SELinux模块中所要求的“底线”POLICYDB_VERSION_XPERMS_IOCTL
(30
),说明现有的3.0.8
版本内核并不支持最新的PolicyDB版本。在此情形下,出错是必然的。
解决问题的方向只有一个:升级SELinux源码。
(三)就地升级
代码的升级,以新版本内核4.17
为蓝本。操作的思想是,找出新版内核SELinux源码中与最新的PolicyDB版本POLICYDB_VERSION_XPERMS_IOCTL
有关的代码,然后在3.0.8
的旧版本内核中实现它们。而寻找不同之处,最便捷直接的手段,莫过于用比较工具。笔者选用的是Meld。
(1)抓住重点文件
打开Meld,选择“目录比较(Directory comparison)”,两边分别指定为旧版本与新版本内核的security/selinux
目录。然后在工具栏上把“显示相同文件(Show identical,按钮文本为“Same”)”按钮取消,只按下“显示新文件(Show new)”和“显示已修改文件(Show modified)”,以确保只显示两边不同的文件。
面对纷繁复杂的文件列表,须确定一个比较的重点。此刻的重点,即为围绕两个关键点查找相关的源文件——PolicyDB与XPERMS
。笔者的做法是,以XPERMS
为关键词(忽略大小写),使用grep -l
检索出包含该关键词的文件列表[^grep_arguments_2],据此发现了以下直接与XPERMS
相关的源文件。
1 |
|
这种以关键字为准的判断方法,得出来的结果比较粗略,它并未考虑到代码上下文的关系。一些包含xperms
关键字的函数有可能还调用了上述文件以外、且同样需要修改的函数。因此还须在实践中找出“漏网之鱼”。
(2)明晰二者差异
源码升级不是一个简单的问题,版本跨越幅度之大,使得升级过程中要注意的细节为数不少。在使用Meld比较的过程中,软件显示出的差异常常占据导航栏相当大的比重。笔者在观察源码时,发现了4.17
版本和3.0.8
版本SELinux源码的以下不同之处:
1. 新增功能
XPERMS属于新功能,因而4.17
版本相比3.0.8
,最明显的变化在于多出了很多与XPERMS相关的函数。它们表现如下:
avc.c
- 加入了以
avc_xperms_
为前缀的结构体与函数。 - 主要的一部分函数加入了与XPERMS相关的参数。
- 加入了以
security.h
- 加入了最新的PolicyDB API定义,即
POLICYDB_VERSION_XPERMS_IOCTL
与POLICYDB_VERSION_INFINIBAND
,分别为30
和31
。 - 加入了XPERMS相关的基本结构体,如
extended_perms
。
- 加入了最新的PolicyDB API定义,即
avtab.c
- 加入了处理XPERMS的代码。
policydb.c
- 在结构体数组
policydb_compat
中加入了XPERMS的条目。
- 在结构体数组
2. 重写原有代码
新版本与旧版本的代码,大部分均实现相同的功能,但新版本对旧版本中的很多函数均进行了重构,使得代码的实现千差万别。表现列举如下:
- 将旧版本函数中的一段代码块整合成单独的函数。
函数中的一段代码块能够实现一个既定的小目标。这时,为了代码简洁的考虑,开发者会将这些代码块单独提取出来,写成一个单独的函数,再在原函数中调用它以实现与原先相同的功能。比对两版本代码中同一个函数的程序逻辑时,若发现这样的情况,则须找出新版本中“独立成家”的代码块,才能避免“漏网之鱼”,导致最终得到的代码出现意想不到的Bug。
典型的例子有avc.c
中的avc_has_perm_noaudit()
函数。3.0.8
的其中一段如下:
1 |
|
但是在4.17
中,则变成了:
1 |
|
重点在if (unlikely(!node))
之中。在3.0.8
中,if
分支里还是由四行代码组成的代码块;但在4.17
中却变成了一行有函数调用的赋值语句,调用的是函数avc_compute_av()
。这一差别实在是不小。
索性浏览一下avc_compute_av()
的定义,会发现:
1 |
|
这实际上就是3.0.8
中相应的代码块!除了INIT_LIST_HEAD
是与新功能有关的代码之外,其余的代码本质均相同,最终均归为给指针变量*node
赋值。
- 加入了新的数据结构。
- 函数接口发生了变化。
e.g.
- 引入struct selinux_state所致的一系列变化!
- list.h接口的变化,如hlist_for_each_entry
(3)巧妙移植
(4)总结
列举出旧版内核中最终修改的文件。以下为diffstat
工具对整理出来的修改前后差异:
1 |
|
五、持续跟进:修复启动错误
升级了SELinux之后,原本近乎停滞的开发进程得以继续,下一步就是要把前方遇到的新问题一一进行解决。
(一)修正init
在检测到panic时的处理机制
内核启动一段时间后,即把控制权交予位于ramdisk根目录的初始化程序init
,所以不难判断之后的重启均由该程序引发。类比其他Linux发行版的策略,理论上init
所发生的任何异常都会导致panic重启,可以让用户通过技术手段获取panic日志。这些异常,有的是init
内部错误处理程序触发,也有的是init
本身发生了崩溃(如段错误(segmentation fault))。
但笔者在调试过程中发现,并不是所有的重启,都能在ATX中使用cat /dev/block/mmcblk0p5
命令读取splash
分区获得日志。有时重启会输出panic日志;而有时检查日志中的崩溃时间标记,发现该分区的内容保持原样(APANIC_MMC
并不支持擦除分区并覆盖原有日志)。笔者试图分析偶尔留下的panic日志,发现内核关于panic的报错提示均为:
1 |
|
仅仅得知这一条日志是不够的,加之只有一部分重启有日志,资料不完备,因此笔者只能从init
的源代码入手分析。
(1)找出与panic相关的源文件
init
的源码位于Android源码的system/core/init
下。为了找出与panic有关的代码,笔者首先在其中用grep
检索关键字panic
[^grep_arguments],结果如下:
1 |
|
一般地,包含有文本“panic
”(或“PANIC
”)的源文件均与init
对panic的处理有关。下一步,即为逐一分析以上涉及到的文件,根据行号定位到相应的代码处。
(2)分析代码
1. init.cpp
init.cpp
是init
的主程序,main()
函数位于其中。一个函数InstallRebootSignalHandlers()
用于指定处理信号的动作,使init
在收到异常信号(如SIGINT
、SIGABRT
)后,调用panic()
函数。panic()
会直接让系统重启进入Bootloader。
1 |
|
这项工作在main
函数开始不久后进行。阅读代码发现,宏REBOOT_BOOTLOADER_ON_PANIC
控制是否进行这项工作,若设为false
(0
),则上述信号产生时内核直接崩溃重启,而不再进入Bootloader。信号产生的大多数情况是init
程序意外崩溃。
1 |
|
除此之外,阅读grep
的输出结果还可发现,在一些函数中也分布着对panic()
的调用。有别于InstallRebootSignalHandlers
中因异常而产生的信号触发,它们均为开发者手动调用,通常是在出现不可逆转的严重错误(unrecoverable fatal error)时立刻重启,进入Bootloader。比如下面的例子:
1 |
|
此处调用selinux_load_policy
来加载SELinux策略(SELinux policy)。8.0开始Android将SELinux视为强制性要求,必须加载SELinux策略才允许启动。因此SELinux加载失败,即为不可逆转的严重错误,必须重启。
根据InstallRebootSignalHandlers()
的注释可知,init
开发者的用意,就是对于开发环境,将panic时自动重启进入Bootloader视为默认动作,以免设备在panic时使用错误的内核配置无限重启,由此利于客户开发者和测试者(均为谷歌公司的客户)在Bootloader中恢复设备。
笔者认为这一设计有利有弊。它易于恢复设备这一优点,适用于有足够条件进行完备测试的工厂环境。因工厂环境支持串口调试等若干更为高级、更为底层的方式调试内核,调试方式独立于Android系统,可以在脱离Android的情况下轻易获取内核日志,不需要以用启动镜像启动系统的形式来获取。然而,笔者的情况相对init
开发者的的设想,尤为特殊:缺乏串口调试等途径,使得panic重启成为了唯一能够获取内核日志的方式——原本的处理逻辑不再适用。为此,笔者将在下一节中对相关代码进行修改。
观察以上代码,不难看出,init.cpp
更多地是在调用在其他源文件中所定义的函数与宏,故实际操作中并不需要修改这个文件,应当追根溯源。
2. Android.bp
与Android.mk
二者都是编译系统的入口文件,作用相同,只是调用它们的是不同的程序。Android.bp
由Android的新版编译系统Soong使用,为支持注释的JSON格式;而Android.mk
则是Makefile,由GNU Make使用。事实上,Soong会通过一个转换程序CKati,将Android.mk
转换成另一款编译系统Ninja的配置文件,不过现阶段二者仍然共存,均发挥彼此不可替代的作用。
两种配置文件都定义了编译C++源文件时,需要传递给编译器的一些参数,其中就包括了对宏REBOOT_BOOTLOADER_ON_PANIC
的定义。编译器接收-D<MacroName>
或-D<MacroName>=<Value>
参数,用于在编译时定义宏,等价于在源码开头添加#define <MacroName> <Value>
;对应的取消定义宏之参数为-U<MacroName>
,等价于#undef <MacroName>
。
Android.bp
的有关部分如下:
1 |
|
代码中的对象cc_defaults
是对编译器的配置。其中cc_defaults.cppflags
数组指定传递给编译器的参数;而cc_defaults.product_variables
则指定不同的产品变量(product variable)应当使用的专用设置。专用设置中的cppflags
会在运行编译器时追加于上方的cc_defaults.cppflags
之后。
上述代码中,REBOOT_BOOTLOADER_ON_PANIC
宏在cc_defaults.cppflags
中定义为0
,表示默认禁止在panic时重启到Bootloader中;但是下方产品变量debuggable
所对应的cppflags
中,则将原先的宏定义取消了,取而代之地将其定义为1
,表示启用。Soong编译系统将userdebug
和eng
视为debuggable
。
对应地, Android.mk
的有关部分如下:
1 |
|
在Makefile的语法中:filter <关键词组>,<字符串>
判断字符串的值是否符合关键词组的其中之一,用于过滤出符合关键词组的子串,若不满足条件则返回空值。条件判断ifneq (<判定值>, <表达式>)
判断表达式的值是否不等于第一个参数给定的判定值。
由此,不难理解整段代码的含义。根据构建变量的不同,会相应地给编译器传递不同的参数,由变量init_options
传递。该段代码会判断用户指定的编译变量TARGET_BUILD_VARIANT
是否为userdebug
与eng
,若是,则将REBOOT_BOOTLOADER_ON_PANIC
宏设为1
,否则设为0
。
修改时,只需要将编译参数中REBOOT_BOOTLOADER_ON_PANIC
的值一律设为0
即可。为了确保一致性,两个文件均需同时修改。
3. util.cpp
util.cpp
相当于一个工具箱,提供了一些实用的函数,在其他源文件中调用。百川东到海,init.cpp
中触发重启进Bootloader的panic()
函数就是在这里定义的。
panic()
函数的定义如下:
1 |
|
显然,该函数的作用很简单:调用DoReboot()
,以重启进入Bootloader。DoReboot()
是Android NDK的一个函数,用于重启设备,支持重启进入特定的模式。
由此可知,修改panic()
函数,正是修改init
源码这项任务的重中之重。
(3)修改代码
掌握原理后,即可立刻着手修改代码。对代码的修改以diff
的形式展示如下。
1. Android.bp
与Android.mk
把所有定义REBOOT_BOOTLOADER_ON_PANIC
宏的值均改为0
。
1 |
|
1 |
|
2. util.cpp
将panic()
函数中调用DoReboot()
的部分改为abort()
。abort()
的作用是强行终止程序,由于调用它的是初始化程序init
,故内核会将其视为异常,从而触发panic。利用此原理,即可在任何条件下init
发生错误时,都能因panic而抓取到内核日志。另外,为区分init
终止的原因是程序崩溃还是手动触发,笔者还在abort()
函数之前加入了有辨识度的日志输出[^printk]:若为手动触发,则输出一行日志——“android::init::panic() invoked. Abort init to trigger kernel panic!
”。
1 |
|
(4)重新编译
完成上述修改后,须重新编译Android方可生效。无论是make
还是make recoveryimage
,均会强制重新编译init
。
也可以在system/core/init
目录下运行以下命令,单独编译init
:
1 |
|
新的init
可执行文件为:
out/target/product/hwp6_u06/obj/EXECUTABLES/init_intermediates/init
。
(二)为内核打补丁
(1)解决“Could not set context for /init
”问题
接下来的研究,进入了“发现一起,查处一起”的查错模式,依赖于日志的变化以找出症结所在。
下一个panic日志与以往有所不同了:
1 |
|
从SELinux的日志信息可以看出,内核中的SELinux已经成功启用。但是却在这样一个地方出错,init
将其视为严重错误,从而手动触发了panic。
1 |
|
带着疑问,我以上述内核日志文本为关键词检索谷歌,在NSA的Bug Tracker中找到了答案。日志中的操作是为根文件系统(rootfs)下的文件设置SELinux上下文,但是该操作并未被内核支持。[^REF: rootfs@NSA_tracker][^REF: rootfs@Android_repo]为此,NSA提供了一个官方补丁,早在2013年9月27日就已应用到Android官方的内核源码当中。根据本地git diff
整理出的补丁如下:
1 |
|
应用该补丁后,调试工作又前进了一步。
(2)禁用request_module()
调用,避免不必要的内核日志
下一次panic时,init
得以继续往前走,并开启了它的第二阶段进程(second stage):
1 |
|
然而好景不长,再次面临崩溃。为了分析原因,笔者再次观察日志,留意到了这一行:
1 |
|
这是SELinux阻止某个操作时的提示。有可能是因为这个操作被阻止,导致这一次再度崩溃。再次就此问题查询谷歌,在NSA Bug Tracker中亦可发现这也是NSA早已解决的问题。[^REF: request_module@NSA_tracker]对于该问题,NSA是这样分析的:
With Android M, Android environments use a separate execution domain for 32bit processes. See: https://android-review.googlesource.com/#/c/122131/
This results in systems that use kernel modules to see selinux audit noise like:
type=1400 audit(28.989:15): avc: denied { module_request } for pid=1622 comm="app_process32" kmod="personality-8" scontext=u:r:zygote:s0 tcontext=u:r:kernel:s0 tclass=system
While using kernel modules is unadvised, some systems do require them.
Thus to avoid developers adding sepolicy exceptions to allow for request_module calls, this patch disables the logic which tries to call request_module for the 32bit personality (ie: personality-8), which doesn’t actually exist.[^REF: request_module@Android_repo]
根据本地git diff
整理出的补丁如下:
1 |
|
应用补丁后,那一行不再输出,但init
仍然在同样的地方发生崩溃。说明init
的崩溃与本补丁要解决的问题并无任何关联。
六、尚待完成的任务
七、总结与体会
[^Ref_HD2]: 相比之下,年代更为久远的HTC旗舰手机HD2(2009年推出,预装Windows Mobile 6.5操作系统)则长期受开发者青睐,先后有多个新版本Android系统得以在该设备上运行。
[^stopped_support]: 现已停止支持,EMUI官方网站(www.emui.com)已经不再提供任何下载。
[^android_website_in_China]: 谷歌已在中国大陆上线该站点,地址为:source.android.google.cn。
[^how_to_disable_them]: 通常的做法为删除,或将代码标为注释。
[^why_prune]: 精简的原因是boot
分区有大小限制,容量仅为9MB。
[^quora]: TODO: Quora。链接在Chrome书签里!
[^unavailable_kernel_debug]: 当然也可以使用串口调试。但诚如前文所述,该方法对于非开发环境而言不现实。
[^grep_arguments]: 参数-r
表示递归检索目录下的所有文件,-i
表示忽略大小写, -n
表示显示行号。
[^grep_arguments_2]: 参数-l
表示只输出符合检索条件的文件名,而不显示它们的匹配之处。
[^printk]: Linux内核使用printk()
向内核日志中输出信息。用法与printf()
相同,但一般会在调用时加入KERN_xxxx
宏以标明日志的轻重缓急程度。定义见内核源码目录include/linux/printk.h
。
[^follow_my_collections]: 根据笔者拥有的早期内核源码判断之。笔者拥有一套Freescale i.mx53的内核源码,其版本即为2.6.35.3
。
[^REF: rootfs@NSA_tracker]: reference-> A programmer's same issue on NSA's bug tracker: http://seandroid-list.tycho.nsa.narkive.com/D3qnO4XY/restorecon-init-fail-in-the-init-cpp-file
[^REF: rootfs@Android_repo]: reference-> NSA's kernel patch: https://android-review.googlesource.com/c/kernel/common/+/58360/1
[^REF: request_module@Android_repo]: reference-> NSA's kernel patch: https://android-review.googlesource.com/c/kernel/common/+/183036
[^REF: request_module@NSA_tracker]: reference-> A programmer's blame on NSA's bug tracker: http://seandroid-list.tycho.nsa.narkive.com/ep7xOE8K/kernel-module-request-personality-8
[^another_highest_poldb_ver]: 另一个同为30
的PolicyDB版本POLICYDB_VERSION_XEN_DEVICETREE
并不适用于一般的Linux环境。
- 本文作者: 爱拼安小匠
- 本文链接: https://anclark.github.io/2018/09/02/Paper/Adapt_Android_Oreo_to_Huawei_P6/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0(署名-非商用-禁止演绎 3.0) 许可协议。转载请注明出处!