Android HAL 与 HIDL 开发笔记

Android 真的是开源的吗?

前言

之前分析过 Android 系统中的进程间通信逆向,即基于 Binder 拓展的以及 AIDL 描述的 IPC。了解 Android 系统的话应该知道在 8.0 之后,/dev/binder 拓展多出了两个域,分别是 /dev/hwbinder/dev/vndbinder 。其中 hwbinder 主要用于 HIDL 接口的通信,而 vndbinder 则是专注于 vendor 进程之间的 AIDL 通信。

本文主要关注的是硬件部分。具体来说,就是作为一个 OEM/ODM 厂商,如何将自己的硬件添加到自己的 ROM 之中;以及作为一个安全工程师,如何对厂商的硬件驱动进行(逆向)分析。其实这两个问题的本质是一致的,即要求了解 Android 硬件开发和集成流程。

HAL

HAL 是 Hardware Abstraction Layer 的缩写,即硬件抽象层。从碎片化角度来说,作为系统的设计者,肯定是希望底层硬件按照类型整齐划一,而不是 Boardcom 实现一套、TI、ESP 又自己实现一套自己的 WIFi 接口;从商业角度说,硬件厂商自己硬件的软件(驱动)也是视为传家宝一样不希望被别人分析,所以要求操作系统可以无视自己的底层实现,只需要协商出统一的交互协议。

不论如何,多方交织的结果就是中间多了一层抽象。对于 Android 系统来说,这层抽象就是 HAL,虽然这并不是 Android 独有的概念。简而言之,Android HAL 就是定义了一个 .h 接口,并由硬件厂商拓展实现为动态链接库 .so,并使用约定的方式去加载和调用。

现在的时间已经来到了 Android 11,其实早在 Android 8 之后就已经弃用了曾经的 HAL 方式,不过由于碎片化原因,现在还有许多 IoT 设备等还是使用传统的 HAL 模式。另外出于对历史进展的研究,了解传统 HAL 也是有必要的。

传统 HAL (Legacy HALs) 的接口文件为 hardware/libhardware/include/hardware/hardware.h ,主要定义了三个结构,分别是:

struct hw_module_t;
struct hw_module_methods_t;
struct hw_device_t;

硬件模块 (hardware module) 表示 HAL 中打包的实现,即输出的.so动态链接库文件。hw_module_t 结构中主要包括 tab、version、name、author 等信息字段以及一个 struct hw_module_methods_t *methods 字段。methods 中包括打开设备的函数指针,如下:

typedef struct hw_module_methods_t {
    /** Open a specific device */
    int (*open)(const struct hw_module_t* module, const char* id,
            struct hw_device_t** device);

} hw_module_methods_t;

每个硬件模块动态库中都需要定义一个符号 HAL_MODULE_INFO_SYM,并且该符号的第一个字段是 hw_module_t 类型。也就是说,厂商可以拓展 hw_module_t 类型,增加自己的额外字段。比如某个摄像头硬件所定义的结构如下:

typedef struct camera_module {
    hw_module_t common;
    int (*get_number_of_cameras)(void);
    int (*get_camera_info)(int camera_id, struct camera_info *info);
} camera_module_t;

这也是使用 C 语言实现继承的一种典型方式。

device 用于抽象产品的某个具体硬件,比如对于某些摄像头模组,其硬件模块中就可能包括 2D 摄像头、3D 深度摄像头、红外摄像头等具体的 device。设备的结构基本元素如下:

/**
 * Every device data structure must begin with hw_device_t
 * followed by module specific public methods and attributes.
 */
