Android QEMU 模拟器是我们日常 Android 开发中一个非常有力的工具,也是一套有很多值得玩的地方的系统,它还是开源的,主要由 Google Android 在维护。(Android QEMU 模拟器的源码下载、编译及调试方法可参考Android 模拟器下载、编译及调试)目前大多数开发者所用的开发机器都是 X86 架构的,因而 Google 官方对于Android QEMU 模拟器的编译和运行,也假设都是在 X86 架构平台进行的。这种假设在整个 Android QEMU 模拟器的编译和运行方面都有所体现,如编译配置仅提供了 X86 的相关选项,编译 Android QEMU 模拟器所需的一些预编译第三方库仅提供了X86 或 X86_64 的版本,编译脚本里仅有对 X86 或 X86_64 主机平台的行为定义,在代码中运行时一些关键的操作上仅有X86 或 X86_64 主机平台的定义等。
Android QEMU 模拟器的玩法很多,某些场景下会有将 Android QEMU 模拟器移植到不同的主机 CPU 架构上的需要,特别是 ARM64 平台。当前已经出现了不少 ARM64 的服务器硬件平台,可以运行完整的 Ubuntu Linux 等系统。大多数 Android 终端采用 ARM 低功耗处理器,Android 系统运行于 ARM 平台,因而大多数 Android 应用对于 ARM 平台的支持会更好一点。ARM 目标 Android 系统运行于我们日常的 X86 开发机的模拟器上时,性能都非常差。在 ARM64 平台上运行 ARM 版模拟器,同时开启 KVM,则可以获得 Android QEMU 非常好的运行性能。(KVM/ARM 虚拟化技术的更多信息,可参考 KVM/ARM: AN OPEN-SOURCE ARM VIRTUALIZATION SYSTEM
))ARM64 服务器 + ARM 版模拟器 + KVM 也就顺利成章地成为许多有趣的场景的技术基础。
本文以 ARM64 平台为例介绍把 Android QEMU 模拟器移植到 ARM64 Ubuntu Linux 平台的过程,其中 Code base 为 2019 年 3 月拉取,qemu 所用的 branch 为 emu-2.3-release。移植过程主要分为几个步骤:
- 配置编译
- 配置脚本成功执行
- 预编译库的编译移植。在 Android QEMU 模拟器的源码库中有编译安装这些库的脚本,但可能需要有针对性地对脚本做些移植。具体需要移植的库主要包括如下这些:
- Zlib
- Libpng
- Libxml2
- LibCURL
- LibANGLEtranslation
- Protobuf
- Breakpad
- Qt
- e2fsprogs
- ffmpeg
- x264
- 构建配置文件的移植修改,主要指 mk 文件
- 运行时问题。代码里面有一些与特定硬件平台相关的代码,需要专门针对 ARM 平台做一些适配
- nowindow 及功能扩充。对于某些需要运行模拟器的机器,显示模拟器的窗口可能不太方便,因而需要将模拟器中窗口显示的部分拆除掉。功能扩充则指的是,某些运行模拟器的机器,渲染能力比较弱,则可以将模拟器中虚拟 Android 系统相关的逻辑和图形渲染相关的逻辑分开在不同的机器上运行。
解决了上面的重重困难之后,Android QEMU 模拟器应用程序本身总算是具备了运行的条件。但要真正让 Android QEMU 模拟器运行起来,还缺少一项关键的东西,那就是 AVD。有了 AVD,Android QEMU 模拟器应用程序才能知道要虚拟什么样的 Android 系统。AVD 的创建,就像我们平常在 X86 的开发机器上所做的那样,需要通过 Android SDK 来完成。除了 AVD,我们日常 Android 开发工具中必不可少的一些工具,如 adb 等也需要能够运行起来,这也离不开 Android SDK。但 Android SDK 不能用我们平常用的那个 X86 的版本,而需要用专门为 ARM 平台移植的版本。在上面的移植步骤之后增加一个关键的步骤:
- Android SDK 的移植
本文主要关注 Android QEMU 模拟器应用程序本身的配置编译问题。
1. 编译问题
Android QEMU 模拟器的源码中提供了构建脚本,可以用来构建生成二进制可执行文件。具体而言,是 qemu/android/rebuild.sh
。切换进 qemu 源码根目录,像下面这样执行 qemu/android/rebuild.sh
可以进行编译:
岁月静好,一切都是安安静静的。执行 rebuild.sh
脚本,并加上 --help
参数,可以看到这个脚本更详细的用法:
在执行 rebuild.sh
脚本时,加上 --no-tests
参数,可以在编译成功之后不运行单元测试,以加快编译完成时间,加上 --verbose
参数可以输出更详细的编译过程信息。
rebuild.sh
脚本的功能可以简单地理解为,清理之前编译遗留下来的配置文件和中间文件,执行 qemu/android/configure.sh
脚本完成配置,然后执行 make
完成编译。了解了这一点对于我们分析配置和编译过程中的错误非常有意义——当我们为出现的某个错误修改了代码时,不用每次都执行 rebuild.sh
脚本完整的进行一次构建来验证。
1.1 配置
如我们上面提到的,配置在执行 rebuild.sh
脚本时,它调用 qemu/android/configure.sh
脚本完成。qemu/android/configure.sh
脚本也可以单独执行进行配置,如下面这样:
在 ARM 64 Ubuntu 平台上执行 qemu/android/configure.sh
脚本时,遇到的第一个问题是如下的报错:
这主要是由 qemu/android/configure.sh
脚本中设置 BUILD_ARCH 的逻辑引起的,如下面这段代码(在 41 行左右):
需要在上面这段代码中加入对与 ARM 64 平台的支持。具体怎么加?上面的代码其实已经给出了提示,ARM 64 的 arch 值可以通过 uname -m
命令获得:
加完之后,脚本中配置 BUILD_ARCH 的代码将像下面这样:
再次执行 qemu/android/configure.sh
脚本,遇到如下的错误:
这个错误信息在 qemu/android/configure.sh
脚本中抛出,但原因是在执行 qemu/android/scripts/gen-android-sdk-toolchain.sh
脚本时出错导致,错误的原因有多个,一是在 qemu/android/scripts/utils/common.shi
脚本中定义的 get_build_arch()
函数有 bug 所致,该函数的定义如下:
其中 bug 主要出在 TEST=$(/usr/bin/file -L $SHELL 2>/dev/null | grep 'x86[_-]64')
。这一行想通过 file 命令获取 shell 程序的文件属性中 CPU 架构有关的信息来获取主机的 CPU 架构信息,但当 grep 'x86[_-]64'
失败时,会导致整个脚本以非零错误值退出。另外这个函数本身没有考虑对非 x86 架构的支持。修正这个问题:
修改完问题依旧:
在 qemu/android/scripts/gen-android-sdk-toolchain.sh
中对于 GNU_CONFIG_HOST
和 EXTRA_CFLAGS
的配置也需要有对 ARM 64 平台的支持:
再次执行 qemu/android/configure.sh
脚本,遇到如下的错误:
这个是在 android/configure.sh
脚本中(大约115 行),调用由 qemu/android/scripts/gen-android-sdk-toolchain.sh
生成的 gcc 编译器包装器执行编译测试时发生的错误。qemu/android/scripts/gen-android-sdk-toolchain.sh
脚本会在 objs/build/toolchain/
目录下生成构建工具链的包装器脚本:
这些脚本的内容类似与下面这样:
即 qemu/android/scripts/gen-android-sdk-toolchain.sh
脚本根据系统配置,将构建工具链的路径统一化。对于 ARM 64 平台的编译,上面的脚本内容显然是错误的。但这些内容是怎么来的呢?它们是在 qemu/android/scripts/gen-android-sdk-toolchain.sh
脚本通过 prepare_build_for_host -> gen_wrapper_toolchain -> gen_wrapper_program 这一系列函数调用产生的。路径中的一些前缀,在 prepare_build_for_host 函数中,通过调用定义在 qemu/android/scripts/utils/aosp_dir.shi
文件中的 aosp_prebuilt_toolchain_subdir_for
和 aosp_prebuilt_toolchain_prefix_for
函数获得,这两个函数缺乏对于 ARM 64 的支持。
修正这个问题(即修改 qemu/android/scripts/utils/aosp_dir.shi
文件中的 aosp_prebuilt_toolchain_subdir_for
和 aosp_prebuilt_toolchain_prefix_for
函数定义):
再次执行 qemu/android/configure.sh
脚本,遇到如下的错误:
这个是在拷贝 C++ 标准库是出现的问题。需要修改几个地方:一是 qemu/android/scripts/utils/aosp_dir.shi
文件中的 aosp_prebuilt_toolchain_sysroot_subdir_for
函数,提供对 ARM 64 的支持,找到正确的文件路径:
二是 qemu/android/configure.sh
脚本中的 copy_toolchain_lib 函数及该函数调用的地方:
这是由于没有32 位 ARM 的平台的 C++ 标准库的动态库文件。
再次执行 qemu/android/configure.sh
脚本,终于成功完成:
至此,总算可以执行编译配置。
1.2 依赖
qemu/android/configure.sh
脚本吐出来的信息包含了构建 Android QEMU 模拟器时所需的主要的库及其预编译库文件的路径。
解决依赖问题主要是为这些库编译出 ARM 64 平台的二进制库文件,并放置在上面的位置。编译上面的这些库,可以通过 qemu/android/scripts/
目录下,名为 build-xxx.sh
的脚本完成:
对于不同库,上面的这些脚本可用性不太一样,有些脚本可以直接为 ARM 64 平台编译出二进制文件,有些则需要做更多的移植修改。这里暂时先不对这些依赖库的构建做更详细的说明。
1.3 编译配置
如上面成功执行 qemu/android/configure.sh
脚本之后看到的那样,配置完成之后,可以通过执行 make
开始编译:
只是一上来,就报了错,在 android/build/core/definitions-init.mk
的 251 行左右:
上面这个报错是由于在 android/build/core/definitions-init.mk
构建配置文件中缺乏对于 ARM 64 平台的支持。将上面那段代码修改为下面这样:
再次执行 make
,出现了另外的 error:
这主要是在 android/build/emulator/binary.make
构建配置文件中,配置的 ‘-m64’ 等编译选项,ARM 平台的编译器不支持。为这个配置文件中,添加 ‘-m64’ 等编译选项的代码,加上判断,仅在 x86_64
平台上才添加这些编译选项,如下面这样:
再次执行 make
,不出意外仍然编译失败,报错如下:
这是由于在 android/build/emulator/binary.make
编译配置文件中,编译 C 文件时,编译选项 ‘-msse2’ 无法被 ARM 平台上的编译器识别所致:
修改 qemu/android/third_party/jpeg-6b/libjpeg.mk
文件中如下的这段代码:
修改为下面这样:
以使得在 ARM 平台上编译的时候,不会加上 ‘-msse2’ 编译选项。再次执行 make
,依然编译失败,报错如下:
这是由于在 qemu/android/third_party/Qt5.mk 构建配置文件中,缺少对 ARM 平台的支持,如下面这样:
修改这个配置文件,增加对 ARM 64 平台的支持,来解决这个问题:
再次执行 make
,依然编译失败,报错如下:
又是一个由于编译选项编译器无法识别的问题。这通过修改 qemu/android/android-emu/android/skin/sources.mk 文件中如下的代码来解决:
修改为:
同时修改 qemu/android/build/Makefile.top.mk 文件中的如下代码段:
修改为:
以增加对 ARM 64 平台的支持。Android QEMU 的构建系统在编译时,会同时编译出QEMU1 和 QEMU2 的模拟器,这主要在 android/build/Makefile.common.mk 文件中控制:
再次执行 make
,在 ARM 平台上编译 QEMU1 时会报出如下错误:
可以修改上面那段代码,避免编译我们不需要的目标文件:
再次执行 make
,依然编译失败,报错如下:
这是由于在 ARM 平台上编译时,缺少了常量 EMUGL_LIBNAME
的定义:
修改这段代码,以便于为 ARM 平台增加常量定义:
再次执行 make
,依然编译失败,报错如下:
这是由于缺少了一个配置头文件,我们参考 qemu/android-qemu2-glue/config/linux-x86_64/config-host.h
文件来创建 qemu/android-qemu2-glue/config/linux-aarch64/config-host.h
文件:
同时我们也不再编译 QEMU2 的部分目标平台模拟器,将如下这段代码:
修改为:
再次执行 make
,依然编译失败,报错如下:
这是由于系统调用常量的定义在 ARM 64 平台上有些不同。修改 qemu/util/compatfd.c
文件中如下这段代码:
修改为:
再次执行 make
,依然编译失败,报错如下:
编译 tcg 失败,又是由于缺少对 ARM 64 平台的支持所致。修改 qemu/android-qemu2-glue/build/Makefile.qemu2-target.mk
文件中的如下这段代码:
修改为:
再次执行 make
,依然编译失败,报错如下:
这是由于加了 -Werror
编译选项。注释掉 qemu/android/build/Makefile.top.mk
文件中加的 -Werror
编译选项即可:
还可以移除对 qemu-upstream 的编译,以节省时间。删除 qemu/android-qemu2-glue/build/Makefile.qemu2-target.mk
文件中如下这段代码即可:
更多编译错误问题解决的细节,此处不再赘述。
Android QEMU 模拟器在 ARM Ubuntu Linux 平台上的编译就到这里。
Done。