【Linux内核模块】调试技巧

内核模块开发最让人头疼的不是写代码,而是调试 —— 代码编译通过了,加载后却要么没反应,要么直接让系统崩溃。这就像在黑屋子里修机器,看不见摸不着。其实内核调试有一套成熟的工具箱,掌握这些工具和技巧,就能给内核装个监控监控仪,让问题无所遁形。


目录

一、调试前的安全须知:别让系统崩溃

二、最基础也最常用:printk 打印日志

2.1 printk 的基本用法

2.2 控制日志输出

2.3 printk 的高级技巧

三、内核 Oops 分析:系统崩溃时的现场照片

3.1 认识 Oops 信息

3.2 定位 Oops 错误位置

3.3 常见 Oops 错误及原因

四、动态调试:按需开启的监控摄像头

五、内核调试器 kgdb:像 gdb 一样调试内核

5.1 搭建 kgdb 环境

5.2 使用 kgdb 调试模块 

5.3 kgdb 的优缺点

六、内存调试工具:检测内存泄漏和越界

6.1 kmemleak:检测内存泄漏

6.2 KASAN:检测内存越界

七、用户态调试工具:从外部观察模块行为

八、调试方法论:解决问题的步骤


一、调试前的安全须知:别让系统崩溃

内核模块调试有个特点:一旦出错可能直接导致系统死机,所以安全措施必须做好。就像拆弹专家要穿防爆服,咱们调试内核也得有防护措施。

1. 必备的调试环境

  • 虚拟机优先:90% 的内核调试应该在虚拟机里进行(推荐 VirtualBox 或 VMware),死机了重启就行
  • 多终端连接:用 SSH 或串口连接虚拟机,即使图形界面卡死,还能通过终端查看日志
  • 快照备份:调试前给虚拟机拍快照,搞崩了能快速恢复(血的教训!)

2. 调试的三不原则

  • 不要在生产环境调试新模块
  • 不要加载来源不明的模块
  • 调试时不要运行重要程序

二、最基础也最常用:printk 打印日志

如果只能选一个调试工具,那一定是printk。它就像医生用的听诊器,简单直接却能解决大部分问题。

2.1 printk 的基本用法

和用户态的printf类似,但多了个日志级别参数:

printk(KERN_INFO "模块初始化成功,当前状态: %d\n", status);

日志级别决定了消息是否显示以及存到哪里,常用的有:

  • KERN_EMERG:紧急情况(系统崩溃前消息)
  • KERN_ALERT:必须立即处理
  • KERN_CRIT:严重错误
  • KERN_ERR:错误信息
  • KERN_WARNING:警告信息
  • KERN_NOTICE:正常但重要的信息
  • KERN_INFO:普通信息(最常用)
  • KERN_DEBUG:调试信息(默认不显示)

2.2 控制日志输出

默认情况下,级别高于KERN_WARNING的消息才会显示到控制台。可以通过dmesg命令查看所有日志: 

dmesg | tail  # 查看最新的10条日志
dmesg -w      # 实时监控日志输出

临时调整日志级别(数值越小级别越高):

sudo echo 7 > /proc/sys/kernel/printk  # 显示所有级别日志(调试时用)

2.3 printk 的高级技巧

  • 添加模块名和函数名:方便定位日志来源 

printk(KERN_INFO "[MY_MODULE] %s: 设备已打开\n", __func__);

__func__是编译器内置宏,会自动替换为当前函数名

  • 条件编译调试信息:只在调试模式输出详细日志 

#ifdef DEBUG
#define DBG_PRINT(fmt, args...) printk(KERN_DEBUG "[DBG] %s: " fmt, __func__, ##args)
#else
#define DBG_PRINT(fmt, args...)
#endif// 使用
DBG_PRINT("缓冲区大小: %d\n", buf_size);

编译时添加-DDEBUG参数启用调试日志

  • 避免日志刷屏:高频操作中限制日志输出

static int log_counter = 0;
if (log_counter % 1000 == 0) {  // 每1000次打印一次printk(KERN_INFO "已处理 %d 个请求\n", log_counter);
}
log_counter++;

三、内核 Oops 分析:系统崩溃时的现场照片

当模块代码有严重错误(如空指针访问),内核会产生 Oops 信息,这相当于系统崩溃时的现场照片,包含大量调试线索。

