最近,我在看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语言是内存访问的语言",同时也是指针的语言),给我们一些启迪:世界上没有太绝对的事情,往往存在一定法则,我们需要遵循这些法则的同时,也要能够理解内部的本质。