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 调试配置与编译流程

以下流程图展示了从源码到可调试内核镜像的完整编译链路:

内核编译与调试符号生成流程 1. 获取内核源码 2. 配置内核选项 3. 编译 vmlinux 4. 生成调试符号 5. 编译内核模块 6. 安装模块 编译产物说明 • vmlinux:未压缩的内核镜像,包含完整调试符号(当 CONFIG_DEBUG_INFO=y 时) • *.ko:可加载内核模块,每个模块独立包含 .debug_info 段 • System.map:内核符号地址映射表,调试器与跟踪工具的重要辅助文件

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 的内核用于生产部署。

2.7 本章小结