3.1 认识 Oops 信息

典型的 Oops 信息长这样: 

BUG: unable to handle kernel NULL pointer dereference at 0000000000000010
IP: [<ffffffffc0a01056>] my_module_write+0x16/0x50 [my_module]
PGD 80000001f8e7067 PUD 1f8e71067 PMD 0 
Oops: 0002 [#1] SMP PTI
CPU: 1 PID: 1234 Comm: insmod Tainted: G        W  OE     5.4.0-100-generic #101-Ubuntu
Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 07/29/2019
RIP: 0010:my_module_write+0x16/0x50 [my_module]
...
Call Trace:<TASK>SyS_write+0x5f/0xe0do_syscall_64+0x57/0x190entry_SYSCALL_64_after_hwframe+0x44/0xa9
...</TASK>
  • NULL pointer dereference:空指针引用错误
  • my_module_write+0x16/0x50:错误发生在my_module_write函数,偏移 0x16 处
  • Call Trace:函数调用栈,显示错误发生前的调用路径

3.2 定位 Oops 错误位置

addr2line工具将内存地址转换为代码行号: 

addr2line -e my_module.ko 0x16

会输出类似/home/user/my_module.c:42的结果,直接定位到出错的代码行。

3.3 常见 Oops 错误及原因

  • NULL pointer dereference:访问空指针(最常见)
  • use-after-free:使用已释放的内存
  • stack overflow:栈溢出
  • invalid opcode:非法指令(通常是汇编错误)

四、动态调试:按需开启的监控摄像头

内核的动态调试(Dynamic Debug)机制可以像开关灯一样控制特定代码的日志输出,不用重新编译模块。

1. 开启动态调试支持

首先确认内核支持动态调试(大部分发行版默认支持): 

grep CONFIG_DYNAMIC_DEBUG /boot/config-$(uname -r)

如果输出CONFIG_DYNAMIC_DEBUG=y,说明支持。

2. 动态调试的基本用法

通过/sys/kernel/debug/dynamic_debug/control文件控制日志输出: 

# 先挂载debugfs
sudo mount -t debugfs none /sys/kernel/debug# 显示my_module.c中所有函数的调试信息
sudo echo 'file my_module.c +p' > /sys/kernel/debug/dynamic_debug/control# 只显示特定函数的调试信息
sudo echo 'func my_module_write +p' > /sys/kernel/debug/dynamic_debug/control# 关闭调试信息
sudo echo 'file my_module.c -p' > /sys/kernel/debug/dynamic_debug/control

3. 在代码中使用动态调试

在代码中用pr_debugdev_dbg代替printk(KERN_DEBUG): 

pr_debug("数据长度: %d\n", data_len);  // 动态调试支持的打印函数

这些函数默认不输出日志,只有通过动态调试开关启用后才会输出。

五、内核调试器 kgdb:像 gdb 一样调试内核

如果 printk 和 Oops 分析还不够,就需要kgdb—— 内核版的 gdb 调试器,支持断点、单步执行等高级调试功能。

5.1 搭建 kgdb 环境

kgdb 需要两台机器(或虚拟机)通过串口连接:

  • 目标机:运行待调试的内核和模块
  • 主机:运行 gdb,通过串口控制目标机

配置步骤(以虚拟机为例):

  1. 给目标虚拟机添加一个串口设备(如 /dev/ttyS0)
  2. 目标机内核启动参数添加:kgdboc=ttyS0,115200 kgdbwait(启动时等待调试连接)
  3. 主机通过screen连接串口:screen /dev/ttyS0 115200

5.2 使用 kgdb 调试模块 

# 在主机上启动gdb
gdb ./vmlinux  # vmlinux是带调试信息的内核镜像# 连接目标机
(gdb) target remote /dev/ttyS0# 设置断点(模块加载后)
(gdb) break my_module_init# 查看变量
(gdb) print buffer_size# 单步执行
(gdb) step# 继续执行
(gdb) continue

5.3 kgdb 的优缺点

  • 优点:可以像调试用户态程序一样单步调试内核代码
  • 缺点:配置复杂,需要两台机器,调试过程会暂停整个系统

六、内存调试工具:检测内存泄漏和越界

内核模块最容易出内存问题,这些问题隐蔽性强,需要专门工具检测。

6.1 kmemleak:检测内存泄漏

kmemleak 可以跟踪内核内存分配,发现未释放的内存:

启用 kmemleak:

# 挂载debugfs
sudo mount -t debugfs none /sys/kernel/debug# 手动触发内存泄漏检查
sudo echo scan > /sys/kernel/debug/kmemleak# 查看内存泄漏报告
sudo cat /sys/kernel/debug/kmemleak

典型的内存泄漏报告:

unreferenced object 0xffff888123456780 (size 128):comm "insmod", pid 1234, jiffies 4567890 (age 30.000s)hex dump (first 32 bytes):00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f  ................10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f  ................backtrace:[<ffffffffc0a01020>] my_module_init+0x20/0x100 [my_module][<ffffffff81000200>] do_one_initcall+0x50/0x220...

报告会显示泄漏内存的地址、大小、分配位置,帮助定位问题。

6.2 KASAN:检测内存越界

KASAN(Kernel Address Sanitizer)能检测数组越界、使用已释放内存等错误,但需要使用带 KASAN 支持的内核:

# 查看内核是否支持KASAN
grep CONFIG_KASAN /boot/config-$(uname -r)

当检测到内存错误时,会输出详细报告:

==================================================================
BUG: KASAN: out-of-bounds in my_module_write+0x30/0x50 [my_module]
Write of size 4 at addr ffff88812345678c by task insmod/1234CPU: 1 PID: 1234 Comm: insmod Tainted: G        W  OE     5.4.0-100-generic #101-Ubuntu
Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 07/29/2019
Call Trace:<TASK>__dump_stack+0x70/0xa0...
Allocated by task 1234:my_module_init+0x20/0x100 [my_module]do_one_initcall+0x50/0x220...
==================================================================

七、用户态调试工具:从外部观察模块行为

除了内核态工具,还有一些用户态工具可以帮助观察模块的行为。

1. lsmod 和 modinfo:查看模块信息 

lsmod  # 查看所有加载的模块及使用计数
lsmod | grep my_module  # 查看特定模块
modinfo my_module.ko  # 查看模块详细信息(版本、作者、依赖等)

2. proc 和 sys 文件系统:模块状态接口

在模块中创建 proc 或 sys 接口,暴露内部状态:

创建 proc 文件示例

#include <linux/proc_fs.h>
#include <linux/seq_file.h>static int my_proc_show(struct seq_file *m, void *v) {seq_printf(m, "当前连接数: %d\n", conn_count);seq_printf(m, "缓冲区使用率: %d%%\n", buf_usage);return 0;
}static int my_proc_open(struct inode *inode, struct file *file) {return single_open(file, my_proc_show, NULL);
}static const struct file_operations my_proc_fops = {.owner = THIS_MODULE,.open = my_proc_open,.read = seq_read,.llseek = seq_lseek,.release = single_release,
};// 在初始化函数中创建
proc_create("my_module_stats", 0, NULL, &my_proc_fops);

