PAYATHON 2026

EasyMockGenerate 在 x86 主机解析 ARM64 内核头文件失败,如何排查?

数据小陈

结论

EasyMock 有用于 Linux 内核驱动单元测试的公开示例,但能否直接处理所有 ARM64 厂商内核头文件,还不能由此确定。建议先让解析环境与目标内核的 Kbuild 编译环境一致,再判断剩余错误是否由生成器限制引起。内核驱动测试示例

仅凭这两条日志,无法确定唯一根因。先检查目标内核是否完成配置和头文件准备、解析器是否使用 ARM64 target,以及源码目录与构建输出目录的头文件是否配套。

两条错误分别说明什么

asm/div64.h 找不到

asm 头文件通过架构相关的 include 路径解析。有些文件由 Kbuild 生成,因此只添加内核源码的 include/ 目录并不够。

Linux 5.15 的通用头文件清单中有 div64.h。架构没有提供对应实现时,Kbuild 会生成包含 asm-generic 实现的包装头文件。因此,这条错误可能是内核构建目录尚未准备好,也可能是遗漏了构建输出中的 arch/arm64/include/generated 路径。Qualcomm 分支的具体情况应以其中的文件和构建规则为准。通用头文件清单、包装头文件生成规则

不要直接把 include/asm-generic 当作 asm 目录使用,也不要为消除报错而复制其他架构的头文件。

struct arch_tlbflush_unmap_batch 类型不完整

原始日志中的字段是按值嵌入的:

struct arch_tlbflush_unmap_batch arch;

这种声明要求编译器已经看到结构体的完整定义,只有前向声明无法满足要求。

在上游 Linux 5.15 中,该字段和 <asm/tlbbatch.h> 的包含都受 CONFIG_ARCH_WANT_BATCHED_UNMAP_TLB_FLUSH 控制。应检查配置宏与架构头文件是否一致;仅凭这一点,无法认定私有分支混用了 x86 配置。相关源码

查看当前分支中该类型的定义、条件编译分支和实际包含的文件。不要临时补一个空结构体,也不要随意取消某个 CONFIG_* 宏来掩盖错误。

1. 准备正确的目标内核构建目录

使用驱动对应的 Qualcomm MSM 源码、ARM64 配置和厂商要求的工具链。确认目标 .config 已生成,再按项目的构建方式运行 prepare 或 modules_prepare。

内核文档将 modules_prepare 作为准备外部模块构建环境的方法,它不会生成完整的 Module.symvers。针对当前的头文件解析问题,应先检查配置和生成的头文件。Linux 5.15 外部模块构建文档

如果通过 O= 分离构建目录,必须同时使用源码树和与之对应的输出树。例如,重点检查:

源码树:
  arch/arm64/include/
  include/
  include/linux/kconfig.h

构建输出树:
  arch/arm64/include/generated/
  include/generated/
  include/generated/autoconf.h

不要使用宿主机 /lib/modules/$(uname -r)/build 中的 x86 配置替代目标 ARM64 配置,也不要将仅供用户空间使用的导出头文件当作完整的内核内部头文件环境。

2. 从真实 Kbuild 命令提取解析参数

编译实际驱动时启用 V=1,记录一个正常编译单元的完整命令。Linux 5.15 的 Makefile 明确支持通过这一参数显示完整命令。Kbuild Makefile

重点保留:

  • 目标架构参数,例如 --target=aarch64-linux-gnu,以及项目实际使用的 CPU、ABI 参数。
  • 所有 -I、-isystem 和强制包含的 -include,保持搜索顺序一致。
  • __KERNEL__、必要时的 MODULE,以及该编译单元的其他宏定义。
  • C 语言模式、语言标准和会影响类型或语法的选项。

删除原始命令中的源文件、-c、-o 和依赖文件输出参数,再用剩余参数进行解析测试。遇到不兼容选项时,先判断它是否影响预处理或类型语义。

ARCH=arm64 是 Kbuild 变量,单独设置它不会让独立调用的 Clang 自动切换 target。Clang 12 文档说明,未指定 target 时会使用宿主机目标。Clang 12 交叉编译文档

3. 先用 Clang 单独复现

先创建一个最小解析文件,每次只验证一个头文件:

/* kernel_probe.c */
#include <linux/mutex.h>

