2 คะแนน โดย GN⁺ 2024-07-28 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Linux Kernel Module Programming Guide เป็นคู่มือฟรีสำหรับสร้าง loadable kernel module บน Linux v5.10 ขึ้นไป โดยครอบคลุมตั้งแต่สภาพแวดล้อมพัฒนาไปจนถึงการ build, load, debug และอินเทอร์เฟซเคอร์เนลหลักในลำดับการเรียนรู้เดียวกัน
  • ตัวอย่างช่วงต้นใช้ hello-*.c เพื่อทำความเข้าใจ module_init(), module_exit(), kbuild, insmod, rmmod, dmesg และลดความเสี่ยงต่อความเสียหายของระบบโฮสต์ด้วย devtools ที่อิง QEMU
  • โมดูลเคอร์เนลทำงานใน kernel address space ดังนั้น pointer ที่ผิดพลาด ลำดับการ unload ปัญหา concurrency และข้อผิดพลาดในการคัดลอกหน่วยความจำผู้ใช้ อาจนำไปสู่ ความเสียหายของหน่วยความจำเคอร์เนล หรือความไม่เสถียรของระบบ
  • เนื้อหาขยายต่อไปยัง character device, /proc, seq_file, threaded IRQ, input, PCI, USB, block, network, Device Model, Device Tree และ static key พร้อมกล่าวถึง ลำดับการ register/ยกเลิก register และการจัดการ lifetime ซ้ำๆ
  • เนื่องจาก API ภายในเคอร์เนลเปลี่ยนไปตามเวอร์ชัน จึงต้องตรวจสอบเงื่อนไขอย่าง LINUX_VERSION_CODE, KERNEL_VERSION, CONFIG_MODVERSIONS, ลายเซ็น SecureBoot และ version magic โดยตัวอย่างก็มีการใช้ conditional compilation รวมอยู่ด้วย

โครงสร้างคู่มือและลำดับการทำงานพื้นฐาน

  • คู่มือนี้เป็นสื่อการเรียนรู้เกี่ยวกับ kernel module ที่มีทั้ง GitHub repository และ เอกสาร PDF และสามารถคัดลอก แก้ไข และแจกจ่ายได้ภายใต้เงื่อนไขของ Open Software License 3.0
  • ปัจจุบันคู่มือกำหนด Linux v5.10 เป็นเกณฑ์ขั้นต่ำที่รองรับ และมีเป้าหมายรักษาความเข้ากันได้ของตัวอย่างและคำแนะนำกับเคอร์เนลสาย long-term support โดยรวม
  • ผู้เรียนควรมีพื้นฐานภาษา C และประสบการณ์เขียนโปรแกรมสำหรับ process ทั่วไป โดย kernel module สามารถ load และ unload แบบไดนามิกเพื่อขยายความสามารถของเคอร์เนลได้โดยไม่ต้องรีบูต
  • ขั้นตอนพัฒนาพื้นฐานคือ ติดตั้ง kernel headers, build .ko ด้วย make, ตรวจสอบด้วย modinfo, load ด้วย insmod, ดู log ด้วย dmesg หรือ journalctl -k, แล้ว unload ด้วย rmmod
  • devtools/ จะ build kernel source และ BusyBox root filesystem จากนั้นบูตบน QEMU, แชร์ examples/ ด้วย 9p virtfs แล้วให้ทดสอบโมดูลภายใน guest
  • แนะนำให้ใช้ module_init() และ module_exit() สำหรับการเริ่มต้นและการ cleanup โมดูล โดยวิธีเดิมอย่าง init_module() และ cleanup_module() อาจทำให้ build ล้มเหลวในบางเงื่อนไขตั้งแต่เคอร์เนล 6.15 เป็นต้นไปเมื่อเปิดใช้ x86 IBT
  • โมดูลเคอร์เนลไม่ใช้ printf() หรือ libc และใช้ได้เฉพาะ symbol ที่เคอร์เนล export ไว้เท่านั้น โดย output จะไม่ไปที่เทอร์มินัลแต่ไปยัง kernel log ring buffer
  • การย้ายข้อมูลระหว่าง userspace กับ kernel space ต้องใช้ฟังก์ชันเฉพาะอย่าง put_user, get_user, copy_to_user, copy_from_user
  • ตัวอย่าง character device แสดงการใช้ register_chrdev, file_operations, dynamic major number, การสร้าง node ใน /dev, exclusive open, การอ่านด้วย put_user และการจัดการ write ที่ไม่รองรับ
  • ตัวอย่าง /proc ครอบคลุม proc_create, proc_ops, callback สำหรับ read/write และ API seq_file โดยสะท้อนการเปลี่ยนแปลงหลัง Linux v5.6 ที่มีการใช้ proc_ops แทน file_operations สำหรับ handler ของ /proc
  • threaded IRQ แบ่ง top-half และ bottom-half ด้วย request_threaded_irq() โดย top-half ทำงานขั้นต่ำใน interrupt context แล้วปลุก bottom-half แบบ thread ด้วย IRQ_WAKE_THREAD
  • หลังจากนั้นบทต่างๆ จะขยายไปยังงาน driver จริง เช่น input, PCI, USB, block, network, Device Model และ Device Tree โดยเน้นวิธี register ของแต่ละ subsystem และการเลือก userspace ABI
  • ส่วนของ optimization และความปลอดภัยกล่าวถึง likely, unlikely, static key, kernel stack ขนาดเล็ก, การห้ามใช้ FPU, การรั่วไหลของ padding ที่ยังไม่ถูก initialize และข้อควรระวังในการใช้ API ภายในที่ขึ้นต้นด้วย double underscore

