PAYATHON 2026

为何 pahole 与内核模块 offsetof 得到的 netlink_sock 偏移不同?

数据阿明

结论

在宿主机上显式指定目标 vmlinux 时,宿主机运行的内核版本通常不会改变 pahole 解析出的结构体布局。pahole 读取与文件关联的类型调试信息,不会根据宿主机的内核头文件重新计算偏移。pahole 官方手册

仅凭这两组输出,无法判断 688 或 792 是否为运行内核的真实偏移。需要核对以下三项是否来自同一次内核构建:

  • 正在运行的内核。
  • pahole 读取的 vmlinux 及其调试信息。
  • 编译模块时使用的源码、头文件、生成头文件和配置。

模块加载后打印的 offsetof 仍是模块编译时确定的常量。它不会在运行时向内核查询结构体定义,所以模块能加载并输出 688,不能单独证明运行内核采用这一布局。

为什么会出现 792 与 688 两种结果?

netlink_sock 的第一个成员是内嵌的 struct sock sk。在所引用的上游 Linux 3.16 定义中,portid 紧接在 sk 后面。因此,struct sock 的大小以及后续成员的对齐要求都会影响 portid 的偏移。Linux v3.16 源码

优先排查以下情况:

  1. vmlinux 或调试文件不匹配。文件名相同、来自同一台虚拟机,甚至版本字符串相同,也不能证明文件对应当前启动的那次构建。
  2. 模块使用了不同的类型定义。模块可能包含另一套内核源码中的内部头文件,或者手工复制了 netlink_sock 的定义。
  3. 源码和构建配置不同。架构、发行版补丁、配置以及生成头文件可能改变 struct sock 等嵌入类型的布局。
  4. 输出上下文被省略。-E 会展开嵌入类型,单独使用 grep 可能把不同嵌套层级的成员混在一起看。

现有材料不足以确定具体是哪一种情况。

flags 为什么出现在 portid 前面?

先查看未展开嵌入类型的完整输出:

pahole -C netlink_sock /absolute/path/to/vmlinux

检查 flags 所在的大括号层级。pahole -E 会展开成员类型,展开后出现在前面的字段可能属于内嵌结构体,不能只凭显示位置判断它是 netlink_sock 的直接成员。-C 用于选择类型,-E 用于展开类型。pahole 参数说明

如果完整的非展开输出确认 flags 和 portid 都是 netlink_sock 的直接成员,而且 flags 确实在前,那么这个结果与所引用的上游 v3.16 定义不一致。此时需要先检查实际构建源码和调试信息来源,再考虑工具解析问题。

普通的成员对齐和填充可以改变偏移,但不能颠倒所引用定义中的直接成员顺序。

对于上游 Linux v3.16,不能直接用它解释。

该版本中,CONFIG_NETLINK_MMAP 控制的成员位于 netlink_sock 的尾部,在 portid 和 flags 之后。这个条件块可以影响结构体总大小,但不会直接把前面的 portid 向后移动,也不会把直接成员 flags 放到 portid 前面。Linux v3.16 条件成员定义

其他配置仍可能通过改变前置的 struct sock 等类型影响偏移。如果使用发行版内核,还应以该发行版对应构建的源码为准,不能仅凭上游版本号推断布局。

如何核对文件和运行内核?

1. 在目标虚拟机记录内核身份

uname -r
uname -m
cat /proc/version

如果系统提供对应文件,再记录其配置:

cat /boot/config-$(uname -r)

这些信息有助于定位构建,但版本字符串本身不能作为最终匹配证据。应尽量取得当前内核软件包对应的完整版本、调试包和源码包;如果构建提供 GNU Build ID,还要核对运行内核的 Build ID 与候选调试文件是否一致。

2. 确认候选文件包含可用调试信息

在保存目标文件的机器上执行:

file /absolute/path/to/vmlinux
readelf -n /absolute/path/to/vmlinux
readelf -S /absolute/path/to/vmlinux

检查 ELF 架构、可用的 Build ID,以及 .debug_info 等调试节或外部调试文件关联。

vmlinux 这个文件名不能保证文件包含类型信息。即使从压缩启动镜像中提取 ELF,也无法恢复构建时未保留或后来被剥离的 DWARF。pahole 需要文件内或关联文件中的调试信息。pahole 调试信息要求

针对这个 Linux 3.16 问题,应优先寻找匹配的 DWARF 调试文件,不要假定目标系统具有现代内核常见的 BTF 接口。

pahole -C sock /absolute/path/to/vmlinux
pahole -C netlink_sock /absolute/path/to/vmlinux

保留完整输出,查看 sk 的大小、portid 的偏移和各直接成员的顺序。暂时不要用 grep 截断上下文。

将同一个文件复制到另一台机器时,可以比较文件摘要,确认传输前后字节一致:

sha256sum /absolute/path/to/vmlinux

摘要一致只能证明两份文件一致,不能证明它们与运行内核匹配。

如何检查模块中的 offsetof?

检查模块实际使用的 struct netlink_sock 定义来自哪个文件。这是内核内部类型,复制一份看起来相同的结构体声明无法保证匹配。

在已有诊断模块的初始化函数中,可以增加以下只读输出;前提是模块已经包含来自目标构建源码的真实类型定义,并具有相应的 offsetof 和日志声明:

pr_info("sizeof(struct sock)=%zu\n", sizeof(struct sock));
pr_info("sizeof(struct netlink_sock)=%zu\n",
        sizeof(struct netlink_sock));
pr_info("offsetof(sk)=%zu\n",
        offsetof(struct netlink_sock, sk));
pr_info("offsetof(portid)=%zu\n",
        offsetof(struct netlink_sock, portid));
pr_info("offsetof(flags)=%zu\n",
        offsetof(struct netlink_sock, flags));

用与目标内核匹配的构建目录重新编译模块,并检查编译命令中的头文件搜索路径。如果内部头文件另取自完整源码树,该源码树也必须与目标构建一致。

这组结果有助于判断差异来自 struct sock 的大小,还是 netlink_sock 的定义,但它仍然反映模块编译时看到的布局。

如何交叉验证 pahole?

可以让 GDB 读取同一个调试文件,独立查看类型并计算成员偏移,无需连接正在运行的内核:

gdb -q -batch /absolute/path/to/vmlinux \
  -ex 'ptype struct netlink_sock' \
  -ex 'p/d sizeof(struct sock)' \
  -ex 'p/d (unsigned long)&((struct netlink_sock *)0)->portid'

这里的空指针表达式用于计算类型中的成员偏移,不是在访问目标内存。

  • 如果 GDB 与 pahole 都得到 792,说明两者对该调试文件的解释一致,应继续核对文件与模块编译环境。
  • 如果两者结果不同,再检查类型选择、调试信息完整性以及工具版本,并用原始 DWARF 进一步定位。
  • 如果确认所有输入都对应同一次构建,结果仍不一致,才有充分理由继续调查工具解析缺陷。

最终要确认运行内核、匹配的调试信息和模块编译环境使用同一套布局。在完成核对前,不应将 688 或 792 视为可跨构建复用的固定偏移,也不应靠写入数据来判断哪个工具正确。

备注:内容仅供参考。