0
点赞
收藏
分享

微信扫一扫

Linux内核的命令行解析机制


Linux kernel’s cmdline parse

kernel版本号:4.9.229

最近在工作中碰到了console的相关bug,于是抽时间学习了一下kernel的命令行解析原理。本文以4.9版本为例,粗略地介绍一下学习心得总结一下cmdline的解析机制。

cmdline往往由BootLoader和dts共同作用后得到。形式一般如下:

Kernel command line: console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw

kernel留出单独一块data段,即​​.ini.setup段​​。

arch/arm/kernel/vmlinux.lds.S
==>
.init.data : {
INIT_DATA
INIT_SETUP(16)
INIT_CALLS
CON_INITCALL
SECURITY_INITCALL
INIT_RAM_FS
}

include/asm-generic/vmlinux.lds.hs
==>
#define INIT_SETUP(initsetup_align) \
. = ALIGN(initsetup_align); \
VMLINUX_SYMBOL(__setup_start) = .; \
*(.init.setup) \
VMLINUX_SYMBOL(__setup_end) = .;

init.setup段起止​​__setup_start​​​和​​__setup_end​​。.init.setup段中存放的就是kernel通用参数和对应处理函数的映射表。

​include/linux/init.h​​​中定义了​​obs_kernel_param​​结构体,该结构体表征参数和对应处理函数,存放在.init.setup段中。

struct obs_kernel_param {
const char *str;
int (*setup_func)(char *);
int early;
};

#define __setup_param(str, unique_id, fn, early) \
static const char __setup_str_##unique_id[] __initconst \
__aligned(1) = str; \
static struct obs_kernel_param __setup_##unique_id \
__used __section(.init.setup) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }

#define __setup(str, fn) \
__setup_param(str, fn, fn, 0)

#define early_param(str, fn) \
__setup_param(str, fn, fn, 1)

我们重点关注console,在kernel/printk/printk.c中定义了

static int __init console_setup(char *str)
{
...
}
__setup("console=", console_setup);

所以我们将​​__setup("console=", console_setup);​​带入展开后得到:

static struct obs_kernel_param __setup_console_setup 
__used_section(.init.setup) __attribute__((aligned((sizeof(long)))) = {
.str = “console=”,
.setup_func = console_setup,
.early = 0
}

__setup_console_setup编译时就会链接到.init.setup段中,kernel运行时就会根据cmdline中的参数名与.init.setup段中obs_kernel_param的name对比。

匹配则调用console_setup来解析该参数,console_setup的参数就是cmdline中console的值。

接下来,当start_kernel函数执行,我们看是怎么一步一步地开始解析cmdline的。重点函数如下:

asmlinkage __visible void __init start_kernel(void)
{
...
/*
* 解析dtb中的bootargs并放置到boot_command_line中
* 并且会执行early param的解析
*/
setup_arch(&command_line);
...
setup_command_line(command_line); //简单的备份和拷贝boot_command_line
...
/*
* 执行early param的解析,由于setup_arch已经执行过一次,
* 所以这里不会重复执行,会直接return
*/
parse_early_param();
/*
* 执行普通的非early类型的cmdline的解析
*/
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, NULL, &unknown_bootoption);
if (!IS_ERR_OR_NULL(after_dashes))
parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
NULL, set_init_arg);
...
}

我们依次看一下这4个关键函数。

setup_arch

该函数与具体架构相关,不同架构对应不同的setup_arch函数,本文我们以arm为例。

setup_arch中解析tags获取cmdline,拷贝到boot_command_line中。同时内存和页表也做了一些对应的初始化。

关键函数如下所示:

void __init setup_arch(char **cmdline_p)
{
...
setup_processor();
// 搜索dtb中的chosen并解析bootargs参数,并放到boot_command_line中
mdesc = setup_machine_fdt(__atags_pointer);
...
strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
*cmdline_p = cmd_line;
...
// 解析cmdline中的early param,从boot_command_line中获取bootargs参数
parse_early_param();
...
early_paging_init(mdesc);
...
paging_init(mdesc);
...
}

setup_machine_fdt函数的调用链如下:

setup_machine_fdt
early_init_dt_scan_nodes
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

最终代码会调用到early_init_dt_scan_chosen,它的功能是扫描dts节点中的chosen,并解析对应的bootargs参数。

接下来调用parse_early_param,解析cmdline中的early param,从boot_command_line中获取bootargs参数。

