Linux系統移植:U-Boot 啟動流程(下)

2022-01-11 22:00:23

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 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);  /* 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 detected\n");
			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=%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 流程分析的最後一部分!