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.
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ẵnCONFIG_DEBUG_KERNEL,CONFIG_MAGIC_SYSRQ,CONFIG_SERIAL_8250_CONSOLE,CONFIG_BLK_DEV_INITRDvàCONFIG_DEVTMPFS_MOUNT, nên không cần bật thêm.- Tắt
CONFIG_RANDOMIZE_BASEcó tác dụng giống tham số bootnokaslr. Có thể giữ option và thêmnokaslrvào command line. - Trên x86,
CONFIG_STRICT_KERNEL_RWXluô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()trongarch/x86/kernel/kgdb.cthử ghi trực tiếp, nếu không được thì dùngtext_poke_kgdb()để ghi lệnh breakpoint vào vùng code chỉ đọc. Vì vậy trên x86 không cầnrodata=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 trongvmlinux.
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ấngtrong vòng 5 giây (SYSRQ_TIMEOUTtronginclude/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:
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:
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:
Hoặc chuyển lúc chạy, không cần boot lại:
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:
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¶
- KGDB và kdb: nguyên lý, cấu hình và tham số của KGDB.
- Kernel Command Line: cú pháp các tham số truyền qua
-append. - Kernel Module: viết và build module.
- KVM & QEMU (Virtualization): kiến trúc QEMU và KVM, các chế độ ảo hóa và giả lập.
- Debug và phân tích (Programming): dùng gdb với chương trình user space.
Tài liệu tham khảo¶
- Kernel docs: Using kgdb, kdb and the kernel debugger internals
- Kernel docs: Debugging kernel and modules via gdb:
CONFIG_GDB_SCRIPTS,make scripts_gdb,lx-symbols, gdbstub của QEMU. - Mã nguồn
scripts/gdb/linux/symbols.py: cáchlx-symbolstìm và nạp symbol của module. - Mã nguồn
arch/x86/kernel/kgdb.c:kgdb_arch_set_breakpoint()vàtext_poke_kgdb(). - Mã nguồn
arch/Kconfig: điều kiện hiển thị củaSTRICT_KERNEL_RWX. - Mã nguồn
arch/x86/configs/x86_64_defconfigvàarch/x86/Kconfig.debug: các option mặc định, unwinder mặc định ORC trên x86_64. - Mã nguồn
include/linux/serial_core.h:SYSRQ_TIMEOUT. - QEMU docs: GDB usage (
docs/system/gdb.rst):-s,-S, cổng 1234, single step. - QEMU docs: Invocation, tùy chọn
-serial:tcp:[host]:port,server,wait,mon:, tối đa 4 cổng serial. Nội dung được đối chiếu với fileqemu-options.hxcủa tagv8.2.2. - QEMU docs: Keys in the character backend multiplexer: các phím
Ctrl-a.