ข้อจำกัดที่มักเจอตั้งแต่ขั้น build และ load

  • โมดูลที่คอมไพล์สำหรับเคอร์เนลหนึ่งอาจไม่สามารถ load บนอีกเคอร์เนลได้ และหาก version magic หรือ CONFIG_MODVERSIONS ไม่ตรงกัน จะเกิด Invalid module format หรือ symbol version mismatch
  • เคอร์เนลของดิสทริบิวชัน Linux ทั่วไปจำนวนมากอาจเปิดใช้ modversioning ไว้ ดังนั้นถ้าตัวอย่างไม่ทำงานทันที อาจต้องพิจารณาใช้ เคอร์เนลที่ปิด modversioning หรือสภาพแวดล้อม QEMU
  • บนระบบที่เปิด SecureBoot การ load โมดูลที่ไม่ได้เซ็นอาจถูกจำกัด และหากเห็น Lockdown: insmod: unsigned module loading is restricted ก็จำเป็นต้องปิด SecureBoot หรือทำขั้นตอนเซ็นโมดูล

สภาพแวดล้อมฝึกปฏิบัติที่อิง QEMU

  • devtools/setup.sh จะดาวน์โหลดและ build kernel tarball กับ BusyBox แล้วแพ็กเกจ initramfs
  • devtools/build-modules.sh ใช้ build โมดูลสำหรับเคอร์เนลเป้าหมายของ QEMU, devtools/boot.sh ให้ guest shell และ devtools/test-modules.sh ใช้ทดสอบ insmod และ rmmod ของแต่ละโมดูลแบบอัตโนมัติ
  • การดีบักด้วย GDB ทำได้โดย build vmlinux ผ่าน LKMPG_NO_PREBUILT=1 devtools/setup.sh แล้วใช้ devtools/boot.sh --gdb พร้อมเชื่อมต่อ GDB ระยะไกล

กฎการเขียนโค้ดเคอร์เนล

  • ในฟังก์ชัน init การ register หรือ allocation อาจล้มเหลวได้ ดังนั้นทรัพยากรที่ได้มาแล้วควรถูก คืนกลับตามลำดับย้อนกลับ ผ่าน error path ที่ใช้ goto
  • เมื่อ register โครงสร้าง callback เข้ากับเคอร์เนล userspace อาจเรียก callback ได้แม้ก่อนที่ init จะ return ดังนั้นกฎ register last, unregister first จึงสำคัญมาก คือทำ initialization ภายในให้เสร็จก่อนแล้วค่อย register เป็นขั้นสุดท้าย และเวลาถอนออกให้ unregister ก่อน
  • process context, softirq/tasklet context และ hardirq context มีข้อแตกต่างกันในเรื่องการ sleep, การเข้าถึงหน่วยความจำผู้ใช้, การใช้ GFP_KERNEL และ mutex และหากเข้าใจความต่างนี้ผิดพลาดก็มักนำไปสู่บั๊กเคอร์เนลที่พบได้บ่อย

