Qemu虚拟arm开发板驱动开发详解(一)——驱动基本架构

        此前在《WSL2下Ubuntu22.04使用Qemu搭建虚拟Vexpress-A9开发板》系列文章中,我们已建立好Linux最小系统的运行环境,并将其成功移植到了由Qemu模拟的arm32开发板上。接下来将介绍如何基于上述环境进行驱动开发。

        本节主要带各位读者了解Linux内核驱动的基本架构,并在WSL的Ubuntu22.04子系统下实现基于x86操作系统的简易Linux驱动“HelloWorld”。

Linux驱动框架

#include <linux/module.h> //包含内核编程最常用的函数声明,如printk
#include <linux/kernel.h> //包含模块编程相关的宏定义,如:MODULE_LICENSE

/*init初始化函数在模块被插入进内核时调用,主要作用为驱动功能做好预备工作
  被称为模块的入口函数
  
  __init的作用 : 
1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text")))   实际是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .init.text区段
3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置
*/
int __init mydriver_init(void)
{
	……………………
	return 0;
}

/*exit退出函数在模块从内核中被移除时调用,主要作用做些init函数的反操作
  被称为模块的出口函数
  
  __exit的作用:
1.一个宏,展开后为:__attribute__ ((__section__ (".exit.text")))   实际也是gcc的一个特殊链接标记
2.指示链接器将该函数放置在 .exit.text区段
3.在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
*/
void __exit mydriver_exit(void)
{
	……………………
}

/*
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(mydriver_init);

/*
module_exit宏
1.用法:module_exit(模块出口函数名)
2.动态加载的模块在卸载时,对应函数被调用
3.静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略
4.对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
5.对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
*/
module_exit(mydriver_exit);

        以上就是Linux操作系统中驱动程序的基本框架,主要包涵三个部分,一个是模块入口部分,另一个是模块出口部分,最后是源码许可证协议声明。驱动程序中只要存在这三个部分,就能被内核所认可,编译通过,且可以加载运行,但仅有这几个模块是不够的,后续章节将详细讲解驱动代码的其他重要组成部分,在本小节先做一个HelloWorld代码并尝试让它跑起来。

Hello World

        进入WorkSpace,并创建一个新的文件夹Drivers,用于存放以后开发的所有驱动代码,同时所有代码共享同一个Makefile。

makedir -p /home/workspace/drivers
cd /home/workspace/drivers
vim myhello.c

在myhello.c驱动文件中填入以下内容:

/*
    /home/workspace/drivers/myhello.c
*/

#include <linux/module.h>
#include <linux/kernel.h>

int __init myhello_init(void)
{
	printk("##############################################\n");
	printk("##############################################\n");
	printk("##############################################\n");
	printk("################ Hello World! ################\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);

接下来新建一个Makefile:

vim Makefile

在Makefile文件中输入以下内容:

ifeq ($(KERNELRELEASE),)

ifeq ($(ARCH),arm)
KERNELDIR ?= /home/workspace/linux-5.10.186
OBJECTDIR ?= /home/workspace/objects/vexpress-v2p-ca9
ROOTFS ?= /sync/rootfs
CROSS_COMPILE ?= arm-linux-gnueabi-
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) CROSS_COMPILE=$(CROSS_COMPILE) O=$(OBJECTDIR) modules

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) CROSS_COMPILE=$(CROSS_COMPILE) O=$(OBJECTDIR) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install

clean:
	rm -rf  *.o  *.ko  .*.cmd  *.mod*  modules.order  Module.symvers   .tmp_versions

else
	obj-m += myhello.o

endif

宿主机运行环境搭建

        如果是完整的Ubuntu操作系统,则可以省掉这个步骤,直接对上述代码进行make操作,编译驱动代码。但如果Ubuntu操作系统不完整,或者是属于WSL这种微软特供版子系统,则需要进行其他操作,才能确保驱动编译过程中不会出现环境问题。

首先查看操作系统内核版本:

uname -r

如果是WSL子系统,则显示结果和上图差不多,如果不是微软特供版的内核,则可以输入以下命令查看内核编译环境是否完整:

扫描二维码关注公众号,回复: 17288476 查看本文章
ls /lib/modules/$(uname -r)

如果显示文件夹下存在build、kernel、source三个子文件夹,则说明内核编译环境基本完整,可以尝试对驱动代码进行编译,如果以上命令提示文件夹不存在之类的情况,则尝试按照下列步骤进行操作。

通用内核

        如果输入uname -r显示的不是微软特供版,而是类似于4.15.0-162-generic之类的格式,则说明操作系统采用的是Linux通用内核,输入以下指令可以直接安装编译环境。

sudo apt install linux-headers-$(uname -r)

WSL内核

        如果输入uname -r显示的是类似5.15.90.1-microsoft-standard-WSL2的格式,则说明操作系统采用的是微软特供版内核,需要下载源码单独进行编译安装。

        进入 Releases · microsoft/WSL2-Linux-Kernel · GitHub 网站上下载对应uname -r显示的内核源码,然后执行下述步骤。

安装依赖

sudo apt install libelf-dev build-essential pkg-config bison build-essential flex libssl-dev libelf-dev bc dwarves

将内核源码拷贝到workspace(其中,/.../为源码下载到本地的路径)

sudo cp /.../WSL2-Linux-Kernel-linux-msft-wsl-5.15.90.1.tar.gz /home/workspace

进入workspace并解压

cd /home/workspace
tar -xvf WSL2-Linux-Kernel-linux-msft-wsl-5.15.90.1.tar.gz

进入源码目录并编译安装

cd WSL2-Linux-Kernel-linux-msft-wsl-5.15.90.1
cp Microsoft/config-wsl .config

执行以下指令进行编译安装

sudo make scripts
sudo make modules -j$(nproc)
sudo make modules_install

安装成功后提示如下图所示

其中有个错误no binutils support,忽略即可,目前不需要直接编译x86架构的32位程序。

编译加载x86架构驱动

编译驱动

cd /home/workspace/drivers
sudo make

加载驱动

sudo insmod myhello.ko

任意终端输入dmesg查看内核打印信息: 

 移除驱动

sudo rmmod myhello

任意终端输入dmesg查看内核打印信息: 

        可以注意到,加载内核的过程中出现loading out-of-tree module taints kernel.提示,意思是加载的树外模块污染了内核。这是我们没有把此驱动模块加入到Kconfig树导致的,即make menuconfig的配置选项中没有此驱动,此时这个驱动模块仍能正常加载和使用。如果仅仅用于学习和验证驱动,则可以忽略此提示,但如果是要求正式写入操作系统的驱动,则需要在Kconfig树中加入驱动信息,这样操作可以直接在menuconfig中选择是否加载驱动,同时也不会出现loading out-of-tree module taints kernel.提示了。

        下一节将介绍如何在Linux内核源码内添加驱动,并在操作系统启动时静态加载驱动代码。

猜你喜欢

转载自blog.csdn.net/cotex_A9/article/details/132607183
今日推荐