1. 程式人生 > >[uboot] (第六章)uboot流程——命令列模式以及命令處理介紹

[uboot] (第六章)uboot流程——命令列模式以及命令處理介紹

轉自https://blog.csdn.net/ooonebook/article/details/53164198

 

以下例子都以project X專案tiny210(s5pv210平臺,armv7架構)為例

[uboot] uboot流程系列: 
[project X] tiny210(s5pv210)上電啟動流程(BL0-BL2) 
[project X] tiny210(s5pv210)從儲存裝置載入程式碼到DDR 
[uboot] (第一章)uboot流程——概述 
[uboot] (第二章)uboot流程——uboot-spl編譯流程 
[uboot] (第三章)uboot流程——uboot-spl程式碼流程

 
[uboot] (第四章)uboot流程——uboot編譯流程 
[uboot] (第五章)uboot流程——uboot啟動流程 
[uboot] (番外篇)global_data介紹 
[uboot] (番外篇)uboot relocation介紹

建議先看《[uboot] (第五章)uboot流程——uboot啟動流程》

=================================================================================

一、說明

命令列模式就是指uboot執行完一切必要的初始化過程之後,等待終端輸入命令和處理命令的一個模式。 
所以後面的章節,我們先介紹命令如何儲存以及處理,再簡單說明命令列模式是如何工作的

1、需要開啟哪些巨集

  • CONFIG_CMDLINE 
    表示是否支援命令列模式,定義如下: 
    ./configs/bubblegum_defconfig:201:CONFIG_CMDLINE=y 
    ./configs/tiny210_defconfig:202:CONFIG_CMDLINE=y

  • CONFIG_SYS_GENERIC_BOARD 
    用於定義板子為通用型別的板子。開啟這個巨集之後,common/board_f.c和common/board_r.c才會被編譯進去,否則,需要自己實現。 
    ./configs/bubblegum_defconfig:7:CONFIG_SYS_GENERIC_BOARD=y 
    ./configs/tiny210_defconfig:7:CONFIG_SYS_GENERIC_BOARD=y 
    開啟之後,board_r.c中最終會執行run_main_loop進入命令列模式。具體參考《[uboot] (第五章)uboot流程——uboot啟動流程》。

  • CONFIG_SYS_PROMPT 
    命令列模式下的提示符。在tiny210中定義如下: 
    ./configs/tiny210_defconfig:203:CONFIG_SYS_PROMPT=”TINY210 => “

  • CONFIG_SYS_HUSH_PARSER 
    表示使用使用hush來對命令列進行解析。後續會繼續說明。在tiny210中定義如下: 
    ./include/configs/tiny210.h:121:#define CONFIG_SYS_HUSH_PARSER /* use “hush” command parser */

  • 對應命令需要開啟對應命令的巨集 
    以bootm命令為例,如果要支援bootm,則需要開啟CONFIG_CMD_BOOTM巨集,具體可以參考cmd/Makefile 
    ./configs/bubblegum_defconfig:226:CONFIG_CMD_BOOTM=y 
    ./configs/tiny210_defconfig:226:CONFIG_CMD_BOOTM=y

2、結合以下幾個問題來看後面的章節

  • 命令的資料結構,也就是程式碼裡面如何表示一個命令?
  • 如何定義一個命令,我們如何新增一個自己的命令?
  • 命令的存放和獲取?
  • 命令列模式的處理流程?

3、API

  • U_BOOT_CMD 
    #define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help) 
    定義一個命令。
  • cmd_process 
    enum command_ret_t cmd_process(int flag, int argc, char * const argv[], int *repeatable, ulong *ticks) 
    命令的處理函式,命令是作為argv[0]傳入。

具體引數意義和實現參考後面。

二、命令處理資料結構的存放

1、資料結構

uboot把所有命令的資料結構都放在一個表格中,我們後續稱之為命令表。表中的每一項代表著一個命令,其項的型別是cmd_tbl_t。 
資料結構如下:

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
};

typedef struct cmd_tbl_s    cmd_tbl_t;

引數說明如下:

  • name:定義一個命令的名字。 其實就是執行的命令的字串。這個要注意。
  • maxargs:這個命令支援的最大引數
  • repeatable:是否需要重複
  • cmd:命令處理函式的地址
  • usage:字串,使用說明
  • help:字串,幫助

2、在dump裡面的表示

通過以下命令解析出dump。

