Linux Kernel 除錯與追蹤技術完整指南
目錄
傳統除錯方法
printk
最基本但仍然有效的除錯方式
// kernel module 中使用
printk(KERN_INFO "Debug message: value=%d\n", value);
printk(KERN_ERR "Error occurred at %s:%d\n", __FILE__, __LINE__);
// 不同的日誌級別
KERN_EMERG // 系統無法使用
KERN_ALERT // 必須立即採取行動
KERN_CRIT // 臨界條件
KERN_ERR // 錯誤條件
KERN_WARNING // 警告條件
KERN_NOTICE // 正常但重要
KERN_INFO // 資訊
KERN_DEBUG // 除錯訊息
檢視訊息:
dmesg | tail -f
journalctl -k -f # systemd 系統
Dynamic Debug (dyndbg)
動態開關除錯訊息
// 在 kernel code 中
pr_debug("Dynamic debug message\n");
dev_dbg(dev, "Device debug message\n");
控制:
# 開啟特定檔案的除錯
echo 'file drivers/usb/core/hub.c +p' > /sys/kernel/debug/dynamic_debug/control
# 開啟特定函數
echo 'func usb_submit_urb +p' > /sys/kernel/debug/dynamic_debug/control
# 開啟特定模組
echo 'module usbcore +p' > /sys/kernel/debug/dynamic_debug/control
KGDB
Kernel 層級的 GDB 除錯
# 設定 kernel 參數
kgdboc=ttyS0,115200 kgdbwait
# 在另一臺機器上
gdb vmlinux
(gdb) target remote /dev/ttyS0
(gdb) break sys_open
(gdb) continue
現代追蹤技術
Ftrace
Linux 內建的追蹤框架
# 掛載 debugfs
mount -t debugfs none /sys/kernel/debug
# 基本使用
cd /sys/kernel/debug/tracing
# 查看可用的 tracer
cat available_tracers
# 設定 function tracer
echo function > current_tracer
echo 1 > tracing_on
cat trace
# 追蹤特定函數
echo do_sys_open > set_ftrace_filter
echo function_graph > current_tracer
# 追蹤函數調用圖
echo function_graph > current_tracer
echo 1 > options/funcgraph-proc
echo 1 > options/funcgraph-duration
進階功能:
# 設定追蹤 buffer 大小
echo 8192 > buffer_size_kb
# 只追蹤特定 CPU
echo 2 > tracing_cpumask
# 追蹤事件
echo 1 > events/sched/sched_switch/enable
echo 1 > events/irq/enable
# 使用 trace marker
echo "Custom marker" > trace_marker
Perf Events
強大的效能分析框架
# 系統整體分析
perf top
perf stat -a sleep 10
# Kernel 函數分析
perf record -ag
perf report
# 追蹤特定事件
perf record -e sched:sched_switch -a
perf script
# 產生火焰圖
perf record -F 99 -ag -- sleep 60
perf script | flamegraph.pl > kernel.svg
# 追蹤 kernel 函數
perf probe --add='do_sys_open filename:string'
perf record -e probe:do_sys_open -aR
eBPF - 革命性技術
基礎概念
eBPF (extended Berkeley Packet Filter) 是 Linux kernel 的革命性技術
# 使用 BCC (BPF Compiler Collection)
from bcc import BPF
prog = """
#include <linux/sched.h>
int trace_sys_open(struct pt_regs *ctx) {
char comm[TASK_COMM_LEN];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("Process %s opened a file\\n", comm);
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="do_sys_open", fn_name="trace_sys_open")
b.trace_print()
bpftrace
高階 eBPF 追蹤語言
# 一行命令追蹤
bpftrace -e 'kprobe:do_sys_open { printf("%s opened a file\n", comm); }'
# 追蹤系統調用延遲
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @start[tid] = nsecs; }
tracepoint:raw_syscalls:sys_exit /@start[tid]/ {
@ns[comm] = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'
# 追蹤 kernel 函數執行時間
bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/ {
@ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'
複雜腳本範例:
#!/usr/bin/env bpftrace
BEGIN {
printf("Tracing kernel mutex locks... Hit Ctrl-C to end.\n");
}
kprobe:mutex_lock {
@lock_start[tid] = nsecs;
@lock_stack[tid] = kstack;
}
kretprobe:mutex_lock /@lock_start[tid]/ {
$duration = nsecs - @lock_start[tid];
@lock_time = hist($duration);
if ($duration > 1000000) { // > 1ms
printf("Slow mutex: %d us\n", $duration / 1000);
printf("%s", @lock_stack[tid]);
}
delete(@lock_start[tid]);
delete(@lock_stack[tid]);
}
libbpf
原生 BPF 程式開發
// kernel space program (kern.c)
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, u32);
__type(value, u64);
} counts SEC(".maps");
SEC("kprobe/do_sys_open")
int trace_open(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *count;
count = bpf_map_lookup_elem(&counts, &pid);
if (count) {
(*count)++;
} else {
u64 init_val = 1;
bpf_map_update_elem(&counts, &pid, &init_val, BPF_ANY);
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";
動態追蹤工具
SystemTap
強大的動態追蹤系統
# 安裝
sudo apt-get install systemtap systemtap-runtime
# 簡單範例
stap -e 'probe kernel.function("do_sys_open") {
printf("%s opened %s\n", execname(), kernel_string($filename))
}'
# 複雜腳本
cat > trace_io.stp << 'EOF'
global io_count
probe vfs.read {
io_count[execname()] <<< bytes_to_read
}
probe timer.s(5) {
foreach (name in io_count) {
printf("%s: %d reads, avg %d bytes\n",
name, @count(io_count[name]), @avg(io_count[name]))
}
delete io_count
}
EOF
sudo stap trace_io.stp
kprobes / kretprobes
動態 kernel 探測點
// kernel module 範例
#include <linux/kprobes.h>
static struct kprobe kp = {
.symbol_name = "do_sys_open",
};
static int handler_pre(struct kprobe *p, struct pt_regs *regs) {
printk(KERN_INFO "do_sys_open called\n");
return 0;
}
static int __init kprobe_init(void) {
kp.pre_handler = handler_pre;
register_kprobe(&kp);
return 0;
}
靜態追蹤點
Tracepoints
預定義的追蹤點
# 列出所有 tracepoints
ls /sys/kernel/debug/tracing/events/
# 啟用特定 tracepoint
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
# 查看輸出
cat /sys/kernel/debug/tracing/trace
在 kernel module 中使用:
#include <trace/events/sched.h>
static void my_sched_switch_probe(void *data, bool preempt,
struct task_struct *prev,
struct task_struct *next) {
printk("Switch from %s to %s\n", prev->comm, next->comm);
}
// 註冊
register_trace_sched_switch(my_sched_switch_probe, NULL);
USDT (User Statically-Defined Tracing)
用戶空間靜態追蹤點
#include <sys/sdt.h>
void process_request() {
DTRACE_PROBE(myapp, request_start);
// 處理邏輯
DTRACE_PROBE1(myapp, request_end, latency);
}
Kernel 崩潰分析
kdump/crash
捕獲和分析 kernel crash dump
# 設定 kdump
sudo apt-get install kdump-tools crash
# 設定 crashkernel
# 在 /etc/default/grub 加入
GRUB_CMDLINE_LINUX_DEFAULT="crashkernel=384M-2G:128M,2G-:256M"
# 分析 crash dump
crash /usr/lib/debug/boot/vmlinux-$(uname -r) /var/crash/*/dump.*
crash> bt # backtrace
crash> ps # process list
crash> log # kernel log
crash> dis -l function_name # disassemble
KASAN (Kernel Address Sanitizer)
記憶體錯誤檢測
# Kernel 編譯選項
CONFIG_KASAN=y
CONFIG_KASAN_INLINE=y
CONFIG_TEST_KASAN=m
# 使用
insmod test_kasan.ko
KTSAN (Kernel Thread Sanitizer)
Race condition 檢測
CONFIG_KTSAN=y
即時除錯技術
Live Patching (kpatch/kGraft)
無需重啟的 kernel 修補
# 使用 kpatch
kpatch-build --sourcedir=/usr/src/linux --config=/boot/config-$(uname -r) patch.diff
kpatch load kpatch-module.ko
# 檢查載入的 patches
kpatch list
drgn
可程式化的 kernel 除錯器
# 連接到執行中的 kernel
from drgn import Program
prog = Program()
prog.set_kernel()
# 檢查 kernel 資料結構
for task in for_each_task(prog):
print(task.comm.string_(), task.pid.value_())
# 檢查特定結構
init_task = prog['init_task']
print(f"Init task state: {init_task.state.value_()}")
新興技術與工具
BTF (BPF Type Format)
提供 kernel 資料結構資訊
# 檢查 BTF 支援
ls /sys/kernel/btf/vmlinux
# 使用 bpftool 檢查
bpftool btf dump file /sys/kernel/btf/vmlinux format c
CO-RE (Compile Once - Run Everywhere)
可攜式 BPF 程式
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
SEC("kprobe/do_sys_open")
int trace_open(struct pt_regs *ctx) {
struct task_struct *task = (void *)bpf_get_current_task();
char comm[16];
bpf_core_read_str(&comm, sizeof(comm), &task->comm);
bpf_printk("Process %s opened file\n", comm);
return 0;
}
Retsnoop
失敗路徑分析工具
# 追蹤錯誤返回
sudo retsnoop -e 'tcp_*' -a ':kernel/net/ipv4/*'
# 追蹤特定錯誤碼
sudo retsnoop -e '*mount*' -c 'ret == -ENOENT'
bpftool
BPF 程式管理工具
# 列出載入的 BPF 程式
bpftool prog list
# 顯示 BPF map
bpftool map list
bpftool map dump id 1
# 追蹤 BPF 程式執行
bpftool prog trace log
實戰範例
追蹤系統調用延遲
#!/usr/bin/env bpftrace
tracepoint:raw_syscalls:sys_enter {
@start[tid] = nsecs;
@syscall[tid] = args->id;
}
tracepoint:raw_syscalls:sys_exit /@start[tid]/ {
$duration = nsecs - @start[tid];
@latency[@syscall[tid]] = hist($duration);
if ($duration > 10000000) { // > 10ms
printf("Slow syscall %d: %d ms\n", @syscall[tid], $duration / 1000000);
}
delete(@start[tid]);
delete(@syscall[tid]);
}
END {
clear(@start);
clear(@syscall);
}
記憶體分配追蹤
from bcc import BPF
prog = """
#include <linux/mm.h>
BPF_HASH(allocs, u64, u64);
TRACEPOINT_PROBE(kmem, kmalloc) {
u64 size = args->bytes_alloc;
u64 *total = allocs.lookup(&size);
if (total) {
(*total)++;
} else {
u64 one = 1;
allocs.update(&size, &one);
}
return 0;
}
"""
b = BPF(text=prog)
# 執行並顯示結果
檔案系統操作監控
# 使用 opensnoop (BCC 工具)
sudo opensnoop -p 1234 # 特定 PID
sudo opensnoop -n nginx # 特定程式名稱
# 自訂 bpftrace 腳本
bpftrace -e '
kprobe:vfs_open {
@opens[comm] = count();
}
kprobe:vfs_read {
@reads[comm] = count();
}
kprobe:vfs_write {
@writes[comm] = count();
}
interval:s:5 {
print(@opens);
print(@reads);
print(@writes);
clear(@opens);
clear(@reads);
clear(@writes);
}'
最佳實踐建議
開發階段
- 使用
pr_debug()和 dynamic debug - 編譯時開啟
CONFIG_DEBUG_*選項 - 使用 KASAN 檢測記憶體問題
測試階段
- SystemTap 或 bpftrace 進行動態分析
- Ftrace 追蹤函數調用
- perf 進行效能分析
生產環境
- eBPF 工具(低開銷)
- 有限的 tracepoints
- kdump 準備 crash 分析
問題診斷流程
- 效能問題: perf → flamegraph → bpftrace
- 記憶體問題: KASAN → kmemleak → crash dump
- 死鎖問題: lockdep → ftrace → drgn
- 系統崩潰: kdump → crash → gdb