linux中的__setup详细解析和使用场景
版本linux3.18
static int __init myfunc(void)
{
……
}
__setup("myfuncstr",myfunc);
__init 宏 __section(.init.text)
__setup() 是一个宏定义,在include/linux/init.h这个文件中.
struct obs_kernel_param {
const char *str;
int (*setup_func)(char *);
int early;
};
extern struct kernel_param __setup_start, __setup_end;
#define __setup(str, fn) \
static char __setup_str_##fn[] __initdata = str; \
static struct kernel_param __setup_##fn __attribute__((unused)) __initsetup = { __setup_str_##fn, fn }
在这个情景中作了替换是这样的
static char __setup_str_myfunc[] = "myfunc";
static struct kernel_param __setup_myfunc = { __setup_str_myfunc, myfunc}
vmlinux.lds这个关于ld 链接器的脚本文件有这样的一段
.init.data : {
*(.init.data) .=ALIGN(8);
//输入段为.init.data 8字节对齐
*(.init.rodata);
//输入段为.init.rodata
__setup_start = .;
//.表示当前的offset,相当于该变量在vmlix镜像中的文件偏移
*(.setup.init)
__setup_end = .;
……
}
这里的意思就是__setup_start是一个节的开始,而__setup_end是一个节的结束,这个节的名称是.init.setup,
这个你可以用readelf -a这个来看一下你的vmlinux这个文件,
可以看到.init.setup就在.init.data的节中, 数据结构数组,一个就是str,一个就是setup_func,
与我前面的说法相一致,那具体是什么呢,就是一个在.init.data节中存储的
字符串-----__initdata是一个宏,就是(__attribute__ ((__section__ (".init.data")))), 所以你可以.init.data在vmlinux中的
在文件中的偏移量与加载的的虚拟地址偏移量相减就可以得到,
举个例子,所有的这些都是用readelf 与od 命令得到的
我现在用的内核版本,它的.init.setup的节在0x53d898 的文件偏移处.
addr offset size
[16] .init.text PROGBITS c05192e0 5212e0 01b7c0 00 AX 0 0 32
[20] .init.data PROGBITS c0535898 53d898 008698 00 WA 0 0 4
再查找myfunc和init.setup在vmlinux被映射为内存地址,
74733:c053d660 0 NOTYPE GLOBAL DEFAULT 20 __setup_start
74733:c053db40 0 NOTYPE GLOBAL DEFAULT 20 __setup_end
77840: c051ef0c 16 FUNC LOCAL DEFAULT 16 myfunc
15506:c053d7c8 12 OBJECT LOCAL DEFAULT 20 __setup_myfunc
15507:c053c0a6 16 OBJECT LOCAL DEFAULT 20 __setup_str_myfunc
0xc051ef0c :@0xc0514c78
//ARM UNWIND IDX 信息
这就可以知道myfunc所在的位置就是0xc051ef0c ,这就是它的虚拟映射地址
__setup_str_myfunc就是myfuncstr字符串的映射地址
ADDR{__setup_str_myfunc}(0xc053c0a6) - ADDR{.init.data}(0xc0535898) + FILEOFF{.init.data}(0x5212e0)=0x5440a6
16进制的方式打开vmlinux查看偏移量0x5440a6得到的字符串正是myfuncstr
同理计算__setup_myfunc对应的vmlinux文件偏移量是0x5457c8
vmlinux偏移量0x5457c8位置的值为A6 C0 53 C0 0C EF 51 C0
与ADDR{myfunc}0xc051ef0c对应和ADDR{__setup_str_myfunc}0xc53c0a6对应
//补充///
文章开始处调用__setup的使用场景
而 obsolete_checksetup 这个函数的工作就是遍历 .init.setup 段,
根据bootloader传入的命令行参数,搜索结构体 obs_kernel_param 中 str 的变量符合"root="、"init="、等字符串的,
调用对应的 setup_func 函数。至于如何解析命令行参数,是在 parse_args 中做的工作,大家可以自行分析。
static int __init obsolete_checksetup(char *line)
{
const struct obs_kernel_param *p;
int had_early_param = 0;
p = __setup_start;
do {
int n = strlen(p->str);
if (parameqn(line, p->str, n)) {
if (p->early) {
/* Already done in parse_early_param?
* (Needs exact match on param part).
* Keep iterating, as we can have early
* params and __setups of same names 8( */
if (line[n] == '\0' || line[n] == '=')
had_early_param = 1;
} else if (!p->setup_func) {
pr_warn("Parameter %s is obsolete, ignored\n",
p->str);
return 1;
} else if (p->setup_func(line + n))
return 1;
}
p++;
} while (p < __setup_end);
return had_early_param;
}
//补充///
参见include/linux/init.h和vmlinux.lds
1)
所有标识为__init的函数在链接的时候都放在.init.text这个区段内,
在这个区段中,函数的摆放顺序是和链接的顺序有关的,是不确定的。
2)
所有的__init函数在区段.initcall.init中还保存了一份函数指针,
在初始化时内核会通过这些函数指针调用这些__init函数指针,
并在整个初始化完成后,释放整个init区段(包括.init.text,.initcall.init等),
注意,这些函数在内核初始化过程中的调用顺序只和这里的函数指针的顺序有关,
和1)中所述的这些函数本身在.init.text区段中的顺序无关。
在2.4内核中,这些函数指针的顺序也是和链接的顺序有关的,是不确定的。
在2.6内核中,initcall.init区段又分成7个子区段,分别是
.initcall1.init
.initcall2.init
.initcall3.init
.initcall4.init
.initcall5.init
.initcall6.init
.initcall7.init
当需要把函数fn放到.initcall1.init区段时,只要声明
core_initcall(fn);
即可。
其他的各个区段的定义方法分别是:
core_initcall(fn) --->.initcall1.init
postcore_initcall(fn) --->.initcall2.init
arch_initcall(fn) --->.initcall3.init
subsys_initcall(fn) --->.initcall4.init
fs_initcall(fn) --->.initcall5.init
device_initcall(fn) --->.initcall6.init
late_initcall(fn) --->.initcall7.init
而与2.4兼容的initcall(fn)则等价于device_initcall(fn)。
各个子区段之间的顺序是确定的,即先调用.initcall1.init中的函数指针
再调用.initcall2.init中的函数指针,等等。
而在每个子区段中的函数指针的顺序是和链接顺序相关的,是不确定的。
在内核中,不同的init函数被放在不同的子区段中,因此也就决定了它们的调用顺序。
这样也就解决了一些init函数之间必须保证一定的调用顺序的问题。
3)
这些顺序和make dep没有关系。
(转自http://blog.csdn.net/jifengszf/archive/2009/04/22/4100193.aspx)
内核组件用__setup宏来注册关键字及相关联的处理函数,__setup宏在include/linux/init.h中定义,其原型如下:
__setup(string, function_handler)
其 中:string是关键字,function_handler是关联处理函数。__setup只是告诉内核在启动时输入串中含有string时,内核要去 执行function_handler。String必须以“=”符结束以使parse_args更方便解析。紧随“=”后的任何文本都会作为输入传给 function_handler。
下面的例子来自于net/core/dev.c,其中netdev_boot_setup作为处理程序被注册给“netdev=”关键字:
__setup("netdev=", netdev_boot_setup);
不 同的关键字可以注册相同的处理函数,例如在net/ethernet/eth.c中为“ether =”关键字注册了同样的处理函数 netdev_boot_setup。当代码作为模块被编译时,__setup宏被忽视,你可以在include/linux/init.h中看到 __setup宏是怎样变化的,不管后续包含它的文件是否是模块,include/linux/init.h都是独立的。
start_kernel两次调用parse_args解析启动配置字符串的原因是启动选项事实上分为两类,且每次调用值能够兼顾到其中一类:
缺省选项:
绝大多数选项归于此类,这些选项由__setup宏定义并在第二次调用parse_args时处理。
先期(处理)选项:
在 内核启动阶段,有些选项要在其它选项之前被处理,内核提供了early_param宏以代替__setup宏申明此类选项。这些选项由 parse_early_params函数解析。early_param宏和__setup宏仅有的不同就是前者设置了一个特殊标志让内核能够区分两种不 同的状况。这个标志是我们将在“.init.setup内存区”小节中看到的obs_kernel_param结构的一部分。
启动时选项在内核 2.6中的处理方式已经改变,但并非所有的内核代码都因此而更新。在最近一次改变之前,还仅用__setup宏。因此,遗留下来将被更新的代码现在使用 __obsolete_setup宏。但用户用__obsolete_setup宏定义的选项给内核时,内核打印一条警告消息说明它已是废弃状态,并提供 一个文件指针和随后被公告的源代码行信息。