typedef struct hw_device_t {
    /** tag must be initialized to HARDWARE_DEVICE_TAG */
    uint32_t tag;

    /**
     * Version of the module-specific device API. This value is used by
     * the derived-module user to manage different device implementations.
     *
     * The module user is responsible for checking the module_api_version
     * and device version fields to ensure that the user is capable of
     * communicating with the specific module implementation.
     *
     * One module can support multiple devices with different versions. This
     * can be useful when a device interface changes in an incompatible way
     * but it is still necessary to support older implementations at the same
     * time. One such example is the Camera 2.0 API.
     *
     * This field is interpreted by the module user and is ignored by the
     * HAL interface itself.
     */
    uint32_t version;

    /** reference to the module this device belongs to */
    struct hw_module_t* module;

    /** padding reserved for future use */
#ifdef __LP64__
    uint64_t reserved[12];
#else
    uint32_t reserved[12];
#endif

    /** Close this device */
    int (*close)(struct hw_device_t* device);

} hw_device_t;

和模块一样,厂商也是通过继承拓展 device 结构来实现具体的设备。除了上面这些简单的标准属性,其实对于不同种类的硬件,也有特定的数据结构类型,见 Android HAL Reference。例如,对于摄像头类型的硬件,在 hardware/camera.h 中定义了其标准拓展接口和数据类型,比如打开/关闭摄像头、设置参数、数据回调等等。

HIDL

HAL 是最初的硬件抽象方案,在 Android 8 中已经废弃并被 HIDL 取代。HIDL 和 AIDL 类似,都是一种接口描述语言 (HAL interface definition language),用来描述硬件的接口。HIDL 设计的初衷是更新 frameworks 时避免重新编译 HAL,后者可以由厂商单独编译并在 vendor 分区中单独更新,此外还支持完善的版本管理。

为了了解开发流程,我现在就是一个厂商的 BSP 工程师。这里假设要创建一个名为 demo 的硬件驱动,并且以华为的 Nexus 6P 为例进行开发。这里不赘述编译 AOSP 的具体过程,只专注于 HIDL 相关部分。

首先是创建 HAL 硬件抽象描述文件。

mkdir -p hardware/interfaces/demo/1.0/default
touch hardware/interfaces/demo/1.0/IDemo.hal

其中 Demo.hal 内容如下:

package android.hardware.demo@1.0;

interface IDemo {
    foo(string name) generates (string result);
    bar(int32_t a, int32_t b) generates (int32_t sum);
    baz();
};

详细的 HAL 语法见: https://source.android.com/devices/architecture/hidl/code-style

PACKAGE=android.hardware.demo@1.0
LOC=hardware/interfaces/demo/1.0/default/
# 生成 Demo.h / Demo.cpp 这两个文件为 Server 端实现
hidl-gen -o $LOC -Lc++-impl -randroid.hardware:hardware/interfaces \
    -randroid.hidl:system/libhidl/transport $PACKAGE
# 生成 Android.bp 文件
hidl-gen -o $LOC -Landroidbp-impl -randroid.hardware:hardware/interfaces \
    -randroid.hidl:system/libhidl/transport $PACKAGE

我们需要做的就是完成 Demo.cpp 的实现,赋予其具体的功能。

值得一提的是,由于 HIDL 是从 HAL 迁移过来的,因此为了平复厂商的心情方便慢慢移植,实现时支持 passthrough 模式,直接加载之前的 libdemo.so 完成实现。当然如果是新的硬件,还是建议将代码移植到 impl 中,这样的实现是 Binderized 的,即通过 IPC 进行调用。这里我们采用后者。

#include "Demo.h"
#include <iostream>

namespace android {
namespace hardware {
namespace demo {
namespace V1_0 {
namespace implementation {

// Methods from IDemo follow.
Return<void> Demo::foo(const hidl_string& name, foo_cb _hidl_cb) {
    std::cout << "Demo::foo()" << std::endl;
    _hidl_cb(name);
    return Void();
}

Return<int32_t> Demo::bar(int32_t a, int32_t b) {
    std::cout << "Demo::bar()" << std::endl;
    return int32_t { a + b };
}

Return<void> Demo::baz() {
    std::cout << "Demo::baz()" << std::endl;
    return Void();
}


// Methods from ::android::hidl::base::V1_0::IBase follow.

//IDemo* HIDL_FETCH_IDemo(const char* /* name */) {
//    return new Demo();
//}

}  // namespace implementation
}  // namespace V1_0
}  // namespace demo
}  // namespace hardware
}  // namespace android

