Skip to content

KGDB trên QEMU

Trang này hướng dẫn dựng môi trường thực hành KGDB bằng máy ảo QEMU thay cho board thật: kernel guest chạy KGDB, gdb trên host kết nối qua một cổng serial ảo. Nội dung đi theo thứ tự: phân biệt KGDB trong guest với gdbstub có sẵn của QEMU, sơ đồ kết nối, build kernel, tạo initramfs, chạy QEMU, kết nối gdb, dùng kdb, debug một module và các lỗi thường gặp. Phần nguyên lý của KGDB (debug core, kgdboc, tham số boot) nằm ở trang KGDB và kdb.

Các lệnh dựa trên tài liệu kernel bản 7.4-rc1 và tài liệu QEMU bản 8.2.2. Ví dụ dùng x86_64.

Chưa chạy thử

Quy trình trên trang này được dựng từ tài liệu kernel và tài liệu QEMU, chưa được chạy thử từ đầu đến cuối. Cần chạy lại toàn bộ các bước, ghi lại output thực tế và bỏ admonition này sau khi kiểm chứng.

Hai cách dùng gdb với kernel trong QEMU

QEMU có sẵn một gdbstub ở mức máy ảo, bật bằng -s (tương đương -gdb tcp::1234) và -S (dừng CPU ngay khi khởi động, chờ gdb ra lệnh chạy). gdbstub này thuộc về QEMU, không chạy trong kernel guest. KGDB thì chạy bên trong kernel guest và giao tiếp với gdb qua cổng serial ảo, giống hệt khi chạy trên board.

Tiêu chí gdbstub của QEMU (-s -S) KGDB trong guest
Nằm ở đâu Trong QEMU, ngoài guest Trong kernel guest
Cấu hình kernel Không cần, chỉ cần thông tin debug trong vmlinux CONFIG_KGDB, CONFIG_KGDB_SERIAL_CONSOLE
Đường kết nối TCP do QEMU mở, mặc định cổng 1234 Cổng serial ảo của guest, được QEMU đưa ra TCP
Dừng được từ lúc nào Từ lệnh đầu tiên của máy ảo Khi I/O driver của KGDB đã sẵn sàng (sớm nhất với kgdbwait hoặc kgdboc_earlycon)
Ngắt bằng Ctrl-C trong gdb Có Không, phải gửi SysRq-G
Có kdb Không Có, nếu bật CONFIG_KGDB_KDB
Giá trị khi thực hành Debug logic chung và giai đoạn boot rất sớm Tập cấu hình và thao tác KGDB, kdb trước khi làm trên board thật

Nếu chỉ cần debug kernel trong máy ảo, gdbstub của QEMU đơn giản hơn. Dựng KGDB trên QEMU có ý nghĩa khi cần làm quen với kgdboc, SysRq-G, kdb và quy trình kết nối qua serial, hoặc khi cần kiểm tra một kernel config có KGDB trước khi nạp lên board.

Sơ đồ kết nối

Guest dùng hai cổng serial. Cổng thứ nhất (ttyS0) làm console, được QEMU nối vào stdio của terminal và ghép chung với QEMU monitor. Cổng thứ hai (ttyS1) dành riêng cho KGDB, được QEMU mở thành một TCP server trên host để gdb kết nối vào.

Kết nối giữa terminal, gdb, QEMU và kernel guest

Tách hai cổng như vậy tránh được việc phải luân phiên giữa terminal và gdb trên cùng một cổng như khi dùng một cáp serial trên board. Tham số kgdbcon cũng dùng được trong cấu hình này, vì ttyS1 không phải system console.

Build kernel

Từ thư mục mã nguồn kernel, tạo cấu hình mặc định cho x86_64 rồi bật các tùy chọn cần cho KGDB:

make O=build x86_64_defconfig
scripts/config --file build/.config \
    -e KGDB -e KGDB_SERIAL_CONSOLE -e KGDB_KDB \
    -d DEBUG_INFO_NONE -e DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT \
    -d DEBUG_INFO_REDUCED -e GDB_SCRIPTS \
    -d RANDOMIZE_BASE
make O=build olddefconfig
make O=build -j"$(nproc)"
make O=build scripts_gdb

Kết quả cần dùng là build/arch/x86/boot/bzImage để boot và build/vmlinux để nạp vào gdb.

Ghi chú về cấu hình:

  • x86_64_defconfig đã bật sẵn CONFIG_DEBUG_KERNEL, CONFIG_MAGIC_SYSRQ, CONFIG_SERIAL_8250_CONSOLE, CONFIG_BLK_DEV_INITRD và CONFIG_DEVTMPFS_MOUNT, nên không cần bật thêm.
  • Tắt CONFIG_RANDOMIZE_BASE có tác dụng giống tham số boot nokaslr. Có thể giữ option và thêm nokaslr vào command line.
  • Trên x86, CONFIG_STRICT_KERNEL_RWX luôn bật và không tắt được trong menuconfig. Điều này không chặn software breakpoint: kgdb_arch_set_breakpoint() trong arch/x86/kernel/kgdb.c thử ghi trực tiếp, nếu không được thì dùng text_poke_kgdb() để ghi lệnh breakpoint vào vùng code chỉ đọc. Vì vậy trên x86 không cần rodata=off.
  • Không cần CONFIG_FRAME_POINTER. Unwinder mặc định của x86_64 là ORC; gdb dựng backtrace từ thông tin DWARF trong vmlinux.

