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模块:
- 创建源码目录:
hardware/libhardware/modules/led/ - 编写
led.cpp,实现led_open、led_write等接口 - 编写
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,
}
- 编译:
make led.default -j8 - 验证:检查
out/target/product/generic/vendor/lib64/hw/led.default.so是否存在
你看,其实没那么复杂。Android.bp的核心就是把你脑子里的编译逻辑,用声明式的方式写出来。你只要记住:名字、源文件、依赖库、安装路径,这四样东西写对了,剩下的交给构建系统。
下一章,我们会聊HAL模块的加载机制。到时候你会看到,今天编译出来的这个.so文件,是怎么被系统找到并加载的。嗯,那才是真正有意思的部分。