Ở phần trước, chúng ta đã tìm hiểu về KGDB và kiến trúc của nó, Trong phần này, chúng ta sẽ cùng thực hiện demo 1 project đơn giản để hiểu rõ hơn về KGDB và ứng dụng của nó.
1 Sử dụng KGDB debug một kernel module bằng 2 máy ảo chạy trên phần mềm Vmware
1..1 Cấu hình 2 máy ảo trên Vmware
Bước 1. Thêm tùy chọn debug vào file .vmx của máy ảo ví dụ file tên target.vmx
1 2 |
debugStub.listen.guest64 = "TRUE" debugStub.listen.guest64.remote = "TRUE" |
Bước 2. Tạo kết nối serial giữa 2 máy thông qua pipe. Khi 2 máy ảo đang tăt, vào settings chọn add > serial. Sau đó cấu hình như sau.
![](https://vinalinux.com.vn/wp-content/uploads/2024/09/image-21.png)
Cấu hình serial máy target
![](https://vinalinux.com.vn/wp-content/uploads/2024/09/image-22.png)
Cấu hình serial port máy host
Bước 3. Để kiểm tra 2 máy kết nối với nhau thành công chưa:
- Trên máy target ở quyền root chạy cat /dev/ttyS0
- Trên máy host ở quyền root chạy echo “hello” > /dev/ttyS0
Khi này trên máy target sẽ hiển thị dòng chữ hello được gửi từ máy host
Bước 4. Cài linux kernel trên 2 máy. Tải phiên bản longterm mới nhất của kernel trên 2 máy host và máy target tại kernel.org
![](https://vinalinux.com.vn/wp-content/uploads/2024/09/image-23.png)
Các phiên bản Linux Kernel
Bước 5, Build kernel cho máy target
- Đầu tiên, tải những package cần thiết
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev fakeroot
sudo apt install dwarves - Tiếp đén giải nén file kernel.tar vừa tải
tar -xf linux-6.6.47.tar.gz - Sau khi giải nén xong, truy cập vào folder kernel
cd linux-6.6.47.tar.gz - Sao chép tệp cấu hình kernel .config vào boot
cp -v /boot/config-$(uname -r) .config - Sau đó, sửa file cấu hình bằng lệnh
make menuconfig
Hoặc
make localmodconfig
=> Localmodconfig là config tối ưu cho kernel được hoạt động trên một máy mà không cần di chuyển sang máy khác. Enter đến hết - Sau đó, truy cập vào file config bằng cách.
sudo vim config
Kiểm tra và đảm bảo các tính năng sau cấu hình đúng.
CONFIG_STRICT_KERNEL_RWX=n => Đảm bảo kernel cho phép KGDB truy cập, thao tác các chương trình trên kernel space
CONFIG_FRAME_POINTER=y => Cho phép sử dụng frame pointer để thực hiện các hàm call stack
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KGDB_KDB=y
CONFIG_KDB_KEYBOARD=y - Lưu file config vừa sửa sau đó, thêm chạy những scripts sau:
scripts/config –disable SYSTEM_TRUSTED_KEYS
scripts/config –disable SYSTEM_REVOCATION_KEYS
scripts/config –set-str CONFIG_SYSTEM_TRUSTED_KEYS “”
scripts/config –set-str CONFIG_SYSTEM_REVOCATION_KEYS “”
Các script trên giúp tạo keys trống để tránh trường hợp yêu cầu key dẫn đến build bị lỗi. - Sau đó, chạy fakeroot make -j8 với j8 tương ứng với 8 threads.
- Để kiểm tra make thành công không, chạy lệnh echo $?. Nếu kết quả là 0 thì đã thành công.
- Cuối cùng để install kernel module chạy
sudo make modules_install
sudo make install
sudo reboot - Khi máy đang khởi động lại, dí shift để chọn kernel linux-6.6.47
- Sửa grub để khi khởi động máy tự thiết lập sẵn tại /etc/default/grub
GRUB_DEFAULT=0
#GRUB_TIMEOUT_STYLE=hidden
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR=lsb_release -i -s 2> /dev/null || echo Debian
GRUB_CMDLINE_LINUX_DEFAULT=”splash kgdboc=ttyS0,115200 nokaslr kgdbwait” - Sau khi sửa xong, lưu lại và chạy sudo update-grub
- Reboot lại máy, chúng ta sẽ thấy giao diện của tính năng kgdbwait – chờ kết nối kgdb từ máy host
Bước 6. Cấu hình máy host.
- Tải phiên bản kernel giống với phiên bản kernel của máy target. Ở ví dụ này là linux-6.6.47
- Giải nén
- Sau khi giải nén truy cập vào folder linux-6.6.47
cd linux-6.6.47 - Copy file vmlinux trong thư mục linux-6.6.47 của máy target qua thư mục linux-6.6.47 của máy host
Truy cập máy target và vào foler linux-6.6.47, sau đó chạy dòng lệnh: scp vmlinux host@192.168.146.140:/home/host/kgdb/linux-6.6.47
1.2 Thực hiện debug 1 module đã có sẵn trong Linux kernel
- Khi máy target đang ở chế độ kgdbwait, từ máy host, di chuyển tới folder /linux-6.6.47 chạy dòng lệnh:
gdb vmlinux -tui
Trong đó, tùy chọn -tui (Text User Interface) cung cấp một giao diện người dùng dạng văn bản, giúp hiển thị mã nguồn, thanh trạng thái và các cửa sổ hiển thị khác ngay trong terminal - Sau đó, chạy target remote /dev/ttyS0 để remote đến máy target
- Tại đây, chúng ta đã có thể đặt breakpoint để kiểm tra kernel module.
Ví dụ, chúng ta đặt breakpoint ở hàm ksys_sync. Hàm này được gọi khi hệ thống có lệnh sync - Sau đó, chạy c để tiếp tục chương trình. Mở ternminal bên target, chạy lệnh sync, khi này máy target sẽ dừng lại tại breakpoint chúng ta đã đặt.
- Như chúng ta thấy, trên giao diện gdb cũng hiển thị
thông tin của hàm ksys_sync. Chúng ta có thể thực hiện các thao tác debug để
kiểm tra chương trình như next, bt, … - Muốn vào chế độ debug để đặt breakpoint, chúng ta chạy trên máy target: echo g > /proc/sysrq-trigger
2. Thực hiện debug một kernel module tự tạo (.ko)
Tạo file modex.c có hức năng chính là tạo một Linux kernel module đơn giản với một thiết bị khác (misc device) để thực hiện các thao tác đọc và ghi từ không gian người dùng (user space) đến kernel. Thiết bị này cho phép người dùng tương tác với nó thông qua các lệnh đọc (read) và ghi (write).File modex.c có nội dung như sau
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
#include <linux/module.h> #include <linux/kernel.h> #include <linux/miscdevice.h> #include <linux/fs.h> #include <linux/uaccess.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("Thong"); MODULE_DESCRIPTION("A simple Linux kernel module with misc device."); MODULE_VERSION("1.0"); // buffer #define BUF_SIZE 128 static char buffer[BUF_SIZE]; static int buffer_size = 0; // open device static int modex_open(struct inode *inode, struct file *file) { printk(KERN_INFO "modex: Device opened\n"); return 0; } // read data from device static ssize_t modex_read(struct file *file, char __user *user_buf, size_t count, loff_t *offset) { if (*offset >= buffer_size) return 0; if (count > buffer_size - *offset) count = buffer_size - *offset; if (copy_to_user(user_buf, buffer + *offset, count)) return -EFAULT; *offset += count; printk(KERN_INFO "modex: Read %zu bytes\n", count); return count; } // write data into device static ssize_t modex_write(struct file *file, const char __user *user_buf, size_t count, loff_t *offset) { if (count > BUF_SIZE - 1) count = BUF_SIZE - 1; if (copy_from_user(buffer, user_buf, count)) return -EFAULT; buffer[count] = '\0'; buffer_size = count; printk(KERN_INFO "modex: Wrote %zu bytes: %s\n", count, buffer); return count; } static const struct file_operations modex_fops = { .owner = THIS_MODULE, .open = modex_open, .read = modex_read, .write = modex_write, }; // define misc static struct miscdevice modex_device = { .minor = MISC_DYNAMIC_MINOR, .name = "modex", .fops = &modex_fops, }; // Intiniztion mo-đun static int __init modex_init(void) { int error; error = misc_register(&modex_device); if (error) { printk(KERN_ERR "modex: Failed to register device\n"); return error; } printk(KERN_INFO "modex: Module initialized and device created\n"); return 0; } // remove function mo-đun static void __exit modex_exit(void) { misc_deregister(&modex_device); printk(KERN_INFO "modex: Module exited and device removed\n"); } module_init(modex_init); module_exit(modex_exit); |
Tạo 1 file Makefile có nội dung như sau:
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 |
# Name of the module MODULE_NAME = modex # Kernel build directory (adjust according to your kernel version) KERNELDIR ?= /lib/modules/$(shell uname -r)/build # Module source files obj-m := $(MODULE_NAME).o # Default target: build the module all: make -C $(KERNELDIR) M=$(PWD) modules # Clean the build files clean: make -C $(KERNELDIR) M=$(PWD) clean # Load the module load: sudo insmod $(MODULE_NAME).ko # Unload the module unload: sudo rmmod $(MODULE_NAME) check_all: cat /sys/module/modex/sections/.bss cat /sys/module/modex/sections/.data cat /sys/module/modex/sections/.init.text cat /sys/module/modex/sections/.text cat /sys/module/modex/sections/.rodata.str1.1 cat /sys/module/modex/sections/.rodata.str1.8 cat /sys/module/modex/sections/.exit.text |
File app.c có chức năng nhận thông tin từ người dùng có nội dung như sau
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 40 41 42 43 44 45 46 47 |
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #define DEVICE_FILE "/dev/modex" int main(int argc, char *argv[]) { int fd; char buffer[128]; ssize_t bytes; if (argc != 2) { fprintf(stderr, "Usage: %s <value>\n", argv[0]); return 1; } fd = open(DEVICE_FILE, O_RDWR); if (fd < 0) { perror("open"); return 1; } // Ghi dữ liệu vào device if (write(fd, argv[1], strlen(argv[1])) < 0) { perror("write"); close(fd); return 1; } // Đọc dữ liệu từ device lseek(fd, 0, SEEK_SET); bytes = read(fd, buffer, sizeof(buffer) - 1); if (bytes < 0) { perror("read"); close(fd); return 1; } buffer[bytes] = '\0'; printf("Read from device: %s\n", buffer); close(fd); return 0; } |
Thực hiện cài đặt và load module .ko để demo debug bằng kgdb:
- Chạy make để build file modex.ko
- Sau đó make load và make check_all để insmod module và kiểm tra địa chỉ thành phần của module
Thông tin địa chỉ của các thành phần trong module modex - Copy file modex.ko và modex.c từ máy target sang máy host để load symbols cho gdb
scp modex.c modex.ko host@192.168.146.140:/home/host/kgdb - Sau khi có thông tin địa chỉ, chạy echo g > /proc/sysrq-trigger để vào chế độ debug
- Sau khi đã vào chế độ debug ở máy target, bên máy host thực hiện kết nối đến máy host
gdb -q vmlinux
target remote /dev/ttyS0 - Sau đó add symbols file là file modex.ko đã được copy qua và địa chỉ của từng thành phần trên máy target
Add-symbols-file bằng file modex.ko và địa chỉ từng thành phần - Sau khi thực hiên xong bước trên, chúng ta có thể đặt breakpoint vào các hàm ở trong module modex. Ví dụ b modex_read
- Chạy c để tiếp tục chương trình cho đến khi hàm modex_read được gọi. Trở lại máy target, biên dịch app.c: gcc -o app app.c. Sau đó, chạy ./app “Hello, Kernel”. Khi đó hàm modex_read sẽ được gọi, target sẽ dừng hoạt động vào chế độ debug.
Một vài lệnh debug
Mặc dù KGDB là một công cụ gỡ lỗi mạnh mẽ và dễ sử dụng, việc sử dụng liên tục có thể dẫn đến tình trạng hệ thống mục tiêu bị treo. Điều này xảy ra vì KGDB yêu cầu tạm dừng tất cả các hoạt động của kernel để thực hiện gỡ lỗi, gây ra sự đồng bộ hóa phức tạp và có thể ngăn kernel xử lý các ngắt hoặc yêu cầu quan trọng khác. Ngoài ra, việc đặt các điểm dừng trong mã kernel quan trọng hoặc phần cứng không được hỗ trợ đầy đủ có thể làm tăng nguy cơ treo hệ thống. Các vấn đề liên quan đến kết nối nối tiếp, lỗi trong mã kernel, và thời gian phản hồi cũng có thể góp phần làm cho hệ thống trở nên không ổn định khi sử dụng KGDB.
3. Tổng kết
GDB (GNU Debugger) nổi bật trong số các công cụ gỡ lỗi nhờ vào sự linh hoạt và tính năng phong phú của nó. Hỗ trợ nhiều ngôn ngữ lập trình như C, C++, Fortran và có khả năng hoạt động trên nhiều nền tảng từ máy tính cá nhân đến hệ thống nhúng và kernel hệ điều hành, GDB cung cấp một giao diện dòng lệnh mạnh mẽ cho phép người dùng tùy chỉnh và mở rộng các lệnh gỡ lỗi. Điều này làm cho GDB có thể gỡ lỗi cả mã nguồn cấp cao và mã máy cấp thấp, đáp ứng nhu cầu xử lý các vấn đề từ ứng dụng người dùng đến hệ thống phần cứng. Tích hợp tốt với các công cụ phát triển khác và là phần mềm mã nguồn mở miễn phí, GDB không chỉ hỗ trợ hiệu quả trong phát triển phần mềm mà còn được cộng đồng mã nguồn mở liên tục cập nhật và cải tiến, làm cho nó trở thành một công cụ gỡ lỗi ưu việt và phổ biến.