Linux内核分析之基础知识-7

韩乔落

第7章:调试技巧

诊断和分析内核启动问题的方法


本章概述

内核启动问题可能发生在任何阶段,从早期汇编代码到用户空间启动。本章提供各种调试技术和工具来诊断这些问题。

调试阶段划分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─────────────────────────────────────────────────────────────┐
│ 调试阶段和技术 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 阶段1: 固件/引导程序 │
│ - BIOS/UEFI 调试 │
│ - 串口输出 │
│ - GRUB 调试 │
│ │
│ 阶段2: 早期汇编代码 │
│ - 早期串口输出 (earlyprintk) │
│ - QEMU/GDB 调试 │
│ - JTAG 调试 │
│ │
│ 阶段3: 解压阶段 │
│ - 解压日志 │
│ - 堆栈跟踪 │
│ │
│ 阶段4: start_kernel │
│ - ftrace 跟踪 │
│ - initcall 调试 │
│ - crash 工具 │
│ │
│ 阶段5: 用户空间启动 │
│ - init 进程调试 │
│ - systemd 调试 │
│ │
└─────────────────────────────────────────────────────────────┘

7.1 早期调试技术

7.1.1 earlyprintk 和 earlycon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# x86_64 earlyprintk
earlyprintk=serial,ttyS0,115200
earlyprintk=vga
earlyprintk=dbgp

# x86_64 earlycon (现代方式)
earlycon=uart,mmio,0x3f8,115200n8
earlycon=pciserial,mmio,0x1234,115200
earlycon=dbgp
earlycon=efifb

# ARM64 earlycon
earlycon=pl011,0x09000000,115200n8
earlycon=uart8250,mmio32,0xff1a0000,115200n8

# 持久化早期控制台
earlyprintk=serial,ttyS0,115200,keep

# 组合使用
earlyprintk=serial,ttyS0,115200 console=ttyS0,115200n8

7.1.2 调试级别控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 内核日志级别
loglevel=0 # 仅 KERN_EMERG
loglevel=1 # KERN_EMERG, KERN_ALERT
loglevel=2 # + KERN_CRIT
loglevel=3 # + KERN_ERR
loglevel=4 # + KERN_WARNING
loglevel=5 # + KERN_NOTICE
loglevel=6 # + KERN_INFO
loglevel=7 # + KERN_DEBUG (最详细)
loglevel=15 # 所有消息, 包括调试

# 忽略默认日志级别
ignore_loglevel

# 静默启动
quiet

7.1.3 启用调试选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 调试选项配置

# 一般调试
CONFIG_DEBUG_KERNEL=y
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_BTF=y # BPF 类型格式

# 早期调试
CONFIG_DEBUG_LL=y # 低层调试
CONFIG_DEBUG_LL_UART_8250=y # UART 8250
CONFIG_DEBUG_ICEDCC=y # DCC (ARM)
CONFIG_EARLY_PRINTK=y # 早期打印
CONFIG_EARLY_PRINTK_DBGP=y # DBGP earlycon

# 页分配调试
CONFIG_DEBUG_PAGEALLOC=y
CONFIG_PAGE_POISONING=y

# slab/slab 调试
CONFIG_DEBUG_SLAB=y
CONFIG_SLUB_DEBUG=y

# 锁调试
CONFIG_DEBUG_RT_LOCKS=y
CONFIG_DEBUG_SPINLOCK=y
CONFIG_PROVE_LOCKING=y # 锁证明

# 内存调试
CONFIG_DEBUG_KMEMLEAK=y # 内存泄漏检测
CONFIG_KASAN=y # KASAN
CONFIG_UBSAN=y # UBSAN
CONFIG_KCOV=y # 覆盖率检测

# 跟踪
CONFIG_TRACING=y
CONFIG_FTRACE=y
CONFIG_FUNCTION_TRACER=y
CONFIG_FUNCTION_GRAPH_TRACER=y

7.2 QEMU 调试

