Crash工具调试内核外部模块:解决符号与源码不匹配问题
2025-03-12 11:08:03
Crash 工具分析:调试外部模块
最近在开发一个内核模块,对已有的模块(比如 ufs.ko
)进行功能扩展。 当我的驱动程序崩溃时,我得到了一个 vmcore 文件,可以使用 crash 工具进行分析。但遇到一个问题:crash 工具链接的是内核调试信息包,这个包是基于一个不同于我当前使用的源码构建的。 这就导致我没法准确地调试问题,因为修改过的模块(比如 ufs.ko
)的源码和 crash 链接的内核调试信息包里面的不一样。
目标很明确:
我需要把我的 .ko 文件(就是我改过的那个)和我修改过的源码关联起来。更具体地说,我需要加载正确的内核模块版本(包含我的更改)进行调试,这样就能把 crash 对应到我在源码中修改的具体位置。
举个例子:
比如,我修改了 ufs.ko
模块,加了一个新函数。但当 crash 发生时,我只能看到内核调试信息包里通用 ufs.ko
模块的调试信息。 这和我定制过的版本不一样,这让我很难找到导致 crash 的那行代码。
下面我们就来解决这个问题。
一、问题原因分析
这个问题的根本原因在于调试信息与实际运行代码的不匹配。内核调试信息包通常是针对标准内核构建的,包含了标准模块的符号和源码信息。 而我们修改过的模块,是一个独立的、外部编译的模块,它的源码和符号信息与调试信息包中的不同。crash 工具默认加载的是系统级别的调试信息,自然找不到我们自定义模块的正确源码。
二、解决方案
解决这个问题的核心思路就是:告诉 crash 工具,去哪里找我们自定义模块的源码和符号信息。 下面列出几种方法:
1. 使用 mod -s
命令 (及 mod -S
)
crash
工具提供了 mod
命令来管理模块。-s
选项可以指定一个模块的符号文件。
- 原理:
mod -s <module> <path/to/module.ko>
告诉 crash 工具,某个模块的符号信息在指定的.ko
文件里。 - 操作步骤:
- 确保你的自定义模块在编译时生成了调试信息 (例如, 使用
-g
编译选项). - 找到编译好的
.ko
文件 (包含调试信息)。 - 在 crash 会话中执行:
crash /usr/lib/debug/lib/modules/5.14.0-427.13.1.el9_4/vmlinux /var/crash/vmcore # 启动crash crash> mod -s ufs /path/to/custom/ufs.ko
mod -s
.
4. 现在dis
或bt
等命令应该能正确显示自定义模块的源码和调用栈信息.crash> bt #0 [ffffbb0dcfaabbd0] machine_kexec at ffffffff9a2781a7 #1 [ffffbb0dcfaabb28] __crash_kexec at ffffffff9a3ef4ea #2 [ffffbb0dcfaabbe8] crash_kexec at ffffffff9a3f0778 ... #8 [ffffbb0dcfaabd70] my_modified_function in ufs.ko at ffffffffc1246162 crash> dis ffffffffc1246162 # 应该可以溯源 ```
- 确保你的自定义模块在编译时生成了调试信息 (例如, 使用
- 进阶 : 可以使用
mod -S /path/to/source/dir/
来为所有后续加载的模块设置一个基本源码路径. 这样就不需要为每一个模块单独使用-s
命令。适用于模块源码在一个统一目录下。如果模块在构建的时候使用了kbuild
系统,且生成了调试信息,Crash能够自动的从.ko中提取出源代码路径。但是如果不是在kbuild
环境中构建,则不会自动找到。这种情况使用mod -S
来批量设定源码的路径。 - 安全提示: 确保只加载信任的
.ko
文件,避免恶意模块注入。
2. 使用带有调试信息的编译的 vmlinux
另一种更彻底的方式,是将内核也一同编译:
- 原理: 直接使用带有调试信息的内核文件进行调试,可以正确溯源到全部的信息(不仅仅局限于外部模块).
- 操作步骤:
-
使用带debug选项重新编译你所使用的整个内核,确保也包含了外部模块. (比较耗时).
-
将新编译的带有调试信息的
vmlinux
用于crash
会话, 代替/usr/lib/debug/lib/modules/5.14.0-427.13.1.el9_4/vmlinux
:crash /path/to/your/vmlinux /var/crash/vmcore
因为使用了含有全部符号表的
vmlinux
文件, 所有的符号包括内核模块(无论是内部的,还是外部的)都可以正确链接到源代码。
-
3. gdb + vmcore (高级用法, 非 crash)
对于一些更复杂的情况, Crash可能仍然不够,此时可以使用 gdb 直接调试 vmcore.
-
原理: gdb 提供了更强大的调试功能,包括手动加载符号文件和设置源码路径。
-
操作步骤:
-
启动 gdb 并加载 vmcore 文件:
gdb /path/to/your/vmlinux /var/crash/vmcore
-
使用
add-symbol-file
命令添加自定义模块的符号表,第二个参数为.text段起始地址. 这个地址可以使用readelf -S your_module.ko
来查看.text
的Addr
得到:
(gdb) add-symbol-file /path/to/custom/ufs.ko <text_segment_address> ```
- 通过
directory
设置源代码的路径:(gdb) directory /path/to/custom/ufs/source/code/
- 现在可以使用 gdb 的所有调试命令,例如
list
、break
、next
、step
、print
等, 都是针对你自定义的模块了。 - 使用
info line *(address)
来反查对应源码。
这种方法更加灵活,但需要对 gdb 和内核调试有一定的了解。
-
-
安全提示: 确保你了解你加载的符号和源码,以防发生安全问题。
4. 利用kdump中的kexec_load_disabled标志位
kdump在内核启动早期就会预留好一段用于存放新内核(用于crash)的内存。这个过程如果被打断(比如一个较早的crash发生),可能导致一些问题. 可以通过禁止 early kdump loading 来绕过这些特定场景下的bug。
-
原理:
- 当kdump服务尝试去准备捕获crash的kernel时候,会在早期加载这个新kernel,
kexec_load_disabled
标志位会影响这个流程.
- 当kdump服务尝试去准备捕获crash的kernel时候,会在早期加载这个新kernel,
-
操作步骤:
- 修改
/etc/sysctl.conf
文件, 加入这行:kernel.kexec_load_disabled=1
- 然后通过命令:
sysctl -p /etc/sysctl.conf
刷新配置. - 通过reboot进行重启.
- 这个标志位的修改将强制让kernel kexec/kdump的相关的加载全部禁用。通常只是用于trouble shooting, 当正常的时候,该参数仍然建议开启。
- 在这个过程中建议不要做其他复杂的操作(如挂载其他的复杂的模块)
- 修改
-
提示 : 该操作只是针对crash kernel的加载, 请注意生产环境需要开启这个功能.
三、总结
调试内核外部模块的 crash,关键是提供正确的符号和源码信息给调试工具。crash
工具的 mod -s
和mod -S
命令, 和带有调试信息的vmlinux
提供了简便的方法.对于一些特殊情况或复杂的问题, gdb 提供了更大的灵活性。 希望你下次碰到自定义模块 crash 的时候不再迷茫!