在分析Linux
网络栈的时候,分析网络子
系统的初始化是一件很重要的事情。有一些子系统并不能以模块的形式出现,因为它们是必须存在于内核当中,随内核启动而加载。不过,与普通
应用程序初始化不同的是,它们的初始化工作,并没有使用显示的函数调用,而是透过一些巧秒的宏来实现。例如:
- /* Initialize the DEV module. */
- static int __init net_dev_init(void)
- {
- ……
- }
- subsys_initcall(net_dev_init);
复制代码 网络设备子系统使用了宏subsys_initcall向内核注册它的初始化函数。当然内核中除了subsys_initcall,还有很多类似的xxx_initcall。
另一方面,内核在实始化的时候,提供了一个内核命令行参数,允许
用户向内核启动时传递参数,一些网络子系统的初始参数配置,也是通过这样一种方式实现初始化。例如:
- __setup("netdev", netdev_boot_setup);
复制代码 之所以这样做,最重要的出发点在于内核优化:一些只用于初始化工作的模块,只应该被执行一次,结束后,它们占用的
内存应该被释放掉——因为内核一旦启动,在关机之间,都是长驻内存的,不释放掉,就是占着茅坑不拉屎。
本文主要是分析这些宏背后的运行机制。透过一个仿真的小程序,来揭秘其机制原理——程序中使用的宏和函数名称,尽量地跟内核代码完全一样。
首先对仿真程序逐行介绍,最后我会贴出程序来,如果感兴趣,可以先运行一下它,观察一下,再来看本贴。
启动的内核命令行参数,以及其对应的处理函数。使用一个名为obs_kernel_param的结构,把启动关键字同其处理函数封装:
- /* 关键与其处理函数的封装 */
- struct obs_kernel_param {
- const char *str; /* 关键字 */
- int (*setup_func)(char *); /* 处理函数 */
- int early; /* 本例中未使用 */
- };
复制代码 再定义一个宏__setup_param,其作用是,允许用户定义一个类型为obs_kernel_param的结构变量,并初始化其成员:
- #define __setup_param(str, unique_id, fn, early) \
- static char __setup_str_##unique_id[] __initdata = str; \
- static struct obs_kernel_param __setup_##unique_id \
- __attribute_used__ \
- __attribute__((__section__(".init.setup"))) \
- __attribute__((aligned((sizeof(long))))) \
- = { __setup_str_##unique_id, fn, early }
复制代码 与普通的定义稍有不同,宏里面使用了gcc的扩展属性。
1、__attribute__((__section__("xxx")))
自定义“段(section)”,elf中,包括原来的a.out格式,在编译和连接阶段,都将二进制
文件按一定格式都分为若干段,如数据段、代码段等等,在加载器加载运行程序的时候,它们又被相应地映射至内存(这一句话估计够一本书来描述了)。这里需要了解的是,gcc允许程序员自定义一个新的段。就像这里展示的一样,一个名为.init.setup的段被创建,所有obs_kernel_param变量都被放在了这里,而不是原来默认的其它地方。
2、另一个重要的扩展是对齐:
__attribute__((aligned((sizeof(long)))))
内存对齐问题,我想用不着过多地讨论了。
OK,主角登场,定义一个名为__setup的宏:
- #define __setup(str, fn) \
- __setup_param(str, fn, fn, 0)
复制代码 这个包裹,主要是对于obs_kernel_param的early成员的操作,否则,它们就完全一样了,可惜,本文出发点不同,并未使用early。
另一个主角是subsys_initcall:
- #define __define_initcall(level,fn) \
- static initcall_t __initcall_##fn __attribute_used__ \
- __attribute__((__section__(".initcall" level ".init"))) = fn
-
- #define subsys_initcall(fn) __define_initcall("4",fn)
- typedef int (*initcall_t)(void);
复制代码 __define_initcall(这个宏定义了一个函数指针(使用typedef定义了这个新的函数指针类型),其名称与宏的参数有关,同样地,它也使用了gcc扩展,创建一个名为.initcall" level ".init"的段,level是参数,也就是说,名称可以由调用者控制,函数指针指向它的另一个参数fn。
subsys_initcall宏是一个包裹定义,主要是指明了level,这里是4。
接下来,使用__setup宏注册了三个关键字:
- __setup("netdev=", netdev_boot_setup);
- __setup("ether=", ether_boot_setup);
- __setup("ip=", ip_auto_config_setup);
复制代码 也定义了一个函数指针:
- subsys_initcall(net_dev_init);
复制代码 对应的函数,具体实现为:
- static int __init ip_auto_config_setup(char *addrs)
- {
- printf("cmdline = %s\n", addrs);
-
- return 1;
- }
- static int __init ether_boot_setup(char *ether)
- {
- printf("ether = %s\n", ether);
-
- return 1;
- }
- static int __init netdev_boot_setup(char *netdev)
- {
- printf("netdev = %s\n", netdev);
-
- return 1;
- }
- static int __init net_dev_init(void)
- {
- /* Initialize the DEV module. */
- printf("call net_dev_init\n");
- return 0;
- }
复制代码 当然,仿真嘛,毕竟不是真的。函数其实没有任何具体任务,就是打印一个记号,冒个泡。
现在深入到问题的核心了,如何使用这些自定义段,也就是说,得知道它们链接后重定位的具体地址,这是通过gcc提供的自定义链接控制脚本来实现的:
- [root@Kendo develop]# cat my.lds
- SECTIONS
- {
- .text : {
- *(.text)
- }
- . = 0x08100000;
- .init.text : { *(.init.text) }
- .init.data : { *(.init.data) }
- . = ALIGN(16);
- __setup_start = .;
- .init.setup : {
- *(.init.setup)
- }
- __setup_end = .;
- __initcall_start = .;
- .initcall.init : {
- *(.initcall1.init)
- *(.initcall2.init)
- *(.initcall3.init)
- *(.initcall4.init)
- *(.initcall5.init)
- *(.initcall6.init)
- *(.initcall7.init)
- }
- __initcall_end = .;
- }
复制代码 脚本中,明确地指明了自定义段的开始地址
:. = 0x08100000;一共有
.init.text //自定义文本段
.init.data //自定义数据段
.init.setup //刚才用__setup定义的几个启动关键字的对应的变量就放在这里
.initcallX.init //一共7个,不过我只用了第4个。
另外要注意的是,同时定义了四个变量,__setup_start = .;它指向.init.setup的开始位置,同样__setup_end = .;就是结束位置。
__initcall_start/end = .;同理。
这样,要程序中使用这四个变量,应该申明它们是外部变量:
- extern struct obs_kernel_param __setup_start[], __setup_end[];
- extern initcall_t __initcall_start[], __initcall_end[];
复制代码
- int main(int argc, char **argv)
- {
- if(argc < 2) return -1;
-
- printf("%x, %x\n", __setup_start, __setup_end);
- start_kernel(argv[1]);
- do_initcalls();
-
- free_initmem();
- return 0;
- }
复制代码 在我的主函数中,调用了两个函数,前者用来调用关键解析函数,后者用于调用xxx_initcalls:
- static void __init do_initcalls(void)
- {
- initcall_t *call;
- for (call = __initcall_start; call < __initcall_end; call++) {
- (*call)();
- }
- }
- 因为__initcall_start指明了自定义段(程序运行后,加载进内存,就不存在段了,通过内存映射机制,对应了Linux内存区域VM)的开始地址,end是结束地址,循环遍历之,调用每个函数,这样,使用subsys_initcall注册的每个函数,都将被调用。
- subsys_initcall(net_dev_init);
- static int __init start_kernel(char *line)
- {
- struct obs_kernel_param *p;
-
- p = __setup_start;
- do {
- p->setup_func(line);
- p++;
- } while (p < __setup_end);
-
- return 0;
- }
- 同样的道理,遍历自定义段.ini.setup的每个obs_kernel_param结构,调用__setup宏注册的关键字的函数,内核具体实现时,肯定有一个字符串解析和匹备的过程,这里也没有必要去分析字符串了。
复制代码 最后,调用free_initmem以释放掉这些不再会被使用的内存区域。
- static void *free_initmem(void)
- {
- /* 释放掉不用的内存 */
-
- }
复制代码 它又是一个空函数,因为用户态程序跟内核的内存管理机制相差太大。暂时无法仿真了。但是这个不影响对整个框架的分析。
编译它:
- [root@Kendo develop]# gcc -o test.o -c test.c
- [root@Kendo develop]# gcc -o test test.o --with-lds my.lds
复制代码 注意,链接的时候,使用了--with-lds,表示要使用自定义链接脚本,这个脚本,刚才已经展示过了。
先来看运行结果:
- [root@Kendo develop]# ./test hello,world!
- 81000e0, 8100104
- netdev = hello,world!
- ether = hello,world!
- cmdline = hello,world!
- call net_dev_init
复制代码 看起来还像那么回事,每个冒的泡泡都出现了。
最使,使用readelf工具,来看看程序中的自定义段:
- [root@Kendo develop]# readelf -S test
- There are 32 section headers, starting at offset 0x1318:
- Section Headers:
- [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
- [color=Red] [25] .init.text PROGBITS 08100000 001000 0000cd 00 AX 0 0 1
- [26] .init.data PROGBITS 081000cd 0010cd 000013 00 WA 0 0 1
- [27] .init.setup PROGBITS 081000e0 0010e0 000024 00 WA 0 0 4
- [28] .initcall.init PROGBITS 08100104 001104 000004 00 WA 0 0 4[/color]
复制代码 附件1,完整程序清单
- #include
- #define __init __attribute__ ((__section__ (".init.text")))
- #define __initdata __attribute__ ((__section__ (".init.data")))
- #define __setup_param(str, unique_id, fn, early) \
- static char __setup_str_##unique_id[] __initdata = str; \
- static struct obs_kernel_param __setup_##unique_id \
- __attribute_used__ \
- __attribute__((__section__(".init.setup"))) \
- __attribute__((aligned((sizeof(long))))) \
- = { __setup_str_##unique_id, fn, early }
-
- #define __setup(str, fn) \
- __setup_param(str, fn, fn, 0)
- #define __define_initcall(level,fn) \
- static initcall_t __initcall_##fn __attribute_used__ \
- __attribute__((__section__(".initcall" level ".init"))) = fn
-
- #define subsys_initcall(fn) __define_initcall("4",fn)
- /* 关键与其处理函数的封装 */
- struct obs_kernel_param {
- const char *str; /* 关键字 */
- int (*setup_func)(char *); /* 处理函数 */
- int early; /* 本例中未使用 */
- };
- typedef int (*initcall_t)(void);
- extern struct obs_kernel_param __setup_start[], __setup_end[];
- extern initcall_t __initcall_start[], __initcall_end[];
- static int __init ip_auto_config_setup(char *addrs)
- {
- printf("cmdline = %s\n", addrs);
-
- return 1;
- }
- static int __init ether_boot_setup(char *ether)
- {
- printf("ether = %s\n", ether);
-
- return 1;
- }
- static int __init netdev_boot_setup(char *netdev)
- {
- printf("netdev = %s\n", netdev);
-
- return 1;
- }
- __setup("netdev=", netdev_boot_setup);
- __setup("ether=", ether_boot_setup);
- __setup("ip=", ip_auto_config_setup);
- static int __init net_dev_init(void)
- {
- /* Initialize the DEV module. */
- printf("call net_dev_init\n");
- return 0;
- }
- static void __init do_initcalls(void)
- {
- initcall_t *call;
- for (call = __initcall_start; call < __initcall_end; call++) {
- (*call)();
- }
- }
- subsys_initcall(net_dev_init);
- static int __init start_kernel(char *line)
- {
- struct obs_kernel_param *p;
-
- p = __setup_start;
- do {
- p->setup_func(line);
- p++;
- } while (p < __setup_end);
-
- return 0;
- }
- static void *free_initmem(void)
- {
- /* 释放掉不用的内存 */
-
- }
- int main(int argc, char **argv)
- {
- if(argc < 2) return -1;
-
- printf("%x, %x\n", __setup_start, __setup_end);
- start_kernel(argv[1]);
- do_initcalls();
-
- free_initmem();
- return 0;
- }
复制代码 附件2:
如果对gcc自定义段还不熟悉,有个官方文档说明:
Normally, the compiler places the objects it generates in sections like data and bss.
Sometimes, however, you need additional sections, or you need certain particular variables
to appear in special sections, for example to map to special hardware. The section attribute
specifies that a variable (or function) lives in a particular section. For example, this small
program uses several specific section names:
struct duart a __attribute__ ((section ("DUART_A"))) = { 0 };
struct duart b __attribute__ ((section ("DUART_B"))) = { 0 };
char stack[10000] __attribute__ ((section ("STACK"))) = { 0 };
int init_data __attribute__ ((section ("INITDATA")));
main()
{
/* Initialize stack pointer */
init_sp (stack + sizeof (stack));
/* Initialize initialized data */
memcpy (&init_data, &data, &edata - &data);
/* Turn on the serial ports */
init_duart (&a);
init_duart (&b);
}
Use the section attribute with global variables and not local variables, as shown in the example.
You may use the section attribute with initialized or uninitialized global variables but the linker
requires each object be defined once, with the exception that uninitialized variables tentatively go
in the common (or bss) section and can be multiply “defined”. Using the section attribute will change
what section the variable goes into and may cause the linker to issue an error if an uninitialized
variable has multiple definitions. You can force a variable to be initialized with the -fno-common
flag or the nocommon attribute.
Some file formats do not support arbitrary sections so the section attribute is not available on all
platforms. If you need to map the entire contents of a module to a particular section, consider using
the facilities of the linker instead.