有一段用户空间代码需要在内核里跑。正常做法是用内核构建系统重新编译,但那样得维护一套 Kbuild,而且代码里的用户空间惯用法改起来很痛苦。于是就冒出一个念头:能不能直接把编译好的二进制文件"转"成 .ko?直觉上这应该可行——反正 .ko 就是 ELF 可重定位文件(ET_REL),普通的 .o 编译产物也是 ET_REL。格式骨架一样,差的无非是元数据。真的上手之后,才发现坑比想象中多得多。先理清几个基本概念。ELF 文件有四种类型:关键认知:.so 是 ET_DYN,.ko 是 ET_REL。它们的 ELF 类型就不同。.so 文件是 ET_DYN(动态链接库),结构上和 .ko 有本质差异:内核加载模块的第一步就检查 ELF 类型——必须是 ET_REL,否则直接返回 -ENOEXEC。无论你怎么处理,ET_DYN 的 .so 连第一道门都过不去。这个检查在 kernel/module.c 的 elf_validity_check() 中:.so 里塞满了动态链接基础设施:.dynamic, .dynsym, .dynstr, .hash, .gnu.hash, .got, .got.plt, .plt.got, .rel.dyn, .rel.plt, .interp……这些段包含了各种不必要的信息,对内核模块加载器毫无意义,必须全部删除。.so 的符号名长这样:内核导出符号可没有这些 @ 后缀。用带后缀的名字去查内核符号表,内核在 simplify_symbols() 里调用 resolve_symbol_wait() 做严格的 strcmp 比对,当然查不到。.so 里的函数调用默认走 PLT(过程链接表),会生成大量 PLT 相关的重定位条目。内核加载器内部的 apply_relocations() 遍历所有 SHT_RELA 段逐个处理重定位,这些 PLT 重定位条目会被逐一处理,但处理逻辑和用户空间 ld.so 完全不同,结果就是错位。结论:用 gcc -c -fPIC 编译成 .o(ET_REL),直接从 ET_REL 转 ET_REL。在讲具体坑之前,先沿着内核源码(以 Linux 5.10 为例)把模块加载的完整路径走一遍。后面所有坑的根因都能在这条链路上找到。三项硬性检查:ELF 魔数、ET_REL 类型、架构匹配。任何一个不过就直接 -ENOEXEC。这就是为什么 .so 不行,同时也意味着我们不能修改 ELF header 把 ET_DYN 改成 ET_REL 了事——架构检查 elf_check_arch() 在 ARM64 上还会验证段结构的完整性。vermagic 的比对逻辑在 same_magic() 中:如果内核启用了 CONFIG_MODVERSIONS,会跳过 vermagic 里第一个空格之前的内容再做比对(因为那部分是 UTS_RELEASE)。否则就是完整的 strcmp。在分配模块内存之前,内核调用架构钩子检查和预处理段结构。ARM64 的实现(arch/arm64/kernel/module-plts.c)尤其值得关注:ARM64 的模块链接脚本同样印证了这一点(arch/arm64/include/asm/module.lds.h):内核构建工具链生成的 .ko 天然带这三个段(大小可以为 0,内容为 1 字节占位)。自己构建的 .ko 如果缺少这些段,ARM64 的 module_frob_arch_sections 直接返回 -ENOEXEC。x86_64 没有这个硬性要求。核心逻辑:所以 UND 符号的 shndx 必须是 0。如果你在转换时把它改成了 SHN_ABS(0xFFF1),它就不会进入 resolve_symbol_wait 分支,内核不会去查符号表,外部引用全部悬空。这段逻辑遍历所有段头,找出类型为 SHT_RELA 的段(重定位段),调用架构特定的 apply_relocate_add() 逐条处理。.rela.gnu.linkonce.this_module 就是在这里被处理的。 内核遍历到这个段时,把 init_module 和 cleanup_module 的最终地址写入 .gnu.linkonce.this_module 段内对应偏移处。这些偏移正是 struct module 中 init/exit 函数指针的位置。内核调用 mod->init 这个函数指针。这个指针的值是在第五步的重定位处理中填入的。如果 .rela.gnu.linkonce.this_module 段不存在或偏移量不正确,mod->init 就是 NULL,内核跳过整个初始化流程,不报任何错误。同样,模块卸载时(kernel/module.c 的 free_module() 路径):mod->exit 也是通过重定位填入的。两个函数指针,两条重定位,缺一不可。如果内核开启了 CONFIG_MODVERSIONS,每个外部符号引用都要比对 CRC:模块的 __versions 段存储了 struct modversion_info 数组(64 字节每项:CRC + 符号名)。内核逐个比对 CRC 值,不匹配则加载失败。其中 module_layout 这个符号的 CRC 实质上代表了整个 struct module 的结构签名。以上就是模块从 insmod 到 init 执行经过的全部内核关卡。接下来看转换过程中的具体坑。搞清楚内核加载路径后,我设计了两阶段流水线:**阶段一(离线转换)**在开发机上完成 ELF 结构层面的转换:删掉不需要的段、保留需要的段、补充内核元数据段、重新索引符号和重定位。所有不确定的内核参数填入占位值。**阶段二(原位修补)**在目标设备上运行。找一个目标设备上已有的、能正常加载的 .ko 作为"参考",从中提取所有内核特定参数,覆写占位值。这个设计的核心思想是:转换工具不需要知道目标内核的任何细节。 vermagic、struct module 大小、字段偏移、CRC——全部由参考 .ko 提供。以下按排查难度排序。转换的第一步:决定哪些段保留、哪些丢弃。必须丢弃的段:必须保留的段:ARM64 上必须额外创建的空段:从上面 module_frob_arch_sections() 的源码可以看到,ARM64 直接按段名查找 .plt 和 .init.plt,找不到就返回 -ENOEXEC。ARM64 的链接脚本 .lds.h 也明确定义了这三个空段。所以转换阶段必须生成:缺任何一个,内核直接拒载,错误信息只是 "module PLT section(s) missing",不给具体缺少哪个。原始重定位段必须删除。删除了部分段、重构了段索引后,旧的重定位条目引用的段索引已失效。如果新旧重定位段并存(比如两套 .rela.text),加载器在处理第二条重定位时发现目标位置已有非零值,会报 "Invalid relocation target, existing value is nonzero"。ET_REL 文件里的地址全部是段相对的:比如符号的 value 应该是类似 0x10 的值("函数入口在 .te
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-291050.htm
[原创]把 .o 变成 .ko(一):一次 ELF 格式的奇妙之旅
455 浏览
8 回复
为什么会有这个需求
mark
推荐去看一下kernelpatch项目中的kpm模块解析代码
tql
tql
感谢分享
感谢分享
感谢分享