第一个linux驱动模块分析:

我们先看一下代码:

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

// 模块安装函数
static int __init chrdev_init(void)
{	
	printk(KERN_INFO "chrdev_init helloworld init\n");
	return 0;
}

// 模块卸载函数
static void __exit chrdev_exit(void)
{
	printk(KERN_INFO "chrdev_exit helloworld exit\n");
}


module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证
MODULE_AUTHOR("Mark");				// 描述模块的作者
MODULE_DESCRIPTION("test for driver");	// 描述模块的介绍信息
MODULE_ALIAS("alias test");			// 描述模块的别名信息

MODULE_xxx 这种宏作用是用来添加模块描述信息
我们可以使用modinfo查看模块相关的信息:例如我在ubuntu上使用:
modinfo modules_test.ko,可以看到:
filename: /mnt/hgfs/edit/2019-05-02/drivers/first_drivers/module_test.ko
alias: alias test
description: test for driver
author: Mark
license: GPL
srcversion: 79217AF7022E18502BC7A9F
depends:
vermagic: 3.13.0-164-generic SMP mod_unload modversions 686 retpoline


我们使用insmod命令来安装这个模块,rmmod来卸载模块,lsmod可以显示已经安装的所有模块,还有modprobe和depmod两个命令,depmod是用来生成模块的依赖关系的,modprode也是用来加载模块的,只是在加载的时候,会首先安装它依赖的模块,所以depmod和modprobe是一起用的。


module_init(chrdev_init);
module_exit(chrdev_exit);
这两个宏是用来申明我们的模块加载函数和模块的卸载函数的,也就是我们加载模块的时候(insmod或者modprobe)就会执行module_init指定的函数chrdev_init,卸载模块rmmod的时候就会执行module_exit指定的函数chrdev_exit

这两个宏定义在: include/linux/init.h中
#ifndef MODULE

#define module_init(x)	__initcall(x);
#define module_exit(x)	__exitcall(x);

