คู่มือการเขียนโปรแกรมโมดูลเคอร์เนลลินุกซ์
(sysprog21.github.io)- 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 และ APIseq_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 แล้วแพ็กเกจ initramfsdevtools/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 ความคิดเห็น
ความคิดเห็นบน Hacker News
QEMU เป็นวิธีที่ดีในการลองสัมผัสประสบการณ์การแฮ็กเคอร์เนล
คงจะดีถ้ามีใครสักคนอัปเดต LDD(Linux Device Drivers) และหนังสือเกี่ยวกับเคอร์เนล Linux และเพราะหนังสือเทคนิคแบบนี้ทำกำไรได้ยาก Linux Foundation น่าจะเป็นผู้สนับสนุนก็ดูเหมาะ
สัปดาห์นี้ก็เพิ่งใช้ 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เธรด 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 เป็นต้น
เป็นบทเรียนที่ยอดเยี่ยม เพราะละเอียด เน้นลงมือทำ และให้ลอง บิลด์โมดูลเคอร์เนล ได้ทันที
แหล่งข้อมูลที่ควรดูควบคู่กัน: https://0xax.gitbooks.io/linux-insides/content/index.html
อยากรู้ว่าถ้าจะดู การเขียนโปรแกรมเคอร์เนล Linux โดยรวม เช่น filesystem หรือ memory management ควรดูจากที่ไหน
เมื่อก่อนมี “Linux Kernel Development” ของ Robert Love แต่ตอนนี้ดูเหมือนจะไม่ได้อัปเดตแล้ว
อ่านสิ่งนี้ครั้งแรกเมื่อประมาณ 22 ปีก่อน :)