L14D6内核模块编译方法-CSDN博客

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

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

   一个内核模块代码错误仍然会导致的内核崩溃。

GPL协议开源规定使用内核一些函数需要

1、单内核的缺点

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

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

#include <linux/module.h> //包含内核编程最常用的函数声明如printk
#include <linux/kernel.h> //包含模块编程相关的宏定义如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则在插入模块时打印抱怨信息
	myhellomodule 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);

myhello.c:内核模块函数代码

二内核模块的三要素

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

二、内核模块多源文件

如果一个内核模块太大多个文件编译成一个.ko文件

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
hello-objs = f1.o f2.o ... ... ... ... ...

endif

Makefile中

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

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

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

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

 makefile:

三、内核模块信息宏

MODULE_AUTHOR(字符串常量); //字符串常量内容为模块作者说明

MODULE_DESCRIPTION(字符串常量); //字符串常量内容为模块功能说明

MODULE_ALIAS(字符串常量); //字符串常量内容为模块别名

四、模块传参

perm权限0664

6用户可读可写

6主用户可读可写

4其他用户可读

可执行一般不用

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
*/
module_param_array(name,type,&num,perm);
/*
name、type、perm同module_paramtype指数组中元素的类型
&num存放数组大小变量的地址可以填NULL确保传参个数不越界
    传参方式 insmod xxx.ko  数组名=元素值0,元素值1,...元素值num-1  
*/

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

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

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

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

五、模块依赖

一个模块用到另外一个模块的参数

B模块依赖于A模块B模块要extern变量A模块内也需要用宏声明可以被外部调用的全局变量。

编译顺序一定要先A后B插入顺序也要先A后B。卸载顺序先B后A。

 modulea.c:

moduleb:

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

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

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

查看符号表的命令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. 一直运行于内核空间处于内核态专门用来处理各种异常 --------- 异常上下文

八、模块编程和应用编程的比较

1、API内核不能使用任何库函数应用程序可以使用库函数。

2、并发考虑内核考虑多种执行流并发手段丰富应用层需要考虑多任务异常上下文。

3、程序出错

九、内核接口头文件查询 

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

  1. 首先在include/linux 查询指定函数grep 名称 ./ -r -n
  2. 找不到则更大范围的include目录下查询命令同上

定位函数/结构体对应的头文件

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6