1. 项目概述:这不是 Kernel 的入门课,而是带你亲手“剖开”内核看心跳
“Kernels: A Deep Dive”这个标题乍看像一本教科书副标题,但在我过去十二年做系统底层开发、嵌入式调试和高性能服务优化的实战中,它从来不是理论推演的代名词——而是一份手术刀级的操作手册。我带过的几十个团队里,90%的新手工程师第一次真正“看见”内核在做什么,不是靠读《Linux内核设计与实现》,而是靠在一台干净的虚拟机里,亲手编译一个最小可启动内核、挂载 initramfs、用 kprobe 动态打点观测进程 fork 的完整路径、再把 perf record 的火焰图和 /proc/sched_debug 的调度器快照对齐起来看——那一刻,他们才明白什么叫“内核不是黑箱,是可触摸的精密钟表”。这篇文章要讲的,就是这套可复现、可验证、可教学的深度内核剖析方法论。它不预设你熟悉汇编或内存管理,但要求你愿意关掉 IDE,打开终端,敲下make menuconfig;它适合三类人:正在啃《深入理解 Linux 内核》却卡在第三章的系统程序员、需要排查生产环境偶发 soft lockup 的 SRE 工程师、以及想为国产芯片平台移植实时补丁的嵌入式开发者。核心关键词——内核剖析、动态观测、启动流程、调度器行为、内存映射——每一个都会落到具体命令、具体文件、具体寄存器值上,而不是停留在“内核负责进程管理”这种百科式描述。
你可能会问:现在都用容器和 Serverless 了,还抠内核干啥?我的回答很直接:上周我们线上一个 Kafka 集群出现持续 300ms 的 P99 延迟抖动,Prometheus 显示 CPU 使用率只有 45%,网络和磁盘 I/O 完全正常。最后用bpftrace -e 'kprobe:try_to_wake_up { @wakes = count(); }'发现每秒有 2.7 万次无效唤醒,根源是某个自定义 cgroup v1 的 cpu.shares 配置错误,导致调度器在 runqueue 上反复扫描空队列。这个问题,任何应用层监控都看不到,只有当你真正理解try_to_wake_up()函数里那个rq->nr_switches计数器和rq->nr_cpus_allowed位图的交互逻辑,才能一眼定位。所以,“Deep Dive”不是怀旧,是现代云原生架构下,最后一道不可绕过的性能护城河。
2. 内核剖析的整体设计思路:为什么必须放弃“静态阅读”,转向“动态切片”
2.1 传统学习路径的三大断层与真实代价
我见过太多人陷在“读代码—>看不懂—>换书—>再卡住”的死循环里。问题不在人,而在方法。传统路径存在三个致命断层:
第一,源码与运行态的断层。比如mm/memory.c里的handle_mm_fault()函数,静态阅读时你看到的是页表项(PTE)的 set/clear 操作,但实际运行中,x86_64 下它可能触发 TLB miss 导致 microcode 级别的 page walk,ARM64 下则可能因 ASID 切换引发 TLB shootdown 中断。这些硬件细节,源码注释不会写,但它们直接决定一次缺页处理耗时是 150ns 还是 3.2μs。我曾为一个数据库引擎优化,发现其随机读延迟方差极大,最终用perf record -e 'mem-loads,mem-stores'抓到大量MEM_LOAD_RETIRED.L1_MISS事件,根源是内核在alloc_pages()时未对齐 hugepage boundary,导致硬件 prefetcher 失效——这完全无法从 C 代码行间推断。
第二,抽象层与物理资源的断层。“进程”在课本里是 PCB 结构体,在现实中是 CPU 寄存器上下文 + TLB 条目 + cache line 占用 + 中断控制器状态的集合体。当fork()返回后,子进程的%rsp寄存器指向哪里?它的 L1d cache 是否被父进程污染?这些不摸到寄存器层面,永远是模糊概念。我们曾在线上遇到一种诡异现象:同一台机器上,两个相同配置的 Java 应用,一个 GC STW 时间稳定在 8ms,另一个波动到 45ms。用rdmsr -a 0x1b读取 IA32_SYSENTER_ESP MSR 发现,高延迟节点的 sysenter stack 被频繁切换,追查到是某个驱动模块在__do_softirq()中调用了schedule_timeout(),意外触发了内核栈切换——这种问题,只看kernel/fork.c的copy_process()函数,连边都摸不到。
第三,配置与行为的断层。CONFIG_PREEMPT=y和CONFIG_PREEMPT_RT=y看似只是 Kconfig 选项,但前者仅让内核抢占点增加,后者则重写了整个调度器锁机制,将spinlock_t替换为rt_mutex_t,并把中断线程化。这意味着开启 RT 补丁后,local_irq_disable()不再关闭本地中断,而是将中断上下文转为高优先级内核线程执行。某次我们为工业控制设备启用 PREEMPT_RT,结果发现串口数据丢包率上升 300%,最终定位到是serial_core.c中一个spin_lock(&port->lock)在 RT 模式下被转化为优先级继承锁,而用户空间串口工具又在低优先级线程中持有该锁长达 20ms——静态看代码毫无问题,运行态却是灾难。
2.2 我们的设计哲学:“四层切片法”直击内核脉搏
为弥合上述断层,我提炼出“四层切片法”,每一层对应一个可观测、可干预、可验证的内核切面,且全部基于主线内核(v6.1+)无需 patch:
启动切片(Boot Slice):从
arch/x86/kernel/head_64.S的_startup_64开始,用 QEMU + GDB 单步跟踪第一条指令如何建立初始页表、如何跳转到start_kernel()。重点观测early_idt_handler_array如何处理早期异常,setup_arch()中init_memory_mapping()如何将物理内存映射到内核线性地址空间。这一层解决“内核从哪来”的问题,避免所有关于“内核加载地址”的玄学讨论。调度切片(Sched Slice):以
kernel/sched/core.c的pick_next_task()为核心,用 ftrace 动态插桩,捕获每次任务切换前后的rq->nr_running、rq->nr_switches、p->se.vruntime值,并关联sched_switchtracepoint 输出的 prev/next pid。我们自制了一个 Python 脚本,自动解析 ftrace 输出,生成“调度决策热力图”,清晰显示哪个 CPU 的 runqueue 长期堆积、哪个进程的 vruntime 异常偏移——这比top或htop看到的 CPU% 真实十倍。内存切片(MM Slice):放弃
cat /proc/meminfo这种汇总视图,直接读/sys/kernel/debug/page_owner(需开启CONFIG_PAGE_OWNER),配合crash工具解析vmcore,定位具体哪一行kmalloc()分配了泄漏的 4MB 连续内存。更进一步,用pahole -C page查看struct page在内存中的实际布局,确认page->lru和page->mapping是否因结构体填充(padding)导致 cache line false sharing——这是 NUMA 敏感型应用性能瓶颈的常见元凶。中断切片(IRQ Slice):用
cat /proc/interrupts只能看到中断计数,我们要看到中断生命周期。通过trace-cmd record -e irq:irq_handler_entry -e irq:irq_handler_exit抓取单次中断从进入 handler 到退出的完整时间戳,再用perf script关联irq_work_run()的执行栈,判断是否因irq_work积压导致软中断延迟。某次我们发现网卡 RX 中断处理耗时正常(<50μs),但net_rx_action()执行却延迟 8ms,最终发现是ksoftirqd/0线程被其他高优先级实时任务饿死——这只有在 IRQ Slice 下才能暴露。
这套方法的核心逻辑是:拒绝把内核当整体,而把它当作一组松耦合、可观测、可独立调试的子系统集合。每个切片都有明确的入口点、可观测指标、可验证假设。比如调度切片的验证假设是:“当rq->nr_running > 1且rq->curr->se.vruntime显著小于rq->cfs.min_vruntime时,pick_next_task_fair()必须返回rq->cfs.curr”。你可以用stress-ng --cpu 4 --timeout 10s制造负载,然后用 ftrace 实时验证该假设是否被违反——这才是真正的“Deep Dive”。
3. 核心细节解析与实操要点:从编译第一个可调试内核开始
3.1 构建可调试内核环境:为什么必须自己编译,而非用发行版内核
很多工程师觉得“用apt install linux-image-debug就行”,这是最大的误区。发行版内核为了通用性,禁用了大量调试符号和追踪功能。以 Ubuntu 22.04 的5.15.0-107-generic为例,其vmlinux文件大小仅 42MB,而我们自己编译的带完整调试信息的内核是 386MB。关键差异在于:
- 缺少
CONFIG_DEBUG_INFO_DWARF4=y:导致 GDB 无法解析struct task_struct的成员偏移,p $rdi->pid会报错 “no symbol table”。 - 禁用
CONFIG_FTRACE=y和CONFIG_FUNCTION_TRACER=y:ftrace 的function_graphtracer 无法启用,你将失去函数级调用栈追踪能力。 CONFIG_KPROBES=y被设为模块(m)而非内置(y):kprobe 在内核启动早期无法使用,错过start_kernel()阶段的关键观测窗口。
因此,构建第一步必须是:从 kernel.org 下载纯净源码,手动配置,全程可控。以下是经过我们团队在 37 台不同配置机器上验证的最小可行配置清单(基于 v6.6):
# 1. 获取源码并解压 wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz tar -xf linux-6.6.tar.xz && cd linux-6.6 # 2. 生成最小基础配置(基于 x86_64 defconfig) make x86_64_defconfig # 3. 启用关键调试选项(这才是 Deep Dive 的基石) scripts/config \ -e CONFIG_DEBUG_INFO_DWARF4 \ -e CONFIG_FTRACE \ -e CONFIG_FUNCTION_TRACER \ -e CONFIG_FUNCTION_GRAPH_TRACER \ -e CONFIG_KPROBES \ -e CONFIG_KPROBE_EVENTS \ -e CONFIG_BPF_SYSCALL \ -e CONFIG_BPF_JIT \ -e CONFIG_PAGE_OWNER \ -e CONFIG_DEBUG_FS \ -e CONFIG_PROC_PAGE_MONITOR \ -d CONFIG_MODULE_SIG # 4. 编译(使用 8 线程加速,但保留符号表) make -j8 bzImage modules sudo make modules_install sudo cp arch/x86_64/boot/bzImage /boot/vmlinuz-6.6-dive提示:
CONFIG_MODULE_SIG被禁用是因为签名模块会干扰 kprobe 对内核函数的 hook,而CONFIG_PAGE_OWNER是内存泄漏分析的黄金开关,但它会带来约 5% 的内存分配开销,仅在调试时启用。
编译完成后,不要急着重启。先用file vmlinux确认输出文件包含 DWARF 调试信息:
$ file vmlinux vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=..., with debug_info, not stripped如果显示 “stripped”,说明CONFIG_DEBUG_INFO_DWARF4未生效,需检查.config文件。这个步骤看似琐碎,但它是后续所有深度观测的前提——就像做外科手术前必须确认无影灯亮度足够,否则一切精细操作都是徒劳。
3.2 启动切片实操:用 QEMU + GDB 单步跟踪内核第一条指令
QEMU 是最安全的内核调试沙盒,无需重启物理机。我们采用-s -S参数组合,让 QEMU 在启动时暂停并等待 GDB 连接:
# 启动 QEMU,加载我们编译的内核,并开放 GDB 调试端口 qemu-system-x86_64 \ -kernel /path/to/linux-6.6/arch/x86_64/boot/bzImage \ -initrd /path/to/initramfs.cgz \ # 最小 initramfs,仅含 busybox -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" \ -nographic \ -s -S \ # -s 等价于 -gdb tcp::1234,-S 表示启动即暂停 -m 2G \ -cpu host此时 QEMU 窗口会静止,等待 GDB 连接。新开终端,启动 GDB 并加载内核符号:
# 启动 GDB,加载 vmlinux(注意:不是 bzImage!) gdb vmlinux # 连接到 QEMU 的 GDB server (gdb) target remote :1234 # 设置架构为 64 位(QEMU 默认可能是 32 位) (gdb) set architecture i386:x86-64 # 查看当前 CPU 状态,确认在实模式还是保护模式 (gdb) info registers rax 0x0 0x0 rbx 0x0 0x0 rcx 0x0 0x0 rdx 0x0 0x0 rsi 0x10000 0x10000 rdi 0x10000 0x10000 rip 0xfffffffffffeffff 0xfffffffffffeffff # 注意:rip 为 0xfffffffffffeffff,这是实模式下的起始地址关键来了:内核第一条有效指令在arch/x86/kernel/head_64.S的_startup_64标签处。但此时 CPU 还在实模式,我们需要先执行到保护模式切换点。查阅head_64.S,找到lgdt指令加载全局描述符表的位置:
# arch/x86/kernel/head_64.S startup_64: # ... 一些初始化 ... lgdt __BOOT_CS(__boot_gdt) movq %rax, %cr3 # 加载页表基址 movq $(X86_CR4_PAE | X86_CR4_PSE | X86_CR4_OSFXSR | X86_CR4_OSXMMEXCPT), %rax movq %rax, %cr4 movq $(X86_EFER_LME | X86_EFER_LMA), %rax wrmsr movq $(X86_CR0_PE | X86_CR0_PG | X86_CR0_WP | X86_CR0_AM), %rax movq %rax, %cr0 # 关键!开启分页和保护模式 jmp *(__startup_64) # 跳转到 64 位模式入口我们在movq %rax, %cr0这一行下断点:
(gdb) b *0xffffffff81000000+0x123 # 具体偏移需用 objdump 查看,此处为示意 (gdb) c当 GDB 停在movq %rax, %cr0时,执行stepi单步,观察cr0寄存器变化:
(gdb) stepi (gdb) info registers cr0 cr0 0x80050033 0x80050033 # bit 0 (PE) 和 bit 31 (PG) 已置位,保护模式和分页已开启此时,CPU 已进入 64 位长模式。下一步是跳转到__startup_64,我们继续stepi,直到看到rip变为0xffffffff81000000附近的大地址——这就是内核被加载到的虚拟地址。这时,我们可以用list *$rip查看对应的 C 代码(如start_kernel()),真正实现从汇编到 C 的无缝衔接。
实操心得:很多新手卡在
info registers看不到cr0,是因为 GDB 默认不显示控制寄存器。必须用info registers cr0显式指定。另外,objdump -d vmlinux | grep startup_64是查找准确地址的最快方法,比翻源码快十倍。
3.3 调度切片实操:用 ftrace 解析一次 fork() 的完整调度链路
fork()看似简单,实则触发内核最复杂的调度路径之一。我们用 ftrace 捕获从用户态clone()系统调用,到子进程首次获得 CPU 时间的全过程:
# 1. 清空 ftrace 缓冲区 echo 0 > /sys/kernel/debug/tracing/tracing_on echo > /sys/kernel/debug/tracing/trace # 2. 启用关键 tracepoints echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_fork/enable echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable echo 1 > /sys/kernel/debug/tracing/events/syscalls/sys_enter_clone/enable echo 1 > /sys/kernel/debug/tracing/events/syscalls/sys_exit_clone/enable # 3. 启动 tracing echo 1 > /sys/kernel/debug/tracing/tracing_on # 4. 在另一终端执行 fork 测试 stress-ng --fork 1 --timeout 1s # 5. 停止 tracing 并查看结果 echo 0 > /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace典型输出如下(已精简):
bash-1234 [000] d... 12345.678901: sys_enter_clone: NR 56 bash-1234 [000] d... 12345.678905: sched_process_fork: comm=bash pid=1234 child_comm=bash child_pid=1235 bash-1234 [000] d... 12345.678910: sched_wakeup: comm=bash pid=1235 prio=120 success=1 target_cpu=0 bash-1234 [000] d... 12345.678915: sched_switch: prev_comm=bash prev_pid=1234 prev_prio=120 prev_state=R ==> next_comm=bash next_pid=1235 next_prio=120关键洞察在于sched_wakeup和sched_switch的时间戳差值:这里只有 5μs,说明子进程被立即调度。但如果在高负载下,这个差值可能达到毫秒级。我们写了一个解析脚本,自动计算每个sched_wakeup到对应sched_switch的延迟,并按 CPU 分组统计:
# parse_fork_delay.py import re from collections import defaultdict delays = defaultdict(list) with open('/sys/kernel/debug/tracing/trace') as f: lines = f.readlines() for i, line in enumerate(lines): if 'sched_wakeup' in line and 'child_pid=1235' in line: wake_time = float(re.search(r'(\d+\.\d+):', line).group(1)) # 向后查找同 pid 的 sched_switch for j in range(i, min(i+100, len(lines))): if f'sched_switch' in lines[j] and f'next_pid=1235' in lines[j]: switch_time = float(re.search(r'(\d+\.\d+):', lines[j]).group(1)) delays['cpu0'].append(switch_time - wake_time) break for cpu, dlist in delays.items(): print(f"{cpu}: avg delay {sum(dlist)/len(dlist)*1e6:.1f}μs")这个脚本揭示了调度器的真实响应能力。在我们的测试中,当CONFIG_NO_HZ_FULL=y(无滴答模式)开启时,cpu0的平均唤醒延迟从 12μs 降至 3.2μs,但cpu1却升至 85μs——因为nohz_full将 tick 迁移到了cpu0,导致cpu1的hrtimer中断被延迟。这种细粒度的量化,是任何宏观监控都无法提供的。
注意:
sched_wakeup的success=1表示唤醒成功,但如果看到success=0,说明目标进程已被SIGSTOP暂停,或处于不可中断睡眠(D 状态),这是排查“进程假死”的关键线索。
4. 实操过程与核心环节实现:从内存映射到中断生命周期的全链路验证
4.1 内存切片:用 page_owner 定位一次真实的内存泄漏
假设我们有一个内核模块leak_mod.ko,其中有一行kmalloc(4096, GFP_KERNEL)被调用但未释放。传统方式用slabtop只能看到kmalloc-4k缓存占用增长,但无法定位到具体模块。page_owner是终极解决方案:
# 1. 开启 page_owner(需内核配置支持) echo 1 > /sys/kernel/debug/page_owner_enabled # 2. 加载泄漏模块 insmod leak_mod.ko # 3. 触发泄漏(模块内部有循环分配) echo 1 > /sys/module/leak_mod/parameters/trigger_leak # 4. 生成 page_owner 报告 echo m > /proc/sysrq-trigger # 触发 sysrq,生成 /sys/kernel/debug/page_owner报告位于/sys/kernel/debug/page_owner,内容类似:
page:00000000abcd1234 refcount:1 mapcount:0 mapping:00000000efgh5678 index:0x0 flags: 0x2000000000000000() raw: 0200000000000000 ffffffffffffffff ffffffffffffffff 0000000000000000 page dumped because: page_owner is enabled ... stack trace: [<ffffffff810a1234>] kmalloc_order+0x45/0x120 [<ffffffff810a1345>] kmalloc+0x123/0x200 [<ffffffffc0001234>] leak_mod_init+0x56/0x1000 [leak_mod] [<ffffffff81002345>] do_one_initcall+0x56/0x120关键信息是stack trace部分,它精确指出泄漏发生在leak_mod_init()函数的第 56 行。我们甚至可以反向解析leak_mod.ko的符号表:
# 获取模块的 .ko 文件地址 readelf -S leak_mod.ko | grep text # 然后用 addr2line 定位源码行 addr2line -e leak_mod.ko 0xc0001234实操心得:
page_owner会显著增加内存分配开销,生产环境切勿长期开启。我们团队的标准流程是:在 staging 环境开启 10 分钟,复现问题后立即关闭。另外,/proc/sysrq-trigger的m命令会 dump 当前所有 page owner,如果内存很大,文件会非常大,建议先echo 1 > /sys/kernel/debug/page_owner_disabled再echo m,避免 OOM。
4.2 中断切片:用 trace-cmd 解析网卡 RX 中断的完整生命周期
网络延迟问题往往藏在中断处理链路中。我们以 Intel e1000e 网卡为例,用trace-cmd抓取一次完整 RX 中断:
# 1. 清空 trace 缓冲 trace-cmd reset # 2. 记录关键事件:中断进入/退出、软中断执行、NAPI poll trace-cmd record \ -e irq:irq_handler_entry \ -e irq:irq_handler_exit \ -e irq:softirq_entry \ -e irq:softirq_exit \ -e napi:napi_poll # 3. 在另一终端发送一个 ping 包触发 RX ping -c 1 192.168.1.1 # 4. 停止记录并解析 trace-cmd report > rx_trace.txtrx_trace.txt的关键片段:
e1000e-24 [000] d..1 12345.678901: irq_handler_entry: irq=24 name=e1000e e1000e-24 [000] d..1 12345.678905: irq_handler_exit: irq=24 ret=handled e1000e-24 [000] d..1 12345.678910: softirq_entry: vec=3 [action=NAPI] e1000e-24 [000] d..1 12345.678915: napi_poll: napi=00000000abcd1234 work=1024 e1000e-24 [000] d..1 12345.678920: softirq_exit: vec=3时间线清晰显示:中断处理(entry→exit)耗时 4μs,软中断执行(entry→exit)耗时 10μs,其中napi_poll占用 5μs。但如果napi_poll的work值远小于网卡 ring buffer 大小(如work=1024而 ring size=4096),说明 NAPI 未一次清空缓冲区,可能触发下一轮中断,形成“中断风暴”。我们曾在一个高吞吐场景中发现work值稳定在 64,而 ring size 是 2048,导致每秒产生 12 万次中断,CPU 花费 35% 时间在中断上下文切换上。解决方案是调大net.core.netdev_budget(默认 300),但必须同步调大net.core.netdev_max_backlog,否则丢包率会上升——这是典型的“改一个参数,牵动一整条链路”的内核特性。
4.3 四层切片联动:一次真实故障的全链路归因
去年我们处理过一个经典案例:某金融交易系统在每日 9:30 开市瞬间,订单处理延迟从 200μs 飙升至 12ms,持续 3 分钟后自动恢复。用四层切片法,我们 17 分钟内定位根因:
启动切片:
dmesg | grep -i "microcode"发现开市前 1 分钟,Intel 微码更新触发了microcode: updated early,但cat /sys/devices/system/cpu/vulnerabilities/spec_store_bypass显示仍为Vulnerable,说明微码未正确加载。这是起点。中断切片:
trace-cmd record -e irq:irq_handler_entry -e irq:irq_handler_exit抓取开市瞬间,发现irq=32(GPU 相关中断)计数激增 200 倍。lspci -vv -s 00:02.0显示这是集成显卡,而交易系统 GUI 进程在开市时刷新行情图,触发 GPU 渲染中断。调度切片:
ftrace显示sched_switch日志中,ksoftirqd/0线程的prev_state长期为R(运行态),但next_comm却是java进程,说明软中断处理严重阻塞了 Java 线程调度。内存切片:
cat /proc/$(pgrep java)/status | grep VmRSS发现 RSS 突增 1.2GB,pstack $(pgrep java)显示大量线程卡在pthread_cond_wait。结合page_owner,发现是 GUI 进程的 OpenGL 上下文创建,占用了大量连续物理内存,触发内核alloc_contig_range(),进而导致kswapd频繁扫描内存,间接饿死了ksoftirqd。
最终方案是:将 GUI 进程绑定到专用 CPU 核心(taskset -c 7 ./gui_app),并设置isolcpus=7隔离该核心,确保交易核心线程不受干扰。这个案例完美诠释了四层切片的价值:单一层只能看到症状,四层联动才能锁定病灶。
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪经验”
5.1 QEMU GDB 调试的五大“静默失败”场景及修复
QEMU + GDB 是最常用的内核调试组合,但有五个场景会导致“看似在调试,实则无效”,且错误信息极其隐蔽:
GDB 加载错误的 vmlinux:最常见的错误是
gdb vmlinux-5.15但实际运行的是v6.6内核。症状是info registers正常,但list *$rip显示 “No such file or directory”。修复:用readelf -h vmlinux | grep Entry确认入口地址,再用qemu-system-x86_64 -kernel指定的内核版本必须严格匹配。QEMU CPU 模式不匹配:在 ARM64 主机上运行 x86_64 内核,若未指定
-cpu qemu64,QEMU 可能降级为 i486 模式,导致movq %rax, %cr0指令非法。症状是 GDB 停在0xfffffffffffeffff后无法stepi。修复:强制指定-cpu host或-cpu qemu64,+smep,+smap。GDB 架构未切换:x86_64 内核启动后,GDB 默认仍是 i386 架构,
info registers只显示 32 位寄存器。症状是rip显示为0x0。修复:set architecture i386:x86-64后,再info registers才能看到rip,rax等 64 位寄存器。符号表路径错误:当内核源码在
/src/linux,而vmlinux编译在/build/linux,GDB 默认在/src/linux查找源码。症状是list命令找不到文件。修复:set substitute-path /src/linux /build/linux。QEMU 内存映射冲突:某些 QEMU 版本(如 6.2)与内核 v6.6 存在内存布局 bug,导致
bzImage加载地址与内核期望的0xffffffff81000000冲突。症状是start_kernel()断点无法命中。修复:升级 QEMU 到 7.2+,或在make menuconfig中修改CONFIG_PHYSICAL_START=0x1000000。
提示:我们维护了一个一键检测脚本
check_qemu_gdb.sh,它自动执行上述五项检查并输出 PASS/FAIL,新成员入职第一天就要跑通这个脚本。
5.2 ftrace 的“幽灵丢失”问题:为什么你的 tracepoint 没有输出?
ftrace 是内核观测的基石,但新手常抱怨“明明启用了sched_switch,却什么也看不到”。根本原因有三:
tracing_on 被意外关闭:
echo 0 > /sys/kernel/debug/tracing/tracing_on后,即使echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable,也不会记录。必须echo 1 > /sys/kernel/debug/tracing/tracing_on。我们习惯在启用事件后立即执行此命令。**缓冲区溢出被