4、HAL编译系统:Android.bp编写、模块编译、生成.so文件

好,咱们进入第四章。这一章我要跟你聊聊HAL移植里最绕不开的一个环节——编译系统。

说实话,我刚接触AOSP那会儿,最头疼的就是这个Android.bp。Makefile还没整明白呢,Google又搞出一套新东西。但后来我发现,这东西其实比Makefile清爽多了。你想想看,一个硬件模块从源码到.so文件,中间要经历多少步骤?依赖管理、链接库、ABI选择……这些事如果全靠手写Makefile,那真是要命。

Android.bp就是来解决这个问题的。它用声明式语法,你只管说「我要编译什么、依赖谁」,剩下的构建系统帮你搞定。

4.1 Android.bp 的基本结构

先看一个最简单的例子。假设我们要写一个虚拟光传感器的HAL模块,它的Android.bp长这样:

cc_library_shared {
    name: "lights.default",
    relative_install_path: "hw",
    srcs: ["lights.cpp"],
    shared_libs: [
        "liblog",
        "libhardware",
        "libcutils",
    ],
    cflags: ["-Wall -Werror"],
    vendor: true,
    proprietary: true,
}

我来拆解一下每个字段的意思:

  • cc_library_shared:这是模块类型,表示编译一个C++共享库(.so文件)。如果是纯C代码,用cc_library_shared也一样,编译器会自动识别。
  • name:模块的名字。注意,这个名字会直接影响最终.so文件的命名。比如这里叫lights.default,编译出来就是lights.default.so
  • relative_install_path:安装路径。HAL模块通常要放到/vendor/lib64/hw/下,这个hw就是相对路径。系统会自动拼接前缀。
  • srcs:源文件列表。可以写多个文件,比如["lights.cpp", "utils.cpp"]
  • shared_libs:动态链接库依赖。这里依赖了liblog(日志)、libhardware(HAL核心库)、libcutils(工具库)。
  • vendor: true:标记为vendor模块。这个很重要,AOSP从Android 8.0开始搞了Treble架构,vendor模块和系统模块是分开编译的。

重要proprietary: true 表示这个模块是闭源的。如果你的HAL模块是开源的,可以不加这行。但大多数硬件厂商的HAL都是闭源的,所以这个字段很常见。

4.2 模块类型的选择

在实际项目中,我遇到过不少新手把模块类型搞混。这里我列个表,你一看就明白:

模块类型 输出文件 适用场景
cc_library_shared .so HAL模块、动态库
cc_library_static .a 静态库,用于内部链接
cc_binary 可执行文件 测试程序、工具
cc_test 测试可执行文件 单元测试

我个人习惯是:HAL模块一律用cc_library_shared。为什么?因为HAL是要被hw_get_module动态加载的,必须是.so文件。静态库没法被dlopen打开。

4.3 多架构编译与ABI选择

嗯,这里要注意一个坑。现在的手机基本都是64位处理器,但有些老设备还是32位的。你的HAL模块要同时支持两种架构吗?

在Android.bp里,你可以通过arch字段来控制:

cc_library_shared {
    name: "sensors.mydevice",
    relative_install_path: "hw",
    srcs: ["sensors.cpp"],
    shared_libs: ["libhardware", "liblog"],
    arch: {
        arm: {
            srcs: ["sensors_arm.cpp"],
            cflags: ["-DARM_SPECIFIC"],
        },
        arm64: {
            srcs: ["sensors_arm64.cpp"],
        },
    },
    vendor: true,
}

这段代码的意思是:32位ARM架构下,额外编译sensors_arm.cpp并定义宏ARM_SPECIFIC;64位ARM架构下,编译sensors_arm64.cpp。这样你就能针对不同架构做优化了。

小技巧:如果你不确定目标设备的架构,可以在编译时加TARGET_ARCH变量查看。比如make TARGET_ARCH=arm64强制编译64位版本。

4.4 编译与生成.so文件

写好了Android.bp,接下来就是编译了。在AOSP根目录下,执行:

source build/envsetup.sh
lunch aosp_arm64-userdebug
make lights.default -j8

这里lights.default就是我们在Android.bp里定义的模块名。编译完成后,生成的.so文件在:

out/target/product/generic/vendor/lib64/hw/lights.default.so

如果你用的是32位架构,路径里的lib64会变成lib

我记得有一次,我编译完发现.so文件没生成。查了半天,原来是Android.bp里少写了vendor: true。没有这个标记,模块会被编译到系统分区,而不是vendor分区。在Treble架构下,系统分区和vendor分区的模块是隔离的,放错位置就加载不了。

避坑指南:我曾经因为relative_install_path写成了hw/(带斜杠),结果编译报错。这个字段不要带斜杠,直接写目录名就行。系统会自动拼接成/vendor/lib64/hw/

4.5 依赖管理与链接顺序

再聊一个容易踩坑的点——链接顺序。在shared_libs里,依赖库的顺序是有讲究的。虽然现代的链接器(比如lld)对顺序要求不那么严格了,但为了保险起见,我建议你把最基础的库放在前面。

比如:

shared_libs: [
    "liblog",      // 日志库,几乎所有模块都需要
    "libhardware", // HAL核心库
    "libcutils",   // 工具库
    "libutils",    // 工具库
    "libhidlbase", // HIDL基础库(如果用了HIDL)
],

如果你依赖了某个第三方库,比如libmydriver,记得把它放在最后。因为第三方库可能又依赖了系统库,放前面会导致链接器找不到符号。

4.6 调试与验证

编译完成后,怎么验证.so文件是否正确?我一般用这几个命令:

# 查看.so文件依赖了哪些库
readelf -d lights.default.so | grep NEEDED

# 查看.so文件导出了哪些符号
nm -D lights.default.so | grep HAL

# 查看.so文件是32位还是64位
file lights.default.so

如果readelf显示缺少某个依赖库,说明你的shared_libs没写全。如果nm找不到HAL_MODULE_INFO_SYM这个符号,那你的HAL模块就没法被hw_get_module加载——这是最常犯的错误之一。

核心要点:每个HAL模块的.so文件必须导出HAL_MODULE_INFO_SYM符号。这个符号是HAL框架用来查找模块入口的。如果你的模块没有这个符号,系统会直接报Failed to load HAL module

4.7 实战:从零写一个Android.bp

最后,我带你把整个流程走一遍。假设我们要移植一个虚拟的LED HAL模块:

  1. 创建源码目录:hardware/libhardware/modules/led/
  2. 编写led.cpp,实现led_openled_write等接口
  3. 编写Android.bp
cc_library_shared {
    name: "led.default",
    relative_install_path: "hw",
    srcs: ["led.cpp"],
    shared_libs: [
        "liblog",
        "libhardware",
    ],
    cflags: ["-Wall -Werror -DLOG_TAG=\"LED_HAL\""],
    vendor: true,
    proprietary: true,
}
  1. 编译:make led.default -j8
  2. 验证:检查out/target/product/generic/vendor/lib64/hw/led.default.so是否存在

你看,其实没那么复杂。Android.bp的核心就是把你脑子里的编译逻辑,用声明式的方式写出来。你只要记住:名字、源文件、依赖库、安装路径,这四样东西写对了,剩下的交给构建系统。

下一章,我们会聊HAL模块的加载机制。到时候你会看到,今天编译出来的这个.so文件,是怎么被系统找到并加载的。嗯,那才是真正有意思的部分。