下面的 Bash 参数模板说明源码路径、输出路径和 target 的关系。它不是 Qualcomm MSM 分支的完整编译命令,需要根据真实的 Kbuild 参数补齐或调整。

KERNEL_SRC=/path/to/kernel-source
KERNEL_OUT=/path/to/kernel-output

KERNEL_PARSE_FLAGS=(
  --target=aarch64-linux-gnu
  -x c
  -std=gnu89
  -D__KERNEL__
  -DMODULE
  -nostdinc
  -isystem "$(clang-12 -print-resource-dir)/include"
  -I"$KERNEL_SRC/arch/arm64/include"
  -I"$KERNEL_OUT/arch/arm64/include/generated"
  -I"$KERNEL_SRC/include"
  -I"$KERNEL_OUT/include"
  -I"$KERNEL_SRC/arch/arm64/include/uapi"
  -I"$KERNEL_OUT/arch/arm64/include/generated/uapi"
  -I"$KERNEL_SRC/include/uapi"
  -I"$KERNEL_OUT/include/generated/uapi"
  -include "$KERNEL_SRC/include/linux/kconfig.h"
  -include "$KERNEL_SRC/include/linux/compiler_types.h"
)

clang-12 "${KERNEL_PARSE_FLAGS[@]}" \
  -fsyntax-only kernel_probe.c

clang-12 "${KERNEL_PARSE_FLAGS[@]}" \
  -E -H kernel_probe.c > kernel_probe.i 2> includes.log

查看 includes.log,确认 asm 头文件来自 ARM64 源码目录或对应输出目录,且没有混入宿主机架构的头文件。再检查预处理结果,确认相关配置宏选择了预期分支。

遇到类型不完整错误时,先处理更早出现的错误。缺失头文件可能引发后续多条类型和宏诊断。

4. 将相同解析参数传给 EasyMockGenerate

EasyMock 的文档说明,生成器会把未识别的编译参数交给解析器。确认本地版本的帮助信息支持相关选项后,可以使用同一组参数:

MOCK_OUT=/path/to/generated-mocks

EasyMockGenerate \
  -i "$KERNEL_SRC/include/linux/mutex.h" \
  -o "$MOCK_OUT" \
  "${KERNEL_PARSE_FLAGS[@]}"

如果 Clang 能解析,生成器却仍然失败,再核对生成器实际链接的 LLVM/Clang 库版本、工作目录和传入参数。命令行安装了 Clang 12,不能说明生成器使用的解析库也是该版本。

如果需要减少生成代码对原始头文件的依赖,可以评估 --generate-types,但输入仍须先解析成功。--mock-only 可以缩小生成函数范围,无法保证避开整份头文件的解析错误。不要将 --ignore-parser-error 当作修复方法:项目文档明确指出,它可能产生无法编译的 mocks。[EasyMock 参数说明](https://github.com/lcarlier/EasyMock# create-a-mock)

在 CMake 中通过 add_custom_command(COMMAND ...) 启动生成器时,要显式传入这些参数;测试 target 的 target_include_directories() 和 target_compile_options() 不会自动成为外部生成器命令的参数。参数宜使用 CMake 列表,并设置 VERBATIM;使用相对路径时,还要固定正确的工作目录。

5. 区分解析成功与测试能否在宿主机运行

按 ARM64 环境解析头文件,只解决生成阶段的一部分问题。最终若要在 x86_64 用户空间运行测试,仍需处理架构汇编、内核运行环境以及实际调用入口。

对于宏和 static inline,需要先确认驱动最终调用的是哪个函数,不能假定生成同名外部函数就一定能够替换它。

如果直接解析 linux/workqueue.h 等头文件引入过多依赖,可以为驱动增加一个窄接口,把内核调用集中到适配层。以下接口仅为设计示例:

/* driver_os.h */
struct driver_lock;

void driver_os_lock(struct driver_lock *lock);
void driver_os_unlock(struct driver_lock *lock);

生产实现调用内核 API,宿主机测试则 mock driver_os_lock() 和 driver_os_unlock()。这种方式适合检查驱动业务逻辑与调用行为;实际锁竞争、内存屏障及 workqueue 调度语义仍需在相应内核环境验证。

如果问题仍然存在,最有用的补充材料是脱敏后的完整生成器命令、首次错误及包含链、源码与输出目录布局、相关配置项,以及生成器版本和 LLVM/Clang 库版本。通常无需公开整个私有驱动。

备注:内容仅供参考。