在创建文件时由于使用了 default 子目录,还需要更新接口的 Android.bp/mk,以便使用 AOSP 的 mmm 编译。

hardware/interfaces/update-makefiles.sh
mmm hardware/interfaces/demo

编译成功后,生成了两个我们主要关注的 so 库文件:

out/target/product/angler/system/lib/android.hardware.demo@1.0.so
out/target/product/angler/system/lib64/android.hardware.demo@1.0.so

out/target/product/angler/vendor/lib/hw/android.hardware.demo@1.0-impl.so
out/target/product/angler/vendor/lib64/hw/android.hardware.demo@1.0-impl.so

其中:

  • android.hardware.demo@1.0.so 是接口 so,由客户端使用;
  • android.hardware.demo@1.0-impl.so 是实现的 so,由服务端使用;

编译的规则可以参考生成的 Android.bp 文件。

有了动态库,我们就可以编写实际的服务程序了。由于服务端使用的是 impl.so,那么就把服务端的代码也在 Demo.cpp 相同的目录中实现。首先是 service.cpp:

#define LOG_TAG "android.hardware.demo@1.0-service"

#include <android/hardware/demo/1.0/IDemo.h>
#include <hidl/HidlTransportSupport.h>

#include "Demo.h"

using android::hardware::demo::V1_0::IDemo;
using android::hardware::demo::V1_0::implementation::Demo;
using android::hardware::configureRpcThreadpool;
using android::hardware::joinRpcThreadpool;
using android::sp;
using android::status_t;

int main() {
    // This function must be called before you join to ensure the proper
    // number of threads are created. The threadpool will never exceed
    // size one because of this call.
    configureRpcThreadpool(1 /*threads*/, true /*willJoin*/);

    sp<IDemo> demo = new Demo();
    const status_t status = demo->registerAsService();
    if (status != ::android::OK) {
        return 1; // or handle error
    }

    // Adds this thread to the threadpool, resulting in one total
    // thread in the threadpool. We could also do other things, but
    // would have to specify 'false' to willJoin in configureRpcThreadpool.
    joinRpcThreadpool();
    return 1; // joinRpcThreadpool should never return
}

直接修改 bp 文件,增加一个 cc_binary 入口:

cc_binary {
    name: "android.hardware.demo@1.0-service",
    defaults: ["hidl_defaults"],
    proprietary: true,
    relative_install_path: "hw",
    srcs: ["service.cpp"],
    init_rc: ["android.hardware.demo@1.0-service.rc"],
    shared_libs: [
        "libhidlbase",
        "libhidltransport",
        "libutils",
        "liblog",
        "android.hardware.demo@1.0",
        "android.hardware.demo@1.0-impl",
    ],
}

生成的可执行文件如下所示:

out/target/product/angler/vendor/bin/hw/android.hardware.demo@1.0-service

如果需要持久化的话,可以增加一个 rc 文件进行开机启动,在后面介绍 SELinux 的时候再详细说。

由于主要实现都在服务端中,因此客户端代码相对简单。

#define LOG_TAG "TEST_CLINET"
#include <android/hardware/demo/1.0/IDemo.h>
#include <log/log.h>

using android::hardware::demo::V1_0::IDemo;
using android::sp;
using android::hardware::hidl_string;

int main(){
    sp<IDemo> demo = IDemo::getService();
    if( demo == nullptr ){
        ALOGE("Can't find IDemo service...");
        return -1;
    }
    printf("found service @ %p\n", demo.get());
    demo->foo("test_client", [&](hidl_string result) {
        printf("ret: %s\n", result.c_str());
    });
    int ret = demo->bar(3, 4);
    printf("3 + 4 = %d\n", ret);
    demo->baz();
    return 0;
}

Android.bp 文件:

cc_binary {
    name: "test_client",
    relative_install_path: "hw",
    defaults: ["hidl_defaults"],
    proprietary: true,
    srcs: ["test_client.cpp"],

    shared_libs: [
        "libhidlbase",
        "libhidltransport",
        "libhwbinder",
        "libutils",
        "libcutils",
        "liblog",
        "android.hardware.demo@1.0",
    ],
}

