Linux操作系统启动时,往往都基于通用bootloader为基础,由bootloader完成基本的硬件基础设施的初始化工作后,再将一个完整的硬件环境交付给kernel。而二者之间的信息传递又尤为关键,前者可以给后者传递其配置、硬件环境信息等。
U-Boot向kernel传递的命令行参数属于环境变量的一种,可以说,整个U-Boot的环境变量都是围绕着命令行参数展开的。因此,分析U-Boot下的命令行参数,首先分析环境变量的初始化及工作原理。
文中所述基于U-Boot 2016.03,ARM 平台
初始化函数为env_init(),在board_init_f中调用。在最新版本的u-boot中,env_init()的定义位于env.c中,在此之前,env_init由板上贴装的存储外设驱动实现。例如:
./common/env_nand.c:66:int env_init(void)
./common/env_nowhere.c:29:int env_init(void)
./common/env_nvram.c:92:int env_init(void)
./common/env_flash.c:61:int env_init(void)
./common/env_flash.c:210:int env_init(void)
./common/env_ubi.c:25:int env_init(void)
./common/board_f.c:882: env_init,
./common/env_remote.c:28:int env_init(void)
./common/env_dataflash.c:77:int env_init(void)
./common/env_fat.c:28:int env_init(void)
./common/env_onenand.c:109:int env_init(void)
./common/spl/spl_net.c:21: env_init();
./common/env_sf.c:356:int env_init(void)
Binary file ./common/env_mmc.o matches
./common/env_sata.c:56:int env_init(void)
./common/env_eeprom.c:247:int env_init(void)
./common/env_mmc.su:4:env_mmc.c:62:5:env_init 0 static
./common/env_mmc.c:62:int env_init(void)
ubuntu16@localhost:uboot-2016$
对于传统的这种env_init(),环境变量的初始化值由default_environment[]决定,默认包含的环境变量如bootargs、bootcmd、ipaddr等等,均可以通过CONFIG_XXX来实现。
对于最新的u-boot工程,env_init()首先会检查存储外设驱动中是否实现了相应的load、save、init等函数。若未实现,采用默认的default_environment[]。
gd->env_addr = (ulong)&default_environment[0];
gd->env_valid = 1;
它位于board_r.c中,意味着第二次对环境变量进行初始化。若需要从存储介质中加载环境变量,例如使用了saveenv更改环境变量后重启,则从存储介质中读取环境变量,否则,继续使用默认的环境变量列表。
static int initr_env(void)
{
/* initialize environment */
if (should_load_env())
env_relocate();
else
set_default_env(NULL);
#ifdef CONFIG_OF_CONTROL
setenv_addr("fdtcontroladdr", gd->fdt_blob);
#endif
/* Initialize from environment */
load_addr = getenv_ulong("loadaddr", 16, load_addr);
#if defined(CONFIG_SYS_EXTBDINFO)
#if defined(CONFIG_405GP) || defined(CONFIG_405EP)
#if defined(CONFIG_I2CFAST)
/*
* set bi_iic_fast for linux taking environment variable
* "i2cfast" into account
*/
{
char *s = getenv("i2cfast");
if (s && ((*s == 'y') || (*s == 'Y'))) {
gd->bd->bi_iic_fast[0] = 1;
gd->bd->bi_iic_fast[1] = 1;
}
}
#endif /* CONFIG_I2CFAST */
#endif /* CONFIG_405GP, CONFIG_405EP */
#endif /* CONFIG_SYS_EXTBDINFO */
return 0;
}
待更改的环境变量时通过hash进行管理,更改并不会保存到存储设备中。u-boot中的hash代码位于lib/hashtable.c中,它是非常好的一个哈希实现。

保存环境变量的代码由两部分组成,其一是环境变量处理函数do_save_env(),其二是由存储设备驱动实现的saveenv()。

例如,环境变量存储于mmc时,需要mmc驱动支持,流程如下:

