Linux系统移植:U-Boot 启动流程(下)

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
	/* main_loop() can return to retry autoboot, if so just run it again */
	for (;;)
		main_loop();
	return 0;
}

函数执行在 main_loop() 死循环,函数代码如下:

/* We come here after U-Boot is initialised and ready to process commands */
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 readn");
	puts("doc/README.generic-board and take action. Boards notn");
	puts("upgraded by the late 2014 may break or be removed.n");
#endif

#ifdef CONFIG_VERSION_VARIABLE
	setenv("ver", version_string);  /* set version variable */
#endif /* CONFIG_VERSION_VARIABLE */

	cli_init();

	run_preboot_environment_command();

#if defined(CONFIG_UPDATE_TFTP)
	update_tftp(0UL, NULL, NULL);
#endif /* CONFIG_UPDATE_TFTP */

	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();
	/* This point is never reached */
	for (;;);
#else
	cli_simple_loop();
#endif /*CONFIG_SYS_HUSH_PARSER*/
}

定义宏 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]"
);
//1、将 U_BOOT_CMD 展开后为:
U_BOOT_CMD_COMPLETE(dhcp, 3, 1, do_dhcp,
    "boot image via network using DHCP/TFTP protocol",
    "[loadAddress] [[hostIPaddr:]bootfilename]",
    NULL)
//2、将 U_BOOT_CMD_COMPLETE 展开后为:
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);
//3、将 ll_entry_declare 和 U_BOOT_CMD_MKENT_COMPLETE 展开后为:
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 中20220111124742

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;		/* Command Name			*/
	int		maxargs;	/* maximum number of arguments	*/
	int		repeatable;	/* autorepeat allowed?		*/
					/* Implementation function	*/
	int		(*cmd)(struct cmd_tbl_s *, int, int, char * const []);
	char		*usage;		/* Usage message	(short)	*/
#ifdef	CONFIG_SYS_LONGHELP
	char		*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
};

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;

	/* Look up command in command table */
	cmdtp = find_cmd(argv[0]);
	if (cmdtp == NULL) {
		printf("Unknown command '%s' - try 'help'n", argv[0]);
		return 1;
	}

	/* found - check max args */
	if (argc > cmdtp->maxargs)
		rc = CMD_RET_USAGE;

#if defined(CONFIG_CMD_BOOTD)
	/* avoid "bootd" recursion */
	else if (cmdtp->cmd == do_bootd) {
		if (flag & CMD_FLAG_BOOTD) {
			puts("'bootd' recursion detectedn");
			rc = CMD_RET_FAILURE;
		} else {
			flag |= CMD_FLAG_BOOTD;
		}
	}
#endif

	/* If OK so far, then do the command */
	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=%dn", 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 流程分析的最后一部分!

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>