linux内核源码中module_init宏定义的静态全局变量如何实现间接访问?

最近,我在看linux的内核源码,其中有好多驱动的源文件是作为模块写的,这就意味着该源文件生成的数据以及代码要么直接被编译链接进入内核,要么以xxx.ko的方式使用insmod应用软件加载该模块。无论是哪种,这些源文件的共同特点就是在代码最后使用了module_init()宏和module_exit()宏实现,分别对应模块加载(insmod)和模块删除(rmmod)。

cpp 复制代码
module_init(evdev_init);		// 将event device实现为一个模块
module_exit(evdev_exit);

分析:如果我们展开module_init()宏的话,发现是定义了一个使用static修饰的静态全局函数指针变量,并使用了gcc的属性指定将函数指针全局变量赐予了一个段属性,也就是指导链接器将具有相同属性的全局变量放在一起,并将evdev_init函数的地址写到该函数指针变量中,完成初始化。

cpp 复制代码
Type: initcall_t (aka int (*)(void))

#define module_init(x) __initcall(x);

// Expands to
static initcall_t __initcall_evdev_init6 __attribute__((__used__))
__attribute__((__section__(".initcall"
                           "6"
                           ".init"))) = evdev_init;

问题:该函数指针变量使用了static关键字修复,也就是为"内链接"属性,只能被源文件evdev.c中的函数进行读写访问,但是源文件evdev.c中没有函数去调用这个函数指针,那么该函数指针变量是如何被调用执行的呢?

分析:(世界上没有真正的决定,只有相对的决定)

(1)定义的是函数指针变量,指针变量是一种类型,也就是占用内存格子是固定的,而且被赋予了__section__段属性,也就是整个内核代码被编译链接后,具有相同属性的函数指针变量被整齐放在了一起,这自然而然形成了c语言中数组的概念。

(2)static修饰的全局变量还是全局变量,static表示内链接属性,而非外链接属性,也就是该全局变量可以被源文件内部的任意一个函数进行读写访问,该原文件外面的函数不能通过以变量名的方式进行直接访问。尤其是最后一句话,外部函数不能使用变量名的方式直接访问。

(3)既然第二条中提到static修饰的全局变量不能被外部函数直接变量名访问,那么外部函数是不是就没有办法访问静态全局变量了呢?答案是否定的,不能直接使用变量名的方式直接访问,可以通过地址的方式间接访问,这就体现了c语言的灵活性、或者高自由度、指针访问是灵魂。写到这里,我想到了以前看到的代码,使用const修饰的变量是一定不能被修改的吗?答案是否定的,const修饰的变量不能作为赋值运算符的左值被直接写入新数值,但是如果我们定义一个同类型的指针变量并指向该const修饰的变量的话,完全可以骗过编译器,使用指针变量解引用的方式实现const修饰变量的修改。这里也是同样的道理,骗过编译器,需要用到指针变量间接访问

因此,我们可以通过指针的方式骗过编译器去实现上述同段的函数指针数组访问。

cpp 复制代码
  __initcall_start = .; *(.initcallearly.init) __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) __initcall_end = .;

在vmlinux.lds链接脚本中定义了__early_initcall_end = .,以及__initcall_end = .这2个地址,也就是我们所说的函数指针数组的起始地址和结束地址。

在main.c中使用了extern关键字进行地址声明,并进行类型转换,因为我们后面要进行遍历操作。

cpp 复制代码
extern initcall_t __initcall_start[], __initcall_end[], __early_initcall_end[];		/**地址声明 */
cpp 复制代码
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();
}

在函数do_initcalls()中使用了for遍历操作,函数指针变量被写入不同地址值,然后实现了解引用,这样就对应函数指针变量被初始化写入的函数地址的访问。

当然了这些函数是同一种类型的函数,返回值,参数列表都是一样的。

整个过程,用到了一些内存访问、指针、指针常量也就是地址、指针解引用、内存变量抽象等,还是挺复杂的。但是最终结果是编译器被成功被骗过,实现了static修饰的全局变量访问,而且编译器未报错。

总结:static修饰的全局变量限制的是访问权限,只有源文件内部函数才能通过变量名直接访问,源文件外部函数不能通过变量名直接访问,但是由于c语言编译器的灵活性,可以使用指针访问的方式实现static修饰的全局变量间接访问。这体现了c语言指针访问的特点,有利有弊,利处就是能够实现很多表面看起来不可能的操作,弊端就是用c语言(编译器放过)写出的代码存在隐藏的漏洞导致c语言写出的代码没有绝对的安全。

本篇文章,继续论述了"指针是c语言灵魂"这就话(我现在说"c语言是内存访问的语言",同时也是指针的语言),给我们一些启迪:世界上没有太绝对的事情,往往存在一定法则,我们需要遵循这些法则的同时,也要能够理解内部的本质。

相关推荐
时光の尘1 个月前
嵌入式面试八股文(二十)·C语言关键字相关知识点速通(static、const、volatile、struct、enum、union)
c语言·const·static·union·volatile·struct·enum
BackCatK Chen3 个月前
第十三章 C 语言中的存储类别、链接与 内存管理
c语言·内存管理·static·extern·存储类别·malloc 动态内存
better_liang3 个月前
每日Java面试场景题知识点之-Java修饰符
java·访问控制·static·abstract·final·修饰符·企业级开发
一个平凡而乐于分享的小比特4 个月前
static 关键字详解
c语言·static
sulikey5 个月前
C++类和对象(下):初始化列表、static、友元、内部类等核心特性详解
c++·static·初始化列表·友元·匿名对象·内部类·编译器优化
BestOrNothing_20155 个月前
【C++基础】Day 4:关键字之 new、malloc、constexpr、const、extern及static
c++·八股文·static·extern·new与malloc·constexpr与const
自在极意功。6 个月前
Java static关键字深度解析
java·开发语言·面向对象·static
布兰妮甜6 个月前
彻底清理:Vue项目中移除static文件夹的完整指南
vue.js·前端框架·static·1024程序员节
茶午此人6 个月前
对static新的认识
static