用户态查看:

cat /proc/my_module_stats

3. perf:性能分析工具

perf可以分析模块的性能瓶颈: 

# 记录模块的函数调用情况
sudo perf record -g -e 'module:my_module:*' sleep 10# 查看报告
sudo perf report

八、调试方法论:解决问题的步骤

掌握工具后,更重要的是形成一套调试思路。遇到问题时可以按这个步骤排查:

①复现问题:明确触发条件,确保问题可重复

②缩小范围:通过注释代码或添加日志,定位问题所在的大致范围

③针对性调试

  • 功能问题:用 printk 打印关键变量值
  • 崩溃问题:分析 Oops 信息
  • 内存问题:用 kmemleak 和 KASAN 检测
  • 性能问题:用 perf 分析

④验证修复:确认问题解决,且没有引入新问题

调试 checklist

  •  模块是否正确加载?(lsmod 检查)
  •  有没有 Oops 信息?(dmesg 查看)
  •  关键变量的值是否符合预期?(printk 输出)
  •  内存分配和释放是否配对?(检查 kmalloc 和 kfree)
  •  函数返回值是否正确处理?(是否检查错误码)

内核模块调试确实有难度,但只要掌握了正确的工具和方法,大部分问题都能解决。记住:

  1. 从简单工具开始:先用 printk 和 dmesg 解决 80% 的问题
  2. 善用系统提供的调试机制:动态调试、kmemleak 等内核自带工具
  3. 复杂问题才需要 kgdb:简单问题用高级工具反而效率低
  4. 安全第一:始终在虚拟机中调试,做好快照备份