Tạo initramfs tối thiểu

Lab chỉ cần một shell trong guest để gửi SysRq-G và nạp module. Một initramfs chứa busybox build tĩnh là đủ. Trên Ubuntu, gói busybox-static cung cấp file /bin/busybox.

mkdir -p rootfs/{bin,dev,proc,sys}
cp /bin/busybox rootfs/bin/
for app in $(rootfs/bin/busybox --list); do ln -sf busybox "rootfs/bin/$app"; done

cat > rootfs/init <<'EOF'
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
exec setsid cttyhack sh
EOF
chmod +x rootfs/init

(cd rootfs && find . | cpio -o -H newc | gzip) > rootfs.cpio.gz

Kernel chạy /init trong initramfs làm process đầu tiên. Applet cttyhack của busybox gắn shell vào console hiện tại để Ctrl-C và job control hoạt động.

Chạy QEMU

qemu-system-x86_64 -m 1G -smp 2 -nographic \
    -kernel build/arch/x86/boot/bzImage \
    -initrd rootfs.cpio.gz \
    -append "console=ttyS0 kgdboc=ttyS1,115200 nokaslr" \
    -serial mon:stdio \
    -serial tcp::5551,server=on,wait=off
Tùy chọn Ý nghĩa
-nographic Không mở cửa sổ đồ họa, đưa serial ra terminal
-kernel, -initrd, -append Boot trực tiếp kernel, initramfs và command line mà không cần ổ đĩa hay bootloader
-serial mon:stdio Cổng serial thứ nhất (ttyS0) nối vào stdio, ghép chung với QEMU monitor
-serial tcp::5551,server=on,wait=off Cổng serial thứ hai (ttyS1) mở TCP server trên cổng 5551 của mọi địa chỉ; wait=off cho guest boot ngay mà không chờ client kết nối
kgdboc=ttyS1,115200 Dùng ttyS1 cho KGDB
nokaslr Giữ địa chỉ kernel khớp với vmlinux (thừa nếu đã tắt CONFIG_RANDOMIZE_BASE)

Thứ tự các tùy chọn -serial quyết định tên thiết bị trong guest: tùy chọn đầu tiên là ttyS0, tùy chọn thứ hai là ttyS1. QEMU giả lập tối đa 4 cổng serial. Cổng 5551 chỉ là ví dụ, có thể chọn cổng bất kỳ còn trống.

Với -nographic, terminal là bộ ghép kênh (multiplexer) giữa console và monitor. Các phím điều khiển bắt đầu bằng Ctrl-a:

Phím Tác dụng
Ctrl-a b Gửi tín hiệu break trên serial, tương đương phím SysRq của Linux
Ctrl-a c Chuyển qua lại giữa console và QEMU monitor
Ctrl-a x Thoát QEMU
Ctrl-a h In danh sách phím

Vì monitor được ghép vào stdio, Ctrl-C không thoát QEMU mà được chuyển vào guest.

Để dừng kernel ngay trong lúc boot, thêm kgdbwait vào sau kgdboc=ttyS1,115200 trong -append. kgdboc phải build sẵn trong kernel (mặc định khi bật CONFIG_KGDB_SERIAL_CONSOLE=y).

Kết nối gdb

Sau khi guest boot xong và hiện dấu nhắc shell, đưa kernel vào debugger bằng một trong hai cách:

  • Gõ trong shell của guest: echo g > /proc/sysrq-trigger.
  • Nhấn Ctrl-a b, sau đó nhấn g trong vòng 5 giây (SYSRQ_TIMEOUT trong include/linux/serial_core.h).

Console in thông báo đang chờ debugger. Trên một terminal khác của host, chạy gdb từ thư mục build để gdb tự nạp các helper script:

$ cd build
$ gdb vmlinux
(gdb) target remote :5551
(gdb) bt
(gdb) lx-dmesg
(gdb) continue

Nếu gdb từ chối nạp vmlinux-gdb.py, thêm add-auto-load-safe-path /path/to/linux/build vào ~/.gdbinit. Không cần set serial baud vì kết nối là TCP.

Sau continue, kernel chạy tiếp và gdb chờ. Muốn dừng lại thì gửi SysRq-G lần nữa từ console của guest; Ctrl-C trong gdb không ngắt được KGDB. Một cách tạo điểm dừng chủ động là đặt breakpoint ở ksys_sync rồi chạy sync trong guest:

(gdb) break ksys_sync
(gdb) continue