编译成功后可以直接在 Android 中以 root 权限运行,如果是非 root 环境则会遇到一些权限错误,主要是 SELinux 相关的问题,因此需要配置好对应的 sepolicy。

在非测试版本中,SELinux 的权限可能导致服务端无法注册或者客户端无法和服务端进行交互,因此需要添加对应的标签和权限。

添加 rc 文件的目的是让硬件服务可以开机启动,并且设置好对应的启动权限,这里的rc 文件路径为: /vendor/etc/init/android.hardware.demo@1.0-service.rc

service demo_hal_service /vendor/bin/hw/android.hardware.demo@1.0-service
    class hal
    user root
    group root
    seclabel u:r:demo:s0
    setenv LD_LIBRARY_PATH /vendor/lib64/hw

更多 initrc 的语法见 system/core/init/README.md

创建一个新的device/huawei/angler/sepolicy/hal_demo.te文件,内容如下:

# demo service
type demo, domain;
type demo_exec, exec_type, file_type;

init_daemon_domain(demo)

这是一个初始化的模板,新的 SELinux 规则可以添加到后面,一个方便搜集新规则的方式是先以 Permissive 模式启动,并通过 AOSP 提供的 audit2allow 等辅助脚本进行分析。

由于 sepolicy 在内存文件系统中,因此不能直接进行持久化修改,需要重新打包 boot.img 并刷机。

make bootimage

device/huawei/angler/sepolicy/file_contexts文件中新增一行:

# Demo hal
/vendor/bin/hw/android\.hardware\.demo@1\.0-service     u:object_r:demo_exec:s0

确保可执行文件被正确地打上对应的 SELinux Label。

在测试阶段,最好先修改platform/device/<vendor>/<target>/BoardConfig.mk文件,将系统设置为 Permissive 模式,等到 SELinux 相关规则添加完成后再恢复成 Enforcing。

BOARD_KERNEL_CMDLINE += androidboot.selinux=permissive

修改完后重新编译 boot.img 并烧写测试。由于华为的 vendor.img 是私有的,所以烧写完后重新 mount 修改就行了,不需要重新打包。

更新镜像并重启系统后,可以看到生效的 SELinux 规则:

$ ls -lZ /vendor/bin/hw/ | grep demo
-rwxr-xr-x 1 root shell u:object_r:demo_exec:s0                             11032 2020-10-28 03:37 android.hardware.demo@1.0-service
$ ps -ef -Z | grep demo
u:r:demo:s0                    system         412     1 0 00:54:18 ?     00:00:00 android.hardware.demo@1.0-service

VINTF (vendor interface object) 的作用是用来收集设备信息并生成可查询的API,使用 XML 格式表示。其中 device manifest 用来声明当前固件的接口,如下所示。

/vendor/manifest.xml:

    <hal format="hidl">
      <name>android.hardware.demo</name>
      <transport>hwbinder</transport>
      <version>1.0</version>
      <interface>
        <name>IDemo</name>
        <instance>default</instance>
      </interface>
    </hal>

网上一些文章说如果没有在 manifest.xml 中定义,client 端是无法获取到 service 的,但是实际测试时仅仅遇到权限的问题,详见 VINTF/device manifest

小结

本文介绍了 Android 中最为常见的两种硬件接口,传统 HAL 和 HIDL。其中 HAL 在 Android 8 中弃用,取而代之的是基于 IPC 的 HIDL 方案,后者同时支持 passthrough 模式兼容传统的 HAL,这也是很多厂商移植前的临时过渡方案。虽然使用 IPC 会在一定程度影响性能,但 HIDL 方案提供了许多优化的措施,比如通过共享内存快速消息队列(FMQ)进行数据交互。此外,我们还基于 HIDL 编写了一个简单的 demo 驱动以及配套的 service 和 client 示例,便于理解硬件创建和调用的流程,这对于固件驱动逆向而言也是必要的知识。