• Linux内核—模块编译方法


    一、向内核添加新功能

    1.1 静态加载法:

    即新功能源码与内核其它代码一起编译进uImage文件内

    1. 新功能源码与Linux内核源码在同一目录结构下

      在linux-3.14/driver/char/目录下编写myhello.c,文件内容如下:

      #include 
      #include 
      
      int __init myhello_init(void)
      {
      	printk("#####################################################\n");
      	printk("#####################################################\n");
      	printk("#####################################################\n");
      	printk("#####################################################\n");
          printk("myhello is running\n");
      	printk("#####################################################\n");
      	printk("#####################################################\n");
      	printk("#####################################################\n");
      	printk("#####################################################\n");
      	return 0;
      }
      
      void __exit myhello_exit(void)
      {
      	printk("myhello will exit\n");
      }
      MODULE_LICENSE("GPL");
      module_init(myhello_init);
      module_exit(myhello_exit);
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
    2. 给新功能代码配置Kconfig

      #进入myhello.c的同级目录
      cd  ~/fs4412/linux-3.14/drivers/char
      
      vim Kconfig
      #39行处添加如下内容:
      config MY_HELLO
      	tristate "This is a hello test"
      	help
      		This is a test for kernel new function
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    3. 给新功能代码改写Makefile

      #进入myhello.c的同级目录
      cd  ~/fs4412/linux-3.14/drivers/char
      
      vim Makefile
      #拷贝18行,粘贴在下一行,修改成:
      obj-$(CONFIG_MY_HELLO)     += myhello.o
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    4. make menuconfig 界面里将新功能对应的那项选择成<*>

      cd  ~/fs4412/linux-3.14
      make menuconfig
      #make menuconfig如果出错,一般是两个原因:
      #1. libncurses5-dev没安装
      #2. 命令行界面太小(太矮或太窄或字体太大了)
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    1. make uImage

    2. cp arch/arm/boot/uImage /tftpboot

    3. 启动开发板观察串口终端中的打印信息

      在这里插入图片描述

    1.2 动态加载法:

    即新功能源码与内核其它源码不一起编译,而是独立编译成内核的插件(被称为内核模块)文件.ko

    a、新功能源码与Linux内核源码在同一目录结构下时

    1. 给新功能代码配置Kconfig

    2. 给新功能代码改写Makefile

    3. make menuconfig 界面里将新功能对应的那项选择成

    4. make uImage

    5. cp arch/arm/boot/uImage /tftpboot

    6. make modules

      make modules会在新功能源码的同级目录下生成相应的同名.ko文件(生成的ko文件只适用于开发板linux)

      注意此命令执行前,开发板的内核源码已被编译

    b、新功能源码与Linux内核源码不在同一目录结构下时

    1. cd ~/fs4412
    2. mkdir mydrivercode
    3. cd mydrivercode
    4. cp …/linux-3.14/drivers/char/myhello.c .
    5. vim Makefile
    6. make (生成的ko文件适用于主机ubuntu linux)
    7. make ARCH=arm (生成的ko文件适用于开发板linux,注意此命令执行前,开发板的内核源码已被编译)
    #file命令可以查看指定ko文件适用于哪种平台,用法:
    file  ko文件
    #结果带x86字样的适用于主机ubuntu linux,带arm字样的适用于开发板linux
    
    • 1
    • 2
    • 3

    c、主机ubuntu下使用ko文件

    sudo insmod ./???.ko  #此处为内核模块文件名,将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
    lsmod #查看已被插入的内核模块有哪些,显示的是插入内核后的模块名
    sudo rmmod ??? #,此处为插入内核后的模块名,此时将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
    
    sudo dmesg -C  #清除内核已打印的信息
    dmesg #查看内核的打印信息
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    d、开发板Linux下使用ko文件

    #先将生成的ko文件拷贝到/opt/4412/rootfs目录下:
    cp ????/???.ko  /opt/4412/rootfs
    
    #在串口终端界面开发板Linux命令行下执行
    insmod ./???.ko  #将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
    lsmod #查看已被插入的内核模块有哪些
    rmmod ??? #将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
    
    内核随时打印信息,我们可以在串口终端界面随时看到打印信息,不需要dmesg命令查看打印信息
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    二、内核模块基础代码解析

    Linux内核的插件机制——内核模块

    类似于浏览器、eclipse这些软件的插件开发,Linux提供了一种可以向正在运行的内核中插入新的代码段、在代码段不需要继续运行时也可以从内核中移除的机制,这个可以被插入、移除的代码段被称为内核模块。

    主要解决:

    1. 单内核扩展性差的缺点
    2. 减小内核镜像文件体积,一定程度上节省内存资源
    3. 提高开发效率
    4. 不能彻底解决稳定性低的缺点:内核模块代码出错可能会导致整个系统崩溃

    内核模块的本质:一段隶属于内核的“动态”代码,与其它内核代码是同一个运行实体,共用同一套运行资源,只是存在形式上是独立的。

    #include  //包含内核编程最常用的函数声明,如printk
    #include  //包含模块编程相关的宏定义,如:MODULE_LICENSE
    
    /*该函数在模块被插入进内核时调用,主要作用为新功能做好预备工作
      被称为模块的入口函数
      
      __init的作用 : 
    1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text")))   实际是gcc的一个特殊链接标记
    2. 指示链接器将该函数放置在 .init.text区段
    3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置
    */
    int __init myhello_init(void)
    {
        /*内核是裸机程序,不可以调用C库中printf函数来打印程序信息,
        Linux内核源码自身实现了一个用法与printf差不多的函数,命名为printk (k-kernel)
        printk不支持浮点数打印*/
    	printk("#####################################################\n");
    	printk("#####################################################\n");
    	printk("#####################################################\n");
    	printk("#####################################################\n");
    	printk("myhello is running\n");
    	printk("#####################################################\n");
    	printk("#####################################################\n");
    	printk("#####################################################\n");
    	printk("#####################################################\n");
    	return 0;
    }
    
    /*该函数在模块从内核中被移除时调用,主要作用做些init函数的反操作
      被称为模块的出口函数
      
      __exit的作用:
    1.一个宏,展开后为:__attribute__ ((__section__ (".exit.text")))   实际也是gcc的一个特殊链接标记
    2.指示链接器将该函数放置在 .exit.text区段
    3.在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
    */
    void __exit myhello_exit(void)
    {
    	printk("myhello will exit\n");
    }
    
    /*
    MODULE_LICENSE(字符串常量);
    字符串常量内容为源码的许可证协议 可以是"GPL" "GPL v2"  "GPL and additional rights"  "Dual BSD/GPL"  "Dual MIT/GPL" "Dual MPL/GPL"等, "GPL"最常用
    
    其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证
    在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印抱怨信息:
    	myhello:module license 'unspecified' taints kernel
    	Disabling lock debugging due to kernel taint
    也会导致新模块没法使用一些内核其它模块提供的高级功能
    */
    MODULE_LICENSE("GPL");
    
    /*
    module_init 宏
    1. 用法:module_init(模块入口函数名) 
    2. 动态加载模块,对应函数被调用
    3. 静态加载模块,内核启动过程中对应函数被调用
    4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。
    5. 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名
    */
    module_init(myhello_init);
    
    /*
    module_exit宏
    1.用法:module_exit(模块出口函数名)
    2.动态加载的模块在卸载时,对应函数被调用
    3.静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略
    4.对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
    5.对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
    */
    module_exit(myhello_exit);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    模块三要素:入口函数 出口函数 MODULE__LICENSE

    三、内核模块的多源文件编程

    ifeq ($(KERNELRELEASE),)
    
    ifeq ($(ARCH),arm)
    KERNELDIR ?= 目标板linux内核源码顶层目录的绝对路径
    ROOTFS ?= 目标板根文件系统顶层目录的绝对路径
    else
    KERNELDIR ?= /lib/modules/$(shell uname -r)/build
    endif
    PWD := $(shell pwd)
    
    modules:
    	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
    
    modules_install:
    	$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
    
    clean:
    	rm -rf  *.o  *.ko  .*.cmd  *.mod.*  modules.order  Module.symvers   .tmp_versions
    
    else
    obj-m += hello.o
    
    endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    Makefile中:

    obj-m用来指定模块名,注意模块名加.o而不是.ko

    可以用 模块名-objs 变量来指定编译到ko中的所有.o文件名(每个同名的.c文件对应的.o目标文件)

    一个目录下的Makefile可以编译多个模块:

    添加:obj-m += 下一个模块名.o

    四、 内核模块信息宏

    MODULE_AUTHOR(字符串常量); //字符串常量内容为模块作者说明
    
    MODULE_DESCRIPTION(字符串常量); //字符串常量内容为模块功能说明
    
    MODULE_ALIAS(字符串常量); //字符串常量内容为模块别名
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这些宏用来描述一些当前模块的信息,可选宏

    这些宏的本质是定义static字符数组用于存放指定字符串内容,这些字符串内容链接时存放在.modinfo字段,可以用modinfo命令来查看这些模块信息,用法:

    modinfo  模块文件名
    
    • 1

    += 下一个模块名.o

    五、模块传参

    module_param(name,type,perm);//将指定的全局变量设置成模块参数
    /*
    name:全局变量名
    type:
        使用符号      实际类型                传参方式
    	bool	     bool           insmod xxx.ko  变量名=0 或 1
    	invbool      bool           insmod xxx.ko  变量名=0 或 1
    	charp        char *         insmod xxx.ko  变量名="字符串内容"
    	short        short          insmod xxx.ko  变量名=数值
    	int          int            insmod xxx.ko  变量名=数值
    	long         long           insmod xxx.ko  变量名=数值
    	ushort       unsigned short insmod xxx.ko  变量名=数值
    	uint         unsigned int   insmod xxx.ko  变量名=数值
    	ulong        unsigned long  insmod xxx.ko  变量名=数值
    perm:给对应文件 /sys/module/name/parameters/变量名 指定操作权限
    	#define S_IRWXU 00700
    	#define S_IRUSR 00400
    	#define S_IWUSR 00200
    	#define S_IXUSR 00100
    	#define S_IRWXG 00070
    	#define S_IRGRP 00040
    	#define S_IWGRP 00020
    	#define S_IXGRP 00010
    	#define S_IRWXO 00007
    	#define S_IROTH 00004
    	#define S_IWOTH 00002  //不要用 编译出错
    	#define S_IXOTH 00001
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    module_param_array(name,type,&num,perm);
    /*
    name、type、perm同module_param,type指数组中元素的类型
    &num:存放数组大小变量的地址,可以填NULL(确保传参个数不越界)
        传参方式 insmod xxx.ko  数组名=元素值0,元素值1,...元素值num-1  
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可用MODULE_PARAM_DESC宏对每个参数进行作用描述,用法:

    MODULE_PARM_DESC(变量名,字符串常量);

    字符串常量的内容用来描述对应参数的作用

    modinfo可查看这些参数的描述信息

    六、模块依赖

    ​ 既然内核模块的代码与其它内核代码共用统一的运行环境,也就是说模块只是存在形式上独立,运行上其实和内核其它源码是一个整体,它们隶属于同一个程序,因此一个模块或内核其它部分源码应该可以使用另一个模块的一些全局特性。

    一个模块中这些可以被其它地方使用的名称被称为导出符号,所有导出符号被填在同一个表中这个表被称为符号表。

    最常用的可导出全局特性为全局变量和函数

    查看符号表的命令:nm
    nm查看elf格式的可执行文件或目标文件中包含的符号表,用法:

    nm 文件名 (可以通过man nm查看一些字母含义)

    两个用于导出模块中符号名称的宏:

    EXPORT_SYMBOL(函数名或全局变量名)
    EXPORT_SYMBOL_GPL(函数名或全局变量名) 需要GPL许可证协议验证

    使用导出符号的地方,需要对这些符号进行extern声明后才能使用这些符号

    B模块使用了A模块导出的符号,此时称B模块依赖于A模块,则:

    1. 编译次序:先编译模块A,再编译模块B,当两个模块源码在不同目录时,需要:i. 先编译导出符号的模块A ii. 拷贝A模块目录中的Module.symvers到B模块目录 iii. 编译使用符号的模块B。否则编译B模块时有符号未定义错误
    2. 加载次序:先插入A模块,再插入B模块,否则B模块插入失败
    3. 卸载次序:先卸载B模块,在卸载A模块,否则A模块卸载失败

    补充说明:
    内核符号表(直接当文本文件查看)
    /proc/kallsyms运行时 /boot/System.map编译后

    七、内核空间和用户空间

    为了彻底解决一个应用程序出错不影响系统和其它app的运行,操作系统给每个app一个独立的假想的地址空间,这个假想的地址空间被称为虚拟地址空间(也叫逻辑地址),操作系统也占用其中固定的一部分,32位Linux的虚拟地址空间大小为4G,并将其划分两部分:

    1. 0~3G 用户空间 :每个应用程序只能使用自己的这份虚拟地址空间

    2. 3G~4G 内核空间:内核使用的虚拟地址空间,应用程序不能直接使用这份地址空间,但可以通过一些系统调用函数与其中的某些空间进行数据通信

    实际内存操作时,需要将虚拟地址映射到实际内存的物理地址,然后才进行实际的内存读写

    八、执行流

    执行流:有开始有结束总体顺序执行的一段独立代码,又被称为代码上下文

    计算机系统中的执行流的分类:

    执行流:

    1. 任务流–任务上下文(都参与CPU时间片轮转,都有任务五状态:就绪态 运行态 睡眠态 僵死态 暂停态)
      1. 进程
      2. 线程
        1. 内核线程:内核创建的线程
        2. 应用线程:应用进程创建的线程
    2. 异常流–异常上下文
      1. 中断
      2. 其它异常

    应用编程可能涉及到的执行流:

    1. 进程
    2. 线程

    内核编程可能涉及到的执行流:

    1. 应用程序自身代码运行在用户空间,处于用户态 ----------------- 用户态app
    2. 应用程序正在调用系统调用函数,运行在内核空间,处于内核态,即代码是内核代码但处于应用执行流(即属于一个应用进程或应用线程) ---- 内核态app
    3. 一直运行于内核空间,处于内核态,属于内核内的任务上下文 --------- 内核线程
    4. 一直运行于内核空间,处于内核态,专门用来处理各种异常 --------- 异常上下文

    九、模块编程与应用编程的比较

    不同点内核模块应用程序
    API来源不能使用任何库函数各种库函数均可以使用
    运行空间内核空间用户空间
    运行权限特权模式运行非特权模式运行
    编译方式静态编译进内核镜像或编译特殊的ko文件elf格式的应用程序可执行文件
    运行方式模块中的函数在需要时被动调用从main开始顺序执行
    入口函数init_modulemain
    退出方式cleanup_modulemain函数返回或调用exit
    浮点支持一般不涉及浮点运算,因此printk不支持浮点数据支持浮点运算,printf可以打印浮点数据
    并发考虑需要考虑多种执行流并发的竞态情况只需考虑多任务并行的竞态
    程序出错可能会导致整个系统崩溃只会让自己崩溃

    十、内核接口头文件查询

    大部分API函数包含的头文件在include/linux目录下,因此:

    1. 首先在include/linux 查询指定函数:grep 名称 ./ -r -n
    2. 找不到则更大范围的include目录下查询,命令同上
  • 相关阅读:
    Keil MDK 安装
    Python进阶:猴子补丁
    Linux网络编程系列之服务器编程——信号驱动模型
    动态规划:03爬楼梯
    IntelliJ IDEA自定义菜单(Menus)、任务栏(toolbars)详细教程
    UDP和TCP两大协议的区别,让你快速高效掌握
    80211 TIM流量指示图 附C语言实现
    SpringBoot学习之SpringBoot3集成OpenApi(三十七)
    电机无位置控制方法研究
    浏览器开发者工具使用技巧总结
  • 原文地址:https://blog.csdn.net/hhltaishuai/article/details/132875791