当改动环境变量、完成调试之后,可将环境变量恢复到原来的默认值,需要注意的是执行了env default命令后,执行env save才可以真正的将环境变量恢复到默认值。
=> env default -f -a
## Resetting to default environment
恢复的默认值从default_environment[]中获取。
void set_default_env(const char *s)
{
int flags = 0;
if (sizeof(default_environment) > ENV_SIZE) {
puts("*** Error - default environment is too large\n\n");
return;
}
if (s) {
if (*s == '!') {
printf("*** Warning - %s, "
"using default environment\n\n",
s + 1);
} else {
flags = H_INTERACTIVE;
puts(s);
}
} else {
puts("Using default environment\n\n");
}
if (himport_r(&env_htab, (char *)default_environment,
sizeof(default_environment), '\0', flags, 0,
0, NULL) == 0)
error("Environment import failed: errno = %d\n", errno);
gd->flags |= GD_FLG_ENV_READY;
}
在U-Boot中命令行参数用bootargs表示,若希望bootargs生效,一定要打开CONFIG_BOOTARGS配置选项。
赋值方式有两种,其一是通过宏定义的方式进行赋值,该赋值针对的是默认环境变量而言,是对default_environment[]中的bootargs进行初始化,也就是说,它指定的是默认bootargs选项。
const uchar default_environment[] = {
#endif
#ifdef CONFIG_ENV_CALLBACK_LIST_DEFAULT
ENV_CALLBACK_VAR "=" CONFIG_ENV_CALLBACK_LIST_DEFAULT "\0"
#endif
#ifdef CONFIG_ENV_FLAGS_LIST_DEFAULT
ENV_FLAGS_VAR "=" CONFIG_ENV_FLAGS_LIST_DEFAULT "\0"
#endif
#ifdef CONFIG_BOOTARGS
"bootargs=" CONFIG_BOOTARGS "\0"
#endif
#ifdef CONFIG_BOOTCOMMAND
"bootcmd=" CONFIG_BOOTCOMMAND "\0"
#endif
那么,也可以在BOOTCMD中直接对bootargs进行赋值,该种赋值方式是对默认环境变量bootargs的一种补充。
altbootcmd=run ${subbootcmds}
bootcmd=run ${subbootcmds}
configure=run set_uimage; setenv tftppath ${IVM_Symbol} ; km_setboardid && saveenv && reset
subbootcmds=tftpfdt tftpkernel nfsargs add_default boot
nfsargs=setenv bootargs root=/dev/nfs rw nfsroot=${serverip}:${toolchain}/${arch}
tftpfdt=if run set_fdthigh || test ${arch} != arm; then if tftpboot ${fdt_addr_r} ${tftppath}/fdt_0x${IVM_BoardId}_0x${IVM_HWKey}.dtb; then; else tftpboot ${fdt_addr_r} ${tftppath}/${hostname}.dtb; fi; else true; fi
tftpkernel=tftpboot ${load_addr_r} ${tftppath}/${uimage}
toolchain=/opt/eldk
rootfssize=0
目前,U-Boot传递给kernel的命令行参数位于内核设备树中的chosen结点中,因此,在修改U-Boot传递给kernel的命令行参数时,只需要考虑修改bootargs,由U-Boot的fdt_chosen()函数获取到最新的bootargs。
当U-Boot启动加载kernel时,由CONFIG_BOOTCMD指定的bootz或bootm,均会调用do_bootm_linux(),在该流程中完成bootargs的最后调整,通过getenv()函数获取新的bootargs。
/* cmd/nvedit.c */
char *getenv(const char *name)
{
if (gd->flags & GD_FLG_ENV_READY) { /* after import into hashtable */
ENTRY e, *ep;
WATCHDOG_RESET();
e.key = name;
e.data = NULL;
hsearch_r(e, FIND, &ep, &env_htab, 0);
return ep ? ep->data : NULL;
}
/* restricted capabilities before import */
if (getenv_f(name, (char *)(gd->env_buf), sizeof(gd->env_buf)) > 0)
return (char *)(gd->env_buf);
return NULL;
}
综上,U-Boot中对bootargs的处理流程如下图所示:

文中所述基于kernel 5.4,ARM 平台
kernel对U-Boot传递过来的命令行参数的处理如上图所示,详细内容见下文。

若使用U-Boot作为加载kernel的bootloader,它给kernel传递参数的方式有两种,一种是原始的atags,其直接存放命令行参数;另外一种是现阶段广泛使用的fdt,将命令行参数存放于fdt的chosen结点中,当我们获取到fdt地址后,再解析fdt文件中的chosen结点即可获取到命令行参数。
u-boot将处理器的控制权交付给kernel之前,会对处理器的通用寄存器进行赋值,填充机器码以及参数地址信息,kernel启动后,在汇编阶段获取到此部分信息,然后基于此进行后续一系列的解析和应用。
获取dtb地址的代码位于kernel的汇编文件中,代码位于head-common.S:
__INIT
__mmap_switched:
......
.long processor_id @ r0
.long __machine_arch_type @ r1
.long __atags_pointer @ r2
......
b start_kernel
ENDPROC(__mmap_switched)
将r2寄存器中存放的dtb地址赋值给__atags_pointer后,跳转到start_kernel中执行C语言环境的初始化工作。
在进入到start_kernel()函数后,同command_line相关的函数有两个,分别是setup_arch(&command_line)与setup_command_line(command_line),前者完成命令行的解析,后者针对命令行的应用。
/* init/main.c */
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
......
setup_arch(&command_line);
setup_command_line(command_line);
......
}
该函数利用到了汇编代码中从R2寄存器获取到的__atags_pointer,将该值作为设备树地址,并再次完成设备树正确性检查,并获取到boot_command_line字符串。
/* arch/arm/kernel/setup.c */
void __init setup_arch(char **cmdline_p)
{
const struct machine_desc *mdesc = NULL;
void *atags_vaddr = NULL;
if (__atags_pointer)
atags_vaddr = FDT_VIRT_BASE(__atags_pointer);
setup_processor();
if (atags_vaddr) {
mdesc = setup_machine_fdt(atags_vaddr);
if (mdesc)
memblock_reserve(__atags_pointer,
fdt_totalsize(atags_vaddr));
}
if (!mdesc)
mdesc = setup_machine_tags(atags_vaddr, __machine_arch_type);
if (!mdesc) {
early_print("\nError: invalid dtb and unrecognized/unsupported machine ID\n");
early_print(" r1=0x%08x, r2=0x%08x\n", __machine_arch_type,
__atags_pointer);
if (__atags_pointer)
early_print(" r2[]=%*ph\n", 16, atags_vaddr);
dump_machine_table();
}
......
}
boot_command_line的定义位于init/main.c中,它作为命令行解析后的中转变量,后续会复制给cmd_line。
/* init/main.c */
/* Untouched command line saved by arch-specific code. */
char __initdata boot_command_line[COMMAND_LINE_SIZE];
当setup_arch()调用setup_machine_fdt(),进行设备树合法性检查。
/* arch/arm/kernel/devtree.c */
const struct machine_desc * __init setup_machine_fdt(void *dt_virt)
{
const struct machine_desc *mdesc, *mdesc_best = NULL;
......
if (!dt_virt || !early_init_dt_verify(dt_virt))
return NULL;
mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
if (!mdesc) {
const char *prop;
int size;
unsigned long dt_root;
early_print("\nError: unrecognized/unsupported "
"device tree compatible list:\n[ ");
dt_root = of_get_flat_dt_root();
prop = of_get_flat_dt_prop(dt_root, "compatible", &size);
while (size > 0) {
early_print("'%s' ", prop);
size -= strlen(prop) + 1;
prop += strlen(prop) + 1;
}
early_print("]\n\n");
dump_machine_table(); /* does not return */
}
......
}
设备树合法性检查通过后,调用early_init_dt_scan_nodes()解析设备树dtb的chosen结点,获取到boot_command_line。
/* arch/arm/kernel/devtree.c */
const struct machine_desc * __init setup_machine_fdt(void *dt_virt)
{
......
early_init_dt_scan_nodes();
/* Change machine number to match the mdesc we're using */
__machine_arch_type = mdesc->nr;
return mdesc;
}
在early_init_dt_scan_nodes()中解析chosen结点,获取命令行参数。该结点信息除kernel设备树文件中定义之外,U-Boot中也可以对该结点进行信息追加,增加额外的命令行参数。
/* drivers/of/fdt.c */
void __init early_init_dt_scan_nodes(void)
{
int rc = 0;
/* Retrieve various information from the /chosen node */
rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
if (!rc)
pr_warn("No chosen node found, continuing without\n");
/* Initialize {size,address}-cells info */
of_scan_flat_dt(early_init_dt_scan_root, NULL);
/* Setup memory, calling early_init_dt_add_memory_arch */
of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}
setup_arch()函数与处理器架构强相关,不同架构的处理器实现不同。在ARM平台下,其代码实现位于arch/arm/kernel/setup.c
中,可以关注下其函数入参char **cmdline_p,该种方式保证了命令行参数可以在后续函数中继续使用及同步修改。
在2.1节中获取到boot_command_line字符串后,将其赋值给cmdline_p,供setup_command_line(command_line)使用。
/* arch/arm/kernel/setup.c */
void __init setup_arch(char **cmdline_p)
{
/* populate cmd_line too for later use, preserving boot_command_line */
strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
*cmdline_p = cmd_line;
......
}
在setup_arch()函数中调用parse_early_param()完成命令行参数的解析,也就是对命令行中所定义的功能进行拆解,这部分属于kernel层面的功能,同硬件架构无关。
/* init/main.c */
void __init parse_early_param(void)
{
......
parse_early_options(tmp_cmdline);
......
}
命令行参数的解析主要获取两部分信息,其一是console相关,其二是initcall相关。
/* init/main.c */
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;
}
void __init parse_early_options(char *cmdline)
{
parse_args("early options", cmdline, NULL, 0, 0, 0, NULL,
do_early_param);
}
以上解析命令行参数中所涉及到的函数parse_args(),属于标准的命令行参数解析代码,在后续命令行参数应用时,除了调用该函数之外,还可以调用其他函数获取到U-Boot传递给kernel驱动的命令行参数。
命令行参数除了给kernel使用之外(如第4节所描述的那样,提供串口信息以及initcall函数),还有两种应用,分别是供proc文件系统查询命令行参数以及驱动代码获取控制参数。
我们都知道,Linux系统启动后,通过查询/proc下的命令行文件可以获取到当前系统的命令行参数,如下所示:
ubuntu16@ubuntu16:linux-5.4$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-4.15.0-117-generic root=UUID=7bf3a312-30c8-4617-a187-561d457ed5b1 ro quiet splash
ubuntu16@ubuntu16:linux-5.4$
那么,如上信息从哪里来的呢?
在前文第2节中有提到"setup_command_line(command_line);",当kernel获取到命令行参数后会将其转出,其目的就是供该函数使用,得到saved_command_line以及static_command_line。
/* init/main.c */
static void __init setup_command_line(char *command_line)
{
size_t len = strlen(boot_command_line) + 1;
saved_command_line = memblock_alloc(len, SMP_CACHE_BYTES);
if (!saved_command_line)
panic("%s: Failed to allocate %zu bytes\n", __func__, len);
initcall_command_line = memblock_alloc(len, SMP_CACHE_BYTES);
if (!initcall_command_line)
panic("%s: Failed to allocate %zu bytes\n", __func__, len);
static_command_line = memblock_alloc(len, SMP_CACHE_BYTES);
if (!static_command_line)
panic("%s: Failed to allocate %zu bytes\n", __func__, len);
strcpy(saved_command_line, boot_command_line);
strcpy(static_command_line, command_line);
}
上面代码中的saved_command_line变量,会提供给proc文件系统使用,如下所示:
/* fs/proc/cmdline.c */
static int cmdline_proc_show(struct seq_file *m, void *v)
{
seq_puts(m, saved_command_line);
seq_putc(m, '\n');
return 0;
}
static int __init proc_cmdline_init(void)
{
proc_create_single("cmdline", 0, NULL, cmdline_proc_show);
return 0;
}
fs_initcall(proc_cmdline_init);
在驱动代码中获取命令行参数的函数是#define __setup_param(str, unique_id, fn, early),它有两种常见的封装,分别是:
__setup和early_param。在驱动代码中随处可见这两种分装的应用。
# __setup
./drivers/video/console/vgacon.c:137:__setup("no-scroll", no_scroll);
./drivers/pcmcia/vrc4171_card.c:694:__setup("vrc4171_card=", vrc4171_card_setup);
./drivers/clk/imx/clk.c:144:__setup_param("earlycon", imx_keep_uart_earlycon,
./drivers/clk/clk.c:1276:__setup("clk_ignore_unused", clk_ignore_unused_setup);
./drivers/cpuidle/sysfs.c:27:__setup("cpuidle_sysfs_switch", cpuidle_sysfs_setup);
./drivers/base/power/domain.c:900:__setup("pd_ignore_unused", pd_ignore_unused_setup);
./drivers/base/dd.c:236:__setup("deferred_probe_timeout=", deferred_probe_timeout_setup);
./drivers/base/devtmpfs.c:57:__setup("devtmpfs.mount=", mount_param);
# early_param
./drivers/char/random.c:861:early_param("random.trust_cpu", parse_trust_cpu);
./drivers/iommu/irq_remapping.c:81:early_param("intremap", setup_irqremap);
./drivers/acpi/tables.c:871:early_param("acpi_apic_instance", acpi_parse_apic_instance);
./drivers/acpi/osl.c:178:early_param("acpi_rsdp", setup_acpi_rsdp);
./drivers/irqchip/irq-ls-scfg-msi.c:83:early_param("lsmsi", early_parse_ls_scfg_msi);
./drivers/cpufreq/intel_pstate.c:2853:early_param("intel_pstate", intel_pstate_setup);
./drivers/pci/pci.c:6483:early_param("pci", pci_setup);
当我们希望在自己的驱动代码中增加命令行解析功能时,按照如下形式进行定义即可:
/* __setup */
static int __init setup_bert_disable(char *str)
{
bert_disable = 1;
return 0;
}
__setup("bert_disable", setup_bert_disable);
/* early_param */
static int __init sysfs_deprecated_setup(char *arg)
{
return kstrtol(arg, 10, &sysfs_deprecated);
}
early_param("sysfs.deprecated", sysfs_deprecated_setup);