Linux系统移植:U-Boot 启动流程(下)
一、run_main_loop 函数详解
uboot 启动以后会进入 3 秒倒计时,如果在 3 秒倒计时结束之前按下按下回车键,那么就会进入 uboot 的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动 Linux 内核,这段过程就是由 run_main_loop 函数实现,函数代码如下:
static int run_main_loop(void)
{
#ifdef CONFIG_SANDBOX
sandbox_main_loop_init();
#endif
for (;;)
main_loop();
return 0;
}
函数执行在 main_loop() 死循环,函数代码如下:
void main_loop(void)
{
const char *s;
bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
#ifndef CONFIG_SYS_GENERIC_BOARD
puts("Warning: Your board does not use generic board. Please read\n");
puts("doc/README.generic-board and take action. Boards not\n");
puts("upgraded by the late 2014 may break or be removed.\n");
#endif
#ifdef CONFIG_VERSION_VARIABLE
setenv("ver", version_string);
#endif
cli_init();
run_preboot_environment_command();
#if defined(CONFIG_UPDATE_TFTP)
update_tftp(0UL, NULL, NULL);
#endif
s = bootdelay_process();
if (cli_process_fdt(&s))
cli_secure_boot_cmd(s);
autoboot_command(s);
cli_loop();
}
其中 bootstage_mark_name 函数,打印出启动进度
定义了宏 CONFIG_VERSION_VARIABLE 的话就会执行函数 setenv,设置换将变量 ver 的值为 version_string,也就是设置版本号环境变量
cli_init 函数,初始化 shell 相关的变量
run_preboot_environment_command 函数,获取环境变量 perboot 的内容,perboot是一些预启动命令,一般不使用这个环境变量
bootdelay_process 函数,此函数会读取环境变量 bootdelay 和 bootcmd 的内容,然后将 bootdelay 的值赋值给全局变量 stored_bootdelay,返回值为环境变量 bootcmd 的值
如果 定义了 CONFIG_OF_CONTROL 的话函数 cli_process_fdt 就会实现,如果没有定义 CONFIG_OF_CONTROL 的话函数 cli_process_fdt 直接返回一个 false。在本 uboot 中没有定义 CONFIG_OF_CONTROL,因此 cli_process_fdt 函数返回值为 false
autoboot_command 函数,此函数就是检查倒计时是否结束,如果倒计时自然结束那么就执行函数 run_command_list,此函数会执行参数 s 指定的一系列命令,也就是环境变量 bootcmd 的命令,bootcmd 里面保存着默认的启动命令,因此 linux 内核启动,如果倒计时结束之前按下按键,那么就会执行 cli_loop 函数,这个就是命令处理函数,负责接收好处理输入的命令,执行对应的指令
二、cli_loop 函数详解
cli_loop 函数是 uboot 的命令行处理函数,在 uboot 中输入各种命令就是由 cli_loop 来处理的,函数原型如下:
void cli_loop(void)
{
#ifdef CONFIG_SYS_HUSH_PARSER
parse_file_outer();
for (;;);
#else
cli_simple_loop();
#endif
}
定义宏 CONFIG_SYS_HUSH_PARSER,后面调用函数 parse_file_outer,后面是个死循环,不会执行到
parse_file_outer 函数如下:
#ifndef __U_BOOT__
static int parse_file_outer(FILE *f)
#else
int parse_file_outer(void)
#endif
{
int rcode;
struct in_str input;
#ifndef __U_BOOT__
setup_file_in_str(&input, f);
#else
setup_file_in_str(&input);
#endif
rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);
return rcode;
}
先调用函数 setup_file_in_str 初始化变量 input 的成员变量,之后调用函数 parse_stream_outer,这个函数就是 hush shell 的命令解释器,负责接收命令行输入,然后解析并执行相应的命令
函数 parse_stream_outer 中使用 do-while 循环就是处理输入命令,在循环中调用函数 parse_stream 进行命令解析,之后调用 run_list 函数来执行解析出来的命令,而函数 run_list 会经过一系列的函数调用,最终通过调用 cmd_process 函数来处理命令
三、 cmd_process 函数详解
cmd_process 函数用来处理命令行的命令,uboot 使用宏 U_BOOT_CMD 来定义命令,宏 U_BOOT_CMD 定义在文件 include/command.h 中,
#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 如下
#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_declar 定义在文件 include/linker_lists.h 中,定义如下
#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)))
_type 为 cmd_tbl_t 结构体,ll_entry_declare 就是定义了一个 cmd_tbl_t 变量,这里用到了 C 语言中的 “##” 连接符。其中的 “##_list” 表示用 _list 的值来替换,“##_name” 就是用 _name 的值来替换
宏 U_BOOT_CMD_MKENT_COMPLETE 定义在文件 include/command.h 中,内容如下:
#define U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \
_usage, _help, _comp) \
{ #_name, _maxargs, _rep, _cmd, _usage, \
_CMD_HELP(_help) _CMD_COMPLETE(_comp) }
上面代码中的 # 就是将 _name 传递过来的值字符串化,U_BOOT_CMD_MKENT_COMPLETE 又用到了_CMD_HELP 和 _CMD_COMPLETE,这两个宏的定义如下
#ifdef CONFIG_AUTO_COMPLETE
# define _CMD_COMPLETE(x) x,
#else
# define _CMD_COMPLETE(x)
#endif
#ifdef CONFIG_SYS_LONGHELP
# define _CMD_HELP(x) x,
#else
# define _CMD_HELP(x)
#endif
_CMD_COMPLETE 和 _CMD_HELP 就 是 取 自 身 的 值 , 然 后 在 加 上 一 个 ‘ , ‘,
以一个具体的命令为例,来看一下 U_BOOT_CMD 经过展开以后究竟是个什么模样的,比如 dhcp 命令:
U_BOOT_CMD(
dhcp, 3, 1, do_dhcp,
"boot image via network using DHCP/TFTP protocol",
"[loadAddress] [[hostIPaddr:]bootfilename]"
);
将其每一步运行函数都展开
U_BOOT_CMD(
dhcp, 3, 1, do_dhcp,
"boot image via network using DHCP/TFTP protocol",
"[loadAddress] [[hostIPaddr:]bootfilename]"
);
U_BOOT_CMD_COMPLETE(dhcp, 3, 1, do_dhcp,
"boot image via network using DHCP/TFTP protocol",
"[loadAddress] [[hostIPaddr:]bootfilename]",
NULL)
ll_entry_declare(cmd_tbl_t, dhcp, cmd) = \
U_BOOT_CMD_MKENT_COMPLETE(dhcp, 3, 1, do_dhcp, \
"boot image via network using DHCP/TFTP protocol", \
"[loadAddress] [[hostIPaddr:]bootfilename]", \
NULL);
cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \
__attribute__((unused,section(.u_boot_list_2_cmd_2_dhcp))) \
{ "dhcp", 3, 1, do_dhcp, \
"boot image via network using DHCP/TFTP protocol", \
"[loadAddress] [[hostIPaddr:]bootfilename]",\
NULL}
dhcp 命令最终展开结果:
cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \
__attribute__((unused,section(.u_boot_list_2_cmd_2_dhcp))) \
{ "dhcp", 3, 1, do_dhcp, \
"boot image via network using DHCP/TFTP protocol", \
"[loadAddress] [[hostIPaddr:]bootfilename]",\
NULL}
先定义了一个 cmd_tbl_t 类型的变量,变量名为_u_boot_list_2_cmd_2_dhcp,此变量 4字节对齐
使用 _attribute_ 关键字设置变量 _u_boot_list_2_cmd_2_dhcp 存储在 .u_boot_list_2_cmd_2_dhcp 段中
u-boot.lds 链接脚本中有一个名为 “.u_boot_list” 的段,所有 .u_boot_list 开头的段都存放到 .u_boot.list 中
cmd_tbl_t 是个结构体
{ "dhcp", 3, 1, do_dhcp, \
"boot image via network using DHCP/TFTP protocol", \
"[loadAddress] [[hostIPaddr:]bootfilename]",\
NULL}
就是初始化这结构体,cmd_tbl_t 结构体定义在文件 include/command.h 中:
struct cmd_tbl_s {
char *name;
int maxargs;
int repeatable;
int (*cmd)(struct cmd_tbl_s *, int, int, char * const []);
char *usage;
#ifdef CONFIG_SYS_LONGHELP
char *help;
#endif
#ifdef CONFIG_AUTO_COMPLETE
int (*complete)(int argc, char * const argv[], char last_char, int maxv, char *cmdv[]);
#endif
};
typedef struct cmd_tbl_s cmd_tbl_t;
上面的代码初始化后,结构体的数值如下:
_u_boot_list_2_cmd_2_dhcp.name = "dhcp"
_u_boot_list_2_cmd_2_dhcp.maxargs = 3
_u_boot_list_2_cmd_2_dhcp.repeatable = 1
_u_boot_list_2_cmd_2_dhcp.cmd = do_dhcp
_u_boot_list_2_cmd_2_dhcp.usage = "boot image via network using DHCP/TFTP protocol"
_u_boot_list_2_cmd_2_dhcp.help = "[loadAddress] [[hostIPaddr:]bootfilename]"
_u_boot_list_2_cmd_2_dhcp.complete = NULL
在 uboot 的命令行中输入“dhcp”这个命令的时候,最终经过一系列处理后执行的就是 do_dhcp 这个函数
总结:uboot 中使用 U_BOOT_CMD 来定义一个命令,最终的目的就是为了定义一个 cmd_tbl_t 类型的变量,并初始化这个变量的各个成员。uboot 中的每个命令都存储在.u_boot_list段中,每个命令都有一个名为 do_xxx(xxx 为具体的命令名)的函数,这个 do_xxx 函数就是具体的命令处理函数
下面看一下 cmd_process 具体代码:
enum command_ret_t cmd_process(int flag, int argc, char * const argv[],
int *repeatable, ulong *ticks)
{
enum command_ret_t rc = CMD_RET_SUCCESS;
cmd_tbl_t *cmdtp;
cmdtp = find_cmd(argv[0]);
if (cmdtp == NULL) {
printf("Unknown command '%s' - try 'help'\n", argv[0]);
return 1;
}
if (argc > cmdtp->maxargs)
rc = CMD_RET_USAGE;
#if defined(CONFIG_CMD_BOOTD)
else if (cmdtp->cmd == do_bootd) {
if (flag & CMD_FLAG_BOOTD) {
puts("'bootd' recursion detected\n");
rc = CMD_RET_FAILURE;
} else {
flag |= CMD_FLAG_BOOTD;
}
}
#endif
if (!rc) {
if (ticks)
*ticks = get_timer(0);
rc = cmd_call(cmdtp, flag, argc, argv);
if (ticks)
*ticks = get_timer(*ticks);
*repeatable &= cmdtp->repeatable;
}
if (rc == CMD_RET_USAGE)
rc = cmd_usage(cmdtp);
return rc;
}
先调用函数 find_cmd 在命令表中找到指定的命令,函数原型如下:
cmd_tbl_t *find_cmd(const char *cmd)
{
cmd_tbl_t *start = ll_entry_start(cmd_tbl_t, cmd);
const int len = ll_entry_count(cmd_tbl_t, cmd);
return find_cmd_tbl(cmd, start, len);
}
传入 cmd 命令,然后通过函数 ll_entry_start 得到数组的第一个元素,也就是命令表起始地址,然后通过函数 ll_entry_count 得到数组长度,也就是命令表的长度,最终通过函数 find_cmd_tbl 在命令表中找到所需的命令,每个命令都有一个 name 成员,所以将参数 cmd 与命令表中每个成员的 name 字段都对比一下,如果相等的话就说明找到了这个命令,找到以后就返回这个命令
最后调用函数 cmd_call 来执行具体的命令,执行指令函数如下:
static int cmd_call(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
int result;
result = (cmdtp->cmd)(cmdtp, flag, argc, argv);
if (result)
debug("Command failed, result=%d\n", result);
return result;
}
所以具体的调用流程就是处理字符串,创建对应的 cmd_tbl_t 存储单元,初始化 cmd_tbl_t 变量,然后 cmd_process 中会查找检测 cmd_tbl 的返回值,然后调用 cmd_call 函数执行指令,如果返回值为 CMD_RET_USAGE 的话就会调用 cmd_usage 函数输出命令的用法,其实就是输出 cmd_tbl_t 的 usage 成员变量
以上就是 U-Boot 流程分析的最后一部分!
|