void __init parse_early_param(void)
{
static int done __initdata;
static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;

if (done) //注意这个done flag,在一次启动过程中,该函数可能会被多次调用,但只会执行一次
return; //因为结尾将done设为1,再次执行时会直接return

strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
parse_early_options(tmp_cmdline); //解析动作会破坏tmp_cmdline中的数据,所以才有了前面一步copy动作
done = 1;
}

==>
void __init parse_early_options(char *cmdline)
{
parse_args("early options", cmdline, NULL, 0, 0, 0, NULL,
do_early_param);
}

parse_args的实现如下:

/* Args looks like "foo=bar,bar2 baz=fuz wiz". */
char *parse_args(const char *doing,
char *args,
const struct kernel_param *params,
unsigned num,
s16 min_level,
s16 max_level,
void *arg,
int (*unknown)(char *param, char *val,
const char *doing, void *arg))
{
char *param, *val, *err = NULL;

/* Chew leading spaces */
args = skip_spaces(args);

if (*args)
pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args);

while (*args) {
int ret;
int irq_was_disabled;

args = next_arg(args, ¶m, &val);
/* Stop at -- */
if (!val && strcmp(param, "--") == 0)
return err ?: args;
irq_was_disabled = irqs_disabled();
ret = parse_one(param, val, doing, params, num,
min_level, max_level, arg, unknown);
if (irq_was_disabled && !irqs_disabled())
pr_warn("%s: option '%s' enabled irq's!\n",
doing, param);

switch (ret) {
case 0:
continue;
case -ENOENT:
pr_err("%s: Unknown parameter `%s'\n", doing, param);
break;
case -ENOSPC:
pr_err("%s: `%s' too large for parameter `%s'\n",
doing, val ?: "", param);
break;
default:
pr_err("%s: `%s' invalid for parameter `%s'\n",
doing, val ?: "", param);
break;
}

err = ERR_PTR(ret);
}

return err;
}

parse_args遍历cmdline字符串,按照空格切割获取参数,对所有参数调用next_arg获取(param, val)键值对。如console=ttymxc0,115200,则param=console,val=ttymxc0,115200。

随后调用parse_one对键值对进行处理。

static int parse_one(char *param,
char *val,
const char *doing,
const struct kernel_param *params,
unsigned num_params,
s16 min_level,
s16 max_level,
void *arg,
int (*handle_unknown)(char *param, char *val,
const char *doing, void *arg))
{
unsigned int i;
int err;

/* Find parameter */
for (i = 0; i < num_params; i++) {
if (parameq(param, params[i].name)) {
if (params[i].level < min_level
|| params[i].level > max_level)
return 0;
/* No one handled NULL, so do it here. */
if (!val &&
!(params[i].ops->flags & KERNEL_PARAM_OPS_FL_NOARG))
return -EINVAL;
pr_debug("handling %s with %p\n", param,
params[i].ops->set);
kernel_param_lock(params[i].mod);
param_check_unsafe(¶ms[i]);
err = params[i].ops->set(val, ¶ms[i]);
kernel_param_unlock(params[i].mod);
return err;
}
}

if (handle_unknown) {
pr_debug("doing %s: %s='%s'\n", doing, param, val);
return handle_unknown(param, val, doing, arg);
}

pr_debug("Unknown argument '%s'\n", param);
return -ENOENT;
}

由于从parse_early_options传入的num_params=0,所以parse_one是直接走的最后handle_unknown函数,即parse-early_options传入的do_early_param。

static int __init do_early_param(char *param, char *val,
const char *unused, void *arg)
{
const struct obs_kernel_param *p;

for (p = __setup_start; p < __setup_end; p++) {
if ((p->early && parameq(param, p->str)) || //early是否置为1
(strcmp(param, "console") == 0 &&
strcmp(p->str, "earlycon") == 0)
) {
if (p->setup_func(val) != 0)
pr_warn("Malformed early option '%s'\n", param);
}
}
/* We accept everything at this stage. */
return 0;
}

do_early_param会从​​__setup_start​​​到​​__setup_end​​​区域进行搜索,这个区域其实就是前面说的​​__section(.init.setup)​​,并找到对应的obs_kernel_param结构数组,轮询其中定义的成员。

如果有obs_kernel_param的early为1,或cmdline中有console参数并且obs_kernel_param有earlycon参数,则会调用该obs_kernel_param的setup函数来解析参数。

do_early_param是为kernel中需要尽早配置的功能(如earlyprintk earlycon)做cmdline的解析。

而obs_kernel_param的early为0的,则延后执行解析,因为会再次调用到parse_args。

setup_command_line

调用setup_command_line将cmdline拷贝2份,放在​​saved_command_line​​​和​​static_command_line​​中。

