Linux 内核初始化的宏实现简析
时间:2010-02-23
来源:互联网
在分析Linux网络栈的时候,分析网络子系统的初始化是一件很重要的事情。有一些子系统并不能以模块的形式出现,因为它们是必须存在于内核当中,随内核启动而加载。不过,与普通应用程序初始化不同的是,它们的初始化工作,并没有使用显示的函数调用,而是透过一些巧秒的宏来实现。例如:
- /* Initialize the DEV module. */
- static int __init net_dev_init(void)
- {
- ……
- }
-
- subsys_initcall(net_dev_init);
另一方面,内核在实始化的时候,提供了一个内核命令行参数,允许用户向内核启动时传递参数,一些网络子系统的初始参数配置,也是通过这样一种方式实现初始化。例如:
- __setup("netdev", netdev_boot_setup);
本文主要是分析这些宏背后的运行机制。透过一个仿真的小程序,来揭秘其机制原理——程序中使用的宏和函数名称,尽量地跟内核代码完全一样。
首先对仿真程序逐行介绍,最后我会贴出程序来,如果感兴趣,可以先运行一下它,观察一下,再来看本贴。
启动的内核命令行参数,以及其对应的处理函数。使用一个名为obs_kernel_param的结构,把启动关键字同其处理函数封装:
- /* 关键与其处理函数的封装 */
- struct obs_kernel_param {
- const char *str; /* 关键字 */
- int (*setup_func)(char *); /* 处理函数 */
- int early; /* 本例中未使用 */
- };
- #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 }
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)
另一个主角是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);
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 = .;
- }
.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;
- }
- 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宏注册的关键字的函数,内核具体实现时,肯定有一个字符串解析和匹备的过程,这里也没有必要去分析字符串了。
- 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
先来看运行结果:
- [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]
- #include <stdio.h>
-
- #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;
- }
如果对gcc自定义段还不熟悉,有个官方文档说明:
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.
作者: 独孤九贱 发布时间: 2010-02-23

作者: jjj137 发布时间: 2010-02-23
作者: Godbach 发布时间: 2010-02-23
作者: Godbach 发布时间: 2010-02-23
Godbach 发表于 2010-02-23 17:43
就是把内核中这块的实现,单独拿了出来,这几个宏挺好玩的。不过自定义段和链接脚本也很有学习价值。于是就写了这个小程序来验证一下,呵呵
作者: 独孤九贱 发布时间: 2010-02-23
作者: accessory 发布时间: 2010-02-24
看书与实践相结合,这才是治学或研究的好方法
作者: Godbach 发布时间: 2010-02-24
作者: wmmy2008 发布时间: 2010-02-24
作者: hb12112 发布时间: 2010-02-24
作者: ownone 发布时间: 2010-03-04
热门阅读
-
office 2019专业增强版最新2021版激活秘钥/序列号/激活码推荐 附激活工具
阅读:74
-
如何安装mysql8.0
阅读:31
-
Word快速设置标题样式步骤详解
阅读:28
-
20+道必知必会的Vue面试题(附答案解析)
阅读:37
-
HTML如何制作表单
阅读:22
-
百词斩可以改天数吗?当然可以,4个步骤轻松修改天数!
阅读:31
-
ET文件格式和XLS格式文件之间如何转化?
阅读:24
-
react和vue的区别及优缺点是什么
阅读:121
-
支付宝人脸识别如何关闭?
阅读:21
-
腾讯微云怎么修改照片或视频备份路径?
阅读:28