ข้อควรระวังตามชนิดอุปกรณ์และ subsystem

  • character device ใช้ major number เพื่อระบุไดรเวอร์ และใช้ minor number เพื่อแยกอุปกรณ์หลายตัวภายในไดรเวอร์เดียวกัน โดยในแนวทางสมัยใหม่แนะนำ อินเทอร์เฟซ cdev มากกว่า register_chrdev()
  • ไดรเวอร์ PCI ไม่ควรสมมติว่าที่อยู่คงที่ แต่ควร map BAR resource ที่ PCI core ทำการ enumerate ให้ และในโค้ดหลัง Linux 5.10 เป็นต้นไป pcim_enable_device() กับ device-managed resource API มีประโยชน์ในการลดบั๊กตอน teardown
  • ไดรเวอร์ USB ต้องมอง hotplug และ disconnect เป็นเหตุการณ์ปกติ และควรออกแบบโดยตั้งสมมติฐานว่า URB completion อาจแข่งกันกับ disconnect, timeout, suspend และการปิดจาก userspace
  • ไดรเวอร์ block ทำงานโดยอิง blk-mq, request, gendisk, queue limit และ flush/FUA semantics ไม่ใช่แค่ callback read/write แบบง่าย แต่ต้องเข้าร่วมในโมเดล asynchronous request completion
  • ไดรเวอร์เครือข่ายทำงานร่วมกับ struct net_device, net_device_ops, sk_buff, NAPI, offload feature flag และการรายงาน link-state โดยการประกาศ offload ที่ผิดพลาดอาจทำให้ทราฟฟิกเสียหายได้

การรับมือกับการเปลี่ยนแปลงของเวอร์ชันเคอร์เนล

  • ตัวอย่างรองรับการเปลี่ยนแปลงอย่างลายเซ็น class_create() ใน Linux 6.4, proc_ops ใน Linux v5.6, การเปลี่ยนชนิดค่าที่คืนจาก remove() ใน Linux 6.11 และการเปลี่ยน helper ของ blk-mq ระหว่าง Linux 5.15~6.9 ผ่าน conditional compilation
  • อินเทอร์เฟซภายในเคอร์เนลเปลี่ยนบ่อยกว่า system call ดังนั้นโมดูลที่รองรับหลายเคอร์เนลจึงหลีกเลี่ยงการเปรียบเทียบ LINUX_VERSION_CODE กับ KERNEL_VERSION ได้ยาก

จุดตรวจสอบด้านความปลอดภัย

  • kernel stack มีขนาดเล็กกว่า stack ใน userspace มาก และในหลายระบบอาจมีเพียง 8 KiB หรือ 16 KiB ดังนั้นอาร์เรย์ขนาดใหญ่ควรใช้ kmalloc() หรือ kzalloc()
  • เมื่อส่งข้อมูลไปยัง userspace ด้วย copy_to_user() ทุกไบต์รวมถึง padding ต้องถูก initialize แล้ว ไม่เช่นนั้นอาจเกิด การรั่วไหลของข้อมูลหน่วยความจำเคอร์เนล
  • API ที่ขึ้นต้นด้วย double underscore อย่าง __kmalloc(), __list_add() อาจตั้งสมมติฐานภายในบางอย่างไว้ ดังนั้นหากเอกสารไม่ได้กำหนดเป็นพิเศษ ควรเลือกใช้ public wrapper ก่อน

ขอบเขตที่ถูกละไว้

  • มีการระบุว่าบาง chunk ของต้นฉบับถูกละไว้ระหว่างการประมวลผลเนื่องจากข้อจำกัดด้านความยาวและค่าใช้จ่าย ดังนั้นสรุปนี้จึงไม่ได้ครอบคลุมทุกบท ทุกตัวอย่าง และทุกเส้นทางโค้ดของคู่มือทั้งหมดอย่างครบถ้วน