调试能力是区分内核开发者水平的关键指标。刚开始可能会觉得挫败,但每解决一个调试难题,你的内核开发水平就会上一个台阶。就像医生通过不断积累病例提高诊断能力,内核开发者也是在一次次调试中成长的。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/91038.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/91038.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RK3568笔记九十一:QT环境搭建

若该文为原创文章,转载请注明原文出处。 记录按照正点原子给的手册搭建QT环境 参考《09【正点原子】ATK-DLRK3568_Qt开发环境搭建V1.2.pdf》 一、安装 1、下载 https://mirrors.sau.edu.cn/qt/archive/online_installers/4.6/qt-unified-linux-x64-4.6.0-online.run 2、赋…

面试实战,问题十六,Java面试,消息队列,如何避免消息重复消费,怎么回答

在Java面试中&#xff0c;关于消息队列如何防止消息被重复消费的问题&#xff0c;可以从以下几个方面进行回答&#xff0c;结合系统架构设计、消息队列机制和业务逻辑处理&#xff0c;确保在不同场景下实现消息的幂等性。 1. 消息队列重复消费的根本原因 消息重复消费的根本原因…

PDF转图片实用指南:如何批量高效转换?

将PDF转换为图片后&#xff0c;可以更方便地在演示文稿、网页或电子相册中使用这些资料&#xff0c;以便更好地展示信息。它 是一款支持多文件批量转换的工具&#xff0c;可将多个 PDF 文档一键转换为图片格式。虽然界面为英文&#xff0c;但操作简单&#xff0c;不影响使用。你…

走入Linux的世界:编辑器Vim

嘿&#xff0c;各位技术潮人&#xff01;好久不见甚是想念。生活就像一场奇妙冒险&#xff0c;而编程就是那把超酷的万能钥匙。此刻&#xff0c;阳光洒在键盘上&#xff0c;灵感在指尖跳跃&#xff0c;让我们抛开一切束缚&#xff0c;给平淡日子加点料&#xff0c;注入满满的pa…

PyTorch中神经网络的模型构建

要构建自定义模型&#xff0c;需完成两个核心步骤&#xff1a;继承 nn.Module 类&#xff1b;重载 __init__ 方法&#xff08;初始化&#xff09;和 forward 方法&#xff08;前向计算&#xff09; 神经网络的构造 初始化方法&#xff08;__init__&#xff09; def __init__…

QML QtCharts坐标轴系统

QtCharts是Qt框架中强大的数据可视化模块&#xff0c;它提供了丰富的图表类型和灵活的坐标轴系统&#xff0c;能够满足各种数据展示需求。本文将全面介绍QML中QtCharts的坐标轴系统&#xff0c;包括数值坐标轴(ValueAxis)、对数坐标轴(LogValueAxis)、分类坐标轴(CategoryAxis)…

TI 2025全国电赛猜题

本科组可能的题目方向本科组器材更侧重高频信号处理、复杂控制系统、精密测量及多设备协同&#xff0c;可能涉及以下题目&#xff1a;四旋翼飞行器相关任务题目示例&#xff1a;设计 “基于四旋翼的 UV 光控自主导航系统”任务要求&#xff1a;利用四旋翼飞行器&#xff08;最大…

Python自动化运维实战指南

什么是自动化运维定义与背景自动化运维是指利用工具和脚本自动执行传统上需要人工操作的IT运维任务&#xff0c;包括但不限于服务器配置管理、软件部署、监控告警、日志分析等日常工作。随着互联网业务规模的扩大&#xff0c;传统手工运维方式已无法满足快速部署、规模化管理等…

k8s的csi对接GPFS

在 Kubernetes&#xff08;k8s&#xff09;集群中&#xff0c;通过 CSI&#xff08;Container Storage Interface&#xff09;对接 GPFS&#xff08;General Parallel File System&#xff0c;现为 IBM Spectrum Scale&#xff09;是实现高性能共享存储的重要方案。GPFS 作为并…

HTB赛季8靶场 - era

nmap扫描 └─$ nmap -p- --min-rate 1000 -T4 10.129.137.201 -oA nmapfullscan Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-07-27 21:19 EDT Warning: 10.129.137.201 giving up on port because retransmission cap hit (6). …

Bug猫学习史#1:面向对象

在Java编程中&#xff0c;掌握几个核心概念对深入学习至关重要&#xff1a;类属性建议采用包装类以提升灵活性&#xff1b;建造者模式中this关键字能有效简化对象构建过程&#xff1b;static关键字涉及类的加载机制&#xff1b;接口默认使用public修饰符并支持默认方法实现&…

优测推出HarmonyOS全场景测试服务,解锁分布式场景应用卓越品质!

随着HarmonyOS NEXT“纯血鸿蒙”的全面商用&#xff0c;生态正以前所未有的速度重构终端操作系统格局。对于APP厂商而言&#xff0c;应用测试需要从单一设备思维向场景化服务验证转变。优测云服务平台正式推出 HarmonyOS全场景测试解决方案&#xff0c;针对鸿蒙系统提供功能测试…

二层环路与三层环路:原理、区别与解决方案全解析

网络环路是网络运维中最常见也最具破坏性的问题之一。本文将深入浅出地解析二层环路和三层环路的核心概念&#xff0c;通过对比分析帮助读者全面理解这两种环路的形成机制、危害表现及解决方案。一、环路问题概述 1.1 什么是网络环路 网络环路是指数据包在网络中循环传输无法到…

Python爬虫库性能与选型实战指南:从需求到落地的全链路解析

目录 一、性能基准测试&#xff1a;用数据打破认知误区 1. 静态页面采集&#xff1a;效率与资源的终极对决 2. 动态页面渲染&#xff1a;速度与真实性的博弈 二、场景化选型矩阵&#xff1a;从需求到工具的精准映射 1. 小规模快速原型开发&#xff08;≤1000页&#xff09;…

uni-app switch(开关选择器) BUG

uni-app switch&#xff08;开关选择器&#xff09; BUGBUG&#xff1a;uni-app中的switch的checked属性并不能根据根据绑定的动态数据进行调整switch开关选择器&#xff08;BUG&#xff09;switch开关选择器&#xff08;BUG&#xff09; - 我的使用用途switch开关选择器&#…

微服务架构中的资源调度与负载均衡实践

更多云服务器知识&#xff0c;尽在hostol.com在今天这个快速发展的数字化时代&#xff0c;微服务架构已经成为了现代企业系统开发的主流。随着技术的不断进步&#xff0c;企业的业务需求也在不断地变化&#xff0c;传统的单体架构已经无法满足日益复杂的应用需求。微服务架构&a…

Rust Web 全栈开发(十一):WebAssembly 尝鲜

Rust Web 全栈开发&#xff08;十一&#xff09;&#xff1a;WebAssembly 尝鲜Rust Web 全栈开发&#xff08;十一&#xff09;&#xff1a;WebAssembly 尝鲜什么是 WebAssembly&#xff1f;安装 wasm-pack 和 cargo-generate使用项目模板构建项目生成网页安装依赖项在 www 中使…

Thymeleaf实战:SpringBoot用户管理系统

Thymeleaf 示例代码下面是完整代码示例&#xff0c;帮助理解 Thymeleaf 语法和后端代码的配合&#xff1a;1. 用户实体类 (User.java)/*** 用户实体类*/ public class User {private Long id; // 用户IDprivate String name; // 用户名private String email; /…

mysql查找数据库表中某几个连续的编号中中断的编号

在MySQL中查找表中连续编号中断的位置,可以通过以下几种方法实现: 基于范围的查询方法 通过自连接查询找出ID序列中的断点,例如查找1-100范围内缺失的ID: SELECT a.id + 1 AS start, MIN(b.id) - 1 AS end FROM

《剑指offer》-数据结构篇-树

题目重建二叉树树的子结构二叉树的镜像从上往下打印二叉树&#xff08;层序遍历&#xff09;把二叉树打印成多行按之字形顺序打印二叉树二叉搜索树的第k个结点&#xff08;中序遍历&#xff09;二叉搜索树的后序遍历序列&#xff08;后序遍历&#xff09;二叉树中和为某一值的路…