本文是一个快速记录,旨在记录使用 QEMU 调试 Linux 内核的方法,文章内容比较短,内容也比较浅显,不会对具体原理进行分析,主打一个够用。使用的 QEMU 版本为 6.2,Linux 内核版本为 5.10,指令集架构为 x86_64。
使用 monitor 使用 monitor 的前提是要先启用该功能,一般将其设置在指定的 telnet 端口(如 6789):
1 -monitor telnet:localhost:6789,server,nowait,nodelay
使用 monitor 调试的基本场景是内核 crash 时,打开 QEMU 的 monitor,打印此时的寄存器值。为了保证 RIP 的值准确,需要在 QEMU 模拟器的内核命令行参数中关闭内核地址空间随机化:
打开新的终端使用 telnet 连接到指定端口:
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 39 $ telnet localhost 6789 Trying 127.0.0.1... Connected to localhost. Escape character is '^]' . QEMU 6.2.0 monitor - type 'help' for more information (qemu) info registers RAX=0000000000000839 RBX=00000000000a328c RCX=0000000000000000 RDX=0000000000001000 RSI=0000000000000086 RDI=0000000000001001 RBP=ffffffff82403f40 RSP=ffffffff82403ec8 R8 =ffffffff82450f20 R9 =2d2d2d5d2063696e R10=20796c6c61756e61 R11=6572656767697274 R12=0000000000000000 R13=00000000000a32f0 R14=0000000000000047 R15=0000000000000000 RIP=ffffffff8142b090 RFL=00000202 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=0 ES =0000 0000000000000000 00000000 00000000 CS =0010 0000000000000000 ffffffff 00af9b00 DPL=0 CS64 [-RA] SS =0000 0000000000000000 ffffffff 00c09300 DPL=0 DS [-WA] DS =0000 0000000000000000 00000000 00000000 FS =0000 0000000000000000 00000000 00000000 GS =0000 ffffffff82b01000 00000000 00000000 LDT=0000 0000000000000000 00000000 00008200 DPL=0 LDT TR =0020 0000000000000000 00000fff 00808900 DPL=0 TSS64-avl GDT= ffffffff82b0c000 0000007f IDT= ffffffff82c38000 00000fff CR0=80050033 CR2=ffff888000013750 CR3=0000000002b82000 CR4=000000a0 DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000 DR6=00000000ffff0ff0 DR7=0000000000000400 EFER=0000000000000d01 FCW=037f FSW=0000 [ST=0] FTW=00 MXCSR=00001f80 FPR0=0000000000000000 0000 FPR1=0000000000000000 0000 FPR2=0000000000000000 0000 FPR3=0000000000000000 0000 FPR4=0000000000000000 0000 FPR5=0000000000000000 0000 FPR6=0000000000000000 0000 FPR7=0000000000000000 0000 XMM00=0000000000000000 0000000000000000 XMM01=0000000000000000 0000000000000000 XMM02=0000000000000000 0000000000000000 XMM03=0000000000000000 0000000000000000 XMM04=0000000000000000 0000000000000000 XMM05=0000000000000000 0000000000000000 XMM06=0000000000000000 0000000000000000 XMM07=0000000000000000 0000000000000000 XMM08=0000000000000000 0000000000000000 XMM09=0000000000000000 0000000000000000 XMM10=0000000000000000 0000000000000000 XMM11=0000000000000000 0000000000000000 XMM12=0000000000000000 0000000000000000 XMM13=0000000000000000 0000000000000000 XMM14=0000000000000000 0000000000000000 XMM15=0000000000000000 0000000000000000
可以看到,此时的 pc(x86_64 架构下为 RIP 寄存器) 停在 0xffffffff8142b090 的位置,此时我们可以对内核进行反汇编并查看此时的代码上下文,注意,需要对 vmlinux 进行 dump 而不是对 bzImage:
1 objdump -d vmlinux > kernel.asm
然后搜索 RIP 值所在的指令位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ffffffff8142b090 <delay_loop>: ffffffff8142b090: 48 89 f8 mov %rdi,%rax ffffffff8142b093: 48 85 c0 test %rax,%rax ffffffff8142b096: 74 1d je ffffffff8142b0b5 <delay_loop+0x25> ffffffff8142b098: eb 06 jmp ffffffff8142b0a0 <delay_loop+0x10> ffffffff8142b09a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) ffffffff8142b0a0: eb 0e jmp ffffffff8142b0b0 <delay_loop+0x20> ffffffff8142b0a2: 66 66 2e 0f 1f 84 00 data16 cs nopw 0x0(%rax,%rax,1) ffffffff8142b0a9: 00 00 00 00 ffffffff8142b0ad: 0f 1f 00 nopl (%rax) ffffffff8142b0b0: 48 ff c8 dec %rax ffffffff8142b0b3: 75 fb jne ffffffff8142b0b0 <delay_loop+0x20> ffffffff8142b0b5: 48 ff c8 dec %rax ffffffff8142b0b8: c3 ret ffffffff8142b0b9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
使用 GDB QEMU 实现了 GDB-stub,因此能够支持使用 GDB 通过远程连接的方式调试模拟器内运行的内核。
为了方便调试的进行,需要在编译内核时开启调试信息:
在启动 QEMU 时,参数加上 -s -S,这样模拟器将会在执行时停止在第一条指令处,并开放默认端口 1234 等待 GDB client 连接。
同样,打开一个新的终端,使用 gdb vmlinux 启动 GDB 并加载内核的符号,并使用 target remote 连接到指定的 GDB 服务端,此后便可以像正常 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 29 30 31 32 $ gdb vmlinux GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git Copyright (C) 2024 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu" . Type "show configuration" for configuration details. For bug reporting instructions, please see: <https://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help , type "help" . Type "apropos word" to search for commands related to "word" ... Reading symbols from vmlinux... (gdb) target remote localhost:1234 Remote debugging using localhost:1234 0x000000000000fff0 in exception_stacks () (gdb) b start_kernel Breakpoint 1 at 0xffffffff82b2c921: file init/main.c, line 853. (gdb) c Continuing. Breakpoint 1, start_kernel () at init/main.c:853 853 set_task_stack_end_magic(&init_task); (gdb)