系列文章 : 資訊工程自學筆記
這一篇是想紀錄一下,該怎麼用 QEMU 開啟 linux kernel,並使用 gdb 接上 QEMU,並對運行在 QEMU 裡面的 linux kernel 下斷點。
第一步就是需要在 QEMU 裡面嘗試去運行 linux kernel。
很幸運的是,這在之前的筆記就有記錄下來了,詳情可以參考 https://ithelp.ithome.com.tw/articles/10399676 。
下面是懶人包,假如沒有意外的話,使用下列的指令可以在 QEMU 上運行 linux kernel。
git clone git@github.com:TommyWu-fdgkhdkgh/qemu-boot-riscv-linux.git
cd qemu-boot-riscv-linux
make buildroot/build
make linux/build
make opensbi/build
make qemu/build
make qemu/run
我們可以在 github 上取得編譯好的 image。
例如我這邊選擇 2026.05.06 release 出來的 image
https://github.com/riscv-collab/riscv-gnu-toolchain/releases/tag/2026.05.06
然後用 wget 把壓縮檔下載到自己的機器上
wget https://github.com/riscv-collab/riscv-gnu-toolchain/releases/download/2026.05.06/riscv64-glibc-ubuntu-24.04-gcc.tar.xz
接著再解壓縮
tar Jxvf riscv64-glibc-ubuntu-24.04-gcc.tar.xz
這樣子,就可以很簡單的得到 gdb !
./riscv/bin/riscv64-unknown-linux-gnu-gdb
…
(gdb)
假如想要下斷點在 linux kernel 裡面的話,多兩個設定會比較方便
我們可以進入 linux 的資料夾
cd linux
然後用 menuconfig 進入選單介面
make ARCH=riscv CROSS_COMPILE=XXX/buildroot/output/host/bin/riscv64-buildroot-linux-gnu- menuconfig
接著做以下的設定
# 替 linux kernel 加上 debug symbol
Kernel hacking
--> Compile-time checks and compiler options
--> Debug information
--> Rely on the toolchain's implicit default DWARF version
# 關掉 KASLR
Kernel features
--> Randomize the address of the kernel image
最後再重新編譯 linux kernel
make ARCH=riscv CROSS_COMPILE=XXX/buildroot/output/host/bin/riscv64-buildroot-linux-gnu- vmlinux Image -j16
XXX/qemu-boot-riscv-linux/qemu/build/qemu-system-riscv64 -M virt -bios XXX/qemu-boot-riscv-linux/opensbi/build/platform/generic/firmware/fw_jump.elf -kernel XXX/qemu-boot-riscv-linux/linux/arch/riscv/boot/Image -append "root=/dev/vda ro console=ttyS0" -drive file=XXX/qemu-boot-riscv-linux/buildroot/output/images/rootfs.ext4,format=raw,id=hd0,if=none -device virtio-blk-device,drive=hd0 -netdev user,id=net0 -device virtio-net-device,netdev=net0 -nographic -s -S
可以在 Makefile 裡面看一下,是怎麼對 QEMU 下指令的,或是在 Makefile 裡面把 @ 拿掉,就可以在使用 Makefile 時候,看一下 Makefile 下的指令長什麼樣子。
並在結尾的地方加上 -s -S
1234 port 等待 gdb 連進來。continue command 或其他繼續執行的 command。接下來可以運行 GDB
./riscv/bin/riscv64-unknown-linux-gnu-gdb ./qemu-boot-riscv-linux/linux/vmlinux
在 GDB 連接到 localhost 的 1234 port
(gdb) target remote localhost:1234
連接到之後,可以嘗試下斷點,並 continue
沒問題的話,可以嘗試列出 stack trace
(gdb) b setup_arch
(gdb) continue
(gdb) bt
#0 setup_arch (cmdline_p=cmdline_p@entry=0xffffffff81403e98) at arch/riscv/kernel/setup.c:230
#1 0xffffffff80a00750 in start_kernel () at init/main.c:897
#2 0xffffffff80001158 in _start_kernel () at arch/riscv/kernel/head.S:321
這時候會發現,原本的方法會沒辦法對開啟 paging 之前的程式碼下斷點 ( e.g. linux/arch/riscv/kernel/head.S/_start ),這需要做一些其他處理。
使用 readelf 取得 符號以及 section 資訊
./qemu-boot-riscv-linux/buildroot/output/host/bin/riscv64-buildroot-linux-gnu-readelf -s ./qemu-boot-riscv-linux/linux/vmlinux > log
我們可以看到 _start 的位址,並且可以知道 _start 是在 .head.text section。
…
1: ffffffff80000000 0 SECTION LOCAL DEFAULT 1 .head.text
…
154735: ffffffff80000000 4648 NOTYPE GLOBAL DEFAULT 1 _start
…
接著我們需要回到 QEMU
替 QEMU 加上 debug symbol,並需要重新編譯 QEMU。
cd qemu
source ./my-venv/bin/activate
./configure --target-list=riscv64-softmmu --enable-debug
make -j16
開啟 QEMU,並對 QEMU 的 qemu/hw/riscv/virt.c/virt_machine_done 下斷點觀察 kernel_start_addr 的值。
在我這邊,kernel_start_addr 會是 0x80200000
{ https://github.com/qemu/qemu/blob/stable-10.0/hw/riscv/virt.c#L1492 }
if (machine->kernel_filename && !kernel_entry) {
kernel_start_addr = riscv_calc_kernel_start_addr(&boot_info,
firmware_end_addr);
riscv_load_kernel(machine, &boot_info, kernel_start_addr,
true, NULL);
kernel_entry = boot_info.image_low_addr;
}
接著回到 GDB,一開啟 GDB 就把 symbol file 用 add-symbol-file 載入到 GDB 裡面,並明確指出 .head.text 的偏移量是 0x80200000。
接著我們就可以嘗試對 _start 下斷點,並 continue 查看有沒有成功。
./riscv/bin/riscv64-unknown-linux-gnu-gdb ./qemu-boot-riscv-linux/linux/vmlinux
(gdb) add-symbol-file ./qemu-boot-riscv-linux/linux/vmlinux -s .head.text 0x80200000
(gdb) target remote localhost:1234
(gdb) b _start
(gdb) continue