arm-none-linux-gnueabi-objdump -D u-boot > uboot_objdump.txt

以bootm命令為例,提取一部分資訊,加上了註釋資訊:

23e364cc <_u_boot_list_2_cmd_2_bootm>:                  
// bootm命令對應的資料結構符號是_u_boot_list_2_cmd_2_bootm,後續我們會說明,其起始地址是0x23e364cc
23e364cc:   23e2bbc2    mvncs   fp, #198656 ; 0x30800
// 這裡對應第一個成員name,其地址是0x23e2bbc2
23e364d0:   00000040    andeq   r0, r0, r0, asr #32
// 這裡對應第二個成員maxargs,maxargs=0x40
23e364d4:   00000001    andeq   r0, r0, r1 
// 這裡對應第三個成員repeatable,repeatable=1
23e364d8:   23e02028    mvncs   r2, #40 ; 0x28 
// 這裡對應第四個成員cmd,cmd命令處理函式的地址是0x23e02028,和下面的do_bootm的地址一致!!!
23e364dc:   23e2bbc8    mvncs   fp, #204800 ; 0x32000
// 這裡對應第五個成員usage,usage字串的地址是0x23e2bbc8
23e364e0:   23e34cb0    mvncs   r4, #45056  ; 0xb000
// 這裡對應第六個成員help,help字串的地址是0x23e34cb0

23e02028 <do_bootm>:    
23e34cb0 <bootm_help_text>:  

根據上述dump就可以把bootm命令的資料結構定義都找出來了。

3、命令資料結構在u-boot.map符號表中的位置定義

通過檢視u-boot.map,過濾出和u_boot_list中cmd相關的部分,對應符號表如下:

 *(SORT(.u_boot_list*))
 .u_boot_list_2_cmd_1
                0x23e3649c        0x0 cmd/built-in.o
 .u_boot_list_2_cmd_1
                0x23e3649c        0x0 common/built-in.o
 .u_boot_list_2_cmd_2_bootefi
                0x23e3649c       0x18 cmd/built-in.o
                0x23e3649c                _u_boot_list_2_cmd_2_bootefi
 .u_boot_list_2_cmd_2_bootelf
                0x23e364b4       0x18 cmd/built-in.o
                0x23e364b4                _u_boot_list_2_cmd_2_bootelf
 .u_boot_list_2_cmd_2_bootm
                0x23e364cc       0x18 cmd/built-in.o
                0x23e364cc                _u_boot_list_2_cmd_2_bootm
......
 .u_boot_list_2_cmd_2_true
                0x23e3670c       0x18 cmd/built-in.o
                0x23e3670c                _u_boot_list_2_cmd_2_true
 .u_boot_list_2_cmd_2_version
                0x23e36724       0x18 cmd/built-in.o
                0x23e36724                _u_boot_list_2_cmd_2_version
 .u_boot_list_2_cmd_3
                0x23e3673c        0x0 cmd/built-in.o
 .u_boot_list_2_cmd_3
                0x23e3673c        0x0 common/built-in.o

可以觀察到命令表是被定義在0x23e3649c( .u_boot_list_2_cmd_1)到0x23e3673c( .u_boot_list_2_cmd_3)的位置中。 
並且每一個項佔用了24個位元組,和cmd_tbl_t結構的大小是一致的。 
注意,根據註釋,.u_boot_list_2_cmd_1和.u_boot_list_2_cmd_3這兩個符號是由連結器自己生成的。 
這裡簡單有個印象,bootm命令對應的資料結構符號是u_boot_list_2_cmd_2_bootm

4、如何定義一個命令

(1)我們以bootm命令的定義為例: 
cmd/bootm.c中

U_BOOT_CMD(
    bootm,  CONFIG_SYS_MAXARGS, 1,  do_bootm,
    "boot application image from memory", bootm_help_text
);
// bootm就是我們的命令字串
// 在tiny210.h中定義了最大引數數量是64
// #define CONFIG_SYS_MAXARGS              64      /* max number of command args */
// 1表示重複一次
// 對應命令處理函式是do_bootm
// usage字串是"boot application image from memory"
// help字串是bootm_help_text定義的字串。

通過上面,可以看出是通過U_BOOT_CMD來定義了bootm命令對應的資料結構!!! 
並且命令處理函式的格式如下:

int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])

當命令處理函式執行成功時,需要返回0.返回非0值 
所以可以參照如上方式自己定義一個命令。

5、介紹一下U_BOOT_CMD的實現

