在分析Linux网络栈的时候,分析网络子系统的初始化是一件很重要的事情。有一些子系统并不能以模块的形式出现,因为它们是必须存在于内核当中,随内核启动而加载。不过,与普通应用程序初始化不同的是,它们的初始化工作,并没有使用显示的函数调用,而是透过一些巧秒的宏来实现。例如:网络设备子系统使用了宏subsys_initcall向内核注册它的初始化函数。当然内核中除了subsys_initcall,还有很多类似的xxx_initcall。
另一方面,内核在实始化的时候,提供了一个内核命令行参数,允许用户向内核启动时传递参数,一些网络子系统的初始参数配置,也是通过这样一种方式实现初始化。例如:之所以这样做,最重要的出发点在于内核优化:一些只用于初始化工作的模块,只应该被执行一次,结束后,它们占用的内存应该被释放掉——因为内核一旦启动,在关机之间,都是长驻内存的,不释放掉,就是占着茅坑不拉屎。
本文主要是分析这些宏背后的运行机制。透过一个仿真的小程序,来揭秘其机制原理——程序中使用的宏和函数名称,尽量地跟内核代码完全一样。
首先对仿真程序逐行介绍,最后我会贴出程序来,如果感兴趣,可以先运行一下它,观察一下,再来看本贴。
启动的内核命令行参数,以及其对应的处理函数。使用一个名为obs_kernel_param的结构,把启动关键字同其处理函数封装:再定义一个宏__setup_param,其作用是,允许用户定义一个类型为obs_kernel_param的结构变量,并初始化其成员:与普通的定义稍有不同,宏里面使用了gcc的扩展属性。
1、__attribute__((__section__("xxx")))
自定义“段(section)”,elf中,包括原来的a.out格式,在编译和连接阶段,都将二进制文件按一定格式都分为若干段,如数据段、代码段等等,在加载器加载运行程序的时候,它们又被相应地映射至内存(这一句话估计够一本书来描述了)。这里需要了解的是,gcc允许程序员自定义一个新的段。就像这里展示的一样,一个名为.init.setup的段被创建,所有obs_kernel_param变量都被放在了这里,而不是原来默认的其它地方。
2、另一个重要的扩展是对齐:
__attribute__((aligned((sizeof(long)))))
内存对齐问题,我想用不着过多地讨论了。
OK,主角登场,定义一个名为__setup的宏:这个包裹,主要是对于obs_kernel_param的early成员的操作,否则,它们就完全一样了,可惜,本文出发点不同,并未使用early。
另一个主角是subsys_initcall:__define_initcall(这个宏定义了一个函数指针(使用typedef定义了这个新的函数指针类型),其名称与宏的参数有关,同样地,它也使用了gcc扩展,创建一个名为.initcall" level ".init"的段,level是参数,也就是说,名称可以由调用者控制,函数指针指向它的另一个参数fn。
subsys_initcall宏是一个包裹定义,主要是指明了level,这里是4。
接下来,使用__setup宏注册了三个关键字:也定义了一个函数指针:对应的函数,具体实现为:当然,仿真嘛,毕竟不是真的。函数其实没有任何具体任务,就是打印一个记号,冒个泡。
现在深入到问题的核心了,如何使用这些自定义段,也就是说,得知道它们链接后重定位的具体地址,这是通过gcc提供的自定义链接控制脚本来实现的:脚本中,明确地指明了自定义段的开始地址:. = 0x08100000;一共有
.init.text          //自定义文本段
.init.data          //自定义数据段
.init.setup       //刚才用__setup定义的几个启动关键字的对应的变量就放在这里
.initcallX.init     //一共7个,不过我只用了第4个。
另外要注意的是,同时定义了四个变量,__setup_start = .;它指向.init.setup的开始位置,同样__setup_end = .;就是结束位置。
__initcall_start/end = .;同理。
这样,要程序中使用这四个变量,应该申明它们是外部变量:在我的主函数中,调用了两个函数,前者用来调用关键解析函数,后者用于调用xxx_initcalls:最后,调用free_initmem以释放掉这些不再会被使用的内存区域。它又是一个空函数,因为用户态程序跟内核的内存管理机制相差太大。暂时无法仿真了。但是这个不影响对整个框架的分析。
编译它:注意,链接的时候,使用了--with-lds,表示要使用自定义链接脚本,这个脚本,刚才已经展示过了。
先来看运行结果:看起来还像那么回事,每个冒的泡泡都出现了。
最使,使用readelf工具,来看看程序中的自定义段:附件1,完整程序清单附件2:
如果对gcc自定义段还不熟悉,有个官方文档说明: