Hướng dẫn lập trình mô-đun nhân Linux
(sysprog21.github.io)- Linux Kernel Module Programming Guide là hướng dẫn miễn phí để tạo các mô-đun nhân có thể nạp trên Linux v5.10 trở lên, bao quát theo một luồng từ môi trường phát triển đến build, nạp, gỡ lỗi và các giao diện nhân chính
- Các ví dụ ban đầu dùng
hello-*.cđể làm quen vớimodule_init(),module_exit(),kbuild,insmod,rmmod,dmesg, đồng thời dùng devtools dựa trên QEMU để giảm rủi ro làm hỏng hệ thống host - Vì mô-đun nhân chạy trong không gian địa chỉ của nhân, các lỗi như con trỏ sai, thứ tự unload, concurrency, lỗi sao chép bộ nhớ người dùng có thể dẫn đến hỏng bộ nhớ nhân hoặc hệ thống không ổn định
- Hướng dẫn mở rộng sang character device,
/proc,seq_file, threaded IRQ, input, PCI, USB, block, network, Device Model, Device Tree, static key, đồng thời lặp lại việc quản lý thứ tự đăng ký/hủy đăng ký và lifetime - API nội bộ của nhân thay đổi theo từng phiên bản, nên cần kiểm tra các điều kiện như
LINUX_VERSION_CODE,KERNEL_VERSION,CONFIG_MODVERSIONS, chữ ký SecureBoot, version magic; các ví dụ cũng bao gồm biên dịch có điều kiện
Cấu trúc hướng dẫn và luồng cơ bản
- Hướng dẫn này là tài liệu học mô-đun nhân có cung cấp kho GitHub và tài liệu PDF, có thể sao chép, chỉnh sửa và phân phối theo điều khoản Open Software License 3.0
- Hướng dẫn hiện lấy Linux v5.10 làm mức hỗ trợ tối thiểu, với mục tiêu duy trì tính tương thích của ví dụ và chỉ dẫn trên toàn bộ các nhân hỗ trợ dài hạn
- Người học cần có kinh nghiệm với ngôn ngữ C và viết chương trình cho tiến trình thông thường; mô-đun nhân được nạp/gỡ động để mở rộng chức năng nhân mà không cần khởi động lại
- Luồng phát triển cơ bản gồm cài đặt kernel header, build
.kobằngmake, kiểm tramodinfo, nạp bằnginsmod, xem log bằngdmesghoặcjournalctl -k, rồi gỡ bằngrmmod devtools/build mã nguồn nhân và hệ thống tệp gốc BusyBox, khởi động trong QEMU, chia sẻexamples/qua 9p virtfs, rồi cho phép thử nghiệm mô-đun trong guest- Khởi tạo và dọn dẹp mô-đun nên dùng
module_init()vàmodule_exit(); cách cũinit_module()vàcleanup_module()có thể gây lỗi build trong một số điều kiện từ kernel 6.15 trở đi khi x86 IBT được bật - Mô-đun nhân không dùng
printf()hay libc, chỉ có thể dùng các symbol do nhân export; output không đi ra terminal mà vào kernel log ring buffer - Việc di chuyển dữ liệu giữa không gian người dùng và không gian nhân cần các hàm chuyên dụng như
put_user,get_user,copy_to_user,copy_from_user - Ví dụ character device minh họa
register_chrdev,file_operations, dynamic major number, tạo node/dev, exclusive open, read dựa trênput_user, và xử lý write không được hỗ trợ - Ví dụ
/procđề cậpproc_create,proc_ops, callback read/write, APIseq_file, phản ánh thay đổi từ Linux v5.6 khiproc_opsđược đưa vào cho handler/procthay chofile_operations - Threaded IRQ dùng
request_threaded_irq()để tách top-half và bottom-half; top-half chỉ thực hiện công việc tối thiểu trong interrupt context và đánh thức bottom-half dựa trên thread bằngIRQ_WAKE_THREAD - Các chương sau mở rộng sang các lĩnh vực driver thực tế như input, PCI, USB, block, network, Device Model, Device Tree, tập trung vào cách đăng ký của từng subsystem và lựa chọn userspace ABI
- Phần tối ưu hóa và an toàn đề cập
likely·unlikely, static key, kernel stack nhỏ, cấm dùng FPU, rò rỉ padding chưa khởi tạo, và lưu ý khi dùng API nội bộ có double-underscore
Những ràng buộc thường gặp đầu tiên khi build và nạp
- Mô-đun được biên dịch cho một kernel có thể không nạp được trên kernel khác; nếu version magic và
CONFIG_MODVERSIONSkhông khớp, có thể xảy raInvalid module formathoặc không khớp phiên bản symbol - Hầu hết kernel của các bản phân phối Linux phổ biến có thể bật modversioning, nên nếu ví dụ không chạy ngay, cần cân nhắc kernel tắt modversioning hoặc môi trường QEMU
- Trên hệ thống bật SecureBoot, việc nạp mô-đun chưa ký có thể bị hạn chế; nếu thấy
Lockdown: insmod: unsigned module loading is restricted, cần tắt SecureBoot hoặc thực hiện quy trình ký mô-đun
Môi trường thực hành dựa trên QEMU
devtools/setup.shtải xuống và build kernel tarball cùng BusyBox, rồi đóng gói initramfsdevtools/build-modules.shbuild mô-đun cho kernel mục tiêu QEMU,devtools/boot.shcung cấp guest shell, còndevtools/test-modules.shtự động kiểm thửinsmod·rmmodtheo từng mô-đun- Gỡ lỗi bằng GDB được thực hiện bằng cách build
vmlinuxvớiLKMPG_NO_PREBUILT=1 devtools/setup.sh, sau đó dùngdevtools/boot.sh --gdbvà kết nối GDB từ xa
Quy tắc viết mã nhân
- Trong hàm init, việc đăng ký/cấp phát có thể thất bại, vì vậy các tài nguyên đã lấy phải được giải phóng theo thứ tự ngược lại trong đường xử lý lỗi dựa trên
goto - Khi đăng ký cấu trúc callback với kernel, không gian người dùng có thể gọi callback ngay cả trước khi init trả về, vì vậy quy tắc register last, unregister first rất quan trọng: hoàn tất khởi tạo nội bộ rồi mới đăng ký cuối cùng, và khi giải phóng thì hủy đăng ký trước
- Process context, softirq/tasklet context và hardirq context khác nhau về khả năng sleep, truy cập bộ nhớ người dùng, dùng
GFP_KERNEL, mutex; hiểu sai các khác biệt này sẽ dẫn đến những lỗi kernel thường gặp
Lưu ý theo device và subsystem
- Character device dùng major number để định danh driver và minor number để phân biệt nhiều thiết bị bên trong driver; trong cách hiện đại, giao diện
cdevđược khuyến nghị hơnregister_chrdev() - Driver PCI không giả định địa chỉ cố định mà ánh xạ BAR resource do PCI core liệt kê; trong mã từ Linux 5.10 trở đi,
pcim_enable_device()và API tài nguyên device-managed hữu ích để giảm lỗi teardown - Driver USB phải xem hotplug và disconnect là sự kiện bình thường, và cần thiết kế với giả định URB completion có thể cạnh tranh với disconnect, timeout, suspend và userspace shutdown
- Driver block hoạt động xoay quanh
blk-mq,request,gendisk, queue limit, ngữ nghĩa flush/FUA, và tham gia mô hình hoàn tất request bất đồng bộ thay vì callback read/write đơn giản - Driver mạng kết hợp với
struct net_device,net_device_ops,sk_buff, NAPI, offload feature flag, báo cáo link-state; khai báo offload sai có thể dẫn đến hỏng traffic
Ứng phó với thay đổi phiên bản kernel
- Các ví dụ xử lý bằng biên dịch có điều kiện đối với thay đổi chữ ký
class_create()trong Linux 6.4,proc_opstrong Linux v5.6, thay đổi kiểu trả về củaremove()trong Linux 6.11, và thay đổi helperblk-mqtrong giai đoạn Linux 5.15~6.9 - Giao diện nội bộ của kernel thay đổi thường xuyên hơn system call, nên các mô-đun hỗ trợ nhiều kernel khó tránh khỏi việc so sánh
LINUX_VERSION_CODEvàKERNEL_VERSION
Checkpoint an toàn
- Kernel stack nhỏ hơn nhiều so với stack không gian người dùng, và trên nhiều hệ thống có thể chỉ ở mức 8 KiB hoặc 16 KiB, nên các mảng lớn cần dùng
kmalloc()·kzalloc() - Khi gửi dữ liệu tới không gian người dùng bằng
copy_to_user(), mọi byte bao gồm cả padding đều phải được khởi tạo; nếu không có thể xảy ra rò rỉ thông tin bộ nhớ nhân - Các API bắt đầu bằng double underscore như
__kmalloc(),__list_add()có thể giả định các tiền điều kiện nội bộ, nên nếu tài liệu không yêu cầu, cần ưu tiên dùng public wrapper
Phạm vi bị lược bỏ
- Trong quá trình xử lý đầu vào có nêu rõ rằng một số chunk nguyên văn đã bị lược bỏ do giới hạn độ dài/chi phí, nên bản tóm tắt này không bao quát đầy đủ mọi chương, ví dụ và đường đi mã của toàn bộ hướng dẫn
1 bình luận
Ý kiến trên Hacker News
QEMU là một cách rất tốt để trải nghiệm việc hack kernel
Sẽ rất hay nếu có ai đó cập nhật LDD (Linux Device Drivers) và các sách về Linux kernel; những sách kỹ thuật kiểu này khó kiếm lợi nhuận, nên Linux Foundation tài trợ cũng hợp lý
Ngay tuần này tôi cũng đã tái hiện một vấn đề trên v6.8: nếu tham số dòng lệnh kernel của arm64 dài quá 146 ký tự thì kernel lập tức im lặng dừng hẳn, bằng QEMU + GDB, rồi dùng mô phỏng build kernel arm64 trên host Debian 12 Bookworm amd64 để lần từng dòng mã gây lỗi và tìm ra nguyên nhân
Quy trình là trong một môi trường đã chuẩn bị sẵn phụ thuộc build và công cụ cross-compile, build image kernel arm64 cùng script cho GDB, cài
gdb, nếu cần thìgdb-multiarch, vàqemu-system-armtrên host, sau đó khởi chạyqemu-system-aarch64ở trạng thái dừng với-S -gdb tcp::1234và từ terminal khác attach bằnggdb-multiarch ./vmlinuxSau đó trong GDB chạy
target remote :1234,break __parse_cmdline,continuethì có thể dùng các tính năng GDB thông thường như kiểm tra bộ nhớ, biến, stack và thực thi từng bướcTham khảo debug kernel bằng GDB và các script
lx-*tại https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-...Để GDB dùng được các script Python
lx-*, thường cũng cần cho phép đường dẫn kiểu nhưecho "add-auto-load-safe-path ${SRC_DIR}/scripts/gdb/vmlinux-gdb.py" > ~/.gdbinitChuỗi thảo luận HN liên quan: https://news.ycombinator.com/item?id=35782630, https://news.ycombinator.com/item?id=28283030
The Linux Memory Manager cũng đáng tham khảo: https://linuxmemory.org/chapters
Theo cập nhật cuối cùng mà tác giả gửi vào đầu tháng 7, bản thảo đầu tiên đã hoàn tất và hiện đã bước vào giai đoạn biên tập cùng nhà xuất bản
Một vài ví dụ có vẻ khó tự chạy thử trực tiếp
Ví dụ, “Detecting button presses” giả định có thể build module cho RPi, mà bản thân việc đó đã đòi hỏi những việc như cross-compiling nên có thể không hề đơn giản
Đây là một tutorial tuyệt vời vì rất chi tiết, thiên về thực hành, và có thể bắt tay vào build kernel module ngay
Tài liệu nên xem cùng: https://0xax.gitbooks.io/linux-insides/content/index.html
Tôi đang thắc mắc nên đọc ở đâu về lập trình Linux kernel nói chung như filesystem hay quản lý bộ nhớ
Ngày xưa có “Linux Kernel Development” của Robert Love, nhưng có vẻ giờ không còn được cập nhật nữa
Lần đầu tôi đọc cái này là khoảng 22 năm trước :)