include/common.h

#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help)      \
    U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, NULL)

#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_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd,      \
                _usage, _help, _comp)           \
        { #_name, _maxargs, _rep, _cmd, _usage,         \
            _CMD_HELP(_help) _CMD_COMPLETE(_comp) }

#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)))

    ll_entry_declare(cmd_tbl_t, _name, cmd)
// 以bootm為例
// _type=cmd_tbl_t
// _name=bootm
// _list=cmd
// 這裡最終會轉化為如下資料結構
// 
// cmd_tbl_t _u_boot_list_2_cmd_2_bootm=
// {
// _name=bootm,
// _maxargs=CONFIG_SYS_MAXARGS,
// _rep=1,
// _cmd=do_bootm,
// _usage="boot application image from memory",
// _help=bootm_help_text,
// _comp=NULL,
// }
// 並且這個資料結構給存放到了 .u_boot_list_2_cmd_2_bootm段中!!!和我們上述的完全一致。

三、命令的處理

1、簡單流程說明:

假設傳進來的命令是cmd。 
* 獲取命令表 
* 從命令表中搜索和cmd匹配的項 
* 執行對應項中的命令

後續我們我們分成“查詢cmd對應的表項”、“執行對應表項中的命令”兩部分進行說明

2、查詢cmd對應的表項——find_cmd

通過find_cmd可以獲取命令對應的命令表項cmd_tbl_t 。

(1)原理簡單說明 
前面我們知道了可以觀察到命令表是被定義在 .u_boot_list_2_cmd_1到.u_boot_list_2_cmd_3的位置中。所以我們從這個區間獲取命令表。 
並且根據表項中的name是否和傳進來的命令是否匹配來判斷是否是我們需要的表項。

(2)對應程式碼 
common/command.c

cmd_tbl_t *find_cmd(const char *cmd)
{
    cmd_tbl_t *start = ll_entry_start(cmd_tbl_t, cmd);
// 獲取命令表的地址,start表示指向命令表的指標,具體實現看後面
    const int len = ll_entry_count(cmd_tbl_t, cmd);
// 獲取命令表的長度,具體實現看後面
    return find_cmd_tbl(cmd, start, len);
// 以命令表的指標和命令表的長度為引數,查詢和cmd匹配的表項,也就是cmd_tbl_t結構,並返回給呼叫者。
}

/* find command table entry for a command */
cmd_tbl_t *find_cmd_tbl(const char *cmd, cmd_tbl_t *table, int table_len)
{
#ifdef CONFIG_CMDLINE
    cmd_tbl_t *cmdtp;
    cmd_tbl_t *cmdtp_temp = table;  /* Init value */
    const char *p;
    int len;
    int n_found = 0;

    if (!cmd)
        return NULL;
    /*
     * Some commands allow length modifiers (like "cp.b");
     * compare command name only until first dot.
     */
    len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);

    for (cmdtp = table; cmdtp != table + table_len; cmdtp++) {
// 通過指標遞增的方式,查詢table中的每一個cmd_tbl_t 
        if (strncmp(cmd, cmdtp->name, len) == 0) {
            if (len == strlen(cmdtp->name))
                return cmdtp;   /* full match */
// 如果是命令字串和表項中的name完全匹配,包括長度一致的,則直接返回

            cmdtp_temp = cmdtp; /* abbreviated command ? */
            n_found++;
// 如果命令字串和表項中的name的前面部分匹配的話(我們稱為部分匹配),暫時儲存下來,並且記錄有幾個這樣的表項,主要是為了支援自動補全的功能
        }
    }
    if (n_found == 1) {         /* exactly one match */
        return cmdtp_temp;
// 如果部分匹配的表項是唯一的話,則可以將這個表項返回,主要是為了支援自動補全的功能
    }
#endif /* CONFIG_CMDLINE */

    return NULL;    /* not found or ambiguous command */
}

include/linker_lists.h

