最近在修改Android源码(厂商定制的源码,非AOSP)时,遇到了一些问题:
- 新增的lib库模块,需要依赖libart等so包,而这些so包在Android Q中被编译到了apex中(apex文件位于/system/apex目录),导致无法找到共享库(需要修改
/system/core/roodir/etc/ld.config.txt
, 修改里面的策略比较麻烦,并且可能导致安全问题)。 - 在将java层代码移动到了
core-libart
中,可以顺利的引用到com.android.runtime.release
中的库文件,也算解决了问题1。 - 在某个最新的Android版本中,system分区为erofs文件系统(只读),无法通过adb remount来重新挂载为可读写。而为了替换apex,需要替换
/system/apex/
下的相应文件。该系统版本通过引入了overlay
文件系统来解决该问题。overlay文件系统在system挂载点上覆盖了一层upper目录。当用户通过adb remount指令重新挂载后,所有对/system的操作实际是对cache
下的upper
目录的操作。由于cache分区太小,导致将文件直接放入。 - 在厂商定制的Android源码中,由于存在很多二进制交付的部件,这使得单纯通过编译源码得到的
System.img
是不完整的镜像,无法直接刷入设备。
解决方案
为了将修改后的APEX文件放入设备中,考虑到APEX特性主要用于模块化升级操作,肯定预留了升级模块的功能。故而,可以利用APEX的升级功能来达到替换APEX的效果。
接下来,我们将对APEX进行简要的学习。相应的源码位置如下
- APEX的实现源码位于:
/system/apex/
模块 - 与运行时库相关的APEX编译模块(
com.android.runtime
)位于art/build/apex
模块,编译指令为:make com.android.runtime.release
, 编译后替换/system/apex/
相应文件,重启后将在/apex
中挂载。
参考资料
APEX简要介绍
APEX是Google继Project Treble后的又一重大举措,它是Android Q的系统组件更新机制。APEX的想法在GNU/Linux发行版中非常普遍:针对Linux库集合中的特定部分的包更新。但这是Google从未尝试过的事情,因为Android中使用了一个ro(只读)分区,在其中存储了所有系统库和框架,而不是大多数Linux发行版中通常使用的rw(读写)分区,从而使得其难以实现标准升级过程。
库是预编译的代码,可以在其它程序中使用。常用的方法是可以制作成Android应用程序调用的库,减少Android应用程序的大小,因为不需要在多个应用程序中实现相同的代码。我们可以在/system/lib
和/system/lib64
目录中找到许多预装系统库。Android系统库通常不会单独更新-相反,它们会通过OTA更新作为Android平台升级的一部分进行更新(备注:意思就是要整个系统升级才能把系统库部分也一并升级了,而不能像Linux那样子,可以针对特定的系统库进行升级)。另一方面,Linux发行版的库可以单独升级。
随着APEX的推出,Android中的系统库可以像Android应用程序一样单独更新。这样做的好处是,应用程序开发人员将能够利用更新后的库,而无需等待OEM推出完整的系统升级。
APEX是一个生态系统,迫使Google重新考虑Android从不同于/system的非标准分区加载所有库和文件的方式。
首先,我们必须区分共享库和静态库。共享库是一个库(如libkind.so的文件),它不包含自身运行所需的所有代码,而是与实际提供代码的其它库进行链接;静态库则是,可以认为它不依赖于其它任何库,并在文件中静态包含了所有内容。
Android历史上使用了名为ld.config.txt[0]
的单个配置库路径文件(即Linux中的LD_LIBRARY_PATH),以配置二进制或其它库中所需的共享库的允许搜索路径。这些路径在配置中是硬编码的,不可扩展。除非用户安装OTA Android更新,否则此布局(包括只读系统分区)会导致不可更新的库。Google解决了该问题,允许扩展搜索路径,让单个APEX包提供自己的ld.config.txt
,其中包含额外(和更新)库路径。
但这样子仍存在问题:首先是ABI(应用程序二进制接口)稳定性,库应该始终导出一组稳定的接口,以允许其它应用程序和库继续使用相同的协议,即使升级后的库也是如此。Google正尝试在APEXed库之间创建稳定的C接口来积极开展该工作。
APEX并不仅与库和二进制文件。实际上,它还包括配置文件、timezone更新以及一些java框架。
当前AOSP提供的APEX包名的示例有:
- com.andrid.runtime: ART和bionic runtime(二进制和库)
- com.android.tzdata: TimeZone和ICU数据(库和配置数据)
- com.android.resolv: Android中用于解决网络相关请求的库(库)
- com.android.conscrypt(Java安全提供程序)(Java框架)
如何安装和构建APEX包
APEX软件包是一个简单的Zip包(如APK),可由ADB进行安装,而后通过包管理器来安装。
apex_manifest.json
指定了package以及version:apex_payload.img
是一个micro-filesytem 镜像(EXT 4格式)
其它文交所 用于安装软件包是的验证过程的一部分,如:
- AndroidManfest.xml:
- META-INF/目录有着包整数,并使用域APK相同的机制。因此,在运行用户安装更新之前,这些包在运行时,将由私钥/公钥对进行验证。Google又增加了2层安全性,它们使用dm-verity检测镜像的完整性以及AVB(Android Verified Boot)验证来确保镜像来自可信的源。最坏情况下,APEX包将被拒绝。
如果所有验证步骤都成功,镜像文件将被标记为有效,并在下一次重新引导时替换系统实体。
如何在启动时安装镜像
预安装的软件包存储在/system/apex中,并且所有软件包当前都是版本1.但当APEX被激活时会发生什么?我们将再次使用com.android.tzdata做例子。
让我们重启设备并分析logcat。
前两行提供了足够的信息来了解软件包的来源与安装位置:/apex/, Android Q引入而来新的目录,用来存储激活的软件包。
使用AVB成功验证软件包后,并且公钥匹配后,使用loop设备将APEX安装到/dev/block/loop0,使系统可以访问EXT4文件系统。loop设备是一种伪设备,它是文件可以作为块设备被访问,使得该文件的内容可以作为安装点被访问。
此时,由于@1后缀(表示软件版本),APEX仍未使用。为了最终让系统知道我们的软件包已经激活,它将被绑定挂载到/apex/com.android.tzdata。其中Android主动期望tzdata生效,绑定装载覆盖不同点下的现有目录或文件。
APEX的实现整个位于AOSP下的单个仓库。apexd(APEX daemon)目录含有Android上运行的代码。apexer目录含有构建系统使用的用来创建APEX包的代码。
Google视图创建一组APEX包的核心集,这些包可被Google更新,从而可以创建一个在供应商之间共享的统一的Android基础核心,使的只有"system"更新称为可能,但只是单个包的升级。
apexd要求 /data/apex
在启动后立即可用,以更新所有的Android模块。使用FDE(全潘加密),/data/apex/在用户解锁设备之前都是加密的,使得apex基本上毫无用处,因为只有系统apex的变种会在启动时加载。除此之外,所有设备都应该支持APEX,但他们需要一些内核补丁。