将-mcmodel=kernel编译的内核目标文件链接到用户态:GS段问题求解
我有一个针对Intel x86_64架构的Linux内核外置模块,源码通过标准内核模块构建流程编译为.o目标文件,再链接成.ko可加载模块:
foo.c → foo.o → foo.ko // 执行make -C <tree> M=<dir>完成
现在要为模块源码中的函数编写并运行用户态单元测试,我提供了模块依赖的模拟实现(确保测试在CPL3环境下不调用CPL0特权指令),并与测试框架入口链接生成可执行文件:
foo.o + test_mocks.c + test_entrypoint.c → foo_test // 由CMake配置构建
重要说明:foo.o直接复用内核模块构建步骤生成的文件,与生产环境使用的二进制代码完全一致。
但测试程序foo_test在调用foo.o中第一个函数的序言阶段就崩溃了,调试发现是指令mov %gs:0x28,%rax触发段错误——GS段未设置(值为0),导致目标地址无映射。函数尾声处还有sub %gs:0x28,%rdx指令,这些操作类似线程本地存储,但本该使用FS段而非GS段。
经排查内核构建过程的编译器选项,发现是-mcmodel=kernel导致使用GS段而非FS段。GCC文档仅提及该选项影响地址范围,未说明段选择的变化:
-mcmodel=kernel
生成适用于内核代码模型的代码。内核运行在地址空间的负2GB区域,该模型必须用于Linux内核代码。
我已验证,编译单个.c文件时移除该选项,目标文件中的%gs会全部替换为%fs。
核心问题:如何让模块文件中的函数能被正常调用而不崩溃?
我考虑过的几种方案:
- 重新构建适用于用户态的
foo.o目标文件,但内核构建环境的特殊配置(如-nostdinc、大量-isystem、重定义true和false等)会带来麻烦,且无法测试生产环境实际使用的二进制代码。 - 在测试初始化阶段将GS段设置为当前FS段的值,避免
mov %gs指令崩溃?测试中不打算使用线程本地存储,假设FS段在程序生命周期内保持不变,能否完全在用户态实现? - 让测试二进制的编译器/链接器知晓代码应使用GS段而非FS段?如何实现?
- 使用其他测试框架简化操作?我仅了解KUnit,但它相对我的需求过于复杂,为调用模块函数而重新配置、构建和运行内核并不合理,且无法为测试场景提供精准的测试替身(如内存分配失败、特定硬件勘误行为、特定调度序列等)。
可行解决方案
方案2:用户态初始化时映射GS段到FS段(推荐)
完全可以在用户态实现GS段的设置,无需修改内核或重新编译模块。x86_64用户态下,可通过arch_prctl系统调用读取FS段基地址,再同步到GS段。
具体实现步骤:
- 在测试入口的初始化代码中添加如下逻辑:
#include <sys/arch.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> void setup_gs_segment(void) { uint64_t fs_base; // 获取当前FS段的基地址 if (arch_prctl(ARCH_GET_FS, &fs_base) != 0) { perror("Failed to get FS base"); exit(EXIT_FAILURE); } // 将GS段的基地址设置为FS段的基地址 if (arch_prctl(ARCH_SET_GS, fs_base) != 0) { perror("Failed to set GS base"); exit(EXIT_FAILURE); } } - 在调用任何
foo.o中的函数之前,先调用setup_gs_segment()。
该方案优势:
- 完全复用生产环境的
foo.o二进制,保证测试真实性 - 实现简单,无需修改构建流程或编译器选项
- 满足测试中不使用线程本地存储的假设(GS和FS指向同一区域,函数中的GS访问不会触发错误)
方案3:让测试二进制适配GS段的使用
如果测试需要保留GS段独立使用,可通过编译器和链接器选项强制用户态代码使用GS段作为TLS段,但需修改CMake配置:
- 编译测试代码时添加
-mtls-segment=gs选项(GCC 10+支持),强制编译器生成使用GS段的TLS代码 - 链接时确保TLS段正确映射
但这种方式可能导致测试代码与模块代码的TLS访问冲突,兼容性不如方案2,不推荐作为首选。
方案1的优化思路(不推荐)
若一定要重新构建用户态版本的foo.o,可提取内核构建的编译选项,剔除-mcmodel=kernel和-nostdinc等内核特有选项,再用调整后的选项编译foo.c。但需要维护两套构建配置,且无法保证与生产二进制的一致性,因此不推荐。
方案4:替代测试框架
除KUnit外,可考虑User Mode Linux (UML) 或QEMU用户态模拟,但这类方案仍需模拟内核环境,复杂度较高,不如方案2直接高效。若需要精准测试替身,方案2配合自定义Mock实现已足够满足需求。
内容的提问来源于stack exchange,提问作者Grigory Rechistov

