uboot的最终目的是引导启动内核加载系统,根据这个线索我们可以首先找到uboot引导内核的main函数,查看系统引导的执行跳转的函数 main_loop。
下面对uboot函数的调用关系和主要调用函数进行分析。
一、uboot函数调用关系梳理
函数调用如下:
main.c common
void main_loop(void)
{cli_init();run_preboot_environment_command();s = bootdelay_process();autoboot_command(s);
}
当用户没有输入时,通常会在bootdealy延时时间到后通过autoboot_command(s)函数,自动执行uboot配置的bootcmd命令,引导启动内核。
这个main_loop(void)函数会循环执行,通过索引:run_main_loop调用main_loop()函数如下。
board_r.c (common)
static int run_main_loop(void)
{
#ifdef CONFIG_SANDBOXsandbox_main_loop_init();
#endif/* main_loop() can return to retry autoboot, if so just run it again */for (;;)main_loop();return 0;
}
在执行 run_main_loop前,uboot的所有初始化工作都在 init_sequence_r[] 初始化函数数组中逐一列出。
static init_fnc_t init_sequence_r[] = {initr_trace,initr_reloc,
initr_caches,
initr_reloc_global_data,
initr_barrier,initr_malloc,log_init,initr_bootstage, /* Needs malloc() but has its own timer */initr_console_record,board_init, /* Setup chipselects */set_cpu_clk_info, /* Setup clock information */efi_memory_init,stdio_init_tables,initr_serial,initr_announce,board_early_init_r,initr_env,initr_secondary_cpu,initr_pci,stdio_add_devices,initr_jumptable,console_init_r, /* fully init console as a device */console_announce_r,show_board_info,misc_init_r, /* miscellaneous platform-dependent init */interrupt_init,initr_enable_interrupts,initr_ethaddr,board_late_init,initr_scsi,initr_net,run_main_loop,
}
void board_init_r(gd_t *new_gd, ulong dest_addr)
{if (initcall_run_list(init_sequence_r))/* NOTREACHED - run_main_loop() does not return */hang();
}
uboot会根据对应的函数指针逐一执行初始化工作。
static inline int initcall_run_list(const init_fnc_t init_sequence[])
{const init_fnc_t *init_fnc_ptr;for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr)ret = (*init_fnc_ptr)();
}
在调用board_init_r之前,通常MCU会做一些与CPU密切相关的初始化工作,即Uboot第一阶段,通常由汇编语言完成。
64位的调用文件:
crt0_64.S (arch\arm\lib) line 152 : b board_init_r /* PC relative jump */b board_init_r /* PC relative jump *//* NOTREACHED - board_init_r() does not return */efi_main in efi_app.c (lib\efi) : board_init_r(NULL, 0);ENTRY(_main)
{}
arm64为调用位置:
start.S arch\arm\cpu\armv8 8533 2023/3/16
.globl _start
_start:
bl lowlevel_init
bl _mainefi_status_t EFIAPI efi_main(efi_handle_t image,struct efi_system_table *sys_table){board_init_r(NULL, 0); }
//uboot已支持uefi
crt0_aarch64_efi.S arch\arm\lib 3817 2023/3/16
_start:bl efi_main
二、主要调用函数分析
uboot命令执行时的调用关系
static int run_main_loop(void)
{
#ifdef CONFIG_SANDBOXsandbox_main_loop_init();
#endif/* main_loop() can return to retry autoboot, if so just run it again */for (;;)//当程序没有跳转运行kernel时(即按下空格按键时),系统将循环执行接收用户输入的命令。main_loop();return 0;
}
void main_loop(void)
{const char *s;bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");if (IS_ENABLED(CONFIG_VERSION_VARIABLE))env_set("ver", version_string); /* set version variable */
//初始化命令行接口(CLI)cli_init();
//执行预启动环境命令、主循环开始前从环境变量中读取并执行的。run_preboot_environment_command();if (IS_ENABLED(CONFIG_UPDATE_TFTP))update_tftp(0UL, NULL, NULL);
// 处理启动延迟。给用户时间来中断自动启动过程并进入命令行。s = bootdelay_process();if (cli_process_fdt(&s))cli_secure_boot_cmd(s);
//执行自动启动命令。这里s可能包含用户通过启动延迟中断选择的命令,或者是在没有中断时,从环境变量中读取的默认自动启动命令。autoboot_command(s);//进入CLI循环。这个函数通常会等待用户输入,解析输入,并执行相应的命令。cli_loop();panic("No CLI available");
}
延时函数的实现:
const char *bootdelay_process(void)
{
// 尝试从环境变量中获取bootdelay,否则使用配置的默认值 s = env_get("bootdelay");debug("### main_loop entered: bootdelay=%d\n\n", bootdelay);
//延时完成,从环境变量中读取bootcmd命令if (bootcount_error())s = env_get("altbootcmd");elses = env_get("bootcmd");s = env_get("bootcmd");
}
D2000#print
bootcmd=run distro_bootcmd
distro_bootcmd=run load_kernel; run load_initrd; run load_fdt; run boot_os
如果用户在2S中内输入了 回车/空格,则会根据用户输入的命令,来运行
void autoboot_command(const char *s)
{
//打印延时,等待命令debug("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");if (stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)) {
#if defined(CONFIG_AUTOBOOT_KEYED) && !defined(CONFIG_AUTOBOOT_KEYED_CTRLC)int prev = disable_ctrlc(1); /* disable Control C checking */
#endif
//解析和执行命令字符串中的命令列表,如果没有按下空格,则会执行默认的bootcmd命令run_command_list(s, -1, 0);#if defined(CONFIG_AUTOBOOT_KEYED) && !defined(CONFIG_AUTOBOOT_KEYED_CTRLC)disable_ctrlc(prev); /* restore Control C checking */
#endif}}
如果按下了空格,输入了命令就会执行用户输入的命令
int run_command_list(const char *cmd, int len, int flag)
{//解析命令rcode = parse_string_outer(buff, FLAG_PARSE_SEMICOLON);rcode = cli_simple_run_command_list(buff, flag);}
解析输入的命令,当按下回车时,检查命令。
int cli_simple_run_command_list(char *cmd, int flag)
{if (*next == '\n'){/* run only non-empty commands */if (*line) if (cli_simple_run_command(line, 0) < 0) {rcode = 1;break;}}}
处理命令,查找uboot中是否包含该命令
int cli_simple_run_command(const char *cmd, int flag)
{if (cmd_process(flag, argc, argv, &repeatable, NULL))/* Look up command in command table */cmdtp = find_cmd(argv[0]);find_cmd_tbl(cmd, start, len);/* If OK so far, then do the command */rc = cmd_call(cmdtp, flag, argc, argv, &newrep);}
解析命令参数,并执行相应的命令。
void cli_loop(void)
{bootstage_mark(BOOTSTAGE_ID_ENTER_CLI_LOOP);
#ifdef CONFIG_HUSH_PARSERparse_file_outer();/* This point is never reached */for (;;);
#elif defined(CONFIG_CMDLINE)cli_simple_loop();
#elseprintf("## U-Boot command line is disabled. Please enable CONFIG_CMDLINE\n");
#endif /*CONFIG_HUSH_PARSER*/
}
三、uboot命令的由来
在执行uboot命令时,可以看到uboot中有很多命令,那这些命令是怎么载入和实现的呢,下面逐一说明。
command.h include 11540 2023/3/16 6
在uboot中提供了一个cmd_tbl_s结构体,所有的命令都声明在 cmd_tbl_t 这个结构体对象中。
typedef struct cmd_tbl_s cmd_tbl_t;
cmd_tbl_t 成员变量如下:
struct cmd_tbl_s {//命令的名称char *name; /* Command Name *///命令的参数个数int maxargs; /* maximum number of arguments *//** Same as ->cmd() except the command* tells us if it can be repeated.* Replaces the old ->repeatable field* which was not able to make* repeatable property different for* the main command and sub-commands.*/int (*cmd_rep)(struct cmd_tbl_s *cmd, int flags, int argc,char * const argv[], int *repeatable);/* Implementation function *///命令对应的函数指针int (*cmd)(struct cmd_tbl_s *, int, int, char * const []);//命令的使用方法char *usage; /* Usage message (short) */
#ifdef CONFIG_SYS_LONGHELP//定义命令的帮组信息 --helpchar *help; /* Help message (long) */
#endif
#ifdef CONFIG_AUTO_COMPLETE/* do auto completion on the arguments */int (*complete)(int argc, char * const argv[], char last_char, int maxv, char *cmdv[]);
#endif
};
在使用时,定义uboot命令如下:
reset命令,调用的函数为do_reset函数。
boot.c cmd 1417 2023/3/16 16
U_BOOT_CMD(reset, 1, 0, do_reset,"Perform RESET of the CPU",""
);
poweroff命令,调用的函数为do_poweroff函数。
#ifdef CONFIG_CMD_POWEROFF
U_BOOT_CMD(poweroff, 1, 0, do_poweroff,"Perform POWEROFF of the device",""
);
定义使用命令示例:
doc/README.commands
在uboot中如果需要添加命令,首先需要包含command.h文件,然后使用U_BOOT_CMD()宏或者U_BOOT_CMD_COMPLETE宏添加命令到cmd_tbl_t 结构体中。
This is done by first including command.h, then using the U_BOOT_CMD() or the
Commands are added to U-Boot by creating a new command structure.
This is done by first including command.h, then using the U_BOOT_CMD() or the
U_BOOT_CMD_COMPLETE macro to fill in a cmd_tbl_t struct.
U_BOOT_CMD(name, maxargs, repeatable, command, "usage", "help")
U_BOOT_CMD_COMPLETE(name, maxargs, repeatable, command, "usage, "help", comp)
子命令定义,定义子命令可以使用 U_BOOT_CMD_MKENT或 U_BOOT_CMD_MKENTCOMPLETE宏
Sub-command definition
Likewise an array of cmd_tbl_t holding sub-commands can be created using either
of the following macros:* U_BOOT_CMD_MKENT(name, maxargs, repeatable, command, "usage", "help")
* U_BOOT_CMD_MKENTCOMPLETE(name, maxargs, repeatable, command, "usage, "help",comp)
static cmd_tbl_t demo_commands[] = {U_BOOT_CMD_MKENT(list, 0, 1, do_demo_list, "", ""),U_BOOT_CMD_MKENT(hello, 2, 1, do_demo_hello, "", ""),U_BOOT_CMD_MKENT(light, 2, 1, do_demo_light, "", ""),U_BOOT_CMD_MKENT(status, 1, 1, do_demo_status, "", ""),
};
实际使用的NVME命令,调用函数为do_nvme函数。
nvme.c cmd 1334 2023/3/16 21
static int do_nvme(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])U_BOOT_CMD(nvme, 8, 1, do_nvme,"NVM Express sub-system","scan - scan NVMe devices\n""nvme detail - show details of current NVMe device\n""nvme info - show all available NVMe devices\n""nvme device [dev] - show or set current NVMe device\n""nvme part [dev] - print partition table of one or all NVMe devices\n""nvme read addr blk# cnt - read `cnt' blocks starting at block\n"" `blk#' to memory address `addr'\n""nvme write addr blk# cnt - write `cnt' blocks starting at block\n"" `blk#' from memory address `addr'"
);
U_BOOT_CMD 命令是如何与 cmd_tbl_t 结构体关联起来的呢?
#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help)
U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, NULL)
U_BOOT_CMD宏定义来自U_BOOT_CMD_COMPLETE宏定义
#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help) \U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, NULL)
U_BOOT_CMD_COMPLETE宏定义由ll_entry_declare函数和U_BOOT_CMD_MKENT_COMPLETE宏定义函数组成
#define U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, _comp) \ll_entry_declare(cmd_tbl_t, _name, cmd) = \U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \_usage, _help, _comp); #define U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, \_comp) \_CMD_REMOVE(sub_ ## _name, _cmd)
#define U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \_usage, _help, _comp) \{ #_name, _maxargs, \_rep ? cmd_always_repeatable : cmd_never_repeatable, \_cmd, _usage, _CMD_HELP(_help) _CMD_COMPLETE(_comp) }#define U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, \_help, _comp) \{ #_name, _maxargs, NULL, 0 ? _cmd : NULL, _usage, \_CMD_HELP(_help) _CMD_COMPLETE(_comp) }
#define U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, _comp) \ll_entry_declare(cmd_tbl_t, _name, cmd) = \U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \_usage, _help, _comp);
ll_entry_declare函数也是一个宏定义,使用该宏定义的函数会被存放在u_boot_list段(attribute)。
#define ll_entry_declare(_type, _name, _list) \_type _u_boot_list_2_##_list##_2_##_name __aligned(4) \__attribute__((unused, \section(".u_boot_list_2_"#_list"_2_"#_name)))
uboot.lds
.u_boot_list : {KEEP(*(SORT(.u_boot_list*)));}
这个宏 ll_entry_declare 是用于在链接时生成的数组中声明一个条目的工具。这种机制在嵌入式系统、特别是像U-Boot这样的引导加载程序中非常有用,因为它允许开发者在编译时注册一系列的条目(如命令、设备驱动程序等),然后在运行时通过遍历这个链接时生成的数组来访问它们。
这个宏的详细解释和用法。
_type:条目的数据类型。
_name:条目的名称,用于在生成的数组中唯一标识该条目。
_list:条目所属列表的名称,这个名称将用于构造最终的变量名和段名。
宏的作用是声明一个全局变量,这个变量被放置在特定的段(section)中,段名由宏参数动态生成。__aligned(4) 确保变量按4字节对齐,这通常是出于硬件访问效率或特定要求的考虑。attribute((unused)) 告诉编译器这个变量可能不会被直接使用,但请保留它,因为它将在其他地方(如链接时)被引用。section 属性指定了变量应该被放置在哪个段中,这个段名是通过宏参数动态构造的,确保了不同列表和名称的条目可以分别存储在不同的段中。
假设我们有一个结构体 struct my_sub_cmd,并希望声明一个名为 my_sub_cmd 的条目,该条目属于名为 cmd_sub 的列表。
struct my_sub_cmd { int x; int y;
}; ll_entry_declare(struct my_sub_cmd, my_sub_cmd, cmd_sub) = { .x = 3, .y = 4,
};
这行代码会声明一个全局变量 _u_boot_list_2_cmd_sub_2_my_sub_cmd,
并将其放置在 .u_boot_list_2_cmd_sub_2_my_sub_cmd 段中。这个变量是 struct my_sub_cmd 类型的,并且被初始化为 { .x = 3, .y = 4 }。在U-Boot的初始化或运行时,可以编写代码来遍历 .u_boot_list_2_cmd_sub_2_my_sub_cmd 段中的所有条目,并执行相应的操作。
uboot启动时,是如何读取u_boot_list段中的参数的呢?
static cmd_tbl_t cmd_se_sub[] =
uboot启动时,会通过init函数加载所有的uboot命令
static int initr_manual_reloc_cmdtable(void)
{fixup_cmdtable(ll_entry_start(cmd_tbl_t, cmd),ll_entry_count(cmd_tbl_t, cmd));return 0;
}#if defined(CONFIG_NEEDS_MANUAL_RELOC)
void env_reloc(void)
{fixup_cmdtable(cmd_env_sub, ARRAY_SIZE(cmd_env_sub));
}
#endif
void fixup_cmdtable(cmd_tbl_t *cmdtp, int size)
{int i;if (gd->reloc_off == 0)return;for (i = 0; i < size; i++) {ulong addr;addr = (ulong)(cmdtp->cmd) + gd->reloc_off;cmdtp->cmd =(int (*)(struct cmd_tbl_s *, int, int, char * const []))addr;addr = (ulong)(cmdtp->name) + gd->reloc_off;cmdtp->name = (char *)addr;if (cmdtp->usage) {addr = (ulong)(cmdtp->usage) + gd->reloc_off;cmdtp->usage = (char *)addr;}
#ifdef CONFIG_SYS_LONGHELPif (cmdtp->help) {addr = (ulong)(cmdtp->help) + gd->reloc_off;cmdtp->help = (char *)addr;}
#endif
#ifdef CONFIG_AUTO_COMPLETEif (cmdtp->complete) {addr = (ulong)(cmdtp->complete) + gd->reloc_off;cmdtp->complete =(int (*)(int, char * const [], char, int, char * []))addr;}
#endifcmdtp++;}
}
se.c cmd 6734
static __maybe_unused void se_reloc(void)
{fixup_cmdtable(cmd_se_sub, ARRAY_SIZE(cmd_se_sub));
}
static int do_se(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{ c = find_cmd_tbl(argv[0], &cmd_se_sub[0], ARRAY_SIZE(cmd_se_sub));
}
uboot引导过程总结: