返回

Crash工具调试内核外部模块:解决符号与源码不匹配问题

Linux

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 文件里。
  • 操作步骤:
    1. 确保你的自定义模块在编译时生成了调试信息 (例如, 使用 -g 编译选项).
    2. 找到编译好的 .ko 文件 (包含调试信息)。
    3. 在 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
      
    如果ko有依赖,则还需要load依赖的ko, 使用 mod -s .
    4. 现在 disbt 等命令应该能正确显示自定义模块的源码和调用栈信息.
        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

另一种更彻底的方式,是将内核也一同编译:

  • 原理: 直接使用带有调试信息的内核文件进行调试,可以正确溯源到全部的信息(不仅仅局限于外部模块).
  • 操作步骤:
    1. 使用带debug选项重新编译你所使用的整个内核,确保也包含了外部模块. (比较耗时).

    2. 将新编译的带有调试信息的 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 提供了更强大的调试功能,包括手动加载符号文件和设置源码路径。

  • 操作步骤:

    1. 启动 gdb 并加载 vmcore 文件:

      gdb /path/to/your/vmlinux /var/crash/vmcore
      
    2. 使用add-symbol-file命令添加自定义模块的符号表,第二个参数为.text段起始地址. 这个地址可以使用 readelf -S your_module.ko来查看 .textAddr 得到:

    (gdb) add-symbol-file /path/to/custom/ufs.ko  <text_segment_address>
    ```
    
    1. 通过directory设置源代码的路径:
       (gdb) directory /path/to/custom/ufs/source/code/
      
    2. 现在可以使用 gdb 的所有调试命令,例如 listbreaknextstepprint 等, 都是针对你自定义的模块了。
    3. 使用info line *(address)来反查对应源码。

    这种方法更加灵活,但需要对 gdb 和内核调试有一定的了解。

  • 安全提示: 确保你了解你加载的符号和源码,以防发生安全问题。

4. 利用kdump中的kexec_load_disabled标志位

kdump在内核启动早期就会预留好一段用于存放新内核(用于crash)的内存。这个过程如果被打断(比如一个较早的crash发生),可能导致一些问题. 可以通过禁止 early kdump loading 来绕过这些特定场景下的bug。

  • 原理:

    • 当kdump服务尝试去准备捕获crash的kernel时候,会在早期加载这个新kernel, kexec_load_disabled 标志位会影响这个流程.
  • 操作步骤:

    1. 修改 /etc/sysctl.conf 文件, 加入这行:
      kernel.kexec_load_disabled=1
      
    2. 然后通过命令: sysctl -p /etc/sysctl.conf 刷新配置.
    3. 通过reboot进行重启.
    • 这个标志位的修改将强制让kernel kexec/kdump的相关的加载全部禁用。通常只是用于trouble shooting, 当正常的时候,该参数仍然建议开启。
    • 在这个过程中建议不要做其他复杂的操作(如挂载其他的复杂的模块)
  • 提示 : 该操作只是针对crash kernel的加载, 请注意生产环境需要开启这个功能.

三、总结

调试内核外部模块的 crash,关键是提供正确的符号和源码信息给调试工具。crash 工具的 mod -smod -S 命令, 和带有调试信息的vmlinux提供了简便的方法.对于一些特殊情况或复杂的问题, gdb 提供了更大的灵活性。 希望你下次碰到自定义模块 crash 的时候不再迷茫!