7.2.1 QEMU 基础调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# x86_64 QEMU 调试
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-append "root=/dev/sda console=ttyS0" \
-serial stdio \
-s -S # -s: 等待 gdb, -S: 冻结 CPU

# ARM64 QEMU 调试
qemu-system-aarch64 \
-M virt \
-cpu cortex-a57 \
-kernel arch/arm64/boot/Image \
-append "console=ttyAMA0" \
-serial stdio \
-s -S

# 添加内核镜像符号
qemu-system-x86_64 \
-kernel bzImage \
-initrd initrd.img \
-smp 2 \
-m 2G \
-serial mon:stdio \
-s

7.2.2 GDB 连接和调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 启动 GDB
gdb vmlinux

# GDB 命令
(gdb) target remote :1234 # 连接到 QEMU
(gdb) break startup_64 # 设置断点
(gdb) break start_kernel # 设置断点
(gdb) continue # 继续执行
(gdb) bt # 回溯栈
(gdb) info registers # 寄存器信息
(gdb) x/50i $pc # 反汇编当前指令
(gdb) stepi # 单步执行指令

# 查看汇编代码
(gdb) disassemble start_kernel

# 查看源代码
(gdb) list start_kernel

# 自动化断点
(gdb) commands 1
Type commands for breakpoint(s) 1 (end with a line saying just "end"):
>bt
>continue
>end

# 条件断点
(gdb) break start_kernel if x0 == 0x1000

7.2.3 GDB Python 扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# gdb 脚本示例
import gdb

class PrintKernelCmd(gdb.Command):
"""Print kernel command"""

def __init__(self):
super(PrintKernelCmd, self).__init__("printkernel",
gdb.COMMAND_DATA)

def invoke(self, arg, from_tty):
# 读取 linux_banner
gdb.execute("print/x *(char*)0x1000000")
return

PrintKernelCmd()

7.3 ftrace 跟踪

7.3.1 ftrace 基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 挂载 debugfs
mount -t debugfs none /sys/kernel/debug

# 检查 ftrace 可用性
cat /sys/kernel/debug/tracing/available_tracers

# 启用函数跟踪
echo function > /sys/kernel/debug/tracing/current_tracer

# 查看跟踪输出
cat /sys/kernel/debug/tracing/trace

# 过滤跟踪函数
echo do_initcalls > /sys/kernel/debug/tracing/set_ftrace_filter

# 启用跟踪
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 禁用跟踪
echo 0 > /sys/kernel/debug/tracing/tracing_on

7.3.2 trace-cmd 工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 记录启动过程
trace-cmd record -p function -e do_initcalls -f \
-b 1000 --max-graph-depth 8 --date

# 重放跟踪
trace-cmd report

# 报告摘要
trace-cmd show

# 图形化跟踪
trace-cmd report --profile

# 事件跟踪
trace-cmd record -e sched_switch -e sched_wakeup

7.3.3 启动时跟踪

1
2
3
4
5
6
7
8
9
10
# 内核参数
ftrace=function # 函数跟踪
ftrace=function_graph # 函数图跟踪
ftrace_graph_max_depth=5 # 图深度
trace_event=sched:sched_switch # 事件跟踪
ftrace_filter=start_kernel # 过滤函数

# initcall 跟踪
initcall_debug # initcall 调试
trace_initcall=do_initcalls # 跟踪 initcall

7.4 initcall 调试

7.4.1 initcall 时间分析

1
2
3
4
5
6
7
8
9
10
11
12
# 启用 initcall 时间
initcall_debug

# 输出示例
[ 0.000000] calling pci_apply_final_quirks+0x0/0x20 @ 1
[ 0.000001] initcall pci_apply_final_quirks+0x0/0x20 returned 0 after 5 usecs

# 黑名单 initcall
initcall_blacklist=problematic_initcall,another_initcall

# 分析工具
grep initcall /var/log/dmesg | sort -k4 -n | head -20

7.4.2 延迟初始化

1
2
3
4
5
# 延迟 initcall
late_initcall(your_init_function)

