2 điểm bởi GN⁺ 2024-07-28 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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ới module_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 GitHubtà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 .ko bằng make, kiểm tra modinfo, nạp bằng insmod, xem log bằng dmesg hoặc journalctl -k, rồi gỡ bằng rmmod
  • 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()module_exit(); cách cũ init_module()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ên put_user, và xử lý write không được hỗ trợ
  • Ví dụ /proc đề cập proc_create, proc_ops, callback read/write, API seq_file, phản ánh thay đổi từ Linux v5.6 khi proc_ops được đưa vào cho handler /proc thay cho file_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ằng IRQ_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_MODVERSIONS không khớp, có thể xảy ra Invalid module format hoặ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.sh tải xuống và build kernel tarball cùng BusyBox, rồi đóng gói initramfs
  • devtools/build-modules.sh build mô-đun cho kernel mục tiêu QEMU, devtools/boot.sh cung cấp guest shell, còn devtools/test-modules.sh tự động kiểm thử insmod·rmmod theo từng mô-đun
  • Gỡ lỗi bằng GDB được thực hiện bằng cách build vmlinux với LKMPG_NO_PREBUILT=1 devtools/setup.sh, sau đó dùng devtools/boot.sh --gdb và 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ơn register_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_ops trong Linux v5.6, thay đổi kiểu trả về của remove() trong Linux 6.11, và thay đổi helper blk-mq trong 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_CODEKERNEL_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

 
GN⁺ 2024-07-28
Ý 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ý

    • Có một bài viết ghi lại đôi chút về quá trình viết driver và gắn một thiết bị tùy chỉnh bằng QEMU: [0] https://blog.davidv.dev/posts/learning-pcie/, [1] https://blog.davidv.dev/posts/pcie-driver-dma/
    • Dùng virtme-ng https://github.com/arighi/virtme-ng thì việc chạy kernel đang phát triển trong QEMU trở nên cực kỳ dễ dàng
    • Debug kernel giai đoạn đầu khi thậm chí chưa có console cũng thường dùng QEMU
      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-arm trên host, sau đó khởi chạy qemu-system-aarch64 ở trạng thái dừng với -S -gdb tcp::1234 và từ terminal khác attach bằng gdb-multiarch ./vmlinux
      Sau đó trong GDB chạy target remote :1234, break __parse_cmdline, continue thì 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ước
      Tham 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" > ~/.gdbinit
    • Bộ test WireGuard hiện đã vào kernel là một ví dụ rất hay để phát triển kernel module bằng QEMU và cả tự động kiểm thử
    • Greg KH đã nói khá rõ rằng sẽ không có LDD bản 4
  • Chuỗ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ục lục trông khá ổn, nhưng tiếc là không có đặt mua trước để hỗ trợ quá trình sản xuất
  • 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

    • Hơi phiền một chút, nhưng có lẽ cứ chạy trình biên dịch trên Raspberry Pi là được?
  • Đâ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 :)