x86_64 Linux内核模块汇编拆解及链接加载技术疑问
背景
我正在学习x86_64系统中栈帧的工作机制,以下讨论基于启用帧指针(CONFIG_FRAME_POINTER=y、CONFIG_UNWINDER_FRAME_POINTER=y,基于x86_64_defconfig)的Linux 6.5.12内核模块,初衷是掌握手动栈展开方法。
测试内核模块中的示例函数:
static void dummy_function_1(int arg1, char arg2) { printk(KERN_INFO "dummy_function_1: arg1=%d, arg2=%c\n", arg1, arg2); dummy_function_2("Hello", (struct dummy_struct){.a = arg1, .b = arg2}); }
首次编译反汇编结果(默认优化)
使用gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0默认编译后,反汇编得到:
0000000000000130 <dummy_function_1.constprop.0>: 130: 55 push %rbp 131: ba 78 00 00 00 mov $0x78,%edx 136: be 2a 00 00 00 mov $0x2a,%esi 13b: 48 c7 c7 00 00 00 00 mov $0x0,%rdi 142: 48 89 e5 mov %rsp,%rbp 145: e8 00 00 00 00 call 14a <dummy_function_1.constprop.0+0x1a> 14a: 48 bf 2a 00 00 00 78 movabs $0x780000002a,%rdi 151: 00 00 00 154: e8 77 ff ff ff call d0 <dummy_function_2.constprop.0>
初始疑问
- 汇编中为何没有
printk调用?实际加载模块时能正常输出:[ 36.823046] dummy_function_1: arg1=42, arg2=x - 为何存在
mov $0x0,%rdi指令? - 为何没有将
dummy_function_2的3个参数分别放入%rdi、%rsi、%rdx寄存器,而是仅在调用前执行movabs $0x780000002a,%rdi?
优化编译后的反汇编结果(-Og)
按照建议使用-Og标志重新编译模块,并用objdump -d -r dummy_module.ko反汇编,得到:
0000000000000120 <dummy_function_1>: 120: 55 push %rbp 121: 48 89 e5 mov %rsp,%rbp 124: 41 54 push %r12 126: 53 push %rbx 127: 89 fb mov %edi,%ebx 129: 41 89 f4 mov %esi,%r12d 12c: 40 0f b6 d6 movzbl %sil,%edx 130: 89 fe mov %edi,%esi 132: 48 c7 c7 00 00 00 00 mov $0x0,%rdi 135: R_X86_64_32S .rodata.str1.8+0x118 139: e8 00 00 00 00 call 13e <dummy_function_1+0x1e> 13a: R_X86_64_PLT32 _printk-0x4 13e: 45 0f b6 e4 movzbl %r12b,%r12d 142: 49 c1 e4 20 shl $0x20,%r12 146: 89 de mov %ebx,%esi 148: 4c 09 e6 or %r12,%rsi 14b: 48 c7 c7 00 00 00 00 mov $0x0,%rdi 14e: R_X86_64_32S .rodata.str1.1+0x24 152: e8 79 ff ff ff call d0 <dummy_function_2>
新增疑问
有观点称“关于printk调用,链接器尚未填充实际地址”,但我是对最终的.ko文件执行objdump,理论上链接器已完成运行,printk符号偏移应已插入,烦请纠正我的理解,我可能需要重新梳理链接/加载流程。
解答
1. 首次反汇编中无printk调用的原因
默认编译时GCC启用了较高优化级别(通常是-O2),编译器进行了常量传播优化:从输出日志看arg1=42(0x2a)、arg2='x'(0x78),这两个值被识别为常量,所以编译器直接将printk的格式化字符串和参数替换为最终输出的常量内容,甚至可能直接内联或优化掉printk调用,转而生成等效的输出操作。函数名后的.constprop.0后缀就是GCC标记该函数经过了常量传播优化的标志。
2. mov $0x0,%rdi指令的作用
%rdi是x86_64 System V调用约定中的第一个参数寄存器。这里的0x0是占位符,实际对应printk的第一个参数——格式化字符串的地址。在默认优化的反汇编中,由于常量传播,格式化字符串的地址被优化为0(或未显示);而在-Og编译的结果中,该位置带有重定位标记R_X86_64_32S .rodata.str1.8+0x118,说明这是一个指向只读数据段中字符串的地址占位,链接/加载时会被填充为实际地址。
3. dummy_function_2参数的奇怪处理
同样是常量传播优化的结果:dummy_function_2的第二个参数是临时struct dummy_struct,其成员.a=42、.b='x'都是常量,编译器直接将这个结构体的值合并为一个64位常量0x780000002a(低32位是0x2a即.a,高32位的低8位是0x78即.b,其余位补0),并简化了参数传递逻辑。而在-Og编译的结果中,能看到正常的参数处理:将arg1存入%ebx,arg2存入%r12d,然后通过移位、或操作将结构体成员合并到%rsi中,符合x86_64调用约定(%rdi是第一个参数"Hello"的地址,%rsi是第二个结构体参数)。
4. .ko文件中printk地址未填充的原因
Linux内核模块的链接分为两个阶段:
- 编译链接生成
.ko文件:此时链接器只完成模块内部符号的解析,对于内核导出的符号(如printk),只会生成重定位条目(即反汇编中看到的R_X86_64_PLT32 _printk-0x4),不会填充实际地址。 - 模块加载阶段:当
insmod或modprobe加载模块时,内核的模块加载器会将模块中的重定位符号与内核符号表中的实际地址进行绑定,填充最终的地址。
所以.ko文件本身是一个未完全链接的目标文件,需要内核加载时完成最终的重定位,这就是为什么你在.ko的反汇编中看到的printk调用地址还是00 00 00 00,只带有重定位标记。
内容的提问来源于stack exchange,提问作者InsaneCoder

