安卓系统编程-全-

安卓系统编程(全)

原文:zh.annas-archive.org/md5/6eaf95f635acd6a1c91afe61cfd71bd5

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

安卓(Android)是全球最受欢迎的移动操作系统。自 2013 年以来,安卓在全球市场的份额约为 80%,而第二大移动操作系统 iOS 的市场份额不到 20%。由于安卓的普及,市场上关于安卓编程的书籍很多。其中大部分针对的是安卓应用开发者,他们是全球安卓开发领域最大的社区。

还有一群人在安卓框架之下的层级上工作。许多人称他们为安卓系统开发者。与安卓应用开发者相比,安卓系统开发者使用 C/C++语言,甚至汇编语言,来开发系统服务或设备驱动程序。与安卓应用开发相比,安卓系统开发的范围和定义要模糊得多。对于安卓应用开发,开发环境和工具非常明确:应使用谷歌的 Android SDK 和 Android Studio,编程语言是 Java。

对于安卓系统开发,我们可以使用安卓 NDK 来开发安卓系统服务或本地应用程序。许多人将基于安卓开源项目(AOSP)的开发称为安卓系统开发。然而,安卓系统开发涵盖了为特定硬件平台产生本地应用程序、服务或设备驱动程序的活动。它更接近硬件和操作系统,而安卓应用开发则更加通用和硬件无关。

由于硬件和操作系统的依赖性,教授安卓系统编程比安卓应用编程更困难。从市场上的书籍数量来看,我们可以看到这一点。使用特定示例教授安卓应用开发要容易得多。应用编程书籍的读者可以跟随示例,并在大多数可用的安卓设备上进行测试。然而,大多数安卓系统编程书籍只能谈论一般概念或想法。当作者想要使用示例时,它们必须与特定的硬件平台和安卓版本相关。这使得读者难以重复相同的过程。

虚拟硬件平台

为了使讨论更加普遍并克服特定硬件平台的问题,我使用虚拟硬件平台来展示在安卓系统层面的工作。

在这本书之前,我在我的前一本书《使用安卓模拟器进行嵌入式系统编程》中尝试使用虚拟硬件平台来解释我们如何通过安卓模拟器学习嵌入式系统编程。似乎很多读者喜欢这个想法,因为他们可以在对所有人开放的虚拟硬件平台上更容易地探索代码示例。

本书使用的安卓版本

Android 仍在以非常快的速度变化。当我完成《Android 嵌入式编程》一书时,我们仍在使用 Android 5(Lollipop),而 Android 6(Marshmallow)正通过预览版发布走向市场。现在,当我正在编写这本书时,市场上已经有了 Android 7 设备,Android 8 的下一个版本已经通过预览版发布。我们将使用 Android 7(Nougat)来构建本书中使用的所有源代码。

本书涵盖内容

在本书中,我们讨论了 Android 系统编程实践。我们将使用两个项目(x86emu 和 x86vbox)来教授 Android 系统编程的基本知识。本书分为两部分。

本书的第一部分讨论了如何定制、扩展和移植 Android 系统。我们将使用 Android 模拟器作为虚拟硬件平台来演示如何定制和扩展 Android 系统。您将学习如何将 ARM 翻译器(Houdini)集成到基于 Intel x86 的模拟器中,以及如何为 Android 模拟器添加 Wi-Fi 支持。我们将使用 x86emu 设备来学习这些主题。之后,我们将学习如何使用 VirtualBox 将 Android 系统移植到新平台。您将学习如何在 PXE/NFS 环境中启动 Android,如何启用图形系统,以及如何将 VirtualBox Guest Additions 集成到 Android 系统中。我们将使用 x86vbox 设备来学习这些主题。

本书第二部分,我们将学习如何使用恢复模式更新或修补已发布的系统。在这一部分,我们将首先提供一个关于恢复模式的一般介绍。之后,我们将探讨如何为 x86vbox 设备构建恢复模式。有了 x86vbox 设备的恢复模式,我们将演示如何刷写更新包以更改系统镜像。我们将使用 Gapps 和 xposed recovery 包等示例来演示如何使用第三方恢复包更新 Android 系统镜像。

第一章,Android 系统编程简介,介绍了 Android 系统编程的概述。它还解释了本书的范围。

第二章,设置开发环境,提供了为 AOSP 编程设置开发环境的详细信息。在设置好开发环境后,我们将构建一个 Android 模拟器镜像来测试我们的设置。除了环境设置外,我们还特别讨论了如何从 GitHub 创建自己的 AOSP 源代码镜像,以帮助您快速切换到不同的配置。

第三章,发现内核、HAL 和虚拟硬件,介绍了 Linux 内核、硬件抽象层和虚拟硬件的入门知识。在本章中,我们查看与移植相关的所有 Android 系统软件堆栈层。我们还深入探讨了我们将在这本书中使用的虚拟硬件的内部结构。

第四章,定制 Android 模拟器,涵盖了新设备 x86emu 的开发。在接下来的几章中,我们将学习如何定制和扩展这个设备。

第五章,启用 ARM 翻译器和介绍本地桥接,探讨了 Android 5 中引入的新功能——本地桥接。由于我们创建了一个基于 x86 的设备 x86emu,我们必须将 ARM 翻译器模块(Houdini)集成到我们的设备中,以便大多数 ARM 原生应用程序可以在其上运行。

第六章,使用自定义 ramdisk 调试启动过程,介绍了一种高级调试技能,用于在启动阶段解决故障。著名的 Android-x86 项目使用特殊的 ramdisk 来启动启动过程。它有助于非常容易地解决设备驱动程序和 init 进程问题。

第七章,在 Android 模拟器上启用 Wi-Fi,详细介绍了如何在我们的 Android 模拟器上启用 Wi-Fi。Android 模拟器仅支持模拟的 3G 数据连接,但许多应用程序都意识到了数据和 Wi-Fi 连接。在本章中,我们演示了如何在 Android 模拟器中启用 Wi-Fi。

第八章,在 VirtualBox 上创建自己的设备,通过介绍新的设备 x86vbox 来探索如何在 VirtualBox 上移植 Android。x86emu 设备用于演示如何定制现有实现,而 x86vbox 用于演示如何将 Android 移植到新的硬件平台。

第九章,使用 PXE/NFS 启动 x86vbox,解释了如何使用 PXE/NFS 在 VirtualBox 上启动 Android。由于 VirtualBox 是一种通用虚拟硬件,我们遇到的第一问题是需要一个引导加载程序来启动系统。我们将使用 PXE/NFS 引导来解决这个问题。这是一项高级调试技能,可以在您的项目中使用。

要讨论一个更高级的案例,即使用运行在主机网络环境中的外部 DHCP/TFTP 服务器设置 PXE/NFS,我已经撰写了一篇文章,您可以在www.packtpub.com/books/content/booting-android-system-using-pxenfs找到。

第十章,启用图形,涵盖了 Android 图形系统。我们介绍了 Android 图形架构以及如何在 x86vbox 设备上启用它。

第十一章,启用 VirtualBox 特定硬件接口,解释了如何将 VirtualBox Guest Additions 中的设备驱动程序集成到 Android 系统中。

第十二章,介绍恢复,提供了对恢复的介绍。我们将学习如何通过为 x86vbox 设备构建恢复来定制和移植到新的硬件平台。

第十三章,创建 OTA 包,介绍了用于恢复的脚本语言:Edify。我们将学习如何构建和测试 OTA 更新。

第十四章,定制和调试恢复,扩展了我们关于恢复和 OTA 包所学的概念。我们将为 x86vbox 设备定制恢复和更新器。我们将使用自己的恢复来测试来自 Gapps 和 Xposed 的第三方 OTA 包。

阅读这本书所需的条件

阅读此书,您应具备嵌入式操作系统和 C/C++编程语言的基本知识。

这本书面向的对象

在我们讨论谁应该阅读这本书之前,我们应该问一下,在现实世界中通常谁会做 Android 系统编程?可能有很多这样的人。在这里,我可以给出几个一般类别。首先,有大量工程师在谷歌工作,专注于 Android 系统本身,因为 Android 是谷歌的产品。谷歌通常与芯片供应商合作,以在各种硬件平台上启用 Android。

在芯片公司,如高通、MTK 或英特尔,有许多工程师,他们使 Android 在其平台上运行。他们开发 HAL 层组件或设备驱动程序以启用硬件平台。这些硬件平台通常被称为参考平台,它们由 OEM/ODM 提供,以构建实际产品。然后,OEM/ODM 公司的工程师通常定制参考平台硬件和软件,以向其产品添加独特功能。所有这些工程师构成了从事系统级编程的主要群体。因此,如果您在这些领域中的任何领域工作,您可能想阅读这本书。

除了之前提到的类别之外,您也可能是一家嵌入式系统公司的开发者。您可能从事的项目包括汽车嵌入式系统、视频监控或智能家居。许多这些系统现在都使用 Android。嵌入式系统中最快速增长的领域之一是物联网(IoT)设备。谷歌宣布 Brillo 作为物联网设备的操作系统。Brillo 是基于 Android 的简化嵌入式操作系统。Brillo 的源代码也包含在 AOSP 中。这本书对使用 Brillo 的人也很有用。

对于 Android 应用程序开发者来说,系统级别的知识可以帮助您解决复杂的问题。如果您正在从事涉及新硬件功能的项目,您可能希望将您的知识扩展到系统级别。

这本书对教授 Android 系统编程或嵌入式系统编程的人来说也很有用。这本书中有大量的源代码,可以用来制定您自己的教学计划。

约定

在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“一般的 Android 内核源代码位于kernel/common文件夹中,看起来非常像 Vanilla 内核。”

代码块设置如下:

static struct hw_module_methods_t lights_module_methods = {
  .open = open_lights,
};

任何命令行输入或输出都如下所示:

$ ls
Light.java LightsManager.java LightsService.java

新术语重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“我们应该将启动类型设置为标准创建进程启动器。”

警告或重要注意事项以如下框中显示。

小贴士和技巧看起来像这样。

读者反馈

我们欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者的反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要发送给我们一般性的反馈,只需发送电子邮件到feedback@packtpub.com,并在您的邮件主题中提及书的标题。

如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com上的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”标签上。

  3. 点击“代码下载 & 勘误”

  4. 在搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击“代码下载”。

文件下载完成后,请确保使用最新版本的以下软件解压或提取文件夹:

  • Windows 上的 WinRAR / 7-Zip

  • Mac 上的 Zipeg / iZip / UnRarX

  • Linux 上的 7-Zip / PeaZip

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Android-System-Programming。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。这样做可以帮助其他读者避免挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误表部分。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索框中输入书籍名称。所需信息将在勘误表部分显示。

盗版

互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过copyright@packtpub.com与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面提供的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以通过questions@packtpub.com与我们联系,我们将尽力解决问题。

第一章:Android 系统编程简介

本书是关于 Android 系统编程的。在本章中,我们将从对系统编程和 Android 系统编程范围的讨论开始(以给出本书的高级概述)。之后,我们将查看 Android 系统架构。从架构中,我们可以看到本书将关注的层次。我们还将讨论本书中将使用的虚拟硬件平台和第三方开源项目。总之,本章将涵盖以下主题:

  • Android 系统编程简介

  • Android 系统架构概述

  • 本书所使用的第三方项目简介

  • 虚拟硬件平台简介

什么是系统编程?

当我们谈论什么是系统编程时,我们可以从维基百科中系统编程的定义开始:

"系统编程(或系统软件编程)是指编写系统软件的活动。与应用编程相比,系统编程的主要区别特征在于,应用编程旨在产生为用户提供服务的软件(例如文字处理器),而系统编程旨在产生为其他软件提供服务的软件和软件平台,这些软件和平台受性能限制,或者两者兼而有之(例如操作系统、计算科学应用、游戏引擎和 AAA 级视频游戏、工业自动化以及软件即服务应用)。"

从前面的定义中,我们可以看出,当我们谈论系统编程时,我们实际上是在处理计算机系统本身的构建块。我们可以描述系统架构及其在系统内部的外观。例如,我们可以参考关于 Windows 或 Linux 的系统编程书籍。由 O'Reilly Media, Inc.出版的《Linux 系统编程》一书包括关于文件 I/O、进程管理、内存管理、中断处理等内容。还有一本名为《Windows 系统编程》的书,由 Addison-Wesley Professional 出版,其内容与 Linux 版本非常相似。

您可能期望在这本书中看到类似的内容,但您会发现本书中的主题与经典的系统编程书籍相当不同。首先,为 Android 编写系统编程书籍讨论文件 I/O、进程管理或内存管理并没有太多意义,因为《Linux 系统编程》一书几乎可以涵盖 Android 的相同主题(Android 使用 Linux 内核和设备驱动模型)。

当你想探索内核空间系统编程时,你可以阅读 O'Reilly 的《Linux 设备驱动程序》或 Prentice Hall 的《Essential Linux Device Drivers》等书籍。当你想探索用户空间系统编程时,你可以阅读我之前提到的书籍,O'Reilly 的《Linux 系统编程》。然后你可能想知道,在这种情况下,我们需要一本《Android 系统编程》的书吗?为了回答这个问题,这取决于我们如何看待 Android 的系统编程。或者换句话说,这取决于我们从哪个角度看待Android 系统编程。我们可以从不同的视角对同一个世界讲述不同的事情。从这个意义上讲,我们可能需要不止一本书来讨论 Android 系统编程。

要谈论 Android 系统编程,我们可以从理论上或实践上进行讨论。在这本书中,我们将通过几个实际项目和动手实例来实践。我们的重点是定制 Android 系统以及如何将其移植到新的平台。

这本书的范围是什么?

如我们所知,Android 有两种编程类型:应用编程和系统编程。

通常,很难在系统编程和应用编程之间划清界限,尤其是在基于 C 语言的操作系统,如 Linux 和各种 Unix 系统。在 Android 框架中,应用层与系统其他部分被很好地分离。你可能知道,Android 应用编程使用 Java 语言,而 Android 系统编程则使用 Java、C/C++或汇编语言。为了简化,我们可以将 Android 世界中除了应用编程之外的所有内容视为系统编程的范畴。从这个意义上讲,Android 框架也属于系统编程的范畴。

从本书读者的角度来看,他们可能想了解他们在项目工作中可能触及的层次。Android 框架是 Google 在大多数情况下唯一会更改的层次。从这个角度来看,我们不会花太多时间讨论框架本身。相反,我们将专注于如何将包括 Android 框架在内的系统从Android 开源项目AOSP)的标准平台移植到其他平台。在这本书中,我们将重点关注移植过程中需要更改的层次。

在我们完成移植工作后,将有一个新的 Android 系统可用。对于这个新系统,我们需要做的一件事是,不时地将新系统的更改传递给最终用户。这可能是一次重大的系统更新或错误修复。这由 Android 中的空中OTA)更新支持。这也是本书的一个主题。

传统上,所有 Unix 编程都是系统级编程。Unix 或 Linux 编程是围绕三个基石构建的,即系统调用、C 库和 C 编译器。Android 系统编程也是如此。在系统调用和 C 库之上,Android 为 Android 应用级别提供了一个额外的抽象层。这就是 Android 框架和 Java 虚拟机。

从这个意义上讲,大多数 Android 应用程序都是使用 Android SDK 和 Java 语言构建的。你可能想知道是否可以使用 C/C++或甚至使用 Java 进行系统级编程。是的,所有这些都是可能的。除了 Android SDK 之外,我们还可以使用 Android NDK 开发本地应用程序。还有许多使用 Java 语言开发的 Android 框架组件。我们甚至可以使用 Visual Studio(Xamarin)用 C#开发 Android 应用程序。然而,本书不会涉及这种复杂性。我们将专注于应用框架之下的层次。再次强调,重点是定制和扩展现有系统,或者将整个系统移植到新的硬件平台。

我们将重点关注 Android 系统的移植和定制,因为这些是大多数从事 Android 系统级工作的人会做的事情。在谷歌发布 Android 新版本后,芯片供应商需要将其移植到他们的参考平台上。当 OEM/ODM 公司获得参考平台时,他们必须将其定制为自己的产品。定制包括构建初始系统本身以及部署到部署系统的更新。本书的第一部分将讨论 Android 系统的移植。本书的第二部分将讨论如何更新现有系统。

如果我们考虑以下图中右侧的 Android 架构,我们可以看到大部分移植工作将集中在 Android 系统架构中的内核硬件抽象层(HAL)上。这对其他 Android 衍生版本也是如此。本书中的知识和概念也适用于 Android 可穿戴设备和 Brillo。以下图的左侧显示了Brillo的架构图。Brillo是谷歌为物联网设备提供的物联网操作系统。它是 Android 物联网设备的简化版和较小版本。然而,移植层与 Android 相同。

图片

Android 与 Brillo 系统架构比较

左侧的 Brillo/Weave 架构图是根据 OpenIoT Summit 上布鲁斯·比尔的演示制作的。感谢布鲁斯·比尔提供的精彩演示和 YouTube 上的视频,它对 Brillo/Weave 架构进行了非常全面的介绍。

Android 系统概述

如我们从架构图中可以看到,Android 的架构层包括应用程序框架Android 系统服务HAL内核。Binder IPC 用作进程间通信的机制。我们将在本节中介绍它们。由于恢复也是系统编程范围的一部分,我们还将在本节中概述恢复。

你可以在以下谷歌网站上找到有关关键移植层和系统架构内部信息的更多信息:

source.android.com/devices/index.html

内核

如我们所知,Android 使用 Linux 内核。Linux 是由 Linus Torvalds 在 1991 年开发的。当前的 Linux 内核由 Linux Kernel Organization Inc.维护。最新的主线内核发布可以在www.kernel.org找到。

Android 使用略微定制的 Linux 内核。以下是 Linux 内核更改的简要列表:

  • ashmem(Android 共享内存):一个基于文件的共享内存系统,用于用户空间

  • Binder:一个进程间通信(IPC)和远程过程调用(RPC)系统

  • logger:一个针对写入进行优化的高速内核日志机制

  • paranoid networking:一种限制网络 I/O 到某些进程的机制

  • pmem(物理内存):一个将大块物理内存映射到用户空间的驱动程序

  • Viking Killer:一个替换的 OOM 杀手,在低内存条件下实现了 Android 的“杀死最近最少使用进程”逻辑

  • wakelocks:Android 独特的电源管理解决方案,其中设备的默认状态是睡眠,需要显式操作(通过 wakelock)来防止设备进入睡眠状态

大多数更改都作为设备驱动程序实现,对核心内核代码的更改很少或没有。唯一显著的跨子系统更改是 wakelocks。

今天,许多 Android 补丁被主线 Linux 内核接受。主线内核甚至可以直接启动 Android。关于如何在运行主线内核的 Nexus 7 上启动的 Linaro 博客,你可以找到说明在wiki.linaro.org/LMG/Kernel/FormFactorEnablement

如果某个硬件设备的 Linux 设备驱动程序可用,它通常也可以在 Android 上工作。设备驱动程序的开发与典型 Linux 设备驱动程序的开发相同。如果你想了解与 Android 相关的主线内核合并,你可以查看内核发布说明kernelnewbies.org/LinuxVersions

Android 内核源代码通常由 SoC 供应商提供,例如高通或 MTK。Google 设备的内核源代码可以在android.googlesource.com/kernel/找到。

Google 设备使用来自不同供应商的 SoC,因此您可以在不同供应商这里找到内核源代码。例如,QualComm SoC 的内核源代码位于kernel/msm,MediaTek 的内核源代码位于kernel/mediatek。一般的 Android 内核源代码在kernel/common文件夹中,看起来与 Vanilla 内核非常相似。

AOSP 的默认构建版本适用于谷歌的各种设备,例如 Nexus 或 Pixel。最近它也开始包含一些来自硅供应商的参考板,例如hikey-linaro等。如果您需要针对您的参考平台的特定供应商的 Android 内核,您应该从您的平台供应商那里获取内核源代码。

同时,还有开源社区在维护 Android 内核。例如,ARM 架构的内核可以在 Linaro 的许多参考板上找到。对于 Intel x86 架构,您可以在 Android-x86 项目中找到各种内核版本。正如您可以从以下 Linaro Linux 内核状态网站上看到的那样,linaro-android树是树外 AOSP 补丁的前向移植。它提供了 Google 下一个 AOSP kernel/common.git树“可能”看起来是什么样的预览。

Linaro 的 Android 内核树可以在android.git.linaro.org/gitweb/kernel/linaro-android.git找到。

该内核树的状态可以在wiki.linaro.org/LMG/Kernel/Upstreaming查看。

HAL

HAL 为硬件供应商定义了一个标准接口,允许 Android 对底层驱动实现保持无知。HAL 允许您在不影响或修改高级系统的情况下实现功能。HAL 实现被打包成模块(.so)文件,并在适当的时候由 Android 系统加载。这是将 Android 系统移植到新平台的一个重点。我们将在第三章“发现内核、HAL 和虚拟硬件”中了解更多关于 HAL 的内容。在这本书中,我将详细分析各种硬件接口的 HAL 层。

Android 系统服务

应用框架 API 公开的功能与系统服务通信,以访问底层硬件。应用开发者可能主要交互的两个服务组是系统(如窗口管理器和通知管理器)和媒体(涉及播放和录制媒体的服务)。这些服务是作为 Android 框架的一部分提供应用程序接口的。

除了这些服务之外,还有支持这些系统服务的本地服务,例如 SurfaceFlinger、netd、logcatd、rild 等等。许多服务与您可能在 Linux 发行版中找到的 Linux 守护进程非常相似。在复杂的硬件模块中,例如图形,系统服务和本地服务都需要访问 HAL,以便向应用层提供框架 API。我们将在第六章,在 Android 模拟器上启用 Wi-Fi到第九章,使用 PXE/NFS 启动 x86vbox时讨论系统服务。

Binder IPC

Binder IPC 机制允许应用框架跨越进程边界并调用 Android 系统服务代码。这使得高级框架 API 能够与 Android 系统服务交互。Android 应用程序通常在其自己的进程空间中运行。它没有直接访问系统资源或底层硬件的能力。它必须通过 Binder IPC 与系统服务通信来访问系统资源。由于应用程序和系统服务运行在不同的进程中,Binder IPC 为此提供了机制。

Binder IPC 代理是应用框架访问不同进程空间中系统服务的渠道。这并不意味着它是应用框架和系统服务之间的一个层。Binder IPC 是任何想要与其他进程通信的进程都可以使用的进程间通信机制。例如,系统服务可以使用 Binder IPC 相互通信。

应用框架

应用框架为应用程序提供 API。它通常被应用程序开发者使用。当应用程序调用一个接口后,应用框架通过 Binder IPC 机制与系统服务进行通信。Android 应用框架不仅仅是一组供应用开发者使用的库。它提供了比这更多的功能。

Android 应用框架带给开发社区的突破性技术是应用层和系统层之间一个非常清晰的分离。正如我们所知,Android 应用开发使用 Java 语言,Android 应用程序在类似于 Java 虚拟机的环境中运行。在这种设置中,应用层与系统层被非常清晰地分离。

Android 应用框架还与 Google 的集成开发环境IDE)紧密集成,并提供了一种独特的编程模型。使用这种编程模型和相关工具,Android 开发者可以以极高的效率和生产力进行应用开发。所有这些都是 Android 在移动设备世界中取得巨大成功的关键原因。

我已经在之前的 Android 系统架构图中对所有的层进行了整体介绍。正如我之前提到的关于 Android 系统编程的范围,我们可以将 Android 系统中的所有编程视为系统编程的范围,而不是应用编程。带着这个概念,我们实际上在之前的架构图中遗漏了一部分,那就是恢复。

恢复

在这一章中,我们还想简要地看看恢复,因为在这本书的第二部分中,我们有三章是关于它的。

恢复是一个可以用来升级或重新安装 Android 系统的工具。它是 AOSP 源代码的一部分。恢复的源代码可以在$AOSP/bootable/recovery找到。

与 Android 的其他部分相比,恢复的独特之处在于它本身是一个自包含的系统。我们可以通过以下图示来查看恢复,并将其与我们之前讨论过的 Android 和 Brillo 架构进行比较:

图片

恢复是一个与 Android 系统分开的系统,它与它所支持的 Android 系统共享相同的内核。我们可以将其视为一个迷你操作系统或一个可以在许多嵌入式设备中找到的嵌入式应用程序。它是在与 Android 相同的 Linux 内核上运行的专用应用程序,它执行单个任务,即更新当前的 Android 系统。

当系统启动到恢复模式时,它从闪存中的专用分区启动。这个分区包括包含 Linux 内核和特殊 ramdisk 图像的恢复镜像。如果我们查看 Nexus 5 分区,我们会看到以下列表:

# parted /dev/block/mmcblk0
parted /dev/block/mmcblk0
GNU Parted 1.8.8.1.179-aef3
Using /dev/block/mmcblk0
Welcome to GNU Parted! Type 'help' to view a list of commands.
(parted) print
print
print
Model: MMC SEM32G (sd/mmc)
Disk /dev/block/mmcblk0: 31.3GB
Sector size (logical/physical): 512B/512B
Partition Table: gpt

Number  Start   End     Size    File system  Name      Flags
 1      524kB   67.6MB  67.1MB  fat16        modem
 2      67.6MB  68.7MB  1049kB               sbl1
 3      68.7MB  69.2MB  524kB                rpm
 4      69.2MB  69.7MB  524kB                tz
 5      69.7MB  70.3MB  524kB                sdi
 6      70.3MB  70.8MB  524kB                aboot
7      70.8MB  72.9MB  2097kB               pad
8      72.9MB  73.9MB  1049kB               sbl1b
9      73.9MB  74.4MB  524kB                tzb
10      74.4MB  75.0MB  524kB                rpmb
11      75.0MB  75.5MB  524kB                abootb
12      75.5MB  78.6MB  3146kB               modemst1
13      78.6MB  81.8MB  3146kB               modemst2
14      81.8MB  82.3MB  524kB                metadata
15      82.3MB  99.1MB  16.8MB               misc
16      99.1MB  116MB   16.8MB  ext4         persist
17      116MB   119MB   3146kB               imgdata
18      119MB   142MB   23.1MB               laf
19      142MB   165MB   23.1MB               boot
 **20      165MB   188MB   23.1MB               recovery** 
21      188MB   191MB   3146kB               fsg
22      191MB   192MB   524kB                fsc
23      192MB   192MB   524kB                ssd
24      192MB   193MB   524kB                DDR
25      193MB   1267MB  1074MB  ext4         system
26      1267MB  1298MB  31.5MB               crypto
27      1298MB  2032MB  734MB   ext4         cache
28      2032MB  31.3GB  29.2GB  ext4         userdata
29      31.3GB  31.3GB  5632B                grow

列表中包括 29 个分区,恢复分区是其中之一。恢复 ramdisk 与正常的 ramdisk 具有类似的目录结构。在恢复 ramdisk 的 init 脚本中,init 启动恢复程序,它是恢复模式的主要进程。恢复本身与 Android 系统中的其他本地守护进程相同。恢复的编程是 Android 系统编程范围的一部分。恢复的编程语言和调试方法也与原生 Android 应用程序相同。我们将在本书的第二部分更深入地讨论这个问题。

从 AOSP 派生出的第三方开源项目

如我们所知,AOSP 源代码是我们可以从系统级编程开始工作的主要源代码。各种芯片供应商通常与谷歌合作,以启用他们的参考平台。这是一项巨大的努力,他们不会将所有内容都公布于众,除非是他们的客户。这给开源世界带来了限制。由于 AOSP 源代码主要是为谷歌设备,如模拟器、Nexus 或 Pixel 系列,因此使用 Nexus 设备作为硬件参考平台的开发者没有问题。其他设备呢?制造商可能会发布他们设备的内核源代码,但仅此而已。在开源世界中,几个第三方组织为这种情况提供了解决方案。在接下来的几节中,我们将简要介绍我们在本书中使用的一些解决方案。

LineageOS (CyanogenMod)

LineageOS 是一个社区,为许多流行的 Android 设备提供售后固件分发。它是高度受欢迎的 CyanogenMod 的继任者。如果您无法从 AOSP 源代码为您的设备构建 ROM,您可以考虑 LineageOS 源代码。由于 LineageOS 支持许多设备,许多主要的第三方 ROM 映像都是基于其前身 CyanogenMod 构建的。从中国的著名 MIUI 到最新的 OnePlus 设备,它们都使用 CyanogenMod 源代码作为基础开始。LineageOS/CyanogenMod 对开源世界的重大贡献是将 Linux 内核和 HAL 适配到各种 Android 设备。

LineageOS 的源代码维护在 GitHub 上,您可以在 github.com/LineageOS 找到它。

要为您的设备构建 LineageOS 源代码,整体构建过程与 AOSP 构建类似。关键区别是 LineageOS 支持的设备数量众多。对于每个设备,都有一个网页提供有关如何为该设备构建的信息。我们以 Nexus 5 为例。您可以通过以下页面获取详细信息:

wiki.lineageos.org/devices/hammerhead

在信息页面上,您可以找到有关如何下载 ROM 映像、如何安装映像以及如何构建映像的信息。有一个针对设备的构建指南,我们可以在 wiki.lineageos.org/devices/hammerhead/build 找到 Nexus 5 的构建指南。

要为 Nexus 5 构建 LineageOS,两个关键元素是 内核设备。内核包括 Linux 内核和 Nexus 5 特定的设备驱动程序,而设备包括设备特定 HAL 代码的大部分。内核和设备文件夹的命名规范是 android_kernel/device_{制造商}_{代号}

Nexus 5 的代号为 hammerhead,制造商为 lge,即 LG。

我们可以找到以下两个用于内核和设备的 Git 仓库:

github.com/LineageOS/android_kernel_lge_hammerhead

github.com/LineageOS/android_device_lge_hammerhead

除了内核和设备之外,其他重要信息是 LineageOS 版本。你可以在同一设备信息页面上找到它。对于 Nexus 5,可用的版本有 11、12、12.1、13 和 14.1。你可能想知道如何将 LineageOS 版本与 AOSP 版本匹配。

有关 CyanogenMod 和 LineageOS 的信息可以在以下两个维基百科页面上找到:

en.wikipedia.org/wiki/CyanogenMod#Version_history

en.wikipedia.org/wiki/LineageOS#Version_history

Nexus 5 支持的 LineageOS/CyanogenMod 和 AOSP 版本是 CM11 (Android 4.4)、CM 12 (Android 5.0)、CM 12.1 (Android 5.1)、CM 13 (Android 6.0) 和 CM 14.1 (Android 7.1.1)。

在阅读这本书的过程中,你将无法访问与 CyanogenMod 相关的链接,因为最近 CyanogenMod 背后的基础设施已经关闭。你可以阅读以下帖子以获取更多信息:

plus.google.com/+CyanogenMod/posts/RYBfQ9rTjEH

尽管如此,前述配置中的想法是,区分不同设备的关键代码是内核和设备。有可能在设备之间共享其余的代码。这是本书中项目的一个目标。我们试图将不同硬件平台的更改限制在内核和设备中,同时保持其余的 AOSP 源代码不变。这并非 100% 可能,但我们尽可能尝试这样做。好处是我们可以将我们的代码与 AOSP 代码分开,并且更新到新 AOSP 版本要容易得多。

Android-x86

虽然 LineageOS/CyanogenMod 为大量 Android 设备提供了出色的支持,但这些设备中的许多都是来自各种硅供应商的 ARM 架构设备,例如高通、三星、MTK 等。同样,也有针对基于 Intel 的 Android 设备的开源社区。这是另一个著名的开源项目,Android-x86。尽管市场上基于 Intel x86 的 Android 设备数量无法与 ARM 架构设备相比,但还有另一个市场广泛使用 Intel x86 Android 构建,那就是 Android 模拟器市场。对于商业 Android 模拟器产品,你可以找到 AMI DuOS、Genymotion、Andy 等等。

与 LineageOS/CyanogenMod 相比,Android-x86 项目在支持各种基于 Intel x86 的设备时采用了非常不同的方法。其目标是为任何 Intel x86 设备提供板级支持包BSP)。这类似于您在 PC 上安装 Microsoft Windows 或 Linux 的方式。您只有一份发布版本,可以在任何 Intel PC 上安装。没有为每个不同的 PC 或笔记本电脑专门构建的 Windows 或 Linux 版本。

为了在 Android 上实现这一目标,Android-x86 对 Android 启动过程进行了显著定制。在 Android-x86 中,启动过程分为两个阶段。第一阶段是使用特殊的 ramdisk--initrd.img启动一个最小的 Linux 环境。当系统可以启动到这个 Linux 环境时,它将通过chrootswitch_root命令启动第二阶段。在这个阶段,它将启动实际的 Android 系统。

这是一种非常聪明的利用现有技术解决新挑战的方法。本质上,我们试图分两步解决问题。在启动过程的第一个阶段,由于 Windows 和 Linux 都可以在没有专用构建的情况下在 Intel x86 PC 上启动,您应该能够不太费力地在 Intel 设备上启动 Linux。这正是 Android-x86 启动的第一个阶段所做的事情。当最小的 Linux 系统可以正常运行时,这意味着最小的一组硬件设备已经初始化,您可以使用这个最小的 Linux 环境来调试或启动系统的其余部分。在第二阶段,可以启动一个适用于 Intel x86 的通用 Android 镜像,并且只需要有限的硬件初始化。这种方法也可以用于硬件设备的调试。本书将展示我们如何在 Android 模拟器上做同样的事情。

Android-x86 项目的官方网站是www.android-x86.org/。您可以在那里找到关于 Android-x86 项目的相关信息。要构建 Android-x86,获取源代码稍微有些棘手。原始源代码托管在http://git.android-x86.org,由台湾 Linux 用户组TLUG)的志愿者维护。它有效了几年。然而,从 2015 年 4 月起,它就不再工作了。

你可以从 Google 讨论组groups.google.com/forum/#!forum/android-x86中找到最新的状态。讨论组中有一个关于git.android-x86.org问题的官方公告,由维护者 Chih-Wei Huang 发布。后来,托管被短暂地移至 SourceForge。然而,自 2016 年 7 月以来,又报告了从 SourceForge 检索源代码的问题。目前,源代码托管在 OSDN 上,你可以在 Android-x86 讨论组中搜索 Chih-Wei Huang 于 2016 年 9 月 8 日发布的公告。由于大多数开源项目都是由志愿者维护的,它们可能会不时地上线或下线。始终保留你正在工作的项目自己的镜像是个好主意。我们将在本书中讨论这个问题,以便你可以完全控制自己的工作。

我们知道许多开源项目之间相互关联,Android-x86 和 LineageOS/CyanogenMod 也是如此。从 2016 年 1 月开始,Jaap Jan Meijer 对 CyanogenMod 进行了初始移植到 Android-x86,这使得 CyanogenMod 可以在大多数英特尔设备上使用。如果你对这个话题感兴趣,你可以在 Android-x86 讨论组中搜索CM 移植计划

CWM/CMR/TWRP

作为系统级编程的一部分,我们在上一节中介绍了恢复功能。AOSP 的原始恢复功能仅支持非常有限的功能,因此有许多第三方恢复项目。

ClockworkMod 恢复CWM)是著名的开源恢复项目之一,由 Koushik Dutta 编写。尽管现在许多人仍然使用 ClockworkMod 恢复,但该项目已经停止开发一段时间了。

另一个恢复项目是CyanogenMod 恢复CMR)。CMR 由 CyanogenMod 团队维护,它与 ClockworkMod 恢复非常相似。

TWRPTeamWin 恢复项目是另一个非常广泛使用的自定义恢复。它是完全触摸驱动的,并拥有最完整的特性集之一。TWRP 是 OmniROM 的默认恢复,其源代码托管在 GitHub 上,作为 OmniROM 的一部分,网址为github.com/omnirom/android_bootable_recovery/

集成策略

在前面的章节中,我们讨论了 Android 架构、AOSP 以及 Android 的第三方开源项目。软件行业已经存在了几十年。有如此多的现有源代码可以重用,从头开始创建的需求非常罕见。为新平台进行移植和定制基本上是集成艺术。

在这本书中,我们将使用 AOSP 源代码作为基础,并尝试在其上构建一切。然而,我们可能不能仅依赖于 AOSP 源代码。实际上,我们想展示如何支持 AOSP 不支持的平台。我们将如何做到这一点?我们是从头开始创建吗?答案是:不是。我们将演示如何将所有现有项目整合在一起以创建一个新平台。这就是我们讨论第三方开源项目的原因。

在我们的案例中,VirtualBox 不受 AOSP 支持,我们将使用 AOSP 和 Android-x86 来启用它。我们需要使用来自 AOSP 和 Android-x86 的项目来为 VirtualBox 构建一个系统。然而,我们的目标是创建一个对 AOSP 源代码树改动最小的 VirtualBox 新构建系统。这也是许多基于 AOSP 的其他项目的目标。

基于前面的理解,在我们的集成过程中,我们有四个类别的项目:

  • 未经修改的原始 AOSP 项目:在这些类型的项目中,我们将使用未经修改的 AOSP 项目。

  • 第三方项目:在这个类别中,项目是由第三方项目添加的,并且不是 AOSP 的一部分,因此也没有涉及任何更改。

  • 由 AOSP 和第三方项目共同修改的项目:这很复杂。我们需要审查第三方更改并决定我们是否希望将它们包含在我们的系统中。

  • 由多个开源项目和 AOSP 修改的项目:这是我们应尽量避免集成或更改的最复杂情况。

很容易理解,我们应该尽可能多地重用类别 1 和 2 中的项目。挑战和主要工作将在类别 3 中,而我们应该尽可能避免类别 4。

虚拟硬件参考平台

新的 Android 发布通常包含两个参考平台。开发者可以先在 Android 模拟器上测试新的 Android 发布。这在预览阶段可能非常有用。在官方发布后,Google 硬件平台,如 Nexus 或 Pixel,通常成为开发者的设备。模拟器和 Nexus/Pixel 构建是 AOSP 中最早可用的构建。

在这本书中,我们将使用 Android 模拟器作为我们主题的虚拟硬件参考平台。由于 Android 模拟器的构建已经在 AOSP 中可用,你可能会想知道我们能用它做什么。实际上,我们可以通过添加新功能来定制现有的平台。这就是 OEM/ODM 公司通常使用来自硅供应商的参考平台所做的事情。使用 Android 模拟器,我们将演示如何创建一个新设备,以便我们可以对其进行定制。如果你了解任何商业模拟器产品,例如 Genymotion 和 AMI DuOS,那么你可能知道这些产品为模拟器添加了哪些功能。我们将以非常相似的方式扩展 Android 模拟器。

在我们探讨了关于新设备定制的主题之后,我们将探讨更多关于移植的高级主题。移植的主要工作是内核和 HAL 的更改。为了讨论关于移植和调试的高级主题,我们还将使用 VirtualBox 作为另一个虚拟硬件参考平台。尽管 VirtualBox 被许多商业模拟器产品(如 Genymotion、AMI DuOS、Leapdroid 等)使用,但它并未直接得到 AOSP 的支持。大多数 PC 上的 Android 模拟器都是基于 VirtualBox 的,它们是为游戏玩家运行 Android 游戏而设计的。在这本书中,我们将学习如何使用各种开源资源创建类似的构建。

x86 架构的 Android 模拟器简介

Android 模拟器在 Android 4、5、6 和 7 版本中也发生了巨大变化。在 Android 5 之前,Android 模拟器是基于名为 goldfish 的虚拟硬件参考板构建的。

金鱼虚拟硬件平台的硬件规范可以在 AOSP 源代码树中的$AOSP/platform/external/qemu/docs/GOLDFISH-VIRTUAL-HARDWARE.TXT找到。在这本书中,我们将把 AOSP 根目录称为$AOSP

金鱼虚拟硬件平台是基于 QEMU 1.x 构建的,用于在 x86 环境中模拟 ARM 设备。x86 主机环境可以是 Windows、Linux 或 macOS X 计算机。由于目标设备架构是通过 QEMU 模拟的,性能较差。模拟器运行非常缓慢,对于应用程序开发者来说难以使用。然而,QEMU 在 x86 架构上得到了积极开发,并且与各种虚拟化技术(如 VT-x、AMD-V 等)广泛使用。

自 Android 4.x 以来,英特尔在 Linux 上使用 KVM 和 Windows 及 macOS X 上的 Intel HAXM 开发了一个基于 x86 的 Android 模拟器。随着虚拟化技术被引入模拟器,基于英特尔 x86 的模拟器比模拟 ARM 或 MIPS 架构的模拟器要快得多。为了方便 Android 应用程序开发者,谷歌官方将基于英特尔 x86 的 Android 模拟器集成到 Android SDK 中。基于英特尔 x86 的 Android 模拟器已成为开发者测试其 Android 应用程序的首选选择。

ranchu 简介

随着 Android 5(Lollipop)的引入,64 位硬件架构对 ARM 和英特尔平台都可用。然而,当时 Android 的 64 位硬件设备仍在开发中。开发者唯一的选择是从硅供应商那里获取硬件参考平台。

为了帮助开发者测试其应用程序在 64 位架构上的运行,Linaro 的工程师在 QEMU 上实现了一个虚拟硬件平台,以测试 ARMv8-A 64 位架构。他们给这个虚拟硬件平台取了一个代号,ranchu。您可以参考 Linaro 的博客,由 Alex Bennée 撰写,www.linaro.org/blog/core-dump/running-64bit-android-l-qemu/

这个改变后来被谷歌采纳,并作为下一代 Android 模拟器的硬件参考平台。如果你安装了 Android SDK 镜像,你可以从 Android 5 开始看到两个内核镜像。内核镜像kernel-qemu是与 goldfish 虚拟硬件平台一起使用的镜像,而镜像kernel-ranchu是与 ranchu 虚拟硬件平台一起使用的镜像。

为了应对这一变化,英特尔和 MIPS 都对其架构进行了工作,以支持 ranchu 中的 64 位硬件模拟。你可以参考在groups.google.com/forum/#!topic/android-emulator-dev/dltBnUW_HzU的群组讨论。

ranchu 硬件平台基于更新的 QEMU 版本,其架构的改变减少了对于谷歌修改和 goldfish 特定设备的依赖。例如,它使用 virtio-block 设备来模拟 NAND 和 SD 卡。这有可能提供更好的性能,同时也使得利用最新 QEMU 代码库提供的功能成为可能。ranchu 内核是基于android-goldfish-3.10分支的新版本构建的,而最新的 goldfish 内核在android-goldfish-3.4分支。你可以通过在 Android 虚拟设备上使用来自 Android SDK 的不同内核来注意到这种差异。

基于 VirtualBox 的 Android 模拟器

随着虚拟化技术的不断演变,市场上也出现了许多基于商业的 Android 模拟器产品。你可能听说过其中的一些,例如 Genymotion、AMIDuOS、Andy、BlueStacks 等等。其中许多都是使用 Oracle 的 VirtualBox 构建的,例如 Genymotion、AMIDuOS 和 Andy。使用 VirtualBox 而不是其他解决方案(如 VMware)的原因是 VirtualBox 是一个开源解决方案。

为了实现最佳的性能和用户体验,商业模拟器产品中的主机和目标都需要进行定制。除了 Android 模拟器,我们还将使用 VirtualBox 作为虚拟硬件平台来演示如何将 Android 移植到新平台。在这本书中我们需要另一个虚拟硬件平台的原因是 Android 模拟器已经在 AOSP 中得到支持。我们将使用 Android 模拟器作为一个平台来教授如何扩展和定制现有平台。而 VirtualBox 在 AOSP 中不受支持,但它可以用作目标平台来教授如何将 Android 移植到新平台。尽管 Android 已经被 Genymotion、AMI 和其他人移植到 VirtualBox,但它们都不是开源产品。

摘要

在本章中,我们讨论了 Android 系统编程是什么以及本书中涉及的系统级编程范围。之后,我们对 Android 系统架构进行了概述,并讨论了本书中我们将关注的层次。我们还讨论了本书中使用的虚拟硬件平台。在本书中,我们使用了来自各种第三方项目的代码,因此在本章中也简要概述了每个项目。在下一章中,我们将开始学习 Android 系统编程的开发环境设置。这包括开发工具和源代码仓库的设置。

第二章:设置开发环境

在上一章介绍系统编程之后,我们需要首先设置一个开发环境,然后我们才能继续前进。在我们探索本书中各种 Android 系统编程主题的同时,我们需要了解如何构建和测试 Android 开源项目(AOSP)。在本章中,我们将涵盖以下主题:

  • 安装 Android SDK 和设置 Android 虚拟设备

  • 设置 AOSP 构建环境和构建测试镜像

  • 创建自己的源代码仓库镜像

Android 版本总结

由于我们将使用 Android 模拟器作为虚拟硬件平台之一,因此在这本书中我们需要使用特定的 Android 版本。在撰写本书时,最新的 Android 版本是 Android 7(牛轧糖)。本书将使用 Android 7。我开始这本书的工作时使用的是 Android 6,因此 Android 6 的源代码也存放在我的 GitHub 仓库中,网址为 github.com/shugaoye

从第一个版本到 Android 7,开发环境和 AOSP 源代码都发生了很大变化。在我们讨论开发环境设置之前,我们将简要回顾各种 Android 版本。

要设置 AOSP 构建环境,有两件事需要特别注意:主机环境和 Java SDK。尽管推荐的宿主环境是运行在英特尔架构上的 Ubuntu,但硬件架构和 Ubuntu 版本从发布到发布都有所变化。你可以始终参考以下 Google 的 URL 获取最新的 AOSP 构建环境设置:

source.android.com/source/index.html

对于 Gingerbread(2.3.x)及以上版本,需要 64 位构建环境。对于较旧版本,构建环境是 32 位系统。

使用的 Ubuntu 版本范围从 Ubuntu 10.04 到 14.04,但对于每个版本都有一个推荐的 Ubuntu 版本。如果是新的设置,建议使用推荐的 Ubuntu 版本来使工作更简单。然而,这里没有硬性要求。你应该能够使用比推荐版本更高的任何 Ubuntu 版本。也有很多关于如何使用不同的 Linux 发行版(如 RedHat 或 Debain)设置 AOSP 构建的文章。

在 Lollipop 及以上版本中,使用 OpenJDK 替代了 Oracle JDK,用于构建 AOSP。

以下表格总结了直到 Nougat 的所有 Android 发布、所需主机和 JDK 环境;你可以参考它以获取详细信息。

AOSP 发布

昵称 AOSP SDK API 级别 主机 JDK 操作系统/Ubuntu Goldfish Ranchu
Cupcake 1.5 3 x86 Oracle JDK 5 10.04 x
Donut 1.6 4 x86 Oracle JDK 5 10.04 x
Eclair 2.0/2.1 5 x86 Oracle JDK 5 10.04 x
甜点 2.0.1 6 x86 Oracle JDK 5 10.04 x
甜点 2.1 7 x86 Oracle JDK 5 10.04 x
甜甜圈 2.2 8 x86 Oracle JDK 5 10.04 x
甜点 2.3.1 9 x64 Oracle JDK 6 12.04 x
姜饼 2.3.3 10 x64 Oracle JDK 6 12.04 x
蜂巢 3.0 11 x64 Oracle JDK 6 12.04 x
蜂巢 3.1 12 x64 Oracle JDK 6 12.04 x
蜂巢 3.2 13 x64 Oracle JDK 6 12.04 x
冰淇淋三明治 4.0 14 x64 Oracle JDK 6 12.04 x
冰淇淋三明治 4.0.3 15 x64 Oracle JDK 6 12.04 x
姜饼 4.1.2 16 x64 Oracle JDK 6 12.04 x
姜饼 4.2.2 17 x64 Oracle JDK 6 12.04 x
姜饼 4.3.1 18 x64 Oracle JDK 6 12.04 x
棉花糖 4.4.2 19 x64 Oracle JDK 6 12.04 x x
棉花糖 4.4W.2 20 x64 Oracle JDK 6 12.04 x x
棉花糖 5.0.1 21 x64 Open JDK 7 12.04 x x
棉花糖 5.1.1 22 x64 Open JDK 7 12.04 x x
棉花糖 6.0 23 x64 Open JDK 7 14.04 x x
棉花糖 7.0.x 24 x64 Open JDK 8 14.04 x x
棉花糖 7.1.1 25 x64 Open JDK 8 14.04 x x

从前面的表格中,你可以看到 ranchu 模拟器支持 KitKat 和其他系统。如果你在 Android SDK 上安装并下载 KitKat 或其他系统的系统镜像,你应该能够找到两个内核文件,kernel-qemukernel-ranchu

Nougat 版本中有两个 API 级别。Android 7.0.0 和 7.1.0 是 API 级别 24。Android 7.1.1 和 7.1.2 是 API 级别 25。本书中的所有源代码都支持到 API 级别 25。

原始 Android 模拟器的代号是 goldfish。它基于较老版本的 QEMU。2016 年基于 QEMU 2.x 发布了一个新的 Android 模拟器版本。这个新模拟器的代号是 ranchu。它支持 KitKat 和其他系统。

安装 Android SDK 并设置 Android 虚拟设备

理想情况下,如果你有一个 AOSP 构建环境,你可以从头开始构建包括 Android SDK 在内的所有内容。然而,拥有一个 Android SDK 安装包来帮助创建虚拟设备或运行模拟器镜像要方便得多。

你可以从以下网站始终下载最新的 Android SDK:

developer.android.com/index.html

本书使用的宿主环境是 Ubuntu 14.04。下载适用于 Linux 的 Android SDK 并将其解压缩到你的Home目录下的一个文件夹中。

Android SDK 中的工具自 API 级别 25 以来已经发生了变化。你可以使用较旧的 Android SDK 或最新的 Android SDK,因此我在这里提供了两种情况的说明。

在较旧版本的 SDK 中创建 AVD

对于较旧的 SDK 版本,例如 android-sdk_r24.4.1-linux.tgz,它包含所有必要的组件,我们可以在解压缩后使用它。解压缩后,我们可以找到以下内容:

$ ls android-sdk-linux
add-ons      platforms       SDK Readme.txt  temp
build-tools  platform-tools  system-images   tools

你可以将 platform-toolstools 目录添加到你的 PATH 环境变量中。

在本书中,我们将使用基于 API 级别 25 的虚拟设备来测试我们的镜像。

要创建虚拟设备,我们可以使用以下命令启动 Android 虚拟设备AVD)管理器,如下面的截图所示:

$ android avd  

图片

AVD 管理器

在 AVD 管理器中单击“创建...”按钮,创建一个名为 a25x86 的新虚拟设备,配置如下,如下面的截图所示:

  • Android 7.1.1 - API 级别 25

  • 1024 MB RAM

  • 400 MB SD 卡

  • 400 MB 内部存储

  • 显示尺寸为 480 x 800:hdpi

图片

Android 虚拟设备 a25x86

在最新版本的 SDK 中创建 AVD

对于较新版本,只有 SDK 命令行工具可供下载。例如,如果你下载了 r25 的命令行工具,如 tools_r25.2.3-linux.zip,你只能找到 tools 文件夹。在这种情况下,你需要使用 Android SDK 管理器在 tools/bin/sdkmanager 中下载其余的 SDK 组件。要下载其余的 SDK 组件,你可以使用以下命令:

$ sdkmanager --update

如果你使用的是最新版本的 Android SDK,如果你遵循前面的说明,你可能会得到以下错误消息:

$ android avd
*********************************************************************
The "android" command is deprecated.
For manual SDK, AVD, and project management, please use Android Studio.
For command-line tools, use tools/bin/sdkmanager and tools/bin/avdmanager
*********************************************************************
Invalid or unsupported command "avd"

Supported commands are:
android list target
android list avd
android list device
android create avd
android move avd
android delete avd
android list sdk
android update sdk

在这种情况下,你可以使用以下命令创建 AVD。

$ avdmanager create avd -n a25x86 --tag google_apis -k 'system-images;android-25;google_apis;x86'
Auto-selecting single ABI x86
Do you wish to create a custom hardware profile? [no]

测试 goldfish 模拟器

在 Android 7 中,ranchu 和 goldfish 模拟器都受到支持。让我们首先测试 goldfish 模拟器。我们可以使用以下命令在 goldfish 模拟器中运行此虚拟设备:

$ emulator @a25x86 -verbose -show-kernel -shell -engine classic
emulator:Found AVD name 'a25x86'
emulator:Found AVD target architecture: x86
emulator:Looking for emulator-x86 to emulate 'x86' CPU
...
 kernel.path = /home/roger/android-sdk-linux/system-images/android-  
  25/default/x86/kernel-qemu
...  

要监控虚拟设备的状态,我们可以使用以下 Android 模拟器选项:

  • -verbose: 显示模拟器调试信息。

  • -show-kernel: 显示内核调试信息。

  • -shell: 使用 stdio 作为命令行提示符。

  • -engine: 选择模拟器引擎。选项可以是 autoclassicqemu2classic 选项是使用 goldfish 模拟器,而 qemu2 选项是使用 ranchu 模拟器。如果选项是 auto 或没有 engine 选项,系统将检查环境并尝试首先启动 ranchu。如果失败,将回退到 goldfish。

从前面的日志中,我们可以看到 goldfish 模拟器使用的是 kernel-qemu 内核文件。

ranchu 和 goldfish 模拟器都是在 QEMU 的基础上开发的,但它们使用不同的内核和 QEMU 版本。我们可以使用以下模拟器命令验证 goldfish 或 ranchu 使用的 QEMU 版本。

要验证 goldfish 使用的 QEMU 版本,我们可以运行以下命令:

$ emulator -engine classic -qemu -version
QEMU PC emulator version 0.10.50 Android, Copyright (c) 2003-2008 Fabrice Bellard  

从前面的输出中,我们可以看到 goldfish 模拟器使用的是 QEMU 版本 0.10.50。

对于最新的模拟器版本,似乎在处理经典引擎方面存在一个错误。当你执行前面的命令时,可能会得到以下错误信息:

$ emulator -engine classic -qemu -version
emulator: ERROR: android_qemud_get_serial_line: can't create charpipe to serial port  

模拟器命令是 QEMU 的包装器。任何 -qemu 之后的所有命令行选项都直接作为 QEMU 的命令行传递给 QEMU。

要找出模拟器版本,我们可以使用以下命令:

**$ emulator -version** 以下命令将显示 QEMU 版本:

**$ emulator -qemu -version**

在 Android 设备成功启动后,从 Android UI,我们可以进入设置 -> 关于手机,并查看以下截图所示的屏幕:

goldfish 的 Android 内核版本

请注意关于“关于手机”屏幕上的以下信息:

  • Android 版本:7.1

  • 内核版本:3.4.67

  • 构建号:sdk_google_phone_x86-userdebug 7.1 NPF26K 3479480 test-keys

如前所述的信息所示,内核版本是 3.4.67,文件系统构建号是 sdk_google_phone_x86-userdebug 7.1 NPF26K 3479480 test-keys,针对金鱼模拟器。在下一节中,我们可以看到 ranchu 模拟器使用不同的内核版本,尽管这两个模拟器共享相同的文件系统。

Android 系统构建包括两部分:AOSP 系统和一个兼容 Android 的 Linux 内核。AOSP 系统的构建结果包括 Android 系统的所有镜像文件,除了内核镜像。它们是分别构建的,并且也处于不同的许可之下。AOSP 的首选许可协议是 Apache 软件许可协议,而 Linux 内核则处于 GPLv2 许可协议之下。请注意这一差异。这也意味着 AOSP 构建不包括内核构建。我们必须单独构建内核。我们还可以使用与金鱼和 ranchu 模拟器测试中相同的文件系统使用不同的内核镜像。

当我们谈论 Android 版本时,我们必须查看内核版本和文件系统构建号的详细信息。

测试 ranchu 模拟器

我们也可以使用相同的虚拟设备测试 ranchu 模拟器。我们可以使用不带 -engine 选项或带有 -engine qemu2 选项的类似命令来启动 ranchu 模拟器:

$ emulator @a25x86 -verbose -show-kernel -shell
emulator:Found AVD name 'a25x86'
emulator:Found AVD target architecture: x86
emulator:  Found directory: /home/roger/android-sdk-linux/system-images/android-25/default/x86/

emulator:Probing for /home/roger/android-sdk-linux/system-images/android-25/default/x86//kernel-ranchu: file exists
emulator:Auto-config: -engine qemu2 (based on configuration)
emulator:Found target-specific 64-bit emulator binary: /home/roger/android-sdk-linux/tools/qemu/linux-x86_64/qemu-system-i386
...  

从前面的日志中,我们可以看到在 ranchu 模拟器中使用内核文件 kernel-ranchu

我们还可以使用以下命令验证 ranchu 模拟器使用的 QEMU 版本:

$ emulator -qemu -version
QEMU emulator version 2.2.0 , Copyright (c) 2003-2008 Fabrice Bellard  

我们可以看到 ranchu 使用了一个支持许多新特性的较新版本的 QEMU,我们将在本书的后续章节中讨论这些特性。

再次,让我们回顾一下设置中的版本信息,就像我们为金鱼模拟器所做的那样;参见图下所示:

ranchu 的 Android 内核版本

我们可以看到 ranchu 模拟器使用的是内核版本 3.10.0,这与金鱼模拟器不同。文件系统构建与金鱼模拟器相同。

AOSP 构建环境和 Android 模拟器构建

为了创建我们自己的 Android 系统,我们必须设置 AOSP 构建环境并构建用于 Android 模拟器的 AOSP 目标。由于 Android 正在快速发展,构建过程和环境设置可能会随时发生变化。您始终可以参考谷歌的网站以获取最新信息,source.android.com/source/building.html

虽然谷歌网站和其他资源可以提供关于 AOSP 构建的一般指南和程序,但在本节中,我们将具体探讨如何为 API 级别 25 的 Android 模拟器镜像构建 AOSP。

AOSP 构建环境

由于我们想要为 API 级别 25 设置构建环境,您可以参考 AOSP 发布表以获取主机和 JDK 的基本要求。建议使用 64 位的 Ubuntu 14.04 主机和 Open JDK 8。对于硬件要求,您可能需要一个至少拥有 8 GB RAM 和 500 GB 硬盘空间的强大计算机。

安装所需的软件包

我们使用 64 位的 Ubuntu 14.04 版本作为我们的宿主操作系统。在安装 Ubuntu 14.04 之后,您必须做的第一件事是按照以下方式安装所有必要的软件包。如果您使用的是不同的 Linux 发行版,您可以参考谷歌的网站或在网上搜索相关的设置程序。让我们执行以下命令来安装 Ubuntu 14.04 的所有必要软件包:

$ sudo apt-get install git-core gnupg flex bison gperf build-essential\ 
 zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386\ 
 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z-dev ccache\ 
 libgl1-mesa-dev libxml2-utils xsltproc unzip  

安装 Open JDK 7 和 8

我们将安装 Open JDK 7 和 8,这样我们就可以在我们的构建环境中构建 Android 6 和 7。

要构建 Android API 级别 23,我们需要安装 OpenJDK 7。我们可以从 Linux 控制台执行以下命令来安装 OpenJDK 7:

$ sudo apt-get update
$ sudo apt-get install openjdk-7-jdk  

对于 Android 7,我们需要使用 OpenJDK 8 来构建。目前还没有适用于 Ubuntu 14.04 的受支持的 OpenJDK 8 软件包,但 Ubuntu 15.04 的 OpenJDK 8 软件包已经成功用于 Ubuntu 14.04。我们需要按照以下说明在 Ubuntu 14.04 上安装 OpenJDK 8。

archive.ubuntu.com下载 64 位架构的.deb软件包:

openjdk-8-jre-headless_8u45-b14-1_amd64.deb with SHA256 0f5aba8db39088283b51e00054813063173a4d8809f70033976f83e214ab56c0
openjdk-8-jre_8u45-b14-1_amd64.deb with SHA256 9ef76c4562d39432b69baf6c18f199707c5c56a5b4566847df908b7d74e15849
openjdk-8-jdk_8u45-b14-1_amd64.deb with SHA256 6e47215cf6205aa829e6a0a64985075bd29d1f428a4006a80c9db371c2fc3c4c

可选地,确认下载文件的校验和与每个先前包中列出的 SHA256 字符串相匹配。

例如,使用sha256sum工具:

$ sha256sum {downloaded.deb file}  

安装软件包:

$ sudo apt-get update  

对您下载的每个.deb文件运行dpkg。由于缺少依赖项,可能会产生错误:

$ sudo dpkg -i {downloaded.deb file}  

为了修复缺少的依赖项:

$ sudo apt-get -f install  

在安装了 OpenJDK 7 和 8 之后,我们可以通过运行以下命令来更新默认的 Java 版本:

$ sudo update-alternatives --config java
$ sudo update-alternatives --config javac  

我们现在已经准备好了构建环境。您可能想要参考谷歌的网站来设置其他事项。例如,我们可能想要使用缓存来加速构建或设置一个独立的输出目录,不在 AOSP 树中。

下载 AOSP 源代码

一旦我们准备好了构建环境,我们需要获取 AOSP 源代码。再次,请参考谷歌的网站或互联网以获取更多信息。

您需要从source.android.com下载 Android 7 源代码。

安装 repo

AOSP 由大量的 Git 仓库组成,我们必须使用 repo 工具来管理这些 Git 仓库。要下载和安装 repo,我们可以使用以下命令:

$ mkdir ~/bin
$ PATH=~/bin:$PATH
$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
$ chmod a+x ~/bin/repo  

初始化 repo 客户端并下载 AOSP 源代码树

在我们有了 repo 工具之后,我们可以执行以下命令来初始化 repo 并下载 AOSP 源代码树:

$ repo init -u https://android.googlesource.com/platform/manifest -b android-7.1.1_r4
$ repo sync  

注意这里的 AOSP 标签android-7.1.1_r4。这是我们全书使用的 AOSP 源代码版本。

获取 AOSP 源代码树需要相当长的时间。在获取源代码树后,让我们看看顶级文件夹:

$ ls -F
abi/      cts/         docs/       libcore/         packages/  tools/
art/      dalvik/      external/   libnativehelper/ pdk/
bionic    developers   filelist    Makefile         prebuilts
bootable  development  frameworks  ndk              sdk
build     device       hardware    out              system  

我不会在这里详细探讨源代码树;我们将在第三章中介绍,发现内核、HAL 和虚拟硬件

构建 AOSP Android 模拟器镜像

在本书中,我们将使用基于 x86 的模拟器。基于 x86 的模拟器可以在主机上使用虚拟化技术,因此它比 ARM 模拟器快得多。我们首先想要构建的是包含 AOSP 源代码的那个。要从 AOSP 顶级文件夹执行以下命令来创建 Android 模拟器构建:

$ . build/envsetup.sh 
including device/generic/mini-emulator-arm64/vendorsetup.sh
including device/generic/mini-emulator-armv7-a-neon/vendorsetup.sh
including device/generic/mini-emulator-mips/vendorsetup.sh
including device/generic/mini-emulator-x86_64/vendorsetup.sh
including device/generic/mini-emulator-x86/vendorsetup.sh
including sdk/bash_completion/adb.bash
$ lunch

You're building on Linux

Lunch menu... pick a combo:
 1\. aosp_arm-eng
 2\. aosp_arm64-eng
 3\. aosp_mips-eng
 4\. aosp_mips64-eng
 5\. aosp_x86-eng
 6\. aosp_x86_64-eng
 7\. mini_emulator_arm64-userdebug
 8\. m_e_arm-userdebug
 9\. mini_emulator_mips-userdebug
 10\. mini_emulator_x86_64-userdebug
 11\. mini_emulator_x86-userdebug

Which would you like? [aosp_arm-eng] 5

============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=7.1.1
TARGET_PRODUCT=aosp_x86
TARGET_BUILD_VARIANT=eng
TARGET_BUILD_TYPE=release
TARGET_BUILD_APPS=
TARGET_ARCH=x86
TARGET_ARCH_VARIANT=x86
TARGET_CPU_VARIANT=
TARGET_2ND_ARCH=
TARGET_2ND_ARCH_VARIANT=
TARGET_2ND_CPU_VARIANT=
HOST_ARCH=x86_64
HOST_2ND_ARCH=x86
HOST_OS=linux
HOST_OS_EXTRA=Linux-4.2.0-27-generic-x86_64-with-Ubuntu-14.04-trusty
HOST_CROSS_OS=windows
HOST_CROSS_ARCH=x86
HOST_CROSS_2ND_ARCH=x86_64
HOST_BUILD_TYPE=release
BUILD_ID=NMF26O
OUT_DIR=out
============================================  

我们首先使用启动脚本envsetup.sh设置环境变量。之后,我们执行lunch命令来选择构建目标。要为 Android-x86 模拟器构建,我们可以选择目标aosp_x86-eng,这将构建适用于 x86 的 Android 模拟器版本。要了解更多关于脚本文件envsetup.sh和命令lunch的信息,请参考谷歌网站source.android.com

在执行以下make命令后,实际构建开始:

$ make -j4
============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=7.1.1
TARGET_PRODUCT=aosp_x86
TARGET_BUILD_VARIANT=eng
TARGET_BUILD_TYPE=release
TARGET_BUILD_APPS=
TARGET_ARCH=x86
TARGET_ARCH_VARIANT=x86
TARGET_CPU_VARIANT=
TARGET_2ND_ARCH=
TARGET_2ND_ARCH_VARIANT=
TARGET_2ND_CPU_VARIANT=
HOST_ARCH=x86_64
HOST_OS=linux
HOST_OS_EXTRA=Linux-4.2.0-27-generic-x86_64-with-Ubuntu-14.04-trusty
HOST_BUILD_TYPE=release
BUILD_ID=MOB30M
OUT_DIR=out
============================================
including ./abi/cpp/Android.mk ...
including ./art/Android.mk ...
...
make_ext4fs -S out/target/product/generic_x86/root/file_contexts -l 576716800 -a system out/target/product/generic_x86/obj/PACKAGING/systemimage_intermediates/system.img out/target/product/generic_x86/system
+ make_ext4fs -S out/target/product/generic_x86/root/file_contexts -l 576716800 -a system out/target/product/generic_x86/obj/PACKAGING/systemimage_intermediates/system.img out/target/product/generic_x86/system
Creating filesystem with parameters:
 Size: 576716800
 Block size: 4096
 Blocks per group: 32768
 Inodes per group: 7040
 Inode size: 256
 Journal blocks: 2200
 Label: 
 Blocks: 140800
 Block groups: 5
 Reserved block group size: 39
Created filesystem with 1277/35200 inodes and 82235/140800 blocks
+ '[' 0 -ne 0 ']'
Install system fs image: out/target/product/generic_x86/system.img
out/target/product/generic_x86/system.img+ maxsize=588791808 blocksize=2112   
    total=576716800 reserve=5947392

整个构建时间取决于您的硬件配置。即使在高端的 Intel Core i7 处理器上,也可能需要大约 40 分钟。选项-j4将启动使用四个处理器核心的并行构建。您可以根据您的计算机硬件选择数字。

测试 AOSP 镜像

构建完成后,我们可以在输出文件夹中找到所有镜像,如下面的截图所示:

generic_x86 的构建输出

AOSP 构建输出存储在$AOSP/out文件夹下。此文件夹包括目标和主机的构建结果。不同设备的构建结果分别存储在$AOSP/out/target/product/{设备名称}下。在我们的例子中,是$AOSP/out/target/product/generic_x86

system.imguserdata.imgramdisk.img这些镜像文件对于运行模拟器是必要的,但如您所见,这里没有内核镜像。我们将在本书的后面部分讨论内核构建。目前,我们将使用 Android SDK 中的内核镜像来测试我们的 AOSP 构建。

要使用我们的 AOSP 镜像进行测试,我们可以创建以下脚本:

#!/bin/sh 

emulator @a25x86 -verbose -show-kernel -system $OUT/system.img -ramdisk $OUT/ramdisk.img -initdata $OUT/userdata.img 

我们可以将这个脚本 test_aosp.sh 放在 $HOME/bin 文件夹中。通常,我们可以将 $HOME/bin 添加到可执行搜索 path 变量中,这样我们就可以在命令行中如下运行这个脚本 test_aosp.sh

$ test_aosp.sh  

如果你使用 Android 6 或更早的版本测试你的 AOSP 构建,你需要使用经典引擎而不是 ranchu。ranchu 构建在 Android 6 AOSP 构建中有一个问题,但在 Android 7 构建中这个问题已经被修复。为了在 6.0.1 AOSP 构建中支持 ranchu,我们必须更改清单以包含最新的模拟器设备。Android SDK 发布版没有这个问题。谷歌内部修复了这个问题,但直到 Android 7 才发布修复。

在模拟器启动后,我们可以检查版本信息,就像之前做的那样。在下面的屏幕截图中,我们可以看到 AOSP 图像中的版本信息:

图片

AOSP 图像的 Android 版本

如我们所见,使用了内核版本 3.10.0;这是因为我们使用了 ranchu 模拟器。让我们将信息与之前测试的 SDK 图像进行比较。从下面的表格中,我们可以看到模型号是 IA 模拟器上的 AOSP 而不是 sdk。SDK 的 Android 版本是 7.1,而 AOSP 的版本是 7.1.1。AOSP 图像的构建号是构建目标 aosp_x86-eng,这是我们之前选择的,这也包括了构建的日期和时间。

SDK 和 AOSP 版本

SDK (goldfish) SDK (ranchu) AOSP
模型 sdk sdk IA 模拟器上的 AOSP
Android 版本 7.1 7.1 7.1.1
内核版本 3.4.67 3.10.0 3.10.0
构建号 sdk_google_phone_x86-userdebug 7.1 NPF26K 3479480 test-keys sdk_google_phone_x86-userdebug 7.1 NPF26K 3479480 test-keys aosp_x86-eng 7.1.1 NMF26O eng.sgye 20170126.183237 test-keys

创建自己的仓库镜像

下载 AOSP 源代码通常需要非常长的时间。在你下载了 AOSP 源代码之后,实际上你已经从远程仓库下载了 AOSP 源代码的特定版本。在你的开发工作中,你可能需要测试不同的配置或版本。切换到不同的版本或创建 AOSP 源代码的新副本是一个非常耗时的工作。

在这本书中,我们将使用 AOSP 源代码作为我们开发的基线。为了重用一些不包括在 AOSP 中的现有开源项目,我们不得不不时修改 repo 清单。这涉及到更改 repo 配置。为了更有效地工作,我们可以使用本地镜像。创建本地镜像可以节省大量时间,而不是从远程仓库下载所有配置更改的源代码。从一个远程仓库更改配置可能需要数小时,但使用本地仓库只需要几分钟。

当我们处理开源项目时,托管项目的服务器可能会不时更改。拥有自己的镜像总是很好的,这样我们就不会过于依赖远程仓库。即使远程服务器在一段时间内不可用,我们仍然可以继续工作。这正是我在尝试将 Android-x86 项目集成到本书的后期部分时遇到的问题。

我将在本节中解释如何创建 AOSP、Android-x86 和 GitHub 的混合本地镜像。

Repo 和 manifest

要创建和管理仓库镜像,我们需要更深入地了解 repo 命令和 repo 管理的目录结构。repo 命令处理 XML 文件 manifest,并将所有内容存储在名为 .repo 的文件夹中。

在我们像上一节那样运行 repo init 命令之后,当前文件夹下会创建一个 .repo 文件夹。如果我们查看 .repo 文件夹,我们可以看到以下内容:

$ ls -F .repo
manifests/  manifests.git/  manifest.xml@  repo/  

创建了三个文件夹和一个符号链接。以下是每个的解释:

  • manifests:这是 manifest 本身 Git 仓库的工作副本。

  • manifests.git:这是 manifest 的 Git 仓库。manifest 本身使用 Git 进行版本控制。

  • manifest.xml:这是到文件 .repo/manifests/default.xml 的符号链接。这是 repo 使用的配置文件。我们稍后会详细了解。

  • repo:repo 工具本身是用 Python 语言编写的。Python 脚本存储在这个文件夹中。

在我们运行 repo init 命令初始化 repo 数据结构之后,我们可以运行 repo sync 命令来检索工作副本。如果我们再次查看 .repo 文件夹,在 repo sync 命令之后,我们可以看到创建了两个与项目相关的文件夹:

$ ls -F .repo
manifests/      manifest.xml@  project-objects/  repo/
manifests.git/  project.list   projects/  

以下是对新创建的文件和文件夹的解释:

  • project.list:这是所有下载项目的列表。

  • project-objects:这是远程仓库的副本。

  • projects:这是与工作副本匹配的仓库层次结构。在仓库复制到本地后,路径可能会重新排列。这个文件夹中的内容是 project-objects 中项的符号链接。

.repo 文件夹中最重要的文件是 .repo/manifests/default.xml 或其符号链接 manifest.xml。该文件的详细规范可以在 .repo 文件夹下的 .repo/repo/docs/manifest-format.txt 文档中找到。我们不会深入细节,但让我们看看最常用的元素。

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="aosp" 
           fetch=".." /> 
  <default revision="refs/tags/android-7.1.1_r4" 
           remote="aosp" 
           sync-j="4" /> 

  <project path="build" name="platform/build" groups="pdk" > 
    <copyfile src="img/root.mk" dest="Makefile" /> 
  </project> 
  <project path="abi/cpp" name="platform/abi/cpp" groups="pdk" /> 
  <project path="art" name="platform/art" groups="pdk" /> 
  <project path="bionic" name="platform/bionic" groups="pdk" /> 
... 
</manifest> 

在前面的代码片段中,我们可以看到在 manifest 中有三个 XML 元素:

  • remoteremote 元素提供了远程仓库的详细信息。我们可以给它起一个名字,例如 aosp。远程仓库的 URL 可以在 fetch 字段中指定。它可以是相对路径或完整路径。

  • default: 清单中可以指定多个remote元素。default元素定义了哪个remote是默认的。

  • project: 每个project元素定义了一个 Git 仓库。path字段提供下载后的本地路径,name字段提供 Git 仓库的远程路径,revision字段提供我们想要获取的分支,remote字段告诉我们我们使用哪个远程服务器来获取 Git 仓库。

清单中还可以使用其他 XML 元素。你可以通过查看前面的规范来了解它们是什么。

使用本地镜像进行 AOSP

如果你参考谷歌网站上的关于下载源的文章,你可以找到一个名为使用本地镜像的部分。它揭示,如果你需要两个不同的 AOSP 构建环境配置,两个客户端的下载大小将大于整个仓库镜像的大小。设置镜像非常简单,如下所示:

$ mkdir -p /usr/local/mirror/aosp
$ cd /usr/local/mirror/aosp
$ repo init -u https://android.googlesource.com/mirror/manifest --mirror
$ repo sync  

从前面的命令中,我们可以看到我们实际上使用不同的清单来创建镜像。如果我们查看镜像清单的内容,我们可以看到以下 XML 代码:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 
  <remote  name="aosp" 
           fetch=".." /> 
  <default revision="master" 
           remote="aosp" 
           sync-j="4" /> 
  <project name="accessories/manifest" /> 
  <project name="brillo/manifest" /> 
... 
</manifest> 

我们可以看到,对于所有项目,每个项目项中只有项目名称,没有其他信息。这是因为我们实际上将每个 Git 仓库复制到本地作为一个裸 Git 仓库。我们不会检出工作副本,所以我们不需要担心版本。

如果我们查看清单以检出工作副本,我们将看到以下内容:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="aosp" 
           fetch=".." /> 
  <default revision="refs/tags/android-6.0.1_r61" 
           remote="aosp" 
           sync-j="4" /> 

  <project path="build" name="platform/build" groups="pdk" > 
    <copyfile src="img/root.mk" dest="Makefile" /> 
  </project> 
  <project path="abi/cpp" name="platform/abi/cpp" groups="pdk" /> 
  <project path="art" name="platform/art" groups="pdk" /> 
... 
</manifest> 

它包含的项比创建镜像的项更多。name字段指定远程仓库的路径,path字段指定下载到本地的本地路径。我们还需要指定我们想要检索的revision

在我们有一个镜像之后,我们可以按照以下方式从该镜像检出 AOSP 源的一个副本:

$ mkdir -p $HOME/aosp/master
$ cd $HOME/aosp/master
$ repo init -u /usr/local/mirror/aosp/platform/manifest.git
$ repo sync  

如果需要,你可以从本地镜像检出多个副本。无论你是检出多个副本还是切换到不同的版本,与从远程仓库检出相比,你可以节省很多时间。

当你在系统级项目上工作时,你可能需要 AOSP 源之外的某些项目。例如,在这本书中,我们使用了来自 CyanogenMod、Android-x86 以及我在 GitHub 上的多个项目。在这种情况下,我们实际上可以创建自己的清单,将我们从本地镜像中需要的所有项目混合在一起。我们的本地镜像将成为公共镜像的超集。我们可以不时地在本地仓库中创建分支和标签,但我们只推送我们想要发布到公共仓库的基线。这正是谷歌开发团队在他们私有仓库中所做的。

创建自己的 GitHub 镜像

本书所使用的所有源代码都存储在 GitHub 上。我们还使用了 GitHub 上其他项目的源代码,因为许多开源项目托管在 GitHub 上,例如 CyanogenMod、OmniROM、Team Win Recovery 等等。我们可以为我们在本地存储中使用的所有项目创建镜像,这样我们就可以提交任何更改并创建自己的基线。如果你想要更改不属于你自己的任何项目,你可以使用 GitHub 的 Fork 功能创建自己的副本。

要为 GitHub 创建自己的清单,你可以在 GitHub 中创建一个仓库,命名为mirror,然后添加一个名为default.xml的 XML 文件,如下所示:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="cm" 
           fetch="git://github.com/CyanogenMod" 
           review="review.cyanogenmod.org" 
           revision="refs/heads/cm-13.0" /> 

  <remote  name="twrp" 
           fetch="git://github.com/TeamWin" 
           revision="master" /> 

  <remote  name="omnirom" 
           fetch="https://github.com/omnirom" /> 

  <remote  name="github" 
           fetch=".." /> 
  <default revision="master" 
           remote="github" 
           sync-j="4" /> 

  <!-- configuration of github repositories v1.0 --> 
  <project name="manifests" /> 
  <project name="manifest" /> 
  <project name="mirror" /> 
  <project name="local_manifests" /> 
... 
  <!-- CyanogenMod --> 
  <project name="android_bootable_recovery" remote="cm" /> 
  <project name="android_external_busybox" remote="cm" /> 
  <project name="android" remote="cm" /> 

  <!-- Team Win Recovery Project --> 
  <project name="Team-Win-Recovery-Project" remote="twrp" /> 
  <project name="android_device_emulator_twrp" remote="twrp" /> 
  <project name="android_device_emulator_twrpx86" remote="twrp" /> 
  <project name="android_device_emulator_twrpx8664" remote="twrp" /> 

  <!-- omnirom --> 
  <project path="external/lz4" name="android_external_lz4" remote="omnirom" 
  revision="android-6.0" groups="pdk-cw-fs,pdk-fs" /> 

  <!-- from original Android repositories --> 
... 
</manifest> 

从前面的default.xml中,我们可以看到,我们实际上使用单个 XML 文件从 CyanogenMod、TWRP、OmniROM 以及我们自己的 GitHub 仓库获取了多个项目。我们将它们全部放在一起,形成我们自己的 GitHub 本地镜像。

要创建本地镜像,我们可以使用以下命令:

$ mkdir -p /media/aosp-mirror/github
$ cd /media/aosp-mirror/github
$ repo init -u https://github.com/shugaoye/mirror.git --mirror
$ repo sync  

在我们创建了本地镜像之后,我们可以通过以下屏幕检查我们下载了什么:

本地镜像的内容

从前面的截图,我们可以看到,我们在default.xml中指定的所有 Git 仓库都被复制到了我们的本地存储中。我在这本书中使用的本地镜像清单文件可以在以下链接找到:github.com/shugaoye/mirror

在 GitHub 之外获取 Git 仓库

如前例所示,我们为 GitHub 镜像创建了我们的清单仓库。之后,我们使用它来初始化我们的镜像仓库。然后我们使用repo sync命令从 GitHub 获取所有 Git 仓库到我们的本地镜像。

那些我们没有写权限的仓库怎么办?在这本书中,我们使用了来自 Android-x86 的大量项目。然而,我们没有 Android-x86 仓库的写权限。Android-x86 项目也没有可用的镜像清单。

我们实际上可以从原始的 Android-x86 清单文件创建一个镜像清单文件。我们可以参考以下链接中的文档来获取 Android-x86 的源代码:

www.android-x86.org/getsourcecode

前面的文档提到,我们可以使用以下命令初始化并同步来自 Android-x86 仓库的 repo:

$ mkdir android-x86
$ cd android-x86
$ repo init -u git://git.osdn.net/gitroot/android-x86/manifest -b $branch
$ repo sync  

我们可以将前面的 Android-x86 清单仓库克隆到一个文件夹中,并对其进行分析:

$ git clone git://git.osdn.net/gitroot/android-x86/manifest -b marshmallow-x86
$ ls
cm.xml  default.xml  

在我们克隆它之后,我们可以找到前面的两个文件。default.xml用于初始化 Android-x86 仓库,而cm.xml用于初始化 CyanogenMod 构建的 Android-x86。

如果我们查看default.xml的内容,我们可以看到以下代码片段:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="aosp" 
           fetch="https://android.googlesource.com/" /> 
  <remote  name="x86" 
           fetch="." /> 
  <default revision="refs/tags/android-6.0.1_r61" 
           remote="aosp" 
           sync-c="true" 
           sync-j="4" /> 

  <!-- from x86 port repositories --> 
  <project path="build" name="platform/build" groups="pdk" remote="x86" 
  revision="marshmallow-x86" > 
    <copyfile src="img/root.mk" dest="Makefile" /> 
  </project> 
  <project path="kernel" name="kernel/common" remote="x86" 
  revision="kernel-4.4" /> 
  <project path="art" name="platform/art" groups="pdk" remote="x86" 
  revision="marshmallow-x86" /> 
... 
  <!-- from original Android repositories --> 
  <project path="abi/cpp" name="platform/abi/cpp" groups="pdk" /> 
  <project path="bootable/recovery" name="platform/bootable/recovery" 
  groups="pdk" /> 
... 
</manifest> 

我们可以看到,Android-x86 的清单文件包括两部分。第一部分是 Android-x86 自身的仓库,其余的是原始的 AOSP 仓库。

我们可以检索第一部分并为 Android-x86 创建一个镜像清单。我们应该把这个文件放在哪里?我们可以把它放在我们 GitHub 上同一个镜像清单仓库的一个分支中。

在我们的 GitHub 镜像仓库的工作副本中,我们可以创建一个名为 android-x86 的分支。我们可以用 Android-x86 清单中的第一部分替换我们 GitHub 镜像中的 default.xml,如下所示:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="github" 
           fetch=".." /> 

  <remote  name="x86" 
           fetch=" git://git.osdn.net/gitroot/android-x86/" /> 

  <default revision="android-x86" 
           remote="github" 
           sync-j="4" /> 

  <!-- from x86 port repositories --> 
  <project name="manifest" remote="x86" /> 
  <project name="platform/build" remote="x86" /> 
  <project name="kernel/common" remote="x86" /> 
  <project name="platform/art" remote="x86" /> 
... 
  <project name="platform/system/extras" remote="x86" /> 
  <project name="platform/system/vold" remote="x86" /> 

</manifest> 

如前所述的列表所示,我们移除了诸如 pathgroups 等不必要的字段。有了这个 Android-x86 镜像的清单,我们现在可以创建一个 Android-x86 的本地镜像,如下所示:

$ mkdir -p /media/aosp-mirror/android-x86
$ cd /media/aosp-mirror/android-x86
$ repo init -u https://github.com/shugaoye/mirror.git -b android_x86 --mirror
$ repo sync  

在我们下载所有 Git 仓库之后,我们可以看到以下内容:

android-x86 的本地镜像

为客户端下载创建自己的清单

现在有了所有本地镜像,我们可以创建自己的清单来检出我们的源代码。我们可以在我们的 GitHub 上的一个名为 manifests 的新仓库中放置它。在这个仓库中,我们可以创建一个 XML 文件,default.xml,如下所示:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="github" 
           fetch="." /> 

  <remote  name="aosp" 
           fetch="../android" /> 

  <remote  name="x86" 
           fetch="../android-x86" /> 

  <default revision="refs/tags/android-7.1.1_r4" 
           remote="aosp" 
           sync-c="true" 
           sync-j="4" /> 

  <!-- android-x86 --> 
  <project path="bootable/newinstaller"  
  name="platform/bootable/newinstaller" 
  remote="x86" revision="nougat-x86" /> 
... 
  <!-- GitHub --> 
  <project path="external/busybox" name="android_external_busybox" 
  remote="github" revision="cm-14.0" /> 
... 
  <!-- TWRP, use the below repositories for TWRP build --> 
  <project path="bootable/recovery" name="Team-Win-Recovery-Project" 
  remote="github" groups="pdk"  revision="android-7.0" /> 
... 
  <!-- AOSP --> 
  <project path="build" name="platform/build" groups="pdk" > 
    <copyfile src="img/root.mk" dest="Makefile" /> 
  </project> 
  <project path="abi/cpp" name="platform/abi/cpp" groups="pdk" /> 
... 
  <project path="tools/swt" name="platform/tools/swt"  
  groups="notdefault,tools" /> 
  <project path="tools/tradefederation" 
  name="platform/tools/tradefederation" 
  groups="notdefault,tradefed" /> 

</manifest> 

在前面的列表中,这是一个基于 AOSP 发布的 android-7.1.1_r4 清单修改后的清单。在这个文件中,我们将来自 AOSP、Android-x86、TWRP 和我们自己的 GitHub 项目的多个项目合并到了一起。通常,我们必须使用 local_manifests 来将所有非 AOSP 项目检索到我们的本地副本。这种方法通常需要非常长的时间,并且很难为我们自己的配置创建基线。

local_manifests 文件可以用来临时覆盖清单文件的配置。你可以参考《Android 嵌入式编程》的附录 B 来了解更多细节。

使用本地镜像和我们的自己的清单,我们可以找到一种干净的方式来完成这个任务。当你有一个 AOSP 的副本和一个 Android-x86 的副本时,你的存储中会有很多重复的项目,因为 Android-x86 的清单包括了来自 AOSP 的许多原始项目。使用前面的设置,你的本地镜像中没有重复的项目。

要检出工作副本,我们可以使用以下命令:

$ mkdir -p $HOME/aosp/android
$ cd $HOME/aosp/android
$ repo init -u /usr/local/mirror/github/manifests.git
$ repo sync  

如果我们要检查 Android-x86 的构建版本,现在它将是一个不同的配置,而不是一个完全不同的仓库:

$ cd $HOME/aosp/android
$ repo init -u /usr/local/mirror/github/manifests.git -b nougat-x86
$ repo sync  

由于我们有自己的本地镜像,我们可以在清单中使用 sync-c="true" 选项,就像前一个列表中看到的那样。使用这个选项,repo 命令将只在我们的工作副本中检出我们需要的版本,而不是创建包含所有修订版本的 Git 仓库。这可以为工作副本节省大量的空间。然而,如果没有本地镜像,这并不推荐,因为当你切换到不同版本时,这将会花费更长的时间。

你可以在我的 GitHub github.com/shugaoye/manifests 上找到用于检出工作副本的清单。

我们将使用这个方法来管理本书中所有不同的构建配置。

我在这里介绍了两种清单文件:

摘要

在本章中,我们为 SDK 和 AOSP 设置了环境。我们为 AOSP 构建了 Android 模拟器镜像。我们还测试并比较了 Android SDK 和 AOSP 中的 Android 镜像。在我们继续探索如何创建自己的 Android 系统之前,所有这些步骤都是必要的。我们还花了一些时间讨论如何设置自己的 repo 镜像。这个技巧可以在我们开始从多个开源项目创建项目时帮助我们。在下一章中,我们将开始探索 Android 的架构。我们将深入研究与 Android 系统的移植和定制相关的层级的细节。

第三章:发现内核、HAL 和虚拟硬件

一旦我们设置了开发环境并准备好了可用的源代码,我们就可以更深入地探索 Android 系统架构。我们首先将查看 AOSP 源代码树。之后,我们将研究本书中将使用的虚拟硬件平台。基于我们对虚拟硬件的理解,我们将查看与系统定制相关的层。在本章中,我们将涵盖以下主题:

  • 使用 goldfish 灯光服务对 Android HAL 进行深度分析

  • 查看 goldfish 的硬件规范

  • goldfish 内核中 QEMU 管道实现的概述

AOSP 内部有什么?

在我们深入细节之前,让我们再次查看 AOSP 源代码树的顶层:

以下表格简要描述了每个文件夹。我们将在本书中查看其中的一些:

目录 描述
packages 库存 Android 应用程序。
libcore 核心 Java 库。Nougat 之前使用 Apache Harmony。Nougat 使用 OpenJDK。Nougat 中使用了 Java 8 的一些功能。
frameworks/* Android 框架核心组件。
frameworks/base/services Android 系统服务。
art Android 运行时。
dalvik Dalvik 虚拟机。
libnativehelper 与 JNI 一起使用的辅助函数。
system/* 原生服务和库。
system/core 启动 Android 所需的最小 Linux 系统。
bionic C 库。
external 导入到 AOSP 的外部项目。它包括 HAL 层和系统服务。
hardware HAL 和硬件库。
device 设备特定的文件和组件。
bootable 恢复和引导加载程序。
abi 最小 C++运行时类型信息支持。
build 构建系统和 Makefiles。
sdk Android SDK。
cts 兼容性测试套件。
development 开发工具。
ndk Android NDK。
tools 各种 IDE 工具。
prebuilts 预构建的镜像和二进制文件。

对于特定的模块或组件,我们可能需要深入多个子文件夹的多个层级来了解其中包含的内容。这对于frameworkssystemexternal文件夹尤其如此。frameworks中的子文件夹包含 Android 框架层代码,但 Android 系统服务也位于frameworks/base/services中,我们将在本节稍后查看它们。systemexternal文件夹中的内容也是如此。

Android 模拟器 HAL

我们在第二章“设置开发环境”中构建了 Android 模拟器。为了对 Android 模拟器 HAL 有一个概览,我们可以查看以下$OUT/system/lib/hw文件夹:

我们可以看到有一系列共享库。这些都是 goldfish HAL 的共享库。上述共享库的源代码可以在device/generic/goldfish文件夹中找到。下表显示了共享库、设备节点和硬件模块之间的关系:

硬件 设备 库 (HAL)
audio /dev/eac audio.primary.goldfish.so
camera /dev/qemu_pipe camera.goldfish.jpeg.so camera.goldfish.so
fingerprint /dev/qemu_pipe fingerprint.goldfish.so
gps /dev/qemu_pipe gps.goldfish.so
lights /dev/qemu_pipe lights.goldfish.so
power /dev/qemu_pipe power.goldfish.so
sensors /dev/qemu_pipe sensors.goldfish.so
vibrator /dev/qemu_pipe vibrator.goldfish.so
graphics /dev/qemu_pipe gralloc.goldfish.so
serial /dev/ttyS[0 - 2] 简单设备不需要单独的共享库

从前表可以看出,除了串行端口和音频外,所有其他硬件模块都使用设备节点/dev/qemu_pipe与内核通信。QEMU 管道设备在模拟设备和 Android 模拟器之间提供了一个桥梁。由于 QEMU 管道是模拟器的重要设备,我们将在本章后面介绍它。

通常,HAL 实现是一个共享库,它将在运行时由系统服务加载。实际上,当决定实际实现时,它取决于硬件本身的复杂性。例如,对于像串行端口这样的简单硬件,没有单独的共享库。串行端口的系统服务实现直接访问设备节点。

对于更复杂的硬件设备,例如图形设备,还有一个在后台运行的专用守护进程 SurfaceFlinger,以及与之相关的多个共享库。

在本章中,我们将分析 goldfish 设备的 HAL,并以此为例了解框架、系统服务器和 HAL 实现之间的关系。然后,我们将介绍 goldfish 设备的硬件接口。最后,我们将分析 goldfish 内核中的 QEMU 管道实现。

调用序列

我们将以灯光硬件接口为例,解释 HAL、系统服务和硬件管理器是如何协同工作的。

灯光 HAL、系统服务和硬件管理器

如前图所示,当应用程序想要访问硬件资源时,它首先需要获取硬件管理器的一个实例。对于 goldfish 灯光,应用程序中的代码可能如下所示:

LightsManager lights =  
LocalServices.getService(LightsManager.class); 
mBacklight = lights.getLight(LightsManager.LIGHT_ID_BACKLIGHT); 
mBacklight.setBrightness(brightness); 

硬件管理器与系统服务通信以获取硬件访问权限。通常,硬件管理器是用 Java 实现的。由于硬件管理器和系统服务在不同的进程空间中运行,它通过 binder 接口调用系统服务。系统服务的上层也是用 Java 实现的。在系统服务接收到请求后,它将使用 JNI 调用 HAL 库,因为 HAL 通常是用 C 或 C++实现的。

图片

灯光服务的调用序列

前面的图示显示了当应用程序想要更改设备上的灯光时的调用序列。在本节中,我们将使用自下而上的方法来查看从 HAL 到应用程序层的调用序列。

金鱼灯 HAL

goldfish 灯光 HAL 实现可以在$AOSP/device/generic/goldfish/lights文件夹中找到。要实现 HAL 层,硬件供应商通常需要实现以下三个数据结构:

struct hw_module_t; 
struct hw_module_methods_t; 
struct hw_device_t; 

所有的前三个数据结构都在 goldfish 的lights_qemu.c文件中实现。在 HAL 实现中,我们首先需要定义名为HAL_MODULE_INFO_SYMstruct hw_module_t,如下所示。这将在系统中注册硬件模块 ID LIGHTS_HARDWARE_MODULE_ID。之后,灯光系统服务可以使用hw_get_module函数获取该模块:

/* 
 * The emulator lights Module 
 */ 
struct hw_module_t HAL_MODULE_INFO_SYM = { 
    .tag = HARDWARE_MODULE_TAG, 
    .version_major = 1, 
    .version_minor = 0, 
    .id = LIGHTS_HARDWARE_MODULE_ID, 
    .name = "Goldfish lights Module", 
    .author = "The Android Open Source Project", 
    .methods = &lights_module_methods, 
}; 

你可能会注意到,在前面的数据结构中method字段有一个指向lights_module_methods的指针。它如下定义:

static struct hw_module_methods_t lights_module_methods = { 
    .open =  open_lights, 
}; 

这定义了第二个 HAL 数据结构hw_module_methods_t。在这个数据结构内部,它定义了一个open_lights方法,这是 HAL 初始化硬件的函数。让我们看一下这个函数如下:

/** Open a new instance of a lights device using name */ 
static int 
open_lights( const struct hw_module_t* module, char const *name, 
struct hw_device_t **device ) 
{ 
    void* set_light; 

    if (0 == strcmp( LIGHT_ID_BACKLIGHT, name )) { 
      set_light = set_light_backlight; 
    } else if (0 == strcmp( LIGHT_ID_KEYBOARD, name )) { 
      set_light = set_light_keyboard; 
    } else if (0 == strcmp( LIGHT_ID_BUTTONS, name )) { 
      set_light = set_light_buttons; 
    } else if (0 == strcmp( LIGHT_ID_BATTERY, name )) { 
      set_light = set_light_battery; 
    } else if (0 == strcmp( LIGHT_ID_NOTIFICATIONS, name )) { 
      set_light = set_light_notifications; 
    } else if (0 == strcmp( LIGHT_ID_ATTENTION, name )) { 
       set_light = set_light_attention; 
    } else { 
        D( "%s: %s light isn't supported yet.", __FUNCTION__, name ); 
        return -EINVAL; 
    } 

struct light_device_t *dev = 
    malloc( sizeof(struct light_device_t) ); 
    if (dev == NULL) { 
        return -EINVAL; 
    } 
    memset( dev, 0, sizeof(*dev) ); 

    dev->common.tag = HARDWARE_DEVICE_TAG; 
    dev->common.version = 0; 
    dev->common.module = (struct hw_module_t*)module; 
    dev->common.close = (int (*)(struct hw_device_t*))close_lights; 
    dev->set_light = set_light; 

    *device = (struct hw_device_t*)dev; 
    return 0; 
} 

open_lights内部,它为继承自第三个 HAL 数据结构hw_device_tlight_device_t数据结构分配内存。当初始化light_device_t数据结构时,它注册了两个函数,close_lightsset_light,这样系统服务就可以调用这些函数来改变灯光或关闭设备。set_light函数指针根据灯光类型设置为特定的函数。

在每个set_light_xxx函数内部,它通过 QEMU 管道设备/dev/qemu_pipe与内核空间通信。例如,我们可以看一下set_light_backlight

static int 
set_light_backlight( struct light_device_t* dev, struct light_state_t const* state ) 
{ 
    /* Get Lights service. */ 
    intfd = qemud_channel_open( LIGHTS_SERVICE_NAME ); 

    if (fd < 0) { 
      ... 

    /* send backlight command to perform the backlight setting. */ 
    if (qemud_channel_send( fd, buffer, -1 ) < 0) { 
        E( "%s: could not query lcd_backlight: %s",
        __FUNCTION__, strerror(errno) ); 
        close( fd ); 
        return -1; 
    } 

    close( fd ); 
    return 0; 
} 

set_light_backlight函数内部,它调用qemud_channel_openqemud_channel_send来完成实际工作。这两个函数最终都使用 QEMU 管道设备/dev/qemu_pipe

系统服务和硬件管理器

要分析应用程序如何访问灯光硬件,请参考灯光服务调用序列的图示。在一个应用程序中,调用getService(LightsManager.class)函数以获取LightsManager实例如下:

LightsManager lights =  
LocalServices.getService(LightsManager.class); 
mBacklight = lights.getLight(LightsManager.LIGHT_ID_BACKLIGHT); 

通常,硬件管理器和系统服务在大多数硬件接口中是分别在不同的进程中实现的。然而,灯的硬件非常简单,所以系统服务和硬件管理器都在同一个进程中实现。

系统服务包括两部分:Java 和 JNI。JNI 的实现可以在 frameworks/base/services/core/jni 目录下找到,而 Java 的实现可以在 frameworks/base/services/core/java/com/android/server 目录下找到。LightsManagerLightsService 都是在 frameworks/base/services/core/java/com/android/server/lights 目录下实现的。

这个目录中有三个文件,如下所示。它们实现了 LightsManagerLightsService

$ ls
Light.java  LightsManager.java  LightsService.java  

让我们先看看 LightsManager。从以下代码片段中我们可以看到,LightsManager 只向调用者返回一个抽象类 Light

package com.android.server.lights; 

public abstract class LightsManager { 
    public static final intLIGHT_ID_BACKLIGHT = 0; 
    public static final intLIGHT_ID_KEYBOARD = 1; 
    public static final intLIGHT_ID_BUTTONS = 2; 
    public static final intLIGHT_ID_BATTERY = 3; 
    public static final intLIGHT_ID_NOTIFICATIONS = 4; 
    public static final intLIGHT_ID_ATTENTION = 5; 
    public static final intLIGHT_ID_BLUETOOTH = 6; 
    public static final intLIGHT_ID_WIFI = 7; 
    public static final intLIGHT_ID_COUNT = 8; 

    public abstract Light getLight(int id); 
} 

让我们跟随代码来查看抽象类 Light。在 Light 抽象类中,它定义了一系列必须实现的功能。这些功能在 LightsService 类中实现:

package com.android.server.lights; 

public abstract class Light { 
    public static final intLIGHT_FLASH_NONE = 0; 
    public static final intLIGHT_FLASH_TIMED = 1; 
    public static final intLIGHT_FLASH_HARDWARE = 2; 

    /** 
     * Light brightness is managed by a user setting. 
     */ 
    public static final intBRIGHTNESS_MODE_USER = 0; 

    /** 
     * Light brightness is managed by a light sensor. 
     */ 
    public static final intBRIGHTNESS_MODE_SENSOR = 1; 

    public abstract void setBrightness(int brightness); 
    public abstract void setBrightness(int brightness, 
    intbrightnessMode); 
    public abstract void setColor(int color); 
    public abstract void setFlashing(int color, int mode, intonMS, 
    intoffMS); 
    public abstract void pulse(); 
    public abstract void pulse(int color, intonMS); 
    public abstract void turnOff(); 
} 

在以下代码片段中的 LightsService.java 中,它实现了 Light 类定义的功能列表:

... 
private final class LightImpl extends Light { 

        private LightImpl(int id) { 
            mId = id; 
        } 

        @Override 
        public void setBrightness(int brightness) { 
            setBrightness(brightness, BRIGHTNESS_MODE_USER); 
        } 
... 

抽象类 Light 中的这组函数调用 setLightLocked 函数来完成实际工作。在这个函数中,它调用一个本地函数,setLight_native,来调用 LightsService 的本地部分:

private void setLightLocked(int color, int mode, int onMS, int offMS, int brightnessMode) { 
     if (color != mColor || mode != mMode || onMS != mOnMS
     || offMS != mOffMS) { 
         if (DEBUG) Slog.v(TAG, "setLight #" + mId + ": color=#" 
                 + Integer.toHexString(color)); 
         mColor = color; 
         mMode = mode; 
         mOnMS = onMS; 
         mOffMS = offMS; 
         Trace.traceBegin(Trace.TRACE_TAG_POWER, 
         "setLight(" + mId + ", 0x" +  
         Integer.toHexString(color) + ")"); 
         try { 
 setLight_native(mNativePointer, 
              mId, color, mode, onMS, offMS, 
              brightnessMode); 
         } finally { 
             Trace.traceEnd(Trace.TRACE_TAG_POWER); 
         } 
     } 
} 

除了 setLight_nativeLightService 还调用了另外两个本地函数,init_nativefinalize_native。我们可以在以下代码片段中看到这一点。这两个函数调用到 HAL 层函数,正如我们在上一节中讨论的那样:

public LightsService(Context context) { 
    super(context); 

    mNativePointer = init_native(); 

    for (inti = 0; i<LightsManager.LIGHT_ID_COUNT; i++) { 
      mLights[i] = new LightImpl(i); 
    } 
} 

... 

@Override 
protected void finalize() throws Throwable { 
 finalize_native(mNativePointer); 
    super.finalize(); 
} 

... 

private static native long init_native(); 
private static native void finalize_native(long ptr); 

static native void setLight_native(long ptr, int light, int color, int mode, int onMS, int offMS, int brightnessMode); 

我们已经看到了 LightsManager 的实现和 LightsService 的 Java 实现。现在,让我们探索 LightsService 实现的 JNI 部分。JNI 部分在 com_android_server_lights_LightsService.cpp 中实现,可以在 $AOSP/frameworks/base/services/core/jni 目录下找到。我们将看看这些在 LightsService 中使用的三个本地函数是如何连接到 HAL 层的:

static jlong init_native(JNIEnv* /* env */, jobject /* clazz */) 
{ 
    int err; 
    hw_module_t* module; 
    Devices* devices; 

    devices = (Devices*)malloc(sizeof(Devices)); 

    err = hw_get_module(LIGHTS_HARDWARE_MODULE_ID,  
        (hw_module_tconst**)&module); 
    if (err == 0) { 
        devices->lights[LIGHT_INDEX_BACKLIGHT] 
                = get_device(module, LIGHT_ID_BACKLIGHT); 
        devices->lights[LIGHT_INDEX_KEYBOARD] 
                = get_device(module, LIGHT_ID_KEYBOARD); 
        devices->lights[LIGHT_INDEX_BUTTONS] 
                = get_device(module, LIGHT_ID_BUTTONS); 
        devices->lights[LIGHT_INDEX_BATTERY] 
                = get_device(module, LIGHT_ID_BATTERY); 
        devices->lights[LIGHT_INDEX_NOTIFICATIONS] 
                = get_device(module, LIGHT_ID_NOTIFICATIONS); 
        devices->lights[LIGHT_INDEX_ATTENTION] 
                = get_device(module, LIGHT_ID_ATTENTION); 
        devices->lights[LIGHT_INDEX_BLUETOOTH] 
                = get_device(module, LIGHT_ID_BLUETOOTH); 
        devices->lights[LIGHT_INDEX_WIFI] 
                = get_device(module, LIGHT_ID_WIFI); 
    } else { 
        memset(devices, 0, sizeof(Devices)); 
    } 

    return (jlong)devices; 
} 

init_native 函数中,它调用 hw_get_module 函数使用 LIGHTS_HARDWARE_MODULE_ID 作为硬件 ID 来获取灯 HAL 模块。如果你回顾一下,它在 HAL 中定义。这个函数加载了 HAL 实现的共享库。在这种情况下,它加载了 lights.goldfish.so。在加载共享库之后,它调用 get_device 来初始化所有灯设备。我们可以在以下代码片段中看到 get_device 的实现:

static light_device_t* get_device(hw_module_t* module, char const* name) 
{ 
    int err; 
    hw_device_t* device; 
    err = module->methods->open(module, name, &device); 
    if (err == 0) { 
        return (light_device_t*)device; 
    } else { 
        return NULL; 
    } 
} 

get_device 函数中,它调用了 open 方法并获取了 HAL 数据结构 hw_device_t 的实例。我们之前在金鱼灯 HAL 中讨论了 open 方法。

现在,让我们看看另一个本地函数,setLight_native

static void setLight_native(JNIEnv* /* env */, jobject /* clazz */, jlong ptr, jint light, jint colorARGB, jint flashMode, jint onMS, jint offMS, jint brightnessMode) 
{ 
    Devices* devices = (Devices*)ptr; 
    light_state_t state; 

    if (light < 0 || light >= LIGHT_COUNT || devices->lights[light] == 
    NULL) { 
        return ; 
    } 

    memset(&state, 0, sizeof(light_state_t)); 
    state.color = colorARGB; 
    state.flashMode = flashMode; 
    state.flashOnMS = onMS; 
    state.flashOffMS = offMS; 
    state.brightnessMode = brightnessMode; 

    { 
        ALOGD_IF_SLOW(50, "Excessive delay setting light"); 
        devices->lights[light]->set_light(devices->lights[light], 
        &state); 
    } 
} 

setLight_native 函数中,它首先获取数据结构 Devices 的指针。之后,它调用 HAL 函数 set_light 来完成实际工作。

最后,让我们看看本地方法finalize_native的实现:

static void finalize_native(JNIEnv* /* env */, jobject /* clazz */, jlong ptr) 
{ 
    Devices* devices = (Devices*)ptr; 
    if (devices == NULL) { 
        return; 
    } 

    free(devices); 
} 

我们可以看到,finalize_native函数只是释放了所有使用的资源。

Android 模拟器内核和硬件

我们以金鱼灯为例,从应用程序到金鱼 HAL 执行调用序列分析。现在我们可以查看内核层和底层硬件。我们也可以从上到下再次概述,以了解整个系统是如何工作的。

金鱼架构

我们使用前面的图来详细解释金鱼内核和硬件。正如您所看到的,前面的图与我们在第一章“Android 系统编程简介”中看到的架构图相似。这个架构图是 Android 的一般架构图,但前面的图是针对金鱼的特定图。

从图中,我们可以看到我们感兴趣的 goldfish 内核和模拟器硬件的部分。从上到下,应用程序利用 Android 框架实现功能并访问硬件。框架通常位于与系统服务层不同的进程中,因此它们使用 Binder IPC 进行通信。系统服务通过 JNI 与HAL通信,因为HAL通常是用本地语言实现的。HAL是硬件控制的用户空间实现,它通过系统调用与内核空间中的设备驱动程序通信。在金鱼硬件的情况下,设备驱动程序通过内存 I/O 寄存器访问虚拟硬件,正如我们将在下一节中讨论的 Android 模拟器硬件。

Android 模拟器硬件

与真实硬件不同,大多数 Android 模拟器硬件接口都是使用 QEMU 进行模拟的,QEMU 是一个流行的开源模拟器引擎,被许多开源项目使用。Android 开发团队定制了 QEMU 并添加了一个名为 goldfish 的虚拟硬件平台。正如我们在第二章“设置开发环境”中提到的,在最新的 SDK 中目前有 Android 模拟器的两个版本可用。原始 Android 模拟器的代号是 goldfish,新的一个是 ranchu。然而,在 QEMU 中用于设备模拟的虚拟硬件代码库在这两个版本中是相同的。

关于 goldfish 硬件接口的详细信息可以在文档GOLDFISH-VIRTUAL-HARDWARE.TXT中找到。该文档可以在 AOSP 源代码的$AOSP/platform/external/qemu/docs/GOLDFISH-VIRTUAL-HARDWARE.TXT中找到。

对于不同的内核版本,硬件接口可能会有一些差异。在这本书中,我们将查看基于 Intel x86 的 ranchu 虚拟硬件,它使用 Android Linux 版本 3.10.0。让我们看看本章中我们将讨论的金鱼设备。

Goldfish 平台总线

在 goldfish 的架构图中,我们有一个内核和 goldfish 硬件的详细图。我们可以看到所有 goldfish 设备都是通过 goldfish 平台总线进行枚举的。平台总线是一种特殊设备,能够将系统上找到的其他平台设备枚举到内核中。这种灵活性允许我们在运行特定的模拟系统配置时自定义哪些虚拟设备可用。以下表格定义了 goldfish 平台总线寄存器。

Goldfish 平台总线 32 位 I/O 寄存器

偏移量 名称 摘要
0x00 BUS_OP R: 迭代到枚举中的下一个设备。W: 开始设备枚举。
0x04 GET_NAME W: 将设备名称复制到内核内存。
0x08 NAME_LEN R: 读取当前设备名称的长度。
0x0c ID R: 读取当前设备的 ID。
0x10 IO_BASE R: 读取当前设备的 I/O 基础地址。
0x14 IO_SIZE R: 读取当前设备的 I/O 基础大小。
0x18 IRQ_BASE R: 读取当前设备的基中断。
0x1c IRQ_COUNT R: 读取当前设备的中断计数。
0x20 NAME_ADDR_HIGH # 仅适用于 64 位虚拟机架构:W: 将 GET_NAME 使用的名称缓冲区内核地址的高 32 位写入。必须在 GET_NAME 写入之前写入。

QEMU 管道设备

在 goldfish 硬件中,最重要的模拟设备之一是 QEMU 管道设备。这是一个完全特定于 QEMU 的特殊设备,但允许虚拟进程以极高的性能直接与模拟器通信。这是通过避免任何内核内存复制来实现的,依赖于 QEMU 能够在运行时(在内核控制的适当条件下)访问虚拟内存。从 goldfish 架构图中我们可以看到,许多其他硬件接口,如 GPS、传感器、基带、摄像头等,都是通过 QEMU 管道进行模拟的。以下表格定义了 QEMU 管道设备寄存器。

QEMU 管道设备寄存器

偏移量 名称 摘要
0x00 COMMAND W: 写入以执行命令(见以下)。
0x04 STATUS R: 读取状态。
0x08 CHANNEL RW: 读取或设置当前通道 ID。
0x0c SIZE RW: 读取或设置当前缓冲区的大小。
0x10 ADDRESS RW: 读取或设置当前缓冲区的物理地址。
0x14 WAKES R: 读取唤醒标志。
0x18 PARAMS_ADDR_LOW RW: 读取/设置参数块地址的低字节。
0x1c PARAMS_ADDR_HIGH RW: 读取/设置参数块地址的高字节。
0x20 ACCESS_PARAMS W: 使用参数块执行访问。

有关设备操作的详细信息,请参阅 AOSP 文档 ANDROID-QEMU-PIPE.TXT

Goldfish 音频设备

goldfish 音频设备实现了具有以下属性的虚拟声卡:

  • 以固定的 44.1 kHz 频率进行立体声输出,使用有符号 16 位样本。这是强制性的。

  • 以固定 8 kHz 频率进行单声道输入,使用有符号 16 位样本。这是可选的。

以下表格定义了 goldfish 音频设备寄存器:

偏移量 名称 摘要
0x00 INT_STATUS
0x04 INT_ENABLE
0x08 SET_WRITE_BUFFER_1 W: 设置第一个内核输出缓冲区的地址。
0x0c SET_WRITE_BUFFER_2 W: 设置第二个内核输出缓冲区的地址。
0x10 WRITE_BUFFER_1 W: 将第一个内核缓冲区样本发送到输出。
0x14 WRITE_BUFFER_2 W: 将第二个内核缓冲区样本发送到输出。
0x18 READ_SUPPORTED R: 如果输入受支持则读取 1,否则为 0。
0x1c SET_READ_BUFFER
0x20 START_READ
0x24 READ_BUFFER_AVAILABLE
0x28 SET_WRITE_BUFFER_1_HIGH # 仅适用于 64 位虚拟机 CPU:W: 设置第一个内核输出缓冲区地址的高 32 位。
0x30 SET_WRITE_BUFFER_2_HIGH # 仅适用于 64 位虚拟机 CPU:W: 设置第二个内核输出缓冲区地址的高 32 位。
0x34 SET_READ_BUFFER_HIGH # 仅适用于 64 位虚拟机 CPU:W: 设置内核输入缓冲区地址的高 32 位。

Goldfish 串口

Android 模拟器有其自己的虚拟串口实现。它始终保留前两个虚拟串口:

  • 第一个用于接收内核消息。这是通过在内核命令行中添加 console=ttyS0 参数来实现的。

  • 第二个用于设置旧的 qemud 通道,用于较老的 Android 平台版本。这是通过在内核命令行中添加 android.qemud=ttyS1 来实现的。qemud 通道作为 Linux 守护进程实现,用作虚拟机和模拟器之间的通道。在最新的模拟器版本中,使用 QEMU 管道代替 qemud

以下表格定义了 goldfish 串口寄存器:

偏移量 名称 摘要
0x00 PUT_CHAR W: 将单个 8 位值写入串口。
0x04 BYTES_READY R: 读取可用的缓冲输入字节数。
0x08 CMD W: 发送命令(见下文)。
0x10 DATA_PTR W: 写入内核缓冲区地址。
0x14 DATA_LEN W: 写入内核缓冲区大小。
0x18 DATA_PTR_HIGH # 仅适用于 64 位虚拟机 CPU:W: 写入内核缓冲区地址的高 32 位。

CMD I/O 寄存器用于向设备发送各种命令,以下值用于标识:

0x00 CMD_INT_DISABLE   Disable device. 
0x01 CMD_INT_ENABLE    Enable device. 
0x02 CMD_WRITE_BUFFER  Write buffer from kernel to device. 
0x03 CMD_READ_BUFFER   Read buffer from device to kernel. 

每个设备实例使用一个中断请求(IRQ),用于指示有 incoming/buffered 数据要读取。

Goldfish 内核

Goldfish 内核可以从 AOSP 源代码库中下载。您可以使用以下命令下载和构建内核源代码:

$ git clone https://android.googlesource.com/kernel/goldfish.git 
$ cd goldfish 
$ git checkout -b android-goldfish-3.10 origin/android-goldfish-3.10 
$ make i386_ranchu_defconfig 
$ make 

下表列出了金鱼设备驱动程序。这是基于内核版本 3.10.0。目前,内核版本 3.10.0 适用于 ranchu,而 3.4.67 适用于 goldfish。下表列出了一些金鱼特定设备。在 ranchu 中,Virtio 设备用作块设备来模拟 EMMC。Virtio 设备是 QEMU 中的半虚拟化设备,其性能优于模拟硬件设备。

设备 路径
金鱼平台总线 drivers/platform/goldfish/pdev_bus.c
QEMU 管道 drivers/platform/goldfish/goldfish_pipe.c
帧缓冲 drivers/video/goldfishfb.c
金鱼音频 drivers/staging/goldfish/goldfish_audio.c
金鱼 NAND drivers/staging/goldfish/goldfish_nand.c
金鱼电池 drivers/power/goldfish_battery.c
金鱼事件 drivers/input/keyboard/goldfish_events.c
金鱼 MMC drivers/mmc/host/android-goldfish.c
金鱼串行 drivers/tty/goldfish.c

QEMU 管道

由于 QEMU 管道被用作模拟许多金鱼设备的通道,我们可以回顾其中一个主要功能,goldfish_pipe_read_write,以了解虚拟机和主机之间的数据传输:

static ssize_t goldfish_pipe_read_write(struct file *filp, char __user *buffer, size_t bufflen, int is_write) 
{ 
... 
    /* Now, try to transfer the bytes in the current page */ 
    spin_lock_irqsave(&dev->lock, irq_flags); 
    if (access_with_param(dev, is_write ? CMD_WRITE_BUFFER : 
    CMD_READ_BUFFER, xaddr, avail, pipe, &status)) { 
      writel((u32)(u64)pipe, dev->base + PIPE_REG_CHANNEL); 
#ifdef CONFIG_64BIT 
    writel((u32)((u64)pipe >> 32), dev->base + PIPE_REG_CHANNEL_HIGH); 
#endif 
    writel(avail, dev->base + PIPE_REG_SIZE); 
    writel(xaddr, dev->base + PIPE_REG_ADDRESS); 
#ifdef CONFIG_64BIT 
    writel((u32)((u64)xaddr>> 32), dev->base + PIPE_REG_ADDRESS_HIGH); 
#endif 
    writel(is_write ? CMD_WRITE_BUFFER : CMD_READ_BUFFER, 
      dev->base + PIPE_REG_COMMAND); 
    status = readl(dev->base + PIPE_REG_STATUS); 
} 
    spin_unlock_irqrestore(&dev->lock, irq_flags); 

if (status > 0 && !is_write) 
    set_page_dirty(page); 
put_page(page); 
... 

从前面的代码中我们可以看到,它首先调用了 access_with_param 函数。这是在虚拟机和模拟器之间使用共享内存进行数据传输的最快方式。使用这种方法,金鱼内核在启动时分配一块内存。虚拟机和模拟器将使用这块共享内存来交换它们之间的参数。如果 access_with_param 函数失败,它将通过以下序列通过 QEMU 管道设备传输数据:

write_channel(<channel>) 
write_address(<buffer-address>) 
REG_SIZE    = <buffer-size> 
REG_CMD     = CMD_WRITE_BUFFER/CMD_READ_BUFFER 
status = REG_STATUS 

现在让我们看一下下面的 access_with_param 函数:

/* A value that will not be set by qemu emulator */ 
#define INITIAL_BATCH_RESULT (0xdeadbeaf) 
static int access_with_param(struct goldfish_pipe_dev *dev, const int cmd, unsigned long address, unsigned long avail, struct goldfish_pipe *pipe, int *status) 
{ 
   struct access_params *aps = dev->aps; 

   if (aps == NULL) 
         return -1; 

   aps->result = INITIAL_BATCH_RESULT; 
   aps->channel = (unsigned long)pipe; 
   aps->size = avail; 
   aps->address = address; 
   aps->cmd = cmd; 
   writel(cmd, dev->base + PIPE_REG_ACCESS_PARAMS); 
   /* 
    * If the aps->result has not changed, that means 
    * that the batch command failed 
    */ 
   if (aps->result == INITIAL_BATCH_RESULT) 
         return -1; 
   *status = aps->result; 
   return 0; 
} 

aps 的地址是虚拟机和模拟器之间预先分配的共享内存。所有需要用于单个操作的数据结构都填充在这个数据结构 aps 中。命令将被写入寄存器 PIPE_REG_ACCESS_PARAMS。对 PIPE_REG_ACCESS_PARAMS 的写入将触发操作。QEMU 将读取 access_params 块的内容,使用其字段执行操作,然后将返回值写回 aps->result。共享内存 aps 和 QEMU 管道设备之间的区别类似于 DMA 和基于寄存器的设备 I/O。在大量内存访问中,共享内存或 DMA 要高效得多。

您可以自行探索金鱼设备驱动程序的其余部分。

摘要

在本章中,我们介绍了 AOSP 源代码的内容。之后,我们以 goldfish lights HAL 为例,分析了从应用程序到 HAL 的调用序列。最后,我们再次回顾了 Android 架构,使用 Android 系统为模拟器。我们还回顾了 goldfish 内核和硬件,以了解它们如何与其他软件栈协同工作。在下一章中,我们将开始着手构建我们自己的 x86emu 设备,并使用它来探索如何扩展模拟器以支持额外的功能。

第四章:自定义 Android 模拟器

在上一章中,我们花了一些时间探索 Android 系统架构的细节。凭借我们对内核、HAL 和系统服务的了解,我们可以开始自己定制 Android 系统。在本章中,我们将涵盖以下主题:

  • 为什么要自定义 Android 模拟器?

  • 创建新的 x86emu 设备

  • 构建和测试新的 x86emu 设备

为什么要自定义 Android 模拟器

你可能想知道为什么我们想要自定义 Android 模拟器。谷歌已经在 Android SDK 中提供了它,我们无需任何额外努力就可以使用它。然而,作为一名开发者,你可能会发现它可能不足以满足你的期望。例如,在最新的 Android Studio 或 SDK 版本中,推荐开发者使用英特尔 x86 模拟器,因为它比 ARM 版本快得多。使用英特尔 x86 模拟器的一个问题是,许多带有原生代码的 Android 应用程序无法正常运行,因为这些应用程序没有内置 x86 原生库。

为了解决这个问题,我们可以将英特尔(Intel)的 Houdini 库集成到模拟器中。有了 Houdini 库,我们可以在英特尔 x86 平台上执行 ARM 原生代码。对于 Android 模拟器,另一个常见的请求是它不包括谷歌移动服务GMS)。许多开发者假设设备上应该有 GMS 可用,因此开发应用程序。在接下来的几章中,我们将学习如何创建 x86emu 设备以自定义 Android 模拟器,这样我们就可以集成 Houdini 或启用 Wi-Fi 等附加硬件接口等组件。掌握了如何创建 x86emu 设备的知识,你可以创建自己的 Android 模拟器以满足你的需求。

我们总是尽量避免直接更改过多的 AOSP 代码。这是因为我们更改得越多,将其移植到最新版本的 Android 就越困难。谷歌不时地发布新的 Android 代码。有时,由于架构变化,新版本可能难以合并。

从本章到第七章,“在 Android 模拟器上启用 Wi-Fi”,我们将介绍一种通过最小更改 AOSP 源代码来定制现有设备的方法。从第八章,“在 VirtualBox 上创建自己的设备”到第十一章,“启用 VirtualBox 特定的硬件接口”,我们将讨论移植到新平台,我们必须直接更改 AOSP 代码。即使在那种情况下,我们仍然需要计划和考虑将合并努力到新的 Android 版本中。

理解构建层

AOSP 构建系统包括构建设备的抽象层。在理解这些层的理念之后,这将有助于我们理解设备各种 Makefile 之间的关系。在开始创建新设备时,总是好的参考以下 URL 的原始 Google 文档:source.android.com/source/add-device.html

在本节中,我们将把之前 Google 文档中的信息应用到我们将要工作的特定 Android 模拟器虚拟硬件上。这样,我们可以根据之前 Google 文档中的通用指导创建所有设备特定的 Makefile。在整个从通用到具体的过程中,我们可以将面向对象概念中的继承应用到 Makefile 系统中。

设备构建系统中有三个层,产品板/设备架构。这些层可以被视为衡量产品特性的不同维度。每个层与其上面的层之间有一个一对多的关系,这在面向对象术语中类似于继承或组合关系。例如,一种硬件架构可以有多个板,每个板可以有多个产品。我们将在本章后面创建新设备时看到这种方法是如何工作的。

下表是 AOSP 构建系统中使用的层列表。我是通过修改 Google 文档中的表格并添加针对我们将要工作的 x86emu 设备的特定注释来创建这个表格的。

描述
产品 产品层定义了发货产品的功能规范,例如要构建的模块、支持的地区以及各种地区的配置。换句话说,这是整体产品的名称。产品特定的变量在产品定义 Makefile中定义。一个产品可以继承自其他产品定义,这简化了维护。一个常见的方法是创建一个包含适用于所有产品的功能的基产品,然后基于该基产品创建产品变体。在本章中,我们继承自 AOSP 中 Android 模拟器的通用设备,以创建我们的 x86emu 设备。对于 x86emu 设备,我们还可以创建两个产品,它们仅通过其架构变体不同(我们可以为 x86 或 x86_64 有不同的构建)。
板/设备 板/设备层代表了设备上的物理塑料层(即,设备的设计)。例如,北美设备可能包括 QWERTY 键盘,而在法国销售的设备可能包括 AZERTY 键盘。这一层还代表了产品的裸机原理图。这包括板上的外围设备和它们的配置。在 x86emu 设备中,我们需要定义文件系统的尺寸、图形硬件和摄像头等。在第七章启用 Android 模拟器的 Wi-Fi 中,我们希望在模拟器中支持 Wi-Fi。我们需要在板配置文件中指定它。
架构 架构层描述了板上的处理器配置和应用二进制接口ABI)。

构建变体

当为特定产品构建时,通常对最终发布构建进行一些小的变化是有用的。通过使用不同的构建变体,可以帮助产品开发周期中的不同各方。到目前为止,AOSP 主要有三种构建变体。工程构建是默认的,适合开发工作。在这种类型的构建中,产品安全策略没有完全执行,并且调试机制被打开。工程师使用工程构建测试和修复问题很容易。

第二种口味是用户构建,用于最终发布。所有调试机制都被关闭,产品安全策略被完全执行。第三种口味是用户调试,位于工程构建和用户构建之间。这种类型的构建可用于现场测试,这也被最终用户使用。

AOSP 构建中的所有组件都称为模块。在模块定义中,模块可以使用LOCAL_MODULE_TAGS指定标签,可以是optional(默认)、debugeng的值之一或多个。通过标签,我们可以定义模块的用途。例如,所有调试工具都只包含在工程构建中。

如果一个模块没有指定标签(通过LOCAL_MODULE_TAGS),其标签默认为optional。一个optional模块只有在产品配置需要PRODUCT_PACKAGES时才会安装。我们通常在设备的 Makefile 中使用PRODUCT_PACKAGES变量来指定产品需要的包。这样,我们可以轻松定义只适用于特定构建的模块。

以下表格显示了在先前的 Google URL 中记录的 AOSP 定义的构建变体:

构建变体 描述

| eng | 这是默认口味:

  • 安装带有标签eng和/或debug的模块

  • 根据产品定义文件安装模块,以及带有标签的模块

  • ro.secure=0

  • ro.debuggable=1

  • ro.kernel.android.checkjni=1

  • 默认启用adb

|

| user | 这是打算作为最终发布的口味:

  • 安装标记为user的模块

  • 根据产品定义文件安装模块,除了标记的模块。

  • ro.secure=1

  • ro.debuggable=0

  • 默认禁用adb

|

| userdebug | 与user相同,除了:

  • 也安装了标记为debug的模块

  • ro.debuggable=1

  • 默认启用adb

|

创建一个新的 x86emu 设备

为了自定义 Android 模拟器,我们需要基于 Android 模拟器创建一个新设备,并在该新设备上进行定制。我们将从这个原始 AOSP 源代码开始工作。

从 AOSP 检出

如我之前所述,我会尽量避免对 AOSP 源代码进行不必要的修改。在本章中,为了设置构建环境,你可以检出 AOSP 源代码的android-7.1.1_r4版本,并将内核和 x86emu 源代码克隆到 AOSP 源代码树中,如下所示:

$ mkdir android-x86emu
$ cd android-x86emu
$ repo init -u https://android.googlesource.com/platform/manifest -b android-7.1.1_r4
$ repo sync
$ git clone https://github.com/shugaoye/goldfish.git -b android-7.1.1_r4_x86emu_ch04_r1 kernel
$ cd device/generic
$ git clone https://github.com/shugaoye/x86emu.git -b android-7.1.1_r4_x86emu_ch04_r1  

现在我们已经逐步检索了源代码。项目 x86emu 是我们本章创建的新设备,可以用于在接下来的几章中定制 Android 模拟器。goldfish 项目是我从 AOSP goldfish 内核分叉的内核:android.googlesource.com/kernel/goldfish/

android-7.1.1_r4_x86emu_ch04_r1标签是本章源代码发布的基线。本书中创建或更改的所有源代码都使用命名约定{Android 版本}_{项目}_{章节号}_{发布号}作为基线。以下是此命名约定的说明:

  • Android version是原始 AOSP 版本号

  • project可以是 x86emu 或 x86vbox

  • chapter number是我们为源代码创建基线所用的章节号

  • 使用release number来表示发布次数

这些可以在本章的简单配置中工作。当我们使用来自多个来源的源代码时(如本书后面的章节中所述),这种方法就不够好了。我们将使用我们自己的清单文件来管理本书中的源代码。

从本地镜像检出

要使用我们自己的清单文件,我们可以使用本地镜像或远程仓库。如果我们使用本地镜像,我们必须稍微修改android-7.1.1_r4manifest.xml以创建我们自己的。我们将.repo/manifest.xml复制到我们的manifests/default.xml并做出以下更改:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

 <remote  name="github" fetch="." /> 
  <remote  name="aosp" 
         fetch="../android" /> 
  <default revision="refs/tags/android-7.1.1_r4" 
           remote="aosp" 
           sync-j="4" /> 

 <!-- github/shugaoye --> <project path="kernel" name="goldfish"   
  remote="github" revision="refs/tags/
  android-7.1.1_r4_x86emu_ch04_r1" /> <project path="device/generic/x86emu" name="x86emu" remote="github"   
  revision="refs/tags/android-7.1.1_r4_x86emu_ch04_r1" /> <!-- AOSP --> 
  <project path="build" name="platform/build" groups="pdk" > 
    <copyfile src="img/root.mk" dest="Makefile" /> 
  </project> 
  <project path="abi/cpp" name="platform/abi/cpp" groups="pdk" /> 
... 
</manifest> 

此清单文件假设我们的本地镜像具有以下目录结构:

$ ls -F
android/  android-x86/  github/  

AOSP 镜像创建在android文件夹下。GitHub 镜像创建在github文件夹下。稍后我们还需要使用 android-x86 源代码。我们可以将其放在android-x86文件夹下。我们的清单文件存储在github/manifests中,前面的清单文件是github/manifests/default.xml。在此文件中,我们添加了额外的行以从 GitHub 检索 Android 内核和 x86emu 设备。

使用此清单,我们可以使用以下命令获取源代码:

$ mkdir android-x86emu
$ cd android-x86emu
$ repo init -u {your mirror URL}/github/manifests.git -b **android-7.1.1_r4_****ch04**
$ repo sync  

我们也可以直接使用我们自己的清单文件从远程仓库检索所有源代码。为此,我们需要稍微更改清单文件如下:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="github" 
           fetch="." /> 

 <remote  name="aosp" 
 fetch="https://android.googlesource.com/" /> 
  <default revision="refs/tags/android-7.1.1_r4" 
           remote="aosp" 
           sync-c="true" 
           sync-j="1" /> 

  <!-- github/shugaoye --> 
  <project path="kernel" name="goldfish" remote="github" 
   revision="refs/tags/android-7.1.1_r4_x86emu_ch04_r1" /> 
  <project path="device/generic/x86emu" name="x86emu" remote="github" 
   revision="refs/tags/android-7.1.1_r4_x86emu_ch04_r1" /> 

  <!-- aosp --> 
  <project path="build" name="platform/build" groups="pdk,tradefed" > 
    <copyfile src="img/root.mk" dest="Makefile" /> 
  </project> 
... 

如你所见,我们更改了远程 aosp 的 URL,在本版本清单文件中使用绝对路径。要使用此修订版检出源代码,我们可以运行以下命令:

$ mkdir android-x86emu
$ cd android-x86emu
$ repo init -u https://github.com/shugaoye/manifests -b **android-7.1.1_r4_ch04_****aosp**
$ repo sync  

由于本书涉及多个仓库,我强烈建议你使用本地镜像。这可以使构建和调试过程更加高效。

也可以使用 local_manifests 来设置你的工作空间。你可以参考书中附录 B,使用 Repo 本书中的 Repo,在《Android 嵌入式编程》一书中。一个示例文件可以在github.com/shugaoye/build/blob/master/local_manifest.xml找到。

在这本书中,我使用分支来管理清单文件的不同版本源代码。为了创建章节中源代码的基线,我使用以下命名约定:

{Android 版本}_{章节编号}_{远程(可选)}

  • Android 版本 是原始 AOSP 版本号

  • 章节编号 是我们为源代码创建基线的章节

  • remote 用于指示如何从远程仓库检出源代码

例如,从以下截图,我们可以看到分支 android-7.1.1_r4_ch04 用于从本地镜像检出第四章的源代码。分支 android-7.1.1_r4_ch04_aosp 用于从远程仓库检出第四章的源代码。由于我身处中国,我并不总是能访问 AOSP 源代码。我为第四章创建了修订版(android-7.1.1_r4_ch04_tunaandroid-7.1.1_r4_ch04_ustc),以便从中国的 AOSP 镜像和 GitHub 检出源代码。你可以根据需要更改清单文件。

图片

创建 x86emu 设备

在我们检出源代码后,我们可以查看如何在 $AOSP/device 文件夹中创建一个新的 x86emu 设备。device 文件夹中的层次结构是 vendor-name/device-name 格式。例如,三星的 Nexus S 可以在 samsung/crespo 文件夹中找到。Nexus S 的设备名称是 crespo。我们可以在一个公共文件夹 generic 下创建我们的设备,如下所示。我们设备的文件夹名称为 generic*/*x86emu

$ cd device/generic 
$ mkdir x86emu 

这是本章中我们创建的项目,你可以在github.com/shugaoye/x86emu.git找到源代码。

我们将在该文件夹中创建一个 Makefiles 列表来构建设备。参考上一节的构建层。以下是需要包含在设备骨架中的 Makefiles 列表:

  • AndroidProducts.mk:这是一个 Makefile,用于描述可以为该设备构建的各种产品

  • BoardConfig.mk:这是硬件板卡的板配置 Makefile。

  • device.mk:这是用于声明设备所需文件和模块的设备 Makefile。

  • vendorsetup.sh:这是一个可以用来将您的产品(一个 "lunch 组合")以及通过破折号分隔的构建变体添加到构建中的 shell 脚本。

  • {Product Makefile}.mk:这是产品定义 Makefile,它用于根据设备创建特定的产品。

现在,我们可以根据前面的列表逐个创建我们设备的 Makefile。

AndroidProducts.mk

我们将所有产品定义 Makefile 包含在这个文件中。AOSP 构建系统将开始使用此文件搜索所有产品定义。以下为 AndroidProducts.mk 的内容:

PRODUCT_MAKEFILES := \ 
    $(LOCAL_DIR)/x86emu_x86.mk \  
    $(LOCAL_DIR)/x86emu_x86_64.mk \  

如我们所见,我们为 x86 和 x86_64 构建定义了两个产品变体。

x86emu_x86.mkx86emu_x86_64.mk 都非常相似。它们为 32 位和 64 位定义了相同的产品定义变量集。

下表比较了 32 位和 64 位构建的产品定义 Makefile:

x86emu_x86.mk x86emu_x86_64.mk

|

$(call inherit-product, device/generic/x86emu/device.mk)
$(call inherit-product, $(SRC_TARGET_DIR)/product/full.mk)
# Overrides
PRODUCT_BRAND := x86emu_x86
PRODUCT_NAME := x86emu_x86
PRODUCT_DEVICE = x86emu
PRODUCT_MODEL := x86emu_x86_ch4
TARGET_ARCH := x86
TARGET_KERNEL_CONFIG := i386_ranchu_defconfig
$(call inherit-product, $(LOCAL_PATH)/x86emu_base.mk)

|

$(call inherit-product, device/generic/x86emu/device.mk)
$(call inherit-product, $(SRC_TARGET_DIR)/product/full_x86_64.mk)
# Overrides
PRODUCT_BRAND := x86emu_x86_64
PRODUCT_NAME := x86emu_x86_64
PRODUCT_DEVICE = x86emu
PRODUCT_MODEL := x86emu_x86_64_ch4
TARGET_SUPPORTS_32_BIT_APPS := true
TARGET_SUPPORTS_64_BIT_APPS := true
TARGET_ARCH := x86_64
TARGET_KERNEL_CONFIG := x86_64_ranchu_defconfig
$(call inherit-product, $(LOCAL_PATH)/x86emu_base.mk)

|

您可能会注意到,我们在开头首先继承了 32 位和 64 位的通用产品定义文件:

$(call inherit-product, $(SRC_TARGET_DIR)/product/full.mk) 

以及:

$(call inherit-product, $(SRC_TARGET_DIR)/product/full_x86_64.mk) 

AOSP 构建系统定义了许多通用产品定义。您可以在 $AOSP/build/target/product 找到它们:

$ ls build/target/product
AndroidProducts.mk      full_base.mk             sdk_base.mk
aosp_arm64.mk           full_base_telephony.mk   sdk_mips.mk
aosp_arm.mk             full_mips64.mk           sdk.mk
aosp_base.mk            full_mips.mk             sdk_phone_arm64.mk
aosp_base_telephony.mk  full.mk                  sdk_phone_armv7.mk
aosp_mips64.mk          full_x86_64.mk           sdk_phone_mips64.mk
aosp_mips.mk            full_x86.mk              sdk_phone_mips.mk
aosp_x86_64.mk          generic_armv5.mk         sdk_phone_x86_64.mk
aosp_x86.mk             generic_mips.mk          sdk_phone_x86.mk
base.mk                 generic.mk               sdk_x86_64.mk
core_64_bit.mk          generic_no_telephony.mk  sdk_x86.mk
core_base.mk            generic_x86.mk           security
core_minimal.mk         languages_full.mk        telephony.mk
core.mk                 languages_small.mk       vboot.mk
core_tiny.mk            locales_full.mk          verity.mk
embedded.mk             runtime_libart.mk
emulator.mk             sdk_arm64.mk  

之后,定义了一系列具有不同值的 product 定义变量 PRODUCT_BRANDPRODUCT_NAMEPRODUCT_DEVICEPRODUCT_MODELTARGET_ARCHTARGET_KERNEL_CONFIG 也分别针对 32 位和 64 位进行定义。请注意 PRODUCT_MODEL。由于我们将在每个章节中更改 Makefile,在这本书中我们使用 PRODUCT_MODEL 来表示每个章节的构建。在本章中,我们将 PRODUCT_MODEL 定义为 x86emu_x86_ch4 以表示本章的构建。在文件末尾,我们还包含了一个通用的 Makefile x86emu_base.mk,用于 32 位和 64 位产品。此文件包括内核构建的额外配置:

TARGET_KERNEL_SOURCE := kernel 

PRODUCT_OUT ?= out/target/product/x86emu 

include $(TARGET_KERNEL_SOURCE)/AndroidKernel.mk 

# define build targets for kernel 
.PHONY: $(TARGET_PREBUILT_KERNEL) 

LOCAL_KERNEL := $(TARGET_PREBUILT_KERNEL) 

PRODUCT_COPY_FILES += \ 
     $(LOCAL_KERNEL):kernel \ 

内核构建通常不包括在 AOSP 构建中。您必须根据 Google 的说明单独构建它们。在这本书中,我们将内核构建集成到我们自己的 Makefile 中。AndroidKernel.mk Makefile 是基于高通内核源码的 Makefile 创建的,该源码位于 android.googlesource.com/kernel/msm/

在前面的 Makefile 中使用了许多产品定义变量。让我们回顾一下我们在这里使用的产品定义变量。请参阅 Google 文档以获取完整列表:

  • PRODUCT_BRAND:这是软件定制的品牌。我们将其定义为我们的设备名称。

  • PRODUCT_NAME:这是我们赋予设备的名称。在这本书中,我们将其设置为 x86emu_x86。它也是我们可以在 lunch 组合中选择的名称前缀,例如 x86emu_x86-eng。后缀是构建变体。

  • PRODUCT_DEVICE:实际产品的名称。TARGET_DEVICE 从这个变量派生。这也是构建系统用来定位 BoardConfig.mk 的板名。对于我们的设备,它是 x86emu,也是我们设备在 $AOSP/device/generic/x86emu 目录的名称。

  • PRODUCT_MODEL:这是我们在设置中的“型号”中可以看到的名称。如我之前所述,我们使用这个变量来区分本书中每个章节的构建。

  • PRODUCT_OUT:这是构建结果的输出文件夹。它与环境变量 $OUT 相同。

  • PRODUCT_COPY_FILES:这是我们希望复制到目标文件系统中的特定文件列表。单词列表看起来像 source_path:destination_path。在构建过程中,位于源路径的文件应该被复制到目标路径。

BoardConfig.mk

BoardConfig.mk 定义了板级特定配置。我们在该文件中定义 CPU/ABI、目标架构、OpenGLES 配置等。我们还在该文件中定义了镜像文件的大小、格式等:

TARGET_NO_BOOTLOADER := true 
TARGET_NO_KERNEL := true 
TARGET_CPU_ABI := x86 
TARGET_ARCH := x86 
TARGET_ARCH_VARIANT := x86 
TARGET_PRELINK_MODULE := false 

# The IA emulator (qemu) uses the Goldfish devices 
HAVE_HTC_AUDIO_DRIVER := true 
BOARD_USES_GENERIC_AUDIO := true 

# no hardware camera 
USE_CAMERA_STUB := true 

# customize the malloced address to be 16-byte aligned 
BOARD_MALLOC_ALIGNMENT := 16 

# Enable dex-preoptimization to speed up the first boot sequence 
# of an SDK AVD. Note that this operation only works on Linux for now 
ifeq ($(HOST_OS),linux) 
WITH_DEXPREOPT := true 
endif 

# Build OpenGLES emulation host and guest libraries 
BUILD_EMULATOR_OPENGL := true 

# Build and enable the OpenGL ES View renderer. When running on the emulator, 
# the GLES renderer disables itself if host GL acceleration isn't available. 
USE_OPENGL_RENDERER := true 

TARGET_USERIMAGES_USE_EXT4 := true 
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 1342177280 
BOARD_USERDATAIMAGE_PARTITION_SIZE := 576716800 
BOARD_CACHEIMAGE_PARTITION_SIZE := 69206016 
BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4 
BOARD_FLASH_BLOCK_SIZE := 512 
TARGET_USERIMAGES_SPARSE_EXT_DISABLED := true 

BOARD_SEPOLICY_DIRS += \ 
        build/target/board/generic/sepolicy \ 
        build/target/board/generic_x86/sepolicy 

此文件是从预定义的 AOSP 板配置 $AOSP/build/target/board/generic_x86/BoardConfig.mk 复制过来的,并做了少量修改。

我们也可以直接使用系统定义的板级配置,并覆盖预定义变量,如下所示:

include $(SRC_TARGET_DIR)/board/generic_x86/BoardConfig.mk 

# 
# Overwrite predefined variables. 
# 

TARGET_USERIMAGES_USE_EXT4 := true 
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 1610612736 
BOARD_USERDATAIMAGE_PARTITION_SIZE := 576716800 
BOARD_CACHEIMAGE_PARTITION_SIZE := 69206016 
BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4 
BOARD_FLASH_BLOCK_SIZE := 512 
TARGET_USERIMAGES_SPARSE_EXT_DISABLED := true 

BOARD_KERNEL_CMDLINE += androidboot.selinux=permissive 

如果我们查看 $AOSP/build/target/board/generic_x86 文件夹,它包含了一些其他文件:

$ ls -F
BoardConfig.mk  device.mk  README.txt  sepolicy/  system.prop  

我们还需要将 system.prop 复制到我们的 device 文件夹中,因为这个文件定义了模拟器的 Radio Interface Layer (RIL)配置,如下所示:

rild.libpath=/system/lib/libreference-ril.so 
rild.libargs=-d /dev/ttyS0 

没有这个,您会发现数据连接在构建中无法正常工作。

device.mk

您可能会注意到 generic_x86 文件夹中有一个 device.mk 文件。是的,我们可以直接重用该文件。以下是我们 device.mk 文件的内容:

$(call inherit-product, $(SRC_TARGET_DIR)/board/generic_x86/device.mk) 

如我们所见,在我们的 device.mk 文件中,我们只是简单地从 generic_x86 设备继承了通用的 device.mk

我们可以查看 generic_x86 设备的 device.mk 文件如下:

PRODUCT_PROPERTY_OVERRIDES := \ 
    ro.ril.hsxpa=1 \ 
    ro.ril.gprsclass=10 \  
    ro.adb.qemud=1 

PRODUCT_COPY_FILES := \ 
device/generic/goldfish/data/etc/apns-conf.xml:system/etc/
apns-conf.xml \  
device/generic/goldfish/camera/media_profiles.xml:system/etc/
media_profiles.xml \  
frameworks/av/media/libstagefright/data/media_codecs_google_audio.xml:system/etc/media_codecs_google_audio.xml \  
frameworks/av/media/libstagefright/data/media_codecs_google_telephony.xml:system/etc/media_codecs_google_telephony.xml \ 
frameworks/av/media/libstagefright/data/media_codecs_google_video.xml:system/etc/media_codecs_google_video.xml \  
device/generic/goldfish/camera/media_codecs.xml:system/etc/media_codecs.xml 

PRODUCT_PACKAGES := \ 
    audio.primary.goldfish \ 
    vibrator.goldfish 

在之前的 device.mk 文件中,针对 generic_x86 设备,它覆盖了一些属性并将配置文件复制到 system 文件夹。它还包括 goldfish 设备的 HAL 层。

现在,我们可以使用以下命令将我们的设备构建添加到构建系统中:

$ add_lunch_combo <product_name>-<build_variant> 
$ lunch <product_name>-<build_variant> 

例如:

$ add_lunch_combo x86emu_x86-eng 
$ lunch x86emu_x86-eng 

要自动将其添加到构建系统中,我们可以添加一个脚本 vendorsetup.sh。在这个脚本中,我们可以为 x86emu_x86 创建所有构建变体:

for i in eng userdebug user; do 
        add_lunch_combo x86emu_x86-${i} 
done 

注意,本书中没有测试 x86emu 设备的 64 位构建。如果您想测试 64 位构建,您必须自行进行必要的更改。

在本节中,除了之前解释的产品级变量之外,还有针对目标设备和板级变量的变量。以下是在 BoardConfig.mkdevice.mk 或产品定义 Makefiles 中定义的目标设备变量列表:

  • TARGET_ARCH: 这是指设备的架构。它通常是诸如 armx86 等之类的某种东西。

  • TARGET_USERIMAGES_USE_EXT4: 为了在 ext4 格式下构建文件系统,这个变量需要设置为 true。在 Android 4.4 之前的旧版本 Android 中,文件系统可以构建为 yaffs2 等其他格式。

  • TARGET_KERNEL_SOURCE: 这是内核源代码的路径。在我们的案例中,内核源代码可以在 $AOSP/kernel 找到。

  • TARGET_KERNEL_CONFIG: 我们用来构建内核源代码的内核配置文件。

以下是我们在本章中使用的一组板级变量列表:

  • BOARD_SYSTEMIMAGE_PARTITION_SIZE: 系统镜像文件系统分区的大小(system.img

  • BOARD_USERDATAIMAGE_PARTITION_SIZE: 用户数据文件系统分区的大小(userdata.img

  • BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE: 缓存分区的文件系统格式

  • BOARD_FLASH_BLOCK_SIZE: 闪存设备的块大小

构建和测试 x86emu

一旦我们有了源代码,我们就可以在本节中开始构建和测试我们的 x86emu 设备。

构建 x86emu

在我们开始构建 x86emu 之前,让我们先快速了解一下 Android 构建系统。与其他基于 make 的构建系统相比,Android 构建系统不依赖于递归的 Makefiles。Android Makefiles 以 .mk 扩展名结尾;特定源目录的主要 Makefile 被命名为 Android.mk。构建系统从各个文件夹导入所有 Android.mk 以创建一个大的 Makefile 来启动构建,正如我们可以在以下代码片段中看到的那样:

$ make -j4 
============================================ 
PLATFORM_VERSION_CODENAME=REL 
PLATFORM_VERSION=7.1.1 
TARGET_PRODUCT=x86emu 
TARGET_BUILD_VARIANT=eng 
TARGET_BUILD_TYPE=release 
TARGET_BUILD_APPS= 
... 
HOST_BUILD_TYPE=release 
BUILD_ID=MOB30Z 
OUT_DIR=out 
============================================ 
including ./abi/cpp/Android.mk ... 
including ./art/Android.mk ... 
including ./bionic/Android.mk ... 
... 

在我们开始构建之前,我们必须首先设置构建环境。Android 构建系统提供了一个 build/envsetup.sh 脚本来设置构建环境。我们可以通过运行以下命令来设置构建环境:

$ source build/envsetup.sh  

之后,我们需要指定我们想要构建的目标。在 Android 构建系统的术语中,这被称为 lunch-combo。我们可以直接指定一个 lunch-combo:

$ lunch x86emu_x86-eng  

或者从菜单中选择它:

$ lunch

You're building on Linux

Lunch menu... pick a combo:
 1\. aosp_arm-eng
 2\. aosp_arm64-eng
 3\. aosp_mips-eng
 4\. aosp_mips64-eng
 5\. aosp_x86-eng
 6\. aosp_x86_64-eng
 7\. x86emu_x86-eng
 8\. x86emu_x86-userdebug
 9\. x86emu_x86-user

Which would you like? [aosp_arm-eng] 7

============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=7.1.1
TARGET_PRODUCT=x86emu_x86
TARGET_BUILD_VARIANT=eng
TARGET_BUILD_TYPE=release
TARGET_BUILD_APPS=
TARGET_ARCH=x86
TARGET_ARCH_VARIANT=x86
TARGET_CPU_VARIANT=
TARGET_2ND_ARCH=
TARGET_2ND_ARCH_VARIANT=
TARGET_2ND_CPU_VARIANT=
HOST_ARCH=x86_64
HOST_2ND_ARCH=x86
HOST_OS=linux
HOST_OS_EXTRA=Linux-4.2.0-27-generic-x86_64-with-Ubuntu-14.04-trusty
HOST_CROSS_OS=windows
HOST_CROSS_ARCH=x86
HOST_CROSS_2ND_ARCH=x86_64
HOST_BUILD_TYPE=release
BUILD_ID=NMF26O
OUT_DIR=out
============================================  

我们在第二章中学习了这一点,设置开发环境,当时我们构建了 Android 模拟器镜像。你可能注意到这里菜单项的差异在于,菜单包括了我们在本章中添加的设备配置。

我们在这里选择的 lunch-combo 是 x86emu_x86-eng。现在我们可以使用以下命令开始构建目标:

$ make -j4  

或者:

$ m -j4  

-j4 选项用于指定并发 make 会话的数量。它与你的系统上的 CPU 核心数相关,例如,在一个更强大的硬件平台上,你可能选择 -j8。在执行 source build/envsetup.sh 后,m 命令是可用的。它等同于 croot; make -j4

如果你想在构建中看到实际的命令,你可以在命令行上使用 showcommands 选项:

$ make -j4 showcommands  

您可以使用其他常用的构建目标。以下是一些您可以在构建中参考的目标列表:

  • make sdk: 构建 SDK 中的工具(如 adbfastboot 等)。

  • make snod: 从当前软件二进制文件构建系统镜像。

  • make all: 构建所有内容,无论是否包含在产品定义中。

  • make clean: 删除所有构建文件(为新构建做准备)。它与 rm -rf out/<configuration>/ 相同。

  • make modules: 显示可以构建的子模块列表(所有 LOCAL_MODULE 定义列表)。

  • make <local_module>: 构建特定模块(请注意,这与目录名不同。这是Android.mk文件中的LOCAL_MODULE定义)。

  • make clean-<local_module>: 清理特定模块。

  • make bootimage TARGET_PREBUILT_KERNEL=/path/to/bzImage: 使用自定义 bzImage 创建新的引导镜像。

  • make recoveryimage: 在 bootable/recovery/ 中构建恢复。

除了构建目标之外,还有一些辅助宏和函数在您源码 envsetup.sh 时被安装。您可以通过使用 hmm 命令来找出它们:

$ hmm
Invoke ". build/envsetup.sh" from your shell to add the following functions to your environment:
- lunch:   lunch <product_name>-<build_variant>
- tapas:   tapas [<App1> <App2> ...] [arm|x86|mips|armv5|arm64|x86_64|mips64] [eng|userdebug|user]
- croot:   Changes directory to the top of the tree.
- m:       Makes from the top of the tree.
- mm:      Builds all of the modules in the current directory, but not their dependencies.
- mmm:     Builds all of the modules in the supplied directories, but not their dependencies.
 To limit the modules being built use the syntax: mmm dir/:target1,target2.
- mma:     Builds all of the modules in the current directory, and their dependencies.
- mmma:    Builds all of the modules in the supplied directories, and their dependencies.
- cgrep:   Greps on all local C/C++ files.
- ggrep:   Greps on all local Gradle files.
- jgrep:   Greps on all local Java files.
- resgrep: Greps on all local res/*.xml files.
- mangrep: Greps on all local AndroidManifest.xml files.
- sepgrep: Greps on all local sepolicy files.
- sgrep:   Greps on all local source files.
- godir:   Go to the directory containing a file.

Environemnt options:
- SANITIZE_HOST: Set to 'true' to use ASAN for all host modules. Note that
 ASAN_OPTIONS=detect_leaks=0 will be set by default until 
 the build is leak-check clean.

Look at the source to view more functions. The complete list is:
addcompletions add_lunch_combo cgrep check_product check_variant choosecombo chooseproduct choosetype choosevariant core coredump_enable coredump_setup cproj croot findmakefile get_abs_build_var getbugreports get_build_var getdriver getlastscreenshot get_make_command getprebuilt getscreenshotpath getsdcardpath gettargetarch gettop ggrep godir hmm is isviewserverstarted jgrep key_back key_home key_menu lunch _lunch m make mangrep mgrep mm mma mmm mmma pez pid printconfig print_lunch_menu qpid rcgrep resgrep runhat runtest sepgrep set_java_home setpaths set_sequence_number set_stuff_for_environment settitle sgrep smoketest stacks startviewserver stopviewserver systemstack tapas tracedmdump treegrep  

在我们成功构建目标之后,我们可以在我们的案例中找到图像在 out/target/product/x86emu。我们还可以使用环境变量 $OUT 如下列出构建输出:

$ ls -F $OUT
Android-info.txt  dex_bootjars/             ramdisk.img           symbols/
boot.img          gen/                      ramdisk-recovery.img  system/
cache/            installed-files.txt       recovery/             system.img
cache.img         kernel                    recovery.id           userdata.img
clean_steps.mk    obj/                      recovery.img
data/             previous_build_config.mk  root/  

测试 x86emu

要测试 x86emu,我们可以使用在 第二章 “设置开发环境”中创建的 AVD a25x86。要使用我们自己的系统镜像,我们可以创建一个 shell 脚本 ~/bin/test-ch04.sh,如下所示:

#!/bin/sh 

emulator @a25x86 -verbose -show-kernel -shell -selinux disabled -system ${OUT}/system.img -ramdisk ${OUT}/ramdisk.img -initdata ${OUT}/userdata.img -kernel ${OUT}/kernel 

您可以从前面的 shell 脚本中看到,用于启动 AVD a25x86 的 x86emu 图像。您需要设置您的 Android SDK 路径,以便您可以从 Android SDK 使用模拟器:

$ test-ch04.sh  

在您启动模拟器后,您可以转到设置 | 关于手机来检查构建信息,如下面的截图所示:

x86emu 构建信息

我们可以从“关于手机”中看到模型是 x86emu_android-7.1.1_r4_ch04,这是我们指定在产品定义 Makefile x86emu_x86.mk 中的。内核版本是 3.10.0,构建号是构建目标 x86emu_x86-eng。

如果您想测试本章中的图像而不设置自己的构建,您可以从 SourceForge 下载图像,网址为 sourceforge.net/projects/Android-system-programming/files/android-7/ch04/

集成到 Eclipse

您可以使用集成开发环境IDE)进行开发工作。可以将 AOSP 构建环境和选定的项目集成到您喜欢的 IDE 中。这里,我将使用 Eclipse 作为示例来解释如何在 Eclipse 中集成我们的项目和 AOSP 构建环境。请注意,由于 AOSP 只能在 Linux 环境中构建,因此这只能适用于 Linux。

尽管 Android Studio 是 Android 应用程序开发的默认 IDE,但我更喜欢 Eclipse 用于 Android 系统编程。使用 Eclipse,我们可以构建本地和 Java 应用程序。我们还可以在 Eclipse 项目中集成 AOSP 构建。

要设置 Eclipse 环境,您可以使用带有 ADT 插件的最新 Eclipse 或从 Google 下载旧的 ADT 套件。

对于 Linux x86 或 x86_64:

要使用 Eclipse,我们需要为我们的 x86emu 设备构建创建一个 Makefile,如下所示:

all: 
   cd ../../..;make -j8 showcommands 2>&1 | tee x86emu-`date +%Y%m%d`.txt 

x86emu: 
   cd ../../..;make -j4 

snod: 
   cd ../../..;make snod 

initrd: 
   cd ../../..;make initrd USE_SQUASHFS=0 

ramdisk: 
   cd ../../..;make -j4 

clean-ramdisk: 
   rm ${OUT}/ramdisk.img 
   rm -rf ${OUT}/root 

clean-initrd: 
   rm ${OUT}/initrd.img 
   rm -rf ${OUT}/installer 

我们需要定义一些可以在 Eclipse 中使用的构建目标。让我们看看如何将 x86emu 设备构建导入到 Eclipse 项目中。我们将使用 ADT 套件中的 Eclipse 来解释这个过程。为了将 AOSP 构建与 Eclipse 集成,我们必须在 AOSP 构建环境中启动 Eclipse。让我们按照以下步骤启动 Eclipse:

$ source build/envsetup.sh
$ lunch x86emu_x86-eng
${SDK_ROOT}/eclipse/eclipse  

在我们安装了 ADT 套件后,我们可以在 SDK 安装路径下的上一个目录中找到 Eclipse。在启动 Eclipse 后,选择 C/C++ 视图,如下所示:

图片

选择 C/C++ 视图

我们可以通过选择“文件 | 导入... | 将现有代码作为 Makefile 项目导入”,将 x86emu 目录作为现有 Makefile 项目导入到 Eclipse 中,如下所示:

图片

将现有代码作为 Makefile 项目导入

点击“下一步”,导航到 $AOSP/device/generic/x86emu 文件夹以导入源代码,如下所示:

图片

导入现有代码

一旦我们导入项目,我们应该能够在项目资源管理器中看到 x86emu 文件夹下的所有文件都显示在右侧,正如我们在以下屏幕截图中所见。然后我们可以点击鼠标右键以查看项目的菜单列表,并选择“Make Targets | Create... | Create Make Target”。我们可以在目标名称字段中添加我们在 Makefile 中定义的构建目标。如果我们定义默认的构建目标 all,Eclipse 中的默认构建将触发我们的 Makefile 中的构建目标 all。这是我们为构建目标 all 定义的:

all: 
   cd ../../..;make -j8 showcommands 2>&1 | tee x86emu-`date +%Y%m%d`.txt 

我们在这里做的是在 AOSP 根目录启动 AOSP 构建。我们还使用命名约定 x86emu-{$DATE}.txt 为构建生成一个日志文件,构建完成后,您可以在 AOSP 根目录中找到这个日志文件。

图片

在 Eclipse 中创建 Make Target

在我们创建所有构建目标后,我们可以通过选择“项目 | 构建所有”或使用快捷键 Ctrl + B 来启动构建,从 Eclipse 中构建 AOSP。

摘要

在本章中,我们学习了如何基于为 Intel x86 架构构建的 Android 模拟器创建一个新设备。我们解释了 AOSP 构建系统中的不同构建层以及这些构建层如何与设备的 Makefiles 关联。之后,我们构建并测试了新的 x86emu 设备。最后,为了提高开发工作的效率,我们将 AOSP 构建集成到了 Eclipse 中。在下一章中,我们将扩展 Android 模拟器以支持使用 x86emu 设备进行 ARM 二进制翻译。

第五章:启用 ARM 翻译器和介绍原生桥接

我们在上一章中创建了一个新的 x86emu 设备。这是进一步定制和扩展的基础。正如我们所知,如果应用程序包含原生库,则无法在不同的处理器架构上运行。大多数 Android 应用程序是为 ARM 平台构建的。我们通常在 Intel x86 平台上运行这些带有 ARM 原生库的应用程序时遇到问题。然而,从 Android 5 及以上版本开始,Google 为此情况提供了一个名为 原生桥接 的解决方案。在本章中,我们将深入研究原生桥接和 Intel Houdini 的实现,以扩展 x86emu 以支持 ARM 原生应用程序。在本章中,我们将涵盖以下主题:

  • 介绍原生桥接

  • 将 Houdini 库集成到 x86emu 设备中

  • 使用 Houdini 集成构建和测试图像

介绍原生桥接

在 Android 架构中,原生桥接作为 Android Runtime(ART)的一部分实现。它用于支持在不同的处理器架构上运行原生库,以便具有原生库的应用程序可以在更广泛的设备上运行。名为 Houdini 的 Intel ARM 翻译器是原生桥接的一个用例。在 ART 中,原生桥接的初始化有两个阶段:

  1. 在第一阶段,原生桥接作为 ART 初始化过程的一部分加载到系统中。这对于所有应用程序都是常见的。

  2. 在第二阶段,当启动具有原生库的应用程序时,它将从 Zygote 中 fork。此时,原生桥接将被初始化并准备好供应用程序使用。这是一个针对单个应用程序的特定过程。例如,如果没有使用原生库,则不会为此应用程序初始化原生桥接。

Zygote Android 在其核心有一个名为 Zygote 的进程,它在 init 时启动。此进程是一个“预热”进程,这意味着它是一个已经初始化并且所有核心库都已链接的进程。当你启动一个应用程序时,Zygote 将被 fork 以创建新的进程。真正的加速是通过复制共享库来实现的。只有当新进程尝试修改它时,此内存才会被复制。这意味着所有核心库都可以存在于一个地方,因为它们是只读的。

当应用程序开始从不同的处理器架构加载原生库时,原生桥接将帮助解决此库的加载。例如,当我们加载 ARM 库在 Intel 的 x86 架构上时,原生桥接将使用 Houdini 在 Intel x86 环境中加载和执行此 ARM 库。

原生桥接在 Android 架构中

Native Bridge 作为 Android 系统库的一部分构建为一个libnativebridge.so共享库,如图所示。实现可以在$AOSP/system/core/libnativebridge中找到。在 Native Bridge 实现中,它在native_bridge.cc中定义了五个状态,如下所示:

enum class NativeBridgeState { 
  kNotSetup,               // Initial state. 
  kOpened,                 // After successful dlopen. 
  kPreInitialized,         // After successful pre-initialization. 
  kInitialized,            // After successful initialization. 
  kClosed                  // Closed or errors. 
}; 

当 Android 系统刚刚启动时,Native Bridge 处于kNotSetup状态。在 ART 的初始化过程中,它将被加载到系统中,阶段变为kOpened

这两个状态是 Native Bridge 初始化的第一阶段。当用户启动带有本地库的应用程序时,系统将从 Zygote 中 fork 一个新的进程。此时,系统将为 Native Bridge 做一些预初始化工作,我们将在本章后面看到这一点。此时状态变为kPreInitialized。从 Zygote fork 进程后,Native Bridge 作为进程创建的一部分被初始化,其状态变为kInitializedkClosed状态通常不使用,除非出现错误并且关闭 Native Bridge。这三个状态属于 Native Bridge 初始化的第二阶段。

在了解了 Android 系统架构中关于 Native Bridge 的概述之后,我们将不得不深入了解运行时使用的 Native Bridge 的每个阶段的细节。

将 Native Bridge 作为 ART 初始化的一部分设置起来

首先,让我们看看 Native Bridge 如何在系统中加载。Native Bridge 作为 ART 初始化的一部分被加载。如图所示,它包括从ARTNative Bridge实现的函数调用。在这个阶段结束时,Native Bridge的状态将被设置为kOpened

图片

加载 Native Bridge

当系统初始化 ART 时,会调用Runtime::Init函数。在Runtime::Init内部,会调用LoadNativeBridge函数来加载 Native Bridge 共享库。我们可以在下面的代码片段中看到这一点:

bool Runtime::Init(const RuntimeOptions& raw_options, bool ignore_unrecognized) { 
  ATRACE_BEGIN("Runtime::Init"); 
  CHECK_EQ(sysconf(_SC_PAGE_SIZE), kPageSize); 
  ... 
    std::string native_bridge_file_name = 
    runtime_options.ReleaseOrDefault(Opt::NativeBridge); 
    is_native_bridge_loaded_ = 
    LoadNativeBridge(native_bridge_file_name);  
   ... 
} 

这个LoadNativeBridge函数是 ART 的一部分,它在native_bridge_art_interface.cc文件中实现,如图所示。这个函数简单地调用在android命名空间中的另一个函数android::LoadNativeBridge,而它本身在art命名空间中。android命名空间中的函数是 Native Bridge 实现的一部分,如图所示,我们将在本章后面看到更多关于这一点。我们可以在下面的代码片段中看到LoadNativeBridge的实现:

static android::NativeBridgeRuntimeCallbacks native_bridge_art_callbacks_ { 
  GetMethodShorty, GetNativeMethodCount, GetNativeMethods 
}; 

bool LoadNativeBridge(std::string& native_bridge_library_filename) { 
  VLOG(startup) << "Runtime::Setup native bridge library: " 
      << (native_bridge_library_filename.empty() ? "(empty)" : 
      native_bridge_library_filename); 
  return android::LoadNativeBridge(native_bridge_library_filename.c_str(), 
                                   &native_bridge_art_callbacks_); 
} 

android命名空间中的android::LoadNativeBridge函数与art命名空间中的art:LoadNativeBridge函数相比,有一个额外的native_bridge_art_callbacks参数。这个参数的类型是struct NativeBridgeRuntimeCallbacks的指针,它在native_bridge.h中定义。在struct NativeBridgeRuntimeCallbacks中,它定义了以下三个回调方法:

// Runtime interfaces to native bridge. 
struct NativeBridgeRuntimeCallbacks { 
  // Get shorty of a Java method. The shorty is supposed to be   
  persistent in 
  // memory. 
  // 
  // Parameters: 
  //   env [IN] pointer to JNIenv. 
  //   mid [IN] Java methodID. 
  // Returns: 
  //   short descriptor for method. 
  const char* (*getMethodShorty)(JNIEnv* env, jmethodID mid); 

  // Get number of native methods for specified class. 
  // 
  // Parameters: 
  //   env [IN] pointer to JNIenv. 
  //   clazz [IN] Java class object. 
  // Returns: 
  //   number of native methods. 
  uint32_t (*getNativeMethodCount)(JNIEnv* env, jclass clazz); 

  // Get at most 'method_count' native methods for specified class 
  'clazz'. 
  // Results are outputed 
  // via 'methods' [OUT]. The signature pointer in JNINativeMethod is 
  reused 
  // as the method shorty. 
  // 
  // Parameters: 
  //   env [IN] pointer to JNIenv. 
  //   clazz [IN] Java class object. 
  //   methods [OUT] array of method with the name, shorty, and fnPtr. 
  //   method_count [IN] max number of elements in methods. 
  // Returns: 
  //   number of method it actually wrote to methods. 
  uint32_t (*getNativeMethods)(JNIEnv* env, jclass clazz, 
  JNINativeMethod* methods, uint32_t method_count); 
}; 

这些作为 ART 一部分的三个回调函数在native_bridge_art_interface.cc文件中实现。这些回调函数为原生方法调用 JNI 原生函数提供了一种方式。我们将在稍后看到这种回调数据结构是如何传递给实际的 Native Bridge 实现的。在我们的例子中,实际的实现是 Houdini 库。

native_bridge.h文件定义了另一个回调函数数据结构,NativeBridgeCallbacks,它用作其实际实现的 Native Bridge 接口。在我们的例子中,这个实现是 Houdini 库。Houdini 库需要实现这些回调函数并将指针传递给 Native Bridge,以便 ART 可以使用它们。以下图显示了这两组回调函数之间的关系:

图片

ART、Native Bridge 和 Houdini

在前面的图中,我们可以看到ART调用Native Bridge函数来加载和初始化Native Bridge模块。Native Bridge模块调用由Houdini注册的回调函数来处理所有 ARM 原生二进制翻译。在Native Bridge初始化期间,NativeBridgeRuntimeCallbacks传递给Houdini库,以便Houdini库中的方法可以调用 JNI 原生函数。

现在我们来看看android::LoadNativeBridge的实现:

bool LoadNativeBridge(const char* nb_library_filename, 
          const NativeBridgeRuntimeCallbacks* runtime_cbs) { 

  if (state != NativeBridgeState::kNotSetup) { 
    // Setup has been called before. Ignore this call. 
    if (nb_library_filename != nullptr) { 
         ALOGW("Called LoadNativeBridge for an already set up native  
           bridge. State is %s.", GetNativeBridgeStateString(state)); 
    } 
    had_error = true; 
    return false; 
  } 

  if (nb_library_filename == nullptr || *nb_library_filename == 0) 
  { 
    CloseNativeBridge(false); 
    return false; 
  } else { 
    if (!NativeBridgeNameAcceptable(nb_library_filename)) { 
      CloseNativeBridge(true); 
    } else { 
      // Try to open the library. 
      void* handle = dlopen(nb_library_filename, RTLD_LAZY); 
      if (handle != nullptr) { 
        callbacks = 
            reinterpret_cast<NativeBridgeCallbacks*>(dlsym(handle, 
            kNativeBridgeInterfaceSymbol)); 
        if (callbacks != nullptr) { 
          if (VersionCheck(callbacks)) { 
            // Store the handle for later. 
            native_bridge_handle = handle; 
          } else { 
            callbacks = nullptr; 
            dlclose(handle); 
            ALOGW("Unsupported native bridge interface."); 
          } 
        } else { 
          dlclose(handle); 
        } 
      } 

      if (callbacks == nullptr) { 
        CloseNativeBridge(true); 
      } else { 
        runtime_callbacks = runtime_cbs; 
        state = NativeBridgeState::kOpened; 
      } 
    } 
    return state == NativeBridgeState::kOpened; 
  } 
} 

从前面的代码片段中我们可以看到,android::LoadNativeBridge首先检查状态。它应该处于kNotSetup状态。否则,它将报告错误并返回。

为了方便起见,在接下来的几段中,我们将把 Android 命名空间中的函数称为LoadNativeBridge,而不是android::LoadNativeBridge。将要讨论的文件可以在以下位置找到:

$AOSP/art/runtime/runtime.c

$AOSP/art/runtime/native_bridge_art_interface.c

$AOSP/system/core/libnativebridge/native_bridge.cc

之后,它将检查第一个参数是否为NULL以及文件名是否可以使用。如果一切正常,它将通过dlopen使用文件名nb_library_filename打开库。

那么nb_library_filename文件名的内容是什么呢?从Runtime::Init函数中我们可以看到,LoadNativeBridge的第一个参数使用Opt::NativeBridge属性初始化:

std::string native_bridge_file_name = runtime_options.ReleaseOrDefault(Opt::NativeBridge); 

这个属性是从默认属性ro.dalvik.vm.native.bridge初始化的,该属性定义在 Android 系统的default.prop文件中。这是在AndroidRuntime::startVm函数中完成的,如以下片段所示。此函数定义在$AOSP/frameworks/base/core/jni/AndroidRuntime.cpp文件中:

int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote) 
{ 
... 
    // Native bridge library. "0" means that native bridge is disabled. 
    property_get("ro.dalvik.vm.native.bridge", propBuf, ""); 
    if (propBuf[0] == '\0') { 
        ALOGW("ro.dalvik.vm.native.bridge is not expected to be 
          empty"); 
    } else if (strcmp(propBuf, "0") != 0) { 
        snprintf(nativeBridgeLibrary, sizeof("-XX:NativeBridge=") + 
        PROPERTY_VALUE_MAX, "-XX:NativeBridge=%s", propBuf); 
        addOption(nativeBridgeLibrary); 
    } 
... 
} 

当启用原生桥接时,ro.dalvik.vm.native.bridge 属性通常包含一个共享库文件名。在我们的例子中,对于英特尔设备是 libhoudini.so,对于 Android-x86 是 libnb.so。如果禁用原生桥接,其值是 0。一旦库加载成功,它将使用 kNativeBridgeInterfaceSymbol 符号来获取内存位置,并将位置转换为 NativeBridgeCallbacks 的指针。这意味着 Houdini 库提供了一个 NativeBridgeCallbacks 的实现。让我们看看 NativeBridgeCallbacks 中都有什么:

struct NativeBridgeCallbacks { 
  uint32_t version; 
  bool (*initialize)(const NativeBridgeRuntimeCallbacks*   
       runtime_cbs, const char* private_dir, const char*  
       instruction_set); 
  void* (*loadLibrary)(const char* libpath, int flag); 
  void* (*getTrampoline)(void* handle, const char* name, const  
        char* shorty, uint32_t len); 
  bool (*isSupported)(const char* libpath); 
  const struct NativeBridgeRuntimeValues* (*getAppEnv)(const char*  
       instruction_set); 
  bool (*isCompatibleWith)(uint32_t bridge_version); 
  NativeBridgeSignalHandlerFn (*getSignalHandler)(int signal); 
}; 

从前面的代码片段中,我们可以看到 NativeBridgeCallbacks 包含一个变量和七个回调函数:

  • version:这是接口的版本号。到目前为止,有两个版本。版本 1 定义了前五个回调函数,版本 2 增加了另外两个新函数,我们很快就会看到。

  • initialize:这个函数初始化 Native Bridge 的一个实例。Native Bridge 的内部实现必须确保多线程安全,并且 Native Bridge 只初始化一次。

  • loadLibrary:这个函数加载 Native Bridge 支持的共享库。

  • getTrampoline:这个函数获取指定原生方法的 Native Bridge 跳转函数。

  • isSupported:这个函数检查 Native Bridge 的实例是否有效,以及它是否为 Native Bridge 支持的 ABI。

在版本 2 中,增加了以下两个函数:

  • isCompatibleWith:这个函数检查桥接是否与给定的库版本兼容。桥接可能决定不向前或向后兼容,此时 libnativebridge 将停止使用它。

  • getSignalHandler:一个回调函数,用于检索指定信号的 Native Bridge 信号处理器。运行时会确保在运行时自己的处理器之后、所有链式处理器之前调用信号处理器。原生桥接不应尝试自行安装处理器,因为这可能导致循环。

现在我们已经完成了 Native Bridge 初始化的第一阶段。从前面的列表中我们可以看到,Native Bridge 在 ART 启动时加载。在这个阶段,初始化不是进程特定的。库名在 ro.dalvik.vm.native.bridge 属性中定义。在我们的例子中,ART 通过在 libnativebridge.so 中定义的 LoadNativeBridge 函数加载 libhoudini.so 库。一旦 Native Bridge 加载成功,状态设置为 kOpened

预初始化 Native Bridge

在 Native Bridge 初始化的第二阶段,它变得与进程相关。Android 应用可以通过 Native Bridge 在不同于当前设备处理器架构的情况下加载本地库。其他两个状态,kPreInitializedkInitialized,与 Android 应用程序的创建有关,因为我们知道在 Android 中所有应用程序都是从 Zygote 分叉出来的。让我们首先看看 Native Bridge 的预初始化,如下面的图所示:

Native Bridge 的预初始化

在创建应用程序的过程中,会调用 ForkAndSpecializeCommon 函数。Native Bridge 的预初始化就在这个函数中完成。这个函数定义在 $AOSP/frameworks/base/core/jni/com_android_internal_os_Zygote.cpp 文件中:

static pid_t ForkAndSpecializeCommon(JNIEnv* env, uid_t uid, gid_t 
    gid, jintArray javaGids, jint debug_flags, jobjectArray 
    javaRlimits, jlong permittedCapabilities, jlong 
    effectiveCapabilities, jint mount_external, jstring 
    java_se_info, jstring java_se_name, bool is_system_server,  
    jintArray fdsToClose, jstring instructionSet, jstring dataDir) { 
      SetSigChldHandler(); 

#ifdef ENABLE_SCHED_BOOST 
  SetForkLoad(true); 
#endif 
... 
  pid_t pid = fork(); 

  if (pid == 0) { 
    // The child process. 
... 
    bool use_native_bridge = !is_system_server && (instructionSet != 
    NULL) && android::NativeBridgeAvailable(); 
    if (use_native_bridge) { 
      ScopedUtfChars isa_string(env, instructionSet); 
      use_native_bridge = 
      android::NeedsNativeBridge(isa_string.c_str()); 
    } 
    if (use_native_bridge && dataDir == NULL) { 
      use_native_bridge = false; 
      ALOGW("Native bridge will not be used because dataDir == NULL."); 
    } 

    if (!MountEmulatedStorage(uid, mount_external, use_native_bridge)) { 
      ALOGW("Failed to mount emulated storage: %s", strerror(errno)); 
      if (errno == ENOTCONN || errno == EROFS) { 
      } else { 
        RuntimeAbort(env, __LINE__, "Cannot continue without emulated 
        storage"); 
      } 
    } 

    if (!is_system_server) { 
        int rc = createProcessGroup(uid, getpid()); 
        if (rc != 0) { 
            if (rc == -EROFS) { 
                ALOGW("createProcessGroup failed, kernel missing 
                CONFIG_CGROUP_CPUACCT?"); 
            } else { 
                ALOGE("createProcessGroup(%d, %d) failed: %s", uid, 
                pid, strerror(-rc)); 
            } 
        } 
    } 

    SetGids(env, javaGids); 

    SetRLimits(env, javaRlimits); 

    if (use_native_bridge) { 
      ScopedUtfChars isa_string(env, instructionSet); 
      ScopedUtfChars data_dir(env, dataDir); 
      android::PreInitializeNativeBridge(data_dir.c_str(), 
      isa_string.c_str()); 
    } 
... 
    env->CallStaticVoidMethod(gZygoteClass, 
           gCallPostForkChildHooks,  
           debug_flags, is_system_server ? NULL : instructionSet);  
... 
  } else if (pid > 0) { 
    // the parent process 

#ifdef ENABLE_SCHED_BOOST 
    // unset scheduler knob 
    SetForkLoad(false); 
#endif 

  } 
  return pid; 
} 

在这个 ForkAndSpecializeCommon 函数中,它检查当前进程是否不是 SystemServer 进程,以及 Native Bridge 是否准备好使用。之后,它调用 NeedsNativeBridge 函数来检查当前进程是否需要使用 Native Bridge:

bool NeedsNativeBridge(const char* instruction_set) { 
  if (instruction_set == nullptr) { 
    ALOGE("Null instruction set in NeedsNativeBridge."); 
    return false; 
  } 
  return strncmp(instruction_set, kRuntimeISA,  
               strlen(kRuntimeISA) + 1) != 0; 
} 

NeedsNativeBridge 函数将 instruction_set 与当前 Android 平台的指令集进行比较。如果这两个指令集不同,那么我们需要使用 Native Bridge;否则,我们不需要。NeedsNativeBridge 函数在 native_bridge.cc 文件中实现。

如果应用程序需要 Native Bridge,那么 PreInitializeNativeBridge 函数将被调用,该函数也实现于 native_bridge.cc,并带有两个参数,app_data_dir_ininstruction_set

bool PreInitializeNativeBridge(const char* app_data_dir_in,  
     const char* instruction_set) { 
  if (state != NativeBridgeState::kOpened) { 
    ALOGE("Invalid state: native bridge is expected to be opened."); 
    CloseNativeBridge(true); 
    return false; 
  } 

  if (app_data_dir_in == nullptr) { 
    ALOGE("Application private directory cannot be null."); 
    CloseNativeBridge(true); 
    return false; 
  } 

  const size_t len = strlen(app_data_dir_in) +  
                    strlen(kCodeCacheDir) + 2; // '\0' + '/' 
  app_code_cache_dir = new char[len]; 
  snprintf(app_code_cache_dir, len, "%s/%s", app_data_dir_in,  
  kCodeCacheDir); 

 state = NativeBridgeState::kPreInitialized; 

#ifndef __APPLE__ 
  if (instruction_set == nullptr) { 
    return true; 
  } 
  size_t isa_len = strlen(instruction_set); 
  if (isa_len > 10) { 
    ALOGW("Instruction set %s is malformed, must be less than or equal 
    to 10 characters.", instruction_set); 
    return true; 
  } 

  char cpuinfo_path[1024]; 

#if defined(__ANDROID__) 
  snprintf(cpuinfo_path, sizeof(cpuinfo_path), "/system/lib" 
#ifdef __LP64__ 
      "64" 
#endif  // __LP64__ 
      "/%s/cpuinfo", instruction_set); 
#else   // !__ANDROID__ 
  snprintf(cpuinfo_path, sizeof(cpuinfo_path), "./cpuinfo"); 
#endif 

  // Bind-mount. 
  if (TEMP_FAILURE_RETRY(mount(cpuinfo_path, 
                               "/proc/cpuinfo", 
                               nullptr, 
                               MS_BIND, 
                               nullptr)) == -1) { 
    ALOGW("Failed to bind-mount %s as /proc/cpuinfo: %s", cpuinfo_path, 
    strerror(errno)); 
  } 
#else  // __APPLE__ 
  UNUSED(instruction_set); 
  ALOGW("Mac OS does not support bind-mounting. Host simulation of 
  native bridge impossible."); 
#endif 

  return true; 
} 

从前面的代码片段中,我们可以看到它会检查状态是否为 kOpened。然后 PreInitializeNativeBridge 将执行两件事。首先,它使用第一个参数 app_data_dir_in 在应用程序的 data 文件夹中为 Native Bridge 创建一个代码缓存目录。接下来,它使用第二个参数 instruction_set 来查找 /system/lib/<isa>/cpuinfo 路径,并将其绑定挂载到 /proc/cpuinfo。如果设备上可用 Houdini,你可以在 system 文件夹中找到 /system/lib/arm/cpuinfo 文件。一旦完成前面的两项任务,Native Bridge 的状态将被设置为 kPreInitialized

初始化 Native Bridge

状态变为 kPreInitialized 后,新的 Android 应用程序将在 ForkAndSpecializeCommon 函数中继续创建。在这个函数的末尾,它通过一个全局变量 gCallPostForkChildHooks 调用一个已注册的 callPostForkChildHooks 函数。调用栈最终会进入 ZygoteHooks_nativePostForkChild 函数,这是 postForkChild Java 方法的 JNI 实现。postForkChild 函数在每次分叉后由 Zygote 在子进程中调用。以下表格是对调用栈的总结:

函数 语言
ForkAndSpecializeCommon C++
gCallPostForkChildHooks C++
callPostForkChildHooks Zygote Java
postForkChild ZygoteHooks Java
ZygoteHooks_nativePostForkChild JNI (postForkChild) C++

ZygoteHooks_nativePostForkChild 函数在 $AOSP/ art/runtime/native/dalvik_system_ZygotHooks.cc 文件中实现。DidForkFromZygote 函数在 $AOSP/art/runtime/runtime.cc 文件中实现。

以下图表是涉及 Native Bridge 初始化第二阶段函数的总结。请注意,我们现在处于子进程中。我们可以看到,ART 中的 Runtime::DidForkFromZygote 函数将调用以下 Native Bridge 接口函数:InitializeNativeBridgeSetupEnvironment。Native Bridge 接口函数最终将调用 Houdini 库中注册的回调函数。

Native Bridge 的初始化

让我们看看 postForkChild 的 JNI 实现:

static void ZygoteHooks_nativePostForkChild(JNIEnv* env,  
    jclass, jlong token, jint debug_flags,  
    jstring instruction_set) { 
... 
  if (instruction_set != nullptr) { 
    ScopedUtfChars isa_string(env, instruction_set); 
    InstructionSet isa = 
    GetInstructionSetFromString(isa_string.c_str()); 
    Runtime::NativeBridgeAction action = 
    Runtime::NativeBridgeAction::kUnload; 
    if (isa != kNone && isa != kRuntimeISA) { 
      action = Runtime::NativeBridgeAction::kInitialize; 
    } 
    Runtime::Current()->DidForkFromZygote(env, action, 
    isa_string.c_str()); 
  } else { 
    Runtime::Current()->DidForkFromZygote(env, 
    Runtime::NativeBridgeAction::kUnload, nullptr); 
  } 
} 

在这里,它再次检查指令集以决定我们是否需要为应用程序使用 Native Bridge。然后它调用 Runtime::DidForkFromZygote 函数以在新进程中初始化 Native Bridge:

void Runtime::DidForkFromZygote(JNIEnv* env,  
    NativeBridgeAction action, const char* isa) { 
  is_zygote_ = false; 

  if (is_native_bridge_loaded_) { 
    switch (action) { 
      case NativeBridgeAction::kUnload: 
        UnloadNativeBridge(); 
        is_native_bridge_loaded_ = false; 
        break; 

      case NativeBridgeAction::kInitialize: 
 InitializeNativeBridge(env, isa); 
        break; 
    } 
  } 
... 
} 

正如我们所见,Runtime::DidForkFromZygote 根据操作调用 InitializeNativeBridge。现在让我们深入了解 InitializeNativeBridge 函数,该函数在 native_bridge.cc 中实现:

bool InitializeNativeBridge(JNIEnv* env,  
    const char* instruction_set) { 

  if (state == NativeBridgeState::kPreInitialized) { 
    // Check for code cache: if it doesn't exist try to create it. 
    struct stat st; 
    if (stat(app_code_cache_dir, &st) == -1) { 
      if (errno == ENOENT) { 
        if (mkdir(app_code_cache_dir, S_IRWXU | S_IRWXG | S_IXOTH) 
        == -1) { 
          ALOGW("Cannot create code cache directory %s: %s.", 
          app_code_cache_dir, strerror(errno)); 
          ReleaseAppCodeCacheDir(); 
        } 
      } else { 
        ALOGW("Cannot stat code cache directory %s: %s.", 
        app_code_cache_dir, strerror(errno)); 
        ReleaseAppCodeCacheDir(); 
      } 
    } else if (!S_ISDIR(st.st_mode)) { 
      ALOGW("Code cache is not a directory %s.", app_code_cache_dir); 
      ReleaseAppCodeCacheDir(); 
    } 

    if (state == NativeBridgeState::kPreInitialized) { 
      if (callbacks->initialize(runtime_callbacks, app_code_cache_dir, 
      instruction_set)) { 
        SetupEnvironment(callbacks, env, instruction_set); 
        state = NativeBridgeState::kInitialized; 
        ReleaseAppCodeCacheDir(); 
      } else { 
        // Unload the library. 
        dlclose(native_bridge_handle); 
        CloseNativeBridge(true); 
      } 
    } 
  } else { 
    CloseNativeBridge(true); 
  } 

  return state == NativeBridgeState::kInitialized; 
} 

InitializeNativeBridge 函数中,它首先创建代码缓存的文件夹。然后,它调用由我们案例中的 Houdini 库实现的 initialize 函数。

在英特尔设备上,共享库是 libhoudini.so。如果您在英特尔设备上运行 Android-x86,共享库是 libnb.so。我们将在本章后面讨论 libnb.so

之后,它会在 native_bridge.cc 中调用另一个 SetupEnvironment 函数,为当前应用程序中的 Native Bridge 设置环境。最后,它将状态设置为 kInitialized。现在 Native Bridge 已准备好供当前应用程序使用。

加载本地库

一旦 Native Bridge 准备好使用,我们可以看看当应用程序在不同的处理器架构中加载本地库时会发生什么。

我们知道,如果我们在一个共享库中实现原生方法,我们需要实现一个 JNI_OnLoad 入口点,用于注册原生方法。Java 代码需要调用 System.loadSystem.loadLibrary 来加载这个共享库。在以下表中,这是从 System.loadLibraryJNI_OnLoad 的调用栈:

函数 语言
System.loadLibrary Runtime Java
doLoad Runtime Java
nativeLoad Runtime JNI
Runtime_nativeLoad Runtime C++
LoadNativeLibrary JavaVMExt C++
JNI_OnLoad C++

让我们深入了解JavaVMExt::LoadNativeLibrary的细节。此函数定义在$AOSP/art/runtime/jni_internal.cc中。以下图是JavaVMExt::LoadNativeLibrary与 Native Bridge 相关部分:

加载本地库

当 Android 应用程序加载本地库时,会调用此函数。通常,我们在这里指的是相同处理器架构下的本地库。通过 Native Bridge,我们也可以使用此函数加载支持的不同处理器架构的本地库:

bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,  
    const std::string& path, jobject class_loader,  
    std::string* error_msg) { 
... 
  const char* path_str = path.empty() ? nullptr : path.c_str(); 
  void* handle = dlopen(path_str, RTLD_NOW); 
  bool needs_native_bridge = false; 
  if (handle == nullptr) { 
    if (android::NativeBridgeIsSupported(path_str)) { 
      handle = android::NativeBridgeLoadLibrary(path_str, RTLD_NOW); 
      needs_native_bridge = true; 
    } 
  } 
... 
  bool was_successful = false; 
  void* sym; 
  if (needs_native_bridge) { 
    library->SetNeedsNativeBridge(); 
 sym = library->FindSymbolWithNativeBridge("JNI_OnLoad", nullptr); 
  } else { 
    sym = dlsym(handle, "JNI_OnLoad"); 
  } 
... 
    typedef int (*JNI_OnLoadFn)(JavaVM*, void*); 
    JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym); 
 int version = (*jni_on_load)(this, nullptr);  
... 
} 

LoadNativeLibrary函数首先调用dlopen来加载共享库。如果它是在不同处理器架构上的共享库,例如在 Intel x86 平台上的 ARM 库,dlopen调用应该失败。在这种情况下,它将尝试使用 Native Bridge 而不是返回错误来重新加载库。

要使用 Native Bridge,它首先调用NativeBridgeIsSupported函数来检查 Native Bridge 是否受支持。NativeBridgeIsSupported函数调用 Houdini 回调函数isSupported来检查给定的共享库是否可以通过 Native Bridge 支持:

bool NativeBridgeIsSupported(const char* libpath) { 
  if (NativeBridgeInitialized()) { 
    return callbacks->isSupported(libpath); 
  } 
  return false; 
} 

如果库可以通过 Native Bridge 支持,LoadNativeLibrary将调用另一个 Native Bridge 函数,android::NativeBridgeLoadLibrary来加载库:

void* NativeBridgeLoadLibrary(const char* libpath, int flag) { 
  if (NativeBridgeInitialized()) { 
    return callbacks->loadLibrary(libpath, flag); 
  } 
  return nullptr; 
} 

Native Bridge 的NativeBridgeLoadLibrary函数将调用 Houdini 回调函数loadLibrary来加载库。本地库加载成功后,库中的JNI_OnLoad入口点将被找到,系统将调用它来注册本地库注册的本地方法。对于正常的本地库,系统函数dlsym用于获取JNI_OnLoad方法,但FindSymbolWithNativeBridge函数用于从 Houdini 库中获取JNI_OnLoad

void* FindSymbolWithNativeBridge(const std::string& symbol_name, 
const char* shorty) { 
    CHECK(NeedsNativeBridge()); 

    uint32_t len = 0; 
    return android::NativeBridgeGetTrampoline(handle_,  
    symbol_name.c_str(), shorty, len); 
} 

FindSymbolWithNativeBridge调用NativeBridgeGetTrampoline Native Bridge 函数,而NativeBridgeGetTrampoline调用getTrampoline Houdini 回调函数来完成实际工作:

void* NativeBridgeGetTrampoline(void* handle, const char* name, 
const char* shorty, uint32_t len) { 
  if (NativeBridgeInitialized()) { 
    return callbacks->getTrampoline(handle, name, shorty, len); 
  } 
  return nullptr; 
} 

从前面的分析中,我们可以看出 Houdini 使用的 ARM 翻译库在 Android 中通过 Native Bridge 支持 ARM 二进制翻译。Houdini 库与系统之间的接口是两套回调函数。在NativeBridgeCallbacks中定义的回调函数被系统用来对 ARM 本地库进行函数调用,而定义在NativeBridgeRuntimeCallbacks中的回调函数可以被 Houdini 库中的函数用来调用系统中的 JNI 函数。

将 Houdini 集成到 x86emu 设备中

本章的目标是在 Android 模拟器中支持 Houdini ARM 二进制翻译。在了解 Native Bridge 的内部原理之后,它是 Houdini 库的基础,我们可以着手为我们的 x86emu 设备提供 Houdini 支持。

由于 Houdini 库是英特尔专有库,因此它不可公开获取。对于那些想要将 Houdini 添加到新设备(如不受英特尔支持的 Android 模拟器)的人,唯一可能的方法是从受支持的设备复制 Houdini 库并将其添加到不受支持的设备。

幸运的是,开源项目 Android-x86 为任何英特尔设备提供了对 Houdini 的基本支持,我们可以将其作为本书的参考。在本章中,我们将基于 Android-x86 项目将 Houdini 添加到 Android 模拟器。

修改 x86emu 构建配置

在新设备上支持 Houdini 的基本步骤是:

  • 根据我们在 第四章,自定义 Android 模拟器为什么要自定义 Android 模拟器? 部分讨论的内容,更改设备配置

  • 将适合版本的 Houdini 库复制到 system 文件夹

要处理前两个步骤,我们首先从修改 x86emu 设备配置开始。我们将为此使用 第四章,自定义 Android 模拟器 中的源代码作为基准,并在其基础上进行修改。这也是本书大多数章节中我们将采用的方法。我们将根据每个主题最简单的代码库进行独立修改。我的意思是,第四章,自定义 Android 模拟器 中 x86emu 的源代码是这个设备的最简单代码库。

既然我们已经有了一份 x86emu 的 AOSP 源代码的工作副本,我们可以在一个新分支中为本章进行修改:

$ cd device/generic/x86emu
$ git checkout android-7.1.1_r4_x86emu_ch04_r1
$ git branch android-7.1.1_r4_ch05
$ git checkout android-7.1.1_r4_ch05  

我为每个章节创建了一个标签,我们可以将其作为新开发的起点。android-7.1.1_r4_x86emu_ch04_r1 标签是 第四章,自定义 Android 模拟器 的基准。从前面的命令中,我们创建了一个新的分支,android-7.1.1_r4_ch05,用于本章的开发工作。我没有将开发分支推送到 GitHub,但我将所有标签推送到 GitHub。在我们完成本章的所有更改后,我们将为这一章创建一个新的标签,android-7.1.1_r4_x86emu_ch05_r1

在我们完成所有修改后,我们还需要更新清单文件,以便我们可以有一个清单来下载本章的代码。我不用标签,而是用分支来管理清单。本章清单的分支是 android-7.1.1_r4_ch05_aosp。我们可以使用以下命令下载以下内容的源代码:

$ repo init -u https://github.com/shugaoye/manifests -b android-7.1.1_r4_ch05_aosp
$ repo sync

如果你已按照我们在 第二章,设置开发环境 中讨论的方式设置了本地镜像,你可以按照以下方式检出源代码:

$ repo init -u {your local mirror}/github/manifests.git -b android-7.1.1_r4_ch05
$ repo sync  

你需要为你的本地镜像创建一个 android-7.1.1_r4_ch05 分支,它引用了 android-7.1.1_r4_ch05_aosp 分支。

使用前面的清单创建源代码的工作副本后,我们可以查看 .repo/manifest.xml 文件:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="github" 
           fetch="." /> 

  <remote  name="aosp" 
           fetch="https://android.googlesource.com/" /> 
  <default revision="refs/tags/android-7.1.1_r4" 
           remote="aosp" 
           sync-c="true" 
           sync-j="1" /> 

  <!-- github/shugaoye --> 
  <project path="kernel" name="goldfish" remote="github"   
  revision="refs/tags/android-7.1.1_r4_x86emu_ch05_r1" /> 
  <project path="device/generic/common" name="device_generic_common" 
   groups="pdk" 
  remote="github" revision="refs/tags/android-7.1.1_r4_x86emu_ch05_r1" /> 
  <project path="device/generic/goldfish" 
  name="device_generic_goldfish" 
  remote="github" groups="pdk" revision="refs/tags/android- 
  7.1.1_r4_x86emu_ch05_r1" /> 
  <project path="device/generic/x86emu" name="x86emu" remote="github" 
  revision="refs/tags/android-7.1.1_r4_x86emu_ch05_r1" /> 

  <!-- aosp --> 
  <project path="build" name="platform/build" groups="pdk,tradefed" > 
    <copyfile src="img/root.mk" dest="Makefile" /> 
... 
</manifest> 

在前面的清单文件中,我们使用 android-7.1.1_r4_x86emu_ch05_r1 标签标记所有不在 AOSP 项目中的项目。device/generic/common 项目是从 Android-x86 复制的,而 device/generic/goldfish 项目是从 AOSP 复制的。除了 kerneldevice/generic/x86emu,这些是我们本章需要更改的两个项目。

扩展 x86emu 设备

一旦我们对源代码配置的所有更改都完成了,我们现在就可以开始扩展 x86emu 设备以支持 Houdini。我们需要做出哪些更改?由于我在解释之前已经做了所有更改,让我们使用一个工具来比较 第四章 中 自定义 Android 模拟器 和本章代码的源代码之间的差异。

支持 Houdini 的更改

如前一个截图所示,我们添加了一个 system 文件夹,并修改了四个 Makefile。我们可以忽略 x86emu_x86_64.mk Makefile,因为我们不会在本书中讨论 64 位构建。x86emu_x86_64.mk 的更改与 x86emu_x86.mk 的更改类似,所以我们省去了重复讨论类似内容的麻烦。你自己启用 x86emu 的 64 位构建不会是一个很大的工作量。其他两个文件,.cproject.project,是由于 Eclipse 集成而生成的,我们也可以忽略它们。让我们逐一查看 BoardConfig.mkx86emu_x86.mkdevice.mk

对 BoardConfig.mk 的更改

在板配置文件中,我们需要将 ARM 指令集添加到 CPU ABI 列表中,以便程序可以检测对 ARM 指令集的支持,如下所示:

... 
# houdini 
# Native Bridge ABI List 
NATIVE_BRIDGE_ABI_LIST_32_BIT := armeabi-v7a armeabi 
NATIVE_BRIDGE_ABI_LIST_64_BIT := arm64-v8a 
TARGET_CPU_ABI_LIST_32_BIT := $(TARGET_CPU_ABI) $(TARGET_CPU_ABI2) $(NATIVE_BRIDGE_ABI_LIST_32_BIT) 
TARGET_CPU_ABI_LIST := $(TARGET_CPU_ABI_LIST_32_BIT) 

BUILD_ARM_FOR_X86 := $(WITH_NATIVE_BRIDGE) 
... 

你可能已经注意到了 BUILD_ARM_FOR_X86 宏。这个宏被 Android-x86 Houdini 支持使用,我们将在稍后讨论它。

对 x86emu_x86.mk 的更改

在产品定义的 Makefile 中,x86emu_x86.mk,我们将 persist.sys.nativebridge 属性设置为 1。然后我们将 $AOSP/device/generic/x86emu/system 文件夹下的所有文件复制到 $OUT/system 映像中。$AOSP/device/generic/x86emu/system/lib/arm 文件夹下的所有文件都是 Houdini 库的副本:

... 
PRODUCT_PROPERTY_OVERRIDES := \ 
    persist.sys.nativebridge=1 \ 

NB_PATH := $(LOCAL_PATH) 
NB_LIB_PATH := system/lib 
NB_ARM_PATH := $(NB_LIB_PATH)/arm 
NB_NBLIB_PATH := $(NB_ARM_PATH)/nb 
NB_BIN_PATH := system/bin 

PRODUCT_COPY_FILES += $(foreach LIB, $(filter-out nb liblog_legacy.so libbinder_legacy.so,\  
      $(notdir $(wildcard $(NB_PATH)/$(NB_ARM_PATH)/*))), $(NB_PATH)/$(NB_ARM_PATH)/$(LIB):$(NB_ARM_PATH)/$(LIB):intel) 
PRODUCT_COPY_FILES += $(foreach NB, $(filter-out libbinder_legacy.so, $(notdir $(wildcard $(NB_PATH)/$(NB_NBLIB_PATH)/*))),\  
      $(NB_PATH)/$(NB_NBLIB_PATH)/$(NB):$(NB_NBLIB_PATH)/$(NB):intel) 
... 

对 device.mk 的更改

在设备 Makefile device.mk 中,我们只添加了一行来包含另一个 Makefile,nativebridge.mk,在 device/generic/common/nativebridge 目录下。正如我们在源配置章节中讨论的那样,我们使用 Android-x86 的版本来支持 Houdini 集成。我们将在下一节分析 nativebridge.mk Makefile:

... 
# Get native bridge settings 
$(call inherit-product-if-exists,device/generic/common/nativebridge/nativebridge.mk) 
... 

使用 Android-x86 实现

由于我们使用 Android-x86 项目的 Houdini 支持,我们可以看到我们只需要对 x86emu Makefile 进行非常小的修改。

现在让我们看看 Android-x86 中的 nativebridge.mk

# Enable native bridge 
WITH_NATIVE_BRIDGE := true 

# Native Bridge ABI List 
NATIVE_BRIDGE_ABI_LIST_32_BIT := armeabi-v7a armeabi 
NATIVE_BRIDGE_ABI_LIST_64_BIT := arm64-v8a 

LOCAL_SRC_FILES := bin/enable_nativebridge 

PRODUCT_COPY_FILES := $(foreach f,$(LOCAL_SRC_FILES),$(LOCAL_PATH)/$(f):system/$(f)) 

PRODUCT_PROPERTY_OVERRIDES := \ 
    ro.dalvik.vm.isa.arm=x86 \ 
    ro.enable.native.bridge.exec=1 \  

ifeq ($(TARGET_SUPPORTS_64_BIT_APPS),true) 
PRODUCT_PROPERTY_OVERRIDES += \ 
    ro.dalvik.vm.isa.arm64=x86_64 \ 
    ro.enable.native.bridge.exec64=1 
endif 

PRODUCT_DEFAULT_PROPERTY_OVERRIDES := ro.dalvik.vm.native.bridge=libnb.so 

PRODUCT_PACKAGES := libnb 

$(call inherit-product-if-exists,vendor/intel/houdini/houdini.mk) 

nativebridge.mk 首先将 enable_nativebridge 脚本复制到 system 文件夹。之后,它设置 ro.dalvik.vm.isa.armro.enable.native.bridge.exec 属性。这两个属性将被添加到系统镜像中的 system/build.prop 文件中。它还设置了默认属性 ro.dalvik.vm.native.bridgelibnb.so。此属性由 ART 用于查找 Houdini 库。Android-x86 使用 libnb.so 库而不是所有支持英特尔设备都使用的 libhoudini.solibnb.so 库是 libhoudini.so 的包装器。由于我们使用 libnb.so 作为 ARM 二进制翻译库,我们需要将此包添加到构建中。

分析 libnb.so

由于 libnb.so 库是 Android-x86 中 Native Bridge 支持的关键起点,我们现在将深入探讨其细节。构建 libnb.so 的 Makefile 可以在 device/generic/common/nativebridge/Android.mk 中找到。libnb.so 的源代码仅包含一个文件,即 libnb.cpp,如下所示:

#define LOG_TAG "libnb" 

#include <dlfcn.h> 
#include <cutils/log.h> 
#include <cutils/properties.h> 
#include "nativebridge/native_bridge.h" 

namespace android { 

static void *native_handle = nullptr; 

static NativeBridgeCallbacks *get_callbacks() 
{ 
    static NativeBridgeCallbacks *callbacks = nullptr; 

    if (!callbacks) { 
        const char *libnb = "/system/" 
        #ifdef __LP64__ 
                "lib64/arm64/" 
        #else 
                "lib/arm/" 
        #endif 
                "libhoudini.so"; 
        if (!native_handle) { 
            native_handle = dlopen(libnb, RTLD_LAZY); 
            if (!native_handle) { 
                ALOGE("Unable to open %s", libnb); 
                return nullptr; 
            } 
        } 
        callbacks = reinterpret_cast<NativeBridgeCallbacks *> 
        (dlsym(native_handle, "NativeBridgeItf")); 
    } 
    return callbacks; 
} 

// NativeBridgeCallbacks implementations 
static bool native_bridge2_initialize(const    
  NativeBridgeRuntimeCallbacks *art_cbs, const char  
  *app_code_cache_dir, const char *isa) 
{ 
    ALOGV("enter native_bridge2_initialize %s %s", 
    app_code_cache_dir, isa); 
    if (property_get_bool("persist.sys.nativebridge", 0)) { 
        if (NativeBridgeCallbacks *cb = get_callbacks()) { 
return cb->initialize(art_cbs, app_code_cache_dir, isa); 
        } 
    } else { 
        ALOGW("Native bridge is disabled"); 
    } 
    return false; 
} 

static void *native_bridge2_loadLibrary(const char *libpath, int flag) 
{ 
    ALOGV("enter native_bridge2_loadLibrary %s", libpath); 
    NativeBridgeCallbacks *cb = get_callbacks(); 
 return cb ? cb->loadLibrary(libpath, flag) : nullptr; 
} 

static void *native_bridge2_getTrampoline(void *handle,  
  const char *name, const char* shorty, uint32_t len) 
{ 
    ALOGV("enter native_bridge2_getTrampoline %s", name); 
    NativeBridgeCallbacks *cb = get_callbacks(); 
    return cb ? cb->getTrampoline(handle, name, shorty, len) 
    : nullptr; 
} 

static bool native_bridge2_isSupported(const char *libpath) 
{ 
    ALOGV("enter native_bridge2_isSupported %s", libpath); 
    NativeBridgeCallbacks *cb = get_callbacks(); 
    return cb ? cb->isSupported(libpath) : false; 
} 

static const struct NativeBridgeRuntimeValues *native_bridge2_getAppEnv(const char *abi) 
{ 
    ALOGV("enter native_bridge2_getAppEnv %s", abi); 
    NativeBridgeCallbacks *cb = get_callbacks(); 
    return cb ? cb->getAppEnv(abi) : nullptr; 
} 

static bool native_bridge2_is_compatible_compatible_with(uint32_t version) 
{ 
    // For testing, allow 1 and 2, but disallow 3+. 
    return version <= 2; 
} 

static NativeBridgeSignalHandlerFn native_bridge2_get_signal_handler(int signal) 
{ 
    ALOGV("enter native_bridge2_getAppEnv %d", signal); 
    NativeBridgeCallbacks *cb = get_callbacks(); 
    return cb ? cb->getSignalHandler(signal) : nullptr; 
} 

static void __attribute__ ((destructor)) on_dlclose() 
{ 
    if (native_handle) { 
        dlclose(native_handle); 
        native_handle = nullptr; 
    } 
} 

extern "C" { 

NativeBridgeCallbacks NativeBridgeItf = { 
    version: 2, 
    initialize: &native_bridge2_initialize, 
    loadLibrary: &native_bridge2_loadLibrary, 
    getTrampoline: &native_bridge2_getTrampoline, 
    isSupported: &native_bridge2_isSupported, 
    getAppEnv: &native_bridge2_getAppEnv, 
    isCompatibleWith: &native_bridge2_is_compatible_compatible_with, 
    getSignalHandler: &native_bridge2_get_signal_handler, 
}; 

} // extern "C" 
} // namespace android 

libnb.cpp 中,我们可以看到它加载了 libhoudini.so 库,这是英特尔提供的原始 Houdini 库,并且它只做了两个修改。在初始化之前,它检查 persist.sys.nativebridge 属性。其余的代码提供了一个 NativeBridgeCallbacks 的包装器,包装器函数直接调用 Houdini 库中的函数。

使用 binfmt_misc

到目前为止,我们所讨论的是如何在英特尔 x86 架构中加载 ARM 共享库。Houdini 还可以支持在英特尔设备上运行独立的 ARM 应用程序。为此,它使用了一种称为 binfmt_misc 的机制。binfmt_misc 是 Linux 内核的一种功能,允许识别任意可执行文件格式并将其传递给某些用户空间应用程序,例如模拟器和虚拟机。

根据 Linux 内核文档,此内核功能允许您通过在 shell 中简单地键入程序名称来调用几乎每个程序。例如,这包括编译的 Java (TM)、Python 或 Emacs。为了实现这一点,您必须告诉 binfmt_misc 应该使用哪个解释器调用哪个二进制文件。binfmt_misc 通过匹配您提供的魔字节序列(屏蔽指定的位)与文件开头的某些字节来识别二进制类型。binfmt_misc 还可以识别文件扩展名,如 .com.exe

要使用此方法,首先我们必须挂载 binfmt_misc

$ mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc  

要注册新的二进制类型,我们必须设置一个看起来如下所示的字符串:

:name:type:offset:magic:mask:interpreter:flags  

然后我们需要将其添加到 /proc/sys/fs/binfmt_misc/register

下面是这些字段的意义:

  • name 是一个标识符字符串。在 /proc/sys/fs/binfmt_misc 目录下将创建一个以该名称命名的新 /proc 文件。

  • type 是识别类型。它给出 M 表示 magic 和 E 表示扩展。

  • offset 是文件中 magic/mask 的偏移量,按字节计算。如果你省略它(即,你写入 :name:type::magic...),则默认为 0。

  • magicbinfmt_misc 匹配的字节序列。magic 字符串可能包含十六进制编码的字符,如 \x0a\xA4。在 shell 环境中,你应该写 \\x0a 以防止 shell 吃掉你的 \。如果你选择了匹配的文件名扩展名,这是要识别的扩展名(不带 .,不允许 \x0a 特殊字符)。扩展名匹配是区分大小写的。

  • mask 是一个(可选,默认为所有 0xff)掩码。你可以通过提供一个类似于 magic 的字符串来屏蔽匹配的一些位。

  • interpreter 是应该用二进制文件作为第一个参数调用的程序(指定完整路径)。

  • flags 是一个可选字段,它控制解释器调用的几个方面。它是一个大写字母的字符串,每个字母控制一个特定的方面。

nativebridge.mk 中,它将 enable_nativebridge 脚本复制到 system 文件夹。此文件用于在 Android-x86 中启用 Houdini。在 Android-x86 中,Houdini 默认未启用。这可以通过在 Android-x86 设置应用中的选项随时打开。当然,这不在 AOSP 源代码中得到支持。当你打开 Android-x86 中的 Houdini 时,它会调用 enable_nativebridge 脚本。此脚本执行两件事:

  1. 它从第三方项目仓库下载 Houdini 到本地仓库,并将其安装到 /system/lib/arm 系统目录中。它还设置 persist.sys.nativebridge 属性为 1

  2. 在此脚本的第二部分,它会在 /proc 目录中创建 binfmt_misc 文件。

我们不会直接使用 enable_nativebridge 脚本,但希望在系统启动时运行 enable_nativebridge 的第二部分。通过第二部分,Houdini 在 Android 模拟器中默认启用。这可以通过将 enable_nativebridge 的第二部分添加到 device/generic/goldfish/init.goldfish.sh 来完成。以下是我们添加到 init.goldfish.sh 结尾的代码片段。这是在系统启动时用于设置 Android 模拟器环境的脚本:

... 
# 
# Houdini integration (Native Bridge) 
# 
houdini_bin=0 
dest_dir=/system/lib$1/arm$1 
binfmt_misc_dir=/proc/sys/fs/binfmt_misc 

# if you don't see the files 'register' and 'status' in /proc/sys/fs/binfmt_misc 
# then run the following command: 
# mount -t binfmt_misc none /proc/sys/fs/binfmt_misc 

# this is to add the supported binary formats via binfmt_misc 

if [ ! -e $binfmt_misc_dir/register ]; then 
   mount -t binfmt_misc none $binfmt_misc_dir 
fi 

cd $binfmt_misc_dir 
if [ -e register ]; then 
   # register Houdini for arm binaries 
   if [ -z "$1" ]; then 
         echo ':arm_exe:M::\\x7f\\x45\\x4c\\x46\\x01\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x28::'"$dest_dir/houdini:P" > register 
         echo ':arm_dyn:M::\\x7f\\x45\\x4c\\x46\\x01\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\x28::'"$dest_dir/houdini:P" > register 
   else 
         echo ':arm64_exe:M::\\x7f\\x45\\x4c\\x46\\x02\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\xb7::'"$dest_dir/houdini64:P" > register 
         echo ':arm64_dyn:M::\\x7f\\x45\\x4c\\x46\\x02\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\xb7::'"$dest_dir/houdini64:P" > register 
   fi 
   if [ -e arm${1}_exe ]; then 
         houdini_bin=1 
   fi 
else 
   log -pe -thoudini "No binfmt_misc support" 
fi 

if [ $houdini_bin -eq 0 ]; then 
   log -pe -thoudini "houdini$1 enabling failed!" 
else 
   log -pi -thoudini "houdini$1 enabled" 
fi 

[ "$(getprop ro.zygote)" = "zygote64_32" -a -z "$1" ] && exec $0 64 

在我们重新构建镜像并启动模拟器后,我们可以使用以下命令验证更改:

$ adb shell
root@x86emu:/ # ls /proc/sys/fs/binfmt_misc/ 
arm_dyn
arm_exe
register
status  

我们可以看到我们注册了两个 binfmt_misc 类型:arm_dynarm_exe/proc 文件 arm_dyn 用于加载共享库,而 arm_exe 用于加载 ARM 可执行文件:

root@x86emu:/ # cat /proc/sys/fs/binfmt_misc/arm_exe 
enabled
interpreter /system/lib/arm/houdini
flags: P
offset 0
magic 7f454c46010101000000000000000000020028  

如果我们查看 arm_exe 的内容,从前面的输出中我们可以看到,/system/lib/arm/houdini 解释器用于解释 ARM 二进制文件。

构建和测试

我们已经对代码进行了所有更改,以启用 Houdini。我们可以使用以下命令构建系统镜像:

$ source build/envsetup.sh
$ lunch x86emu_x86-eng
$ m -j4  

在我们构建系统镜像之后,我们可以对其进行测试。当然,我们可以使用任何可以在 ARM 架构上运行的 Android 应用程序来测试镜像。然而,为了获取有关测试目标的详细信息,我们将使用两个单元测试应用程序来验证本章中的工作。第一个是一个独立的 ARM 应用程序,可以从命令行运行。第二个是一个仅针对 ARM 的 JNI 共享库的 Android 应用程序。本章中的 Android 模拟器镜像和测试二进制文件可以从 sourceforge.net/projects/android-system-programming/files/android-7/ch05/ch05.zip/download 下载。

这两个测试应用程序的源代码托管在 GitHub 上。您可以从 github.com/shugaoye/asp-sample/tree/master/ch05 获取源代码。

要构建测试应用程序,您需要同时拥有 Android SDK 和 NDK,这样您就可以构建 Android 应用程序和本地应用程序。

测试命令行应用程序

在您克隆了测试应用程序的先前 Git 仓库之后,您可以构建和测试它们。让我们首先测试命令行应用程序。这是一个非常简单的“hello world”应用程序,它将一行消息打印到标准输出,如下所示:

#include <stdio.h> 

void main() 
{ 
    printf("This is built using NDK r12.n"); 
} 

您可以在模拟器中按照以下方式构建和测试它:

$ cd ch05/test1
$ ./build.sh 
[armeabi-v7a] Install : ch05_test => libs/armeabi-v7a/ch05_test
$ file libs/armeabi-v7a/ch05_test
libs/armeabi-v7a/ch05_test: ELF 32-bit LSB  shared object, ARM, EABI5 version 1 (SYSV), dynamically linked (uses shared libs), BuildID[sha1]=b3cf0ae12c0d5b192053dc40c31f665196145039, stripped
$ adb push libs/armeabi-v7a/ch05_test /data/local/tmp
[100%] /data/local/tmp/ch05_test
$ adb shell
root@x86emu:/ # cd /data/local/tmp
127|root@x86emu:/data/local/tmp # ./ch05_test
This is built using NDK r12.  

在构建完成后,您可以使用 file 命令检查文件格式。您可以看到输出是一个 32 位的 ARM ELF 文件。您可以使用 adb 将此二进制文件推送到模拟器并运行它。您将看到它可以将输出消息正确地打印到标准输出。

测试 Android JNI 应用程序

接下来,让我们测试带有 ARM JNI 库的 Android 应用程序。

JNI 库位于 ch05/test2/jni。支持的处理器架构在 Application.mk 中定义如下:

# Build both ARMv5TE and ARMv7-A and x86 machine code. 
# armeabi armeabi-v7a  
APP_ABI := armeabi armeabi-v7a 
APP_PLATFORM := android-23 

我们可以看到我们为 armeabiarmeabi-v7a 构建了 JNI 库。让我们首先使用 NDK 构建 JNI 库:

$ cd ch05/test2/jni
$ ./build.sh
[armeabi] Install : libHelloJNI.so => libs/armeabi/libHelloJNI.so
[armeabi-v7a] Install: libHelloJNI.so => libs/armeabi-v7a/libHelloJNI.so  

在我们构建 JNI 库之后,我们可以将 Android 源代码导入到 Eclipse 或 Android Studio 中来构建应用程序本身。我们不会解释导入和构建 Android 应用程序的详细步骤。您可以通过阅读关于如何开发 Android 应用程序和如何开发 JNI 库的书籍来了解更多信息。我们在这里想要调查的是测试结果。在我们获得 APK 文件后,我们可以在模拟器中安装并运行它。同时,我们可以使用 logcat 捕获调试日志。以下是我环境的日志:

... 
10-02 00:44:57.871: I/ActivityManager(1527): START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=fr.myrddin.hellojni/.HelloJNIActivity (has extras)} from uid 10008 on display 0 
10-02 00:44:57.900: I/ActivityManager(1527): Start proc 2652:fr.myrddin.hellojni/u0a53 for activity fr.myrddin.hellojni/.HelloJNIActivity 
10-02 00:44:57.902: I/art(2652): Late-enabling JIT 
10-02 00:44:57.903: D/houdini(2652): [2652] Initialize library(version: 6.1.1a_x.48413 RELEASE)... successfully. 
10-02 00:44:57.907: W/art(2652): Unexpected CPU variant for X86 using defaults: x86 
10-02 00:44:57.907: I/art(2652): JIT created with code_cache_capacity=2MB compile_threshold=1000 
10-02 00:44:58.546: W/art(1527): Long monitor contention event with owner method=int com.android.server.wm.WindowManagerService.relayoutWindow(com.android.server.wm.Session, android.view.IWindow, int, android.view.WindowManager$LayoutParams, int, int, int, int, android.graphics.Rect, android.graphics.Rect, android.graphics.Rect, android.graphics.Rect, android.graphics.Rect, android.graphics.Rect, android.content.res.Configuration, android.view.Surface) from WindowManagerService.java:3104 waiters=0 for 632ms 
10-02 00:44:58.580: W/dex2oat(2667): Unexpected CPU variant for X86 using defaults: x86 
10-02 00:44:58.581: W/dex2oat(2667): Mismatch between dex2oat instruction set features (ISA: X86 Feature string: smp,-ssse3,-sse4.1,-sse4.2,-avx,-avx2) and those of dex2oat executable (ISA: X86 Feature string: smp,ssse3,-sse4.1,-sse4.2,-avx,-avx2) for the command line: 
10-02 00:44:58.581: W/dex2oat(2667): /system/bin/dex2oat --runtime-arg -classpath --runtime-arg  --compiler-filter=interpret-only --instruction-set=x86 --instruction-set-features=smp,ssse3,-sse4.1,-sse4.2,-avx,-avx2 --runtime-arg -Xrelocate --boot-image=/system/framework/boot.art --runtime-arg -Xms64m --runtime-arg -Xmx512m --compiler-filter=verify-at-runtime --instruction-set-variant=x86 --instruction-set-features=default --dex-file=/data/app/fr.myrddin.hellojni-1/base.apk --oat-file=/data/dalvik-cache/x86/data@app@fr.myrddin.hellojni-1@base.apk@classes.dex 
10-02 00:44:58.581: E/dex2oat(2667): Failed to create oat file: /data/dalvik-cache/x86/data@app@fr.myrddin.hellojni-1@base.apk@classes.dex: Permission denied 
10-02 00:44:58.581: I/dex2oat(2667): dex2oat took 774.330us (threads: 2)  
10-02 00:44:58.582: W/art(2652): Failed execv(/system/bin/dex2oat --runtime-arg -classpath --runtime-arg  --compiler-filter=interpret-only --instruction-set=x86 --instruction-set-features=smp,ssse3,-sse4.1,-sse4.2,-avx,-avx2 --runtime-arg -Xrelocate --boot-image=/system/framework/boot.art --runtime-arg -Xms64m --runtime-arg -Xmx512m --compiler-filter=verify-at-runtime --instruction-set-variant=x86 --instruction-set-features=default --dex-file=/data/app/fr.myrddin.hellojni-1/base.apk --oat-file=/data/dalvik-cache/x86/data@app@fr.myrddin.hellojni-1@base.apk@classes.dex) because non-0 exit status 
10-02 00:44:58.603: D/houdini(2652): [2652] Added shared library /data/app/fr.myrddin.hellojni-1/lib/arm/libHelloJNI.so for ClassLoader by Native Bridge. 
10-02 00:44:58.603: E/JNI(2652): Number : 4  
... 
10-02 00:44:59.906: I/ActivityManager(1527): Displayed fr.myrddin.hellojni/.HelloJNIActivity: +2s9ms  
... 

从前面的日志消息中我们可以看到 Houdini 已成功初始化,并且 libHelloJNI.so JNI 库已被 Native Bridge 加载。

摘要

在本章中,我们首先介绍了 Android 架构中的 Native Bridge,以便我们了解其工作原理。基于我们对 Native Bridge 的理解,我们扩展了 x86emu 设备以支持 Houdini。我们修改了 x86emu 设备的 Makefiles,并且还利用了开源项目 Android-x86 来节省集成工作。在我们将 Houdini 集成到 x86emu 之后,我们测试了两种 Houdini 的使用场景:

  • 一个独立的命令行应用程序

  • 一个使用 JNI 构建的本地共享库的 Android 应用程序

在下一章中,我们将探讨更多关于 x86emu 启动过程的内容,并且我们将学习如何使用定制的 ramdisk 图像来调试启动过程。

第六章:使用自定义 ramdisk 调试启动过程

在上一章中,我们学习了如何在安卓模拟器中使用自己的 x86emu 设备启用 Houdini。有了这个,我们可以在接下来的几章中继续进行更具挑战性的任务。大多数设备或系统级别的定制都将涉及对安卓系统启动顺序的更改。在本章中,我们将分析安卓系统启动顺序,并学习与启动顺序的定制和调试相关的知识。在本章中,我们将涵盖以下主题:

  • 安卓启动过程分析

  • Android-x86 的启动过程

  • 为 Android-x86 的initrd.img创建文件系统

我们将从分析正常的安卓启动过程开始。之后,我们将介绍 Android-x86 的双阶段启动。我们将为安卓模拟器构建一个文件系统,它可以与 Android-x86 的initrd.img一起工作。这种方法提供了一种灵活的方式来帮助调试启动过程。

分析安卓启动过程

安卓系统的启动顺序与其他从处理器内部的 Boot ROM 开始的嵌入式 Linux 系统类似。Boot ROM 将找到引导加载程序。引导加载程序将加载内核和 ramdisk 镜像。内核使用 ramdisk 作为根文件系统。在桌面 Linux 环境中,一旦内核初始化了基本设备,它将在硬盘等物理存储上重新挂载根文件系统。在安卓中,各种分区(系统、数据、缓存等)将挂载到内存中的根文件系统,而不是存储设备。内核将在 ramdisk 中调用init进程以启动系统的其余部分,如图所示:

图片

引导加载程序和内核

如我们所见,当我们构建自己的设备时,我们无法避免引导加载程序。然而,我们不会在这个主题上花费太多时间,因为引导加载程序不是本书的重点。在安卓模拟器中,不需要有引导加载程序,因为模拟器内部已经构建了一个最小化的引导加载程序。

QEMU 中的一个非常小的引导加载程序来启动 Linux 如果您对 QEMU 中的小引导加载程序感兴趣,可以参考位于$AOSP/external/qemu/hw/arm/boot.c的 AOSP 源代码。

由于引导加载程序是硬件平台特定的,因此 QEMU 中的引导加载程序实现对于不同的硬件架构(如 ARM、x86 或 MIPS)是不同的。我之所以提到 ARM 实现,是因为它是最干净且最容易理解的。

您可以参考由我撰写并由 Addison-Wesley Professional 出版的《嵌入式编程与安卓》一书,以了解更多关于安卓模拟器引导加载程序的信息。

对于我们将从第八章的在 VirtualBox 上创建自己的设备到第十一章的启用 VirtualBox 特定硬件接口使用的虚拟硬件,我们将使用网络引导来解决引导加载程序问题。

Linux 内核是支持各种硬件设备的关键元素之一。我们将在本书中讨论 Linux 内核的定制和配置。在本章中,我们将重点关注 init 进程,并了解它在 Android 系统中的工作方式。

分析 init 进程和 ramdisk

init 进程的实现可以在$AOSP/system/core/init目录中找到。如果我们查看init.cpp中的main函数,它包括ueventdwatchdogd的代码,如下面的代码片段所示:

int main(int argc, char** argv) { 
    if (!strcmp(basename(argv[0]), "ueventd")) { 
        return ueventd_main(argc, argv); 
    } 

    if (!strcmp(basename(argv[0]), "watchdogd")) { 
        return watchdogd_main(argc, argv); 
    } 
    ... 

我们不会讨论ueventdwatchdogd,因为它们与我们的主题无关。我们将专注于init.cpp的主线代码。init 的主线代码实现了以下逻辑:

  1. 环境准备,例如创建系统文件夹、设置标准 I/O、初始化日志系统等。环境设置还包括 SELinux 设置和加载 SELinux 策略。

  2. 解析 init 脚本init.rcinit.${ro.hardware}.rc等。将 init 脚本中的项目添加到action_listservice_list中的动作或服务。

  3. action_list中执行early-init动作。

  4. action_list中执行init动作。

  5. action_list中执行late-init动作。

  6. 进入一个无限循环以执行以下任务:

  7. action_queue中执行动作。

  8. service_list中重启标记为SVC_RESTARTING的服务。

  9. 提供属性服务,处理/dev/keychord事件。

  10. 监控系统属性变化、信号和键盘事件。

init 脚本存储在 ramdisk 中,并在引导过程中由引导加载程序加载到内存中。如果我们查看 x86emu 的ramdisk.img内容,我们将看到以下文件:

init 脚本定义了两种类型的元素:动作服务。init 进程解析所有脚本并根据元素的类型运行任务。

动作

动作语法如下:

on <trigger> 
    <command> 
    <command> 
    <command> 
    ... 

动作以关键字on开头,后跟一个触发器。动作左对齐,后续的命令缩进,如前文所示片段。

例如,我们使用fstab.goldfish在触发fs时挂载所有模拟器的分区:

on fs 
        mount_all /fstab.goldfish 

触发器是用于匹配某些类型事件的字符串,并且它们用于触发动作的发生。有两种类型的动作触发器:预定义触发器属性值变化激活的触发器

预定义触发器可以是early-initinitearly-fsfspost-fsearly-bootboot,如 init 脚本中定义的那样。

属性值触发器具有以下形式:

<name>=<value> 

<name>属性设置为特定值<value>时,这种形式的触发器会发生。

例如,当sys.init_log_level属性更改时,我们需要按照以下方式重置日志级别:

on property:sys.init_log_level=* 
    loglevel ${sys.init_log_level} 

初始化脚本中的命令重新组装了 shell 命令,并添加了 init 特定的命令。

服务

服务是 init 启动的程序,并在它们退出时(可选地)重新启动。服务的形式如下:

service <name> <pathname> [ <argument> ]* 
   <option> 
   <option> 
   ... 

服务将以<name>的形式被 init 所知。指向<pathname>的实际二进制文件名将不会被识别。

选项是服务的修饰符。它们影响 init 如何以及何时运行服务。我们可以使用以下 goldfish 特定的服务作为例子:

service goldfish-setup /system/etc/init.goldfish.sh 
    user root 
    group root 
    oneshot 

服务的名称是goldfish-setup,它以 root 用户身份运行init.goldfish.sh脚本。oneshot选项意味着当它退出时,此服务不会重新启动。

完整的init命令和service选项列表可以在以下文件中找到:

$AOSP/system/core/init/readme.txt

设备特定的动作和服务

系统生成的初始化脚本的源代码位于$AOSP/system/core/rootdir文件夹中。它们在构建过程中被复制到$OUT/root

初始化进程首先解析init.rc脚本。所有其他脚本都由init.rc导入,然后由初始化进程解析。如果我们查看以下init.rc代码片段,我们可以看到有几个脚本是由init.rc导入的:

# Copyright (C) 2012 The Android Open Source Project 
# 
# IMPORTANT: Do not create world writable files or directories. 
# This is a common source of Android security bugs. 
# 

import /init.environ.rc 
import /init.usb.rc 
import /init.${ro.hardware}.rc 
import /init.usb.configfs.rc 
import /init.${ro.zygote}.rc 
import /init.trace.rc 

on early-init 
... 

init.${ro.hardware}.rc脚本是可以用来为设备特定更改进行定制的脚本。ro.hardware属性在运行时传递给 init,以便 init 可以加载适合设备的正确版本。我们应该尽量避免更改其他 init 脚本,并将设备特定的更改仅保留在init.${ro.hardware}.rc中。

如果我们具体查看 goldfish 或 ranchu 设备,它们分别有init.goldfish.rcinit.ranchu.rc脚本。这两个脚本都是 goldfish 设备的一部分,可以在$AOSP/device/generic/goldfish中找到,如下面的代码片段所示。它们在构建过程中被复制到$OUT/root

$ ls device/generic/goldfish
audio           fstab.ranchu      libqemu  qemu-props
camera          gps               lights   sensors
data            init.goldfish.rc  opengl   ueventd.goldfish.rc
fingerprint     init.goldfish.sh  power    ueventd.ranchu.rc
fstab.goldfish  init.ranchu.rc    qemud    vibrator  

init.goldfish.rcinit.ranchu.rc内部,定义了一个goldfish-setup服务,如下所示:

service goldfish-setup /system/etc/init.goldfish.sh 
    user root 
    group root 
    oneshot 

在上一章中,我们在init.goldfish.sh脚本中添加了 Houdini 初始化,这就是 Houdini 在启动过程中初始化的方式。

在 Android 模拟器中,硬件名称是通过内核命令行传递的。当你以-verbose-show-kernel选项启动模拟器时,你将在控制台看到以下命令行参数:

...
emulator: argv[08] = "-append"
emulator: argv[09] = "qemu=1 clocksource=pit androidboot.console=ttyGF2 android.checkjni=1 console=ttyS0,38400 **androidboot.hardware=ranchu** qemu.gles=1 android.qemud=1"
...  

这些参数作为内核命令行参数传递给内核,然后由 init 用来决定硬件名称。由于我们无法在模拟器中更改内核参数,因此我们无法在我们的设备中使用自己的脚本,如init.x86emu.rc。如果我们想自定义启动顺序,我们应该更改$AOSP/device/generic/goldfish中的代码,这正是我们在上一章所做的事情。

定制启动序列的理想方法是将所有定制内容放在我们自己的 device 文件夹下,例如 $AOSP/device/generic/x86emu。在这种情况下,我们可以非常容易地升级到新的 Android 版本。我们更改的 AOSP 代码越通用,迁移到新 Android 版本就越困难。

如果我们能控制引导加载程序,我们就可以通过引导加载程序传递自己的内核参数。我们将在第八章 在 VirtualBox 上创建自己的设备 中看到这一点,直到第十一章 启用 VirtualBox 特定硬件接口

如果你真的需要更改 init.rc 以完全定制启动序列,你可以在你的 BoardConfig.mk 中定义 TARGET_PROVIDES_INIT_RC := true 变量。有了这个定义,你就可以将 init.rc 复制到你的 device 文件夹中,并为你自己的设备进行更改。

源代码和清单文件更改

现在我们已经介绍了 Android 启动过程,接下来我们将把 Android-x86 项目的双阶段启动过程应用到 Android 模拟器中。在讨论双阶段启动过程之前,让我们先看看 AOSP 源代码和清单文件的变化。

如果我们查看我们将用于本章的以下清单文件,我们可以看到我们只更改了 kernelx86emu 设备和来自 Android-x86 项目的 newinstaller

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="github" 
           revision="refs/tags/android-7.1.1_r4_x86emu_ch06_r1" 
           fetch="." /> 

  <remote  name="aosp" 
           fetch="https://android.googlesource.com/" /> 
  <default revision="refs/tags/android-7.1.1_r4" 
           remote="aosp" 
           sync-c="true" 
           sync-j="1" /> 

  <!-- github/shugaoye --> 
 <project path="kernel" name="goldfish" remote="github" /> <project path="device/generic/x86emu" name="x86emu" 
  remote="github" /> <project path="bootable/newinstaller"  
  name="platform_bootable_newinstaller" remote="github" /> 

  <!-- aosp --> 
  <project path="build" name="platform/build" groups="pdk,tradefed" > 
    <copyfile src="img/root.mk" dest="Makefile" /> 
  </project> 
... 
</manifest> 

使用 newinstaller 项目,我们将构建另一个 ramdisk 映像,initrd.img,它将在双阶段启动过程中使用。

使用 Git 标签 android-7.1.1_r4_x86emu_ch06_r1 作为本章源代码更改的基准。

Android-x86 启动过程

在 第一章 Android 系统编程简介 中,我们介绍了 Android-x86 项目,这是一个开源项目,为英特尔设备提供 Android 板级支持包BSP)。它使用类似于微软 Windows 或桌面 Linux 分发的通用媒体来启动各种英特尔设备。

为了实现使用一种介质启动所有设备的目标,它将启动序列分为两个阶段。第一阶段是启动一个最小的嵌入式 Linux 环境,以启用硬件设备。在第二阶段,它通过 chrootswitch_root 切换到 Android 系统。启动过程的第二阶段与我们之前讨论的相同。让我们详细看看 Android-x86 的启动过程的第一阶段。我们将在本章中重用它来模拟 Android 模拟器。这种方法可以帮助简化启动过程,同时也有助于我们调试启动过程。

使用 initrd.img 的第一阶段启动

Android-x86 的启动过程第一阶段使用特定的 ramdisk initrd.img。源代码可以在 $AOSP/bootable/newinstaller 找到。该项目是从 Android-x86 项目复制的。因为它托管在 GitHub 上,我可以对其进行自己的修改:

$ ls -1 -F
Android.mk
boot/
editdisklbl/
initrd/
install/ 

如果我们查看 newinstaller 文件夹中的内容,我们可以看到前面的文件夹和文件。以下是对 newinstaller 内容的解释:

  • boot: 这是安装媒体的引导加载程序。Android-x86 的镜像可以构建成不同的格式(ISO、UEFI 等)

  • editdisklbl: 用于编辑系统镜像分区的宿主工具

  • initrd: 第一阶段引导的 ramdisk

  • install : Android-x86 的安装程序

  • Android.mk : newinstaller 的 Android Makefile

如果我们构建 newinstaller,它可以生成几种不同的镜像格式,如 ISO、USB 或 UEFI。在设置好环境并选择构建目标后,可以运行以下命令来构建指定的镜像:

$ make iso_img/usb_img/efi_img  

除了安装镜像,它还会生成另外两个镜像,initrd.imginstall.img

  • initrd.img : 第一阶段引导的 ramdisk 镜像

  • install.img : 包含 Android-x86 安装程序的镜像

我们将详细了解 initrd.imginstall.img,以了解 Android-x86 中第一阶段引导的工作原理。

initrd.img

如果我们查看 initrd 文件夹,我们可以看到以下内容:

$ cd bootable/newinstaller/initrd 
$ ls -1F 
bin/ 
init* 
lib/ 
sbin/ 
scripts/ 

initrd.img 的内容由基于 busybox 的最小 Linux 环境组成。我们可以在 bin/busybox 中找到 busybox,以及 busybox 所需的共享库在 lib/ 中。initrd 文件夹内有一个可执行的 init 文件和一些文件夹。我们知道,init 进程是系统启动时内核调用的第一个进程。Android-x86 提供了一个单独的 init 进程来在 initrd.img 内启动系统。这个版本的 init 实际上是一个 shell 脚本而不是二进制可执行文件:

这个 shell 脚本将执行前面图示中的任务:

  1. 当内核调用脚本时,它首先准备环境。这包括控制台 tty 设置、调试日志的初始化和调试级别。

  2. 环境准备就绪后,它将尝试在存储设备上找到现有的 Android 系统或安装媒体。在这一步,必须找到 ramdisk.img,否则将返回错误。

  3. 一旦找到 Android 系统或安装媒体,它将 ramdisk.img 提取到工作文件夹 /android。如果设置了 INSTALL 变量,它还将 install.img 提取到文件系统根目录。工作文件夹 /android 用作 Android 系统的根目录,而当前根目录是 initrd.img 的镜像。

  4. 现在,它将加载所有额外的脚本以准备下一步。如果环境变量 INSTALL 设置为 1,它将调用安装脚本将 Android-x86 安装到硬盘等存储设备。

  5. 在切换到 Android 系统之前,它将加载所有设备的内核模块,挂载数据和 SD 卡分区,设置触摸屏和显示 DPI 等。

  6. 一切准备就绪后,它将使用 /android 作为新的根目录切换到 Android 系统,并在新的根目录下调用 /init。从这一点开始,Android 系统将被启动。

让我们看看脚本中的一些重要代码片段,以真正了解其感觉:

#!/bin/busybox sh 
# 
# By Chih-Wei Huang <cwhuang@linux.org.tw> 
# and Thorsten Glaser <tg@mirbsd.org> 
# 
# Last updated 2015/10/23 
# 
# License: GNU Public License 
# We explicitely grant the right to use the scripts 
# with Android-x86 project. 
# 

PATH=/sbin:/bin:/system/bin:/system/xbin; export PATH 
... 
echo -n Detecting Android-x86... 
... 
while :; do 
   for device in ${ROOT:-/dev/[hmsv][dmr][0-9a-z]*}; do 
 check_root $device && break 2 
         mountpoint -q /mnt && umount /mnt 
   done 
   sleep 1 
   echo -n . 
done 
... 

在前面的代码片段中,我们可以看到它通过无限循环调用 shell 函数 check_root 来寻找 Android 系统的根。如果它找不到根文件系统,它将卡在这个循环中。

在下面的 check_root 函数中,环境变量 SRC 从内核命令行传递,并指定文件系统根路径。它将检查在这个路径中是否可以找到 ramdisk.img。如果在这个路径中找到了 ramdisk.img,它将被提取到 /android 路径,即当前目录,否则;它将返回错误:

... 
check_root() 
{ 
... 
   if [ -n "$iso" -a -e /mnt/$iso ]; then 
         mount --move /mnt /iso 
         mkdir /mnt/iso 
         mount -o loop /iso/$iso /mnt/iso 
         SRC=iso 
 elif [ ! -e /mnt/$SRC/ramdisk.img ]; then return 1 fi zcat /mnt/$SRC/ramdisk.img | cpio -id > /dev/null 
... 

在检测到根文件系统后,它将检查环境变量 INSTALL。这个 INSTALL 变量也是从内核命令行传递的。如果设置了 INSTALL,它将提取 install.img 到当前根目录。这将覆盖 initrd.img 中的某些文件,我们将在稍后详细讨论这一点:

... 
if [ -n "$INSTALL" ]; then 
 zcat /src/install.img | ( cd /; cpio -iud > /dev/null ) 
fi 
... 

然后,它将从 /scripts/src/scripts 文件夹加载所有其他 shell 脚本:

... 
# load scripts 
for s in `ls /scripts/* /src/scripts/*`; do 
   test -e "$s" && source $s 
done 
... 

一旦所有 shell 脚本都加载到内存中,它将再次检查 INSTALL 变量,以查看是否应该执行安装脚本:

... 
[ -n "$INSTALL" ] && do_install 

load_modules 
mount_data 
mount_sdcard 
setup_tslib 
setup_dpi 
post_detect 
... 
exec ${SWITCH:-switch_root} /android /init 

# avoid kernel panic 
while :; do 
   echo 
   echo '      Android-x86 console shell. Use only in emergencies.' 
   echo 
   debug_shell fatal-err 
done 

无论是否执行安装脚本,它都会为 Android 系统的启动准备环境。它将加载内核模块,挂载数据/SD 卡分区,并设置所有其他环境相关要求。最后,它将执行 switch_rootchroot 以切换到 Android 系统。从这一点开始,Android 系统将被启动。

switch_rootchroot 之间的主要区别在于,switch_root 的目的是将整个系统切换到新的根目录,并移除对旧目录的依赖,这样你就可以卸载原始根目录并继续使用,就像它从未被使用过一样。

chroot 的目的是应用于单个进程的生命周期,其余的系统继续在旧的根目录中运行,当 chrooted 进程退出时,系统保持不变。

在 Android-x86 中,switch_root 用于发布模式,而 chroot 用于调试模式。

install.img 内部

我们已经分析了 Android-x86 的大部分第一阶段启动过程。我们还想进行更多分析的是install.img在第一阶段启动过程中的工作方式。

如果设置了INSTALL环境变量,install.img将被提取。这将覆盖initrd.img中的一些内容。现在让我们看看这个。如果我们列出initrdinstall两个目录的内容,我们可以看到以下截图中的bin/lib/sbin/scripts/在两个镜像中都存在重复:

图片

bin/sbin/lib/文件夹中,有诸如cfdiskcgdiskmkntfsgrub等工具。这些工具用于分区硬盘、格式化额外的文件系统等。

scripts/文件夹包括安装脚本,我们将查看scripts/以探索 Android-x86 安装的工作原理。

如果我们查看initrdinstall文件夹中的脚本文件,我们会发现两者都包含一个1-install脚本。initrd.img在第一阶段引导时用作根文件系统。如果设置了INSTALL变量,install.img也将被提取到根目录。在这种情况下,install文件夹中的脚本将覆盖initrd文件夹中的脚本。我们可以从以下图中看到initrd.imgramdisk.imginstall.img是如何集成以形成第一阶段和第二阶段文件系统的:

图片

如果我们查看initrd/scripts文件夹下的1-install,我们将看到以下 shell 脚本函数:

do_install() 
{ 
   error -e 'n  Android-x86 installer is not available.\n  
   Press RETURN to run live version.\n' 
   read 
   cd /android 
} 

它实现了一个do_install函数,该函数将返回错误信息。如果此脚本没有被install.img中的脚本覆盖,这意味着安装程序不可用。如果提取了install.img,则将调用真正的do_install函数以启动安装:

do_install() 
{ 
  until install_hd; do 
    if [ $retval -eq 255 ]; then 
      dialog --title ' Error! ' --yes-label Retry --no-label Reboot  
      --yesno 'nInstallation failed! Please check if you have enough 
      free disk space to install Android-x86.' 8 51 
       [ $? -eq 1 ] && rebooting 
    fi 
  done 

  [ -n "$VESA" ] || runit="Run Android-x86" 
... 
} 

do_install函数将调用另一个函数install_hd,而install_hd将调用install_to函数以执行实际安装。install_to函数接受一个参数,即安装的目标设备。它将执行以下安装任务:

  • 它将首先格式化目标设备,然后将设备挂载到/hd文件夹。

  • 它将安装 GRUB 作为引导加载程序。

  • 它将在/hd文件夹中使用android-$VER命名约定创建一个文件夹,作为目标安装文件夹。例如,由于我们的设备是 x86emu,安装目标将是/hd/android-x86emu

  • 它将使用cpio命令将文件从安装介质复制到安装目标。这些文件包括kernelinitrd.imgramdisk.img以及来自 AOSP 构建的system文件夹下的所有内容。它取决于配置;它可能复制system.sfssystem.img镜像文件,或者它可能直接将system文件夹中的所有内容复制到/hd/android-$VER/system

在接下来的章节中,我们需要重复安装过程以创建一个可用于 Android-x86 双阶段引导序列的文件系统。

使用 initrd.img 构建 x86emu

在我们对 Android-x86 的 initrd.img 进行了所有分析之后,我们现在可以为 Android 模拟器构建一个类似的镜像。请注意,这只能在 ranchu 上工作,而不能在 goldfish 上工作。goldfish 模拟器使用较旧的 QEMU 版本,并且不支持模拟器的附加存储设备。为了支持从 initrd.img 启动,我们必须更改文件系统的布局。在 AOSP 中更改原始文件系统镜像并不是一个好的选择。我们将创建另一个用于与 initrd.img 启动的文件镜像。

在 ranchu 模拟器中,镜像被模拟为 virtio 块设备。在我们启动模拟器后,我们可以检查挂载点,如下面的屏幕截图所示。我们可以看到 system.img 被挂载为 /dev/block/vdauserdata.img/dev/block/vdb,而 cache.img/dev/block/vdc

图片

ranchu 镜像模拟为 virtio 块设备

在 ranchu 模拟器中,所有分区都使用 fstab.ranchu 文件挂载,如下面的代码片段所示:

... 
/dev/block/vda  /system  ext4      ro                 wait 
/dev/block/vdb  /cache   ext4      noatime,nosuid,nodev,nomblk_io_submit,errors=panic    wait 
/dev/block/vdc  /data    ext4      noatime,nosuid,nodev,nomblk_io_submit,errors=panic    wait 
... 

使用 ranchu 模拟器,我们可以轻松地通过 -hda QEMU 选项添加另一个存储设备。使用此选项,我们可以看到在模拟器启动后,一个新的块设备 /dev/block/sda 可用。我们将在稍后详细讨论这个问题。在我们能够测试这个想法之前,我们需要首先创建磁盘镜像。

创建文件系统镜像

我们可以创建磁盘镜像的方法有很多。QEMU 支持许多磁盘镜像格式。如果您想查找 QEMU 可以支持哪些图像格式的详细信息,可以使用以下 Linux 命令进行查看:

$ man qemu-img  

支持的图像格式有:

  • raw:这种简单的磁盘镜像格式具有简单且易于导出到所有其他模拟器的优势。

  • qcow2:这是 QEMU 镜像格式,是最通用的格式。它是一种压缩镜像格式,因此具有更小的镜像大小,并且可以支持快照。

  • qcow:这是旧的 QEMU 镜像格式。

  • cow:这是 User Mode Linux Copy-On-Write 镜像格式。

  • vdi:这是与 VirtualBox 1.1 兼容的镜像格式。

  • vmdk:这是 VMware 3 和 4 兼容的镜像格式。

  • vpc:这是与 VirtualPC 兼容的镜像格式(VHD)。

  • cloop:这是 Linux 压缩循环镜像,仅适用于直接重用现有的压缩 CD-ROM 镜像,例如 Knoppix CD-ROM。

我们将使用 qcow2 文件格式来测试 Android 模拟器的 initrd.img。为了创建 qcow2 格式的文件镜像,我们需要在 bootable/newinstallerAndroid.mk Makefile 中添加以下代码:

... 
initrd:  $(BUILT_IMG) 

X86EMU_EXTRA_SIZE := 100000000 
X86EMU_DISK_SIZE := $(shell echo ${BOARD_SYSTEMIMAGE_PARTITION_SIZE}+${X86EMU_EXTRA_SIZE} | bc) 
X86EMU_TMP := x86emu_tmp 

qcow2_img: $(BUILT_IMG) 
  mkdir -p $(PRODUCT_OUT)/${X86EMU_TMP}/${TARGET_PRODUCT} 
  cd $(PRODUCT_OUT)/${X86EMU_TMP}/${TARGET_PRODUCT}; mkdir data 
  mv $(PRODUCT_OUT)/initrd.img $(PRODUCT_OUT)/${X86EMU_TMP}/${TARGET_PRODUCT} 
  mv $(PRODUCT_OUT)/install.img $(PRODUCT_OUT)/${X86EMU_TMP}/${TARGET_PRODUCT} 
  mv $(PRODUCT_OUT)/ramdisk.img $(PRODUCT_OUT)/${X86EMU_TMP}/${TARGET_PRODUCT} 
  mv $(PRODUCT_OUT)/system.img $(PRODUCT_OUT)/${X86EMU_TMP}/${TARGET_PRODUCT} 
  make_ext4fs -T -1 -l $(X86EMU_DISK_SIZE) $(PRODUCT_OUT)/${TARGET_PRODUCT}.img $(PRODUCT_OUT)/${X86EMU_TMP}  
  mv $(PRODUCT_OUT)/${X86EMU_TMP}/${TARGET_PRODUCT}/*.img $(PRODUCT_OUT)/ 
  qemu-img convert -c -f raw -O qcow2 $(PRODUCT_OUT)/${TARGET_PRODUCT}.img $(PRODUCT_OUT)/${TARGET_PRODUCT}-qcow2.img 
  cd $(PRODUCT_OUT); qemu-img create -f qcow2 -b 
  ./${TARGET_PRODUCT}-qcow2.img ./${TARGET_PRODUCT}.img 
... 

在前面的 Makefile 中,我们必须做的第一件事是创建一个目录布局,该布局可以被 initrd.img 使用,如下面的代码片段所示:

图片

x86emu_x86.img 的目录布局

我们创建一个 data 文件夹作为数据存储使用。然后,我们将 AOSP 输出文件夹中的现有镜像文件移动到 $OUT/x86emu_tmp/x86emu_x86 目录,以创建前面的目录结构。这些文件镜像将在文件镜像生成后移回。

一旦我们有了正确的目录结构,我们就可以使用 make_ext4fs 命令创建具有以下选项的原始文件系统镜像:

make_ext4fs -T {timestamp} -l {size of file system} {image file name} {source directory} {target out directory}  

文件系统的尺寸是 BOARD_SYSTEMIMAGE_PARTITION_SIZE;此外,X86EMU_EXTRA_SIZEBOARD_SYSTEMIMAGE_PARTITION_SIZE 定义在系统镜像大小的板级配置文件中。X86EMU_EXTRA_SIZE 是用于 ramdisk 和内核镜像的空间。

下一步是使用 qemu-img 命令将原始文件镜像转换为 qcow2 格式。原始和 qcow2 格式的镜像都可以由模拟器使用,但原始文件镜像比 qcow2 镜像大得多。

由于 qcow2 镜像支持快照功能,我们也可以根据 qcow2 镜像(x86emu_x86-qcow2.img)生成快照镜像(x86emu_x86.img)。如果我们使用快照镜像,我们可以在任何时候恢复到原始的 qcow2 镜像。可以使用以下命令创建快照镜像:

$ cd $OUT
$ qemu-img create -f qcow2 -b ./x86emu_x86-qcow2.img ./x86emu_x86.img  

镜像生成后,我们可以使用以下 qemu-img 命令检查它:

$ qemu-img info x86emu_x86.img
image: x86emu_x86.img
file format: qcow2
virtual size: 1.3G (1442177024 bytes)
disk size: 196K
cluster_size: 65536
backing file: ./x86emu_x86-qcow2.img
Format specific information:
 compat: 1.1
 lazy refcounts: false 

我们可以看到 x86emu_x86.img 镜像是 x86emu_x86-qcow2.img 的快照镜像。

在我们刚刚创建的镜像中,没有创建分区。当我们将其挂载到 Android 模拟器中时,它将显示为 /dev/sda/dev/block/sda 设备。如果我们想为镜像文件创建分区,我们需要使用 edit_mbr 工具来完成。您可以自行探索这个选项。使用多个分区,我们可以将系统、数据和缓存放入不同的分区,这更接近大多数移动设备的磁盘布局。

内核更改

从 Android 4.4 开始,SELinux 默认启用。当我们更改 Android 中的文件系统时,我们还需要注意 SELinux 设置。这将使配置比我们预期的更复杂。如果您对此感兴趣,您可以为此情况配置 SELinux,并完成您的作业。

在这本书中,我们将默认禁用 SELinux,以便我们可以专注于我们的主题。要禁用 SELinux,我们必须对内核配置文件进行一些更改。您可以使用以下 git 命令检查更改:

$ cd $AOSP/kernel
$ git branch
* android-x86emu-3.10
$ gitk  

我们可以使用 gitk 查看在 android-x86emu-3.10 分支中的更改,如下所示截图。我们可以看到我们设置了默认的安全策略为 DAC,并移除了 SELinux 设置,CONFIG_SECURITY_SELINUX=y

在 ranchu 内核中禁用 SELinux

在 Android 模拟器上启动磁盘镜像

一旦我们完成了所有更改,我们可以使用以下命令构建 qcow2 镜像:

$ make qcow2_img USE_SQUASHFS=0
...
make_ext4fs -T -1 -S out/target/product/x86emu/root/file_contexts -L 
system -l 1342177280 -a system out/target/product/x86emu/obj/PACKAGING/systemimage_intermediates/system.img out/target/product/x86emu/system out/target/product/x86emu/system
Creating filesystem with parameters:
 Size: 1342177280
 Block size: 4096
 Blocks per group: 32768
 Inodes per group: 8192
 Inode size: 256
 Journal blocks: 5120
 Label: system
 Blocks: 327680
 Block groups: 10
 Reserved block group size: 79
Created filesystem with 2122/81920 inodes and 178910/327680 blocks
Install system fs image: out/target/product/x86emu/system.img  

如前述命令行输出所示,system.img将按常规构建。之后,将创建 ramdisk 镜像initrd.img,如下所示。请注意VER环境变量。我们将脚本更改为将其设置为x86emu。Android-x86 中的原始版本是当前日期,例如 2016-11-11:

VER ?= $(shell date +"%F") 

此变量用作安装文件夹名称的一部分。让我们继续审查构建日志:

out/target/product/x86emu/system.img+ maxsize=1370278272 blocksize=2112 total=1342177280 reserve=13842048
rm -rf out/target/product/x86emu/installer
out/host/linux-x86/bin/acp -pr bootable/newinstaller/initrd out/target/product/x86emu/installer
ln -s /bin/ld-linux.so.2 out/target/product/x86emu/installer/lib
mkdir -p out/target/product/x86emu/installer/android out/target/product/x86emu/installer/iso out/target/product/x86emu/installer/mnt out/target/product/x86emu/installer/proc out/target/product/x86emu/installer/sys out/target/product/x86emu/installer/tmp out/target/product/x86emu/installer/sfs out/target/product/x86emu/installer/hd
echo "VER=x86emu" > out/target/product/x86emu/installer/scripts/00-ver
out/host/linux-x86/bin/mkbootfs out/target/product/x86emu/installer | gzip -9 > out/target/product/x86emu/initrd.img  

在创建 ramdisk initrd.img之后,将根据我们在Android.mk文件中为bootable/newinstaller添加的内容创建原始和 qcow2 文件镜像:

mkdir -p out/target/product/x86emu/x86emu_tmp/x86emu_x86
cd out/target/product/x86emu/x86emu_tmp/x86emu_x86; mkdir data
mv out/target/product/x86emu/initrd.img out/target/product/x86emu/x86emu_tmp/x86emu_x86
mv out/target/product/x86emu/install.img out/target/product/x86emu/x86emu_tmp/x86emu_x86
mv out/target/product/x86emu/ramdisk.img out/target/product/x86emu/x86emu_tmp/x86emu_x86
mv out/target/product/x86emu/system.img out/target/product/x86emu/x86emu_tmp/x86emu_x86
make_ext4fs -T -1 -l 1442177280 out/target/product/x86emu/x86emu_x86.img out/target/product/x86emu/x86emu_tmp out/target/product/x86emu/x86emu_tmp
Creating filesystem with parameters:
 Size: 1442177024
 Block size: 4096
 Blocks per group: 32768
 Inodes per group: 8016
 Inode size: 256
 Journal blocks: 5501
 Label: 
 Blocks: 352094
 Block groups: 11
 Reserved block group size: 87
Created filesystem with 17/88176 inodes and 340722/352094 blocks
mv out/target/product/x86emu/x86emu_tmp/x86emu_x86/*.img out/target/product/x86emu/
qemu-img convert -c -f raw -O qcow2 out/target/product/x86emu/x86emu_x86.img out/target/product/x86emu/x86emu_x86-qcow2.img
cd out/target/product/x86emu; qemu-img create -f qcow2 -b ./x86emu_x86-qcow2.img ./x86emu_x86.img
Formatting './x86emu_x86.img', fmt=qcow2 size=1442177024 backing_file='./x86emu_x86-qcow2.img' encryption=off cluster_size=65536 lazy_refcounts=off  

我们现在有x86emu_x86-qcow2.img qcow2 镜像和x86emu_x86.img快照镜像。为了测试这些镜像,我们可以使用一个 shell 脚本来帮助我们。这个 shell 脚本可以从以下 GitHub URL 下载:

github.com/shugaoye/asp-sample/blob/master/scripts/test-initrd.sh

要运行此脚本,您应该首先设置您的 SDK 环境,这样我们就可以在$PATH环境变量中找到模拟器:

#!/bin/sh 

if [ -z "$1" ]; then 
  EMULATOR1=emulator 
else 
  EMULATOR1="/opt/VirtualGL/bin/vglrun emulator" 
fi 

if [ -z "$OUT" ]; then 
  IMG_ROOT=. 
else 
  IMG_ROOT=$OUT 
fi 

$EMULATOR1 @a23x86 -verbose -show-kernel -shell -system $IMG_ROOT/system.img -ramdisk $IMG_ROOT/initrd.img -initdata $IMG_ROOT/userdata.img -kernel $IMG_ROOT/kernel -qemu -append "qemu=1 clocksource=pit android.checkjni=1 DEBUG=2 console=ttyS0,11520 androidboot.hardware=ranchu qemu.gles=1 android.qemud=1 root=/dev/sda SRC=x86emu_x86" -hda $IMG_ROOT/x86emu_x86.img 

要启动此脚本,您可以直接使用 AOSP 构建结果,或者您可以从以下 SourceForge URL 下载镜像:

sourceforge.net/projects/android-system-programming/files/android-7/ch06/ch06.zip/download

如果您使用 AOSP 构建结果,脚本将使用$OUT环境变量来查找镜像。如果$OUT环境变量未设置,它将假定镜像存储在当前目录中。

要在远程X窗口会话中运行 Android 模拟器,我们需要使用 VirtualGL 来支持 OpenGL ES。无论使用哪个命令行参数,脚本都会使用 VirtualGL 启动模拟器。如果你使用带有本地X窗口会话的 Linux 机器,你不需要这样做。

要使用initrd.img作为 ramdisk,我们可以看到在模拟器命令行中我们指定了initrd.img-ramdisk选项中。接下来我们需要注意的点是 QEMU 选项。我们可以在-qemu Android 模拟器选项之后指定 QEMU 选项。我们使用两个 QEMU 选项,-append-hda。使用-hda选项,我们可以将x86emu_x86-qcow2.img镜像或x86emu_x86.img快照镜像作为模拟器的另一个硬盘。使用-append选项,我们可以提供我们想要传递给 ranchu 内核的内核参数。所有其他内核参数与模拟器提供的相同,除了以下参数:

  • DEBUG=2:此选项将调试级别设置为2,这样我们就可以在启动时获取调试控制台

  • root=/dev/sda:此选项指定根设备为/dev/sda,这是我们作为 QEMU 选项提供的x86emu_x86-qcow2.img镜像或x86emu_x86.img快照镜像

  • SRC=x86emu_x86:此选项定义了 init 可以在根设备上使用的文件夹名称,以查找所有镜像

您可以从命令行启动脚本,您将看到以下屏幕输出:

$ test-initrd.sh
...
(debug-found)@android:/android # mount
rootfs on / type rootfs (rw)
proc on /proc type proc (rw,relatime)
sys on /sys type sysfs (rw,relatime)
tmpfs on /android type tmpfs (rw,relatime)
/dev/block/sda on /mnt type ext4 (rw,relatime,data=ordered)
/dev/loop0 on /android/system type ext4 (rw,relatime,data=ordered)
(debug-found)@android:/android # losetup -a 
/dev/loop0: 0 /mnt/x86emu_x86/system.img  

在命令行日志和下面的屏幕截图中,你可以看到 /dev/sda 根设备已被找到并挂载到 /mnt。Android 系统镜像被挂载为循环设备到 /dev/loop0

图片

initrd.img 的调试控制台

退出 shell 控制台后,Android 系统将像往常一样启动。使用这种方法,您可以在需要调试任何问题时获得调试控制台。您还可以即时更改任何 Android 启动脚本,而无需重新构建新镜像进行测试。这种设置中的所有灵活性都将极大地帮助调试启动过程。

摘要

在本章中,我们学习了 Android 系统的启动过程。之后,我们深入探讨了 Android-x86 的启动过程。我们发现了一种新的启动系统的方法,首先将系统启动到最小 Linux 环境,然后使用该环境启动 Android 系统。在这个过程中,我们可以通过获取 shell 控制台来获得控制权,以便在特定点检查系统。为了支持这种启动方式,我们学习了如何构建可以与 initrd.img 一起使用的系统镜像。

在下一章中,我们将继续探讨如何通过添加 Wi-Fi 连接来自定义 Android 模拟器。

第七章:在 Android 模拟器上启用 Wi-Fi

在过去的三章中,我们探讨了自定义和扩展 Android 模拟器的方法。在本章中,我们将继续探讨这个主题,以在 Android 模拟器中添加 Wi-Fi 支持。如果你是使用 Android 模拟器的开发者,你可能注意到 Android 模拟器中只有数据连接。一些应用程序可能知道连接类型,并根据连接类型表现出不同的行为。在这种情况下,你不能使用模拟器来测试你的应用程序。在本章中,我们将涵盖以下主题:

  • 介绍 Android 中的 Wi-Fi 架构

  • 扩展 x86emu 设备以支持 Wi-Fi 连接

  • 在 x86emu 设备上测试 Wi-Fi 连接

本章的主题处于高级水平。我们将在本章的开始分析 Wi-Fi 源代码,以帮助理解 Wi-Fi 架构。我建议你打开源代码编辑器并定位讨论中的函数。这是理解本章源代码分析部分的一种非常有效的方法。

Android 上的 Wi-Fi

在第三章“发现内核、HAL 和虚拟硬件”中,我们讨论了与 Android 系统相关的端口层,我们以 goldfish lights 为例来描述从应用程序到 HAL 访问硬件的调用序列。在本章中,我们将采用类似的方法来探索 Android 的 Wi-Fi 架构。基于我们对 Wi-Fi 架构的理解,我们将在本章的后面部分将 Wi-Fi 添加到模拟器中。

Wi-Fi 架构

如我们从前面的章节所知,Android 应用程序使用管理器来访问系统服务。管理器将使用各种系统服务来访问硬件抽象层HAL)。Wi-Fi 架构也遵循相同的方法,让应用程序访问 Wi-Fi 硬件。

Android Wi-Fi 架构

如我们从前面显示 Android 系统中 Wi-Fi 层的图所示,WifiSettings是默认 AOSP 构建中用于控制 Wi-Fi 连接的应用程序。WifiSettings使用WifiManager来获取访问 Wi-Fi 服务。

WifiManager提供以下功能:

  • 提供配置网络的列表--可以修改单个条目的属性。

  • 监控当前活动的 Wi-Fi 网络(如果有)。可以建立或断开连接,并查询网络状态的动态信息。

  • 提供接入点扫描的结果,包含足够的信息来决定连接哪个接入点。

  • 定义在 Wi-Fi 状态发生任何变化时广播的各种 intent 动作的名称。

WifiManager被创建时,它获得IWifiManager的接口,如下面的代码片段所示。该接口通过 binder 机制由WifiService实现:

public WifiManager(Context context, IWifiManager service, Looper looper) { 
    mContext = context; 
    mService = service; 
    mLooper = looper; 
    mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion; 
} 

WifiManager 定义在 $AOSP/frameworks/base/wifi/java/android/net/wifi/WifiManager.java 文件中。

WifiService 的实现中,它使用 WifiStateMachine 来管理 Wi-Fi 状态:

public final class WifiServiceImpl extends IWifiManager.Stub { 
    private static final String TAG = "WifiService"; 
    private static final boolean DBG = true; 
    private static final boolean VDBG = false; 

    final WifiStateMachine mWifiStateMachine; 
      private final Context mContext; 
... 

WifiServiceImpl 定义在 $AOSP/frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiServiceImpl.java 文件中。

我们可以通过以下序列图查看 Wi-Fi HAL 的初始化过程:

xref.opersys.com/ 有一个非常好的 Android 源代码交叉引用工具。

您可以使用这个交叉引用工具搜索函数定义并定位源代码位置。

Android Wi-Fi 初始化的序列图

WifiStateMachine 处理来自 WifiManager 的请求。当系统通过发送 CMD_START_SUPPLICANT 命令初始化 Wi-Fi 时,WifiStateMachine 将调用其 processMessage 方法来处理此请求,如下所示:

processMessage 方法通过 WifiNative 调用原生方法来加载 Wi-Fi 驱动程序(loadDriver)并启动 Wi-Fi HAL(startHAL)。

请注意函数调用 mWifiNative.loadDriverWifiNative.startHal,如下所示的流程图所示:

WifiNative 的实现包括 Java 部分 和 原生部分。Java 实现可以在 $AOSP/frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiNative.java 文件中找到。

原生实现可以在 $AOSP/frameworks/opt/net/wifi/service/jni/com_android_server_wifi_WifiNative.cpp 文件中找到。

当创建 WifiNative 类的实例时,它首先加载 Wi-Fi 服务共享库,并调用 registerNatives 函数来注册所有原生函数,如下所示:

public class WifiNative { 
... 
    static { 
        /* Native functions are defined in libwifi-service.so */ 
        System.loadLibrary("wifi-service"); 
        registerNatives(); 
    } 

    private static native int registerNatives(); 

    public native static boolean loadDriver(); 
... 

registerNatives 的原生实现如下所示。它通过一个 gWifiMethods 全局变量来注册原生函数:

/* User to register native functions */ 
extern "C" 
jint Java_com_android_server_wifi_WifiNative_registerNatives(JNIEnv* env, jclass clazz) { 
    return AndroidRuntime::registerNativeMethods(env, 
            "com/android/server/wifi/WifiNative", gWifiMethods,  
            NELEM(gWifiMethods)); 
} 

在此函数中,它调用另一个框架函数 registerNativeMethods 来在 Java 层注册原生方法,以便 Java 层可以调用 WifiNative 中实现的功能。如果您在 Android NDK 编程方面工作过,您可能知道 registerNativeMethods 函数。我们可以在以下片段中查看 gWifiMethods 全局变量。gWifiMethods 全局变量包含在 WifiNative 中实现的原生函数列表,这些函数应作为 WifiNative 类的 Java 原生方法导出。我们可以看到 loadDriverstartHalNative 在列表中:

loadDriver 方法在 android_net_wifi_loadDriver 函数中实现,如下所示:

static jboolean android_net_wifi_loadDriver(JNIEnv* env, jobject) 
{ 
    return (::wifi_load_driver() == 0); 
} 

它调用一个 wifi_load_driver 函数,这是 Wi-Fi HAL 的一部分,位于 $AOSP/hardware/libhardware_legacy/wifi/wifi.c

int wifi_load_driver() 
{ 
    char driver_status[PROPERTY_VALUE_MAX]; 
    #ifdef WIFI_DRIVER_MODULE_PATH 
    FILE *proc; 
    char line[sizeof(DRIVER_MODULE_TAG)+10]; 
    #endif 

    if (!property_get(DRIVER_PROP_NAME, driver_status, NULL) 
    || strcmp(driver_status, "ok") != 0) { 
        return 0;  /* driver not loaded */ 
    } 
    #ifdef WIFI_DRIVER_MODULE_PATH 
    /* 
     * If the property says the driver is loaded, check to 
     * make sure that the property setting isn't just left 
     * over from a previous manual shutdown or a runtime 
     * crash. 
     */ 
    if ((proc = fopen(MODULE_FILE, "r")) == NULL) { 
        ALOGW("Could not open %s: %s", MODULE_FILE, strerror(errno)); 
        property_set(DRIVER_PROP_NAME, "unloaded"); 
        return 0; 
    } 
    while ((fgets(line, sizeof(line), proc)) != NULL) { 
        if (strncmp(line, DRIVER_MODULE_TAG, strlen(DRIVER_MODULE_TAG)) 
        == 0) 
        { 
            fclose(proc); 
            return 1; 
        } 
    } 
    fclose(proc); 
    property_set(DRIVER_PROP_NAME, "unloaded"); 
    return 0; 
    #else 
    return 1;  
    #endif  
} 

如果需要使用特定的 Wi-Fi 驱动程序,需要定义WIFI_DRIVER_MODULE_PATH宏来指定驱动模块的路径。驱动程序加载成功后,会设置wlan.driver.status属性为ok

现在我们将探讨另一种方法,startHalNative。它在android_net_wifi_startHal函数中实现:

static jboolean android_net_wifi_startHal(JNIEnv* env, jclass cls) { 
    JNIHelper helper(env); 
    wifi_handle halHandle = getWifiHandle(helper, cls); 
    if (halHandle == NULL) { 
        if(init_wifi_hal_func_table(&hal_fn) != 0 ) { 
            ALOGD("Can not initialize the basic function pointer 
            table"); 
            return false; 
        } 

        wifi_error res = init_wifi_vendor_hal_func_table(&hal_fn); 
        if (res != WIFI_SUCCESS) { 
            ALOGD("Can not initialize the vendor function pointer 
            table"); 
            return false; 
        } 

        int ret = set_iface_flags("wlan0", 1); 
        if(ret != 0) { 
            return false; 
        } 

        res = hal_fn.wifi_initialize(&halHandle); 
        if (res == WIFI_SUCCESS) { 
            helper.setStaticLongField(cls, WifiHandleVarName, 
            (jlong)halHandle); 
            ALOGD("Did set static halHandle = %p", halHandle); 
        } 
        env->GetJavaVM(&mVM); 
        mCls = (jclass) env->NewGlobalRef(cls); 
        ALOGD("halHandle = %p, mVM = %p, mCls = %p", halHandle, mVM, 
        mCls); 
        return res == WIFI_SUCCESS; 
    } else { 
        return (set_iface_flags("wlan0", 1) == 0); 
    } 
} 

Wi-Fi 芯片供应商通常提供两个组件:Wi-Fi 实现。第一个是我们讨论过的内核驱动程序,第二个是供应商 HAL 库。startHalNative函数用于将供应商实现的函数钩接到预定义的函数列表中。正如我们可以在前面的代码片段中看到的那样,调用init_wifi_hal_func_table函数来初始化hal_fn中的函数列表。之后,调用init_wifi_vendor_hal_func_table函数来初始化hal_fn中的函数指针。如果此操作成功,它将调用供应商初始化函数hal_fn.wifi_initialize

QEMU 网络和 Android 中的 wpa_supplicant

在 HAL 中,使用wpa_supplicant来支持设备与接入点之间的认证。它作为 Android 系统中的本地守护进程启动。上层控制请求被发送到wpa_supplicantwpa_supplicant处理设备驱动程序和内核网络系统以提供网络连接。

由于 Android 模拟器使用 QEMU,网络系统由 QEMU 网络系统提供。QEMU 提供多个网络后端,包括 TAP、VDE、套接字和 SLIRP。Android 模拟器使用用户网络(SLIRP),这是 QEMU 的默认网络后端。由于 SLIRP 是 TCP/IP 网络堆栈的软件实现,它不需要 root 权限来支持网络功能。作为一个软件实现,它有以下限制:

  • 有很多开销,因此性能较差

  • 通常情况下,ICMP 流量无法正常工作,因此您无法在虚拟机内部使用 ping 命令。

  • 在 Linux 主机上,如果初始设置是由 root 完成的,则 ping 命令可以在虚拟机内部工作。

  • 虚拟机无法直接从主机或外部网络访问。

下图是 Android 模拟器中 SLIRP 网络的一个典型示例:

图片

QEMU SLIRP 网络

在前面的图中,客户端的 IP 地址为10.0.2.15,网关的 IP 地址为10.0.2.2。默认 DNS IP 地址为10.0.2.3。它可能支持 SMB,这是可选的。如果您启动 Android 模拟器,默认网络接口为eth0,IP 地址为10.0.2.15。这通常用于模拟蜂窝数据连接。要模拟 Wi-Fi 连接,我们可以使用以下 QEMU 选项添加一个额外的网络接口eth1

-netdev user,id=mynet1,net=10.0.2.0/24,dhcpstart=10.0.2.50 -device virtio-net,netdev=mynet1 

使用 -device QEMU 选项,我们添加了一个新的网络设备 mynet1,它使用 virtio 网络硬件。QEMU 可以模拟许多现有的网络硬件类型,我们在这章中选择 virtio 网络硬件。如果您喜欢,您也可以选择其他选项。

使用 -netdev QEMU 选项,我们通过提供 IP 地址范围和 DHCP 协议的起始地址来指定此网络设备的属性。

注意,前面的选项只能与 ranchu 一起使用,而不能与 goldfish 一起使用。要使用前面的 QEMU 选项启动 Android 模拟器,我们可以运行以下命令:

$ emulator @a25x86 -qemu -netdev user,id=mynet1,net=10.0.2.0/24,dhcpstart=10.0.2.50 -device virtio-net,netdev=mynet1  

在模拟器中添加 Wi-Fi

随着 Android 中 Wi-Fi 架构的引入,我们现在可以扩展模拟器以支持 Wi-Fi。要在模拟器中添加 Wi-Fi,我们需要为模拟器构建 wpa_supplicant 并为 eth1 网络接口选择正确的设备驱动程序。

在 BoardConfig.mk 中启用 wpa_supplicant

在默认的模拟器构建中,wpa_supplicant 不会被构建。要为模拟器启用 wpa_supplicant 的构建,我们可以在我们的 BoardConfig.mk 中添加以下行:

BOARD_WPA_SUPPLICANT_DRIVER := WIRED 
WPA_SUPPLICANT_VERSION      := VER_0_8_X VER_2_1_DEVEL 
BOARD_WLAN_DEVICE           := eth1 

BOARD_WPA_SUPPLICANT_DRIVER 被定义时,external/wpa_supplicant_8/wpa_supplicant/Android.mk 中的以下配置将被更改为 true:

ifneq ($(BOARD_WPA_SUPPLICANT_DRIVER),) 
  CONFIG_DRIVER_$(BOARD_WPA_SUPPLICANT_DRIVER) := y 
endif 

BOARD_WPA_SUPPLICANT_DRIVER 的值表示应该构建哪个驱动程序。由于我们使用有线以太网连接来模拟 Wi-Fi,我们将选择 有线 驱动程序,它可以在 external/wpa_supplicant_8/src/drivers/driver_wired.c 中找到。

我们还定义了要使用的 wpa_supplicant 版本和有线以太网接口。

提供适当的 wpa_supplicant 配置

要使 wpa_supplicant 正确工作,我们需要准备一个具有正确权限的 wpa_supplicant.conf 配置文件。Wi-Fi 相关的配置文件存储在 /data/misc/wifi/ 目录中。此目录属于 wifi 用户,也是 wpa_supplicant 运行的用户。

eth1 有线连接的 wpa_supplicant.conf 配置文件可以在以下片段中找到:

ctrl_interface=eth1 
ap_scan=2 
update_config=1 
device_name=x86emu 
manufacturer=unknown 
serial_number= 
device_type=10-0050F204-5 
config_methods=physical_display virtual_push_button 
external_sim=1 

network={ 
   ssid="WiredSSID" 
   key_mgmt=NONE 
   engine=1 
   priority=1 
} 

在此配置文件中,我们定义了要使用的网络 SSID 和建立连接的认证方法。由于这是一个预定义的有线连接,我们将认证方法设置为 key_mgmt=NONE,这意味着在这种情况下我们不需要使用任何认证方法。

要将 wpa_supplicant.conf 以正确的权限复制到 /data/misc/wifi/ 目录,我们需要按以下方式更改 device.mk

# Wi-Fi support 
PRODUCT_PROPERTY_OVERRIDES := \ 
    wifi.interface=eth1 

PRODUCT_PACKAGES += \ 
    libwpa_client \ 
    hostapd \ 
    dhcpcd.conf \ 
    wlutil \ 
    wpa_supplicant \ 
    wpa_supplicant.conf 

# These are the hardware-specific features 
PRODUCT_COPY_FILES += \    frameworks/native/data/etc/android.hardware.wifi.xml:system/etc/ 
permissions/android.hardware.wifi.xml 

# For android_filesystem_config.h 
PRODUCT_PACKAGES += \ 
   fs_config_files 

PRODUCT_COPY_FILES += \    device/generic/x86emu/wpa_supplicant.conf:data/misc/wifi/
wpa_supplicant.conf \  

device.mk 中,我们定义 wifi.interfaceeth1,正如我们之前讨论的那样。之后,我们将所有 Wi-Fi 相关模块添加到 PRODUCT_PACKAGES 中,以便它们可以被添加到系统镜像中。我们将 wpa_supplicant.conf 配置文件复制到 /data/misc/wifi 目录,以便 wpa_supplicant 可以以读写权限访问它。此文件属于 wifi 用户,权限为 0555

从 Android 6 版本开始,系统对供应商文件的权限定义在 device 文件夹下的 android_filesystem_config.h 文件中。PRODUCT_PACKAGES 必须包含 fs_config_dirs 和/或 fs_config_files,以便将它们分别安装到 /system/etc/fs_config_dirs/system/etc/fs_config_files。生成的 fs_config_dirsfs_config_files 文件用于设置运行时权限。我们可以在以下代码片段中看到定义在 android_filesystem_config.h 中的所有者和权限:

#include <private/android_filesystem_config.h> 

#define NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS 
/* static const struct fs_path_config android_device_dirs[] = { }; */ 

/* Rules for files. 
** These rules are applied based on "first match", so they 
** should start with the most specific path and work their 
** way up to the root. Prefixes ending in * denotes wildcard 
** and will allow partial matches. 
*/ 
static const struct fs_path_config android_device_files[] = { 
    { 00555, AID_WIFI, AID_WIFI, 0, "data/misc/wifi/wpa_supplicant.conf" }, 
#ifdef NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS 
    { 00000, AID_ROOT, AID_ROOT, 0, "system/etc/fs_config_dirs" }, 
#endif 
}; 

device.mk 的最后一个更改与设置用户界面有关。在模拟器构建中,Wi-Fi 设置用户界面不可用。要启用 Wi-Fi 设置,我们需要将 android.hardware.wifi.xml 添加到 system/etc/permissions 文件夹。

在初始化脚本中创建服务

要初始化网络接口 eth1 并启动 wpa_supplicant,我们需要在初始化脚本中定义相关服务。

初始化网络接口 eth1

要初始化 eth1,我们可以参考模拟器中 eth0 的初始化过程。网络接口 eth0system/etc/init.goldfish.sh 脚本中初始化,如下所示:

#!/system/bin/sh 

# Setup networking when boot starts 
ifconfig eth0 10.0.2.15 netmask 255.255.255.0 up 
route add default gw 10.0.2.2 dev eth0 
... 

如我们所见,固定 IP 地址 10.0.2.15 被分配给了 eth0 接口。我们可以添加以下命令来初始化接口 eth1

ifconfig eth1 up 
dhcpcd -d eth1 

在前面的命令中,我们首先使用 ifconfig 命令启用接口 eth1。然后,我们不是使用固定 IP 地址,而是使用 DHCP 客户端为 eth1 获取 IP 地址。

如我们在第六章使用自定义 ramdisk 调试启动过程中讨论的那样,init 进程将在系统启动时处理 init.rc 脚本。init.rc 脚本将包括一个特定于硬件的 init 脚本,init.${ro.hardware}.rc。在我们的案例中,ro.hardwareranchu,因此特定于硬件的 init 脚本是 init.ranchu.rc

init.ranchu.rc 初始化脚本中,定义了一个服务,如下面的代码片段所示,用于运行 init.goldfish.sh 脚本:

... 
service goldfish-setup /system/etc/init.goldfish.sh 
    user root 
    group root 
    oneshot  
... 

这就是在模拟器中完成 goldfish- 或 ranchu- 相关设置过程的方式。

启动 wpa_supplicant

我们可以在 init.ranchu.rc 脚本中添加一个服务来启动 wpa_supplicant。以下是我们添加到 init.ranchu.rc 脚本中的服务:

service wpa_supplicant /system/bin/wpa_supplicant -ieth1 -Dwired -c/data/misc/wifi/wpa_supplicant.conf -e/data/misc/wifi/entropy.bin -g@android:wpa_eth1 
    class main 
    socket wpa_eth1 dgram 660 wifi wifi 
    disabled 
    oneshot 

此服务用于使用 DHCP 客户端启动或重启 eth1 接口。对于 wpa_supplicant 服务,我们使用以下选项启动它:

  • -i: 使用网络接口 eth1 进行 Wi-Fi

  • -D: 在接口 eth1 上使用有线驱动程序进行 Wi-Fi

  • -c: 使用位于 /data/misc/wifi/wpa_supplicant.conf 的配置文件

  • -e: 定义熵文件的路径

  • -g: 将全局 ctrl_interface 定义为 @android:wpa_eth1

如果我们参考本章前面提到的 Wi-Fi 初始化的时序图,可以使用以下步骤解释 wpa_supplicant 的启动序列:

  1. WifiStateMachine 处理 CMD_START_SUPPLICANT 命令。

  2. WifiStateMachine 调用 WifiNativestartSupplicant 方法。

  3. startSupplicant 方法是一个作为 android_net_wifi_startSupplicant 原生函数实现的本地方法。这个原生函数调用在 Wi-Fi HAL wifi.c 中定义的 wifi_start_supplicant 函数。

wifi_start_supplicant 函数通过设置 ctl.start 系统属性来启动 wpa_supplicantctl.startctl.stop 是由属性服务实现的两个系统属性,可以用来启动或停止在 init 脚本中定义的服务:

int wifi_start_supplicant(int p2p_supported) 
{ 
    char supp_status[PROPERTY_VALUE_MAX] = {'\0'};  
    ... 
    property_get("wlan.interface", primary_iface, WIFI_TEST_INTERFACE); 

    property_set("ctl.start", supplicant_name); 
    sched_yield(); 
    ... 
} 

构建源代码

我们现在已经完成了所有支持在模拟器中运行 Wi-Fi 所需的更改。让我们构建这一章的 AOSP 源代码,以便我们可以测试 Wi-Fi 连接。

获取源代码

正如我们在前面的章节中所做的那样,我们将查看这一章中我们更改的项目。我们可以从这一章的清单文件中检查:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="github" 
           revision="refs/tags/android-7.1.1_r4_x86emu_ch07_r2" 
           fetch="." /> 

  <remote  name="aosp" 
           fetch="https://android.googlesource.com/" /> 
  <default revision="refs/tags/android-7.1.1_r4" 
           remote="aosp" 
           sync-c="true" 
           sync-j="1" /> 

  <!-- github/shugaoye --> 
  <project path="kernel" name="goldfish" remote="github" /> 
  <project path="device/generic/x86emu" name="x86emu" remote="github" /> 
  <project path="bootable/newinstaller"   
  name="platform_bootable_newinstaller"  
   remote="github" /> 
  <project path="device/generic/goldfish" 
  name="device_generic_goldfish" 
   remote="github" groups="pdk" /> 

  <!-- aosp --> 
  <project path="build" name="platform/build" groups="pdk,tradefed" > 
    <copyfile src="img/root.mk" dest="Makefile" /> 
  </project> 

  ... 
</manifest> 

上述代码是位于 https://github.com/shugaoye/manifests/blob/android-7.1.1_r4_ch07_aosp/default.xmldefault.xml 文件。

我们可以看到,我们为这一章有一个 android-7.1.1_r4_x86emu_ch07_r2 标签。在这一章中,我们有我们自己的项目,kernelx86emunewinstallergoldfish。我们将使用此清单来下载或更新这一章的源代码:

$ repo init https://github.com/shugaoye/manifests -b android-7.1.1_r4_x86emu_ch07_r2
$ repo sync  

在我们获得这一章的源代码后,我们可以设置环境和构建系统,如下所示:

$ . build/envsetup.sh
$ lunch x86emu_x86-eng
$ make -j4  

启用使用 initrd.img 的启动

正如我们在 第六章 中所学的,使用自定义 ramdisk 调试启动过程,我们可以分两个阶段启动模拟器。这对于调试 init 进程和解决系统级别的问题非常有帮助。在 第六章 中,使用自定义 ramdisk 调试启动过程,我们创建了一个单独的磁盘镜像 x86emu_x86.img,以存储所有必要的文件镜像以支持类似于 Android-x86 的第一阶段启动。x86emu_x86.img 镜像在系统中显示为 /dev/sda,并包含所有镜像:system.imginstall.imginitrd.imgramdisk.imgkernel 等。

在这一章中,我们将进一步修改 Android-x86 的 newinstaller 以支持两阶段启动,仅使用 system.img 而不是创建单独的镜像。我们将使用第一阶段启动来帮助我们在本章后面调试 Wi-Fi 初始化。

在启动的第一阶段,initrd.img 中的 init 脚本将挂载系统镜像并将 ramdisk.img 提取到内存中的文件系统。由于我们将直接使用 system.img,我们需要将 ramdisk.img 放入 system.img 中。我们使用 x86emu 设备中的 Makefile 来完成这项工作,而不是更改 AOSP 源代码。以下是我们添加到 device/generic/x86emu/Makefile 中的构建目标:

qcow2_img: 
   mkdir -p ${OUT}/system/x86emu_ch07 
   cp ${OUT}/ramdisk.img ${OUT}/system/x86emu_ch07 
   cd ../../..;make qcow2_img USE_SQUASHFS=0 

qcow2_img 构建目标中,我们在系统镜像中创建一个 x86emu_ch07 文件夹,并将 ramdisk.img 复制到该文件夹。之后,我们以 QCOW2 格式构建系统镜像。

要在 QCOW2 格式下构建系统镜像,我们需要更改 bootable/newinstaller 文件夹中的 Android.mk

bootable/newinstaller/Android.mk 中的 diff

从前面的 diff 工具输出中,我们可以看到我们将 VER 变量更改为 x86emu_ch07initrd.img 的 init 脚本使用此变量来查找镜像文件夹。第二次更改是添加一个构建目标,使用 qemu-img 工具生成 QCOW2 镜像。

最后,我们需要将 initrd.img 中的 init 脚本更改为以下内容,以从 system.img 内提取 ramdisk.img

... 
check_root() 
{ 
   if [ "`dirname $1`" = "/dev" ]; then 
         [ -e $1 ] || return 1 
         blk=`basename $1` 
         [ ! -e /dev/block/$blk ] && ln $1 /dev/block 
         dev=/dev/block/$blk 
   else 
         dev=$1 
   fi 
   try_mount ro $dev /mnt || return 1 
   if [ -n "$iso" -a -e /mnt/$iso ]; then 
         mount --move /mnt /iso 
         mkdir /mnt/iso 
         mount -o loop /iso/$iso /mnt/iso 
         SRC=iso 
   elif [ ! -e /mnt/$SRC/ramdisk.img ]; then 
         return 1 
   fi 
   zcat /mnt/$SRC/ramdisk.img | cpio -id > /dev/null 
   if [ -e /mnt/$SRC/system.sfs ]; then 
         mount -o loop /mnt/$SRC/system.sfs /sfs 
         mount -o loop /sfs/system.img system 
   elif [ -e /mnt/$SRC/system.img ]; then 
         remount_rw 
         mount -o loop /mnt/$SRC/system.img system 
   elif [ -d /mnt/$SRC/system ]; then 
         remount_rw 
         mount --bind /mnt/$SRC/system system 
   else 
 echo Moving mount point to /android/system mount --move /mnt /android/system 
   fi 
   mkdir mnt 
   echo " found at $1" 
   rm /sbin/mke2fs 
   hash -r 
} 
... 
echo -n Detecting x86emu... export DEBUG=2 export SRC=x86emu_ch07 
... 

原始脚本将尝试在 SQUASH 格式(system.sfs)或普通镜像(system.img)中查找系统镜像。如果找不到任何系统镜像,它将尝试将 system/ 文件夹作为系统镜像。之后,它将挂载镜像文件或文件夹到 /android/system。在我们的情况下,系统镜像已经挂载在 /mnt,所以我们只需将挂载点从 /mnt 移动到 /android/system

对 init 脚本的第二次更改是定义 DEBUGSRC 环境变量。这两个变量是从 第六章 的内核命令行传递的,使用自定义 ramdisk 调试启动过程。在这里,我们在脚本内部定义它们,所以我们不需要担心测试脚本中的内核命令行。

一旦我们完成所有这些更改,我们可以按照以下方式构建 initrd.img 和系统镜像:

$ cd device/generic/x86emu
$ make qcow2_img
...
Created filesystem with 1976/81920 inodes and 158476/327680 blocks
Install system fs image: out/target/product/x86emu/system.img
out/target/product/x86emu/system.img+ maxsize=1370278272 blocksize=2112 total=1342177280 reserve=13842048
rm -rf out/target/product/x86emu/installer
out/host/linux-x86/bin/acp -pr bootable/newinstaller/initrd out/target/product/x86emu/installer
ln -s /bin/ld-linux.so.2 out/target/product/x86emu/installer/lib
mkdir -p out/target/product/x86emu/installer/android out/target/product/x86emu/installer/iso out/target/product/x86emu/installer/mnt out/target/product/x86emu/installer/proc out/target/product/x86emu/installer/sys out/target/product/x86emu/installer/tmp out/target/product/x86emu/installer/sfs out/target/product/x86emu/installer/hd
echo "VER=x86emu_ch07" > out/target/product/x86emu/installer/scripts/00-ver
out/host/linux-x86/bin/mkbootfs out/target/product/x86emu/installer | gzip -9 > out/target/product/x86emu/initrd.img
qemu-img convert -c -f raw -O qcow2 out/target/product/x86emu/system.img out/target/product/x86emu/system-qcow2.img
make[1]: Leaving directory `/home/roger/src/android-6'

#### make completed successfully (03:30 (mm:ss)) ####

我们可以从前面的输出中看到,initrd.img 已创建,system-qcow2.img 是从 system.img 生成的。

在模拟器上测试 Wi-Fi

我们现在已经准备好了测试过程中所需的全部镜像。本章的预构建测试镜像可以从以下 URL 下载:

sourceforge.net/projects/android-system-programming/files/android-7/ch07/ch07.zip/download

使用 initrd.img 启动 Android 模拟器

我们可以执行以下命令,首先使用 initrd.img 启动系统:

$ cd $OUT 
$ emulator @a25x86 -ranchu -verbose -show-kernel -system ./system-qcow2.img -ramdisk ./initrd.img -initdata ./userdata-qcow2.img -kernel ./kernel -qemu -netdev user,id=mynet1,net=10.0.2.0/24,dhcpstart=10.0.2.50 -device virtio-net,netdev=mynet1 

在前面的命令中,我们使用 QCOW2 格式的镜像来存储系统和用户数据,因为它们比普通文件镜像小得多。我们使用 initrd.img 作为 ramdisk,这样我们就可以在启动的第一阶段调试配置。我们也可以将此脚本更改为直接使用 ramdisk.img。在这种情况下,这是模拟器的正常启动过程。

一旦我们使用 initrd.img 启动模拟器,我们就可以进入调试控制台,在那里我们可以在继续前进之前检查配置并做出必要的更改。

从输出中,我们可以看到设备上的系统镜像 /dev/block/vda 已挂载到 /android/system。此时,我们有机会在启动它们之前检查和更改任何启动脚本。例如,我们可以在启动 Android 系统之前,通过 -dd 选项编辑 init.ranchu.rc 来提高 wpa_supplicant 的调试级别。

使用 ramdisk.img 启动 Android 模拟器

要使用 ramdisk.img 启动系统,我们可以执行以下命令:

$ cd $OUT
$ emulator @a25x86 -ranchu -verbose -show-kernel -system ./system-qcow2.img **-ramdisk ./ramdisk.img** -initdata ./userdata-qcow2.img -kernel ./kernel -qemu -netdev user,id=mynet1,net=10.0.2.0/24,dhcpstart=10.0.2.50 -device virtio-net,netdev=mynet1

调试 Wi-Fi 启动过程

系统启动后,我们可以使用 logcat 检查 wpa_supplicant 的调试信息,如下所示:

$ adb logcat -s "wpa_supplicant"  

图片 7-9

我们可以看到 wpa_supplicant 成功启动,使用以太网 eth1 和全局控制套接字 wpa_eth1。此全局控制套接字在 init.ranchu.rc 中指定,作为 wpa_supplicant 服务的一部分,如下所示:

service wpa_supplicant /system/bin/wpa_supplicant -ieth1 -Dwired -c/data/misc/wifi/wpa_supplicant.conf -e/data/misc/wifi/entropy.bin -g@android:wpa_eth1 -dd 
    class main 
    socket wpa_eth1 dgram 660 wifi wifi 
    disabled 
    oneshot 

我们还可以使用以下片段中的 ifconfig 命令检查网络状态。我们可以看到 eth0 被分配了一个固定的 IP 地址,10.0.2.15,而 eth1 通过 DHCP 被分配了 IP 地址 10.0.2.50

图片 7-10

系统启动后,我们可以进入设置 | Wi-Fi,我们会看到以下屏幕。接入点 SSID 是 WiredSSID,我们可以像预期的那样打开或关闭 Wi-Fi:

图片 7-11

摘要

在本章中,我们介绍了 Android 中的 Wi-Fi 架构,并对 Wi-Fi 初始化过程进行了分析。基于此,我们修改了我们的 x86emu 设备,通过有线以太网接口 eth1 支持模拟 Wi-Fi。我们利用 QEMU 的高级功能为 ranchu 模拟器添加了第二个网络接口。在所有这些对 x86emu 的修改完成后,我们构建并测试了镜像。为了帮助调试,我们重新使用了从第六章,“使用自定义 ramdisk 调试启动过程”中学到的技术,使用 initrd.img 启动系统,以便在 Android 系统启动之前获得调试控制台。

通过从第四章,“自定义 Android 模拟器”到第七章,“在 Android 模拟器上启用 Wi-Fi”的知识,我们学习了如何基于现有设备创建新设备。我们还学习了如何自定义和扩展设备以支持新功能。从下一章到第十一章,“启用 VirtualBox 特定硬件接口”,我们将接受一个新的挑战,支持 AOSP 不支持的新的平台。我们将创建并构建一个新的 x86vbox 设备,以探索 Android 系统编程领域的更多高级主题。

第八章:在 VirtualBox 上创建您的设备

我们已经学习了如何使用 x86emu 定制和增强现有设备以支持新功能。x86emu 设备是在以下 Android 模拟器之上创建的设备:goldfish 和 ranchu。从本章到第十一章[启用 VirtualBox 特定硬件接口],我们将转向一个高级主题:移植 Android 系统。对于 AOSP 不支持的平台,我们能做什么?

在本章中,我们将转向一个新的设备,x86vbox。我们将创建这个新的 x86vbox 设备,以便在 VirtualBox 上运行它。由于 VirtualBox 是 AOSP 直接不支持的虚拟硬件,我们必须自己创建 HAL 层。自己创建 HAL 层并不意味着我们必须从头开始创建一切。正如我之前提到的,移植和定制是集成的艺术。我们可以从其他开源项目中集成我们需要的设备驱动程序。在本章中,我们将涵盖以下主题:

  • 分析 Android-x86 项目的 HAL 并使用 Android-x86 HAL 为 x86vbox 设备

  • 基于 Android-x86 HAL 分析创建 x86vbox 设备

  • 分析 x86vbox 的启动过程

x86vbox 的 HAL

在我们创建新的 x86vbox 设备之前,我们需要解决一个关键问题:创建 x86vbox 的 HAL。这意味着我们需要支持在 VirtualBox 上出现的硬件设备。正如我们之前所说的,Android-x86 项目是一个旨在为任何基于 x86 的计算设备提供板级支持包BSP)的项目。尽管 VirtualBox 是一个虚拟化的 x86 硬件环境,我们仍然可以使用 Android-x86 项目的一部分来支持它。在下面的表中,我们可以看到我们从 Android-x86 中重用的项目列表。我们需要在构建中包含以下三个项目类别:

  • Linux 内核:Android-x86 提供了一个可以与 Android 配合使用的内核,用于 Intel x86 架构。

  • 针对 Intel x86 架构的 HAL:Android-x86 在大多数你可以在 PC 上找到的设备上包含了 HAL 支持。

  • Android 系统项目和框架项目:Android-x86 将system/frameworks/目录下的某些项目进行了更改,以满足 x86 架构特定的要求。例如,system/core下的initinit.rc已经被更改以与 Android-x86 的双阶段启动相兼容。

在下面的表中,我们还可以从另一个维度查看项目:

  • Android-x86 更改的 AOSP 项目。

  • 仅支持 Android-x86 的项目。

  • 仅支持 x86vbox 的项目。

在本章和随后的章节中,我们将创建 x86vbox 设备,并对以下一些项目进行更改,以便在 VirtualBox 上运行 x86vbox。

在下面的表中,我们还列出了来自 AOSP、Android-x86 和 x86vbox 的所有内核和 HAL 相关项目。由它们创建或更改的项目用X标记:

项目 AOSP Android-x86 x86vbox HAL 模块
kernel X X X
device/generic/x86vbox X
bionic X X
bootable/newinstaller X X
device/generic/common X X X
device/generic/firmware X
external/alsa-lib X
external/alsa-utils X
external/bluetooth/bluez X bluetooth.default audio.a2dp.default
external/bluetooth/glib X
external/bluetooth/sbc X
external/busybox X
external/drm_gralloc X X gralloc.drm
external/drm_hwcomposer X X hwcomposer.drm
external/e2fsprogs X X
external/ffmpeg X
external/libdrm X X
external/libpciaccess X
external/libtruezip X
external/llvm X X
external/mesa X
external/s2tc X
external/stagefright-plugins X
external/v86d X
frameworks/av X X
frameworks/base X X X
frameworks/native X X
hardware/broadcom/wlan X X
hardware/gps X gps.default gps.huawei
hardware/intel/audio_media X X audio.primary.hdmi
hardware/intel/libsensors X sensors.hsb
hardware/libaudio X audio.primary.x86
hardware/libcamera X camera.x86
hardware/libhardware X X libhardware
hardware/libhardware_legacy X X audio_policy.default
hardware/liblights X lights.default

| hardware/libsensors | | X | | sensors.hdaps sensors.iio

sensors.kbd

sensors.s103t

sensors.w500 |

hardware/ril X X
hardware/x86power X power.x86
system/core X X

x86vbox 的清单

根据 preceding 表格的分析,我们可以为 x86vbox 创建清单文件。从前面的表格中,我们可以看到我们重用了来自 Android-x86 的 39 个项目来形成 VirtualBox 的 HAL。在这 39 个项目中,有 16 个来自 AOSP,并由 Android-x86 进行了修改。为了在 VirtualBox 上运行我们的 x86vbox 设备,我们需要在 device/generic/x86vbox 创建设备 x86vbox。我们还需要更改四个项目:kernelbootable/newinstallerdevice/generic/commonframeworks/base

在 x86vbox 的清单中,我们将包括前面提到的项目,用于 x86 内核、HAL,并且修改了 system/ 以及 frameworks/

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="github" 
           revision="refs/tags/android-7.1.1_r4_x86vbox_ch08_r1" 
           fetch="." /> 

  <remote  name="aosp" 
           fetch="https://android.googlesource.com/" /> 
  <default revision="refs/tags/android-7.1.1_r4" 
           remote="aosp" 
           sync-c="true" 
           sync-j="1" /> 

  <!-- github/android-7.1.1_r4_ch08 --> 
  <project path="kernel" name="goldfish" remote="github" /> 
  <project path="bootable/newinstaller"   
  name="platform_bootable_newinstaller" 
  remote="github" /> 
  <project path="device/generic/common" name="device_generic_common" 
  groups="pdk" 
  remote="github" /> 
  <project path="device/generic/x86vbox" name="x86vbox" 
  remote="github" /> 
  <project path="bootable/recovery" name="android_bootable_recovery" 
  remote="github" groups="pdk" /> 

  <project path="frameworks/base" name="platform_frameworks_base" 
  groups="pdk-cw-fs,pdk-fs" remote="github" /> 

  <project path="bionic" name="platform_bionic" groups="pdk" 
  remote="github" /> 
  <project path="device/generic/firmware" 
  name="device_generic_firmware" 
  remote="github" /> 
  <project path="external/alsa-lib" name="platform_external_alsa-lib" 
  remote="github" /> 
  <project path="external/alsa-utils" 
  name="platform_external_alsa-utils" 
  remote="github" /> 
  <project path="external/bluetooth/bluez" 
  name="platform_external_bluetooth_bluez" remote="github" /> 
  <project path="external/bluetooth/glib" 
  name="platform_external_bluetooth_glib" 
  remote="github" /> 
  <project path="external/bluetooth/sbc" 
  name="platform_external_bluetooth_sbc" 
  remote="github" /> 
  <project path="external/busybox" name="platform_external_busybox" 
  remote="github" /> 
  <project path="external/drm_gralloc" 
  name="platform_external_drm_gralloc" 
  groups="drm_gralloc" remote="github" /> 
  <project path="external/drm_hwcomposer" 
  name="platform_external_drm_hwcomposer" 
  groups="drm_hwcomposer" remote="github" /> 
  <project path="external/e2fsprogs" name="platform_external_e2fsprogs" 
  groups="pdk" remote="github" /> 
  <project path="external/ffmpeg" name="platform_external_ffmpeg" 
  remote="github" /> 
  <project path="external/libdrm" name="platform_external_libdrm" 
  groups="pdk" 
  remote="github" /> 
  <project path="external/libtruezip" 
  name="platform_external_libtruezip" 
  remote="github" /> 
  <project path="external/llvm" name="platform_external_llvm" 
  groups="pdk" 
  remote="github" /> 
  <project path="external/mesa" name="platform_external_mesa" 
  remote="github" /> 
  <project path="external/s2tc" name="platform_external_s2tc" 
  remote="github" /> 
  <project path="external/stagefright-plugins" 
  name="platform_external_stagefright-plugins" remote="github" /> 
  <project path="external/v86d" name="platform_external_v86d" 
  remote="github" /> 
  <project path="frameworks/av" name="platform_frameworks_av" 
  groups="pdk" 
  remote="github" /> 
  <project path="frameworks/native" name="platform_frameworks_native" 
  groups="pdk" remote="github" /> 
  <project path="hardware/broadcom/wlan" 
  name="platform_hardware_broadcom_wlan" 
  groups="pdk,broadcom_wlan" remote="github" /> 
  <project path="hardware/gps" name="platform_hardware_gps" 
  remote="github" /> 
  <project path="hardware/intel/audio_media" 
  name="platform_hardware_intel_audio_media" groups="intel" 
  remote="github" /> 
  <project path="hardware/intel/libsensors" 
  name="platform_hardware_intel_libsensors" remote="github" /> 
  <project path="hardware/libaudio" name="platform_hardware_libaudio" 
  remote="github" /> 
  <project path="hardware/libcamera" name="platform_hardware_libcamera" 
  remote="github" /> 
  <project path="hardware/libhardware" 
  name="platform_hardware_libhardware" 
  groups="pdk" remote="github" /> 
  <project path="hardware/libhardware_legacy" 
  name="platform_hardware_libhardware_legacy" groups="pdk" 
  remote="github" /> 
  <project path="hardware/liblights" name="platform_hardware_liblights" 
  remote="github" /> 
  <project path="hardware/libsensors" 
  name="platform_hardware_libsensors" 
  remote="github" /> 
  <project path="hardware/ril" name="platform_hardware_ril" 
  groups="pdk" 
  remote="github" /> 
  <project path="hardware/x86power" name="platform_hardware_x86power" 
  remote="github" /> 
  <project path="system/core" name="platform_system_core" groups="pdk" 
  remote="github" /> 

  <!-- aosp --> 
  <project path="build" name="platform/build" groups="pdk,tradefed" > 
    <copyfile src="img/root.mk" dest="Makefile" /> 
  </project> 

... 
</manifest> 

我们可以看到,x86vbox 的清单包含两部分。第一部分包括 x86 内核、x86vbox HAL 以及所有位于 GitHub 上的修改过的 AOSP 项目。第二部分包括原始的 AOSP 项目。第二部分的所有项目都没有被 Android-x86 或 x86vbox 修改过。第一部分的大多数项目只被 Android-x86 修改过,所以我们也不必对这些项目做任何事情。

在清单的第一部分中,external/hardware/目录下的所有项目都与 x86 HAL 相关。你可能对唯一的 AOSP 项目bionic有疑问。你可能想知道为什么 Android-x86 会修改它,因为它是 Android 的 C 库。你可能知道系统调用是在 Linux 系统的 C 库中实现的。原始的 bionic 缺少两个系统调用iopermiopl,而它们是external/v86d项目所需要的,该项目是vesafb帧缓冲驱动器的用户空间守护进程。

所有的前述分析帮助我们明确了工作范围。正如我们所见,工作范围并不像我们最初想象的那么大。现在有很多开源项目可供使用。如果我们尽可能多地重用它们,通常可以大幅减少工作量。

GitHub 上的所有 Android-x86 项目都是从 Android-x86 镜像分叉的,这样我们就可以修改它们。

创建新的 x86vbox 设备

一旦我们有了 VirtualBox 的 HAL,我们现在可以创建一个名为 x86vbox 的新设备。如果我们回顾一下在第四章中创建 x86emu 设备的过程,自定义 Android 模拟器,我们知道我们需要为新设备准备一个板/设备配置 Makefile 和一个产品定义 Makefile。我们也可以通过继承现有设备来创建一个新设备。如果我们查看前面的 x86 HAL 表格,我们可以看到一个共同的 x86 设备项目,device/common,可以在 Android-x86 中找到。我们将通过继承这个共同的 x86 设备来创建我们的新设备 x86vbox。本章中创建的 x86vbox 是一个 32 位 x86 设备。你可以按照相同的说明自己创建一个 x86_64 设备。

正如我们在第四章中所述,自定义 Android 模拟器,我们创建了一个AndroidProducts.mk Makefile 来包含 x86vbox 的产品定义 Makefile,如下所示:

PRODUCT_MAKEFILES := \ 
    $(LOCAL_DIR)/x86vbox.mk 

x86vbox 的产品定义 Makefile

如我们所知,AOSP 构建系统会查找AndroidProducts.mk以找到特定设备的产品定义 Makefile。让我们回顾一下产品定义 Makefile x86vbox.mk,如下所示:

# includes the base of Android-x86 platform 
$(call inherit-product,device/generic/common/x86.mk) 

# Overrides 
PRODUCT_NAME := x86vbox 
PRODUCT_BRAND := Android-x86 
PRODUCT_DEVICE := x86vbox 
PRODUCT_MODEL := x86vbox_ch8 

TARGET_KERNEL_SOURCE := kernel 
TARGET_KERNEL_CONFIG := android-x86_defconfig 
TARGET_ARCH := x86 

PRODUCT_OUT ?= out/target/product/$(PRODUCT_DEVICE) 

include $(TARGET_KERNEL_SOURCE)/AndroidKernel.mk 

# define build targets for kernel 
.PHONY: $(TARGET_PREBUILT_KERNEL) 

LOCAL_KERNEL := $(TARGET_PREBUILT_KERNEL) 

PRODUCT_COPY_FILES += \ 
    $(LOCAL_KERNEL):kernel \ 

如我们所见,产品定义 Makefile 非常简单。它执行以下操作:

  • 它包含了通用的 x86 产品定义 Makefile,device/generic/common/x86.mk

  • 它定义了产品定义变量,例如PRODUCT_NAMEPRODUCT_BRANDPRODUCT_DEVICEPRODUCT_MODEL

  • 它指定了如何为 x86vbox 构建内核。

它看起来甚至比我们在第四章中创建的x86emu 的 Android 模拟器的定制版本还要简单。继承的x86.mk Makefile 做了大部分实际工作,我们将在稍后进行更深入的分析。

x86vbox 的板级配置

我们将为 x86vbox 创建的另一个 Makefile 是板级配置 Makefile BoardConfig.mk,如下所示:

TARGET_NO_BOOTLOADER := true 

TARGET_ARCH := x86 
TARGET_CPU_ABI := x86 

TARGET_CPU_ABI_LIST_32_BIT := $(TARGET_CPU_ABI) $(TARGET_CPU_ABI2) $(NATIVE_BRIDGE_ABI_LIST_32_BIT) 
TARGET_CPU_ABI_LIST := $(TARGET_CPU_ABI_LIST_32_BIT) 

TARGET_USERIMAGES_USE_EXT4 := true 
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 1153433600 
BOARD_USERDATAIMAGE_PARTITION_SIZE := 419430400 
BOARD_CACHEIMAGE_PARTITION_SIZE := 69206016 
BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4 
BOARD_FLASH_BLOCK_SIZE := 512 
TARGET_USERIMAGES_SPARSE_EXT_DISABLED := true 

BOARD_SEPOLICY_DIRS += build/target/board/generic/sepolicy 
BOARD_SEPOLICY_DIRS += build/target/board/generic_x86/sepolicy 

include device/generic/common/BoardConfig.mk 

它看起来也很简单。它定义了目标架构的特定变量TARGET_ARCHTARGET_CPU_ABITARGET_CPU_ABI_LIST_32_BITTARGET_CPU_ABI_LIST。然后它定义了系统映像文件的参数。最后,它包含了通用的板级配置 Makefile device/generic/common/BoardConfig.mk,我们稍后会查看这个文件。

常见的 x86 设备

在 Android-x86 项目中,它定义了一个通用的 x86 设备,以便每个人都可以基于它创建特定的 x86 设备。继承的设备可以是 32 位或 64 位的 x86 设备。

我们可以先查看device/generic/common的内容,如下所示:

图片

我们可以看到有很多文件和目录。我们将首先从BoardConfig.mkx86.mk Makefile 开始分析。

BoardConfig.mk中,构建系统所需的变量定义如下:

TARGET_BOARD_PLATFORM := android-x86 

# Some framework code requires this to enable BT 
BOARD_HAVE_BLUETOOTH := true 

BOARD_USE_LEGACY_UI := true 

# BOARD_SYSTEMIMAGE_PARTITION_SIZE = $(if $(MKSQUASHFS),0,1610612736) 

# customize the malloced address to be 16-byte aligned 
BOARD_MALLOC_ALIGNMENT := 16 

# Enable dex-preoptimization to speed up the first boot sequence 
# of an SDK AVD. Note that this operation only works on Linux for now 
ifeq ($(HOST_OS),linux) 
WITH_DEXPREOPT := true 
WITH_DEXPREOPT_PIC := true 
endif 

# the following variables could be overridden 
TARGET_PRELINK_MODULE := false 
TARGET_NO_KERNEL ?= false 
TARGET_NO_RECOVERY ?= true 
TARGET_EXTRA_KERNEL_MODULES := tp_smapi 
ifneq ($(filter efi_img,$(MAKECMDGOALS)),) 
TARGET_KERNEL_ARCH ?= x86_64 
endif 
TARGET_USES_64_BIT_BINDER := $(if $(filter x86_64,$(TARGET_ARCH) $(TARGET_KERNEL_ARCH)),true) 

BOARD_USES_GENERIC_AUDIO ?= false 
BOARD_USES_ALSA_AUDIO ?= true 
... 

这是一个长长的列表。它定义了音频、Wi-Fi、GPU 和蓝牙相关特性。它也是一个禁用的模拟器相关构建。

现在,让我们看看x86.mk

PRODUCT_PROPERTY_OVERRIDES := \ 
    ro.com.android.dateformat=MM-dd-yyyy \ 

$(call inherit-product,$(LOCAL_PATH)/device.mk) 
$(call inherit-product,$(LOCAL_PATH)/packages.mk) 

# Get a list of languages. 
$(call inherit-product,$(SRC_TARGET_DIR)/product/locales_full.mk) 

# Get everything else from the parent package 
$(call inherit-product,$(SRC_TARGET_DIR)/product/full.mk) 

x86.mk中,它从 AOSP 构建系统中包含了两个通用 Makefile,full.mklocales_full.mk。如果我们回想一下 x86emu 的设备定义 Makefile,它也从构建系统中包含了这两个 Makefile。

x86.mk还导入了另外两个本地 Makefile,device.mkpackages.mk。在packages.mk中,HAL 模块包定义如下:

PRODUCT_PACKAGES := \ 
    camera.x86 \ 
    com.android.future.usb.accessory \ 
    drmserver \ 
    gps.default \ 
    gps.huawei \ 
    hwcomposer.x86 \ 
    io_switch \ 
    libGLES_android \ 
    libhuaweigeneric-ril \ 
    lights.default \ 
    power.x86 \ 
    powerbtnd \ 
    sensors.hsb \ 
    tablet-mode \ 
    v86d \ 
    wacom-input \ 

PRODUCT_PACKAGES += \ 
    libwpa_client \ 
    hostapd \ 
    wpa_supplicant \ 
    wpa_supplicant.conf \ 

这并不是包的完整列表。在device.mk中,还有更多组件被添加到PRODUCT_PACKAGES中,如下所示:

PRODUCT_DIR := $(dir $(lastword $(filter-out device/common/%,$(filter device/%,$(ALL_PRODUCTS))))) 

PRODUCT_PROPERTY_OVERRIDES := \ 
    ro.ril.hsxpa=1 \ 
    ro.ril.gprsclass=10 \ 
    keyguard.no_require_sim=true \ 
    ro.com.android.dataroaming=true 

PRODUCT_DEFAULT_PROPERTY_OVERRIDES := \ 
    ro.arch=x86 \ 
    persist.rtc_local_time=1 \ 

PRODUCT_COPY_FILES := \... 
PRODUCT_TAGS += dalvik.gc.type-precise 

PRODUCT_CHARACTERISTICS := tablet 

PRODUCT_AAPT_CONFIG := normal large xlarge mdpi hdpi 
PRODUCT_AAPT_PREF_CONFIG := mdpi 

DEVICE_PACKAGE_OVERLAYS := $(LOCAL_PATH)/overlay 

# Get the firmwares 
$(call inherit-product,device/generic/firmware/firmware.mk) 

# Get the touchscreen calibration tool 
$(call inherit-product-if-exists,external/tslib/tslib.mk) 

# Get the alsa files 
$(call inherit-product-if-exists,hardware/libaudio/alsa.mk) 

# Get GPS configuration 
$(call inherit-product-if-exists,device/common/gps/gps_as.mk) 

# Get the hardware acceleration libraries 
$(call inherit-product-if-exists,$(LOCAL_PATH)/gpu/gpu_mesa.mk) 

# Get the sensors hals 
$(call inherit-product-if-exists,hardware/libsensors/sensors.mk) 

# Get tablet dalvik parameters 
$(call inherit-product,frameworks/native/build/tablet-10in-xhdpi-2048-dalvik-heap.mk) 

# Get GMS 
$(call inherit-product-if-exists,vendor/google/products/gms.mk) 

# Get native bridge settings 
$(call inherit-product-if-exists,$(LOCAL_PATH)/nativebridge/nativebridge.mk) 

device.mk中,它定义了 x86 设备的属性,并随后列出了要复制的文件列表。最后,它包括各种组件的单独 Makefile,例如固件、触摸屏校准工具、音频、GPS、传感器和本地桥接器等。你可以自己分别在每个相应的文件夹中找到并调查它们。在本章中,我们只概述了如何创建 x86vbox 设备。我们将在后面的章节中深入探讨一些硬件接口的细节。

获取源代码和构建 x86vbox 设备

要构建 x86vbox 设备,我们可以使用以下命令从 GitHub 和 AOSP 获取源代码:

$ repo init https://github.com/shugaoye/manifests -b android-7.1.1_r4_ch08_aosp
$ repo sync  

使用android-7.1.1_r4_ch08_aosp标签作为本章更改的基线。

在我们获取本章的源代码后,我们可以设置环境和构建系统,如下所示:

$ source build/envsetup.sh
$ lunch x86vbox-eng
$ make -j4 

启动过程和设备初始化

由于我们使用 Android-x86 内核和 HAL 为 x86vbox,我们将进一步分析本节中 x86vbox 的启动过程。通过分析,我们可以了解 Android-x86 是如何使用一个代码库支持多个设备的。你可以回顾我们在第六章,使用自定义 ramdisk 调试启动过程中讨论的两个阶段启动过程。现在,我们将在此基础上进行更详细的分析。

Android-x86 的内核与我们第六章,使用自定义 ramdisk 调试启动过程中用于模拟器的内核不同。Android-x86 内核并不知道它需要支持哪些硬件接口,因此它尽可能多地构建与它相关的设备驱动程序。另一方面,goldfish 内核知道它需要支持哪些硬件。这种差异意味着它们是以两种不同的方式构建的。goldfish 内核包含了内核内部支持的所有设备,因此它根本不使用内核模块。然而,对于 Android-x86 内核来说,这样做是不可能的,因为这会使内核的大小变得过大。Android-x86 的内核广泛使用内核模块。

我们在本章中将重点分析设备节点是如何在启动过程中创建的,以及内核模块是如何加载的。由于 Android-x86 的启动分为两个阶段,设备初始化也被分为两个阶段。

Android 启动前的设备初始化

启动过程将从嵌入式 Linux 环境作为第一阶段开始。大多数设备将在这一阶段进行初始化。好事是,Android-x86 可以通过定义的环境变量进入一个带有调试控制台的控制台环境。在这个控制台中,我们可以检查系统状态,以确定我们是否拥有我们想要创建的正确配置。默认的 init 脚本包含两个调试检查点。第一个检查点是在根设备挂载之后。第二个检查点是在所有驱动程序加载之后。当然,你可以通过更改 init 脚本来设置尽可能多的检查点。

以下是我们进入第一个检查点之前想要查看的 init 脚本的一部分:

PATH=/sbin:/bin:/system/bin:/system/xbin; export PATH 
... 
# early boot 
if test x"$HAS_CTTY" != x"Yes"; then 
    # initialise /proc and /sys 
    busybox mount -t proc proc /proc 
    busybox mount -t sysfs sys /sys 
    # let busybox install all applets as symlinks 
    busybox --install -s 
    # spawn shells on tty 2 and 3 if debug or installer 
    if test -n "$DEBUG" || test -n "$INSTALL"; then 
        # ensure they can open a controlling tty 
        mknod /dev/tty c 5 0 
        # create device nodes then spawn on them 
        mknod /dev/tty2 c 4 2 && openvt 
        mknod /dev/tty3 c 4 3 && openvt 
    fi 
    if test -z "$DEBUG" || test -n "$INSTALL"; then 
        echo 0 0 0 0 > /proc/sys/kernel/printk 
    fi 
    # initialise /dev (first time) 
    mkdir -p /dev/block 
    echo /sbin/mdev > /proc/sys/kernel/hotplug 
    mdev -s 
    # re-run this script with a controlling tty 
    exec env HAS_CTTY=Yes setsid cttyhack /bin/sh "$0" "$@" 
fi 
... 

在早期启动阶段,init 脚本使用内核挂载/proc/sys文件系统。之后,它设置busybox的符号链接,以便我们可以使用所有busybox命令。然后,它将/sbin/mdev设置为热插拔的处理程序。mdev命令是udev的最小实现。mdev可以在内核检测到新设备时动态管理/dev下的设备节点。mdevbusybox的一部分,因此我们需要首先创建所有busybox符号链接。它还需要/proc/sys文件系统。设置热插拔后,脚本运行mdev -s命令以找到内核当前检测到的所有设备。此时,/dev下的所有设备节点都已创建。

udev 和mdev udev是 Linux 内核的设备管理器。作为 devfsd 和 hotplug 的后继者,udev 主要管理/dev目录下的设备节点。同时,udev 还处理在硬件设备添加到系统或从系统中移除时产生的所有用户空间事件,包括某些设备所需的固件加载。

mdevbusybox中 udev 的最小实现。它用于嵌入式系统以替换 udev。mdev 在 udev 中缺少一些功能,例如设备驱动程序加载的完整实现等。我们可以看到,Android-x86 在第一次启动阶段使用 mdev。

让我们看看这个阶段的内核模块和设备节点:

内核模块和设备节点

如前一个截图所示,所有设备节点都创建在/dev下。然而,此时只加载了一个内核模块。我们现在处于第一个检查点。

让我们继续前进,看看在达到下一个检查点之前的脚本中会发生什么。要退出第一个检查点,我们需要运行exit命令以继续执行脚本。

(debug-found)@android:/android # exit  

在退出第一个检查点后,它将继续执行以下脚本:

... 
[ -n "$INSTALL" ] && do_install 

load_modules 
mount_data 
mount_sdcard 
setup_tslib 
setup_dpi 
post_detect 

if [ 0$DEBUG -gt 1 ]; then 
    echo -e "\nUse Alt-F1/F2/F3 to switch between virtual consoles" 
    echo -e "Type 'exit' to enter Android...\n" 

    debug_shell debug-late 
fi 

... 

我们可以看到,在进入下一个检查点之前,init 脚本执行以下任务:

  1. 加载内核模块。

  2. 挂载数据分区。

  3. 挂载 SD 卡。

  4. 设置触摸屏校准工具。

  5. 设置屏幕 DPI。

  6. 执行任何其他启动后的检测。

您可以自行学习任务 2 到 6 的脚本,因为它们非常直接且易于理解。我们在这里想更详细地看看第一个任务:

auto_detect() 
{ 
    tmp=/tmp/dev2mod 
    echo 'dev2mod() { while read dev; do case $dev in' > $tmp 
    sort -r /lib/modules/`uname -r`/modules.alias | \ 
        sed -n 's/^alias  *\([^ ]*\)  *\(.*\)/\1)busybox modprobe 
        \2;;/p' >> $tmp 
    echo 'esac; done; }' >> $tmp 
    sed -i '/brcmfmac/d' $tmp 
    source $tmp 
    cat /sys/bus/*/devices/*/uevent | grep MODALIAS | sed 
    's/^MODALIAS=//' 
    | sort -u | dev2mod 
    cat /sys/devices/virtual/wmi/*/modalias | dev2mod 
} 

load_modules() 
{ 
    if [ -z "$FOUND" ]; then 
        auto_detect 
    fi 

    # 3G modules 
    for m in $EXTMOD; do 
        busybox modprobe $m 
    done 
} 

load_modules脚本函数在前面代码片段所示的脚本文件0-auto-detect中实现。它调用另一个函数auto-detect来完成实际工作。这个函数并不容易理解。现在让我们解释一下它做了什么。这个函数的目的是动态创建一个名为dev2mod的 shell 命令。这个dev2mod函数的作用是接受模块别名作为参数,并根据模块别名加载相应的驱动模块。在创建dev2mod命令后,auto_detect将使用在/sys/bus文件夹下由内核找到的设备调用此函数。

Android-x86 内核的所有内核模块都可以在/lib/modules/4.x.x-android-x86/modules.alias文件中找到。此文件被处理,以便在每行的末尾添加modprobe命令,以便可以使用模块别名作为参数加载内核模块。临时脚本文件位于/tmp/dev2mod,如下所示代码片段:

# cat /tmp/dev2mod
dev2mod() { while read dev; do case $dev in 
xts)busybox modprobe xts;; 
xtea)busybox modprobe tea;; 
xeta)busybox modprobe tea;; 
xcbc)busybox modprobe xcbc;; 
wp512)busybox modprobe wp512;; 
wp384)busybox modprobe wp512;; 
wp256)busybox modprobe wp512;; 
...
acpi*:80860ABC:*)busybox modprobe intel_lpss_acpi;; 
acpi*:80860AAC:*)busybox modprobe intel_lpss_acpi;; 
acpi*:193C9890:*)busybox modprobe snd_soc_max98090;; 
acpi*:10EC5670:*)busybox modprobe snd_soc_rt5670;; 
acpi*:10EC5650:*)busybox modprobe snd_soc_rt5645;; 
acpi*:10EC5645:*)busybox modprobe snd_soc_rt5645;; 
acpi*:10EC5642:*)busybox modprobe snd_soc_rt5640;; 
acpi*:10EC5640:*)busybox modprobe snd_soc_rt5640;; 
acpi)busybox modprobe acpi_cpufreq;; 
esac; done; }   

在将/sys文件系统中的设备传递给dev2mod函数之前,我们可以查看在我的系统上输出看起来如下:

# cat /sys/bus/*/devices/*/uevent | grep MODALIAS | sed 's/^MODALIAS=//' 
| sort -u
acpi:ACPI0003: 
acpi:APP0001:SMC-NAPA: 
acpi:LNXCPU: 
acpi:LNXPWRBN: 
acpi:LNXSLPBN: 
acpi:LNXSYBUS: 
acpi:LNXSYSTM: 
acpi:LNXVIDEO: 
acpi:PNP0000: 
acpi:PNP0100: 
acpi:PNP0103:PNP0C01: 
acpi:PNP0200: 
acpi:PNP0303: 
acpi:PNP0400: 
acpi:PNP0501: 
acpi:PNP0700: 
acpi:PNP0A03: 
acpi:PNP0B00: 
acpi:PNP0C02: 
acpi:PNP0C0A: 
acpi:PNP0C0F: 
acpi:PNP0F03: 
acpi:PNP8390: 
cpu:type:x86,ven0000fam0006mod003A:feature:,0000,0001,0002,0003,0004,0005,0006,0
hdaudio:v83847680r00103401a01 
hid:b0003g0001v000080EEp00000021 
pci:v0000106Bd0000003Fsv00000000sd00000000bc0Csc03i10 
pci:v00001AF4d00001000sv00001AF4sd00000001bc02sc00i00 
pci:v00008086d00001237sv00000000sd00000000bc06sc00i00 
pci:v00008086d0000265Csv00000000sd00000000bc0Csc03i20 
pci:v00008086d00002668sv00008384sd00007680bc04sc03i00 
pci:v00008086d00007000sv00000000sd00000000bc06sc01i00 
pci:v00008086d00007111sv00000000sd00000000bc01sc01i8A 
pci:v00008086d00007113sv00000000sd00000000bc06sc80i00 
pci:v000080EEd0000BEEFsv00000000sd00000000bc03sc00i00 
pci:v000080EEd0000CAFEsv00000000sd00000000bc08sc80i00 
platform:alarmtimer 
platform:goldfish_pdev_bus
platform:i8042 
platform:microcode 
platform:pcspkr 
platform:platform-framebuffer 
platform:reg-dummy 
platform:rtc_cmos 
platform:serial8250 
scsi:t-0x00 
scsi:t-0x05 
serio:ty01pr00id00ex00 
serio:ty06pr00id00ex00 
usb:v1D6Bp0001d0404dc09dsc00dp00ic09isc00ip00in00 
usb:v1D6Bp0002d0404dc09dsc00dp00ic09isc00ip00in00 
usb:v80EEp0021d0100dc00dsc00dp00ic03isc00ip00in00 
virtio:d00000001v00001AF4

如前所述输出所示,它包括了内核找到的所有模块别名。模块别名的先前输出将通过管道传递给 shell 脚本函数dev2moddev2mod函数将加载内核找到的所有相应模块。

在执行load_modules之后,我们进入第二个检查点,现在我们可以查看系统状态:

图片

内核模块加载

从前述截图我们可以看到,现在系统中加载了许多内核模块。从内核模块名称中,我们可以看到音频、鼠标和键盘驱动程序已被加载。这就是 Android-x86 init 脚本在initrd.img中自动加载设备驱动程序的方式。在 init 脚本末尾,它将根据环境变量DEBUG的设置调用chrootswitch_root。在任一情况下,根文件系统将更改为 Android 的ramdisk.img,并启动 Android init 进程,如下所示:

... 
[ -n "$DEBUG" ] && SWITCH=${SWITCH:-chroot} 

# We must disable mdev before switching to Android 
# since it conflicts with Android's init 
echo > /proc/sys/kernel/hotplug 

exec ${SWITCH:-switch_root} /android /init 
... 

Android init 进程将为这些内核无法自动检测到的设备执行硬件初始化。init 进程还将初始化 Android-x86 的 HAL。

Android 启动时的 HAL 初始化

让我们更详细地探讨一下内核无法自动检测到的设备的硬件初始化,以及 Android-x86 HAL 的初始化,本节将进行介绍。尚未初始化的一个外围设备是 Android 图形用户界面的帧缓冲区。我们将以此为例,解释 Android 的ramdisk.img中 init 进程是如何初始化硬件的。

如果我们回顾第六章 调试启动过程使用自定义的 ramdisk 中对 init 进程的分析,init 进程将执行 init.rc 脚本,这是适用于所有 Android 设备的通用脚本。在 init.rc 脚本中,它将导入特定设备的脚本 init.${ro.hardware}.rc。在我们的案例中,这个脚本是在目标设备上的 init.x86vbox.rcro.hardware 属性根据内核命令行参数 androidboot.hardware 设置,我们将其设置为 x86vboxinit.x86vbox.rc 的源代码可以在 device/generic/common/init.x86.rc 中找到。它通过 device.mk 中的以下行复制到目标输出。请注意,脚本名称在复制后已更改:

... 
PRODUCT_COPY_FILES := \ 
    $(if $(wildcard $(PRODUCT_DIR)init.rc),$(PRODUCT_DIR)init.rc:root/init.rc) \ 
    $(if $(wildcard $(PRODUCT_DIR)init.sh),$(PRODUCT_DIR),$(LOCAL_PATH)/)init.sh:system/etc/init.sh \  
... 
    $(if $(wildcard $(PRODUCT_DIR)init.$(TARGET_PRODUCT).rc),$(PRODUCT_DIR)init.$(TARGET_PRODUCT).rc,$(LOCAL_PATH)/init.x86.rc):root/init.$(TARGET_PRODUCT).rc \ 
    $(if $(wildcard $(PRODUCT_DIR)ueventd.$(TARGET_PRODUCT).rc),$(PRODUCT_DIR)ueventd.$(TARGET_PRODUCT).rc,$(LOCAL_PATH)/ueventd.x86.rc):root/ueventd.$(TARGET_PRODUCT).rc \ 
... 

从前述代码片段中我们还可以看到,shell 脚本 init.sh 也被复制到了系统镜像的 /system/etc/init.sh 路径下。这是在 init.x86vbox.rc 中用于加载设备驱动程序和初始化 HAL 的脚本。

init.x86vbox.rc 文件中,一个动作触发器被定义为如下:

on post-fs 
    exec -- /system/bin/logwrapper /system/bin/sh /system/etc/init.sh 

在预定义的触发器 post-fs 中,init.sh 脚本将作为初始化过程的一部分执行。以下为 init.sh 的代码片段:

... 
PATH=/sbin:/system/bin:/system/xbin 

DMIPATH=/sys/class/dmi/id 
BOARD=$(cat $DMIPATH/board_name) 
PRODUCT=$(cat $DMIPATH/product_name) 

# import cmdline variables 
for c in `cat /proc/cmdline`; do 
    case $c in 
        BOOT_IMAGE=*|iso-scan/*|*.*=*) 
            ;; 
        *=*) 
            eval $c 
            if [ -z "$1" ]; then 
                case $c in 
                    HWACCEL=*) 
                        set_property debug.egl.hw $HWACCEL 
                        ;; 
                    DEBUG=*) 
                        [ -n "$DEBUG" ] && set_property debug.logcat 1 
                        ;; 
                esac 
            fi 
            ;; 
    esac 
done 

[ -n "$DEBUG" ] && set -x || exec &> /dev/null 

# import the vendor specific script 
hw_sh=/vendor/etc/init.sh 
[ -e $hw_sh ] && source $hw_sh 

case "$1" in 
    netconsole) 
        [ -n "$DEBUG" ] && do_netconsole 
        ;; 
    bootcomplete) 
        do_bootcomplete 
        ;; 
    hci) 
        do_hci 
        ;; 
    init|"") 
        do_init 
        ;; 
esac 

return 0 

如前述代码片段所示,init.sh 脚本首先处理内核命令行。之后,它遇到一个多选择语句。根据传递给它的第一个参数执行一个函数。这个参数用于让 do_init 函数初始化特定的 HAL 模块。在第一个参数的情况下,它是 init 或没有参数,将执行 do_init 函数。在这种情况下,所有 HAL 模块都将被初始化,这是我们目前想要调查的情况。我们可以如下查看 do_init 函数做了什么:

function do_init() 
{ 
    init_misc 
    init_hal_audio 
    init_hal_bluetooth 
    init_hal_camera 
    init_hal_gps 
    init_hal_gralloc 
    init_hal_hwcomposer 
    init_hal_lights 
    init_hal_power 
    init_hal_sensors 
    init_tscal 
    init_ril 
    post_init 
} 

do_init 函数将逐个调用各个 HAL 模块初始化函数。我们在这里不会查看所有这些函数。我们将看看在 init_hal_gralloc 函数中如何初始化帧缓冲区设备。这是我们将在第十章 启用图形 中更深入调查的内容,因为图形支持在移植过程中是最重要的任务之一:

function init_uvesafb() 
{ 
    case "$PRODUCT" in 
        ET2002*) 
            UVESA_MODE=${UVESA_MODE:-1600x900} 
            ;; 
        *) 
            ;; 
    esac 

    [ "$HWACCEL" = "0" ] && bpp=16 || bpp=32 
    modprobe uvesafb mode_option=${UVESA_MODE:-1024x768}-$bpp 
    ${UVESA_OPTION:-mtrr=3 scroll=redraw} 
} 

function init_hal_gralloc() 
{ 
    case "$(cat /proc/fb | head -1)" in 
        *virtiodrmfb) 
        # set_property ro.hardware.hwcomposer drm 
            ;& 
        0*inteldrmfb|0*radeondrmfb|0*nouveaufb|0*svgadrmfb) 
            set_property ro.hardware.gralloc drm 
            set_drm_mode 
            ;; 
        "") 
            init_uvesafb 
            ;& 
        0*) 
            ;; 
    esac 

    [ -n "$DEBUG" ] && set_property debug.egl.trace error 
} 

init_hal_gralloc 函数中,它将根据 /proc/fb 的内容执行相应的任务。从 /proc/fb,它可以检测设备上图形硬件的类型。如果无法检测到图形硬件的类型,它将使用通用的 VESA 帧缓冲区(uvesafb),在我们的案例中用于 VirtualBox。它将调用另一个 shell 函数 init_uvesafb 来加载 VESA 帧缓冲区驱动程序。uvesafb 驱动程序将启动一个用户空间守护进程 v86d 来执行 x86 BIOS 代码。代码在受控环境中执行,并通过 netlink 接口将结果传回内核。这就是在我们的环境中初始化图形驱动程序的方式。

摘要

在本章中,我们分析了 Android-x86 HAL 并将其集成到 x86vbox 中,以便我们能够在接下来的几章中启动 x86vbox。我们还分析了 Android-x86 的启动过程。在启动过程的第一个阶段,我们使用了调试控制台来分析内核模块加载过程。在我们实际上在 VirtualBox 上启动 x86vbox 之前,一个尚未解决的问题是我们应该使用哪个引导加载程序。与模拟器不同,它不需要引导加载程序,因为模拟器使用内置的迷你引导加载程序来加载内核和 ramdisk。VirtualBox 非常类似于真实硬件。如果没有合适的引导加载程序,我们将无法启动操作系统。

在下一章中,我们将讨论这个问题,并解释我们如何使用 VirtualBox 支持的 PXE 引导来解决它。

第九章:使用 PXE/NFS 引导 x86vbox

在上一章中,我们创建了 x86vbox 设备,并且我们能够在我们的环境中构建它。在这一章中,我们将开始调试 x86vbox 的引导过程。在引导过程中我们遇到的第一件事是引导加载程序问题。我们可以使用与 Android-x86 相同的 GRUB 引导加载程序。使用 GRUB,我们仍然会遇到如何在存储介质上配置和安装它的问题。如果我们这样做,我们需要花时间讨论与引导加载程序相关的话题。

使用 VirtualBox 作为虚拟硬件平台,我们有一个更简单的解决方案。我们可以使用内置的 PXE 引导机制来避免引导加载程序问题。从调试的角度来看,PXE 引导可以使整个引导过程对我们更加透明。使用 PXE 引导,我们可以将引导加载程序的安装移出画面,这样我们就可以专注于调试 Android 系统本身。在本章中,我们将涵盖以下主题:

  • 设置 PXE 引导环境

  • 配置 VirtualBox 从 PXE 引导

  • 使用 NFS 设置根文件系统

设置 PXE 引导环境

什么是 PXE?PXE 代表 预引导执行环境。为了构建 Linux 环境,我们需要找到一种方法将内核和 ramdisk 装载到系统内存中。这是大多数 Linux 引导加载程序执行的主要任务之一。引导加载程序通常从某种存储设备中获取内核和 ramdisk,例如闪存存储、硬盘、USB 等。它也可以从网络连接中获取。PXE 是一种可以启动具有 LAN 连接和 PXE 兼容 网络接口控制器NIC)的设备的方法。

如下图中所示,PXE 使用 DHCP 和 TFTP 协议来完成引导过程。在最简单的环境中,PXE 服务器被设置为 DHCP 和 TFTP 服务器。NIC 客户端从 DHCP 服务器获取 IP 地址,并使用 TFTP 协议获取内核和 ramdisk 映像以启动引导过程:

图片

PXE 引导环境

在本节中,我们将学习如何为 VirtualBox virtio 网络适配器准备一个 PXE 兼容的 ROM,以便我们可以使用这个 ROM 通过 PXE 引导系统。我们还将学习如何设置 PXE 服务器,这是 PXE 设置中的关键元素。在 VirtualBox 中,它包括一个内置的 PXE 服务器。我们将使用这个内置的 PXE 服务器来引导 Android 系统。

准备 PXE 引导 ROM

尽管 VirtualBox 支持 PXE 引导,但不同网络适配器上的设置并不一致。在引导过程中,您可能会收到诸如PXE-E3C - TFTP 错误 - 访问违规之类的错误消息。这是因为 PXE 引导依赖于 LAN 引导 ROM。当您选择不同的网络适配器时,您可能会得到不同的测试结果。为了获得一致的测试结果,您可以使用来自 Etherboot/gPXE 项目的 LAN 引导 ROM。gPXE 是一个开源(GPL)网络引导程序。它提供了对专有 PXE ROM 的直接替换,具有许多额外功能,如 DNS、HTTP、iSCSI 等。在 gPXE 项目网站上有一个页面,介绍了如何为 VirtualBox 设置 LAN 引导 ROM:

www.etherboot.org/wiki/romburning/vbox

以下表格列出了 VirtualBox 支持的网络适配器:

VirtualBox 适配器 PCI 厂商 ID PCI 设备 ID 制造商名称 设备名称
Am79C970A 1022h 2000h AMD PCnet-PCI II (AM79C970A)
Am79C973 1022h 2000h AMD PCnet-PCI III (AM79C973)
82540EM 8086h 100Eh Intel Intel PRO/1000 MT Desktop (82540EM)
82543GC 8086h 1004h Intel Intel PRO/1000 T Server (82543GC)
82545EM 8086h 100Fh Intel Intel PRO/1000 MT Server (82545EM)
virtio 1AF4h 1000h 虚拟化网络(virtio-net)

由于在大多数情况下虚拟化网络具有更好的性能,我们将探讨如何使用 virtio-net 网络适配器支持 PXE 引导。

下载和构建 LAN 引导 ROM

网络上可能有可用的 LAN 引导 ROM 二进制镜像,但它们在 gPXE 项目中并未提供。我们必须根据 gPXE 项目网站上的说明从源代码构建。

让我们使用以下命令下载和构建源代码:

$ git clone git://git.etherboot.org/scm/gpxe.git
$ cd gpxe/src
$ make bin/1af41000.rom  # for virtio 1af4:1000  

修复 ROM 镜像

在 ROM 镜像可以使用之前,由于 VirtualBox 对 ROM 镜像大小有以下要求,因此必须更新 ROM 镜像:

  • 大小必须是 4K 对齐的(即 4,096 的倍数)

  • 大小不能超过 64K

首先检查镜像大小,确保它不超过 65,536 字节(64K):

$ ls -l bin/1af41000.rom | awk '{print $5}'
62464  

我们可以看到它小于 64K。现在我们必须将镜像文件填充到 4K 边界。我们可以使用以下命令来完成此操作:

$ python
>>> 65536 - 62464             # Calculate padding size
3072
>>> f = open('bin/1af41000.rom', 'a')
>>> f.write('\0' * 3072)      # Pad with zeroes
>>> f.close()  

我们再次检查镜像文件大小:

$ ls -l bin/1af41000.rom | awk '{print $5}'
65536  

如我们所见,文件大小现在是 64K。为了方便,我将在以下链接上传此文件,您可以下载它:

sourceforge.net/projects/android-system-programming/files/android-7/ch14/1af41000.rom/download

配置虚拟机使用 LAN 引导 ROM

基于用户的 VirtualBox 配置可以存储在$HOME/.VirtualBox文件夹中,我们需要使用此文件夹来配置内置的 PXE 服务器。

此文件夹默认不会创建,因此我们需要首先创建它:

$ mkdir .VirtualBox  

在我们创建这个文件夹后,我们可以启动 VirtualBox 并退出。然后,让我们再次查看 $HOME/.VirtualBox 文件夹的内容,如下面的截图所示:

图片

从前面的截图,我们可以看到在运行 VirtualBox 之前,这个文件夹的内容是空的。在执行 VirtualBox 并退出后,这个文件夹中会创建一系列由 VirtualBox 生成的文件。

现在,我们可以更改配置以使用我们刚刚创建的 LAN 启动 ROM。要使用此 LAN 启动 ROM,我们可以使用 VBoxManage 命令更新 VirtualBox 设置。我们使用以下命令设置 LanBootRom 路径:

$ VBoxManage setextradata global VBoxInternal/Devices/pcbios/0/Config/LanBootRom $HOME/.VirtualBox/1af41000.rom  

我们将 LAN 启动 ROM 复制到了 $HOME/.VirtualBox/1af41000.rom。在这里我们使用 global,然后所有虚拟机都将使用 gPXE LAN 启动 ROM。我们可以将 global 改为特定的虚拟机名称。在这种情况下,gPXE LAN 启动 ROM 将仅由该虚拟机使用。

设置好配置后,让我们看看 $HOME/.VirtualBox/VirtualBox.xml 配置文件:

<?xml version="1.0"?> 
<!-- 
** DO NOT EDIT THIS FILE. 
** If you make changes to this file while any VirtualBox related application 
** is running, your changes will be overwritten later, without taking effect. 
** Use VBoxManage or the VirtualBox Manager GUI to make changes. 
--> 
<VirtualBox  version="1.12-linux"> 
  <Global> 
    <ExtraData> 
      <ExtraDataItem name="GUI/DetailsPageBoxes" 
      value="general,system,preview,display,storage,audio,
      network,usb,sharedFolders,description"/> 
      <ExtraDataItem name="GUI/LastWindowPosition" 
      value="475,240,770,550"/> 
      <ExtraDataItem name="GUI/SplitterSizes" value="255,511"/> 
      <ExtraDataItem name="GUI/UpdateCheckCount" value="2"/> 
      <ExtraDataItem name="GUI/UpdateDate" value="1 d, 2017-05-15, 
      stable, 5.1.2"/> 
<ExtraDataItem 
 name="VBoxInternal/Deices/pcbios/o/Config/LanBootRom"
 value="/home/roger/.VirtualBox/1af41000.rom"/> 
    </ExtraData> 
... 

如我们所见,VBoxInternal/Deices/pcbios/o/Config/LanBootRom 配置设置在这个配置文件中。

要删除前面的配置,我们只需将路径值重置如下。$VM_NAME 参数可以是 global 或虚拟机名称:

$ VBoxManage setextradata $VM_NAME VBoxInternal/Devices/pcbios/0/Config/LanBootRom

您也可以使用以下命令检查当前配置:

$ VBoxManage getextradata $VM_NAME VBoxInternal/Devices/pcbios/0/Config/LanBootRom
Value: /home/roger/.VirtualBox/1af41000.rom  

设置 PXE 启动环境

在安装了适当的 PXE ROM 之后,我们现在可以设置 PXE 服务器了。在我们设置 PXE 服务器之前,我们需要考虑网络连接。在 VirtualBox 中,虚拟机可以通过三种方式连接到网络:

  • 桥接网络:这连接到与主机相同的物理网络。看起来虚拟机连接到与主机相同的 LAN 连接。

  • 仅主机网络:这连接到仅由虚拟机和主机可见的虚拟网络。在这个配置中,虚拟机无法连接到外部网络,例如互联网。

  • NAT 网络:这个连接通过 NAT 连接到主机网络。这是最常见的选项。在这个配置中,虚拟机可以访问外部网络,但外部网络无法直接连接到虚拟机。例如,如果您在虚拟机上设置 FTP 服务,主机局域网中的计算机无法访问此 FTP 服务。如果您想发布此服务,您必须使用端口转发设置来完成此操作。

在理解了这些概念之后,如果您想使用专用机器作为 PXE 服务器,您可以在环境中使用桥接网络。但是,您必须非常小心使用这种设置。这通常由您组织的 IT 部门完成,因为您不能在局域网中设置 DHCP 服务器而不影响其他人。我们这里不会使用这个选项。

仅主机网络实际上是这种情况下的一个好选择,因为这种网络是一种隔离的网络配置。网络连接仅存在于主机和虚拟机之间。可以使用仅主机网络来设置 PXE 服务器,但我们在设置中不会使用此选项。

在 VirtualBox 中,NAT 网络支持 PXE 启动。使用此选项,我们不需要自己设置单独的 PXE 服务器。本书中将使用此内置 PXE 服务器。从本章到第十四章 自定义和调试恢复 的测试环境将使用此设置。

配置和测试 PXE 启动

我们可以创建一个虚拟机实例来测试环境。我们将以 Ubuntu 14.04 环境为例进行演示。相同的设置也可以复制到 Windows 或 OS X 环境。

设置虚拟机

让我们在 VirtualBox 中首先创建一个名为 pxeAndroid 的虚拟机。启动 VirtualBox 后,我们可以点击新建按钮来创建一个新的虚拟机,如下面的截图所示:

图片

我们称之为 pxeAndroid,并选择 Linux 作为虚拟机的类型。我们可以直接按照向导创建具有合适配置的虚拟机。虚拟机创建完成后,我们需要对设置进行一些修改。

需要更改的第一件事是网络配置。我们需要将网络适配器设置为 NAT 网络。我们可以先点击虚拟机的名称,pxeAndroid,然后点击设置按钮来更改设置。在左侧选择“网络”选项,如下面的截图所示:

图片

我们选择适配器 1,NAT 网络的默认适配器。由于我们将使用我们刚刚构建的 PXE ROM,我们需要将适配器类型更改为虚拟化网络(virtio-net)。NAT 网络可以连接到外部网络。它支持端口转发,这样我们就可以访问虚拟机中的某些服务。我们需要设置的是 ADB 服务。我们需要使用 ADB 来调试 x86vbox 设备。我们可以按照以下方式设置 ADB 的端口转发:

图片

接下来,我们可以点击“系统”选项来指定默认的启动顺序是从网络接口启动,如下面的截图所示:

图片

使用 VirtualBox 内部 PXE 启动与 NAT

一旦我们设置了虚拟机,我们就可以使用 VirtualBox 内置的 PXE 服务器通过 NAT 网络进行 PXE 启动。要使用内置 PXE 服务器,我们需要按照以下步骤进行设置:

  1. 创建一个 $HOME/.VirtualBox/TFTP 文件夹。Linux 上的内置 TFTP 根目录位于 $HOME/.VirtualBox/TFTP,Windows 上的 %USERPROFILE%\.VirtualBox\TFTP

  2. 通常,对于 PXE 引导,默认引导映像名称是pxelinux.0,但对于 VirtualBox 内置的 PXE,它是vmname.pxe。例如,如果我们使用pxeAndroid作为虚拟机名称,我们必须在TFTP根目录下复制pxelinux.0并将其命名为pxeAndroid.pxe

配置 pxelinux.cfg

在我们能够测试刚刚设置的虚拟机之前,我们需要在配置文件中指定它,以便让 PXE 引导知道在哪里找到内核和 ramdisk 映像。

PXE 引导过程大致如下:

  1. 当 pxeAndroid 虚拟机开机时,客户端将通过 DHCP 获取 IP 地址。

  2. 找到 DHCP 配置后,配置包括标准信息,如 IP 地址、子网掩码、网关和 DNS 等。此外,它还提供了 TFTP 服务器的位置和引导映像的文件名。引导映像的名称通常是pxelinux.0。对于内置 PXE 引导环境,引导映像的名称是vmname.pxe,其中vmname应该是虚拟机的名称。例如,对于我们的虚拟机,它是pxeAndroid.pxe

  3. 客户端联系 TFTP 服务器以获取引导映像。引导映像应放在TFTP根目录下,在我们的例子中是$HOME/.VirtualBox/TFTP

  4. TFTP 服务器发送引导映像(pxelinux.0vmname.pxe),客户端执行它。

  5. 默认情况下,引导映像在 TFTP 服务器上的pxelinux.cfg目录中搜索引导配置文件。

  6. 客户端下载它需要的所有文件(内核、ramdisk、根文件系统等),然后加载它们。

  7. pxeAndroid目标机器重新启动。

在第 5 步中,引导映像按照以下步骤搜索引导配置文件:

  1. 首先,它搜索根据表示为带短划线分隔的小写十六进制数字的 MAC 地址命名的引导配置文件。例如,对于 MAC 地址 08:00:27:90:99:7B,它搜索文件08-00-27-90-99-7b

  2. 然后,它使用大写十六进制数字中的机器(正在引导的机器)的 IP 地址来搜索配置文件。例如,对于 IP 地址 192.168.56.100,它搜索C0A83864文件。

  3. 如果找不到该文件,它将从末尾删除一个十六进制数字并再次尝试。然而,如果搜索仍然不成功,它最终会寻找一个名为default(小写)的文件。

例如,如果引导文件名为$HOME/.VirtualBox/TFTP/pxeAndroid.pxe,以太网 MAC 地址为 08:00:27:90:99:7B,IP 地址为 192.168.56.100,引导映像将按照以下顺序搜索文件名:

$HOME/.VirtualBox/TFTP/pxelinux.cfg/08-00-27-90-99-7b
$HOME/.VirtualBox/TFTP/pxelinux.cfg/C0A83864
$HOME/.VirtualBox/TFTP/pxelinux.cfg/C0A8386
$HOME/.VirtualBox/TFTP/pxelinux.cfg/C0A838
$HOME/.VirtualBox/TFTP/pxelinux.cfg/C0A83
$HOME/.VirtualBox/TFTP/pxelinux.cfg/C0A8
$HOME/.VirtualBox/TFTP/pxelinux.cfg/C0A
$HOME/.VirtualBox/TFTP/pxelinux.cfg/C0
$HOME/.VirtualBox/TFTP/pxelinux.cfg/C
$HOME/.VirtualBox/TFTP/pxelinux.cfg/default  

pxelinux.0引导映像是 Syslinux 开源项目的一部分。我们可以使用以下命令从 Syslinux 项目获取引导映像和菜单用户界面:

$ sudo apt-get install syslinux  

安装 Syslinux 后,pxelinux.0可以按照以下方式复制到TFTP根目录:

$ cp /usr/lib/syslinux/pxelinux.0 $HOME/.VirtualBox/TFTP/pxelinux.0  

为了有一个更好的用户界面,我们还可以将menu.c32复制到TFTP文件夹中:

$ cp /usr/lib/syslinux/menu.c32 $HOME/.VirtualBox/TFTP/menu.c32  

pxelinux.cfg/default

现在,我们将查看如何配置启动配置文件$HOME/.VirtualBox/TFTP/pxelinux.cfg/default。在我们的设置中,它看起来像以下代码片段:

prompt 1 
default menu.c32 
timeout 100 

label 1\. NFS Installation (serial port) - x86vbox 
menu x86vbox_install_serial 
kernel x86vbox/kernel 
append ip=dhcp console=ttyS3,115200 initrd=x86vbox/initrd.img root=/dev/nfs rw androidboot.hardware=x86vbox INSTALL=1 DEBUG=2 SRC=/x86vbox ROOT=10.0.2.2:/home/sgye/vol1/android-x86vbox/out/target/product qemu=1 qemu.gles=0 

label 2\. x86vbox (ROOT=/dev/sda1, serial port) 
menu x86vbox_sda1 
kernel x86vbox/kernel 
append ip=dhcp console=ttyS3,115200 initrd=x86vbox/initrd.img androidboot.hardware=x86vbox DEBUG=2 SRC=/android-x86vbox ROOT=/dev/sda1 
... 

之前提到的文件可以从github.com/shugaoye/asp-sample/blob/master/ch09/pxelinux.cfg/default下载。

您可以从上述 GitHub URL 复制它,并且需要将 NFS 共享文件夹更改为您自己的ROOT=10.0.2.2:/{您的 NFS 共享文件夹}

启动配置文件中的语法可以在 Syslinux 项目的以下 URL 中找到:

www.syslinux.org/wiki/index.php?title=SYSLINUX

在本章中我们使用的配置文件中,我们可以看到以下命令和选项:

  • prompt:它将让引导加载程序知道是否会显示 LILO 风格的boot:提示。使用此命令行提示,您可以直接输入选项。所有启动选项都由label命令定义。

  • default:定义默认启动选项。

  • timeout:如果有多个label条目可用,此指令表示在启动提示符处暂停多长时间直到自动启动,单位为 1/10 秒。按下任何键时取消超时,假设用户将完成命令行。超时设置为 0 将完全禁用超时。默认值为 0。

  • label:一个描述内核和选项的易读字符串。默认标签是linux,但您可以使用DEFAULT关键字更改它。

  • kernel:引导映像将启动的内核文件。

  • append:在引导过程中可以传递给内核的内核命令行。

在之前的配置文件中,我们展示了两个启动选项。在第一个选项中,我们可以使用 NFS 根文件系统启动到最小 Linux 环境。我们可以从该环境安装 x86vbox 镜像到硬盘。在第二个选项中,我们可以从/dev/sda1磁盘分区启动 x86vbox。我们将在稍后详细探讨这些选项。

设置调试用的串行端口

我们想要使用 PXE 和 NFS 启动 Android 的原因是因为我们想要使用一个非常简单的引导加载程序,并找到一种更容易调试系统的方法。为了查看调试日志,我们想要将视频控制台的调试输出重定向到串行端口,以便我们可以将图形用户界面与调试输出分开。为了达到我们的目标,我们需要做两件事。

Linux 内核调试消息可以通过内核命令行参数重定向到特定通道。我们在 PXE 引导配置中使用console=ttyS3,115200选项指定此选项。这在pxelinux.cfg/default中定义如下:

label 1\. NFS Installation (serial port) - x86vbox 
menu x86vbox_install_serial 
kernel x86vbox/kernel 
append ip=dhcp console=ttyS3,115200 initrd=x86vbox/initrd.img root=/dev/nfs rw androidboot.hardware=x86vbox INSTALL=1 DEBUG=2 SRC=/x86vbox ROOT=10.0.2.2:/home/sgye/vol1/android-x86vbox/out/target/product qemu=1 qemu.gles=0 

我们将在本章后面解释 append 选项中关于内核参数的详细信息。接下来,我们需要创建一个可以连接到的虚拟串行端口。我们可以在虚拟机设置页面中配置此端口,如图所示:

图片

我们使用主机管道来模拟虚拟串行端口。我们可以将路径设置为类似 /tmp/pxeAndroid_p 的内容。

COMx/dev/ttySx 的映射如下:

/dev/ttyS0 - COM1 
/dev/ttyS1 - COM2 
/dev/ttyS2 - COM3 
/dev/ttyS3 - COM4 

要连接到主机管道,我们可以使用 minicom 这样的工具。如果您还没有安装 minicom,可以按照以下步骤安装和配置 minicom

$ sudo apt-get install minicom  

要设置 minicom,我们可以使用以下命令:

$ sudo minicom -s  

minicom 启动后,选择串行端口设置,并将串行设备设置为 unix#/tmp/pxeAndroid_p。完成此操作后,选择将设置保存为默认并从 Minicom 中退出,如图所示。现在我们可以使用 minicom 连接到虚拟串行端口。

图片

在我们对 x86vbox 配置进行了所有更改后,我们可以开启虚拟机并对其进行测试。我们应该能够看到以下引导屏幕:

图片

从前面的屏幕截图我们可以看到,虚拟机加载了 pxelinux.cfg/default 文件,并等待引导提示。现在我们可以从 PXE ROM 引导了。

NFS 文件系统

我们在 第八章 中创建了 x86vbox 设备,在 VirtualBox 中创建自己的设备,并且我们能够构建它。然而,我们没有讨论如何引导镜像。这里的问题是构建输出的标准 AOSP 镜像不能直接由 VirtualBox 使用。例如,system.img 可以由模拟器使用,但不能由 VirtualBox 使用。VirtualBox 可以使用标准虚拟磁盘镜像,格式为 VDI、VHD 或 VMDK,但不能使用像 system.img 这样的原始磁盘镜像。

在 Android-x86 构建中,输出是安装镜像,例如 ISO 或 USB 磁盘镜像格式。使用安装镜像,它可以烧录到 CDROM 和 USB 驱动器上。然后,我们可以从 CDROM 或 USB 引导 VirtualBox 来安装系统,就像我们在 PC 上安装 Windows 一样。当我们调试系统时,使用这种方法既繁琐又低效。作为开发者,我们希望有一个简单快捷的方法,这样我们就可以在构建系统后立即开始调试。

我们在这里将使用的方法是使用 NFS 文件系统来引导系统。关键点是,我们将直接将 AOSP 构建输出的输出文件夹视为根文件系统,这样我们就可以用它来引导系统而无需任何额外的工作。

如果您是嵌入式系统开发者,您可能已经在工作中使用过这种方法。当我们对嵌入式 Linux 系统的初始调试阶段进行工作时,我们经常使用 NFS 文件系统作为根文件系统。使用这种方法,我们可以在每次构建后避免将镜像刷入闪存存储。

准备内核

要支持 NFS 引导,我们需要一个具有 NFS 文件系统支持的 Linux 内核。Android 的默认 Linux 内核没有 NFS 引导支持。为了引导 Android 并将 NFS 目录作为根文件系统挂载,我们必须重新编译 Linux 内核,并启用以下选项:

CONFIG_IP_PNP=y 
CONFIG_IP_PNP_DHCP=y 
CONFIG_IP_PNP_BOOTP=y 
CONFIG_IP_PNP_RARP=y 
CONFIG_USB_USBNET=y 
CONFIG_USB_NET_SMSC95XX=y 
CONFIG_USB=y 
CONFIG_USB_SUPPORT=y 
CONFIG_USB_ARCH_HAS_EHCI=y 
CONFIG_NETWORK_FILESYSTEMS=y 
CONFIG_NFS_FS=y 
CONFIG_NFS_V3=y 
CONFIG_NFS_V3_ACL=y 
CONFIG_ROOT_NFS=y 

我们可以使用menuconfig来更改内核配置或复制带有 NFS 支持的配置文件。

要使用menuconfig配置内核构建,我们可以使用以下命令:

$ source build/envsetup.sh
$ lunch x86vbox-eng
$ make -C kernel O=$OUT/obj/kernel ARCH=x86 menuconfig

我们还可以使用我在 GitHub 上启用了 NFS 的配置文件。我们可以如下观察此配置文件与 Android-x86 默认内核配置文件的差异:

$ diff kernel/arch/x86/configs/android-x86_defconfig ~/src/android-x86_nfs_defconfig
216a217
> # CONFIG_SYSTEM_TRUSTED_KEYRING is not set
1083a1085
> CONFIG_DNS_RESOLVER=y
1836c1838
< CONFIG_VIRTIO_NET=m
---
> CONFIG_VIRTIO_NET=y
1959c1961
< CONFIG_E1000=m
---
> CONFIG_E1000=y
5816a5819
> # CONFIG_ECRYPT_FS is not set
5854,5856c5857,5859
< CONFIG_NFS_FS=m
< CONFIG_NFS_V2=m
< CONFIG_NFS_V3=m
---
> CONFIG_NFS_FS=y
> CONFIG_NFS_V2=y
> CONFIG_NFS_V3=y
5858c5861
< # CONFIG_NFS_V4 is not set
---
> CONFIG_NFS_V4=y
5859a5863,5872
> CONFIG_NFS_V4_1=y
> CONFIG_NFS_V4_2=y
> CONFIG_PNFS_FILE_LAYOUT=y
> CONFIG_PNFS_BLOCK=y
> CONFIG_NFS_V4_1_IMPLEMENTATION_ID_DOMAIN="kernel.org"
> # CONFIG_NFS_V4_1_MIGRATION is not set
> CONFIG_NFS_V4_SECURITY_LABEL=y
> CONFIG_ROOT_NFS=y
> # CONFIG_NFS_USE_LEGACY_DNS is not set
> CONFIG_NFS_USE_KERNEL_DNS=y
5861,5862c5874,5875
< CONFIG_GRACE_PERIOD=m
< CONFIG_LOCKD=m
---
> CONFIG_GRACE_PERIOD=y
> CONFIG_LOCKD=y
5865c5878,5880
< CONFIG_SUNRPC=m
---
> CONFIG_SUNRPC=y
> CONFIG_SUNRPC_GSS=y
> CONFIG_SUNRPC_BACKCHANNEL=y
5870a5886
> # CONFIG_CIFS_UPCALL is not set
5873a5890
> # CONFIG_CIFS_DFS_UPCALL is not set
6132c6149,6153
< # CONFIG_KEYS is not set
---
> CONFIG_KEYS=y
> # CONFIG_PERSISTENT_KEYRINGS is not set
> # CONFIG_BIG_KEYS is not set
> # CONFIG_ENCRYPTED_KEYS is not set
> # CONFIG_KEYS_DEBUG_PROC_KEYS is not set
6142a6164
> # CONFIG_INTEGRITY_SIGNATURE is not set
6270a6293
> # CONFIG_ASYMMETRIC_KEY_TYPE is not set
6339a6363
> CONFIG_ASSOCIATIVE_ARRAY=y
6352a6377
> CONFIG_OID_REGISTRY=y  

我们可以复制此配置文件并使用它来构建 Linux 内核。以下命令仅显示如何单独构建内核。如果您通过检查本章的源代码构建 x86vbox,则无需执行此操作。这包含在 x86vbox 设备的 Makefiles 中:

$ repo init https://github.com/shugaoye/manifests -b android-7.1.1_r4_x86vbox_ch08_r1
$ repo sync
$ source build/envsetup.sh
$ lunch x86vbox-eng
$ make -C kernel O=$OUT/obj/kernel ARCH=x86  

构建完成后,我们可以将内核和 ramdisk 文件复制到TFTP根目录$HOME/.VirtualBox/TFTP/x86vbox

设置 NFS 服务器

当我们有一个具有 NFS 功能的内核时,我们需要在我们的开发主机上设置 NFS 服务器,以便我们可以挂载 NFS 服务器导出的 NFS 文件夹。我们可以使用以下命令检查 NFS 服务器是否已经安装:

$ dpkg -l | grep nfs  

如果 NFS 服务器未安装,我们可以使用以下命令安装它:

$ sudo apt-get install nfs-kernel-server  

一旦我们有一个 NFS 服务器准备就绪,我们需要通过 NFS 导出我们的根文件系统。我们将使用之前提到的 AOSP 构建输出文件夹。我们可以在/etc/exports配置文件中添加以下行:

$AOSP/out/target/product/ *(rw,sync,insecure,no_subtree_check,async) 

之后,我们执行以下命令来导出$AOSP/out/target/product文件夹。您需要用您设置中的绝对路径替换$AOSP

$ sudo exportfs -a  

配置 PXE 引导菜单

当我们有一个真实的引导加载程序,如 PXE 引导 ROM 时,我们有一种支持引导路径的方式,就像真正的 Android 设备一样。正如我们所知,Android 设备可以启动到三种不同的模式--引导加载程序模式、恢复模式和正常启动。

使用 PXE 引导 ROM,我们可以轻松支持相同和更多的功能。通过配置pxelinux.cfg/default文件,我们可以允许 x86vbox 在不同的路径上启动。我们将在这里配置多个引导路径。

启动到 NFS 安装

由于我们不能使用 AOSP 镜像文件直接启动 x86vbox,我们需要将 AOSP 镜像安装到 VirtualBox 硬盘上。这与 Android-x86 非常相似。在 Android-x86 中,我们需要使用 CDROM 或 USB 闪存驱动器来安装系统,以便在安装后启动 Android。而不是使用安装的 CDROM 或 USB 镜像,我们可以直接从 NFS 路径安装系统。如果我们设置 NFS 路径为$AOSP/out/target/product路径,我们可以在构建完成后立即安装系统。

我们可以将系统引导到安装模式,这样我们就可以使用我们之前讨论的 Android-x86 安装脚本将 x86vbox 镜像安装到虚拟硬盘:

label 1\. NFS Installation (serial port) - x86vbox 
menu x86vbox_install_serial 
kernel x86vbox/kernel 
append ip=dhcp console=ttyS3,115200 initrd=x86vbox/initrd.img root=/dev/nfs rw androidboot.hardware=x86vbox INSTALL=1 DEBUG=2 SRC=/x86vbox ROOT=10.0.2.2:$AOSP/out/target/product 

在前面的配置中,我们使用TFTP文件夹中的 NFS 兼容内核,例如$HOME/.VirtualBox/TFTP/x86vbox/kernelinitrd.img ramdisk 镜像也存储在同一个文件夹中。TFTP文件夹下的这两个文件实际上可以是指向 AOSP 输出的符号链接。在这种情况下,我们不需要在构建后复制它们,如下面的截图所示:

图片

我们使用以下三种选项来配置 NFS 引导:

  • ip=dhcp:使用 DHCP 从 DHCP 服务器获取 IP 地址。DHCP 服务器可以是 VirtualBox 的内置 DHCP 服务器或外部 DHCP 服务器。

  • root=/dev/nfs:使用 NFS 引导。

  • ROOT=10.0.2.2:$AOSP/out/target/product:根是开发主机上的 AOSP 输出文件夹。如果我们使用内置 PXE,IP 地址10.0.2.2是 NAT 网络中的默认主机 IP 地址。它可以通过 VirtualBox 配置进行更改。在你的配置中,你需要将$AOSP替换为一个绝对路径。

我们想监控调试输出,因此将控制台设置为之前配置的虚拟串行端口console=ttyS3,115200。我们可以使用minicom通过主机管道连接到它。

我们通过使用 Android-x86 init 脚本和安装脚本设置了三个内核参数:

  • INSTALL=1:告诉 init 脚本我们想要安装系统

  • DEBUG=2:这将使我们能够在引导过程中进入调试控制台

  • SRC=/x86vbox:这是根文件系统的目录

最后,androidboot.hardware=x86vbox选项被传递给 Android init 进程,以告诉它运行哪个 init 脚本。在这种情况下,设备 init 脚本init.x86vbox.rc将按照我们在上一章中讨论的方式执行。

在我们的 PXE 引导菜单中,我们可以添加另一个配置,以便在没有console=ttyS3,115200选项的情况下进行安装。在这种情况下,所有调试输出将打印到屏幕上,这是默认的标准输出。

要找出硬盘上安装了什么,你可以参考第六章,使用自定义 ramdisk 调试引导过程。硬盘上的文件系统布局与x86emu_x86.img的目录布局相似。

从硬盘引导

在使用之前的配置安装系统后,我们可以选择另一个选项,如下所示,从硬盘引导系统:

label 2\. x86vbox (ROOT=/dev/sda1, serial port) 
menu x86vbox_sda1_S3 
kernel x86vbox/kernel 
append ip=dhcp console=ttyS3,115200 initrd=x86vbox/initrd.img androidboot.hardware=x86vbox DEBUG=2 SRC=/android-x86vbox ROOT=/dev/sda1 

在前面的配置中,我们使用/dev/sda1设备作为根,并且没有INSTALL=1选项。使用此配置,虚拟机将从硬盘/dev/sda1引导到 Android 系统,并且调试输出将打印到虚拟串行端口。

我们可以使用另一个类似的配置,将调试输出打印到屏幕。

进入恢复模式

使用 PXE 引导菜单,我们还可以配置系统引导到恢复。我们可以使用以下配置:

label 5\. x86vbox recovery (ROOT=/dev/sda2) 
menu x86vbox_recovery 
kernel x86vbox/kernel 
append ip=dhcp console=ttyS3,115200 initrd=x86vbox/ramdisk-recovery.img androidboot.hardware=x86vbox DEBUG=2 SRC=/android-x86vbox ROOT=/dev/sda2 

当我们探索恢复编程时,我们将在第十二章“介绍恢复”到第十四章“定制和调试恢复”中使用与此类似的配置。这里的区别在于我们使用恢复 ramdisk 而不是initrd.img。由于恢复是一个自包含的环境,我们还可以将ROOT变量设置为其他分区。

注意,x86vbox recovery配置在本章中无法进行测试。我们将在第十二章“介绍恢复”到第十四章“定制和调试恢复”中测试此配置。

在完成所有前面的设置后,我们可以启动到 PXE 引导菜单,如下面的截图所示:

图片

我们可以从前面的 PXE 引导菜单中选择第一个选项来引导到调试控制台,如下所示:

图片

从前面的调试输出中,我们可以看到虚拟机从 DHCP 服务器10.0.2.2获得了 IP 地址10.0.2.15。NFS 根文件系统位于 IP 地址10.0.2.2,即开发主机。在默认的 VirtualBox NAT 网络设置中,DHCP 服务器或主机的 IP 地址为10.0.2.2。内置 TFTP 服务器的 IP 地址为10.0.2.4。DNS 服务器 IP 地址为10.0.2.3

使用 NFS 文件系统从$OUT/system目录启动 Android 系统是可能的。然而,我们需要对netd进行修改以禁用刷新路由规则。这些修改可以在以下文件中的flushRules函数中完成:

$AOSP/system/netd/server/RouteController.cpp

如果不进行此更改,则在刷新路由规则后,网络连接将被重置。然而,我们仍然可以使用 NFS 引导执行第一阶段引导或安装系统到硬盘。

摘要

在本章中,我们学习了一种使用 PXE 引导和 NFS 根文件系统的组合进行调试的方法。这在嵌入式 Linux 开发领域是一种常见做法。我们尝试为 Android 系统开发使用类似的设置。正如我们所看到的,这种设置可以使开发和调试过程更加高效。我们可以使用这个设置来移除引导加载程序的依赖。我们还可以减少将构建镜像刷入或配置到设备上的时间。

我写了一篇文章来讨论使用在主机-only 网络环境中运行的外部 DHCP/TFTP 服务器进行 PXE/NFS 设置的更高级案例。如果您对这个主题感兴趣,您可以在以下 URL 中阅读它:

www.packtpub.com/books/content/booting-android-system-using-pxenfs

在下一章中,我们将继续我们的旅程,探讨 x86vbox 的启动过程。我们将探索和学习如何启用 VirtualBox 上的图形系统,以便最终为 x86vbox 设备启动 Android 系统。

第十章:启用图形

在上一章中,我们学习了如何使用 PXE 和 NFS 启动 x86vbox 设备。我们可以将设备引导到嵌入式 Linux 环境,这是 Android-x86 引导的第一阶段。在这个阶段,我们可以使用调试控制台来验证系统的状态,以确保我们在启动真正的 Android 系统之前一切正常。在本章中,我们将讨论在 Android 系统启动过程中遇到的第一问题。这是关于如何为 x86vbox 设备启用 Android 图形系统。本章将涵盖以下主题:

  • Android 图形架构概述

  • 深入探讨图形 HAL

  • 分析 Android 模拟器图形 HAL 进行比较

图形系统可能是 Android 系统架构中最复杂的软件堆栈。

正如您将看到的,本章的内容比其他章节都要长。阅读和理解本章的内容可能更困难。我的建议是,您可以在阅读本章的同时打开源代码编辑器并加载相关源代码。这将极大地帮助您理解源代码以及我在本章中想要阐述的点。

Android 图形架构简介

Android 中的图形系统与我们讨论过的架构相似,即第三章中提到的发现内核、HAL 和虚拟硬件。在那里,我们以 goldfish lights HAL 为例,从应用层到 HAL 和设备驱动层进行了详细分析。这种分析有助于我们垂直理解 Android 架构。

然而,图形系统可能是 Android 架构中最复杂的系统。需要另一本书来详细介绍 Android 图形系统。本书的重点是如何将 Android 系统移植到新的硬件平台。为了专注于这个目标,我们将在本章中讨论图形 HAL,而不是整个图形系统。如果我们能选择正确的图形 HAL 并正确配置它,图形系统就能正常工作。

根据谷歌关于图形实现文档,Android 图形支持需要以下组件:

  • EGL 驱动程序

  • OpenGL ES 1.x 驱动程序

  • OpenGL ES 2.0 驱动程序

  • OpenGL ES 3.x 驱动程序(可选)

  • Vulkan(可选)

  • Gralloc HAL 实现

  • 硬件合成器 HAL 实现

在前面的列表中,OpenGL ES 实现是图形系统中最复杂的组件。我们将讨论它如何在 Android 模拟器和 Android-x86 中选取和集成,但我们不会深入讨论如何分析 OpenGL ES 实现,而是会概述底层的 OpenGL ES 库。Android 系统中必须支持 OpenGL ES 1.x 和 2.0。OpenGL ES 3.x 目前是一个可选组件。EGL 驱动通常作为 OpenGL ES 实现的一部分实现,我们将在讨论 Android 模拟器和 Android-x86(x86vbox)图形系统时看到这一点。

Vulkan 是 Khronos Group 新一代 GPU API。Vulkan 是新的且可选的,仅在 Android 7 中引入。涵盖 Vulkan 超出了本书的范围,因此我们不会讨论它。Gralloc HAL 是处理图形硬件的 HAL,是我们深入分析的重点。在大多数图形系统的移植工作中,Gralloc HAL 是启用图形的关键。

硬件合成器是图形 HAL 的一部分。然而,它不是 Android 模拟器或 Android-x86 必须拥有的组件。硬件合成器HWC)HAL 用于将表面合成到屏幕上。HWC 抽象了如叠加层等对象,并帮助卸载一些通常使用 OpenGL 完成的工作。

图片 10_001

Android 图形架构

如前所述的 Android 图形架构图所示,我们也可以将相关组件在 Android 架构中划分为不同的层,就像我们在前面的章节中所做的那样。这个架构图是图形系统的一个简化视图。SurfaceFlinger是提供图形相关系统支持的系统服务,面向应用层。SurfaceFlinger将连接到OpenGL ES库和HAL层组件以执行实际工作。在HAL中,我们有HWCgralloc以及与内核空间中的驱动程序通信的特定于供应商的 GPU 库。

深入分析图形 HAL

在我们了解图形系统架构概述之后,我们将分析 Gralloc 模块,它是图形 HAL。在 AOSP 源代码中,Gralloc HAL 实现的骨架可以在以下文件夹中找到:

$AOSP/hardware/libhardware/modules/gralloc

这是一个通用实现,为开发者提供创建他们自己的 Gralloc 模块的参考。Gralloc 将访问帧缓冲区和 GPU,为上层提供服务。在本节中,我们将首先分析这个通用实现。在分析完这个通用的 Gralloc HAL 模块之后,我们将介绍 Android 模拟器的 Gralloc HAL。

加载 Gralloc 模块

当应用程序开发者将图像绘制到屏幕上时,有两种方式可以实现。他们可以使用 Canvas 或 OpenGL。从 Android 4.0 开始,这两种方法默认都使用硬件加速。要使用硬件加速,我们需要使用 Open GL 库,最终 Gralloc 模块将作为图形系统初始化的一部分被加载。正如我们在第三章“发现内核、HAL 和虚拟硬件”中看到的,每个 HAL 模块都有一个引用 ID,该 ID 可以被 hw_get_module 函数用来将其加载到内存中。hw_get_module 函数定义在 $AOSP/hardware/libhardware/hardware.c 文件中:

int hw_get_module(const char *id, const struct hw_module_t **module) 
{ 
    return hw_get_module_by_class(id, NULL, module); 
} 

hw_get_module 中,它实际上调用另一个函数,hw_get_module_by_class 来完成工作:

int hw_get_module_by_class(const char *class_id, const char *inst, 
                           const struct hw_module_t **module) 
{ 
    int i = 0; 
    char prop[PATH_MAX] = {0}; 
    char path[PATH_MAX] = {0}; 
    char name[PATH_MAX] = {0}; 
    char prop_name[PATH_MAX] = {0}; 

    if (inst) 
        snprintf(name, PATH_MAX, "%s.%s", class_id, inst); 
    else 
        strlcpy(name, class_id, PATH_MAX); 

    snprintf(prop_name, sizeof(prop_name), "ro.hardware.%s", name); 
    if (property_get(prop_name, prop, NULL) > 0) { 
        if (hw_module_exists(path, sizeof(path), name, prop) == 0) { 
            goto found; 
        } 
    } 

    for (i=0 ; i<HAL_VARIANT_KEYS_COUNT; i++) { 
        if (property_get(variant_keys[i], prop, NULL) == 0) { 
            continue; 
        } 
        if (hw_module_exists(path, sizeof(path), name, prop) == 0) { 
            goto found; 
        } 
    } 

    /* Nothing found, try the default */ 
    if (hw_module_exists(path, sizeof(path), name, "default") == 0) { 
        goto found; 
    } 

    return -ENOENT; 

found: 
    return load(class_id, path, module); 
} 

在前面的函数中,它试图在 /system/lib/hw/vendor/lib/hw 中使用以下名称查找 Gralloc 模块的共享库:

gralloc.<ro.hardware>.so 
gralloc.<ro.product.board>.so 
gralloc.<ro.board.platform>.so 
gralloc.<ro.arch>.so 

如果上述文件中的任何一个存在,它们将调用 load 函数来加载共享库。如果它们都不存在,将使用默认的共享库 gralloc.default.so。Gralloc 的硬件模块 ID 在 gralloc.h 文件中定义如下:

#define GRALLOC_HARDWARE_MODULE_ID "gralloc" 

load 函数将调用 dlopen 来加载库,并调用 dlsym 来获取数据结构 hw_module_t 的地址:

static int load(const char *id, 
        const char *path, 
        const struct hw_module_t **pHmi) 
{ 
    int status = -EINVAL; 
    void *handle = NULL; 
    struct hw_module_t *hmi = NULL; 

    handle = dlopen(path, RTLD_NOW); 
    if (handle == NULL) { 
        char const *err_str = dlerror(); 
        ALOGE("load: module=%s\n%s", path, err_str?err_str:"unknown"); 
        status = -EINVAL; 
        goto done; 
    } 

    const char *sym = HAL_MODULE_INFO_SYM_AS_STR; 
    hmi = (struct hw_module_t *)dlsym(handle, sym); 
    if (hmi == NULL) { 
        ALOGE("load: couldn't find symbol %s", sym); 
        status = -EINVAL; 
        goto done; 
    } 

    if (strcmp(id, hmi->id) != 0) { 
        ALOGE("load: id=%s != hmi->id=%s", id, hmi->id); 
        status = -EINVAL; 
        goto done; 
    } 

    hmi->dso = handle; 

    status = 0; 

    done: 
    if (status != 0) { 
        hmi = NULL; 
        if (handle != NULL) { 
            dlclose(handle); 
            handle = NULL; 
        } 
    } else { 
        ALOGV("loaded HAL id=%s path=%s hmi=%p handle=%p", 
                id, path, *pHmi, handle); 
    } 

    *pHmi = hmi; 

    return status; 
} 

在我们获取到数据结构 hw_module_t 的地址后,我们可以调用 Gralloc HAL 中定义的 open 方法来初始化帧缓冲区和 GPU。

如我们在第三章“发现内核、HAL 和虚拟硬件”中讨论的,硬件供应商需要实现以下三个 HAL 数据结构:

struct hw_module_t; 
struct hw_module_methods_t; 
struct hw_device_t; 

在 HAL 共享库加载后,数据结构 hw_module_t 被用来发现 HAL 模块,正如我们在前面的代码片段中看到的。每个 HAL 模块都应该在数据结构 hw_module_methods_t 中实现一个 open 方法,该方法负责硬件的初始化。我们可以看到,在以下代码片段中,gralloc_device_open 函数被定义为 Gralloc 模块的 open 方法:

static struct hw_module_methods_t gralloc_module_methods = { 
        .open = gralloc_device_open 
}; 

struct private_module_t HAL_MODULE_INFO_SYM = { 
    .base = { 
        .common = { 
            .tag = HARDWARE_MODULE_TAG, 
            .version_major = 1, 
            .version_minor = 0, 
            .id = GRALLOC_HARDWARE_MODULE_ID, 
            .name = "Graphics Memory Allocator Module", 
            .author = "The Android Open Source Project", 
            .methods = &gralloc_module_methods 
        }, 
        .registerBuffer = gralloc_register_buffer, 
        .unregisterBuffer = gralloc_unregister_buffer, 
        .lock = gralloc_lock, 
        .unlock = gralloc_unlock, 
    }, 
    .framebuffer = 0, 
    .flags = 0, 
    .numBuffers = 0, 
    .bufferMask = 0, 
    .lock = PTHREAD_MUTEX_INITIALIZER, 
    .currentBuffer = 0, 
}; 

在数据结构 hw_module_methods_t 中,open 方法被分配为一个静态函数,gralloc_device_openHAL_MODULE_INFO_SYM 符号定义为 struct private_module_t

你可能会注意到,当我们加载 Gralloc 模块时,实际上是将 HAL_MODULE_INFO_SYM_AS_STR 符号映射到 hw_module_t,而在默认的 Gralloc 模块中,数据结构 hw_module_t 是通过另外两个继承的数据结构 private_module_tgralloc_module_t 实现的。让我们看看 private_module_tgralloc_module_thw_module_t 之间的关系。

如果你在这部分的分析中感到有些困惑,我建议你在阅读这部分内容的同时查看源代码。如果你没有 AOSP 源代码,有一个非常好的 AOSP 代码交叉引用网站 xref.opersys.com/

您可以访问此网站并搜索我们正在讨论的数据结构。

数据结构private_module_t在以下文件中定义:

$AOSP/hardware/libhardware/modules/gralloc/gralloc_priv.h

struct private_module_t { 
    gralloc_module_t base; 

    private_handle_t* framebuffer; 
    uint32_t flags; 
    uint32_t numBuffers; 
    uint32_t bufferMask; 
    pthread_mutex_t lock; 
    buffer_handle_t currentBuffer; 
    int pmem_master; 
    void* pmem_master_base; 

    struct fb_var_screeninfo info; 
    struct fb_fix_screeninfo finfo; 
    float xdpi; 
    float ydpi; 
    float fps; 
}; 

如我们所见,第一个基类字段,或者说在 C++术语中的成员变量,是数据结构gralloc_module_t。第二个成员变量framebufferprivate_handle_t数据类型的指针。它是一个指向 framebuffer 的句柄,我们将在后面探讨它。

成员变量flags用于指示系统是否支持双缓冲。如果支持,则PAGE_FLIP位设置为 1;否则,设置为 0。

numBuffers成员变量表示 framebuffer 中的缓冲区数量。它与可见分辨率和虚拟分辨率相关。例如,如果显示器的可见分辨率为 800 x 600,则虚拟分辨率可以是 1600 x 600。在这种情况下,framebuffer 可以为显示器提供两个缓冲区,并且系统可以支持显示器的双缓冲。

bufferMask成员变量用于标记 framebuffer 设备中缓冲区的使用情况。如果我们假设 framebuffer 中有两个缓冲区,则bufferMask变量在二进制中可以有四个值:00、01、10 和 11。值 00 表示两个缓冲区都为空。值 01 表示第一个缓冲区正在使用,第二个缓冲区为空。值 10 表示第一个缓冲区为空,第二个缓冲区正在使用。值 11 表示两个缓冲区都在使用。

lock成员变量用于保护对private_module_t的访问。

currentBuffer成员变量用于跟踪用于渲染的当前缓冲区。

infofinfo成员变量是数据类型fb_var_screeninfofb_fix_screeninfo。它们用于存储显示设备的属性。fb_var_screeninfo中的属性是可编程的,而fb_fix_screeninfo中的属性是只读的。

xdpiydpi成员变量用于以水平和垂直方向描述像素密度。

fps成员变量是每秒显示的帧数。

gralloc_module_t数据结构在以下文件中定义:

$AOSP/hardware/libhardware/include/hardware/gralloc.h

typedef struct gralloc_module_t { 
    struct hw_module_t common; 
    int (*registerBuffer)(struct gralloc_module_t const* module, 
            buffer_handle_t handle); 
    int (*unregisterBuffer)(struct gralloc_module_t const* module, 
            buffer_handle_t handle); 
    int (*lock)(struct gralloc_module_t const* module, 
            buffer_handle_t handle, int usage, 
            int l, int t, int w, int h, 
            void** vaddr); 
    int (*unlock)(struct gralloc_module_t const* module, 
            buffer_handle_t handle); 
    ... 
} 

如我们所预期,gralloc_module_t中的第一个字段是来自前面代码片段的hw_module_t。这三个数据结构之间的关系类似于以下 UML 类图中的面向对象表示法:

图片

Gralloc 数据结构之间的关系

这是 C 语言中模拟继承关系的方式。这样,我们可以将private_module_t的数据类型转换为gralloc_module_thw_module_t

gralloc_module_t中定义了一组成员函数。在本章中,我们将查看其中的四个。

registerBufferunregisterBuffer 成员函数用于注册或注销一个缓冲区。要注册一个缓冲区,我们需要将缓冲区映射到应用程序的进程空间。

lockunlock 成员函数用于锁定或解锁一个缓冲区。缓冲区使用 buffer_handle_t 作为函数的参数进行描述。我们可以使用 ltwh 参数来提供缓冲区的位置和大小。缓冲区锁定后,我们可以在 vaddr 输出参数中获取缓冲区的地址。使用完毕后,我们应该解锁缓冲区。

初始化 GPU

我们已经讨论了 Gralloc 模块的 HAL 数据结构 hw_module_thw_module_methods_t。最后一个,hw_device_t,在 Gralloc HAL 模块的 open 方法中初始化。现在我们可以查看 Gralloc 模块的 open 方法如下:

int gralloc_device_open(const hw_module_t* module, const char* name, 
        hw_device_t** device) 
{ 
    int status = -EINVAL; 
    if (!strcmp(name, GRALLOC_HARDWARE_GPU0)) { 
        gralloc_context_t *dev; 
        dev = (gralloc_context_t*)malloc(sizeof(*dev)); 

        memset(dev, 0, sizeof(*dev)); 

        dev->device.common.tag = HARDWARE_DEVICE_TAG; 
        dev->device.common.version = 0; 
        dev->device.common.module = const_cast<hw_module_t*>(module); 
        dev->device.common.close = gralloc_close; 

        dev->device.alloc   = gralloc_alloc; 
        dev->device.free    = gralloc_free; 

        *device = &dev->device.common; 
        status = 0; 
    } else { 
        status = fb_device_open(module, name, device); 
    } 
    return status; 
} 

如此可见,gralloc_device_open 函数可以根据输入参数 name 初始化两种设备,GRALLOC_HARDWARE_GPU0GRALLOC_HARDWARE_FB0

让我们先看看 GPU0 设备的初始化。open 方法的输出参数是 hw_device_t 数据结构的地址。调用应用程序获取 hw_device_t 实例后,可以使用硬件设备来完成它们的工作。在 Gralloc HAL 的 open 方法中,它首先为 gralloc_context_t 数据结构分配内存。之后,它填充其 device 成员变量并将输出参数赋值给 dev->device.common 成员变量的地址。正如我们所期望的,输出是 hw_device_t 实例的地址。让我们看看 gralloc_context_talloc_device_thw_device_t 之间的关系:

如前图所示,gralloc_context_t 的第一个字段或成员变量是 device,其数据类型为 alloc_device_t

struct gralloc_context_t { 
    alloc_device_t  device; 
    /* our private data here */ 
}; 

以下是对 alloc_device_t 数据结构的定义。它在 gralloc.h 文件中定义:

typedef struct alloc_device_t { 
    struct hw_device_t common; 

    int (*alloc)(struct alloc_device_t* dev, 
            int w, int h, int format, int usage, 
            buffer_handle_t* handle, int* stride); 

    int (*free)(struct alloc_device_t* dev, 
            buffer_handle_t handle); 

    void (*dump)(struct alloc_device_t *dev, char *buff, int buff_len); 

    void* reserved_proc[7]; 
} alloc_device_t; 

我们可以看到 alloc_device_t 的第一个字段的数据类型是 hw_device_t。这是我们讨论 private_module_tgralloc_module_thw_module_t 之间的关系时提到的 C 语言中模拟继承关系的技巧。

Gralloc 设备的 allocfree 方法在 gralloc.cpp 文件中的 gralloc_allocgralloc_free 函数中实现。

初始化帧缓冲

如果我们使用 GRALLOC_HARDWARE_FB0 作为 name 值调用 Gralloc 模块的 open 方法,它将初始化帧缓冲设备。调用 fb_device_open 函数来打开帧缓冲设备:

status = fb_device_open(module, name, device); 

fb_device_open 函数在 framebuffer.cpp 文件中实现如下:

int fb_device_open(hw_module_t const* module, const char* name, 
        hw_device_t** device) 
{ 
    int status = -EINVAL; 
    if (!strcmp(name, GRALLOC_HARDWARE_FB0)) { 
        /* initialize our state here */ 
        fb_context_t *dev = (fb_context_t*)malloc(sizeof(*dev)); 
        memset(dev, 0, sizeof(*dev)); 

        /* initialize the procs */ 
        dev->device.common.tag = HARDWARE_DEVICE_TAG; 
        dev->device.common.version = 0; 
        dev->device.common.module = const_cast<hw_module_t*>(module); 
        dev->device.common.close = fb_close; 
        dev->device.setSwapInterval = fb_setSwapInterval; 
        dev->device.post            = fb_post; 
        dev->device.setUpdateRect = 0; 

        private_module_t* m = (private_module_t*)module; 
        status = mapFrameBuffer(m); 
        if (status >= 0) { 
            int stride = m->finfo.line_length / 
            (m->info.bits_per_pixel >> 3); 
            /* 
             * Auto detect current depth and select mode 
             */ 
            int format; 
            if (m->info.bits_per_pixel == 32) { 
                format = (m->info.red.offset == 16) ?  
                HAL_PIXEL_FORMAT_BGRA_8888 
                : (m->info.red.offset == 24) ? 
                HAL_PIXEL_FORMAT_RGBA_8888 : 
                HAL_PIXEL_FORMAT_RGBX_8888; 
            } else if (m->info.bits_per_pixel == 16) { 
                format = HAL_PIXEL_FORMAT_RGB_565; 
            } else { 
                ALOGE("Unsupported format %d", m->info.bits_per_pixel); 
                return -EINVAL; 
            } 
            const_cast<uint32_t&>(dev->device.flags) = 0; 
            const_cast<uint32_t&>(dev->device.width) = m->info.xres; 
            const_cast<uint32_t&>(dev->device.height) = m->info.yres; 
            const_cast<int&>(dev->device.stride) = stride; 
            const_cast<int&>(dev->device.format) = format; 
            const_cast<float&>(dev->device.xdpi) = m->xdpi; 
            const_cast<float&>(dev->device.ydpi) = m->ydpi; 
            const_cast<float&>(dev->device.fps) = m->fps; 
            const_cast<int&>(dev->device.minSwapInterval) = 1; 
            const_cast<int&>(dev->device.maxSwapInterval) = 1; 
            *device = &dev->device.common; 
        } 
    } 
    return status; 
} 

fb_device_open 函数中,它为 fb_context_t 数据结构分配内存。之后,它填充数据结构中的字段。正如我们在 GPU0 初始化中讨论的那样,我们期望输出为 hw_device_t 数据结构的一个实例,以便调用者可以通过 hw_device_t HAL 数据结构使用帧缓冲区设备。这三个数据结构 fb_context_tframebuffer_device_thw_device_t 之间有类似的继承关系,如下面的图所示:

fb_context_tframebuffer_device_thw_device_t 之间的关系

fb_context_t 数据结构将 framebuffer_device_t 作为第一个字段,如下所示:

struct fb_context_t { 
    framebuffer_device_t  device; 
}; 

相应地,framebuffer_device_t 数据结构将 hw_device_t 作为第一个字段,因此 fb_context_t 可以用作 framebuffer_device_thw_device_t

typedef struct framebuffer_device_t { 
    struct hw_device_t common; 

    const uint32_t  flags; 

    const uint32_t  width; 
    const uint32_t  height; 

    const int       stride; 

    const int       format; 

    const float     xdpi; 
    const float     ydpi; 

    const float     fps; 

    const int       minSwapInterval; 

    const int       maxSwapInterval; 

    const int       numFramebuffers; 

    int reserved[7]; 
    int (*setSwapInterval)(struct framebuffer_device_t* window, 
            int interval); 
    int (*setUpdateRect)(struct framebuffer_device_t* window, 
            int left, int top, int width, int height); 
    int (*post)(struct framebuffer_device_t* dev, buffer_handle_t 
    buffer); 
    int (*compositionComplete)(struct framebuffer_device_t* dev); 
    void (*dump)(struct framebuffer_device_t* dev, char *buff, int 
    buff_len); 
    int (*enableScreen)(struct framebuffer_device_t* dev, int enable); 
    void* reserved_proc[6]; 

} framebuffer_device_t; 

对于 framebuffer_device_t 中剩余的字段,它们是:

  • flags: 用于描述帧缓冲区的某些属性。

  • widthheight:帧缓冲区的像素尺寸。

  • stride:帧缓冲区的像素步长或每行的像素数。

  • format:帧缓冲区像素格式。可以是 HAL_PIXEL_FORMAT_RGBX_8888HAL_PIXEL_FORMAT_565 等。

  • xdpiydpi:帧缓冲区显示面板的每英寸像素分辨率。

  • fps:显示面板的每秒帧数。

  • minSwapInterval:此帧缓冲区支持的最低交换间隔。

  • maxSwapInterval:此帧缓冲区支持的最高交换间隔。

  • numFramebuffers:支持的帧缓冲区数量。

在填充 framebuffer_device_t 的所有字段之前,fb_device_open 函数调用 mapFrameBuffer 函数以获取帧缓冲区信息。除了获取帧缓冲区信息外,此 mapFrameBuffer 函数还将帧缓冲区映射到当前进程空间,以便当前进程可以使用它。在 Android 中,Gralloc 模块由 SurfaceFlinger 拥有和管理。

让我们看看 mapFrameBuffer 函数:

static int mapFrameBuffer(struct private_module_t* module) 
{ 
    pthread_mutex_lock(&module->lock); 
    int err = mapFrameBufferLocked(module); 
    pthread_mutex_unlock(&module->lock); 
    return err; 
} 

正如我们所见,mapFrameBuffer 首先获取一个互斥锁,然后调用另一个函数 mapFrameBufferLocked 来完成剩余的工作:

int mapFrameBufferLocked(struct private_module_t* module) 
{ 
    // already initialized... 
    if (module->framebuffer) { 
        return 0; 
    } 

    char const * const device_template[] = { 
            "/dev/graphics/fb%u", 
            "/dev/fb%u", 
            0 }; 

    int fd = -1; 
    int i=0; 
    char name[64]; 

    while ((fd==-1) && device_template[i]) { 
        snprintf(name, 64, device_template[i], 0); 
        fd = open(name, O_RDWR, 0); 
        i++; 
    } 
    if (fd < 0) 
        return -errno; 
    ... 

mapFrameBufferLocked 函数中,它检查是否存在 /dev/graphics/fb0/dev/fb0 设备节点。如果设备节点存在,它将尝试打开它并将文件描述符存储在 fd 变量中:

    ... 
    struct fb_fix_screeninfo finfo; 
    if (ioctl(fd, FBIOGET_FSCREENINFO, &finfo) == -1) 
        return -errno; 

    struct fb_var_screeninfo info; 
    if (ioctl(fd, FBIOGET_VSCREENINFO, &info) == -1) 
        return -errno; 
    ... 

接下来,它将使用 ioctl 命令获取帧缓冲区信息。有两个帧缓冲区数据结构,fb_fix_screeninfofb_var_screeninfo,可以用来与帧缓冲区通信。fb_fix_screeninfo 数据结构存储固定的帧缓冲区信息,而 fb_var_screeninfo 数据结构存储可编程的帧缓冲区信息:

    ... 
    info.reserved[0] = 0; 
    info.reserved[1] = 0; 
    info.reserved[2] = 0; 
    info.xoffset = 0; 
    info.yoffset = 0; 
    info.activate = FB_ACTIVATE_NOW; 

    /* 
     * Request NUM_BUFFERS screens (at lest 2 for page flipping) 
     */ 
    info.yres_virtual = info.yres * NUM_BUFFERS; 

    uint32_t flags = PAGE_FLIP; 
#if USE_PAN_DISPLAY 
    if (ioctl(fd, FBIOPAN_DISPLAY, &info) == -1) { 
        ALOGW("FBIOPAN_DISPLAY failed, page flipping not supported"); 
#else 
    if (ioctl(fd, FBIOPUT_VSCREENINFO, &info) == -1) { 
        ALOGW("FBIOPUT_VSCREENINFO failed, page flipping not supported"); 
#endif 
        info.yres_virtual = info.yres; 
        flags &= ~PAGE_FLIP; 
    } 

    if (ioctl(fd, FBIOGET_FSCREENINFO, &finfo) == -1) 
        return -errno; 

    if (finfo.smem_len <= 0) 
        return -errno; 

    if (finfo.smem_len / finfo.line_length < info.yres_virtual) 
        info.yres_virtual = finfo.smem_len / finfo.line_length; 

    if (info.yres_virtual < info.yres * 2) { 
        // we need at least 2 for page-flipping 
        info.yres_virtual = info.yres; 
        flags &= ~PAGE_FLIP; 
        ALOGW("page flipping not supported (yres_virtual=%d, 
        requested=%d)", 
                info.yres_virtual, info.yres*2); 
    } 
    ... 

在获取到帧缓冲区信息后,它试图设置帧缓冲设备的虚拟分辨率。xresyres字段用于存储帧缓冲设备的可见分辨率,而xres_virtualyres_virtual字段用于存储帧缓冲设备的虚拟分辨率。

为了设置虚拟分辨率,它试图将虚拟垂直分辨率增加到info.yres * NUM_BUFFERS值。NUM_BUFFERS是用于帧缓冲设备中可以使用的缓冲区数量的宏。在我们的情况下,NUM_BUFFERS的值是2,因此我们可以使用双缓冲技术来显示。它使用ioctl命令FBIOPUT_VSCREENINFO来设置虚拟分辨率。如果它成功设置了虚拟分辨率,它将在flags中设置PAGE_FLIP位;否则,它将清除PAGE_FLIP位:

    ... 
    if (ioctl(fd, FBIOGET_VSCREENINFO, &info) == -1) 
        return -errno; 

    if (finfo.smem_len / finfo.line_length < info.yres_virtual) 
        info.yres_virtual = finfo.smem_len / finfo.line_length; 

    uint64_t  refreshQuotient = 
    ( 
            uint64_t( info.upper_margin + info.lower_margin + info.yres ) * 
            ( info.left_margin  + info.right_margin + info.xres ) * 
            info.pixclock 
    ); 

    /* Beware, info.pixclock might be 0 under emulation, so avoid  
     * a division-by-0 here (SIGFPE on ARM) */ 
    int refreshRate = refreshQuotient > 0 ? (int)(1000000000000000LLU / 
    refreshQuotient) : 0; 

    if (refreshRate == 0) { 
        // bleagh, bad info from the driver 
        refreshRate = 60*1000;  // 60 Hz 
    } 
    ... 

在设置虚拟分辨率后,它将计算刷新率。要了解刷新率的计算,可以参考 Linux 内核源代码中的文档Documentation/fb/framebuffer.txt

    ... 
    if (int(info.width) <= 0 || int(info.height) <= 0) { 
        // the driver doesn't return that information 
        // default to 160 dpi 
        info.width  = ((info.xres * 25.4f)/160.0f + 0.5f); 
        info.height = ((info.yres * 25.4f)/160.0f + 0.5f); 
    } 

    float xdpi = (info.xres * 25.4f) / info.width; 
    float ydpi = (info.yres * 25.4f) / info.height; 
    float fps  = refreshRate / 1000.0f; 

    module->finfo = finfo; 
    module->xdpi = xdpi; 
    module->ydpi = ydpi; 
    module->fps = fps; 
    ... 

接下来,它将计算水平和垂直的像素密度。它还将刷新率转换为每秒帧数,并将其存储到fps中。在它有了所有信息后,它将它们存储到数据结构private_module_t的字段中。

最后,它将帧缓冲区映射到进程地址空间:

    ... 
    while (info.yres_virtual > 0) { 
        size_t fbSize = roundUpToPageSize(finfo.line_length * 
        info.yres_virtual); 
        module->numBuffers = info.yres_virtual / info.yres; 
        void* vaddr = mmap(0, fbSize, PROT_READ|PROT_WRITE, MAP_SHARED, 
        fd, 0); 
        if (vaddr != MAP_FAILED) { 
            module->info = info; 
            module->flags = flags; 
            module->bufferMask = 0; 
            module->framebuffer = new private_handle_t(dup(fd), 
            fbSize, 0); 
            module->framebuffer->base = intptr_t(vaddr); 
            memset(vaddr, 0, fbSize); 
            return 0; 
        } 

        ALOGE("Error mapping the framebuffer (%s)", strerror(errno)); 

        info.yres_virtual -= info.yres; 
        ALOGW("Fallback to use fewer buffer: %d", info.yres_virtual /  
        info.yres); 
        if (ioctl(fd, FBIOPUT_VSCREENINFO, &info) == -1) 
            break; 

        if (info.yres_virtual <= info.yres) 
            flags &= ~PAGE_FLIP; 
    } 

    return -errno; 
} 

按虚拟分辨率计算的帧缓冲区大小是finfo.line_length * info.yres_virtualfinfo.line_length的值等于每行的字节数,而info.yres_virtual的值是每帧的行数。为了进行内存映射,我们必须使用roundUpToPageSize函数将大小四舍五入到页面边界。

在帧缓冲设备中可以使用的实际缓冲区数量是info.yres_virtual除以info.yres,并存储在numBuffers字段中。bufferMask字段被设置为 0,这意味着所有缓冲区都是空的,可以用来使用。

它调用mmap系统调用来将帧缓冲区映射到当前进程地址空间。当前进程地址空间中帧缓冲区的起始地址是vaddr,由mmap系统调用返回。它被存储到framebuffer->base字段中,这样 Gralloc 模块就可以使用它来为应用程序分配缓冲区。

到目前为止,我们已经完成了对mapFrameBuffer函数的分析。这个函数负责在 Gralloc HAL 模块中初始化帧缓冲设备的大部分工作。

图形缓冲区的分配和释放

到目前为止,在本章中,我们已经讨论了加载 Gralloc 模块和 Gralloc 模块提供的open方法。现在让我们回顾上层加载、初始化和使用 Gralloc 模块时的要点:

  • 例如,Gralloc 模块主要被 SurfaceFlinger 使用。SurfaceFlinger 使用 Gralloc;当它创建 FramebufferNativeWindow 的实例时,在 FramebufferNativeWindow 构造函数中,它将调用 hw_get_module 来获取 hw_module_t 的实例。

  • hw_module_t 数据结构中,它有一个名为 methods 的字段,其数据类型为 hw_module_methods_t。在 hw_module_methods_t 中,它有一个返回 hw_device_t 数据结构的 open 方法。

  • 通过 hw_device_tSurfaceFlinger 可以使用 hw_device_t 内部的 allocfree 方法来分配或释放图形缓冲区。

让我们在本节中看看 Gralloc 模块如何分配和释放图形缓冲区。我们首先查看 gralloc_alloc 的源代码:

static int gralloc_alloc(alloc_device_t* dev, 
        int w, int h, int format, int usage, 
        buffer_handle_t* pHandle, int* pStride) 
{ 
    if (!pHandle || !pStride) 
        return -EINVAL; 

    size_t size, stride; 

    int align = 4; 
    int bpp = 0; 
    switch (format) { 
        case HAL_PIXEL_FORMAT_RGBA_8888: 
        case HAL_PIXEL_FORMAT_RGBX_8888: 
        case HAL_PIXEL_FORMAT_BGRA_8888: 
            bpp = 4; 
            break; 
        case HAL_PIXEL_FORMAT_RGB_888: 
            bpp = 3; 
            break; 
        case HAL_PIXEL_FORMAT_RGB_565: 
        case HAL_PIXEL_FORMAT_RAW16: 
            bpp = 2; 
            break; 
        default: 
            return -EINVAL; 
    } 

    private_module_t* m = reinterpret_cast<private_module_t*>( 
                        dev->common.module); 

    size_t bpr = usage & GRALLOC_USAGE_HW_FB ? m->finfo.line_length : 
    (w*bpp + (align-1)) & ~(align-1); 
    size = bpr * h; 
    stride = bpr / bpp; 

    int err; 
    if (usage & GRALLOC_USAGE_HW_FB) { 
        err = gralloc_alloc_framebuffer(dev, size, usage, pHandle); 
    } else { 
        err = gralloc_alloc_buffer(dev, size, usage, pHandle); 
    } 

    if (err < 0) { 
        return err; 
    } 

    *pStride = stride; 
    return 0; 
} 

如前述代码片段所示,alloc 方法是在 gralloc_alloc 函数中实现的。gralloc_alloc 有以下参数:

  • dev: 它具有从 hw_device_t 继承的 alloc_device 数据类型。

  • w : 它是图形缓冲区的宽度。

  • h: 它是图形缓冲区的高度。

  • format : 它定义了像素的颜色格式。例如,格式可以是 HAL_PIXEL_FORMAT_RGBA_8888HAL_PIXEL_FORMAT_RGB_888HAL_PIXEL_FORMAT_RGB_565 等。

  • usage : 它定义了图形缓冲区的用途。例如,如果设置了 GRALLOC_USAGE_HW_FB 位,缓冲区将从帧缓冲区分配。

  • pHandle : 它具有 buffer_handle_t 数据类型。我们将讨论这个数据结构的详细信息。它用于存储分配的缓冲区。

  • pStride : 每行的像素数。

gralloc_alloc 中,它检查像素的格式以决定像素的大小。它可以是以 32 位、24 位、16 位等。像素的大小存储在 bpp 变量中。bpr 变量是每行的字节数,它是通过 w 乘以 bpp 计算得出的。bpr 变量需要对齐到四个字节的边界以进行内存分配。缓冲区的大小可以通过 h 乘以 bpr 来计算。

在计算缓冲区大小后,它将根据 GRALLOC_USAGE_HW_FB 位调用 gralloc_alloc_framebuffergralloc_alloc_buffer 函数。

gralloc_alloc 分配的图形缓冲区存储在 buffer_handle_t 数据类型中。buffer_handle_t 被定义为 native_handle 的指针。native_handle 被用作 private_handle_t 的父类。private_handle_t 是实际用于管理图形缓冲区的数据类型,它是一个硬件相关的数据结构。

private_handle_t 和 native_handle 之间的关系

上述图表显示了 private_handle_tnative_handle 之间的关系。以下是对 native_handle 的定义:

typedef struct native_handle 
{ 
    int version;     /* sizeof(native_handle_t) */ 
    int numFds;      /* number of file-descriptors at &data[0] */ 
    int numInts;     /* number of ints at &data[numFds] */ 
    int data[0];     /* numFds + numInts ints */ 
} native_handle_t; 

version 字段被设置为 native_handle 的大小。numFdsnumInts 字段描述了 data 数组中的文件描述符和整数的数量。data 数组用于存储特定于硬件的信息,我们可以在以下 private_handle_t 的定义中看到:

#ifdef __cplusplus 
struct private_handle_t : public native_handle { 
#else 
struct private_handle_t { 
    struct native_handle nativeHandle; 
#endif 

    enum { 
        PRIV_FLAGS_FRAMEBUFFER = 0x00000001 
    }; 

    // file-descriptors 
    int     fd; 
    // ints 
    int     magic; 
    int     flags; 
    int     size; 
    int     offset; 

    // FIXME: the attributes below should be out-of-line 
    uint64_t base __attribute__((aligned(8))); 
    int     pid; 

#ifdef __cplusplus 
    static inline int sNumInts() { 
        return (((sizeof(private_handle_t) - 
        sizeof(native_handle_t))/sizeof(int)) - sNumFds); 
    } 
    static const int sNumFds = 1; 
    static const int sMagic = 0x3141592; 

    private_handle_t(int fd, int size, int flags) : 
        fd(fd), magic(sMagic), flags(flags), size(size), offset(0), 
        base(0), pid(getpid()) 
    { 
        version = sizeof(native_handle); 
        numInts = sNumInts(); 
        numFds = sNumFds; 
    } 
    ~private_handle_t() { 
        magic = 0; 
    } 

    static int validate(const native_handle* h) { 
        const private_handle_t* hnd = (const private_handle_t*)h; 
        if (!h || h->version != sizeof(native_handle) || 
                h->numInts != sNumInts() || h->numFds != sNumFds || 
                hnd->magic != sMagic) 
        { 
            ALOGE("invalid gralloc handle (at %p)", h); 
            return -EINVAL; 
        } 
        return 0; 
    } 
#endif 
}; 

fd 成员变量是一个文件描述符,用于描述帧缓冲区或共享内存区域。magic 成员变量存储为在 sMagic 静态变量中定义的魔术数字。flags 成员变量用于描述图形缓冲区的类型。例如,如果它等于 PRIV_FLAGS_FRAMEBUFFER,则此缓冲区是从帧缓冲区分配的。size 成员变量是图形缓冲区的大小。offset 成员变量是从内存起始地址的偏移量。base 成员变量是为缓冲区分配的地址。pid 成员变量是图形缓冲区创建者的进程 ID。

构造函数填充 native_handle 的成员变量。validate 成员函数用于验证图形缓冲区是否是 private_handle_t 的实例。

正如我们之前提到的,我们正在分析的 Gralloc 模块是 AOSP 中的默认实现,构建为 galloc.default.so。在这个实现中,GPU 不被使用,缓冲区将分配在帧缓冲区或共享内存中。尽管这不是性能的理想情况,但它具有最少的硬件依赖性,这对于理解更复杂的 Gralloc 模块实现是一个很好的参考。

从帧缓冲区分配

gralloc_alloc 函数中我们可以看到,当 usage 位设置为 GRALLOC_USAGE_HW_FB 时,调用 gralloc_alloc_framebuffer 函数。gralloc_alloc_framebuffer 函数将从帧缓冲区设备分配缓冲区:

static int gralloc_alloc_framebuffer_locked(alloc_device_t* dev, 
        size_t size, int usage, buffer_handle_t* pHandle) 
{ 
    private_module_t* m = reinterpret_cast<private_module_t*>( 
            dev->common.module); 

    // allocate the framebuffer 
    if (m->framebuffer == NULL) { 
        // initialize the framebuffer, the framebuffer is mapped once 
        // and forever. 
        int err = mapFrameBufferLocked(m); 
        if (err < 0) { 
            return err; 
        } 
    } 

    const uint32_t bufferMask = m->bufferMask; 
    const uint32_t numBuffers = m->numBuffers; 
    const size_t bufferSize = m->finfo.line_length * m->info.yres; 
    if (numBuffers == 1) { 
        // If we have only one buffer, we never use page-flipping.  
        // Instead we return a regular buffer which will be 
        // memcpy'ed to the main screen when post is called. 
        int newUsage = (usage & ~GRALLOC_USAGE_HW_FB) |   
        GRALLOC_USAGE_HW_2D; 
        return gralloc_alloc_buffer(dev, bufferSize, newUsage, 
        pHandle); 
    } 

    if (bufferMask >= ((1LU<<numBuffers)-1)) { 
        // We ran out of buffers. 
        return -ENOMEM; 
    } 

    // create a "fake" handles for it 
    intptr_t vaddr = intptr_t(m->framebuffer->base); 
    private_handle_t* hnd = new private_handle_t(dup(m->framebuffer-
    >fd), 
    size, private_handle_t::PRIV_FLAGS_FRAMEBUFFER); 

    // find a free slot 
    for (uint32_t i=0 ; i<numBuffers ; i++) { 
        if ((bufferMask & (1LU<<i)) == 0) { 
            m->bufferMask |= (1LU<<i); 
            break; 
        } 
        vaddr += bufferSize; 
    } 

    hnd->base = vaddr; 
    hnd->offset = vaddr - intptr_t(m->framebuffer->base); 
    *pHandle = hnd; 

    return 0; 
} 

static int gralloc_alloc_framebuffer(alloc_device_t* dev, 
        size_t size, int usage, buffer_handle_t* pHandle) 
{ 
    private_module_t* m = reinterpret_cast<private_module_t*>( 
            dev->common.module); 
    pthread_mutex_lock(&m->lock); 
    int err = gralloc_alloc_framebuffer_locked(dev, size, usage,  
    pHandle); 
    pthread_mutex_unlock(&m->lock); 
    return err; 
} 

gralloc_alloc_framebuffer 首先获取一个互斥锁,并调用另一个函数,gralloc_alloc_framebuffer_locked。在锁定版本中,它调用之前分析的 mapFrameBufferLocked 函数,以获取帧缓冲区信息并将其映射到当前进程地址空间。

它将检查帧缓冲区设备是否支持双缓冲。如果它支持双缓冲,它将创建一个新的 private_handle_t 实例,并填充此实例中的信息,然后返回给调用者。如果缓冲区是从帧缓冲区设备分配的,它将标记 private_handle_tflags 成员变量为 PRIV_FLAGS_FRAMEBUFFER。它还将设置帧缓冲区的 usage 状态在 bufferMask 中,这是 private_module_t 的成员变量。

如果它不支持双缓冲,它将调用 gralloc_alloc_buffer 从系统内存分配缓冲区并返回给调用者。

从系统内存分配

usage 位未设置为 GRALLOC_USAGE_HW_FB 或系统不支持双缓冲时,我们必须使用 gralloc_alloc_buffer 从系统内存分配缓冲区。让我们看看 gralloc_alloc_buffer 的实现:

static int gralloc_alloc_buffer(alloc_device_t* dev, 
        size_t size, int /*usage*/, buffer_handle_t* pHandle) 
{ 
    int err = 0; 
    int fd = -1; 

    size = roundUpToPageSize(size); 

    fd = ashmem_create_region("gralloc-buffer", size); 
    if (fd < 0) { 
        ALOGE("couldn't create ashmem (%s)", strerror(-errno)); 
        err = -errno; 
    } 

    if (err == 0) { 
        private_handle_t* hnd = new private_handle_t(fd, size, 0); 
        gralloc_module_t* module = reinterpret_cast<gralloc_module_t*>( 
                dev->common.module); 
        err = mapBuffer(module, hnd); 
        if (err == 0) { 
            *pHandle = hnd; 
        } 
    } 

    ALOGE_IF(err, "gralloc failed err=%s", strerror(-err)); 

    return err; 
} 

gralloc_alloc_buffer 中,它首先将缓冲区大小向上舍入到页面大小。然后使用 ashmem_create_region 创建一个匿名共享内存区域。它创建一个新的 private_handle_t 实例来表示这个共享内存区域。

这个共享内存区域被描述为一个文件描述符。要使用它,我们需要将其映射到当前进程的地址空间。这是通过 mapBuffer 函数完成的:

int mapBuffer(gralloc_module_t const* module, 
        private_handle_t* hnd) 
{ 
    void* vaddr; 
    return gralloc_map(module, hnd, &vaddr); 
} 

mapBuffer 调用另一个函数 gralloc_map 来进行内存映射:

static int gralloc_map(gralloc_module_t const* /*module*/, 
        buffer_handle_t handle, 
        void** vaddr) 
{ 
    private_handle_t* hnd = (private_handle_t*)handle; 
    if (!(hnd->flags & private_handle_t::PRIV_FLAGS_FRAMEBUFFER)) { 
        size_t size = hnd->size; 
        void* mappedAddress = mmap(0, size, 
                PROT_READ|PROT_WRITE, MAP_SHARED, hnd->fd, 0); 
        if (mappedAddress == MAP_FAILED) { 
            ALOGE("Could not mmap %s", strerror(errno)); 
            return -errno; 
        } 
        hnd->base = uintptr_t(mappedAddress) + hnd->offset; 
        //ALOGD("gralloc_map() succeeded fd=%d, off=%d, size=%d, vaddr=%p", 
        //        hnd->fd, hnd->offset, hnd->size, mappedAddress); 
    } 
    *vaddr = (void*)hnd->base; 
    return 0; 
} 

grallo_map 中,如果 private_handle_t 中的文件描述符是帧缓冲设备,我们不需要再次进行映射,因为帧缓冲已经在 fb_device_open 中初始化并映射到 SurfaceFlinger 地址空间,正如我们之前分析的。

如果是一个共享内存区域,需要使用 mmap 系统函数将其映射到当前进程的地址空间。

释放图形缓冲区

正如我们之前提到的,Gralloc 模块可以用来分配和释放图形缓冲区。现在我们已经学会了如何从帧缓冲设备或系统内存分配缓冲区,让我们看看如何释放图形缓冲区。

要释放图形缓冲区,使用 gralloc_free 函数:

static int gralloc_free(alloc_device_t* dev, 
        buffer_handle_t handle) 
{ 
    if (private_handle_t::validate(handle) < 0) 
        return -EINVAL; 

    private_handle_t const* hnd = reinterpret_cast<private_handle_t const*>
    (handle); 
    if (hnd->flags & private_handle_t::PRIV_FLAGS_FRAMEBUFFER) { 
        // free this buffer 
        private_module_t* m = reinterpret_cast<private_module_t*>( 
                dev->common.module); 
        const size_t bufferSize = m->finfo.line_length * m->info.yres; 
        int index = (hnd->base - m->framebuffer->base) / bufferSize; 
        m->bufferMask &= ~(1<<index);  
    } else {  
        gralloc_module_t* module = reinterpret_cast<gralloc_module_t*>( 
                dev->common.module); 
        terminateBuffer(module, const_cast<private_handle_t*>(hnd)); 
    } 

    close(hnd->fd); 
    delete hnd; 
    return 0; 
} 

要释放一个图形缓冲区,使用 buffer_handle_t 描述缓冲区。gralloc_free 将首先使用 private_handle_t::validate 静态函数验证缓冲区。

handle 参数可以转换为 private_handle_t 的指针,正如我们从之前关于 private_handle_tnative_handle 的讨论中回忆的那样。如果 hndflags 字段是 PRIV_FLAGS_FRAMEBUFFER,这意味着缓冲区是从帧缓冲设备分配的。它将更新 bufferMask 以从帧缓冲中释放它。

如果缓冲区是从系统内存分配的,它将调用 terminateBuffer 函数来释放内存:

int terminateBuffer(gralloc_module_t const* module, 
        private_handle_t* hnd) 
{ 
    if (hnd->base) { 
        // this buffer was mapped, unmap it now 
        gralloc_unmap(module, hnd); 
    } 

    return 0; 
} 

terminateBuffer 函数调用另一个函数 gralloc_unmap 来释放内存:

static int gralloc_unmap(gralloc_module_t const* /*module*/, 
        buffer_handle_t handle) 
{ 
    private_handle_t* hnd = (private_handle_t*)handle; 
    if (!(hnd->flags & private_handle_t::PRIV_FLAGS_FRAMEBUFFER))  
    { 
        void* base = (void*)hnd->base; 
        size_t size = hnd->size; 
        //ALOGD("unmapping from %p, size=%d", base, size); 
        if (munmap(base, size) < 0) { 
            ALOGE("Could not unmap %s", strerror(errno)); 
        } 
    } 
    hnd->base = 0; 
    return 0; 
} 

gralloc_unmap 中,它再次检查这个缓冲区不是来自帧缓冲,并调用 munmap 系统函数来释放它。

渲染帧缓冲

正如我们在本章前面讨论的那样,Gralloc 模块可以支持两种类型的设备:Gralloc 设备和帧缓冲设备。在 Gralloc 设备的 open 方法中,它创建一个名为 GRALLOC_HARDWARE_GPU0 的设备,并支持两种方法,allocfree,正如我们可以在下面的代码片段中看到的那样。我们已经在本章前面详细讨论了这两种方法:

    ... 
    if (!strcmp(name, GRALLOC_HARDWARE_GPU0)) { 
        gralloc_context_t *dev; 
        dev = (gralloc_context_t*)malloc(sizeof(*dev)); 

        /* initialize our state here */ 
        memset(dev, 0, sizeof(*dev)); 

        /* initialize the procs */ 
        dev->device.common.tag = HARDWARE_DEVICE_TAG; 
        dev->device.common.version = 0; 
        dev->device.common.module = const_cast<hw_module_t*>(module); 
        dev->device.common.close = gralloc_close; 

        dev->device.alloc   = gralloc_alloc; 
        dev->device.free    = gralloc_free; 

        *device = &dev->device.common; 
    ... 

在帧缓冲设备 open 方法中,它创建一个名为 GRALLOC_HARDWARE_FB0 的设备,并支持四种方法 closesetSwapIntervalpostsetUpdateRect

    ... 
    if (!strcmp(name, GRALLOC_HARDWARE_FB0)) { 
        /* initialize our state here */ 
        fb_context_t *dev = (fb_context_t*)malloc(sizeof(*dev)); 
        memset(dev, 0, sizeof(*dev)); 

        /* initialize the procs */ 
        dev->device.common.tag = HARDWARE_DEVICE_TAG; 
        dev->device.common.version = 0; 
        dev->device.common.module = const_cast<hw_module_t*>(module); 
        dev->device.common.close = fb_close; 
        dev->device.setSwapInterval = fb_setSwapInterval; 
        dev->device.post            = fb_post; 
        dev->device.setUpdateRect = 0; 

        private_module_t* m = (private_module_t*)module; 
    ... 

您可以参考 AOSP 源代码或以下 URL 获取有关这些方法实现的信息:

xref.opersys.com/android-7.0.0_r1/xref/hardware/libhardware/modules/gralloc/framebuffer.cpp

让我们看看 post 方法,它在 fb_post 中实现:

static int fb_post(struct framebuffer_device_t* dev, buffer_handle_t buffer) 
{ 
    if (private_handle_t::validate(buffer) < 0) 
        return -EINVAL; 

    fb_context_t* ctx = (fb_context_t*)dev; 

    private_handle_t const* hnd = reinterpret_cast<private_handle_t const*>
    (buffer); 
    private_module_t* m = reinterpret_cast<private_module_t*>( 
            dev->common.module); 

    if (hnd->flags & private_handle_t::PRIV_FLAGS_FRAMEBUFFER) { 
        const size_t offset = hnd->base - m->framebuffer->base; 
        m->info.activate = FB_ACTIVATE_VBL; 
        m->info.yoffset = offset / m->finfo.line_length; 
        if (ioctl(m->framebuffer->fd, FBIOPUT_VSCREENINFO, &m->info) == -1) { 
            ALOGE("FBIOPUT_VSCREENINFO failed"); 
            m->base.unlock(&m->base, buffer);  
            return -errno; 
        } 
        m->currentBuffer = buffer; 

    } else { 
        // If we can't do the page_flip, just copy the buffer to the front  
        // FIXME: use copybit HAL instead of memcpy 

        void* fb_vaddr; 
        void* buffer_vaddr; 

        m->base.lock(&m->base, m->framebuffer,  
                GRALLOC_USAGE_SW_WRITE_RARELY,  
                0, 0, m->info.xres, m->info.yres, &fb_vaddr); 

        m->base.lock(&m->base, buffer,  
                GRALLOC_USAGE_SW_READ_RARELY,  
                0, 0, m->info.xres, m->info.yres, &buffer_vaddr); 

        memcpy(fb_vaddr, buffer_vaddr, m->finfo.line_length * m-
        >info.yres); 

        m->base.unlock(&m->base, buffer);  
        m->base.unlock(&m->base, m->framebuffer);  
    } 

    return 0; 
} 

在应用程序准备完图形缓冲区后,它需要将缓冲区发布到显示,以便用户可以在屏幕上看到它。这个 fb_post 函数用于将图形缓冲区显示到屏幕上。它接受两个参数,devbufferdev 参数是 framebuffer_device_t 数据结构实例的指针,这之前已经讨论过(参考 fb_context_tframebuffer_device_t 之间关系的图)。根据之前的讨论,dev 可以转换为 ctx,它是 fb_context_t 指针。

在我们获得设备实例后,我们可以按照以下方式从其中获取 Gralloc 模块的实例:

private_module_t* m = reinterpret_cast<private_module_t*>( 
dev->common.module); 

另一个参数是 buffer,它具有 buffer_handle_t 数据类型。它包括要发布的缓冲区。正如我们之前讨论的,它可以转换为 private_handle_t 的指针,并存储在 hnd 变量中。这个缓冲区可以是系统内存中的图形缓冲区,也可以是帧缓冲区的一部分。根据 hnd->flags 成员变量的值,我们可以确定它是哪种类型的缓冲区。

如果它是帧缓冲区的一部分,我们需要将其激活为显示的缓冲区。这可以通过使用帧缓冲区的 ioctl 函数来完成。要调用 ioctl 函数,我们需要一个 fb_var_screeninfo 数据结构,这可以在 m->info 中找到。为了在双缓冲中交换缓冲区,我们只需设置垂直偏移并按照以下方式激活它:

    ... 
        m->info.activate = FB_ACTIVATE_VBL; 
        m->info.yoffset = offset / m->finfo.line_length; 
        if (ioctl(m->framebuffer->fd, FBIOPUT_VSCREENINFO, &m->info) == -1) { 
    ... 

如果它是在系统内存中分配的缓冲区,我们需要将其复制到帧缓冲区。在这种情况下,它首先尝试锁定图形缓冲区和帧缓冲区,然后使用 memcpy 复制图形缓冲区:

memcpy(fb_vaddr, buffer_vaddr, m->finfo.line_length * m->info.yres); 

Android 模拟器的图形 HAL

在我们分析了默认 Gralloc 模块实现之后,我们想简要地看看另一个 Gralloc 模块实现,以便我们可以比较在不同图形硬件上应该如何实现 Gralloc 模块。

在本节中我们将要分析的 Gralloc 模块是 Android 模拟器使用的 Gralloc 模块。默认的 Gralloc 模块 gralloc.default.so 只使用帧缓冲区设备,并且不使用 GPU。如果使用默认的 Gralloc 模块,OpenGL 支持必须在软件层中实现。目前的情况是 VirtualBox,因为 VirtualBox 主机端没有 Mesa/DRM 兼容的 OpenGL 实现。这并不意味着 VirtualBox 不支持 OpenGL。它确实支持 OpenGL 和 3D 硬件加速,但实现并不符合开源 Mesa/DRM 架构。

如果您对关于 VirtualBox 上 OpenGL 支持的此主题感兴趣,您可以在 Android-x86 讨论组中阅读以下线程:

groups.google.com/forum/?hl=en#!starred/android-x86/gZYx6oWx4LI

硬件 GLES 模拟概述

Android 模拟器上的 3D 图形支持以不同的方式实现,如下所示:

  • host:这是默认模式。这也称为硬件 GLES 模拟。它使用特定的翻译库将客户机 EGL/GLES 命令转换为宿主 GL 命令。这需要在宿主机器上安装有效的 OpenGL 驱动程序。

  • swiftshader:这是一个用于在 CPU 上进行高性能图形渲染的软件库。它利用现代 CPU 上的 SIMD 来执行图形渲染。

  • mesa:这已被弃用。这是一个使用 Mesa3D 库的软件库。它的速度比 swiftshader 模式慢,并且比 host 模式慢得多。

  • guest:这是在客户机侧的纯软件实现。

在模拟器中选择图形模式,您可以通过命令行使用 -gpu 选项指定,或者在 config.ini 配置文件中定义,如下所示:

hw.gps=yes 
hw.gpu.enabled=yes 
hw.gpu.mode=swiftshader 

我们将在此处查看 host 模式下的 Gralloc 模块实现。在硬件 GLES 模拟中,实现了几个宿主“翻译”库:EGL、GLES 1.1 和 Khronos 定义的 GLES 2.0 ABIs(应用程序二进制接口)。这些库将相应的函数调用转换为调用适当的宿主 OpenGL API。

在模拟的客机系统中实现了用于 EGL、GLES 1.1 和 GLES 2.0 ABIs 的相同系统库集合。它们收集 EGL/GLES 函数调用的序列,并将它们转换为发送到模拟器程序的定制线协议流,该流通过称为“QEMU 管道”的高速通信通道发送。管道使用定制的内核驱动程序实现,它可以提供主机和客机系统之间非常快速的通信通道。我在第三章“发现内核、HAL 和虚拟硬件”中简要介绍了 QEMU 管道,您可以参考它以获取更多信息。

硬件 GLES 模拟

您可以在模拟器源代码的 $AOSP/external/qemu/distrib/android-emugl/DESIGN 中找到前面的图表。

在本章中,不会使用 manifest 文件下载模拟器源代码。您可以参考以下 URL:

android.googlesource.com/platform/external/qemu/+/master/distrib/android-emugl/DESIGN

或者,您可以使用以下命令获取整个仓库:

$ git clone https://android.googlesource.com/platform/external/qemu  

前面的图示显示了 GLES 仿真的主机(模拟器)端和客户机端的组件。我们可以将主机端实现视为 GPU,QEMU PIPE是 GPU 和 CPU 之间的连接。需要访问 GPU 进行 3D 图形加速的两个东西是 Gralloc 模块和供应商库。这里的供应商库是指 Android 模拟器的硬件 GLES 仿真库。Gralloc 模块是我们本节想要探索的模块。

GLES 硬件仿真 Gralloc 模块与我们本章中讨论的默认 Gralloc 模块非常相似。它需要实现以下三个 HAL 数据结构:

struct hw_module_t; 
struct hw_module_methods_t; 
struct hw_device_t; 

对于第一个数据结构,hw_module_t,两个 Gralloc 模块都有自己的实现,称为private_module_t,它继承自hw_module_t,但定义不同,如下面的代码片段所示。

默认 Gralloc 模块中的private_module_t如下:

struct private_module_t { 
    gralloc_module_t base; 

    private_handle_t* framebuffer; 
    uint32_t flags; 
    uint32_t numBuffers; 
    uint32_t bufferMask; 
    pthread_mutex_t lock; 
    buffer_handle_t currentBuffer; 
    int pmem_master; 
    void* pmem_master_base; 

    struct fb_var_screeninfo info; 
    struct fb_fix_screeninfo finfo; 
    float xdpi; 
    float ydpi; 
    float fps; 
}; 

GLES 仿真 Gralloc 模块中的private_module_t如下:

struct private_module_t { 
    gralloc_module_t base; 
}; 

对于hw_device_t数据结构实现,我们可以从以下表格中获取详细信息。我们可以使用hw_module_methods_t数据结构中的open方法创建两种设备,GPU0FB0。在这两种实现中,都使用了从hw_device_t继承的数据结构:

Gralloc 模块中的 hw_device_t GPU0 FB0
Android 模拟器 gralloc_device_t fb_device_t
默认 Gralloc gralloc_context_t fb_context_t

我们在初始化 GPU部分分析了gralloc_context_tfb_context_t。我们可以在以下 GLES 仿真实现中查看gralloc_device_tfb_device_t的定义:

struct gralloc_device_t { 
    alloc_device_t  device; 

    AllocListNode *allocListHead;    // double linked list of allocated buffers 
    pthread_mutex_t lock; 
}; 

struct fb_device_t { 
    framebuffer_device_t  device; 
}; 

初始化 GLES 仿真的 GPU0 和 FB0

正如我们所知,设备初始化是在hw_module_methods_t数据结构中定义的open方法中完成的。让我们看看 GLES 仿真的open方法的实现。它是在gralloc_device_open函数中实现的,如下面的代码片段所示:

static int gralloc_device_open(const hw_module_t* module, 
                               const char* name, 
                               hw_device_t** device) 
{ 
    int status = -EINVAL; 

    D("gralloc_device_open %s\n", name); 

    pthread_once( &sFallbackOnce, fallback_init ); 
    if (sFallback != NULL) { 
        return sFallback->common.methods->open(&sFallback->common, 
        name, device); 
    } 

    if (!strcmp(name, GRALLOC_HARDWARE_GPU0)) { 

        // Create host connection and keep it in the TLS. 
        // return error if connection with host can not be established 
        HostConnection *hostCon = HostConnection::get(); 
        if (!hostCon) { 
            ALOGE("gralloc: failed to get host connection 
            while opening %s\n", 
            name); 
            return -EIO; 
        } 

        // 
        // Allocate memory for the gralloc device (alloc interface) 
        // 
        gralloc_device_t *dev; 
        dev = (gralloc_device_t*)malloc(sizeof(gralloc_device_t)); 
        if (NULL == dev) { 
            return -ENOMEM; 
        } 

        // Initialize our device structure 
        // 
        dev->device.common.tag = HARDWARE_DEVICE_TAG; 
        dev->device.common.version = 0; 
        dev->device.common.module = const_cast<hw_module_t*>(module); 
        dev->device.common.close = gralloc_device_close; 

        dev->device.alloc   = gralloc_alloc; 
        dev->device.free    = gralloc_free; 
        dev->allocListHead  = NULL; 
        pthread_mutex_init(&dev->lock, NULL); 

        *device = &dev->device.common; 
        status = 0; 
    } 
    else if (!strcmp(name, GRALLOC_HARDWARE_FB0)) { 
    ... 
    } 

    return status; 
} 

前面的代码片段是GPU0初始化的一部分。在它为GPU0FB0创建设备之前,它将调用一个fallback_init函数来检查系统设置中的硬件仿真。在fallback_init中,它将检查一个ro.kernel.qemu.gles系统属性。如果这个属性设置为 0,则 GPU 仿真将被禁用。将使用默认的 Gralloc 模块。在这种情况下,默认 Gralloc 模块中定义的open方法,即sFallback,将被调用。

对于GPU0初始化,它将检查设备名称是否等于GRALLOC_HARDWARE_GPU0。如果是GPU0,它将首先获取主机连接。主机连接是我们之前讨论的主机和客户机系统之间的 QEMU pipe 链接。

之后,它初始化GPU0设备,就像我们为默认 Gralloc 模块讨论的初始化过程一样。

接下来,让我们看一下以下 FB0 的初始化:

static int gralloc_device_open(const hw_module_t* module, 
                               const char* name, 
                               hw_device_t** device) 
{ 
    int status = -EINVAL; 

    D("gralloc_device_open %s\n", name); 

    pthread_once( &sFallbackOnce, fallback_init ); 
    if (sFallback != NULL) { 
        return sFallback->common.methods->open(&sFallback->common, 
        name, device); 
    } 

    if (!strcmp(name, GRALLOC_HARDWARE_GPU0)) { 
      ... 
    } 
    else if (!strcmp(name, GRALLOC_HARDWARE_FB0)) { 

        // return error if connection with host can not be established 
        DEFINE_AND_VALIDATE_HOST_CONNECTION; 

        // 
        // Query the host for Framebuffer attributes 
        // 
        D("gralloc: query Frabuffer attribs\n"); 
        EGLint width = rcEnc->rcGetFBParam(rcEnc, FB_WIDTH); 
        D("gralloc: width=%d\n", width); 
        EGLint height = rcEnc->rcGetFBParam(rcEnc, FB_HEIGHT); 
        D("gralloc: height=%d\n", height); 
        EGLint xdpi = rcEnc->rcGetFBParam(rcEnc, FB_XDPI); 
        D("gralloc: xdpi=%d\n", xdpi); 
        EGLint ydpi = rcEnc->rcGetFBParam(rcEnc, FB_YDPI); 
        D("gralloc: ydpi=%d\n", ydpi); 
        EGLint fps = rcEnc->rcGetFBParam(rcEnc, FB_FPS); 
        D("gralloc: fps=%d\n", fps); 
        EGLint min_si = rcEnc->rcGetFBParam(rcEnc,  
        FB_MIN_SWAP_INTERVAL); 
        D("gralloc: min_swap=%d\n", min_si); 
        EGLint max_si = rcEnc->rcGetFBParam(rcEnc, 
        FB_MAX_SWAP_INTERVAL); 
        D("gralloc: max_swap=%d\n", max_si); 

        // 
        // Allocate memory for the framebuffer device 
        // 
        fb_device_t *dev; 
        dev = (fb_device_t*)malloc(sizeof(fb_device_t)); 
        if (NULL == dev) { 
            return -ENOMEM; 
        } 
        memset(dev, 0, sizeof(fb_device_t)); 

        // Initialize our device structure 
        // 
        dev->device.common.tag = HARDWARE_DEVICE_TAG; 
        dev->device.common.version = 0; 
        dev->device.common.module = const_cast<hw_module_t*>(module); 
        dev->device.common.close = fb_close; 
        dev->device.setSwapInterval = fb_setSwapInterval; 
        dev->device.post            = fb_post; 
        dev->device.setUpdateRect   = 0; //fb_setUpdateRect; 
        dev->device.compositionComplete = fb_compositionComplete; 

        const_cast<uint32_t&>(dev->device.flags) = 0; 
        const_cast<uint32_t&>(dev->device.width) = width; 
        const_cast<uint32_t&>(dev->device.height) = height; 
        const_cast<int&>(dev->device.stride) = width; 
        const_cast<int&>(dev->device.format) = 
        HAL_PIXEL_FORMAT_RGBA_8888; 
        const_cast<float&>(dev->device.xdpi) = xdpi; 
        const_cast<float&>(dev->device.ydpi) = ydpi; 
        const_cast<float&>(dev->device.fps) = fps; 
        const_cast<int&>(dev->device.minSwapInterval) = min_si; 
        const_cast<int&>(dev->device.maxSwapInterval) = max_si; 
        *device = &dev->device.common; 

        status = 0; 
    } 

FB0 初始化中,它尝试使用 DEFINE_AND_VALIDATE_HOST_CONNECTION 宏获取主机连接和一个 rcEnc 指针,这是一个 renderControl_encoder_context_t 数据结构的实例。有了 rcEnc,它可以从主机连接中获取帧缓冲区属性(widthheightxdpiydpifpsmin_simax_si)。之后,它创建了一个 fb_device_t 数据结构的实例,并在该实例中填写了帧缓冲区属性。

GPU0 设备实现

正如我们在默认的 Gralloc 模块中所做的那样,我们将分析 GPU0 设备中的 allocfree 方法。alloc 方法实现在 gralloc_alloc 函数中。gralloc_alloc 函数比默认 Gralloc 模块中的函数要长得多,但它基本上做了三件事:

  • 检查 usage 参数并决定像素格式以确定像素的大小。

  • 根据由 usage 参数提供的信息,whformatusage 创建一个共享内存区域并在主机端(GPU)分配缓冲区。

  • 在 Gralloc 设备数据结构 grdev 中存储共享内存区域和主机端(GPU)缓冲区信息。

现在让我们看一下 gralloc_alloc 的代码:

static int gralloc_alloc(alloc_device_t* dev, 
                         int w, int h, int format, int usage, 
                         buffer_handle_t* pHandle, int* pStride) 
{ 
    D("gralloc_alloc w=%d h=%d usage=0x%x\n", w, h, usage); 

    gralloc_device_t *grdev = (gralloc_device_t *)dev; 
    if (!grdev || !pHandle || !pStride) { 
        ALOGE("gralloc_alloc: Bad inputs (grdev: %p, pHandle: %p, 
        pStride: %p", 
        grdev, pHandle, pStride); 
        return -EINVAL; 
    } 

    // 
    // Note: in screen capture mode, both sw_write 
    // and hw_write will be on 
    // and this is a valid usage 
    // 
    bool sw_write = (0 != (usage & GRALLOC_USAGE_SW_WRITE_MASK)); 
    bool hw_write = (usage & GRALLOC_USAGE_HW_RENDER); 
    bool sw_read = (0 != (usage & GRALLOC_USAGE_SW_READ_MASK)); 
    bool hw_cam_write = usage & GRALLOC_USAGE_HW_CAMERA_WRITE; 
    bool hw_cam_read = usage & GRALLOC_USAGE_HW_CAMERA_READ; 
    bool hw_vid_enc_read = usage & GRALLOC_USAGE_HW_VIDEO_ENCODER; 

    // Keep around original requested format for later validation 
    int frameworkFormat = format; 
    // Pick the right concrete pixel format given the endpoints as    
    // encoded in the usage bits.  
    // Every end-point pair needs explicit listing here. 
    if (format == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED) { 
        // Camera as producer 
        ... 
    if (usage & GRALLOC_USAGE_HW_FB) { 
        // keep space for postCounter 
        ashmem_size += sizeof(uint32_t); 
    } 

    if (sw_read || sw_write || hw_cam_write || hw_vid_enc_read) { 
        // keep space for image on guest memory if SW access is needed 
        // or if the camera is doing writing 
        if (yuv_format) { 
            size_t yStride = (w*bpp + (align - 1)) & ~(align-1); 
            size_t uvStride = (yStride / 2 + (align - 1)) & ~(align-1); 
            size_t uvHeight = h / 2; 
            ashmem_size += yStride * h + 2 * (uvHeight * uvStride); 
            stride = yStride / bpp; 
        } else { 
            size_t bpr = (w*bpp + (align-1)) & ~(align-1); 
            ashmem_size += (bpr * h); 
            stride = bpr / bpp; 
        } 
    } 

    D("gralloc_alloc format=%d, ashmem_size=%d, stride=%d, 
    tid %d\n", format, 
            ashmem_size, stride, gettid()); 

gralloc_alloc 的前面代码中,它首先创建了一个 gralloc_device_t 数据结构的实例。之后,它检查 usageformat 参数以确定像素的大小以及相应的 GLES 颜色格式和像素类型,并将它们存储在 bppglFormatglType 变量中。有了必要的信息,它可以计算出需要为图形缓冲区分配的共享内存的大小,并将其存储在 ashmem_size 变量中:

    // 
    // Allocate space in ashmem if needed 
    // 
    int fd = -1; 
    if (ashmem_size > 0) { 
        // round to page size; 
        ashmem_size = (ashmem_size + (PAGE_SIZE-1)) & ~(PAGE_SIZE-1); 

        fd = ashmem_create_region("gralloc-buffer", ashmem_size); 
        if (fd < 0) { 
            ALOGE("gralloc_alloc failed to create ashmem region: %s\n", 
                    strerror(errno)); 
            return -errno; 
        } 
    } 

    cb_handle_t *cb = new cb_handle_t(fd, ashmem_size, usage, 
                                      w, h, frameworkFormat, format, 
                                      glFormat, glType); 

    if (ashmem_size > 0) { 
        // 
        // map ashmem region if exist 
        // 
        void *vaddr; 
        int err = map_buffer(cb, &vaddr); 
        if (err) { 
            close(fd); 
            delete cb; 
            return err; 
        } 

        cb->setFd(fd); 
    } 

    // 
    // Allocate ColorBuffer handle on the host (only if h/w access is   
    //allowed) only do this for some h/w usages, not all. 
    // 
    if (usage & (GRALLOC_USAGE_HW_TEXTURE | GRALLOC_USAGE_HW_RENDER | 
                    GRALLOC_USAGE_HW_2D | GRALLOC_USAGE_HW_COMPOSER | 
                    GRALLOC_USAGE_HW_FB) ) { 
        DEFINE_HOST_CONNECTION; 
        if (hostCon && rcEnc) { 
            cb->hostHandle = rcEnc->rcCreateColorBuffer(rcEnc, w, h, 
            glFormat); 
            D("Created host ColorBuffer 0x%x\n", cb->hostHandle); 
        } 

        if (!cb->hostHandle) { 
           // Could not create colorbuffer on host !!! 
           close(fd); 
           delete cb; 
           return -EIO; 
        } 
    } 

至于共享内存大小 ashmem_size,它使用 ashmem_create_region 函数分配一个共享内存区域,并获取共享内存区域作为一个 fd 文件描述符。为了存储共享内存区域和 GPU 缓冲区(我们将讨论的主机端缓冲区),它创建了一个 cb_handle_t 数据结构的实例。如果我们回想一下,我们在默认的 Gralloc 模块中使用了 private_handle_t 数据结构来表示分配的图形缓冲区。在这里,cb_handle_tprivate_handle_t 的等价物:

struct cb_handle_t : public native_handle { 

    cb_handle_t(int p_fd, int p_ashmemSize, int p_usage, 
                int p_width, int p_height, int p_frameworkFormat, 
                int p_format, int p_glFormat, int p_glType) : 
    ... 
    // file-descriptors 
    int fd;   

    // ints 
    int magic; 
    int usage; 
    int width; 
    int height; 
    int frameworkFormat; 
    int format; 
    int glFormat; 
    int glType; 
    int ashmemSize; 

    union { 
        intptr_t ashmemBase; 
        uint64_t padding; 
    } __attribute__((aligned(8))); 

    int ashmemBasePid; 
    int mappedPid; 
    int lockedLeft; 
    int lockedTop; 
    int lockedWidth; 
    int lockedHeight; 
    uint32_t hostHandle; 
}; 

因为 cb_handle_t 是一个大型数据结构,所以在前面的代码片段中我们没有展示 cb_handle_t 的所有成员函数。从成员变量中我们可以看出,它们与 private_handle_t 类似。您可以参考 private_handle_t 的部分来了解大多数成员变量的解释。请注意最后一个成员变量 hostHandle,它用于存储在 GPU 上分配的缓冲区(在 GLES 模拟中的主机端)。如果您对主机端 GLES 模拟感兴趣,可以参考 QEMU 源代码。

让我们看一下 gralloc_alloc 的最后一部分代码:

    // 
    // alloc succeeded - insert the allocated handle to the allocated    
    // list 
    // 
    AllocListNode *node = new AllocListNode(); 
    pthread_mutex_lock(&grdev->lock); 
    node->handle = cb; 
    node->next =  grdev->allocListHead; 
    node->prev =  NULL; 
    if (grdev->allocListHead) { 
        grdev->allocListHead->prev = node; 
    } 
    grdev->allocListHead = node; 
    pthread_mutex_unlock(&grdev->lock); 

    *pHandle = cb; 
    if (frameworkFormat == HAL_PIXEL_FORMAT_YCbCr_420_888) { 
        *pStride = 0; 
    } else { 
        *pStride = stride; 
    } 
    return 0; 
} 

在缓冲区在 GPU 上分配并且从系统内存中获取共享内存区域后,它们被存储在grdev变量中,并添加到gralloc_device_t中的双链表节点。

对于gralloc_device_tfree方法,它比alloc简单得多。为了节省空间,这里不会列出源代码。free方法是在gralloc_free函数中实现的。它所做的是:

  1. 验证buffer_handle_t指向有效的cb_handle_t数据结构。

  2. 在主机端(GPU)释放缓冲区,调用rcCloseColorBuffer函数。

  3. 在共享内存区域取消映射缓冲区并释放共享内存。

  4. 从链表中移除节点。

  5. 释放cb_handle_t数据结构使用的内存。

FB0 设备实现

对于FB0设备的实现,我们将像分析默认的 Gralloc 模块那样查看post方法。这是在fb_post函数中实现的,我们可以如下查看其实现:

static int fb_post(struct framebuffer_device_t* dev, buffer_handle_t buffer) 
{ 
    fb_device_t *fbdev = (fb_device_t *)dev; 
    cb_handle_t *cb = (cb_handle_t *)buffer; 

    if (!fbdev || !cb_handle_t::validate(cb) || !cb->canBePosted()) { 
        return -EINVAL; 
    } 

    // Make sure we have host connection 
    DEFINE_AND_VALIDATE_HOST_CONNECTION; 

    // increment the post count of the buffer 
    intptr_t *postCountPtr = (intptr_t *)cb->ashmemBase; 
    if (!postCountPtr) { 
        // This should not happen 
        return -EINVAL; 
    } 
    (*postCountPtr)++; 

    // send post request to host 
    rcEnc->rcFBPost(rcEnc, cb->hostHandle); 
    hostCon->flush(); 

    return 0; 
} 

它所做的是非常简单的;它增加缓冲区的 post 计数,并调用rcFBpost函数来更新 GPU 中的缓冲区。

我们现在已经完成了对 Android 模拟器图形 HAL 的分析。希望对通用图形 HAL 和 Android 模拟器图形 HAL 的分析能帮助你理解你系统中的图形 HAL。

摘要

在本章中,我们探讨了并回顾了两个 Gralloc HAL 模块的实现,即默认的 Gralloc 模块和 Android 模拟器使用的模块。默认的 Gralloc HAL 仅使用帧缓冲设备,而 OpenGLES 支持使用软件实现。Android 模拟器使用的是主机端的硬件仿真。其实现与基于 GPU 的 Gralloc 模块类似。

由于图形系统非常复杂,我们将在下一章查看 VirtualBox 特定实现时,继续更深入地探讨这个主题。我们将解释 Gralloc HAL 和 OpenGL ES 库的加载过程。我们将为 Android 构建一个 VirtualBox 扩展包,以便我们可以利用 VirtualBox 提供的功能。

第十一章:启用 VirtualBox 特定硬件接口

在上一章中,我们对 Android Gralloc HAL 模块进行了深入分析。我们分析了默认的 Gralloc 模块和 Android 模拟器的硬件 GPU 仿真 Gralloc HAL。我们还没有时间走完与图形系统相关的启动过程。在本章中,我们将走完图形系统的启动过程,并探索 VirtualBox 特定的硬件驱动程序。在本章结束时,我们将在 VirtualBox 上拥有一个相对完整的系统。本章我们将涵盖以下主题:

  • OpenGL ES 和图形硬件初始化

  • VirtualBox Guest Additions 的集成

OpenGL ES 和图形硬件初始化

在 Android 系统中,图形系统的初始化是由 SurfaceFlinger 完成的。除了我们在第十章中讨论的 Gralloc HAL 之外,启用图形是图形系统初始化的另一个重要部分,即加载 OpenGL ES 库。在我们的 VirtualBox 实现中,我们使用了 Android-x86 的大部分 HAL 模块。图形系统支持包括以下组件:

  • Gralloc HAL

  • Mesa 库用于 OpenGL ES

  • uvesafb 帧缓冲区驱动程序或 VirtualBox 视频驱动程序

我们在上章中讨论了 Gralloc HAL。在本章中,我们将探讨 OpenGL ES 库和 uvesafb 帧缓冲区驱动程序的加载。在介绍图形系统初始化时,我们将使用默认的 uvesafb 帧缓冲区驱动程序。在后面讨论 VirtualBox Guest Additions 的集成时,我们还将介绍如何从 VirtualBox 中使用本地图形驱动程序。

加载 OpenGL ES 库

OpenGL ES代表OpenGL 嵌入式系统,它是 Khronos 的 OpenGL 的一个子集。EGL 是 OpenGL ES 和底层本地平台之间的接口。EGL 的 API 应该是平台无关的,但 EGL API 的实现不是。

Android 中 OpenGL ES 的实现包括 Java API 和本地实现。这两部分可以在以下位置找到:

  • Java API:$AOSP/frameworks/base/opengl

  • OpenGL ES 本地:$AOSP/frameworks/native/opengl

这两部分 OpenGL 实现依赖于供应商实现来提供 OpenGL ES API 的完整功能。在系统启动过程中,系统将搜索/system/lib/egl/vendor/lib/egl路径以找到供应商的 OpenGL 库。

OpenGL ES 供应商库应遵循以下命名约定。如果供应商库是一个单独的库,它应使用libGLES_*.so的名称。在我们的例子中,VirtualBox 的 OpenGL ES 库是libGLES_mesa.so,它作为一个单独的库提供。

如果供应商库作为单独的库提供,它们必须类似于以下内容:

  • /system/lib/egl/libEGL_*.so

  • /system/lib/egl/libGLESv1_CM_*.so

  • /system/lib/egl/libGLESv2_*.so

这适用于 Android 模拟器的硬件仿真库。我们可以找到以下 Android 模拟器的库:

  • /system/lib/egl/libEGL_emulation.so

  • /system/lib/egl/libGLESv1_CM_emulation.so

  • /system/lib/egl/libGLESv2_emulation.so

供应商库在 SurfaceFlinger 初始化期间加载。在我们深入探讨启动过程之前,让我们先看看调试日志中的消息。

我已经从下面的日志中移除了时间戳,以便我们有一个更好的格式:

I SurfaceFlinger: SurfaceFlinger is starting 
I SurfaceFlinger: SurfaceFlinger's main thread ready to run. Initializing graphics H/W... 
D libEGL  : loaded /system/lib/egl/libGLES_mesa.so 
W linker  : /system/lib/libglapi.so has text relocations. This is wasting memory and prevents security hardening. Please fix. 
I HAL     : loaded HAL id=gralloc path=/system/lib/hw/gralloc.default.so hmi=0x5 handle=0xb7145664 
I EGL-DRI2: found extension DRI_Core version 1 
I EGL-DRI2: found extension DRI_SWRast version 5 
I EGL-DRI2: found extension DRI_TexBuffer version 2 
I EGL-DRI2: found extension DRI_IMAGE version 11 
I HAL     : loaded HAL id=gralloc path=/system/lib/hw/gralloc.default.so hmi=0x0 handle=0xb7145664 
I powerbtn: open event0(Power Button) ok fd=4 
W gralloc : page flipping not supported (yres_virtual=768, requested=1536) 
I gralloc : using (fd=12) 
I gralloc : id           = VESA VGA 
I gralloc : xres         = 1024 px 
I gralloc : yres         = 768 px 
I gralloc : xres_virtual = 1024 px 
I gralloc : yres_virtual = 768 px 
I gralloc : bpp          = 32 
I gralloc : r            = 16:8 
I gralloc : g            =  8:8 
I gralloc : b            =  0:8 
I gralloc : a            = 24:8 
I gralloc : stride       = 4096 
I gralloc : fbSize       = 12582912 
I gralloc : width        = 163 mm (159.568100 dpi) 
I gralloc : height       = 122 mm (159.895081 dpi) 
I gralloc : refresh rate = 65.46 Hz 
E SurfaceFlinger: hwcomposer module not found 

如我们所见,当 SurfaceFlinger 的主线程准备运行时,它会在 x86vbox 设备启动期间加载 /system/lib/egl/libGLES_mesa.so 库。之后,它加载并初始化 gralloc.default.so Gralloc 模块:

I SurfaceFlinger: EGL information: 
I SurfaceFlinger: vendor    : Android 
I SurfaceFlinger: version   : 1.4 Android META-EGL 
I SurfaceFlinger: extensions: EGL_KHR_get_all_proc_addresses EGL_ANDROID_presentation_time EGL_KHR_swap_buffers_with_damage EGL_KHR_image_base EGL_KHR_gl_texture_2D_image EGL_KHR_gl_texture_3D_image EGL_KHR_gl_texture_cubemap_image EGL_KHR_gl_renderbuffer_image EGL_KHR_reusable_sync EGL_KHR_fence_sync EGL_KHR_create_context EGL_KHR_surfaceless_context EGL_ANDROID_image_native_buffer EGL_KHR_wait_sync EGL_ANDROID_recordable  
I SurfaceFlinger: Client API: OpenGL_ES 
I SurfaceFlinger: EGLSurface: 8-8-8-8, config=0xb46a3800 

接下来,SurfaceFlinger 初始化 EGL 库,正如前面的日志消息所示。我们环境中的 EGL 版本是 1.4:

I SurfaceFlinger: OpenGL ES informations: 
I SurfaceFlinger: vendor    : VMware, Inc. 
I SurfaceFlinger: renderer  : Gallium 0.4 on llvmpipe (LLVM 3.7, 256 bits) 
I SurfaceFlinger: version   : OpenGL ES 3.0 Mesa 12.0.1 (git-c3bb2e3) 
I SurfaceFlinger: extensions: GL_EXT_blend_minmax GL_EXT_multi_draw_arrays GL_EXT_texture_compression_dxt1 GL_EXT_texture_format_BGRA8888 GL_OES_compressed_ETC1_RGB8_texture GL_OES_depth24 GL_OES_element_index_uint GL_OES_fbo_render_mipmap GL_OES_mapbuffer GL_OES_rgb8_rgba8 GL_OES_standard_derivatives GL_OES_stencil8 GL_OES_texture_3D GL_OES_texture_float GL_OES_texture_float_linear GL_OES_texture_half_float GL_OES_texture_half_float_linear GL_OES_texture_npot GL_EXT_texture_sRGB_decode GL_OES_EGL_image GL_OES_depth_texture GL_OES_packed_depth_stencil GL_EXT_texture_type_2_10_10_10_REV GL_OES_get_program_binary GL_APPLE_texture_max_level GL_EXT_discard_framebuffer GL_EXT_read_format_bgra GL_NV_fbo_color_attachments GL_OES_EGL_image_external GL_OES_EGL_sync GL_OES_vertex_array_object GL_ANGLE_texture_compression_dxt3 GL_ANGLE_texture_compression_dxt5 GL_EXT_texture_rg GL_EXT_unpack_subimage GL_NV_draw_buffers GL_NV_read_buffer GL_NV_read_depth GL_NV_read_depth_stencil GL_NV_read_stencil GL_EXT_draw_buffers GL_EXT_map_buffer_ra 
I SurfaceFlinger: GL_MAX_TEXTURE_SIZE = 8192 
I SurfaceFlinger: GL_MAX_VIEWPORT_DIMS = 8192 
D SurfaceFlinger: Open /dev/tty0 OK 
I HAL     : loaded HAL id=gralloc path=/system/lib/hw/gralloc.default.so hmi=0xb769a108 handle=0xb7145664 
I HAL     : loaded HAL id=gralloc path=/system/lib/hw/gralloc.default.so hmi=0xb769a108 handle=0xb7145664 
D SurfaceFlinger: Set power mode=2, type=0 flinger=0xb70e2000 
D SurfaceFlinger: shader cache generated - 24 shaders in 25.081509 ms 

在 EGL 初始化之后,OpenGL ES 库被初始化,正如我们从前面的日志消息中可以看到的。我们可以看到 Mesa 库支持 OpenGL ES 3.0。渲染引擎是一个使用 Gallium 和 llvmpipe 的软件实现。

每个图形硬件供应商通常都有自己的 OpenGL 实现。Mesa 是 OpenGL 的开源实现。Mesa 为 OpenGL 提供了多个后端支持。它可以根据硬件 GPU 支持硬件和软件实现。如果您没有硬件 GPU,Mesa 有三个基于 CPU 的实现:swrast、softpipe 和 llvmpipe。我们在 x86vbox 中使用的是 llvmpipe。Mesa 驱动实现有两种架构。Gallium 是 Mesa 驱动实现的新架构。

分析加载过程

在我们对 x86vbox 中 OpenGL ES 实现进行一般介绍(重用自 Android-x86)之后,我们将分析源代码,以获得更深入的理解。由于图形系统和 OpenGL ES 的详细实现非常庞大,我们无法在一个章节中涵盖它们。在我们的分析中,我们将专注于图形系统的加载过程和 OpenGL ES 库。

再次,当我们遍历源代码时,您可能会感到沮丧。帮助您最好的方法是,在阅读本章内容的同时打开您的源代码编辑器。如果您手头没有 AOSP 源代码,您始终可以参考以下网站:

xref.opersys.com/

您只需搜索本章中讨论的函数名,即可定位源代码。

从前面的调试日志中,我们将从看到第一个与图形系统和 SurfaceFlinger 相关的调试信息的位置开始,如下所示:

I SurfaceFlinger: SurfaceFlinger is starting 
I SurfaceFlinger: SurfaceFlinger's main thread ready to run. Initializing graphics H/W... 

第一条信息是由 SurfaceFlinger 的构造函数打印的,第二条信息是从 SurfaceFlingerinit 方法打印出来的。

SurfaceFlinger 的源代码可以在以下位置找到:$AOSP/frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

我们将从SurfaceFlinger:init开始分析,根据以下图中所示的流程:

图片

OpenGL ES 库的加载

SurfaceFlinger:init中,如以下代码片段所示,它首先调用 EGL 函数eglGetDisplay。之后,它尝试创建一个硬件合成器实例。使用显示实例mEGLDisplay和硬件合成器mHwc,它使用底层的 OpenGL ES 实现创建了一个渲染引擎:

void SurfaceFlinger::init() { 
    ALOGI(  "SurfaceFlinger's main thread ready to run. " 
            "Initializing graphics H/W..."); 

    Mutex::Autolock _l(mStateLock); 

    // initialize EGL for the default display 
    mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); 
    eglInitialize(mEGLDisplay, NULL, NULL); 

    ... 

    // Initialize the H/W composer object.  There may or may not  
    // be an actual hardware composer underneath. 
    mHwc = new HWComposer(this, 
            *static_cast<HWComposer::EventHandler *>(this)); 

    // get a RenderEngine for the given display / config 
    mRenderEngine = RenderEngine::create(mEGLDisplay, mHwc-
    >getVisualID()); 

    // retrieve the EGL context that was selected/created 
    mEGLContext = mRenderEngine->getEGLContext(); 

让我们先分析 EGL 函数eglGetDisplayeglGetDisplay函数在frameworks/native/opengl/libs/EGL/eglApi.cpp文件中实现,如下面的代码片段所示:

EGLDisplay eglGetDisplay(EGLNativeDisplayType display) 
{ 
    clearError(); 

    uintptr_t index = reinterpret_cast<uintptr_t>(display); 
    if (index >= NUM_DISPLAYS) { 
        return setError(EGL_BAD_PARAMETER, EGL_NO_DISPLAY); 
    } 

    if (egl_init_drivers() == EGL_FALSE) { 
        return setError(EGL_BAD_PARAMETER, EGL_NO_DISPLAY); 
    } 

    EGLDisplay dpy = egl_display_t::getFromNativeDisplay(display); 
    return dpy; 
} 

eglGetDisplay函数中,它首先检查要初始化的显示索引。在当前的 Android 代码中,EGL_DEFAULT_DISPLAY参数为 0,NUM_DISPLAYS的定义为 1。这意味着当前 Android 实现只能支持一个显示。这里是什么意思呢?例如,如果你有一台笔记本电脑,你可以将其连接到投影仪。在这种情况下,你可以同时拥有两个显示。现在一些新电脑甚至可以同时连接三个显示。在检查显示数量后,它调用egl_init_drivers函数来加载 OpenGL ES 库:

static EGLBoolean egl_init_drivers_locked() { 
    if (sEarlyInitState) { 
        // initialized by static ctor. should be set here. 
        return EGL_FALSE; 
    } 

    // get our driver loader 
    Loader& loader(Loader::getInstance()); 

    // dynamically load our EGL implementation 
    egl_connection_t* cnx = &gEGLImpl; 
    if (cnx->dso == 0) { 
        cnx->hooks[egl_connection_t::GLESv1_INDEX] = 
                &gHooks[egl_connection_t::GLESv1_INDEX]; 
        cnx->hooks[egl_connection_t::GLESv2_INDEX] = 
                &gHooks[egl_connection_t::GLESv2_INDEX]; 
        cnx->dso = loader.open(cnx); 
    } 

    return cnx->dso ? EGL_TRUE : EGL_FALSE; 
} 

static pthread_mutex_t sInitDriverMutex = PTHREAD_MUTEX_INITIALIZER; 

EGLBoolean egl_init_drivers() { 
    EGLBoolean res; 
    pthread_mutex_lock(&sInitDriverMutex); 
    res = egl_init_drivers_locked(); 
    pthread_mutex_unlock(&sInitDriverMutex); 
    return res; 
} 

egl_init_drivers函数获取一个互斥锁并调用另一个函数egl_init_drivers_locked来加载 OpenGL ES 库。在egl_init_drivers_locked函数中,它获取一个Loader类的实例,该类使用单例模式定义。之后,它初始化全局变量gEGLImpl,该变量定义为egl_connection_t数据结构:

struct egl_connection_t { 
    enum { 
        GLESv1_INDEX = 0, 
        GLESv2_INDEX = 1 
    }; 

    inline egl_connection_t() : dso(0) { } 
    void *              dso; 
    gl_hooks_t *        hooks[2]; 
    EGLint              major; 
    EGLint              minor; 
    egl_t               egl; 

    void*               libEgl; 
    void*               libGles1; 
    void*               libGles2; 
}; 

egl_connection_t数据结构中,它定义了以下字段:

  • dso: 这是一个指向driver_t数据结构的指针,该数据结构在Loader类内部定义。这个driver_t数据结构存储了Loader类加载后的 OpenGL ES 库的句柄。

  • hooks: 这是一个指向gl_hooks_t数据结构指针的数组。gl_hooks_t数据结构用于定义 OpenGL ES API 的所有函数指针。在 OpenGL ES 库加载后,库中的 OpenGL ES 函数将被初始化并分配给hooks字段。在enum { GLESv1_INDEX , GLESv2_INDEX }中定义了两个 OpenGL ES 版本。hooks[GLESv1_INDEX]用于存储 OpenGL ES 1 API,并指向全局变量gHooks[GLESv1_INDEX]。对于GLESv2_INDEX也是同样的情况。OpenGL ES API 的列表可以在以下文件中找到:$AOSP/frameworks/native/opengl/libs/entries.in

  • majorminor: 这两个用于存储 EGL 版本。

  • egl: 这被定义为egl_t,用于存储 EGL API。EGL API 的列表可以在以下文件中找到:$AOSP/frameworks/native/opengl/libs/EGL/egl_entries.in

  • libEgllibGles1libGles2:这些都是 OpenGL ES 包装库的句柄。我们稍后将看到这些库的初始化。

cnx数据结构初始化后,它调用loader.open函数来加载库。让我们看看loader.open函数:

void* Loader::open(egl_connection_t* cnx) 
{ 
    void* dso; 
    driver_t* hnd = 0; 

    dso = load_driver("GLES", cnx, EGL | GLESv1_CM | GLESv2); 
    if (dso) { 
        hnd = new driver_t(dso); 
    } else { 
        // Always load EGL first 
        dso = load_driver("EGL", cnx, EGL); 
        if (dso) { 
            hnd = new driver_t(dso); 
            hnd->set( load_driver("GLESv1_CM", cnx, GLESv1_CM), 
            GLESv1_CM ); 
            hnd->set( load_driver("GLESv2",    cnx, GLESv2),    GLESv2 ); 
        } 
    } 

    LOG_ALWAYS_FATAL_IF(!hnd, "couldn't find an OpenGL ES 
    implementation"); 

#if defined(__LP64__) 
    cnx->libEgl   = load_wrapper("/system/lib64/libEGL.so"); 
    cnx->libGles2 = load_wrapper("/system/lib64/libGLESv2.so"); 
    cnx->libGles1 = load_wrapper("/system/lib64/libGLESv1_CM.so"); 
#else 
    cnx->libEgl   = load_wrapper("/system/lib/libEGL.so"); 
    cnx->libGles2 = load_wrapper("/system/lib/libGLESv2.so"); 
    cnx->libGles1 = load_wrapper("/system/lib/libGLESv1_CM.so"); 
#endif 
    LOG_ALWAYS_FATAL_IF(!cnx->libEgl, 
            "couldn't load system EGL wrapper libraries"); 

    LOG_ALWAYS_FATAL_IF(!cnx->libGles2 || !cnx->libGles1, 
            "couldn't load system OpenGL ES wrapper libraries"); 

    return (void*)hnd; 
} 

Loader::open中,它首先尝试加载单个 OpenGL ES 库。如果失败,它将逐个尝试加载分离的库。如果库加载成功,它将把句柄存储到driver_t数据结构中。我们之前在讨论egl_connection_t数据结构中的dso字段时解释了driver_t。实际的加载过程是在load_driver函数中完成的,我们很快就会看到它。在 OpenGL ES 库加载后,它还尝试使用load_wrapper函数加载包装库。load_wrapper函数只是调用dlopen系统调用并返回句柄,所以我们不需要调查它。

加载驱动程序

让我们分析load_driver函数,这是找到和加载 OpenGL ES 用户空间驱动程序的函数:

void *Loader::load_driver(const char* kind, 
        egl_connection_t* cnx, uint32_t mask) 
{ 
    class MatchFile { 
    public: 
        static String8 find(const char* kind) { 
    ... 
    }; 

    String8 absolutePath = MatchFile::find(kind); 
    if (absolutePath.isEmpty()) { 
        // this happens often, we don't want to log an error 
        return 0; 
    } 
    const char* const driver_absolute_path = absolutePath.string(); 

    void* dso = dlopen(driver_absolute_path, RTLD_NOW | RTLD_LOCAL); 
    if (dso == 0) { 
        const char* err = dlerror(); 
        ALOGE("load_driver(%s): %s", driver_absolute_path, err?
        err:"unknown"); 
        return 0; 
    } 

    ALOGD("loaded %s", driver_absolute_path); 

    if (mask & EGL) { 
        getProcAddress = (getProcAddressType)dlsym(dso, 
        "eglGetProcAddress"); 

        ALOGE_IF(!getProcAddress, 
                "can't find eglGetProcAddress() in %s", 
                driver_absolute_path); 

        egl_t* egl = &cnx->egl; 
        __eglMustCastToProperFunctionPointerType* curr = 
            (__eglMustCastToProperFunctionPointerType*)egl; 
        char const * const * api = egl_names; 
        while (*api) { 
            char const * name = *api; 
            __eglMustCastToProperFunctionPointerType f = 
                (__eglMustCastToProperFunctionPointerType)dlsym(dso, 
                name); 
            if (f == NULL) { 
                // couldn't find the entry-point, use 
                // eglGetProcAddress() 

                f = getProcAddress(name); 
                if (f == NULL) { 
                    f = (__eglMustCastToProperFunctionPointerType)0; 
                } 
            } 
            *curr++ = f; 
            api++; 
        } 
    } 

    if (mask & GLESv1_CM) { 
        init_api(dso, gl_names, 
            (__eglMustCastToProperFunctionPointerType*) 
                &cnx->hooks[egl_connection_t::GLESv1_INDEX]->gl, 
               getProcAddress); 
    } 

    if (mask & GLESv2) { 
      init_api(dso, gl_names, 
            (__eglMustCastToProperFunctionPointerType*) 
                &cnx->hooks[egl_connection_t::GLESv2_INDEX]->gl, 
            getProcAddress); 
    } 

    return dso; 
} 

load_driver函数中,它定义了一个MatchFile内部类。它使用MatchFile::find方法来查找库的路径。load_driver函数有三个参数:kindcnxmask。根据库的类型,参数kind可以是GLESEGLGLESv1_CMGLESv2。一旦它获取到库的绝对路径,它就调用dlopen系统函数来打开共享库。mask参数是kind参数的位图。使用mask参数,它可以根据库的类型初始化cnx参数。正如我们之前提到的,cnx参数,它是一个egl_connection_t实例,有一个egl字段来存储所有 EGL 函数指针。它还有一个字段,hooks[GLESv1_INDEX]/hooks[GLESv2_INDEX],用于存储所有 OpenGL ES 函数。

如果库类型是 EGL,它首先通过调用dlsym系统函数来获取eglGetProcAddress函数的地址。之后,它将遍历在egl_names全局变量中定义的所有函数名称,以找出地址并将它们存储在cnx->egl中。在这个过程中,它首先尝试使用dlsym系统函数获取地址。如果dlsym调用失败,它将再次尝试使用eglGetProcAddress函数。

如果库类型是GLESv1_CMGLESv2,它将调用另一个函数init_api来初始化所有 OpenGL ES 函数指针。在init_api函数中,它将遍历在gl_names全局变量中定义的所有函数名称,以找出地址并将它们存储在cnx->hooks[egl_connection_t::GLESv?_INDEX]->gl中。

现在我们已经完成了 OpenGL ES 用户空间驱动程序的初始化,我们可以使用egl_connection_t数据结构来访问所有 OpenGL ES 供应商 API。

创建渲染引擎

在加载 OpenGL ES 供应商库之后,SurfaceFlinger:init将创建渲染引擎:

mRenderEngine = RenderEngine::create(mEGLDisplay, mHwc->getVisualID()); 

RenderEngine::create内部,它将调用RenderEngine::chooseEglConfig,这将打印出 EGL 的调试信息:

EGLConfig RenderEngine::chooseEglConfig(EGLDisplay display, int format) { 
    status_t err; 
    EGLConfig config; 

    // First try to get an ES2 config 
    err = selectEGLConfig(display, format, EGL_OPENGL_ES2_BIT, 
    &config); 
    ... 
    eglGetConfigAttrib(display, config, EGL_ALPHA_SIZE, &a); 
    ALOGI("EGL information:"); 
    ALOGI("vendor    : %s", eglQueryString(display, EGL_VENDOR)); 
    ALOGI("version   : %s", eglQueryString(display, EGL_VERSION)); 
    ALOGI("extensions: %s", eglQueryString(display, EGL_EXTENSIONS)); 
    ALOGI("Client API: %s", eglQueryString(display, 
    EGL_CLIENT_APIS)?:"Not 
    Supported"); 
    ALOGI("EGLSurface: %d-%d-%d-%d, config=%p", r, g, b, a, config); 

    return config; 
} 

RenderEngine::create的末尾,它将打印出以下 OpenGL ES 初始化信息:

RenderEngine* RenderEngine::create(EGLDisplay display, int hwcFormat) { 
    EGLConfig config = EGL_NO_CONFIG; 
    if (!findExtension( 
            eglQueryStringImplementationANDROID(display,  
            EGL_EXTENSIONS), 
            "EGL_ANDROIDX_no_config_context")) { 
        config = chooseEglConfig(display, hwcFormat); 
    } 

    ... 

    engine->setEGLHandles(config, ctxt); 

    ALOGI("OpenGL ES informations:"); 
    ALOGI("vendor    : %s", extensions.getVendor()); 
    ALOGI("renderer  : %s", extensions.getRenderer()); 
    ALOGI("version   : %s", extensions.getVersion()); 
    ALOGI("extensions: %s", extensions.getExtension()); 
    ALOGI("GL_MAX_TEXTURE_SIZE = %zu", engine->getMaxTextureSize()); 
    ALOGI("GL_MAX_VIEWPORT_DIMS = %zu", engine->getMaxViewportDims()); 

    eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, 
    EGL_NO_CONTEXT); 
    eglDestroySurface(display, dummy); 

    return engine; 
} 

uvesafb 帧缓冲区驱动程序

帧缓冲区驱动程序是我们需要支持的 x86vbox 图形系统的第三个组件。由于您可能在不同的英特尔设备上运行 VirtualBox,它们可能使用不同的图形硬件,例如 Nvidia、AMD 或英特尔。为了在虚拟化环境中获得最佳性能,您可能想探索各种 GPU 虚拟化技术,如 GPU 直通、GPU 共享、GPU 软件仿真等。为了有一个简单的解决方案,我们使用了 Android-x86 的默认解决方案,即 uvesafb 帧缓冲区驱动程序。

什么是 uvesafb?

uvesafb 是一个与 VESA 2.0 兼容的图形卡一起工作的用户空间 VESA 帧缓冲区驱动程序。VESA BIOS 扩展通过 BIOS 接口提供了 VESA 标准的主要功能。在 Linux 上,uvesafb 需要一个名为v86d的用户空间守护进程作为需要执行 x86 BIOS 代码的内核驱动程序的后端。由于 BIOS 代码只能在受控环境中执行,因此v86d执行的代码可以在完全软件模拟的环境或由 CPU 支持的虚拟化环境中运行。Android-x86 项目已经将v86d移植到 Android。它可以在$AOSP/external/v86d找到。由于v86d项目需要额外的系统调用,如iopermiopl,Android-x86 项目将 bionic 库更改为支持这些系统调用。

您可以参考以下内核文档以了解更多关于 uvesafb 的信息:

www.kernel.org/doc/Documentation/fb/uvesafb.txt

测试 uvesafb 帧缓冲区驱动程序

在我们尝试了解 uvesafb 在我们环境中是如何加载之前,我们可以使用两个帧缓冲区测试工具fbsetfbtest来测试它。

正如我们所知,我们可以使用 Android-x86 的二级引导从第九章,“使用 PXE/NFS 引导 x86vbox”进行两阶段引导进入调试控制台。我们可以在调试控制台中用fbsetfbtest测试 uvesafb。

fbset是一个显示或更改帧缓冲区设备设置的系统工具。您可以通过查看 Linux 命令的帮助页面来了解如何使用fbset。在我们的环境中,我们在第一阶段引导中使用busybox,在 Android 环境中使用toyboxtoolbox。由于fbsetbusybox支持,因此我们可以通过busybox在第一阶段或第二阶段引导中使用它。

fbtest是一个帧缓冲区测试程序,可以在git.kernel.org/pub/scm/linux/kernel/git/geert/fbtest.git找到。

我从内核 Git 仓库克隆了它,并将其移植到 Android 环境中。Android 的源代码可以通过以下链接在 GitHub 上找到:

github.com/shugaoye/fbtest

要构建fbtest,我们可以从 GitHub 获取它,并在 AOSP 构建环境中构建:

$ cd {your AOSP root folder}
$ source build/envsetup.sh
$ lunch x86vbox-eng  

在我们设置好 AOSP 构建环境后,我们可以使用以下命令检出并构建fbtest源代码:

$ cd $HOME
$ git clone https://github.com/shugaoye/fbtest
$ cd fbtest
$ git checkout -b android-x86 remotes/origin/android-x86
$ make  

注意,我已经修改了 Makefile,并且它依赖于 AOSP 环境变量$OUT,如下所示:

# Paths and settings 
TARGET_PRODUCT = x86vbox 
ANDROID_ROOT   = $(OUT)/../../../.. 
BIONIC_LIBC    = $(ANDROID_ROOT)/bionic/libc 
PRODUCT_OUT    = $(ANDROID_ROOT)/out/target/product/$(TARGET_PRODUCT) 
CROSS_COMPILE  = \ 
    $(ANDROID_ROOT)/prebuilts/gcc/linux-x86/x86/x86_64-linux-android-  
    4.9/bin/x86_64-linux-android- 

ARCH_NAME = x86 

# Tool names 
AS            = $(CROSS_COMPILE)as 
AR            = $(CROSS_COMPILE)ar 
... 

如果我们查看前面的fbtest/Rules.make Makefile,我们使用$OUT环境变量来找到正确的 AOSP 构建环境。之后,我们可以使用预构建的工具链和bionic库来构建fbtest

在我们构建fbtest后,我们可以将其复制到$OUT/system/bin文件夹,这样我们就可以在测试环境中稍后使用它。正如我们从第九章,“使用 PXE/NFS 引导 x86vbox”中记得的那样,我们可以使用 PXE/NFS 在第一阶段引导时引导到调试控制台。在这种情况下,我们可以更改并测试fbtest而不需要重新引导 x86vbox,因为我们可以从 x86vbox 通过 NFS 访问构建结果。

让我们在第一阶段引导时引导 x86vbox 到调试控制台,并执行测试。正如我们回忆的那样,我们在第一阶段引导的调试控制台中有一个最小嵌入式 Linux 环境。在这个环境中我们有内置的busybox。在我们测试帧缓冲设备之前,我们必须手动加载uvesafb模块,如下面的截图所示:

图片

我们使用以下命令加载uvesafb模块:

(debug-late)@android: /android # system/xbin/modprobe uvesafb  

从调试输出中,我们可以看到底层图形硬件是Oracle VM VirtualBox VBE Adapter

在加载uvesafb模块后,我们可以找到/dev/fb0设备。我们可以使用fbset来更改帧缓冲设备设置。例如,我们可以根据需要切换到不同的支持分辨率。让我们运行fbset命令看看会发生什么。如果我们不带任何参数运行fbset,我们可以看到以下输出:

(debug-late)@android:/android # fbset 

mode "640x480-60" 
 # D: 23.845 MHz, H: 29.844 kHz, V: 60.048 Hz 
 geometry 640 480 640 9830 16 
 timings 41937 80 16 13 1 63 3 
 accel false 
 rgba 5/11,6/5,5/0,0/0 
endmode  

如果不带任何参数运行fbset,它只会打印出帧缓冲设备的当前设置。正如我们从输出中可以看到的,如果我们不带任何参数加载uvesafb,默认分辨率为 640 x 480,16 位颜色。

我们可以用分辨率的名称尝试更改分辨率,如下所示:

(debug-late)@android:/android # fbset vga 
fbset: /etc/fb.modes: No such file or directory 
fbset: unknown video mode 'vga'   

我们得到了一个错误信息,告诉我们分辨率在/etc/fb.modes文件中未定义。我们需要创建此文件来更改分辨率。我们可以在/etc/fb.modes中添加以下分辨率,如下所示:

mode "640x480-60" 
        # D: 23.845 MHz, H: 29.844 kHz, V: 60.048 Hz  
        geometry 640 480 640 9830 16  
        timings 41937 80 16 13 1 63 3  
        accel false  
        rgba 5/11,6/5,5/0,0/0  
endmode 

mode "1024x768-60"  
        # D: 64.033 MHz, H: 47.714 kHz, V: 60.018 Hz  
        geometry 1024 768 1024 768 32  
        timings 15617 159 52 23 1 107 3  
        accel false  
        rgba 8/16,8/8,8/0,8/24  
endmode  

现在我们可以测试分辨率更改。如果我们运行以下命令,我们可以切换到具有真彩色的更高分辨率:

(debug-late)@android:/android # fbset 1024x768-60  

在我们加载帧缓冲驱动程序并测试配置更改后,我们可以通过在屏幕上绘制一些东西来测试帧缓冲。使用我们在本节中构建的 fbtest 命令,我们可以运行一系列帧缓冲测试用例。首先,让我们找出 fbtest 可以运行多少个测试用例:

(debug-late)@android:/android # fbtest -f /dev/fb0 -l
Listing all tests 
test001: Draw a 16x12 checkerboard pattern 
test002: Draw a grid and some circles 
test003: Draw the 16 Linux console colors 
test004: Show the penguins 
test005: Draw the default color palette 
test006: Draw grayscale bands 
test007: DirectColor test 
test008: Draw the UV color space 
test009: Show the penguins using copy_rect 
test010: Hello world 
test011: Panning test 
test012: Filling squares   

如果我们在 fbtest 中使用 -l 选项,它将打印出可用的测试用例列表。我们可以看到我们有 12 个测试用例:

(debug-late)@android:/android # fbtest -f /dev/fb0 test002  

例如,我们可以运行测试用例 002,我们将看到以下屏幕。您可以自由地测试任何前面的测试用例。

初始化 uvesafb 在 x86vbox

在 x86vbox 中初始化 uvesafb

在 x86vbox 中初始化 uvesafb 是在启动脚本 init.sh 中完成的。如果我们回顾第八章创建您的虚拟机设备中关于 HAL 初始化的讨论,我们可以看到 init.sh 中的以下代码。我们在第八章创建您的虚拟机设备中简要讨论了图形 HAL 的初始化,现在我们可以深入了解细节:

function init_uvesafb() 
{ 
    case "$PRODUCT" in 
        ET2002*) 
            UVESA_MODE=${UVESA_MODE:-1600x900} 
            ;; 
        *) 
            ;; 
    esac 

    [ "$HWACCEL" = "0" ] && bpp=16 || bpp=32 
    modprobe uvesafb mode_option=${UVESA_MODE:-1024x768}-$bpp ${UVESA_OPTION:-    
    mtrr=3 scroll=redraw} 
} 

function init_hal_gralloc() 
{ 
    case "$(cat /proc/fb | head -1)" in 
        *virtiodrmfb) 
#            set_property ro.hardware.hwcomposer drm 
            ;& 
        0*inteldrmfb|0*radeondrmfb|0*nouveaufb|0*svgadrmfb) 
            set_property ro.hardware.gralloc drm 
            set_drm_mode 
            ;; 
        "") 
            init_uvesafb 
            ;& 
        0*) 
            ;; 
    esac 

    [ -n "$DEBUG" ] && set_property debug.egl.trace error 
} 

在我们当前的设置中,让我们看看 /proc/fb 的内容。我们可以从调试控制台或 adb 控制台来检查这一点。在帧缓冲设备初始化之前,/proc/fb 的内容是空的。在我们的情况下,它是空的,因为没有帧缓冲设备可用,直到执行 init.sh 脚本。如果输出为空,init.sh 脚本将调用 init_uvesafb 函数来初始化 uvesafb。在帧缓冲设备初始化后,我们可以看到 /proc/fb 的内容如下:

root@x86vbox:/ # cat /proc/fb 
0 VESA VGA  

如果在调用 init.sh 之前有帧缓冲设备可用,init_hal_gralloc 将为 DRM 驱动程序设置 ro.hardware.gralloc 系统属性。对于 init_hal_gralloc 无法处理的设备,它将不执行任何操作。

init_uvesafb 中,加载 uvesafb 的实际命令可以扩展到以下之一:

# modprobe uvesafb mode_option=1024x768-32 mtrr=3 scroll=redraw  

uvesafb 的选项有:

  • mtrr:n: 为帧缓冲设置内存类型范围寄存器,其中 n 可以是:

    • 0: 禁用(相当于 nomtrr 选项)

    • 3: 写合并(默认)

内存类型范围寄存器是英特尔处理器中一组处理器补充功能控制寄存器。写合并允许将总线写传输组合成更大的传输,然后再在总线上爆发。这可以帮助提高图形性能。

  • redraw: 通过重新绘制受影响的屏幕部分来滚动。

  • mode_option: 设置分辨率到一个支持的值。

在加载 uvesafb 之后,我们可以使用以下命令找到所有支持的分辨率:

# cat /sys/bus/platform/drivers/uvesafb/uvesafb.0/vbe_modes
640x400-8, 0x0100 
640x480-8, 0x0101 
800x600-8, 0x0103 
1024x768-8, 0x0105 
1280x1024-8, 0x0107 
320x200-15, 0x010d 
320x200-16, 0x010e 
320x200-24, 0x010f 
640x480-15, 0x0110 
640x480-16, 0x0111 
640x480-24, 0x0112 
800x600-15, 0x0113 
800x600-16, 0x0114 
800x600-24, 0x0115 
1024x768-15, 0x0116 
1024x768-16, 0x0117 
1024x768-24, 0x0118 
1280x1024-15, 0x0119 
1280x1024-16, 0x011a 
1280x1024-24, 0x011b 
320x200-32, 0x0140 
640x400-32, 0x0141 
640x480-32, 0x0142 
800x600-32, 0x0143 
1024x768-32, 0x0144 
1280x1024-32, 0x0145 
320x200-8, 0x0146 
1600x1200-32, 0x0147 
1152x864-8, 0x0148 
1152x864-15, 0x0149 
1152x864-16, 0x014a 
1152x864-24, 0x014b 
1152x864-32, 0x014c  

集成 VirtualBox Guest Additions

到目前为止,我们可以启动 x86vbox 到 Android。我们可以进一步做的是将 VirtualBox Guest Additions 集成到 x86vbox 中。

VirtualBox 是一个虚拟化环境。我们可以在 VirtualBox 中安装一个虚拟操作系统,就像它真的存在一样。然而,以这种方式工作有一些限制。在主机环境中运行虚拟操作系统时,你可能期望的不仅仅是硬件虚拟化。例如,当你在不同主机和虚拟系统之间移动鼠标光标时,你可能发现鼠标光标表现不佳。你可能希望轻松地在主机和虚拟机之间共享数据,例如共享剪贴板、共享文件夹等。为了满足这些需求,主机和虚拟机需要相互了解并有一种相互交流的方式。在 VirtualBox 架构中,有一个称为 主机-虚拟机通信管理器HGCM)的组件。在主机端,VirtualBox 实现了一个名为 HGCM 服务的功能,可以响应虚拟机的请求。在虚拟机端,有几个来自 VirtualBox 的内核驱动程序,可以用来与主机通信。

VirtualBox 为主机和虚拟机集成提供的附加功能通常包含在一个名为 VirtualBox 扩展包VirtualBox Extension Pack)的包中。在 VirtualBox 扩展包中,它包括主机端和虚拟机端所需的所有文件。VirtualBox 扩展包可以从 www.virtualbox.org/wiki/Downloads 下载。

对于虚拟机端,有一个包含设备驱动程序二进制工具和源代码的单独分发包,称为 VirtualBox 虚拟机增强功能。有针对 Windows、Linux 和 OS X 的单独 VirtualBox 虚拟机增强功能。没有针对 Android 的虚拟机增强功能。然而,由于 Android 使用 Linux 内核,我们可以使用 Linux 的源代码构建 Android 的内核驱动程序。安装 VirtualBox 扩展包后,我们可以找到一个名为 VBoxGuestAdditions.iso 的镜像文件,如下所示:

$ cd /usr/share/virtualbox
$ ls
nls                    src                     VBoxSysInfo.sh
rdesktop-vrdp-keymaps  VBoxCreateUSBNode.sh
rdesktop-vrdp.tar.gz   **VBoxGuestAdditions.iso**  

我们可以提取这个镜像文件,并在 VirtualBox 虚拟机增强功能中找到以下文件:

$ ls
deffiles    routines.sh      vboxadd-x11                       x86
installer   vboxadd          **VBoxGuestAdditions-amd64.tar.bz2**
install.sh  vboxadd-service  **VBoxGuestAdditions-x86.tar.bz2**  

有两个压缩文件:VBoxGuestAdditions-amd64.tar.bz2VBoxGuestAdditions-x86.tar.bz2。如以下截图所示,这是为 Intel x86 Linux 虚拟机提供的虚拟机增强功能的文件夹和文件列表:

图片

VirtualBox 虚拟机增强功能

在虚拟机增强功能中有三个驱动程序的源代码可用:vboxguestvboxsfvboxvideo

  • vboxguest: 这个模块为虚拟操作系统提供与主机通信的基本服务。

  • vboxsf: 这个模块是一个内核驱动程序,用于在主机和虚拟机之间共享文件的能力。

  • vboxvideo: 这个模块是针对虚拟机的视频驱动程序。通过这个驱动程序,我们可以通过主机使用图形硬件加速。

我们将构建并将这三个驱动程序集成到 x86vbox 中。

构建 VirtualBox 虚拟机增强功能

Guest Additions 中的驱动程序的唯一依赖项是内核源代码。为 Android 构建驱动程序非常简单。要构建 Guest Additions,你可以从你的 VirtualBox 安装中获取源代码,或者你可以从我的 GitHub 上获取一个版本,如下所示:

$ source build/envsetup.sh
$ lunch x86vbox-eng
$ cd $HOME
$ git clone https://github.com/shugaoye/vboxguest-linux-modules
$ cd vboxguest-linux-modules
$ make BUILD_TARGET_ARCH=x86 KERN_DIR=$OUT/obj/KERNEL_OBJ  

在我们成功构建驱动程序后,我们可以按照以下方式找到驱动模块:

$ ls
build_in_tmp  Makefile   vboxguest.ko  vboxsf.ko  vboxvideo.ko
LICENSE       vboxguest  vboxsf        vboxvideo  

我们可以在我们的 x86vbox 设备文件夹下的 vbox 文件夹中存储内核模块,因此我们可以在构建过程中将它们复制到文件系统中:

$ croot
$ cd device/generic/x86vbox
$ ls vbox
vboxguest.ko  vboxsf.ko  vboxvideo.ko  

在我们获得 Guest Additions 的可加载模块后,我们可以将它们添加到我们的 x86vbox 设备 Makefile x86vbox.mk 中,如下所示:

... 
PRODUCT_COPY_FILES += \ 
    device/generic/x86vbox/vbox/vboxguest.ko:system/vendor/vbox/vboxguest.ko \ 
    device/generic/x86vbox/vbox/vboxsf.ko:system/vendor/vbox/vboxsf.ko \ 
    device/generic/x86vbox/vbox/vboxvideo.ko:system/vendor/vbox/vboxvideo.ko \ 
... 

这三个模块将被复制到系统镜像中的 /system/vendor/vbox 文件夹。

集成 vboxsf

使用可加载模块 vboxsf.ko,我们能够在 Android 系统运行时在主机和虚拟机之间交换文件。要创建主机和虚拟机之间的共享文件夹,我们需要首先加载 vboxsf.ko 模块。

要使用 vboxsf.ko,我们需要一个名为 mount.vboxsf 的工具,它可以用来将主机文件系统上的共享文件夹挂载到 Android 文件系统。这个 mount.vboxsf 工具是 VirtualBox Guest Additions 提供的实用工具之一。我们将其放在我们的 x86vbox 设备文件夹中,如下所示:

$ ls mount.vboxsf/ 
Android.mk  mount.vboxsf.c  vbsfmount.h 

它包括一个 C 文件和一个头文件。我们创建了以下 Android Makefile 来构建它:

LOCAL_PATH:= $(call my-dir) 
include $(CLEAR_VARS) 

LOCAL_SRC_FILES:= mount.vboxsf.c 

LOCAL_CFLAGS:=-O2 -g 
#LOCAL_CFLAGS+=-DLINUX 

LOCAL_MODULE:=mount.vboxsf 
LOCAL_MODULE_TAGS := optional 

include $(BUILD_EXECUTABLE) 

为了将其包含在系统镜像中,我们还需要将其添加到 x86vbox.mk Makefile 中,如下所示:

... 
PRODUCT_PACKAGES += \ 
    mount.vboxsf \ 
... 

为了在系统启动时加载 vboxsf.ko,我们需要将 vboxsf.ko 的加载添加到 initrd.img 中的启动脚本。如果我们回想一下 第六章,使用自定义 Ramdisk 调试启动过程,我们讨论了 initrd.img 中的 init 脚本。shell 脚本函数 load_modules 被调用以在第一阶段启动时加载大部分设备驱动程序。我们可以更改此脚本以加载 VirtualBox 设备驱动程序,如下所示:

load_modules() 
{ 
   if [ -z "$FOUND" ]; then 
         auto_detect 
   fi 

   # 3G modules 
   for m in $EXTMOD; do 
         busybox modprobe $m 
   done 

    if [ -n "$VBOX_GUEST_ADDITIONS" ]; then 
         echo "Loading VBOX_GUEST_ADDITIONS ..." 
         insmod /android/system/vendor/vbox/vboxguest.ko 
         insmod /android/system/vendor/vbox/vboxsf.ko 
         if [ ! -e /android$SDCARD ]; then 
               mkdir /android$SDCARD 
               /android/system/bin/mount.vboxsf sdcard /android$SDCARD 
         fi 
   fi 
} 

我们定义了一个 VBOX_GUEST_ADDITIONS 内核参数,它可以用来启用加载 VirtualBox 特定设备驱动程序。如果定义了这个内核参数,我们将加载两个可加载模块,vboxguest.kovboxsf.ko。另一个内核参数 SDCARD 也被定义,这样我们就可以将共享文件夹挂载为外部 SD 卡存储。SDCARD 内核参数被 shell 脚本函数 mount_sdcard 使用。

要在内核命令行上定义这两个内核参数,我们需要更改 $HOME/.VirtualBox/TFTP/pxelinux.cfg/default 下的 PXE 启动脚本,如下所示:

label 1\. x86vbox (2 stages boot) 
menu x86vbox_initrd 
kernel x86vbox/kernel 
append ip=dhcp console=ttyS3,115200 androidboot.selinux=permissive buildvariant=eng initrd=x86vbox/initrd.img androidboot.hardware=x86vbox DEBUG=2 SRC=/android-x86vbox ROOT=/dev/sda1 VBOX_GUEST_ADDITIONS=1 SDCARD=vendor DATA=sda2 X86VBOX=1 

注意两个变量 SDCARDVBOX_GUEST_ADDITIONS。它们是我们添加的两个新内核参数,用于支持加载 VirtualBox 设备驱动程序。为了挂载共享文件夹,我们在脚本中添加以下命令:

/android/system/bin/mount.vboxsf sdcard /android$SDCARD 

mount.vboxsf的第一个参数是我们定义在 VirtualBox 设置中的共享文件夹,如下面的截图所示:

图片

通过所有与共享文件夹相关的更改,我们可以有一个可以用来在主机和客户机之间轻松共享数据的方法。

集成 vboxvideo

在 VirtualBox 的 Guest Additions 中,还有一个可以在 Android 上使用的设备驱动程序,即vboxvideo.ko。这是一个视频硬件的设备驱动程序。与本章中刚刚讨论的 uvesafb 相比,它提供了一个功能更强大的视频驱动程序。

uvesafb 是基于 VESA 2.0 标准的标准帧缓冲驱动程序,它不支持在 VirtualBox 上的硬件加速。vboxvideo.ko是一个支持硬件加速的基于 DRM/DRI 的视频驱动程序。

直接渲染基础设施DRI)是 Linux 平台上 X Window 系统的新架构,允许X客户端直接与图形硬件通信。直接渲染管理器DRM)是 DRI 架构的内核部分。

Android-x86 项目是第一个将 Mesa/DRM 引入 Android 平台的开源项目。这是一个针对支持的图形硬件的开源 OpenGL ES 实现。通过以下组件,我们应该能够支持 Android 上的 OpenGL ES 的硬件加速:

  • external_libdrm

  • external_mesa

  • external_drm_gralloc

我们在 VirtualBox 上已经有了vboxvideo的 DRM 驱动程序,但相关的实现仍需要添加到external_mesaexternal_drm_gralloc中,以支持使用主机 GPU 的 OpenGL ES。

external_mesaexternal_drm_gralloc中没有针对 VirtualBox 的特定实现,我们只能使用 Mesa 的相同基于软件的实现来支持 OpenGL ES 和默认的 Gralloc 模块gralloc.default.so。这就是为什么大多数基于 VirtualBox 的模拟器解决方案,如 Genymotion、Andy 或 AMI DuOS,仍然在使用硬件 GPU 模拟,这与我们在第十章“启用图形”部分中讨论的硬件 GLES 模拟概述中的类似。

要加载vboxvideo.ko,我们需要在load_modules中添加以下这三行:

   if [ -n "$VBOX_VIDEO_DRIVER" ]; then 
       modprobe ttm 
       modprobe drm_kms_helper 
       insmod /android/system/vendor/vbox/vboxvideo.ko 
   fi 

ttmdrm_kms_helper内核模块是vboxvideo.ko所需的两个内核模块。我们还使用VBOX_VIDEO_DRIVER内核参数来配置vboxvideo.ko的加载。使用这个内核参数,我们可以在 uvesafb 帧缓冲和 VirtualBox 帧缓冲之间切换。系统启动后,我们可以看到以下日志消息。我们可以看到vboxvideo已成功加载:

[   25.240357] vboxguest: misc device minor 53, IRQ 20, I/O port d040, MMIO at) 
[   25.261044] [drm] Initialized drm 1.1.0 20060810                             
[   25.290777] [drm] VRAM 08000000                                              
[   25.309754] [TTM] Zone  kernel: Available graphics memory: 440884 kiB        
[   25.337733] [TTM] Zone highmem: Available graphics memory: 1034776 kiB       
[   25.349078] [TTM] Initializing pool allocator                                
[   25.349735] fbcon: vboxdrmfb (fb0) is primary device                         
[   25.360984] Console: switching to colour frame buffer device 100x37          
[   25.380299] vboxvideo 0000:00:02.0: fb0: vboxdrmfb frame buffer device       
[   25.388745] [drm] Initialized vboxvideo 1.0.0 20130823 for 0000:00:02.0 on m0 

从日志消息中,我们可以看到vboxvideo创建了一个vboxdrmfb帧缓冲设备。我们可以使用fbset来检查帧缓冲设置,就像我们之前做的那样。我们可以看到,对于vboxdrmfb,硬件加速被设置为 true:

(debug-late)@android:/android # fbset 

mode "800x600-0" 
 # D: 0.000 MHz, H: 0.000 kHz, V: 0.000 Hz 
 geometry 800 600 800 600 32
 timings 0 0 0 0 0 0 0
 accel true
 rgba 8/16,8/8,8/0,0/0
endmode  

我们也可以检查 /proc/fb 的输出。由于输出是 0 vboxdrmfbinit.sh 中的 init_hal_gralloc 脚本函数不会加载 uvesafb

(debug-late)@android:/android # cat /proc/fb 
0 vboxdrmfb                                        

使用这种设置,我们可以使用 vboxvideo 驱动程序而不是 uvesafb 来启动 x86vbox。正如我提到的,在我们能够充分利用 vboxvideo 的所有潜在功能之前,还有很多工作要做。

使用 VirtualBox Guest Additions 构建和测试镜像

要构建和测试本章中的镜像,我们可以使用 repo 工具检索本章中的源代码,如下所示:

我们可以使用以下命令从 GitHub 和 AOSP 获取源代码:

$ repo init https://github.com/shugaoye/manifests -b android-7.1.1_r4_ch11_aosp
$ repo sync  

在我们获取本章的源代码后,我们可以设置环境和构建系统,如下所示:

$ source build/envsetup.sh
$ lunch x86vbox-eng
$ make -j4 

要构建 initrd.img,我们可以运行以下命令:

$ make initrd USE_SQUASHFS=0  

摘要

在本章中,我们学习了图形系统的启动过程。这包括 OpenGL ES 库、Gralloc 模块和设备驱动程序。我们在上一章中讨论了 Gralloc 模块。在本章中,我们分析了另外两个组件,即 OpenGL ES 库和帧缓冲区驱动程序。在掌握所有这些知识的基础上,我们将来自 VirtualBox Guest Additions 的驱动程序集成到了 x86vbox 设备中。在下一章中,我们将开始另一个项目,以探索 Android 系统中恢复功能的工作原理。

第十二章:介绍恢复

在这本书中,我们到目前为止已经完成了两个项目。通过第一个 x86emu 项目,我们学习了如何扩展现有设备以支持附加功能。之后,我们通过第二个项目 x86vbox 学习了如何创建新设备。在 Android 的系统级编程中,还有一个重要的话题,那就是如何修补或更新已发布的系统。

在 Android 系统中,修补或更新已发布系统的方法是使用一个名为恢复的工具。在接下来的三个章节中,我们将学习如何在 x86vbox 设备上构建恢复。由于 x86vbox 是为 VirtualBox 构建的,我们将使用 VirtualBox 作为本章的虚拟硬件,第十四章,创建 OTA 更新包。我们还将使用我们构建的恢复准备和测试几个更新包。在本章中,我们将涵盖以下主题:

  • 恢复介绍

  • 分析恢复源代码

  • 为 x86vbox 构建恢复

恢复介绍

在 Android 中,恢复是一个包含内核和专用 ramdisk 的最小 Linux 环境。当这个最小 Linux 环境启动时,它会运行一个二进制工具,即恢复,以进入所谓的恢复模式。恢复模式的 Linux 内核和 ramdisk 通常存储在一个专用的可启动分区中。在恢复模式下,内核和根文件系统都在内存中,因此它可以管理其他分区而无需任何依赖。

在现场更新设备有两种方法。第一种方法是使用引导加载程序中的 fastboot 协议。设备可以通过引导加载程序进行重写。在这种情况下,您可以在 fastboot 模式下启动您的设备,并使用 Android SDK 中的 fastboot 工具来重写您的设备。第二种方法是通过恢复模式来重写设备。如果您将设备启动到恢复模式,您可以使用存储上的映像文件或通过 USB 在 sideload 模式下提供映像来重写设备。

引导加载程序和恢复可以使用的映像文件不同。来自 AOSP 构建输出的映像文件可以直接由引导加载程序使用。我们可以直接使用fastboot工具在引导加载程序中重写system.imguserdata.imgboot.imgrecovery.img映像文件。我们不能使用这些映像文件进行恢复。我们必须使用 AOSP 中提供的工具专门构建恢复映像文件。我们将在下一章中介绍这个主题。

与快启协议相比,恢复模式的关键优势在于支持空中OTA)更新。如果 OTA 服务器上有可用的更新,用户将收到通知。用户可以将更新下载到缓存或数据分区。在更新包使用其签名进行验证后,用户可以响应更新通知。之后,设备将重启进入恢复模式。在恢复模式下,将启动恢复二进制文件,并使用存储在/cache/recovery/command文件中的命令行参数来查找更新包以更新系统镜像。

Android 设备分区

要在设备上启用恢复,我们需要再次查看设备分区。在 Android SDK 中,我们有以下可用于模拟器的镜像文件:

$ ls system-images/android-25/default/x86
build.prop   kernel-ranchu  ramdisk.img        system.img
kernel-qemu  NOTICE.txt     source.properties  userdata.img  

启动模拟器后,我们可以看到以下分区已挂载:

root@x86emu:/ # mount
rootfs / rootfs ro,seclabel,relatime 0 0
tmpfs /dev tmpfs rw,seclabel,nosuid,relatime,mode=755 0 0
devpts /dev/pts devpts rw,seclabel,relatime,mode=600 0 0
proc /proc proc rw,relatime 0 0
sysfs /sys sysfs rw,seclabel,relatime 0 0
selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
debugfs /sys/kernel/debug debugfs rw,seclabel,relatime 0 0
none /acct cgroup rw,relatime,cpuacct 0 0
none /sys/fs/cgroup tmpfs rw,seclabel,relatime,mode=750,gid=1000 0 0
tmpfs /mnt tmpfs rw,seclabel,relatime,mode=755,gid=1000 0 0
none /dev/cpuctl cgroup rw,relatime,cpu 0 0
/dev/block/vda /system ext4 ro,seclabel,relatime,data=ordered 0 0
/dev/block/vdb /cache ext4 rw,seclabel,nosuid,nodev,noatime,errors=panic,data=ordered 0 0
/dev/block/vdc /data ext4 rw,seclabel,nosuid,nodev,noatime,errors=panic,data=ordered 0 0
...  

我们可以看到systemdatacache分区已挂载为 virtio 块设备。由于 virtio 是网络和磁盘设备驱动程序的虚拟化标准,性能应该优于物理设备驱动程序。仅使用这些分区,我们无法创建一个可以使用恢复工具的系统。在下图中,这些是我们需要在存储设备上拥有的最小分区,以支持快启和恢复:

图片

Android 设备分区

  • 引导:这是包含内核和 ramdisk 镜像的分区。

  • 系统:这是包含 Android 系统的分区。它通常作为只读挂载,只能在 OTA 更新期间进行更改。

  • 厂商:这是包含厂商私有系统文件的分区。它与系统分区类似,作为只读挂载,只能在 OTA 更新期间进行更改。

  • 用户数据:此分区包含用户安装的应用程序保存的数据。此分区通常不会在 OTA 更新过程中被触及。

  • 缓存:此分区持有临时数据。OTA 包安装可以使用它作为工作空间。

  • 恢复:此分区包含用于恢复的 Linux 内核和 ramdisk。它与引导分区类似,只是 ramdisk 镜像仅用于恢复模式。

  • 杂项:此分区由恢复用于在不同引导会话之间存储信息。

在本章中,我们将为 x86vbox 设备构建恢复。正如我们从第八章“在 VirtualBox 上创建自己的设备”到第十一章“启用 VirtualBox 特定硬件接口”所学的,我们只为 x86vbox 设备使用一个分区来存储所有内容。我们将在本章前面的解释基础上,稍后扩展 x86vbox 设备以使用多个分区。

分析恢复

在我们开始为我们的 x86vbox 设备构建恢复之前,我们将分析恢复的代码流程以了解其工作原理。从最终用户的角度来看,有两种进入恢复模式的方法。当用户想要执行出厂重置或可用 OTA 更新时,主系统可以在重置系统之前将恢复命令写入引导加载程序控制块BCB)和缓存分区。

进入恢复模式的第二种方法是手动使用键组合。在关闭手机后,同时按下一个键组合以手动进入恢复模式。键组合由设备制造商定义,例如,它可以是音量下键和电源按钮的组合。

在这两种情况下,进入恢复模式与引导加载程序的实现密切相关。Android 系统、恢复和引导加载程序通过两个接口相互通信:分区/cache/misc。我们可以使用以下图表来描述通信接口:

图片

Android 系统、恢复和引导加载程序的接口

在前面的图表中,引导加载程序使用/misc分区中的BCB与 Android 系统和恢复进行通信。Android 系统和恢复使用/cache分区中的信息相互通信。让我们深入了解这两个通信通道的细节。

BCB

BCB 是引导加载程序与主系统和恢复之间的通信接口。

Android 系统在恢复源代码中也被称为主系统。在本章中,我们将主系统一词与 Android 系统视为同义词。

BCB 以原始分区格式存储在/misc分区中,这意味着这个分区就像一个没有文件系统的二进制文件一样被使用。

恢复使用recovery.fstab文件挂载系统中的所有分区。如果我们查看recovery.fstab/misc分区的文件系统类型,它是emmc,这是恢复中使用的原始文件系统之一:

/dev/block/by-name/misc    /misc    emmc    defaults    defaults  

恢复支持五种文件系统类型,包括两种原始文件系统和三种正常文件系统。

支持的两个原始文件系统是:

  • mtd:这是旧版 Android 设备中使用的分区。这些设备使用 NAND 闪存和 MTD 分区。

  • emmc:这是最近 Android 设备中使用的原始 eMMC 块设备。

bootrecoverymisc分区可以是mtdemmc文件系统类型。

支持的正常文件系统类型包括:

  • yaffs2yaffs2文件系统通常用于 MTD 设备的systemuserdatacache分区。这通常用于较老的 Android 设备。

  • ext4:在最新的 Android 设备中,使用 eMMC 块设备。通常在 eMMC 块设备上使用标准的 Linux ext4文件系统。与yaffs2文件系统类型一样,systemuserdatacache分区可以使用ext4格式。

  • vfat:这是用于外部存储(如 SD 卡或 USB)的文件系统类型。

让我们回到 BCB 的话题。在$AOSP/bootable/recovery/bootloader.h文件中,BCB 被定义为以下数据结构:

struct bootloader_message { 
    char command[32]; 
    char status[32]; 
    char recovery[768]; 
    char stage[32]; 
    char reserved[224]; 
}; 

当主系统想要将设备重启到恢复模式时,会使用command字段。这可能是用户从设置中选择出厂重置或可用 OTA 更新时的情况。引导加载程序也可以使用此字段,当引导加载程序完成固件更新时,它可能想要进入恢复模式进行最后的清理。

系统引导加载程序在完成固件更新后更新status字段。

recovery字段由主系统用于向恢复发送消息,或者恢复可能使用此字段向主系统发送消息。

stage字段用于指示更新的阶段。在某些情况下,安装更新包可能需要多次重启。恢复用户界面可以使用此字段来显示安装的当前阶段:

在前面的图中,显示与检查键组合和 BCB 相关的引导加载程序逻辑。只要引导加载程序根据 AOSP 恢复定义处理 BCB,实现可以是供应商特定的。通常,引导加载程序首先检查键组合,以决定用户是否想要进入恢复模式。如果没有按下键组合,它将检查 BCB 以决定引导路径。

缓存分区

缓存分区中有三个文件,可以作为主系统和恢复工具之间的通信通道。这三个文件是:

  • /cache/recovery/command:这是一个从恢复点输入参数的文件。此文件中每行有一个命令。可能在此文件中提供的参数是:

    • -send_intent=anystring:主系统可能使用此命令在恢复退出后向自身发送消息

    • -update_package=path:此命令指定安装 OTA 包文件的路径

    • -wipe_data:此命令指示恢复擦除用户数据(和缓存),然后重启

    • -wipe_cache:此命令指示恢复擦除缓存(但不擦除用户数据),然后重启

    • -set_encrypted_filesystem=on|off:启用/禁用加密文件系统

    • -just_exit:不执行任何操作;退出并重启

  • /cache/recovery/log:恢复的运行时日志文件位于/tmp/recovery.log。在恢复退出之前,它将备份旧日志文件并将当前日志文件移动到/cache/recovery/log

  • /cache/recovery/intent:在恢复退出之前,它将检查是否有任何需要通过此文件发送到主系统的意图。意图可以是主系统通过/cache/recovery/command文件中的-send_intent命令发送给恢复的消息。

恢复的主流程

在我们了解了关于恢复以及与恢复相关的所有背景知识后,让我们看看恢复的主要工作流程。我们将使用以下流程图来探索恢复的工作流程:

  1. 当开始恢复时,它首先将日志文件设置为 /tmp/recovery.log

  2. 之后,它检查 --adbd 选项。如果指定了此选项,它将运行用于侧载的 adb 守护程序。您可以参考 $AOSP/bootable/recovery/adb_install.cpp 中的源代码,了解如何以 adb 守护程序启动恢复。

  3. 它通过调用 get_args 函数从缓存分区和 BCB 中检索和处理参数。

  4. 根据 get_args 从中检索到的命令,它可能会调用 install_package 函数来安装更新,或者调用 wipe_datawipe_cache 函数来擦除用户数据或缓存分区。

  5. 如果没有更新包或擦除数据的命令,它将调用 prompt_and_wait 函数进入恢复用户界面。根据用户输入,它可能会调用 apply_from_adbapply_from_sdcard 从 USB 或 SD 卡更新包。它可能会调用 wipe_datawipe_cache 函数来擦除用户数据或缓存分区等。

  6. 在所有任务完成或用户选择退出恢复后,它将调用清理函数 finish_recovery 来进行最后的清理。之后,它将重新启动或关闭系统:

恢复工作流程

根据前面的流程分析,我们可以查看 $AOSP/bootable/recovery/recovery.cpp 中的 main 函数的代码片段如下:

int 
main(int argc, char **argv) { 
    time_t start = time(NULL); 

    redirect_stdio(TEMPORARY_LOG_FILE); 

    ... 
    if (argc == 2 && strcmp(argv[1], "--adbd") == 0) { 
        adb_main(0, DEFAULT_ADB_PORT); 
        return 0; 
    } 

    printf("Starting recovery (pid %d) on %s", getpid(), 
    ctime(&start)); 

    load_volume_table(); 
    get_args(&argc, &argv); 

    ... 
    ui->Print("Supported API: %d\n", RECOVERY_API_VERSION); 

    int status = INSTALL_SUCCESS; 

    if (update_package != NULL) { 
        status = install_package(update_package, &should_wipe_cache, 
        TEMPORARY_INSTALL_FILE, true); 
        if (status == INSTALL_SUCCESS && should_wipe_cache) { 
            wipe_cache(false, device); 
        } 
    ... 
    } else if (should_wipe_data) { 
        if (!wipe_data(false, device)) { 
            status = INSTALL_ERROR; 
        } 
    } else if (should_wipe_cache) { 
        if (!wipe_cache(false, device)) { 
            status = INSTALL_ERROR; 
        } 
    } else if (sideload) { 
    ... 
    Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : 
    Device::REBOOT; 
    if ((status != INSTALL_SUCCESS && !sideload_auto_reboot) || ui-
    >IsTextVisible()) { 
        Device::BuiltinAction temp = prompt_and_wait(device, status); 
        if (temp != Device::NO_ACTION) { 
            after = temp; 
        } 
    } 

    // Save logs and clean up before rebooting or shutting down. 
    finish_recovery(send_intent); 

    switch (after) { 
        case Device::SHUTDOWN: 
            ui->Print("Shutting down...\n"); 
            property_set(ANDROID_RB_PROPERTY, "shutdown,"); 
            break; 

        case Device::REBOOT_BOOTLOADER: 
            ui->Print("Rebooting to bootloader...\n"); 
            property_set(ANDROID_RB_PROPERTY, "reboot,bootloader"); 
            break; 

        default: 
            ui->Print("Rebooting...\n"); 
            property_set(ANDROID_RB_PROPERTY, "reboot,"); 
            break; 
    } 
    sleep(5); // should reboot before this finishes 
    return EXIT_SUCCESS; 
} 

在我们了解了恢复工作流程的概述后,我们将看看恢复如何在 get_args 函数中从 BCB 或缓存文件中检索参数。之后,我们将从用户的角度查看两个重要的工作流程:工厂重置和 OTA 更新。

从 BCB 和缓存文件中检索参数

如我们在恢复的主要函数中看到的那样,它调用了 get_args 函数从主系统或引导加载程序中检索参数。以下是为 get_args 的流程图。它与恢复的 main 函数位于同一 $AOSP/bootable/recovery/recovery.cpp 文件中。

get_args 流程图

从以下代码片段中,我们可以看到它调用了 get_bootloader_message 函数来获取 BCB 数据结构 boot

static void 
get_args(int *argc, char ***argv) { 
    struct bootloader_message boot; 
    memset(&boot, 0, sizeof(boot)); 
    get_bootloader_message(&boot);  // this may fail, leaving a zeroed 
                                    //structure 
    stage = strndup(boot.stage, sizeof(boot.stage)); 
    ... 

如果没有参数,argc 的值将小于或等于 1。它将尝试从 BCB 中获取参数,如下面的代码片段所示。在 BCB 的 recovery 字段中,命令将以 recovery\n 开始。recovery\n 之后的内容与缓存命令文件格式相同,即 /cache/recovery/command

if (*argc <= 1) { 
    boot.recovery[sizeof(boot.recovery) - 1] = '\0'; 
    const char *arg = strtok(boot.recovery, "\n"); 
    if (arg != NULL && !strcmp(arg, "recovery")) { 
        *argv = (char **) malloc(sizeof(char *) * MAX_ARGS); 
        (*argv)[0] = strdup(arg); 
        for (*argc = 1; *argc < MAX_ARGS; ++*argc) { 
            if ((arg = strtok(NULL, "\n")) == NULL) break; 
            (*argv)[*argc] = strdup(arg); 
        } 
        LOGI("Got arguments from boot message\n"); 
    } else if (boot.recovery[0] != 0 && boot.recovery[0] != 255) { 
        LOGE("Bad boot message\n\"%.20s\"\n", boot.recovery); 
    } 
} 

如果可以从 BCB 中检索到参数,它将跳过缓存命令文件。否则,它将尝试按照以下方式从缓存命令文件中读取参数:

if (*argc <= 1) { 
    FILE *fp = fopen_path(COMMAND_FILE, "r"); 
    if (fp != NULL) { 
        char *token; 
        char *argv0 = (*argv)[0]; 
        *argv = (char **) malloc(sizeof(char *) * MAX_ARGS); 
        (*argv)[0] = argv0;  // use the same program name 

        char buf[MAX_ARG_LENGTH]; 
        for (*argc = 1; *argc < MAX_ARGS; ++*argc) { 
            if (!fgets(buf, sizeof(buf), fp)) break; 
            token = strtok(buf, "\r\n"); 
            if (token != NULL) { 
                (*argv)[*argc] = strdup(token); 
            } else { 
                --*argc; 
            } 
        } 

        check_and_fclose(fp, COMMAND_FILE); 
        LOGI("Got arguments from %s\n", COMMAND_FILE); 
    } 
} 

在处理完 BCB 和缓存命令文件后,它将 BCB 块写入/misc分区,以便在更新或擦除过程中出现任何错误时,重启后相同的进程将继续:

strlcpy(boot.command, "boot-recovery", sizeof(boot.command)); 
strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery)); 
int i; 
for (i = 1; i < *argc; ++i) { 
    strlcat(boot.recovery, (*argv)[i], sizeof(boot.recovery)); 
    strlcat(boot.recovery, "\n", sizeof(boot.recovery)); 
} 
set_bootloader_message(&boot); 

从前面的代码分析中,我们可以看到缓存命令文件只是一个普通的文本文件。它可以通过使用标准的 C 函数来访问。要访问 BCB 数据结构的/misc分区,使用get_bootloader_message函数读取 BCB,并使用set_bootloader_message函数写入 BCB。BCB 数据结构bootloader_messagebootloader.h文件中定义,相关函数在bootloader.cpp文件中实现。

/misc分区是一个原始分区,它被bootloader.cpp中的代码作为一个普通文件而不是文件系统卷来使用。

我们可以快速查看get_bootloader_message函数及其支持函数get_bootloader_message_block,如下所示:

int get_bootloader_message(struct bootloader_message *out) { 
    Volume* v = volume_for_path("/misc"); 
    if (v == NULL) { 
      LOGE("Cannot load volume /misc!\n"); 
      return -1; 
    } 
    if (strcmp(v->fs_type, "mtd") == 0) { 
        return get_bootloader_message_mtd(out, v); 
    } else if (strcmp(v->fs_type, "emmc") == 0) { 
        return get_bootloader_message_block(out, v); 
    } 
    LOGE("unknown misc partition fs_type \"%s\"\n", v->fs_type); 
    return -1; 
} 

get_bootloader_message函数中,它将根据分区类型调用另一个函数,/misc。正如我们所见,支持的原始文件系统类型是mtdemmc。我们可以查看emmc版本的get_bootloader_message_block,如下所示:

static int get_bootloader_message_block(struct bootloader_message *out, 
const Volume* v) { 
    wait_for_device(v->blk_device); 
    FILE* f = fopen(v->blk_device, "rb"); 
    if (f == NULL) { 
        LOGE("Can't open %s\n(%s)\n", v->blk_device, strerror(errno)); 
        return -1; 
    } 
    struct bootloader_message temp; 
    int count = fread(&temp, sizeof(temp), 1, f); 
    if (count != 1) { 
        LOGE("Failed reading %s\n(%s)\n", v->blk_device, 
        strerror(errno)); 
        return -1; 
    } 
    if (fclose(f) != 0) { 
        LOGE("Failed closing %s\n(%s)\n", v->blk_device, 
        strerror(errno)); 
        return -1; 
    } 
    memcpy(out, &temp, sizeof(temp)); 
    return 0; 
} 

正如我们所见,在get_bootloader_message_block函数中,它使用 C 函数fopenfreadfclose/misc分区作为普通文件访问。

现在我们已经完成了 BCB 和缓存文件处理的分析。在接下来的两节中,我们将探讨恢复过程中最重要的两个工作流程:

  • 工厂数据重置

  • OTA 更新

工厂数据重置

恢复的主要功能之一是支持工厂数据重置。通常,用户可以从设备上的设置中选择工厂数据重置,如下面的截图所示:

图片

工厂数据重置

整个过程可以分为以下步骤:

  1. 用户从设置中选择工厂数据重置。

  2. 主系统将--wipe_data写入/cache/recovery/command

  3. 主系统重新启动设备进入恢复模式。我们已经在上一节讨论 BCB 时对此进行了分析。

  4. 恢复从 BCB 或/cache/recovery/command中的get_args()检索参数。在读取参数后,恢复将使用boot-recovery--wipe_data写入 BCB。

  5. 恢复擦除/data/cache分区。在此之后,任何后续的重启将继续此步骤,直到擦除完成或用户从恢复用户界面采取其他操作退出恢复。

  6. 在擦除/data/cache分区后,恢复调用finish_recovery函数来擦除 BCB。

  7. 恢复将设备重新启动到主系统。

我们已经分析了前面的大多数步骤,除了finish_recovery。让我们看看finish_recovery函数:

static void 
finish_recovery(const char *send_intent) { 
    // By this point, we're ready to return to the main system... 
    if (send_intent != NULL) { 
        FILE *fp = fopen_path(INTENT_FILE, "w"); 
        if (fp == NULL) { 
            LOGE("Can't open %s\n", INTENT_FILE); 
        } else { 
            fputs(send_intent, fp); 
            check_and_fclose(fp, INTENT_FILE); 
        } 
    } 

    if (locale != NULL) { 
        LOGI("Saving locale \"%s\"\n", locale); 
        FILE* fp = fopen_path(LOCALE_FILE, "w"); 
        fwrite(locale, 1, strlen(locale), fp); 
        fflush(fp); 
        fsync(fileno(fp)); 
        check_and_fclose(fp, LOCALE_FILE); 
    } 

    copy_logs(); 

    struct bootloader_message boot; 
    memset(&boot, 0, sizeof(boot)); 
    set_bootloader_message(&boot); 

    if (ensure_path_mounted(COMMAND_FILE) != 0 || 
        (unlink(COMMAND_FILE) && errno != ENOENT)) { 
        LOGW("Can't unlink %s\n", COMMAND_FILE); 
    } 

    ensure_path_unmounted(CACHE_ROOT); 
    sync();  // For good measure. 
} 

finish_recovery函数中,它将意图写入/cache/recovery/intent。然后,它处理本地文件并创建日志文件备份。最后,通过调用set_bootloader_message擦除 BCB,并删除/cache/recovery/command以恢复正常的启动过程。

OTA 更新

OTA 更新是恢复的另一个主要功能。在手动进入恢复模式后,可以使用恢复用户界面更新 OTA 包。在接收到更新通知后也可以自动更新。在这两种情况下,更新包的路径可能不同,但安装过程是相同的。在本节中,我们将查看设备接收到 OTA 更新通知后的流程。然后,我们将探讨安装过程的细节:

  1. 设备接收到 OTA 更新通知后,主系统将 OTA 包下载到/cache/update.zip

  2. 主系统将--update_package=/cache/update.zip命令写入/cache/recovery/command

  3. 主系统将设备重新启动到恢复模式。

  4. 恢复过程在get_args()中从 BCB 或/cache/recovery/command检索参数。读取参数后,恢复过程将使用boot-recoveryupdate_package=...写入 BCB。

  5. 恢复过程调用install_package来安装更新。在此步骤中,任何后续的重启将继续此步骤,直到安装完成。

  6. 如果安装失败,将调用prompt_and_wait函数来显示错误并等待用户操作。如果安装成功完成,它将进入下一步。

  7. 恢复过程调用finish_recovery函数来擦除 BCB 并删除/cache/recovery/command文件。

  8. 恢复过程将设备重新启动到主系统。

一旦更新包下载完成,安装将由install_package函数完成:

int 
install_package(const char* path, bool* wipe_cache, const char* install_file, bool needs_mount) 
{ 
    modified_flash = true; 

    FILE* install_log = fopen_path(install_file, "w"); 
    if (install_log) { 
        fputs(path, install_log); 
        fputc('\n', install_log); 
    } else { 
        LOGE("failed to open last_install: %s\n", strerror(errno)); 
    } 
    int result; 
    if (setup_install_mounts() != 0) { 
        LOGE("failed to set up expected mounts for install; 
        aborting\n"); 
        result = INSTALL_ERROR; 
    } else { 
        result = really_install_package(path, wipe_cache, needs_mount); 
    } 
    if (install_log) { 
        fputc(result == INSTALL_SUCCESS ? '1' : '0', install_log); 
        fputc('\n', install_log); 
        fclose(install_log); 
    } 
    return result; 
} 

install_package函数中,它首先设置安装日志文件。日志文件路径是/tmp/last_install。然后,它调用setup_install_mounts来挂载相关分区。实际的安装是在really_install_package函数中完成的,如下面的代码片段所示:

static int 
really_install_package(const char *path, bool* wipe_cache, bool needs_mount) 
{ 
    ui->SetBackground(RecoveryUI::INSTALLING_UPDATE); 
    ... 

    MemMapping map; 
    if (sysMapFile(path, &map) != 0) { 
        LOGE("failed to map file\n"); 
        return INSTALL_CORRUPT; 
    } 

    int numKeys; 
    Certificate* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys); 
    if (loadedKeys == NULL) { 
        LOGE("Failed to load keys\n"); 
        return INSTALL_CORRUPT; 
    } 
    LOGI("%d key(s) loaded from %s\n", numKeys, PUBLIC_KEYS_FILE); 

    ui->Print("Verifying update package...\n"); 

    int err; 
    err = verify_file(map.addr, map.length, loadedKeys, numKeys); 
    free(loadedKeys); 
    LOGI("verify_file returned %d\n", err); 
    if (err != VERIFY_SUCCESS) { 
        LOGE("signature verification failed\n"); 
        sysReleaseMap(&map); 
        return INSTALL_CORRUPT; 
    } 

    /* Try to open the package. 
     */ 
    ZipArchive zip; 
    err = mzOpenZipArchive(map.addr, map.length, &zip); 
    if (err != 0) { 
        LOGE("Can't open %s\n(%s)\n", path, err != -1 ? strerror(err) : 
        "bad"); 
        sysReleaseMap(&map); 
        return INSTALL_CORRUPT; 
    } 

    /* Verify and install the contents of the package. 
     */ 
    ui->Print("Installing update...\n"); 
    ui->SetEnableReboot(false); 
    int result = try_update_binary(path, &zip, wipe_cache); 
    ui->SetEnableReboot(true); 
    ui->Print("\n"); 

    sysReleaseMap(&map); 

    return result; 
} 

really_install_package函数中,它初始化用户界面并在屏幕上显示包位置。然后,它为更新包创建内存映射,这是zip函数所需的。之后,它使用其签名验证更新包。最后,它调用另一个函数try_update_binary,来完成安装。

try_update_binary函数执行三个任务:

  1. 从更新包中提取update_binary

  2. 准备环境以执行update_binary

  3. 监控安装进度。

让我们详细了解这三个任务:

static int 
try_update_binary(const char* path, ZipArchive* zip, bool* wipe_cache) { 
    const ZipEntry* binary_entry = 
            mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME); 
    if (binary_entry == NULL) { 
        mzCloseZipArchive(zip); 
        return INSTALL_CORRUPT; 
    } 

    const char* binary = "/tmp/update_binary"; 
    unlink(binary); 
    int fd = creat(binary, 0755); 
    if (fd < 0) { 
        mzCloseZipArchive(zip); 
        LOGE("Can't make %s\n", binary); 
        return INSTALL_ERROR; 
    } 
    bool ok = mzExtractZipEntryToFile(zip, binary_entry, fd); 
    close(fd); 
    mzCloseZipArchive(zip); 

    if (!ok) { 
        LOGE("Can't copy %s\n", ASSUMED_UPDATE_BINARY_NAME); 
        return INSTALL_ERROR; 
    } 

它尝试从更新包中提取update_binaryupdate_binary在更新包中的路径在META-INF/com/google/android/update-binary中预定义。

如果update_binary可以成功提取,它将被复制到/tmp/update_binary

int pipefd[2]; 
pipe(pipefd); 
const char** args = (const char**)malloc(sizeof(char*) * 5); 
args[0] = binary; 
args[1] = EXPAND(RECOVERY_API_VERSION);   // defined in Android.mk 
char* temp = (char*)malloc(10); 
sprintf(temp, "%d", pipefd[1]); 
args[2] = temp; 
args[3] = (char*)path; 
args[4] = NULL; 

pid_t pid = fork(); 
if (pid == 0) { 
    umask(022); 
    close(pipefd[0]); 
    execv(binary, (char* const*)args); 
    fprintf(stdout, "E:Can't run %s (%s)\n", binary, strerror(errno)); 
    _exit(-1); 
} 

如前述代码片段所示,在提取update_binary之后,它将准备环境以执行update_binary。更新包的安装实际上是通过脚本由update_binary完成的。以下参数被传递给update_binary以执行:

  • update_binary的路径

  • 恢复版本

  • 父进程和子进程之间的管道用于通信

  • 更新包的路径

在环境准备就绪后,它将派生一个子进程来运行update_binary。父进程将通过管道与子进程通信来监控安装进度:

    close(pipefd[1]); 

    *wipe_cache = false; 

    char buffer[1024]; 
    FILE* from_child = fdopen(pipefd[0], "r"); 
    while (fgets(buffer, sizeof(buffer), from_child) != NULL) { 
        char* command = strtok(buffer, " \n"); 
        if (command == NULL) { 
            continue; 
        } else if (strcmp(command, "progress") == 0) { 
            char* fraction_s = strtok(NULL, " \n"); 
            char* seconds_s = strtok(NULL, " \n"); 

            float fraction = strtof(fraction_s, NULL); 
            int seconds = strtol(seconds_s, NULL, 10); 

            ui->ShowProgress(fraction * (1-VERIFICATION_PROGRESS_FRACTION), 
            seconds); 
        } else if (strcmp(command, "set_progress") == 0) { 
            char* fraction_s = strtok(NULL, " \n"); 
            float fraction = strtof(fraction_s, NULL); 
            ui->SetProgress(fraction); 
        } else if (strcmp(command, "ui_print") == 0) { 
            char* str = strtok(NULL, "\n"); 
            if (str) { 
                ui->Print("%s", str); 
            } else { 
                ui->Print("\n"); 
            } 
            fflush(stdout); 
        } else if (strcmp(command, "wipe_cache") == 0) { 
            *wipe_cache = true; 
        } else if (strcmp(command, "clear_display") == 0) { 
            ui->SetBackground(RecoveryUI::NONE); 
        } else if (strcmp(command, "enable_reboot") == 0) { 
            ui->SetEnableReboot(true); 
        } else { 
            LOGE("unknown command [%s]\n", command); 
       } 
    } 
    fclose(from_child); 

    int status; 
    waitpid(pid, &status, 0); 
    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { 
       LOGE("Error in %s\n(Status %d)\n", path, WEXITSTATUS(status)); 
       return INSTALL_ERROR; 
    } 

如前述代码片段所示,父进程将从子进程接收命令以显示进度,打印信息到屏幕,或在安装后设置清理配置。

为 x86vbox 构建恢复

在分析恢复源代码的工作流程和关键元素后,我们现在可以开始为我们的 x86vbox 设备构建它。

支持恢复构建的更改包括对 x86vbox 设备的更改以及对recoverynewinstaller的更改。

构建配置

在我们查看本章的更改之前,让我们先看看配置文件。像往常一样,我们为每个章节都有一个清单文件。我们根据第十一章的源代码,启用 VirtualBox 特定的硬件接口进行本章的更改。以下是我们将要更改的项目:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="github" 
           revision="refs/tags/android-7.1.1_r4_x86vbox_ch12_r1" 
           fetch="." /> 

  <remote  name="aosp" 
           fetch="https://android.googlesource.com/" /> 
  <default revision="refs/tags/android-7.1.1_r4" 
           remote="aosp" 
           sync-c="true" 
           sync-j="1" /> 

  ... 
  <project path="bootable/newinstaller" 
  name="platform_bootable_newinstaller" remote="github" /> 
  <project path="device/generic/common" name="device_generic_common" 
  groups="pdk" remote="github" /> 
  <project path="device/generic/x86vbox" name="x86vbox" remote="github" 
  /> 
  <project path="bootable/recovery" name="android_bootable_recovery" 
  remote="github" groups="pdk" /> 
  ... 

我们可以看到我们需要更改四个项目:recoverynewinstallercommonx86vbox。我们使用android-7.1.1_r4_x86vbox_ch12_r1标签作为本章源代码的基线。

我们可以使用以下命令从 GitHub 和 AOSP 获取源代码:

$ repo init -u https://github.com/shugaoye/manifests -b android-7.1.1_r4_ch12_aosp
$ repo sync 

在我们获取本章的源代码后,我们可以设置环境并按如下方式构建系统:

$ source build/envsetup.sh
$ lunch x86vbox-eng
$ make -j4

要构建initrd.img,您可以运行以下命令:

$ make initrd USE_SQUASHFS=0 

x86vbox 的更改

对于 x86vbox 设备,我们首先需要更改 Makefiles 设备。由于我们从通用的 Android-x86 设备继承了 x86vbox,所以我们只有以下 Makefiles:

$ ls *.mk
AndroidProducts.mk  BoardConfig.mk  x86vbox.mk  

AndroidProducts.mk是 Android 构建系统的入口,它包括我们的x86vbox.mk Makefile。在x86vbox.mk中,我们添加以下与恢复相关的文件:

PRODUCT_COPY_FILES += \ 
... 
device/generic/x86vbox/recovery.fstab:recovery/root/etc/recovery.fstab \    device/generic/x86vbox/recovery/root/init.recovery.x86vbox.rc:root/init.recovery.x86vbox.rc \    device/generic/x86vbox/recovery/root/sbin/network_start.sh:recovery/root/sbin/network_start.sh \    device/generic/x86vbox/recovery/root/sbin/create_partitions.sh:recovery/root/sbin/create_partitions.sh \ 
... 

这些更改包括两部分。第一部分与针对 VirtualBox 的特定环境设置相关,因为我们是在 VirtualBox 的虚拟硬件上运行恢复。x86vbox 特定的初始化脚本init.recovery.x86vbox.rc将在系统启动时由 init 进程执行。

第二部分与存储设备的分区有关。正如我们在前面的章节中讨论的,我们不能像在第八章,在 VirtualBox 上创建自己的设备,到第十一章,启用 VirtualBox 特定的硬件接口中那样,使用单个分区进行恢复。分区表定义在 recovery.fstab 文件中。让我们首先看看启动脚本,init.recovery.x86vbox.rc

on init 
    exec -- /system/bin/logwrapper /system/bin/sh /system/etc/init.sh 

service network_start /sbin/network_start.sh 
    user root 
    seclabel u:r:recovery:s0 
    oneshot 

service console /system/bin/sh 
    class core 
    console 
    disabled 
    user shell 
    group shell log 
    seclabel u:r:shell:s0 

on property:ro.debuggable=1 
    start console 

作为 Android 的 init 脚本,恢复也有一个针对特定设备的 init 脚本,init.recovery.${ro.hardware}.rc。在我们的例子中,它是 init.recovery.x86vbox.rc。在 init.recovery.x86vbox.rc 中,它调用 Android-x86 HAL 初始化脚本,/system/etc/init.sh。在第八章,在 VirtualBox 上创建自己的设备中的 Android 启动部分的 HAL 初始化过程中,我们详细解释了 /system/etc/init.sh 脚本。

我们在 init.recovery.x86vbox.rc 中添加了两个服务,network_startconsole。有了这两个服务,我们能够启用 VirtualBox 特定的网络接口,并且在启动后也可以拥有一个控制台。有了这个调试控制台,我们可以在本书的后续部分更容易地调试恢复过程。

x86vbox.mk 中的另一个重要部分是我们为恢复添加了一个 recovery.fstab 分区表,如下所示:

/dev/block/sda1 /system  ext4  ro          wait 
/dev/block/sda2 /data    ext4  noatime,... wait 
/dev/block/sda3 /sdcard  vfat  defaults    voldmanaged=sdcard:auto 
/dev/block/sda5 /cache   ext4  noatime,... wait 
/dev/block/sda6 /misc    emmc  defaults    defaults 
/dev/block/sda7 /recovery emmc defaults    defaults 

如我们所见,我们现在有六个分区。我们实际上并没有一个支持 fastboot 协议和恢复 BCB 的引导加载程序,所以我们实际上并不使用 /boot/recovery 分区。然而,我们确实有一个来自 Android-x86 的两阶段引导过程,并且我们可以有一个无需引导加载程序支持的解决方案。我们将在本章稍后查看对 newinstaller 的更改时看到这一点。

recovery.fstab 分区表由恢复使用,我们需要更改 Android 主系统的相关分区表,即 device/generic/common/fstab.x86 中的文件。

我们需要在 device/generic/common/fstab.x86 中添加两个条目,如下所示:

/dev/block/sda3  /sdcard  vfat  defaults  voldmanaged=sdcard:auto 
/dev/block/sda5  /cache   ext4  noatime,... wait 

这个 fstab.x86 文件将在构建过程中被复制到系统镜像中,作为 fstab.x86vbox。init 进程将处理它以挂载分区。你可能想知道为什么分区表中没有 /system/data。我们使用两阶段引导,它们在 Android 启动之前的第一阶段引导中挂载。/system/data 的来源可以通过内核参数进行配置,正如我们在前面章节中解释两阶段引导过程时讨论的那样。

请注意,恢复和主系统应该挂载相同的块设备分区。例如,如果恢复和主系统为 /cache 挂载不同的分区,它们将无法通过 /cache/recovery/command 中的命令文件相互通信。

这就是x86vbox.mk更改的全部内容,现在让我们看看另一个 Makefile,BoardConfig.mk。为了启用恢复的构建,我们需要在BoardConfig.mk中添加以下两个宏:

TARGET_NO_KERNEL := false 
TARGET_NO_RECOVERY := false 

这两个宏的默认值都设置为 true,这意味着内核和恢复在默认配置中都没有内置。

我们添加了另一个与恢复源代码更改相关的宏,我们将在稍后查看源代码更改:

# Double buffer cannot work well on virtualbox 
RECOVERY_GRAPHICS_FORCE_SINGLE_BUFFER := true 

RECOVERY_GRAPHICS_FORCE_SINGLE_BUFFER宏是从Team Win Recovery ProjectTWRP)的最新代码中借用的。随着 x86vbox Makefiles 的更改,我们实际上可以构建 TWRP。这是一个许多第三方 ROM(如 LineageOS/CyanogenMod、Omnirom 等)常用的第三方恢复工具。

恢复更改

AOSP 恢复代码在 VirtualBox 上可以很好地工作。只有一个与显示相关的问题。为了修复显示问题,我们需要更改恢复源代码中的两个文件。

我们使用前面提到的RECOVERY_GRAPHICS_FORCE_SINGLE_BUFFER宏来配置帧缓冲区更改。我们首先需要将其添加到恢复 Makefile minui/Android.mk中,如下所示:

ifeq ($(RECOVERY_GRAPHICS_FORCE_SINGLE_BUFFER), true) 
LOCAL_CFLAGS += -DRECOVERY_GRAPHICS_FORCE_SINGLE_BUFFER 
endif 

由于双缓冲在 VirtualBox 上目前无法很好地工作,我们必须将其禁用如下:

... 
    /* check if we can use double buffering */ 
#ifndef RECOVERY_GRAPHICS_FORCE_SINGLE_BUFFER 
    if (vi.yres * fi.line_length * 2 <= fi.smem_len) { 
        double_buffered = true; 

        memcpy(gr_framebuffer+1, gr_framebuffer, sizeof(GRSurface)); 
        gr_framebuffer[1].data = gr_framebuffer[0].data + 
            gr_framebuffer[0].height * gr_framebuffer[0].row_bytes; 

        gr_draw = gr_framebuffer+1; 

    } else { 
#else 
    { 
        printf("RECOVERY_GRAPHICS_FORCE_SINGLE_BUFFER := true\n"); 
#endif 
        double_buffered = false; 

        gr_draw = (GRSurface*) malloc(sizeof(GRSurface)); 
        memcpy(gr_draw, gr_framebuffer, sizeof(GRSurface)); 
        gr_draw->data = (unsigned char*) malloc(gr_draw->height * 
        gr_draw->row_bytes); 
        if (!gr_draw->data) { 
            perror("failed to allocate in-memory surface"); 
            return NULL; 
        } 
    }  
... 

通过对 TWRP 的类似更改,TWRP 也可以为 x86vbox 构建。构建 TWRP 的分支包含在 GitHub 的源代码中,您可以自己尝试。

新安装器更改

如我们在 BCB 部分所讨论的,引导加载程序根据 BCB 中存储的参数来决定引导路径。BCB 中存储的恢复命令与/cache分区中的/cache/recovery/command相同。实际上,我们可以将相同的逻辑移动到initrd.img的第一阶段引导中。在这种情况下,我们可以借助第一阶段引导实现相同的结果。工厂数据重置和 OTA 更新的逻辑将变为以下步骤:

  1. 用户可以选择工厂数据重置或可用的 OTA 更新。

  2. 主系统将命令--wipe_data--update_package=/cache/update.zip写入/cache/recovery/command

  3. 主系统重新启动设备。

  4. 在第一阶段引导中,init 脚本将检查/cache分区中是否存在/cache/recovery/command文件。

  5. 如果/cache/recovery/command存在,它将加载ramdisk-recovery.img,否则,它将加载ramdisk.img

  6. 其余步骤将与正常引导过程或恢复引导过程相同。

为了实现前面的逻辑,我们在$AOSP/bootable/newinstaller/initrd/init文件中添加了一个 shell 函数find_ramdisk,如下所示:

find_ramdisk() 
{ 
   busybox mount /dev/sda5 /hd 
   if [ ! -e /hd/recovery/command ]; then 
         busybox umount /hd 
         if [ "$RECOVERY" = "1" ]; then 
               RAMDISK=/mnt/$SRC/ramdisk-recovery.img 
         else 
               RAMDISK=/mnt/$SRC/ramdisk.img 
         fi 
   else 
         busybox umount /hd 
         RAMDISK=/mnt/$SRC/ramdisk-recovery.img 
         return 
   fi 
   echo boot using $RAMDISK ... 
} 

在这个函数中,我们将缓存分区挂载到/hd,并检查/hd/recovery/command是否存在。如果存在,我们将RAMDISK变量设置为ramdisk-recovery.img;否则,我们将其设置为ramdisk.img。init 脚本将在稍后提取RAMDISK变量中包含的 ramdisk,如下所示:

... 
   zcat $RAMDISK | cpio -id > /dev/null  
... 

还有一个名为RECOVERY的变量,它在find_ramdisk中定义,可以从内核命令行传递给 init 脚本。使用这个变量,我们可以强制引导到恢复模式。

测试恢复

在我们构建了恢复和 AOSP 镜像之后,我们可以在 VirtualBox 中测试它们。正如我们在第九章中学到的,使用 PXE/NFS 启动 x86vbox,我们可以使用 PXE 引导系统,并使用 NFS 访问 AOSP 镜像。为了测试恢复,我们可以在$HOME/.VirtualBox/TFTP/pxelinux.cfg/default文件中添加一个选项,使用kernelramdisk/recovery.img引导。尽管我们现在可以引导系统到恢复模式,但我们无法使用本章中的恢复来更新系统。我们将在接下来的两章中了解更多信息。

摘要

我们已经完成了对 x86vbox 设备的恢复分析和实现。在本章的第一部分,我们分析了恢复源代码中的工作流程和关键元素。在本章的第二部分,我们将第一部分中获得的知识应用于 x86vbox 设备的恢复实现。我们修改了 x86vbox 设备本身以添加恢复支持。我们还修改了恢复源代码以修复显示问题。最后,我们修改了 newinstaller,以便我们可以在主系统和恢复系统中都有完整的引导流程。

在下一章中,我们将讨论如何创建恢复包,并解释恢复包中包含的内容。

第十三章:创建 OTA 包

在上一章中,我们分析了恢复的内部结构,并学习了它是如何工作的。正如我们所见,恢复的主要功能之一是支持 OTA 更新。在本章中,我们将研究 OTA 包,并研究 OTA 包更新的过程。我们将涵盖以下主题:

  • 我们将查看 OTA 包内部的内容。我们将研究updaterupdater-script的内部结构。

  • 我们将学习如何构建 OTA 包的过程。

  • 最后,我们需要改进恢复以从 Android 系统中移除依赖。

OTA 包内部有什么

在我们开始构建 OTA 包之前,让我们看看 OTA 包内部的内容。OTA 包可以用来将系统更新到新版本。新版本可以是主要版本或次要版本。例如,它可能是对现有 Android 版本的小幅更新,以修复关键问题或安全漏洞。它也可能是从 Android 6 到 Android 7 的主要更新。让我们看看本章将要创建的 OTA 包的内容,以了解 OTA 包内部有什么。本章将要创建的 OTA 包是我们整个 ROM 的 OTA 更新包。我们可以使用恢复来将 OTA 包刷入我们的 VirtualBox 设备。这是将我们构建的系统镜像安装到虚拟设备上的另一种方法。

让我们看看本章将要构建的 OTA 包的内容。OTA 包本身是一个 ZIP 文件。在我们解压 ZIP 文件后,我们可以列出 ZIP 文件的内容如下:

$ ls -F
boot.img*  file_contexts*  META-INF/  recovery/  system/  

我们可以看到它包括两个文件和三个文件夹。在我们使用恢复刷写这个更新包后,它将更新/boot分区和/system分区:

  • boot.img: 这是/boot分区的镜像,其中包含内核和 ramdisk。

  • file_contexts: 此文件用于根据 SELinux 策略为文件分配标签。SELinux 在最新的 Android 系统中默认启用。在恢复更新系统分区后,它必须使用此文件应用标签。

  • META-INF: 这个文件夹包含 OTA 包、更新程序和更新脚本的签名。我们将在稍后查看这个文件夹的详细信息。

  • recovery: 这个文件夹包含一个install-recovery.shshell 脚本和一个recovery-from-boot.p补丁文件。

  • system: 这是恢复将要更新到/system分区的system文件夹。

OTA 包通常用于更新/boot/system分区。它不会更新自身。/recovery分区的更新在正常的启动过程中进行。在启动过程中,init 将通过以下flash_recovery服务在init.rc脚本中执行install-recovery.sh

service flash_recovery /system/bin/install-recovery.sh 
    class main 
    oneshot 

install-recovery.sh脚本使用recovery-from-boot.p补丁文件安装恢复,如下所示:

#!/system/bin/sh 
if ! applypatch -c EMMC:/dev/block/sda7:7757824:853301871de495db2b8c93f7a37779b9eeccb169; then 
  applypatch -b /system/etc/recovery-resource.dat EMMC:/dev/block/sda8:6877184:2f58cc1a4035176c8fefc19be70c00e625acc16b EMMC:/dev/block/sda7 853301871de495db2b8c93f7a37779b9eeccb169 7757824 2f58cc1a4035176c8fefc19be70c00e625acc16b:/system/recovery-from-boot.p && log -t recovery "Installing new recovery image: succeeded" || log -t recovery "Installing new recovery image: failed" 
else 
  log -t recovery "Recovery image already installed" 
fi 

在我们的环境设置中,/recovery分区位于/dev/block/sda7分区。此脚本将检查/dev/block/sha7分区的sha1哈希值。如果sha1哈希值不同,它将更新/recovery分区。

现在,让我们看看下面的截图所示的META-INF文件夹:

如我们所见,更新包、更新器和更新脚本的签名包含在META-INF文件夹中。在恢复应用更新之前,它将验证META-INF文件夹中的包签名与/system/etc/security/otacerts.zip中的受信任证书。

更新器位于META-INF/com/google/android/update-binary的可执行文件。它解释META-INF/com/google/android/updater-script文件中的脚本。该脚本是用一种可扩展的脚本语言(edify)编写的,支持典型更新相关任务的命令。

由于更新器和更新脚本是在 OTA 包中支持 OTA 更新的关键组件,我们将深入了解它们的细节。

更新器

更新器是 AOSP 源树中针对目标设备的单个可执行文件。它可以在$AOSP/bootable/recovery/updater文件夹中找到。让我们看看updater.cpp文件中的主函数。由于main函数比较长,我们将分几个段落来看:

#include <stdio.h> 
#include <unistd.h> 
#include <stdlib.h> 
#include <string.h> 

#include "edify/expr.h" 
#include "updater.h" 
#include "install.h" 
#include "blockimg.h" 
#include "minzip/Zip.h" 
#include "minzip/SysUtil.h" 

#include "register.inc" 

#define SCRIPT_NAME "META-INF/com/google/android/updater-script" 

extern bool have_eio_error; 

struct selabel_handle *sehandle; 

int main(int argc, char** argv) { 
    setbuf(stdout, NULL); 
    setbuf(stderr, NULL); 

    if (argc != 4 && argc != 5) { 
        printf("unexpected number of arguments (%d)\n", argc); 
        return 1; 
    } 

    char* version = argv[1]; 
    if ((version[0] != '1' && version[0] != '2' && version[0] != '3')  
    || 
        version[1] != '\0') { 
        // We support version 1, 2, or 3\. 
        printf("wrong updater binary API; expected 1, 2, or 3; " 
                        "got %s\n", 
                argv[1]); 
        return 2; 
    } 

更新器有四个参数。它首先会检查是否传入了四个参数。从代码中我们可以看到,这四个参数是:

  • 第一个参数是可执行文件名,在这里是update-binary

  • 第二个参数是更新器版本

  • 第三个参数是可以用来与恢复进行通信的管道

  • 第四个参数是 OTA 包的路径

在继续之前,它将检查更新器版本:

// Set up the pipe for sending commands back to the parent process. 

int fd = atoi(argv[2]); 
FILE* cmd_pipe = fdopen(fd, "wb"); 
setlinebuf(cmd_pipe); 

// Extract the script from the package. 

const char* package_filename = argv[3]; 
MemMapping map; 
if (sysMapFile(package_filename, &map) != 0) { 
    printf("failed to map package %s\n", argv[3]); 
    return 3; 
} 
ZipArchive za; 
int err; 
err = mzOpenZipArchive(map.addr, map.length, &za); 
if (err != 0) { 
    printf("failed to open package %s: %s\n", 
           argv[3], strerror(err)); 
    return 3; 
} 
ota_io_init(&za); 

const ZipEntry* script_entry = mzFindZipEntry(&za, SCRIPT_NAME); 
if (script_entry == NULL) { 
    printf("failed to find %s in %s\n", SCRIPT_NAME, package_filename); 
    return 4; 
} 

char* script = reinterpret_cast<char*>(malloc(script_entry->uncompLen+1)); 
if (!mzReadZipEntry(&za, script_entry, script, script_entry->uncompLen)) { 
    printf("failed to read script from package\n"); 
    return 5; 
} 
script[script_entry->uncompLen] = '\0';     

下一步是打开管道以建立与恢复的通信通道。然后它从 OTA 包中提取updater-script以准备执行脚本:

// Configure edify's functions. 

RegisterBuiltins(); 
RegisterInstallFunctions(); 
RegisterBlockImageFunctions(); 
RegisterDeviceExtensions(); 
FinishRegistration(); 

// Parse the script. 

Expr* root; 
int error_count = 0; 
int error = parse_string(script, &root, &error_count); 
if (error != 0 || error_count > 0) { 
    printf("%d parse errors\n", error_count); 
    return 6; 
} 

struct selinux_opt seopts[] = { 
  { SELABEL_OPT_PATH, "/file_contexts" } 
}; 

sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1); 

if (!sehandle) { 
    fprintf(cmd_pipe, "ui_print Warning: No file_contexts\n"); 
} 

// Evaluate the parsed script. 

UpdaterInfo updater_info; 
updater_info.cmd_pipe = cmd_pipe; 
updater_info.package_zip = &za; 
updater_info.version = atoi(version); 
updater_info.package_zip_addr = map.addr; 
updater_info.package_zip_len = map.length; 

State state; 
state.cookie = &updater_info; 
state.script = script; 
state.errmsg = NULL; 

if (argc == 5) { 
    if (strcmp(argv[4], "retry") == 0) { 
        state.is_retry = true; 
    } else { 
        printf("unexpected argument: %s", argv[4]); 
    } 
} 

char* result = Evaluate(&state, root); 

if (have_eio_error) { 
    fprintf(cmd_pipe, "retry_update\n"); 
} 

if (result == NULL) { 
    if (state.errmsg == NULL) { 
        printf("script aborted (no error message)\n"); 
        fprintf(cmd_pipe, "ui_print script aborted (no error 
        message)\n"); 
    } else { 
        printf("script aborted: %s\n", state.errmsg); 
        char* line = strtok(state.errmsg, "\n"); 
        while (line) { 
            if (*line == 'E') { 
              if (sscanf(line, "E%u: ", &state.error_code) != 1) { 
               printf("Failed to parse error code: [%s]\n", line); 
               } 
            } 
            fprintf(cmd_pipe, "ui_print %s\n", line); 
            line = strtok(NULL, "\n"); 
        } 
        fprintf(cmd_pipe, "ui_print\n"); 
    } 

    if (state.error_code != kNoError) { 
        fprintf(cmd_pipe, "log error: %d\n", state.error_code); 
        if (state.cause_code != kNoCause) { 
            fprintf(cmd_pipe, "log cause: %d\n", state.cause_code); 
        } 
    } 

    free(state.errmsg); 
    return 7; 
} else { 
    fprintf(cmd_pipe, "ui_print script succeeded: result was [%s]\n", 
    result); 
    free(result); 
} 

if (updater_info.package_zip) { 
    mzCloseZipArchive(updater_info.package_zip); 
} 
sysReleaseMap(&map); 
free(script); 

return 0; 
} 

在开始执行更新脚本之前,它需要注册函数以解释更新脚本内部的 edify 语言。从前面的代码中我们可以看到,这些函数包括以下四个类别:

  • 内置函数以支持 edify 语言语法。这些函数在bootable/recovery/edify/expr.cpp中实现。

  • 与包安装相关的函数。这些函数在bootable/recovery/updater/install.cpp中实现。

  • 处理基于块 OTA 包的函数。在 Android 4.4 及更早版本中,使用基于文件的 OTA 更新。在 Android 5.0 及以后版本中,使用基于块的 OTA 更新。有关文件与块 OTA 的比较,请参阅以下 URL:

    source.android.com/devices/tech/ota/block.html

    基于块的函数在bootable/recovery/updater/blockimg.cpp中实现。

  • 开发者可以扩展恢复和更新器以提供特定设备的 OTA 扩展。

在注册所有函数后,它调用 parse_string 函数来解析脚本。最后,它调用 Evaluate 函数来执行脚本。

更新器脚本

在我们探索更新器的实现之后,我们将在本节中查看更新器脚本。更新器脚本是在目标设备上执行更新操作的那个脚本。更新器脚本是用一种简单的脚本语言 edify 编写的。edify 脚本是一系列表达式,每行一个表达式。它支持以下运算符:

  • 比较运算符,例如 ==(字符串相等)和 !=(字符串不等)

  • 逻辑运算符,例如 ||(逻辑或)、&&(逻辑与)和 !(逻辑非)

  • 连接运算符 +

唯一的保留关键字是条件关键字 ifthenelseendif

edify 中的所有值都是字符串。在布尔上下文中,空字符串为 false,所有其他字符串为 true

您可以参考以下 URL 了解 edify 语法的更多信息:

source.android.com/devices/tech/ota/inside_packages

Edify 函数

edify 语言的主体功能以 edify 函数的形式实现,而 edify 函数则注册在先前的更新器源代码中。为了支持 OTA 更新,edify 函数包括内置函数、安装函数、块镜像函数和设备扩展。我们将在接下来的几节中查看每个类别。

内置函数

内置函数用于支持 edify 语言语法。内置函数通过 RegisterBuiltins 注册。我们可以查看以下源代码:

void RegisterBuiltins() { 
    RegisterFunction("ifelse", IfElseFn); 
    RegisterFunction("abort", AbortFn); 
    RegisterFunction("assert", AssertFn); 
    RegisterFunction("concat", ConcatFn); 
    RegisterFunction("is_substring", SubstringFn); 
    RegisterFunction("stdout", StdoutFn); 
    RegisterFunction("sleep", SleepFn); 

    RegisterFunction("less_than_int", LessThanIntFn); 
    RegisterFunction("greater_than_int", GreaterThanIntFn); 
} 

RegisterBuiltins 函数注册以下内置函数:

  • ifelse(cond, e1[, e2]): 评估 cond,如果为 true,则评估并返回 e1 的值,否则评估并返回 e2(如果存在)。

  • abort([msg]): 立即终止脚本的执行,可选的 msg 参数。如果用户已开启文本显示,msg 将出现在恢复日志和屏幕上。

  • assert(expr[, expr, ...]): 依次评估每个 expr。如果其中任何一个为 false,则立即终止执行并显示消息 assert failed

  • concat(expr[, expr, ...]): 评估每个表达式并将它们连接起来。

  • is_substring(substring, string): 如果可以找到子字符串,则返回 true。

  • stdout(expr[, expr, ...]): 评估每个表达式并将它们的值输出到 stdout。这在调试中非常有用。

  • sleep(secs): 等待 secs 秒。

  • less_than_int(a, b): 如果且仅当 a(解释为整数)小于 b(解释为整数)时返回 true。

  • greater_than_int(a, b): 如果且仅当 a(解释为整数)大于 b(解释为整数)时返回 true。

安装函数

与安装相关的函数通过 RegisterInstallFunctions 注册。以下是其源代码:

void RegisterInstallFunctions() { 
    RegisterFunction("mount", MountFn); 
    RegisterFunction("is_mounted", IsMountedFn); 
    RegisterFunction("unmount", UnmountFn); 
    RegisterFunction("format", FormatFn); 
    RegisterFunction("show_progress", ShowProgressFn); 
    RegisterFunction("set_progress", SetProgressFn); 
    RegisterFunction("delete", DeleteFn); 
    RegisterFunction("delete_recursive", DeleteFn); 
    RegisterFunction("package_extract_dir", PackageExtractDirFn); 
    RegisterFunction("package_extract_file", PackageExtractFileFn); 
    RegisterFunction("symlink", SymlinkFn); 
    RegisterFunction("set_metadata", SetMetadataFn); 
    RegisterFunction("set_metadata_recursive", SetMetadataFn); 
    RegisterFunction("getprop", GetPropFn); 
    RegisterFunction("file_getprop", FileGetPropFn); 
    RegisterFunction("write_raw_image", WriteRawImageFn); 
    RegisterFunction("apply_patch", ApplyPatchFn); 
    RegisterFunction("apply_patch_check", ApplyPatchCheckFn); 
    RegisterFunction("apply_patch_space", ApplyPatchSpaceFn); 
    RegisterFunction("wipe_block_device", WipeBlockDeviceFn); 
    RegisterFunction("read_file", ReadFileFn); 
    RegisterFunction("sha1_check", Sha1CheckFn); 
    RegisterFunction("rename", RenameFn); 
    RegisterFunction("wipe_cache", WipeCacheFn); 
    RegisterFunction("ui_print", UIPrintFn); 
    RegisterFunction("run_program", RunProgramFn); 
    RegisterFunction("reboot_now", RebootNowFn); 
    RegisterFunction("get_stage", GetStageFn); 
    RegisterFunction("set_stage", SetStageFn); 
    RegisterFunction("enable_reboot", EnableRebootFn); 
    RegisterFunction("tune2fs", Tune2FsFn); 
} 

如我们所见,大多数函数都注册在这里;我们现在将查看它们:

  • mount(fs_type, partition_type, name, mount_point): 此函数在 mount_point 处挂载 fs_type 文件系统。partition_type 参数必须是 MTD 或 EMMC 之一。name 参数是分区名称(系统、userdata 或 cache 等)。恢复默认不挂载任何文件系统,更新脚本必须挂载它需要修改的任何分区。

  • is_mounted(mount_point): 如果在 mount_point 处挂载了文件系统,则返回 true。

  • unmount(mount_point): 卸载在 mount_point 处挂载的文件系统。

  • format(fs_type, partition_type, location, fs_size, mount_point): 此函数格式化给定的分区。fs_type 参数可以是 yaffs2、ext4 或 f2fs。partition_type 参数可以是 MTD 或 EMMC。location 参数是分区或设备的名称。fs_size 参数是文件系统大小,mount_point 是挂载点名称。

  • show_progress(frac, secs): 在 secs 秒内将进度条向前推进到其长度的 frac 部分。secs 参数可以是零,在这种情况下,进度条不会自动前进,而是通过以下定义的 set_progress 函数使用:

    • set_progress(frac): 此函数设置进度条在最近一次 show_progress 调用定义的块中的位置。
  • delete([filename, ...]): 删除列出的所有文件名。返回成功删除的文件数量。

  • delete_recursive([dirname, ...]): 递归删除 dirname 及其所有内容。返回成功删除的目录数量。

  • package_extract_dir(package_dir, dest_dir): 从 package_dir 下的包中提取所有文件,并将它们写入 dest_dir 下的相应树中。任何现有文件都将被覆盖。

  • package_extract_file(package_file[, dest_file]): 从 update 包中提取单个 package_file 并将其写入 dest_file,如果需要则覆盖现有文件。

  • symlink(target[, source, ...]): 将所有源创建为指向目标的符号链接。

  • set_metadata(filename, key1, value1[, key2, value2, ...]): 将给定文件名的键设置为值。

  • set_metadata_recursive(dirname, key1, value1[, key2, value2, ...]): 递归地将给定 dirname 及其所有子目录的键设置为值。

  • getprop(key): 返回系统属性键的值(如果没有定义,则返回空字符串)。

  • file_getprop(filename, key): 读取给定的文件名,将其解释为属性文件(例如,/system/build.prop),并返回给定键的值,如果键不存在,则返回空字符串。

  • write_raw_image(filename_or_blob, partition): 将 filename_or_blob 中的镜像写入 MTD 分区。

  • apply_patch(src_file, tgt_file, tgt_sha1, tgt_size, patch1_sha1, patch1_blob, [...]): 将二进制补丁应用到 src_file 上以生成 tgt_file

  • apply_patch_check(filename, sha1[, sha1, ...]): 如果 filename 的内容或缓存分区中的临时副本(如果存在)的 SHA1 校验和等于给定的 sha1 值之一,则返回 true。

  • apply_patch_space(bytes): 如果至少有 bytes 的临时空间可用于应用二进制补丁,则返回 true。

  • wipe_block_device(block_dev, len): 清除给定块设备 block_devlen 字节。

  • read_file(filename): 读取 filename 并将其内容作为二进制块返回。

  • sha1_check(blob[, sha1]): blob 参数是 read_file 返回的类型或 package_extract_file 的一参数形式。如果没有 sha1 参数,此函数返回 blob 的 SHA1 哈希。如果有一个或多个 sha1 参数,此函数返回等于其中一个参数的 SHA1 哈希,如果不等于任何一个参数,则返回空字符串。

  • rename(src_filename, tgt_filename): 将 src_filename 重命名为 tgt_filename

  • wipe_cache(): 在成功安装结束时清除缓存分区。

  • ui_print([text, ...]): 连接所有文本参数并将结果打印到 UI。

  • run_program(path[, arg, ...]): 使用参数 arg 执行 path 上的二进制文件。返回程序的退出状态。

  • reboot_now(name[, arg, ...]): 立即重启设备。name 参数是传递给 Android 重启属性的分区名称。

  • get_stage(name): 此函数返回由 set_stage 函数保存的值。name 参数是 /misc 分区的块设备。

  • set_stage(name, stage): 这个函数存储一个字符串值,以便未来的恢复调用可以访问。name 参数是 /misc 分区的块设备。stage 是要存储的字符串。

  • enable_reboot(): 通过管道发送 enable_reboot 命令到恢复。

  • tune2fs(arg, ...): 在 ext2/ext3 文件系统上更改文件系统参数。

块图像函数

在 Android 5.0 或更高版本中,可以使用基于块的 OTA 包。基于块的 OTA 包将整个分区视为单个文件,并在块级别进行更新。基于块的 OTA 包的函数通过 RegisterBlockImageFunctions 函数注册:

void RegisterBlockImageFunctions() { 
    RegisterFunction("block_image_verify", BlockImageVerifyFn); 
    RegisterFunction("block_image_update", BlockImageUpdateFn); 
    RegisterFunction("range_sha1", RangeSha1Fn); 
} 

基于块的更新实现包括三个函数:

  • block_image_verify(partition, transfer_list, new, patch): partition 参数是更新将进行的设备。通常,它是 /system 分区。transfer_list 参数是一个包含在 target 分区上从一个地方传输到另一个地方的命令的文本文件。此命令仅执行干运行,不写入,以测试更新是否可以继续。

  • block_image_update(partition, transfer_list, new, patch): 此函数与 block_image_verify 相同,但它执行实际更新。

  • range_sha1(partition, range): 这个函数检查指定范围内的分区的 SHA1 哈希值。

设备扩展

作为 Android 系统开发者,我们可以扩展 edify 语言以满足我们设备的特定需求。要使用我们自己的函数扩展 edify 语言,我们可以使用以下函数调用注册我们的函数:

RegisterDeviceExtensions();  

我们将在下一章解释如何扩展 edify 语言。

准备 x86vbox 的 OTA 包

到目前为止,我们已经了解了 OTA 包内的更新器和更新器脚本。现在我们可以为我们的 x86vbox 设备构建 OTA 包了。要构建 OTA 包,我们可以使用以下命令:

$ mkdir -p dist_output
$ make dist DIST_DIR=dist_output  

Android 5 及以上版本默认构建的 OTA 包是构建基于块的 OTA 包,但我们在为 x86vbox 构建基于块的 OTA 包时会遇到错误。在我们的环境中,还需要进行很多配置才能支持基于块的 OTA 包。所有第三方恢复包也无法使用基于块的更新包。

为了避免这个错误,我们需要将以下build/core/Makefile文件更改为移除--block选项:

$(INTERNAL_OTA_PACKAGE_TARGET): $(BUILT_TARGET_FILES_PACKAGE) $(DISTTOOLS) 
        @echo "Package OTA: $@" 
        $(hide) PATH=$(foreach 
        p,$(INTERNAL_USERIMAGES_BINARY_PATHS),$(p):)$$PATH 
        MKBOOTIMG=$(MKBOOTIMG) \ 
           ./build/tools/releasetools/ota_from_target_files -v \ 
 --block \ 
           -p $(HOST_OUT) \ 
           -k $(KEY_CERT_PAIR) \ 
           $(if $(OEM_OTA_CONFIG), -o $(OEM_OTA_CONFIG)) \ 
           $(BUILT_TARGET_FILES_PACKAGE) $@ 

构建完成后,我们可以按照以下方式检查 OTA 包:

$ ls dist_output/**-ota-*.zip
dist_output/x86vbox-ota-eng.sgye.zip  

让我们看看我们刚刚构建的 OTA 包内的更新器脚本:

(!less_than_int(1482376066, getprop("ro.build.date.utc"))) || abort("Can't install this package (Thu Dec 22 11:07:46 CST 2016) over newer build (" + getprop("ro.build.date") + ")."); 
getprop("ro.product.device") == "x86vbox" || abort("This package is for \"x86vbox\" devices; this is a \"" + getprop("ro.product.device") + "\"."); 
ui_print("Target: Android-x86/x86vbox/x86vbox:7.1.1/MOB30Z/roger12221103:eng/test-keys"); 
show_progress(0.750000, 0); 
format("ext4", "EMMC", "/dev/block/sda1", "0", "/system"); 
mount("ext4", "EMMC", "/dev/block/sda1", "/system", "max_batch_time=0,commit=1,data=ordered,barrier=1,errors=panic,nodelalloc"); 
package_extract_dir("system", "/system"); 
symlink("../../gm200/acr/bl.bin", "/system/lib/firmware/nvidia/gm204/acr/bl.bin", 
        "/system/lib/firmware/nvidia/gm206/acr/bl.bin"); 
... 
symlink("wl127x-nvs.bin", "/system/lib/firmware/ti-connectivity/wl1271-nvs.bin", 
        "/system/lib/firmware/ti-connectivity/wl12xx-nvs.bin"); 
set_metadata_recursive("/system", "uid", 0, "gid", 0, "dmode", 0755, "fmode", 0644, "capabilities", 0x0, "selabel", "u:object_r:system_file:s0"); 
set_metadata_recursive("/system/bin", "uid", 0, "gid", 2000, "dmode", 0755, "fmode", 0755, "capabilities", 0x0, "selabel", "u:object_r:system_file:s0"); 
set_metadata("/system/bin/app_process32", "uid", 0, "gid", 2000, "mode", 0755, "capabilities", 0x0, "selabel", "u:object_r:zygote_exec:s0"); 
... 
set_metadata("/system/xbin/su", "uid", 0, "gid", 2000, "mode", 04751, "capabilities", 0x0, "selabel", "u:object_r:su_exec:s0"); 
show_progress(0.050000, 5); 
package_extract_file("boot.img", "/dev/block/sda8"); 
show_progress(0.200000, 10); 
unmount("/system"); 

在更新器脚本中,它首先检查当前系统的构建信息。如果当前系统比 OTA 包新,则不会更新系统。之后,它还会检查运行系统的设备名称和 OTA 包,两者应该匹配。否则,我们可能会使用错误的 OTA 包来更新系统。

在完成所有验证工作后,脚本将格式化/system分区,并从 OTA 包中创建一个新的system文件夹。一旦系统文件安装完成,脚本将创建所有必要的软链接,并应用 SELinux 属性。

最后,它将使用新的内核和 ramdisk 更新/boot分区。

一旦我们为 x86vbox 设备构建了 OTA 包,并在第十二章“介绍恢复”中构建了恢复,我们就可以更新我们的系统到 OTA 包。我们应该能够使用这个 OTA 包来更新系统,但此时系统可能无法启动。在我们能够进行更多操作之前,我们有两个问题需要解决。

回顾我们为 x86vbox 构建恢复的过程,我们尽可能重用了从第八章“在 VirtualBox 上创建您的设备”到第十一章“启用 VirtualBox 特定硬件接口”中开发的源代码。这意味着我们在第十二章“介绍恢复”中构建的恢复中继承了以下功能:

  • 从两个阶段的启动继承的第一个问题是,我们使用 Android system 文件夹中的组件来启动恢复。理想情况下,恢复不应该依赖于其他任何东西。它应该是一个自包含的系统。例如,即使系统镜像损坏,恢复也应该能够正常工作。我们可以使用恢复来修复系统。

  • 我们使用 Android-x86 项目的两个阶段启动过程。正如我们从前几章中看到的那样,两个阶段启动的系统磁盘布局与标准 Android 系统不同。我们使用 OTA 包创建的系统是标准的 Android 系统磁盘布局。我们只能在 OTA 更新后使用标准的启动过程来启动系统。这意味着我们必须使用 ramdisk.img 而不是 initrd.img 来启动系统。

移除对 /system 的依赖

对 Android /system 文件夹的依赖包括两部分:

  • 所有设备驱动程序的内核模块都位于:$OUT/system/lib/modules/4.x.x-android-x86

  • 在恢复启动过程中,我们需要运行一些基本的 Linux 命令。例如,我们使用以下命令进行硬件初始化:

    on init exec -- /system/bin/logwrapper /system/bin/sh /system/etc/init.sh

让我们在接下来的几节中逐一讨论前两点。

恢复中的硬件初始化

为了加载恢复所需的最低限度的设备驱动程序,我们必须更改 Android 系统启动的 shell 脚本执行。这是一个从一般到具体的定制过程,这与 Android-x86 项目的目标不同。在 Android-x86 项目中,所有可能的设备驱动程序都是可用的,而在这里我们只应该包含恢复所需的 VirtualBox 驱动程序。正如我们在介绍两个阶段启动时看到的那样,所有可能的设备驱动程序都在 $OUT/system/lib/modules/4.x.x-android-x86 文件夹中编译并可用。

内核模块将根据内核动态找到的硬件加载到系统中。在我们的例子中,我们将移除动态加载过程,只保留恢复启动所必需的最小内核模块。让我们看看 x86vbox 的原始启动脚本:

on init 
    exec -- /system/bin/logwrapper /system/bin/sh /system/etc/init.sh 

在启动过程中,init 进程将运行前面的命令行来执行 /system/etc/init.sh 脚本。/system/bin/logwrapper/system/bin/sh 命令都是 Android 系统中 /system/bin 文件夹的一部分。它们在恢复模式下不可用,因为恢复启动后没有挂载 /system 分区。

为了解决这个问题,我们将使用 initrd.img 中的 busybox 二进制文件在恢复环境中提供一个最小的环境来执行 Linux shell 命令。我们也不能执行 /system/etc/init.sh 脚本,因为它存储在 /system/etc 文件夹中,这个文件夹在恢复模式下也不可用。我们将通过在恢复环境中的 /sbin 下创建另一个脚本 init.x86vbox.sh 来替换它。

我们将init.recovery.x86vbox.rc更改为以下内容以移除对/system的依赖:

on early-init 
    # for /bin/busybox 
    symlink /bin/ld-linux.so.2 /lib/ld-linux.so.2 
    symlink /bin/busybox /bin/sh 

on init 
    mkdir /vendor 
    exec -- /bin/sh /sbin/init.x86vbox.sh 

service network_start /sbin/network_start.sh 
    user root 
    seclabel u:r:recovery:s0 
    oneshot 

service console /bin/sh 
    class core 
    console 
    group shell log 
    seclabel u:r:shell:s0 

on property:ro.debuggable=1 
    start console 

early-init阶段,我们创建软链接以使/bin/sh可用。我们将/system/bin/sh替换为位于恢复 ramdisk 中的/bin/sh

init.x86vbox.sh脚本中,我们加载恢复所需的设备驱动程序如下:

#!/bin/busybox sh  

echo -n "Initializing x86vbox hardware ..." 
PATH=/bin:/sbin:/bin; export PATH 

cd /bin;busybox --install -s 

cd /x86vbox 
insmod atkbd.ko 
insmod cn.ko 
insmod vboxguest.ko 
insmod vboxsf.ko 
insmod uvesafb.ko mode_option=${UVESA_MODE:-1024x768}-32 

/sbin/mount.vboxsf sdcard /vendor 

如我们所见,在 shell 脚本init.x86vbox.sh中,我们首先创建了busybox的所有软链接。然后,我们加载了所有必要的设备驱动程序。我们还挂载了 VirtualBox 在/vendor文件夹下的共享文件夹,以便我们可以在主机和客户机之间交换数据。我们将在下一章中使用此文件夹。

恢复时的最小执行环境

如我们从两个脚本中可以看出,init.recovery.x86vbox.rcinit.x86vbox.sh,我们需要执行一些 Linux 命令,以便我们可以在启动过程中执行我们的任务。

我们需要在ramdisk-recovery.img中包含所有这些 Linux 命令,以便它们在恢复时可用。然而,问题并不像我们迄今为止所认为的那么简单。大多数命令在 AOSP 构建输出中是动态链接的,而不是静态链接的。

在我们的情况下,我们需要在ramdisk-recovery.img中包含两套共享库。Android-x86 中的initrd.img中的busybox二进制文件是从 AOSP 树中预构建的,因此它们有自己的依赖关系。如果我们转到newinstaller文件夹bootable/newinstaller/initrd,我们可以看到可执行文件和共享库的列表:

$ ls -1 lib
libcrypt.so.1
libc.so.6
libdl.so.2
libm.so.6
libntfs-3g.so.31
libpthread.so.0
librt.so.1
$ ls -1 bin
busybox
ld-linux.so.2
lndir  

除了busybox二进制文件外,还有八个共享库,如前所述的片段所示。

除了busybox外,我们还有一些作为 AOSP 源树一部分构建的可执行文件。它们有一组不同的共享库,这些库也需要包含在ramdisk-recovery.img中。例如,显示uvesafb驱动程序需要一个用户空间守护进程/sbin/v86d,它是作为 AOSP 树的一部分构建的。如果没有一组共享库,它将无法正常工作。为了使我们能够运行这些可执行文件,我们需要在ramdisk-recovery.img中包含以下共享库:

$ ls -1 recovery/root/system/lib
libc.so
libc++.so
libcutils.so
libext2_uuid.so
liblog.so
libm.so
libpcre.so
libselinux.so 

你可能想知道如何找到共享库的依赖关系。我们可以通过以下命令获取链接信息的一种方法:

$ readelf -d $OUT/recovery/root/sbin/v86d

Dynamic section at offset 0x3e68 contains 29 entries:
 Tag        Type                         Name/Value
 0x00000003 (PLTGOT)                     0x4f7c
0x00000002 (PLTRELSZ)                   240 (bytes)
0x00000017 (JMPREL)                     0x5b0
0x00000014 (PLTREL)                     REL
0x00000011 (REL)                        0x5a8
0x00000012 (RELSZ)                      8 (bytes)
0x00000013 (RELENT)                     8 (bytes)
0x00000015 (DEBUG)                      0x0
0x00000006 (SYMTAB)                     0x1a0
0x0000000b (SYMENT)                     16 (bytes)
0x00000005 (STRTAB)                     0x3d0
0x0000000a (STRSZ)                      324 (bytes)
0x6ffffef5 (GNU_HASH)                   0x514
0x00000001 (NEEDED)                     Shared library: [libc++.so]
0x00000001 (NEEDED)                     Shared library: [libdl.so]
0x00000001 (NEEDED)                     Shared library: [libc.so]
0x00000001 (NEEDED)                     Shared library: [libm.so]
0x00000020 (PREINIT_ARRAY)              0x4e50
0x00000021 (PREINIT_ARRAYSZ)            0x8
0x00000019 (INIT_ARRAY)                 0x4e58
0x0000001b (INIT_ARRAYSZ)               8 (bytes)
0x0000001a (FINI_ARRAY)                 0x4e60
0x0000001c (FINI_ARRAYSZ)               8 (bytes)
0x0000001e (FLAGS)                      BIND_NOW
0x6ffffffb (FLAGS_1)                    Flags: NOW
0x6ffffff0 (VERSYM)                     0x540
0x6ffffffe (VERNEED)                    0x588
0x6fffffff (VERNEEDNUM)                 1
0x00000000 (NULL)                       0x0 

如前所述的输出所示,我们可以使用readelf命令找到/sbin/v86d所需的共享库。我们还需要通过恢复环境中的测试来验证依赖关系,我们将在下一章中进一步讨论。

为了将所有讨论过的内核模块和共享库包含在ramdisk-recovery.img中,我们更改了x86vbox.mk的一部分,如下所示:

图片

构建和测试

在本章完成所有分析后,我们现在可以构建和测试我们的代码了。

如常,我们为每一章都准备了一个清单文件。我们根据第十二章,“介绍恢复”的源代码对这一章进行了修改。以下是我们在这一章中更改的项目:

<?xml version="1.0" encoding="UTF-8"?> 
<manifest> 

  <remote  name="github" 
           revision="refs/tags/android-7.1.1_r4_x86vbox_ch13_r1" 
           fetch="." /> 

  <remote  name="aosp" 
           fetch="https://android.googlesource.com/" /> 
  <default revision="refs/tags/android-7.1.1_r4" 
           remote="aosp" 
           sync-c="true" 
           sync-j="1" /> 

  <!-- github/android-7.1.1_r4_ch13 --> 
  <project path="kernel" name="goldfish" remote="github" /> 
  <project path="bootable/newinstaller"  
  name="platform_bootable_newinstaller" remote="github" /> 
  <project path="device/generic/common" name="device_generic_common"  
  groups="pdk" remote="github" /> 
  <project path="device/generic/x86vbox" name="x86vbox" remote="github" 
  /> 
  <project path="bootable/recovery" name="android_bootable_recovery" 
  remote="github" groups="pdk" /> 
...  

我们可以看到我们需要更改四个项目:recoverynewinstallercommonx86vbox。我们有一个android-7.1.1_r4_x86vbox_ch13_r1标签作为本章源代码的基线。

要从 GitHub 和 AOSP 直接获取源代码,可以使用以下命令:

$ repo init -u https://github.com/shugaoye/manifests -b android-7.1.1_r4_ch13_aosp
$ repo sync  

源代码准备就绪后,我们可以设置环境并按照以下步骤构建系统:

$ . build/envsetup.sh
$ lunch x86vbox-eng
$ make -j4  

要构建initrd.img,我们可以运行以下命令:

$ make initrd USE_SQUASHFS=0  

要为 x86vbox 设备构建 OTA 包,我们可以运行以下命令:

$ mkdir -p dist_output
$ make dist DIST_DIR=dist_output  

要在 VirtualBox 中测试 AOSP 镜像,我们需要使用我们在第九章,“使用 PXE/NFS 启动 x86vbox”中介绍过的 PXE 引导和 NFS。

构建完成后,我们可以在 PXE 引导配置文件$HOME/.VirtualBox/TFTP/pxelinux.cfg/default中添加一个条目,如下以测试恢复:

label 3\. Recovery - x86vbox
menu x86vbox_ramdisk_recovery
kernel x86vbox/kernel
append ip=dhcp console=ttyS3,115200 initrd=x86vbox/ramdisk-recovery.img androidboot.hardware=x86vbox  

恢复启动后,我们可以在 x86vbox 设备上看到以下恢复屏幕:

图片

x86vbox 的恢复用户界面在任何 Android 设备上看起来都一样。

在你下载源代码并自行构建所有内容之前,你也可以下载并测试本章中提供的预构建镜像,链接为sourceforge.net/projects/android-system-programming/files/android-7/ch13/ch13.zip/download

摘要

在这一章中,我们学习了更新器的流程,它是实际执行 OTA 更新工作的工具。更新器解释 OTA 包内的更新脚本以执行更新。我们不必自己创建更新脚本。它是在构建过程中自动创建的。你可能在这里有一些疑问,因为你可能使用了一些开源开发者或 ROM 开发者创建的恢复包。你甚至可能使用 LineageOS/CyanogenMod 或 TWRP 分发的恢复。它们与我们本章讨论的主题有何关联?这些是我们将在下一章中讨论的主题。

posted @ 2025-10-25 10:45  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报