写个Linux内核模块差点把系统搞崩了

📅 2026/6/26 13:24:17
写个Linux内核模块差点把系统搞崩了
前段时间做了个项目需要在嵌入式Linux上搞个自定义的字符设备驱动。以前写用户态程序写惯了心想不就是个内核模块吗翻翻LDDLinux Device Drivers第三版就能上手。结果——第一版insmod刚敲下去回车终端直接卡死按啥都没反应最后只能按住板子上的复位键硬重启。事情是这样开始的需求其实挺简单一块FPGA通过并行总线往ARM端发数据ARM端要把FPGA映射出来的寄存器空间暴露给用户态程序做read。硬件上FPGA挂在地址0x43C00000一共64KB空间。我琢磨着写个platform driver在probe里做ioremap实现file_operations里的read就行。代码其实不长刚开始就这么写的#include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/platform_device.h #include linux/io.h #include linux/uaccess.h #include linux/slab.h #define DEVICE_NAME fpga_data #define FPGA_PHYS_BASE 0x43C00000 #define FPGA_REG_SIZE 0x10000 static struct file_operations fpga_fops { .owner THIS_MODULE, .open fpga_open, .read fpga_read, }; static ssize_t fpga_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { u32 val; val ioread32(fpga_regs *offset); if (copy_to_user(buf, val, sizeof(val))) return -EFAULT; *offset sizeof(val); return sizeof(val); }看着是不是挺正常我当时也觉得没啥问题。然后写了probe编译出.ko文件scp到板子上insmod fpga.ko——结果终端直接冻住了。第一轮排查重启之后试了好几次每次insmod必死。我开始怀疑不是代码的问题——会不会是内核版本不对查了下uname -r输出5.4.47模块用modinfo看vermagic也是5.4.47没问题。那就只剩代码本身了。我把probe函数里的内容逐行注释掉先让insmod能成功再说。最后发现问题出在probe里直接调了device_create但class压根还没创建。// 这代码看着没毛病但顺序全反了 static int fpga_probe(struct platform_device *pdev) { // ... 分配内存、ioremap ... alloc_chrdev_region(fpga-dev_num, 0, 1, DEVICE_NAME); cdev_init(fpga-cdev, fpga_fops); cdev_add(fpga-cdev, fpga-dev_num, 1); // 崩就崩在这行 device_create(fpga_class, NULL, fpga-dev_num, NULL, DEVICE_NAME); return 0; }fpga_class是个全局指针我在module_init里只调了platform_driver_register压根没初始化它。所以在probe跑到device_create的时候传入的class指针是个野指针——内核直接oops然后panic。干嵌入式Linux调试不方便的地方就在这里。x86上崩了至少能看到ksymtab的符号还能sysrq抓堆栈。这破板子串口一刷屏就丢数据oops信息还没看清楚就滚过去了。最后我是用printk在关键位置打1、2、3这种标记看最后打出来的是几来判断在执行到哪之前崩的。printk默认日志级别是KERN_INFO有时候被别的进程的输出淹没了调低到KERN_EMERG用dmesg看才稳。low归low管用。正确的写法后来改成了这样class的创建放在module_init里而且在驱动注册之前完成。static struct class *fpga_class; static int __init fpga_init(void) { fpga_class class_create(THIS_MODULE, fpga); if (IS_ERR(fpga_class)) return PTR_ERR(fpga_class); return platform_driver_register(fpga_driver); } static void __exit fpga_exit(void) { platform_driver_unregister(fpga_driver); class_destroy(fpga_class); }然后probe里对device_create的返回值也加了检查。之前犯的另一个错误是没有做资源回滚——比如ioremap成功了但后面cdev_add失败了直接return错误码走人ioremap的那块物理地址映射就永远悬在那儿了。下次再probe同一个物理地址又ioremap一次虽然不会崩但内核里会有两块虚拟地址映射到同一块物理地址搞不好就踩到别的驱动。这个问题刚开始没暴露出来是后来多次rmmod/insmod之后发现可用内存越来越少才意识到的。vermagic的坑另外还遇到一个跟代码逻辑无关的问题。有次我在PC上用交叉编译链编了个模块拷到板子上insmod结果报Invalid module format查了半天才发现板子上跑的Linux内核是Buildroot用特定配置编出来的CONFIG_PREEMPT、CONFIG_SMP这些选项的值跟我交叉编译时用的内核头文件不一致。vermagic字符串里包含了这些配置的哈希值对不上就加载不了。解决办法有几种路子看情况选。板子上跑的是自己用Buildroot编的内核那就把内核源码树留着别删编模块时指定-C指向它。如果板子跑的是厂家提供的固件懒得重新编内核那就去厂家拿对应版本的kernel headers装上或者干脆在板子上装个交叉编译链本地编。obj-m fpga.o KDIR : /home/ubuntu/linux-5.4.47 all: $(MAKE) -C $(KDIR) M$(PWD) modules或者干脆把模块代码放到内核源码的drivers/目录里跟内核一起编。说实话搞内核驱动跟写用户态程序完全是两码事。用户态挂了顶多segment fault大不了gdb attach上去看core dump。内核里踩个指针直接死给你看连抢救的机会都没有。我现在每次insmod之前都盯着串口输出深吸一口气手指悬在复位键上——怕它又黑屏。