static void __init setup_command_line(char *command_line)
{
saved_command_line =
memblock_virt_alloc(strlen(boot_command_line) + 1, 0);
initcall_command_line =
memblock_virt_alloc(strlen(boot_command_line) + 1, 0);
static_command_line = memblock_virt_alloc(strlen(command_line) + 1, 0);
strcpy(saved_command_line, boot_command_line);
strcpy(static_command_line, command_line);
}

parse_early_param

parse_early_param拷贝了一份boot_command_line,通过parse_early_options调用到了parse_args。

注意:start_kernel一共会调用2次parse_early_param,这是第2次。

/* Arch code calls this early on, or if not, just before other parsing. */
void __init parse_early_param(void)
{
static int done __initdata;
static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;

if (done)
return;

/* All fall through to do_early_param. */
strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
parse_early_options(tmp_cmdline);
done = 1;
}

如前文所述,done flag已被置1,所以这里会直接return。

parse_args

继续往下走,parse_early_param执行完成后,会执行parse_args。

注意,此时是start_kernel第2次执行parse_args。

第二次执行parse_args,其形参parse_args不再是NULL,而是指定了​​.__param段​​。

after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, NULL, &unknown_bootoption);

parse_args还是会遍历cmdline,分割cmdline为param和val键值对,对每对参数调用parse_one。这次parse_one的处理方式为:

  • 首先会遍历​​.__param​​​段中所有​​kernel_param​​,将其name与参数的param对比,同名则调用该kernel_param成员变量kernel_param_ops的set方法来设置参数值。这里主要是针对加载驱动的命令行参数的。
  • 如果parse_args传给parse_one是kernel通用参数,如console=ttyS0,115200。则parse_one前面遍历.__param段不会找到匹配的kernel_param。就走到后面调用handle_unknown。就是parse_args传来的​​unknown_bootoption​​。

unknown_bootoption如下:

static int __init unknown_bootoption(char *param, char *val,
const char *unused, void *arg)
{
repair_env_string(param, val, unused, NULL);

/* Handle obsolete-style parameters */
if (obsolete_checksetup(param)) //该函数是最终解析early=0类型param的
return 0;

/* Unused module parameter. */
if (strchr(param, '.') && (!val || strchr(param, '.') < val))
return 0;

if (panic_later)
return 0;

if (val) {
/* Environment option */
unsigned int i;
for (i = 0; envp_init[i]; i++) {
if (i == MAX_INIT_ENVS) {
panic_later = "env";
panic_param = param;
}
if (!strncmp(param, envp_init[i], val - param))
break;
}
envp_init[i] = param;
} else {
/* Command line option */
unsigned int i;
for (i = 0; argv_init[i]; i++) {
if (i == MAX_INIT_ARGS) {
panic_later = "init";
panic_param = param;
}
}
argv_init[i] = param;
}
return 0;
}

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) { //如果early=1,跳过,继续轮询
/* 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) { //如果setup_func不存在,就停止
pr_warn("Parameter %s is obsolete, ignored\n",
p->str);
return 1;
} else if (p->setup_func(line + n)) //循环执行setup_func
return 1;
}
p++;
} while (p < __setup_end);
return had_early_param;
}

  1. 首先repair_env_string会将param val重新组合为param=val形式。
  2. obsolete_checksetup则遍历-init_setup段所有obs_kernel_param,如有param->str与param匹配,则调用param_>setup进行参数值配置。
  3. parse_one对于parse_args传来的每一个cmdline参数都会将​​.__param​​​以及​​.init.setup​​段遍历匹配,匹配到str或name一致,则调用其相应的set或setup函数进行参数值解析或设置。

start_kernel中parse_args结束,kernel的cmdline就解析完成!

总结

  • kernel编译链接,利用​​.__param​​ 和​​.init.setup​​段将kernel所需参数和对应处理函数的映射表存放起来;
  • kernel启动,do_early_param处理kernel早期使用的参数(如earlyprintk earlycon)
  • parse_args对cmdline每个参数都遍历​​__param​​ 以及​​.init.setup​​进行匹配,匹配成功,则调用对应处理函数进行参数值的解析和设置。

需要注意的点

  • parse_early_param会执行2次:
  • 第一次在setup_arch中,解析early=1时对应的early params
  • 第二次由于done flag已经置1,会直接return。
  • parse_args也会执行2次
  • 第一次parse_args对应第一次执行parse_early_param时,对应的early params
  • 第二次parse_args在start_kernel中直接调用,执行解析early=0时对应的params


举报

相关推荐

0 条评论