Kết thúc phiên bằng detach để kernel chạy tiếp. QEMU chỉ nhận một kết nối TCP tại một thời điểm trên cổng 5551, nên phải thoát gdb cũ trước khi kết nối lại.

Dùng kdb trên console

Muốn dùng kdb ngay trên terminal đang chạy QEMU, cho kgdboc dùng ttyS0 thay cho ttyS1:

-append "console=ttyS0 kgdboc=ttyS0,115200 nokaslr"

Hoặc chuyển lúc chạy, không cần boot lại:

echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc

Sau đó nhấn Ctrl-a b rồi g, console hiện dấu nhắc kdb>. Chạy help, ps, bt, dmesg, lsmod, và go để kernel chạy tiếp. Danh sách lệnh và cách giới hạn quyền của kdb xem ở Dùng kdb.

Không nên dùng gdb trên ttyS0 trong cấu hình này, vì cổng đó đang là stdio của terminal và không có TCP để gdb kết nối.

Debug một module

Ví dụ debug hàm init của một module ngoài cây mã nguồn kernel. Module mẫu hello.c:

#include <linux/module.h>
#include <linux/init.h>

static int counter;

static int __init hello_init(void)
{
    counter = 42;
    pr_info("hello: counter=%d\n", counter);
    return 0;
}

static void __exit hello_exit(void)
{
    pr_info("hello: exit\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

Makefile đi kèm:

obj-m := hello.o

Build với kernel đã build ở trên, chép vào initramfs và tạo lại file cpio:

make -C /path/to/linux/build M=$PWD modules
cp hello.ko rootfs/
(cd rootfs && find . | cpio -o -H newc | gzip) > rootfs.cpio.gz

Khởi động lại QEMU. Dừng kernel bằng SysRq-G, kết nối gdb, rồi đặt breakpoint chờ (pending) vào hàm của module chưa nạp:

(gdb) target remote :5551
(gdb) lx-symbols /path/to/hello
(gdb) break hello_init
Function "hello_init" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
(gdb) continue

Trong guest chạy insmod /hello.ko. lx-symbols đặt một breakpoint nội bộ tại do_init_module() trong kernel/module/main.c, nên mỗi khi có module được nạp, script tự nạp symbol của module đó; gdb sau đó dừng tại hello_init. Từ đây dùng next, print counter, bt như debug chương trình thông thường. lx-symbols lấy vmlinux và tìm file .ko đệ quy từ thư mục làm việc hiện tại của gdb; các đường dẫn truyền thêm (cách nhau bởi dấu cách) được bổ sung vào danh sách tìm kiếm. Cách viết module được trình bày ở Kernel Module.

Kết hợp với gdbstub của QEMU

Một số lỗi xảy ra trước khi kgdboc sẵn sàng, ví dụ trong giai đoạn đầu của start_kernel(). Khi đó dùng gdbstub của QEMU: thêm -s -S vào lệnh QEMU, kết nối bằng target remote :1234 và đặt breakpoint, ví dụ break start_kernel, rồi continue. Phần boot của kernel được trình bày ở Kernel Boot Process.

Hai gdbstub dùng hai cổng khác nhau (1234 và 5551) và nằm ở hai tầng khác nhau. Nên dùng một trong hai cho mỗi phiên debug: khi gdbstub của QEMU dừng máy ảo thì kernel guest, kể cả KGDB, cũng dừng theo.

Với gdbstub của QEMU, hành vi single step mặc định là tắt ngắt và timer trong lúc step, có thể chỉnh bằng maintenance packet Qqemu.sstep=<mask>. Các lệnh này chỉ áp dụng cho gdbstub của QEMU, không áp dụng cho KGDB.

Lỗi thường gặp

Hiện tượng Nguyên nhân thường gặp Cách xử lý
Breakpoint không dừng, địa chỉ symbol lệch KASLR đang bật Thêm nokaslr hoặc tắt CONFIG_RANDOMIZE_BASE
target remote :5551 không có phản hồi Kernel chưa dừng trong debugger Gửi SysRq-G trước khi kết nối
target remote bị từ chối Một gdb khác đang giữ kết nối Thoát gdb cũ; QEMU chỉ nhận một kết nối tại một thời điểm
QEMU không boot, đứng yên khi khởi động Thiếu wait=off, QEMU chờ client kết nối vào cổng serial Thêm wait=off, hoặc kết nối gdb trước
kgdbwait không dừng kgdboc build thành module, hoặc kgdbwait đứng trước kgdboc= Build kgdboc sẵn trong kernel và đặt kgdbwait sau kgdboc=
Nhấn Ctrl-a b rồi g không vào debugger kgdboc chưa được cấu hình Kiểm tra cat /sys/module/kgdboc/parameters/kgdboc
gdb không có lệnh lx-* Chưa chạy make scripts_gdb hoặc gdb từ chối nạp script Chạy make scripts_gdb, thêm add-auto-load-safe-path

Liên quan

Tài liệu tham khảo