背景

我们常用cmdline去控制某些功能的开启或关闭,或是传递一些参数。
在系统下,我们可以使用cat /proc/cmdline来查看启动参数,
那uboot或是grub的启动参数cmdline是怎么传递解析的呢?

传递与解析

以arm64为例,
流程图:

early_param --> obs_kernel_param(.init.setup段)  ----
                                                	|
                                                    ---> do_early_param (匹配,处理,执行`early_param`中的处理函数)
                                                    |
(uefi) -------> fdt ------> boot_command_line    ----

early_param

以常用的loglevel参数为例,

static int __init loglevel(char *str)
{
	int newlevel;

	/*
	 * Only update loglevel value when a correct setting was passed,
	 * to prevent blind crashes (when loglevel being set to 0) that
	 * are quite hard to debug
	 */
	if (get_option(&str, &newlevel)) {
		console_loglevel = newlevel;
		return 0;
	}

	return -EINVAL;
}

early_param("loglevel", loglevel);

early_param的定义:

/*
 * NOTE: fn is as per module_param, not __setup!
 * Emits warning if fn returns non-zero.
 */
#define early_param(str, fn)						\
	__setup_param(str, fn, fn, 1)
/*
 * Only for really core code.  See moduleparam.h for the normal way.
 *
 * Force the alignment so the compiler doesn't space elements of the
 * obs_kernel_param "array" too far apart in .init.setup.
 */
#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 }

按照上面的宏定义展开后为:

static const char __setup_str_loglevel __initconst \  //__initconst: 存放到.init.rodata段
        __aligned(1) = "loglevel";

static struct obs_kernel_param __setup_loglevel		\
    __used __section(.init.setup)				\       //__section(.init.setup): 存放到.init.setup段, __used: 避免被编译器优化
    __attribute__((aligned((sizeof(long)))))		\   //对齐
    = { "loglevel", loglevel, 1 }

do_early_param

Kernel初始化时

start_kernel(init/main.c)
    --> parse_early_param
        -->parse_early_options
            --> do_early_param

do_early_param该函数会遍历__setup_start __setup_end之间的所有结构体,然后将 cmdline中的各个参数与字符串str ”xxx”比较,如果匹配,那么调用setup_func(early_xxx)函数。
具体实现如下:

/* Check for early params. */
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)) ||
		    (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;
}

拷贝boot_command_linetmp_cmdline, 给parse_early_options处理

/* 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;
}
void __init parse_early_options(char *cmdline)
{
	parse_args("early options", cmdline, NULL, 0, 0, 0, NULL,
		   do_early_param);
}

parse_args函数主要是用于分解参数,并将分解的每个参数交给do_early_param处理。
实现如下:

/* 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, &param, &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;
}

boot_command_line

/* Untouched command line saved by arch-specific code. */
char __initdata boot_command_line[COMMAND_LINE_SIZE];

该变量boot_command_line怎么来的呢?怎么传递获取的?

以acpi+uefi+arm64为例:

efi_entry(drivers/firmware/efi/libstub/arm-stub.c)
    --> efi_convert_cmdline --从uefi中拿到cmdline
    --> allocate_new_fdt_and_exit_boot
        --> update_fdt --更新fdt
            --> fdt_setprop --设置chosen节点下的bootargs

后面就和使用fdt方式是一样了, 具体见后面。

arm64+devicetree:

start_kernel(init/main.c)
    --> setup_arch -- 对应不同架构的`setup_arch`
        --> setup_machine_fdt(__fdt_pointer) -- devicetree的物理地址转成虚拟地址
            --> early_init_dt_scan -- 扫描devicetree的节点
                --> early_init_dt_scan_nodes
                    --> of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line) -- `early_init_dt_scan_chosen`中拷贝chosen节点下`bootargs`等到boot_command_line

__fdt_pointer地址由bootloader启动kernel时通过x0,x21寄存器传递, 具体可见汇编代码:arch/arm64/kernel/head.S

efi_entry(efi-entry.S)-> start_kernel (arch/arm64/kernel/head.S)

扩展:__setup

early_param比较类似的还有一个:__setup
但也有些不同:

  1. 处理顺序不一样:early_param 宏注册的内核选项必须要在其他内核选项之前被处理。 即__setup_param 函数对应的第三个参数early不一样
  2. 对应的参数处理函数不一样:在函数start_kernel中,parse_early_param处理early_param定义的参数,parse_args处理__setup定义的参数

__setup的源码定义:

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

参考

early_param:
https://blog.csdn.net/flc2762/article/details/108626144

boot_command_line:
https://blog.csdn.net/tiantao2012/article/details/54923232
https://blog.csdn.net/tiantao2012/article/details/54923552