# 观察哪些 initcall 被延迟
cat /proc/initcalls_blacklist

7.5 crash/kdump 分析

7.5.1 配置 kdump

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 保留崩溃内存
# /etc/default/grub
GRUB_CMDLINE_LINUX="crashkernel=256M"

# 或指定位置
GRUB_CMDLINE_LINUX="crashkernel=256M@2G"

# 2. 配置 kdump 服务
# /etc/kdump.conf
path /var/crash
core_collector makedumpfile -c --message-level 1 -d 31
kdump_post_notag_level 1

# 3. 启动服务
systemctl enable kdump
systemctl start kdump

# 4. 测试配置
kdumpctl status
kdumpctl estimate

7.5.2 使用 crash 分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 分析 vmcore
crash vmlinux /var/crash/127.0.0.1-2025-01-04-12:34:56/vmcore

# crash 命令
crash> bt # 回溯
crash> bt -a # 所有任务回溯
crash> log # dmesg
crash> ps # 进程列表
crash> ps -A # 所有任务
crash> task <pid> # 切换任务上下文
crash> sys # 系统信息
crash> mount # 挂载信息
crash> net -A # 网络信息
crash> kmem -i # 内存信息
crash> files <pid> # 打开的文件
crash> vm # 虚拟内存

# 反汇编
crash> dis -l start_kernel

# 搜索符号
crash> sym -l | grep start

# 显示变量
crash> p jiffies
crash> p current_task

7.5.3 分析示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 查找崩溃原因
crash> bt
PID: 0 TASK: ffff88001a200000 CPU: 0 COMMAND: "swapper/0"
#0 [ffff88001a2005c0] __schedule at ffffffff810a2345
#1 [ffff88001a200640] schedule at ffffffff810a2789
#2 [ffff88001a2006c0] schedule_timeout at ffffffff810c1234
#3 [ffff88001a200740] wait_for_completion at ffffffff810b5678
...

# 查看变量
crash> p jiffies_64
jiffies_64 = 4294938592

# 查看崩溃信息
crash> log | tail -20

# 分析死锁
crash> lock -a
crash> lock -t <lock_addr>

# 内存转储
crash> dump -q <address> <count>

# 导出数据
crash> ascii <address>

7.6 ARM64 特定调试

7.6.1 ARM64 串口调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# U-Boot 环境变量设置
setenv bootargs 'console=ttyAMA0,115200n8 earlycon=pl011,0x09000000'
setenv earlycon 'earlycon=pl011,0x09000000'

# 设备树配置
/ {
chosen {
bootargs = "console=ttyAMA0,115200n8";
stdout-path = "serial0:115200n8";
};

serial@9010000 {
compatible = "arm,pl011";
reg = <0x0 0x9010000 0x0 0x1000>;
interrupts = <0 1>;
clock-frequency = <24000000>;
current-speed = <115200>;
};
};

7.6.2 ARM64 JTAG 调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 硬件要求
- JTAG 调试器 (Segger J-Link, ST-Link, 等)
- OpenOCD 或 pyOCD
- GDB with ARM 支持

# OpenOCD 配置
# openocd.cfg
adapter driver ftdi
transport select jtag
target create a64.target cortex_a -chain-position a64.dap

# 启动 OpenOCD
openocd -f openocd.cfg

# 连接 GDB
aarch64-linux-gnu-gdb vmlinux
(gdb) target remote :3333
(gdb) load vmlinux
(gdb) break start_kernel
(gdb) continue

7.7 性能分析

7.7.1 启动性能分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# initcall 时间分析
grep "initcall" /var/log/dmesg | \
awk '{print $1, $2, $NF}' | \
sort -k3 -n | \
head -20

# 使用 bootchart
systemctl enable bootchart
systemctl start bootchart

# 使用 systemd-analyze
systemd-analyze time # 总启动时间
systemd-analyze blame # 服务启动时间
systemd-analyze critical-chain # 关键链分析
systemd-analyze plot > boot.svg # 生成图表

7.7.2 perf 分析