#else
/* Each module must use one module_init(). */
#define module_init(initfn)
static inline initcall_t __inittest(void)
{ return initfn; }
int init_module(void) attribute((alias(#initfn)));

/* This is only required if you want to be unloadable. */
#define module_exit(exitfn)					\
	static inline exitcall_t __exittest(void)		\
	{ return exitfn; }					\
	void cleanup_module(void) __attribute__((alias(#exitfn)));

#endif
这里就有两种情况了,定义为模块还是定义为非模块,所谓模块就是指定为M,生成.ko文件。所谓非模块就是集成到内核里面去了,Y选项


先看定义为非模块:也就是集成在内核里面了
定义继续往下:
#define __initcall(fn) device_initcall(fn)
定义继续往下:
#define device_initcall(fn) __define_initcall(“6”,fn,6)
定义继续往下:
#define __define_initcall(level,fn,id)
static initcall_t _initcall##fn##id __used
attribute((section(".initcall" level “.init”))) = fn
所以就是这样的:
static initcall_t __initcall_chrdev_init6 __used
attribute((section(".initcall" 6".init"))) = chrdev_init
分析一下:

  1. initcall_t 是一个函数指针,typedef int (*initcall_t)(void);
  2. 函数指针变量的名字是:__initcall_chrdev_init6
  3. __used 也是一个宏,还可以继续追,这个是告诉编译器我们定义的__initcall_chrdev_init6变量有用,即使不使用也不要报警告
  4. attribute((section(".initcall" 6".init"))),attribute__用来指定变量或结构位域的特殊属性,其后的双括弧中的内容是属性说明,它的语法格式为:attribute ((attribute-list))。这里的attribute-list为__section(“.initcall6.init”),section属性就是用来指定将一个函数、变量存放在特定的段中。那么这里的意思就是:定义一个函数指针,它的值是chrdev_init,然后把这个指针变量放到.initcall6.init段中。
  5. 我们查看内核的链接脚本:vim arch/arm/kernel/vmlinux.lds.S,可以看到有一个INIT_CALLS段。
  6. 我们看INIT_CALLS的定义,在这个文件里面:vim include/asm-generic/vmlinux.lds.h ,
    #define INIT_CALLS
    VMLINUX_SYMBOL(__initcall_start) = .;
    INITCALLS
    VMLINUX_SYMBOL(__initcall_end) = .;
    7.再看INITCALLS
    #define INITCALLS
    *(.initcallearly.init)
    VMLINUX_SYMBOL(__early_initcall_end) = .;
    *(.initcall0.init)
    *(.initcall0s.init)
    *(.initcall1.init)
    *(.initcall1s.init)
    *(.initcall2.init)
    *(.initcall2s.init)
    *(.initcall3.init)
    *(.initcall3s.init)
    *(.initcall4.init)
    *(.initcall4s.init)
    *(.initcall5.init)
    *(.initcall5s.init)
    *(.initcallrootfs.init)
    *(.initcall6.init)
    *(.initcall6s.init)
    *(.initcall7.init)
    *(.initcall7s.init)
    8.所以:
    #define INIT_CALLS
    VMLINUX_SYMBOL(__initcall_start) = .;
    *(.initcallearly.init)
    VMLINUX_SYMBOL(__early_initcall_end) = .;
    *(.initcall0.init)
    *(.initcall0s.init)
    *(.initcall1.init)
    *(.initcall1s.init)
    *(.initcall2.init)
    *(.initcall2s.init)
    *(.initcall3.init)
    *(.initcall3s.init)
    *(.initcall4.init)
    *(.initcall4s.init)
    *(.initcall5.init)
    *(.initcall5s.init)
    *(.initcallrootfs.init)
    *(.initcall6.init)
    *(.initcall6s.init)
    *(.initcall7.init)
    *(.initcall7s.init)
    VMLINUX_SYMBOL(__initcall_end) = .;
    9.所以上面的这些段,在我们linux启动的时候,都会被加载。而且这些段在位置上也是有先后之分的。例如:.initcall0.init就会在.initcall6.init前面被加载。
    10…init 或者 .initcalls 段的特点就是,当内核启动完毕后,这个段中的内存会被释放掉。这一点从内核启动信息可以看到:Freeing unused kernel memory: 124K (80312000 - 80331000)

那么问题来了,上面的那个段是在哪里被加载的?
start_kernel
rest_init
kernel_init
do_basic_setup
do_initcalls
do_one_initcall

static void __init do_initcalls(void)
{
initcall_t *fn;

for (fn = __early_initcall_end; fn < __initcall_end; fn++)
	do_one_initcall(*fn);

/* Make sure there is no pending stuff from the initcall sequence */
flush_scheduled_work();

}
kernel_init是一个内核线程,它工作在内核层,可是最后会调用一个应用层的程序,变成应用层,它是所有进程的父进程和祖宗进程。
看到这里基本上就明白了。它会使用函数指针的形式,从init段的开始,一个一个函数去调用,直到段尾。
__early_initcall_end和__initcall_end是在include/asm-generic/vmlinux.lds.h定义的,具体可以去看。


那我们再看一下,如果定义为模块。
原型:
#define module_init(initfn)
static inline initcall_t __inittest(void)
{ return initfn; }
int init_module(void) attribute((alias(#initfn)));
1.__inittest 仅仅是为了检测定义的函数是否符合 initcall_t 类型,如果不是 __inittest 类型在编译时将会报错
2.所以可以看成:
static inline initcall_t
int init_module(void) attribute((alias(#initfn)));
alias 属性是 gcc 的特有属性,将定义 init_module 为函数 initfn 的别名。所以 module_init(chrdev_init) 的作用就是定义一个变量名 init_module,其地址和 chrdev_init是一样的
3.我们最前面编译的代码会生成一个module_test.mod.c的文件:

#include <linux/module.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>

MODULE_INFO(vermagic, VERMAGIC_STRING);

__visible struct module __this_module
attribute((section(".gnu.linkonce.this_module"))) = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};

static const struct modversion_info ____versions[]
__used
attribute((section("__versions"))) = {
{ 0xc7b3657, __VMLINUX_SYMBOL_STR(module_layout) },
{ 0x50eedeb8, __VMLINUX_SYMBOL_STR(printk) },
};

static const char __module_depends[]
__used
attribute((section(".modinfo"))) =
“depends=”;

MODULE_INFO(srcversion, “79217AF7022E18502BC7A9F”);
从上面的程序看:struct module __this_module,定义了一个__this_module的全局变量,放在".gnu.linkonce.this_module"这个段上。有个 .init = init_module,,这个init_module就是代表的chrdev_init


编译生成的module_test.ko文件需要通过insmod来加载,由于 insmod 是 busybox 提供的用户层命令,所以我们需要阅读 busybox 源码。调用如下:(文件 busybox/modutils/ insmod.c)

insmod_main
|
–> bb_init_module
|
–> init_module

而 init_module 定义如下:(文件 busybox/modutils/modutils.c)

#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)

因此,该系统调用对应内核层的 sys_init_module 函数。
回到Linux内核源代码(kernel/module.c),代码梳理:
SYSCALL_DEFINE3(init_module, …)
|
–>load_module
|
–> do_init_module(mod)
|
–> do_one_initcall(mod->init);

文件(include/linux/syscalls.h)中,有:

#define SYSCALL_DEFINE3(name, …) SYSCALL_DEFINEx(3, _##name, VA_ARGS)

从而形成 sys_init_module 函数。
最后的一部分还是没有分析清楚,先不分析了。

参考:https://blog.csdn.net/lu_embedded/article/details/51432616


部分素材来源于网络,如有侵权,请联系作者删除,谢谢!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值