1 ความคิดเห็น

 
GN⁺ 2024-07-28
ความคิดเห็นบน Hacker News
  • QEMU เป็นวิธีที่ดีในการลองสัมผัสประสบการณ์การแฮ็กเคอร์เนล
    คงจะดีถ้ามีใครสักคนอัปเดต LDD(Linux Device Drivers) และหนังสือเกี่ยวกับเคอร์เนล Linux และเพราะหนังสือเทคนิคแบบนี้ทำกำไรได้ยาก Linux Foundation น่าจะเป็นผู้สนับสนุนก็ดูเหมาะ

    • มีบทความที่เขียนไว้เล็กน้อยเกี่ยวกับการเขียนไดรเวอร์และการสร้างอุปกรณ์แบบกำหนดเองใน QEMU แล้วนำมาต่อใช้งาน: [0] https://blog.davidv.dev/posts/learning-pcie/, [1] https://blog.davidv.dev/posts/pcie-driver-dma/
    • ถ้าใช้ virtme-ng https://github.com/arighi/virtme-ng การบูตเคอร์เนลที่กำลังพัฒนาอยู่ใน QEMU จะง่ายมาก
    • สำหรับ การดีบักเคอร์เนลระยะแรก ที่ยังไม่มีแม้แต่คอนโซล ก็มักใช้ QEMU กันมาก
      สัปดาห์นี้ก็เพิ่งใช้ QEMU + GDB ทำซ้ำปัญหาใน v6.8 ที่ถ้าพารามิเตอร์บรรทัดคำสั่งของเคอร์เนล arm64 ยาวเกิน 146 ตัวอักษร เคอร์เนลจะหยุดนิ่งทันทีแบบเงียบ ๆ แล้วจำลองการบิลด์เคอร์เนล arm64 บนโฮสต์ Debian 12 Bookworm amd64 เพื่อตามโค้ดที่เป็นปัญหาทีละบรรทัดจนเจอสาเหตุ
      ลำดับการทำงานคือ ในสภาพแวดล้อมที่เตรียม build dependencies และเครื่องมือ cross-compile ไว้แล้ว ให้บิลด์อิมเมจเคอร์เนล arm64 กับสคริปต์สำหรับ GDB จากนั้นติดตั้ง gdb, หากจำเป็นก็ gdb-multiarch, และ qemu-system-arm บนโฮสต์ แล้วรัน qemu-system-aarch64 โดยหยุดไว้ด้วย -S -gdb tcp::1234 จากนั้นเปิดเทอร์มินัลอีกอันแล้วต่อเข้าไปด้วย gdb-multiarch ./vmlinux
      หลังจากนั้นใน GDB ให้รัน target remote :1234, break __parse_cmdline, continue ก็จะใช้ฟังก์ชันทั่วไปของ GDB ได้ เช่น ตรวจสอบหน่วยความจำ ตัวแปร สแต็ก และรันทีละขั้น
      ดูข้อมูลเกี่ยวกับการดีบักเคอร์เนลด้วย GDB และสคริปต์ lx-* ได้ที่ https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-...
      เพื่อให้ GDB ใช้สคริปต์ Python lx-* ได้ โดยทั่วไปต้องอนุญาตพาธด้วย เช่น echo "add-auto-load-safe-path ${SRC_DIR}/scripts/gdb/vmlinux-gdb.py" > ~/.gdbinit
    • ชุดทดสอบ WireGuard ที่อยู่ในเคอร์เนลตอนนี้เป็นตัวอย่างที่ดีสำหรับการพัฒนาโมดูลเคอร์เนลด้วย QEMU ไปจนถึงการทำ automated test
    • Greg KH พูดค่อนข้างชัดเจนว่า LDD ฉบับพิมพ์ครั้งที่ 4 จะไม่ออกมา
  • เธรด HN ที่เกี่ยวข้อง: https://news.ycombinator.com/item?id=35782630, https://news.ycombinator.com/item?id=28283030

  • The Linux Memory Manager ก็น่าอ้างอิงเช่นกัน: https://linuxmemory.org/chapters
    ตามอัปเดตล่าสุดที่ผู้เขียนส่งเมื่อต้นเดือนกรกฎาคม ระบุว่าเขียนต้นฉบับเสร็จแล้ว และตอนนี้เข้าสู่ขั้นตอนแก้ไขร่วมกับสำนักพิมพ์แล้ว

    • สารบัญดูดี แต่น่าเสียดายที่ไม่มี พรีออร์เดอร์ เพื่อช่วยสนับสนุนการจัดทำ
  • ตัวอย่างบางส่วนดูเหมือนจะลองรันเองได้ยาก
    เช่น “Detecting button presses” สมมติว่าสามารถบิลด์โมดูลสำหรับ RPi ได้ แต่แค่นั้นเองก็อาจไม่ง่าย เพราะต้องทำงานอย่าง cross-compile เป็นต้น

    • ถึงจะยุ่งยากนิดหน่อย แต่ก็น่าจะรัน คอมไพเลอร์บน Raspberry Pi ไปเลยได้ไม่ใช่หรือ
  • เป็นบทเรียนที่ยอดเยี่ยม เพราะละเอียด เน้นลงมือทำ และให้ลอง บิลด์โมดูลเคอร์เนล ได้ทันที

  • แหล่งข้อมูลที่ควรดูควบคู่กัน: https://0xax.gitbooks.io/linux-insides/content/index.html

  • อยากรู้ว่าถ้าจะดู การเขียนโปรแกรมเคอร์เนล Linux โดยรวม เช่น filesystem หรือ memory management ควรดูจากที่ไหน
    เมื่อก่อนมี “Linux Kernel Development” ของ Robert Love แต่ตอนนี้ดูเหมือนจะไม่ได้อัปเดตแล้ว

  • อ่านสิ่งนี้ครั้งแรกเมื่อประมาณ 22 ปีก่อน :)