1
2
3
4
5
6
7
8
9
10
11
# 启动时 perf 记录
perf record -g -a -- sleep 30

# 报告
perf report

# 注释性能图
perf annotate start_kernel

# 时间线
perf script

7.8 常见问题和解决方案

7.8.1 启动卡住

1
2
3
4
5
6
7
8
9
10
11
12
13
症状: 系统启动到某处停止

诊断步骤:
1. 添加 earlyprintk=serial,ttyS0,115200
2. 观察最后输出位置
3. 检查是否缺少驱动或配置
4. 使用 initcall_debug 定位具体函数

常见原因:
- 缺少必要的驱动 (存储, 网络等)
- 设备树配置错误
- 中断配置问题
- 内存不足

7.8.2 内核恐慌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
症状: 内核崩溃并重启

诊断步骤:
1. 记录 panic 信息
2. 使用 addr2line 定位代码
3. 检查栈回溯
4. 分析崩溃原因
5. 添加相关调试选项重新编译

常见原因:
- 空指针解引用
- 内存访问越界
- 断言失败
- 死锁

7.8.3 设备检测失败

1
2
3
4
5
6
7
8
9
10
11
12
13
症状: 设备未被识别

诊断步骤:
1. 检查 dmesg 输出
2. 验证设备树配置
3. 确认驱动已编译
4. 检查硬件连接

调试命令:
lspci -vvv
lsusb -vvv
cat /proc/interrupts
cat /proc/iomem

7.9 调试工具清单

调试工具总结

工具 用途 阶段
QEMU 虚拟化调试 所有
GDB 源码级调试 汇编/C
crash 崩溃分析 运行时
ftrace 函数跟踪 内核
trace-cmd 事件跟踪 内核
perf 性能分析 内核
SystemTap 动态插桩 内核
eBPF 运行时跟踪 内核
JTAG 硬件调试 早期

实用脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# analyze_boot.sh - 启动分析脚本

echo "=== Initcall Times ==="
grep "initcall" /var/log/dmesg | \
awk '{print $NF, $0}' | \
sort -rn | head -20

echo ""
echo "=== Long Boot Services ==="
systemd-analyze blame | head -20

echo ""
echo "=== Late Initcalls ==="
grep "initcall" /var/log/dmesg | \
awk '$NF > 100000' | sort -k3 -n

7.10 本章小结

调试策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌─────────────────────────────────────────────────────────────┐
│ 调试策略树 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 问题发生在哪里? │
│ │ │
│ ├─ 无输出 → 早期汇编问题 │
│ │ - 使用 QEMU/GDB │
│ │ - 添加 earlyprintk │
│ │ │
│ ├─ 早期输出后停止 → 解压或早期 C 代码 │
│ │ - 检查 initcall_debug │
│ │ - 分析 last printk 位置 │
│ │ │
│ ├─ 部分驱动初始化失败 → 驱动问题 │
│ │ - 检查 dmesg │
│ │ - 验证配置 │
│ │ │
│ ├─ panic → 崩溃分析 │
│ │ - 分析栈回溯 │
│ │ - 使用 crash/kdump │
│ │ │
│ └─ 系统正常但启动慢 → 性能分析 │
│ - systemd-analyze │
│ - perf record │
│ │
└─────────────────────────────────────────────────────────────┘

快速参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 添加详细日志
earlyprintk=serial,ttyS0,115200 loglevel=7 debug

# 调试 initcall
initcall_debug

# QEMU 调试
qemu-system-x86_64 -kernel bzImage -s -S

# GDB 连接
gdb vmlinux
(gdb) target remote :1234

# 查看 initcall 时间
grep initcall /var/log/dmesg | sort -k4 -n

# 分析崩溃
crash vmlinux /proc/kcore
  • Title: Linux内核分析之基础知识-7
  • Author: 韩乔落
  • Created at : 2026-01-08 02:47:13
  • Updated at : 2026-01-19 13:40:35
  • Link: https://jelasin.github.io/2026/01/08/Linux内核分析之基础知识-7/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments