2. 内核编译与调试符号:源码获取、配置选项与编译实践
调试符号与内核编译配置是内核调试的基石。没有正确的编译选项,调试器无法将内存地址映射回源代码行号,动态跟踪工具也无法插入探针。本章将完整演示从内核源码获取、关键调试选项配置、到生成带完整符号的 vmlinux 与可加载模块的全流程。
核心目标:构建一个“可调试、可跟踪、可分析”的内核环境。调试符号(Debug Symbols)是连接二进制指令与源代码的桥梁,而
CONFIG_DEBUG_INFO 与 CONFIG_KGDB 等选项则是开启这座桥梁的开关。
2.1 内核源码获取与版本选择
获取内核源码是第一步。推荐从 kernel.org 获取主线或长期支持(LTS)版本,或从发行版仓库获取打了特定补丁的源码。
| 来源 | 命令/方式 | 适用场景 |
|---|---|---|
| kernel.org 主线 | wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.x.tar.xz |
通用调试、学习最新特性 |
| 发行版源码包 | apt-get source linux-image-$(uname -r) (Debian/Ubuntu) |
调试当前运行的内核,配置与当前环境一致 |
| Git 仓库 | git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git |
跟踪开发分支、定制补丁 |
实践建议:调试时尽量使用与目标系统完全相同的源码版本。使用
uname -r 查看当前内核版本,然后从发行版仓库获取对应源码,可以避免因版本不匹配导致的符号偏移问题。
2.2 关键调试配置选项详解
内核的调试功能通过 Kconfig 选项控制。以下是与调试符号和远程调试最核心的配置项:
| 配置选项 | 功能描述 | 推荐值 | 影响 |
|---|---|---|---|
CONFIG_DEBUG_INFO |
生成 DWARF 格式的调试符号,包含在 vmlinux 和模块中 |
y | 增加内核体积(约 2-5 倍),但调试必备 |
CONFIG_DEBUG_INFO_DWARF5 |
使用 DWARF 5 标准(替代 DWARF 4) | y (工具链支持时) | 更高效的符号压缩与行号信息 |
CONFIG_KGDB |
启用内核内建调试器,支持通过串口或网络远程调试 | y | 增加少量内存开销,提供断点/单步能力 |
CONFIG_KGDB_SERIAL_CONSOLE |
允许 KGDB 通过串口控制台交互 | y | 调试死机、早期启动问题 |
CONFIG_KALLSYMS |
导出所有内核符号表到 /proc/kallsyms |
y | kprobe/ftrace 等动态跟踪工具依赖此选项 |
CONFIG_FRAME_POINTER |
使用帧指针寄存器记录调用栈 | y | 提升栈回溯准确性,轻微性能影响 |
CONFIG_GDB_SCRIPTS |
加载 GDB 辅助脚本(lx-* 命令) | y | 极大简化 GDB 调试内核的复杂度 |
注意:
CONFIG_DEBUG_INFO 会显著增加编译时间和磁盘占用。在生产环境的内核中通常关闭此选项。调试专用内核建议与生产内核分开编译,使用不同的内核镜像和模块目录。
2.3 调试配置与编译流程
以下流程图展示了从源码到可调试内核镜像的完整编译链路:
2.4 编译内核与模块:分步实践
以下步骤基于 Linux 6.x LTS 版本,在 x86_64 架构上演示。请根据实际环境调整路径和版本号。
步骤 1:配置内核
使用当前运行内核的配置作为基础,然后手动启用调试选项:
# 解压源码
tar -xf linux-6.x.tar.xz
cd linux-6.x
# 基于当前内核配置
zcat /proc/config.gz > .config
# 或使用发行版默认配置
# make defconfig
# 启用调试相关选项
make menuconfig
# 进入 Kernel hacking 菜单,设置:
# [*] Compile the kernel with debug info
# [*] Generate DWARF5 debuginfo
# [*] KGDB: kernel debugger
# [*] KGDB: use serial console
# [*] Kernel debugging
# [*] Compile the kernel with frame pointers
# [*] Provide GDB scripts for kernel debugging
步骤 2:编译内核与模块
# 编译内核镜像(vmlinux)和所有模块
# -j 参数根据 CPU 核心数调整,加速编译
make -j$(nproc)
# 编译完成后,确认 vmlinux 包含调试符号
file vmlinux
# 输出应包含 "with debug_info, not stripped"
# 查看调试符号段大小
readelf -S vmlinux | grep debug
# 应看到 .debug_info, .debug_abbrev, .debug_line 等段
步骤 3:安装模块与内核
# 安装模块到 /lib/modules/<version>/
sudo make modules_install
# 安装内核到 /boot/,并更新引导器
sudo make install
# 重启进入新内核
sudo reboot
加速技巧:如果只需要调试符号用于离线分析(如 crash dump 解析),可以编译
vmlinux 后不安装内核,仅保留 vmlinux 和 System.map 文件。使用 make vmlinux 可以只编译内核镜像,跳过模块编译,大幅节省时间。
2.5 验证调试符号与 KGDB 配置
编译完成后,必须验证调试符号是否完整加载,以及 KGDB 是否可用。
验证调试符号
# 使用 objdump 查看符号
objdump -t vmlinux | head -20
# 应看到函数名与地址的对应关系
# 使用 gdb 加载 vmlinux 测试
gdb vmlinux
(gdb) lx-symbols
# 如果 CONFIG_GDB_SCRIPTS 启用,会加载所有模块符号
(gdb) lx-dmesg
# 应能打印内核日志缓冲区内容
验证 KGDB 配置
# 检查内核启动参数是否包含 kgdboc
cat /proc/cmdline | grep kgdboc
# 如果没有,在引导器中添加:kgdboc=ttyS0,115200
# 进入 KGDB 等待模式
echo g > /proc/sysrq-trigger
# 此时内核暂停,等待远程 GDB 连接
# 在另一台机器上(通过串口连接):
gdb vmlinux
(gdb) target remote /dev/ttyUSB0
(gdb) bt
# 应显示当前内核栈回溯
2.6 常见问题与避坑指南
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| GDB 无法显示源码行号 | CONFIG_DEBUG_INFO 未启用,或源码路径与编译时不一致 |
重新编译并启用 CONFIG_DEBUG_INFO;使用 dir 命令在 GDB 中设置源码路径 |
| KGDB 无法连接 | 串口参数不匹配,或内核未启用 CONFIG_KGDB_SERIAL_CONSOLE |
检查波特率、流控设置;确认内核启动参数包含 kgdboc=ttyS0,115200 |
| 模块符号未加载 | GDB 未找到模块路径,或模块编译时未包含调试符号 | 在 GDB 中使用 lx-symbols 命令自动加载;确保模块编译时 CONFIG_DEBUG_INFO 生效 |
| 内核 panic 后无法获取栈回溯 | 未启用 CONFIG_FRAME_POINTER 或 CONFIG_UNWINDER_FRAME_POINTER |
启用帧指针选项,重新编译内核 |
安全提醒:启用
CONFIG_KGDB 的内核在生产环境中存在安全风险,因为攻击者可能通过串口或网络接口获取内核控制权。调试完成后,务必使用关闭 KGDB 的内核用于生产部署。