#define ll_entry_start(_type, _list)                    \
({                                  \
    static char start[0] __aligned(4) __attribute__((unused,    \
        section(".u_boot_list_2_"#_list"_1")));         \
    (_type *)&start;                        \
})
// 因為傳進來的_list是cmd,所以這裡就是獲取命令表的起始地址,也就是.u_boot_list_2_cmd_1的地址,

#define ll_entry_end(_type, _list)                  \
({                                  \
    static char end[0] __aligned(4) __attribute__((unused,      \
        section(".u_boot_list_2_"#_list"_3")));         \
    (_type *)&end;                          \
})
// 因為傳進來的_list是cmd,所以這裡就是獲取命令表的結束地址,也就是.u_boot_list_2_cmd_3的地址,

#define ll_entry_count(_type, _list)                    \
    ({                              \
        _type *start = ll_entry_start(_type, _list);        \
        _type *end = ll_entry_end(_type, _list);        \
        unsigned int _ll_result = end - start;          \
        _ll_result;                     \
    })
// 因為傳進來的_list是cmd,所以這裡就是計算命令表的長度

3、執行對應表項中的命令——cmd_call

通過呼叫cmd_call可以執行命令表項cmd_tbl_t 中的命令 
common/command.c

static int cmd_call(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
    int result;

    result = (cmdtp->cmd)(cmdtp, flag, argc, argv);
// 直接執行命令表項cmd_tbl_t 中的cmd命令處理函式
    if (result)
        debug("Command failed, result=%d\n", result);
// 命令返回非0值時,報錯
    return result;
}

4、命令處理函式——cmd_process

程式碼如下: 
common/command.c

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]);
        // 第一個引數argv[0]表示命令,呼叫find_cmd獲取命令對應的表項cmd_tbl_t。
    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 OK so far, then do the command */
    if (!rc) {
        if (ticks)
            *ticks = get_timer(0);
        rc = cmd_call(cmdtp, flag, argc, argv);
                // 呼叫cmd_call執行命令表項中的命令,成功的話需要返回0值
        if (ticks)
            *ticks = get_timer(*ticks);
                // 判斷命令執行的時間
        *repeatable &= cmdtp->repeatable;
                // 這個命令執行的重複次數存放在repeatable中的
    }
    if (rc == CMD_RET_USAGE)
        rc = cmd_usage(cmdtp);
        // 命令格式有問題,列印幫助資訊
    return rc;
}

返回0表示執行成功,返回非0值表示執行失敗。 
後續需要執行一個命令的時候,直接呼叫cmd_process即可。

四、命令列模式的流程

命令列模式有兩種簡單的方式。正常模式是簡單地獲取串列埠資料、解析和處理命令。 
hush模式則是指命令的接收和解析使用busybox的hush工具,對應程式碼是hush.c。 
關於hush模式的作用和使用自己還不是很清楚,還要再研究一下。這裡簡單的寫一點流程。

1、入口

通過《[uboot] (第五章)uboot流程——uboot啟動流程》,我們知道了uboot在執行完所有初始化程式之後,呼叫run_main_loop進入主迴圈。 
通過主迴圈進入了命令列模式。 
common/board_r.c

static int run_main_loop(void)
{
    /* main_loop() can return to retry autoboot, if so just run it again */
    for (;;)
        main_loop();
// 這裡進入了主迴圈,而autoboot也是在主迴圈裡面實現
    return 0;
}

main_loop實現如下: 
這裡瞭解一個縮寫,cli,Command Line Interface,命令列介面,命令列介面。 
common/main.c

void main_loop(void)
{
    const char *s;

    bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
// 這裡用於標記uboot的進度,對於tiny210來說起始什麼都沒做

    cli_init();
// cli的初始化,主要是hush模式下的初始化

    run_preboot_environment_command();
// preboot相關的東西,後續有用到再說明

    s = bootdelay_process();
    if (cli_process_fdt(&s))
        cli_secure_boot_cmd(s);
    autoboot_command(s);
// autoboot的東西,後續使用autoboot的時候再專門說明

    cli_loop();
// 進入cli的迴圈模式,也就是命令列模式
    panic("No CLI available");
}

通過呼叫cli_loop進入了命令列模式,並且不允許返回。 
common/cli.c

void cli_loop(void)
{
#ifdef CONFIG_SYS_HUSH_PARSER
    parse_file_outer();
// 這裡進入hush命令模式
    /* This point is never reached */
    for (;;);
#elif defined(CONFIG_CMDLINE)
// 這裡進入通用命令列模式
    cli_simple_loop();
#else
// 說明沒有開啟CONFIG_CMDLINE巨集,無法進入命令列模式,直接列印一個提示
    printf("## U-Boot command line is disabled. Please enable CONFIG_CMDLINE\n");
#endif /*CONFIG_SYS_HUSH_PARSER*/
}

最終通過cli_simple_loop進入通用命令列模式,或者通過parse_file_outer進入hush命令列模式。 
因為通用命令列模式相對較為簡單,所以這邊先說明通用命令列模式。

2、通用命令列模式

程式碼如下:

void cli_simple_loop(void)
{
    static char lastcommand[CONFIG_SYS_CBSIZE + 1] = { 0, };

    int len;
    int flag;
    int rc = 1;

    for (;;) {

        len = cli_readline(CONFIG_SYS_PROMPT);
// 列印CONFIG_SYS_PROMPT,然後從串列埠讀取一行作為命令,儲存在console_buffer中
// 在tiny210中定義如下:./configs/tiny210_defconfig:203:CONFIG_SYS_PROMPT="TINY210 => "

        flag = 0;   /* assume no special flags for now */
        if (len > 0)
            strlcpy(lastcommand, console_buffer,
                CONFIG_SYS_CBSIZE + 1);
// 如果獲得了一個新行時,命令會儲存在console_buffer,將命令複製到lastcommand中
        else if (len == 0)
            flag |= CMD_FLAG_REPEAT;
// 只是得到一個單純的換行符時,設定重複標識,後續重複執行上一次命令

        if (len == -1)
            puts("<INTERRUPT>\n");
// 獲得非資料值時,直接列印中斷
        else
            rc = run_command_repeatable(lastcommand, flag);
// 否則,執行lastcommand中的命令

        if (rc <= 0) {
            /* invalid command or not repeatable, forget it */
            lastcommand[0] = 0;
// 命令執行出錯時,清空lastcommand,防止下一次重複執行這個命令
        }
    }
}

/* run_command_repeatable實現如下 */
int run_command_repeatable(const char *cmd, int flag)
{
    return cli_simple_run_command(cmd, flag);
}

/* cli_simple_run_command實現如下 */
int cli_simple_run_command(const char *cmd, int flag)
{
    char cmdbuf[CONFIG_SYS_CBSIZE]; /* working copy of cmd      */
    char *token;            /* start of token in cmdbuf */
    char *sep;          /* end of token (separator) in cmdbuf */
    char finaltoken[CONFIG_SYS_CBSIZE];
    char *str = cmdbuf;
    char *argv[CONFIG_SYS_MAXARGS + 1]; /* NULL terminated  */
    int argc, inquotes;
    int repeatable = 1;
    int rc = 0;

    debug_parser("[RUN_COMMAND] cmd[%p]=\"", cmd);
    if (DEBUG_PARSER) {
        /* use puts - string may be loooong */
        puts(cmd ? cmd : "NULL");
        puts("\"\n");
    }
    clear_ctrlc();      /* forget any previous Control C */

    if (!cmd || !*cmd)
        return -1;  /* empty command */

    if (strlen(cmd) >= CONFIG_SYS_CBSIZE) {
        puts("## Command too long!\n");
        return -1;
    }

    strcpy(cmdbuf, cmd);

    /* Process separators and check for invalid
     * repeatable commands
     */

    debug_parser("[PROCESS_SEPARATORS] %s\n", cmd);
    while (*str) {
// 這裡過濾掉一些對命令進行處理的部分程式碼
        /* find macros in this token and replace them */
        cli_simple_process_macros(token, finaltoken);

        /* Extract arguments */
        argc = cli_simple_parse_line(finaltoken, argv);
// 對命令進行加工處理,轉化成argv和argc格式。
        if (argc == 0) {
            rc = -1;    /* no command at all */
            continue;
        }

        if (cmd_process(flag, argc, argv, &repeatable, NULL))
            rc = -1;
// 呼叫cmd_process對命令進行處理
// 關於這個的實現我們在上述三、4中說明過了

        /* Did the user stop this? */
        if (had_ctrlc())
            return -1;  /* if stopped then not repeatable */
    }

    return rc ? rc : repeatable;
}

3、hush命令列模式

hush的實現自己也沒搞太懂,簡單的說明一下流程

static int parse_file_outer(FILE *f)
{
    int rcode;
    struct in_str input;
    setup_file_in_str(&input);
// 安裝一個輸入流,具體沒怎麼研究
    rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);
// 處理流資料
    return rcode;
}
後續的流程簡單說明如下:
parse_stream_outer
——》run_list
————》run_list_real
——————》run_pipe_real
————————》cmd_process

可以觀察到,最終還是呼叫了cmd_process來對命令進行處理,上述第三節已經說明了,這裡不重複說明了。