uboot的命令体系

分析了uboot命令解析和执行的过程

分析了uboot命令解析和执行的过程

uboot命令体系基础

使用uboot命令

uboot启动后进入命令行环境下,在此输入命令按回车结束,uboot会收取这个命令然后解析,然后执行。

uboot命令体系实现代码在哪里

uboot命令体系的实现代码在 uboot/common/cmd_xxx.c 中。有若干个 .c 文件和命令体系有关。(还有 command.c, main.c 也是和命令有关的)

每个命令对应一个函数

  1. 每一个uboot的命令背后都对应一个函数。这就是uboot实现命令体系的一种思路和方法。
  2. 我们要找到每一个命令背后所对应的那个函数,而且要分析这个函数和这个命令是怎样对应起来的。

命令参数以argc&argv传给函数

  1. 有些uboot的命令还支持传递参数。也就是说命令背后对应的函数接收的参数列表中有argc和argv,然后命令体系会把我们执行命令时的命令+参数(md 30000000 10)以 argc(3)argv(argv[0]=md, argv[1]=30000000 argv[2]=10) 的方式传递给执行命令的函数。

  2. 举例分析,以help命令为例:

    • help命令背后对应的函数名叫:do_help

    • uboot/common/command.c 的236行

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

uboot命令解析和执行过程分析

从main_loop说起

  1. uboot启动的第二阶段,在初始化了所有该初始化的东西后,进入了一个死循环,死循环的循环体就是 main_loop
  2. main_loop 函数执行一遍,就是一个获取命令、解析命令、执行命令的过程。
  3. run_command 函数就是用来执行命令的函数。

main_loop 函数详解

  1. main_loop() 函数开始设置了一些变量
  2. 然后对关键字构建了一个 HUSH 链表

u_boot_hush_start

int u_boot_hush_start(void)
{
	if (top_vars == NULL) {
		top_vars = malloc(sizeof(struct variables));
		top_vars->name = "HUSH_VERSION";
		top_vars->value = "0.01";
		top_vars->next = 0;
		top_vars->flg_export = 0;
		top_vars->flg_read_only = 1;
		u_boot_hush_reloc();
	}
	return 0;
}

其中 top_vars 是一个全局变量,也是一个链表的首节点

struct variables {
	char *name;
	char *value;
	int flg_export;
	int flg_read_only;
	struct variables *next;
};

u_boot_hush_reloc() 函数是把定义在代码中的 reserved_list 指针数组元素重定位到DDR中

static struct reserved_combo reserved_list[] = {
	{ "if",    RES_IF,    FLAG_THEN | FLAG_START },
	{ "then",  RES_THEN,  FLAG_ELIF | FLAG_ELSE | FLAG_FI },
	{ "elif",  RES_ELIF,  FLAG_THEN },
	{ "else",  RES_ELSE,  FLAG_FI   },
    ......
	{ "done",  RES_DONE,  FLAG_END  }
}

重定位的方式

static void u_boot_hush_reloc(void)
{
	unsigned long addr;
	struct reserved_combo *r;

	for (r=reserved_list; r<reserved_list+NRES; r++) {
		addr = (ulong) (r->literal) + gd->reloc_off;
		r->literal = (char *)addr;
	}
}
  1. reserved_list 在SDRAM中和DDR中都有一份,重定位之前的 reserved_list 是SDRAM中的,也就是说这里 r=reserved_listreserved_list 是在SDRAM中的
  2. NRES 是 reserved_list 数组的个数
  3. r->literal 是结构体数组中某个结构体的首地址,这个首地址加上DDR中重定位的地址 reloc_off,就是 reserved_list 在DDR中重定位后的地址
  4. r->literal = (char *)addr 就是让 r 指向 DDR 中的那份关键字数组,于是从这之后 reserved_list 中的结构体元素就是DDR中的那份了

install_auto_complete

完成了关键字重定位之后,就来到了自动补全命令的绑定函数 install_auto_complete()。虽然在uboot中不使用自动补全,当我们还是来分析一下这个函数的具体实现

  1. install_auto_complete() 函数的实现

    void install_auto_complete(void)
    {
        install_auto_complete_handler("printenv", var_complete);
        install_auto_complete_handler("setenv", var_complete);
    }
    

    这里的install_auto_complete就是把 printenv的自动补全函数绑定到 var_complete() 函数上

  2. install_auto_complete_handler() 函数的实现

    static void install_auto_complete_handler(const char *cmd,
            int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]))
    {
        cmd_tbl_t *cmdtp;
       
        cmdtp = find_cmd(cmd);
        if (cmdtp == NULL)
            return;
       
        cmdtp->complete = complete;
    }
    

    也就是让printenv的补全函数指向 var_complete() 函数,即: printenv->complete = var_complete()。这里就是通过find_cmd(printenv)找到 printenv 命令对应的结构体指针,再把结构体中的complete指针指向var_complete()函数

    那么这里 find_cmd(cmd) 又做了什么呢?我们继续分析。

  3. find_cmd() 函数的实现

    • 第一部分

      cmd_tbl_t *find_cmd (const char *cmd)
      {
          cmd_tbl_t *cmdtp;
          cmd_tbl_t *cmdtp_temp = &__u_boot_cmd_start;/*Init value */
      

      这里出现了 &__u_boot_cmd_start 变量,它是来自于 uboot/board/samsung/x210/u-boot.lds 链接文件

      __u_boot_cmd_start = .;
      .u_boot_cmd : { *(.u_boot_cmd) }
      __u_boot_cmd_end = .;
      

      也就是 __u_boot_cmd_start 保存了 u_boot_cmd 段的起始地址,那么 cmdtp_temp 则是指向这个变量的指针。而且所有的uboot命令在编译阶段都被放在这个uboot段中,以数组的形式一个接一个的组织起来,只是命令排列的顺序是随机的,不是按照字母先后顺序排列。

      那么写uboot命令是怎么组织到一起的呢?答案在下面,所有的uboot命令都包含了命令的实现和命令的属性设置。以help命令为例,help命令的实现是 do_help() 函数。在实现了 do_help() 函数后,立刻跟上了一个属性设置:

      U_BOOT_CMD(
          help, CFG_MAXARGS, 1, do_help,
          "help    - print online help\n",
          "[command ...]\n"
          "    - show help information (for 'command')\n"
          "'help' prints online help for the monitor commands.\n\n"
          "Without arguments, it prints a short usage message for all commands.\n\n"
          "To get detailed help information for specific commands you can type\n"
          "'help' with one or more command names as arguments.\n"
      );
      

      这是一个宏

      #define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
      cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
      

      其中值得关注的:

      • name, maxarags, … 都是参数,比如 U_BOOT_CMD(help, ...) 这里 name = help

      • ##name 是一个连字符,它会把比如 U_BOOT_CMD(help, ...) 展开成 ___u_boot_cmd_help 字样,注意这里 ##name 展开成 name,它是预编译时的宏展开字符。

      • #name 则是把 help 替换成 "help" 字符串

      • 注意 name 和字符串 "name" 的区别

      • 宏定义中的 Struct_Section 内容如下:

        #define Struct_Section  __attribute__ ((unused,section (".u_boot_cmd")))
        

        这就是把 ___u_boot_cmd_help 设置成 .u_boot_cmd 段属性的秘密

      • 通过 U_BOOT_CMD 宏,我们方便的设置了 do_help() 函数容纳的最大参数量、是否回车重复执行命令、命令的实现、长短提示信息,以及所属的段

    • 第二部分

      len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);
      

      查找cmd中是否有 .,比如命令 md 就有 md.b, md.i 等命令,那么 len 记录的是主命令 md 的长度

    • 第三部分

      for (cmdtp = &__u_boot_cmd_start;
           cmdtp != &__u_boot_cmd_end;
           cmdtp++) {
          if (strncmp (cmd, cmdtp->name, len) == 0) {
              if (len == strlen (cmdtp->name))
                  return cmdtp;/* 全匹配 */
              cmdtp_temp = cmdtp;/* 缩写命令? 如:md.b */
              n_found++;
          }
      }
           
      // 如果只有一个缩写命令就返回它
      if (n_found == 1) {
          return cmdtp_temp;
      }
           
      /* 没找到或者有两可的命令,如md.b, md.i*/
      return NULL;
      

      首先我们知道 cmdtp 指针指向的结构体内容是:

      struct cmd_tbl_s {
          char *name; /* 命令名称 */
          int maxargs;/* 最大的参数数量 */
          int repeatable;/* 是否回车重复执行命令? */
                  /* 命令函数实现 */
          int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
          char *usage;/* 使用方法(简介) */
      #ifdef CFG_LONGHELP
          char *help;/*帮助信息(详细)*/
      #endif
      #ifdef CONFIG_AUTO_COMPLETE
          /* 自动补全函数指针 */
          int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);
      #endif
      };
      

      那么,我们通过 for 遍历uboot命令数组,比对数组中的命令名称和传入的命令名称是否相同,相同就返回这个命令结构体所在的地址

run_command

run_command() 的函数声明如下

int run_command (const char *cmd, int flag)

使用方式如下:

run_command("fastboot", 0);

下面我们来解析一下这个函数

这个函数的框架是在 while(*str) 中,其中 str 是命令字符串的副本,比如这里 str = "fastboot"

while (*str){
}
  1. 查找分隔符 ; 或字符串尾,但允许简单的转义字符 \;

    for (inquotes = 0, sep = str; *sep; sep++) {
        // 是不是在引号' '中, \'是, \\是\
        if ((*sep=='\'') &&
            (*(sep-1) != '\\'))
            inquotes=!inquotes;
       
        // 发现分隔符:;
        // 而且不在引号中,不是字符串串首;不是 '\;'
        if (!inquotes &&
            (*sep == ';') &&	/* separator		*/
            ( sep != str) &&	/* past string start	*/
            (*(sep-1) != '\\'))	/* and NOT escaped	*/
            break;
    }
    

    现在 sep 就是 ; 所在的位置

  2. ; 替换成 \0

    token = str;
    if (*sep) {
        // str 指向 ; 后面的内容
        str = sep + 1;
        *sep = '\0';
    } else {
        str = sep;/* no more commands for next pass */
    }
    
  3. 把命令行分解到argv[]中

    /* find macros in this token and replace them */
    /* 从字符串中找到宏并展开 */
    process_macros (token, finaltoken);
       
    /* 提取命令字符串中的参数 */
    if ((argc = parse_line (finaltoken, argv)) == 0) {
        rc = -1;	/* no command at all */
        continue;
    }
    

    注意这里的 parse_line() 是把 line 中的各个参数放到 argv[] 数组中,参数的个数通过返回值提供

    int parse_line (char *line, char *argv[])
    

    parse_line() 函数是把命令行字符串进行处理。比如 echo abcd 1234 b12 处理成 echo\0abcd\01234\0b12\0,同时 argv[0] 指向 echo\0eargv[1] 指向 abcd\0aargv[2] = "1234"

  4. 从uboot命令列表中查找命令并执行

    cmdtp = find_cmd(argv[0]);
    if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) {
        rc = -1;
    }
    
  5. 是否要重复执行,以及收尾处理

    repeatable &= cmdtp->repeatable;
       
    /* Did the user stop this? */
    // 判断一下是不是输入了 <ctrl>+c
    if (had_ctrlc ())
        return -1;  /* if stopped then not repeatable */
    }
       
    return rc ? rc : repeatable;
    

后续处理

在运行了 run_command(fastboot) 之后,还执行了如下指令:

    s = getenv ("bootcmd");
    run_command (s, 0);

    parse_file_outer(); //这里是do{}while()循环

    /* This point is never reached */
    for (;;);

这样就开始了 uboot 的命令处理循环,知道用户输入终止或者启动内核为止

关键点分析

  1. 控制台命令获取
  2. 命令解析。parse_line 函数把 md 30000000 10 解析成 argv[0]=md, argv[1]=30000000 argv[2]=10
  3. 命令集中查找命令。find_cmd(argv[0]) 函数去uboot的命令集合当中搜索有没有argv[0]这个命令
  4. 执行命令。最后用函数指针的方式调用执行了对应函数。

uboot如何处理命令集

可能的管理方式

  1. 数组。结构体数组,数组中每一个结构体成员就是一个命令的所有信息。
  2. 链表。链表的每个节点data段就是一个命令结构体,所有的命令都放在一条链表上。这样就解决了数组方式的不灵活。坏处是需要额外的内存开销,然后各种算法(遍历、插入、删除等)需要一定复杂度的代码执行。
  3. 有第三种吗?uboot没有使用数组或者链表,而是使用了一种新的方式来实现这个功能。

命令结构体cmd_tbl_t

struct cmd_tbl_s {
    char *name; /* 命令名称 */
    int maxargs;/* 最大的参数数量 */
    int repeatable;/* 是否回车重复执行命令? */
            /* 命令函数实现 */
    int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
    char *usage;/* 使用方法(简介) */
#ifdef CFG_LONGHELP
    char *help;/*帮助信息(详细)*/
#endif
#ifdef CONFIG_AUTO_COMPLETE
    /* 自动补全函数指针 */
    int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);
#endif
};

uboot的命令体系在工作时,一个命令对应一个cmd_tbl_t结构体的一个实例,然后uboot支持多少个命令,就需要多少个结构体实例。uboot的命令体系把这些结构体实例管理起来,当用户输入了一个命令时,uboot会去这些结构体实例中查找(查找方法和存储管理的方法有关)。如果找到则执行命令,如果未找到则提示命令未知。

uboot实现命令管理的思路

  1. 填充1个结构体实例构成一个命令
  2. 给命令结构体实例附加特定段属性(用户自定义段),链接时将带有该段属性的内容链接在一起排列(挨着的,不会夹杂其他东西,也不会丢掉一个带有这种段属性的,但是顺序是乱序的)。
  3. uboot重定位时将该段整体加载到DDR中。加载到DDR中的uboot镜像中带有特定段属性的这一段其实就是命令结构体的集合,有点像一个命令结构体数组。
  4. 段起始地址和结束地址(链接地址、定义在u-boot.lds中)决定了这些命令集的开始和结束地址。

uboot中增加自定义命令

在已有的c文件中直接添加命令

  1. uboot/common/command.c中添加一个命令,叫:mycmd
  2. 在已有的.c文件中添加命令比较简单,直接使用U_BOOT_CMD宏即可添加命令,给命令提供一个do_xxx的对应的函数这个命令就齐活了。
  3. 添加完成后要重新编译工程(make distclean; make x210_sd_config; make),然后烧录新的uboot去运行即可体验新命令。
  4. 还可以在函数中使用argc和argv来验证传参。

自建一个c文件并添加命令

  1. uboot/common目录下新建一个命令文件,叫cmd_wilson.c(对应的命令名就叫wilson,对应的函数就叫do_wilson函数),然后在c文件中添加命令对应的U_BOOT_CMD宏和函数。注意头文件包含不要漏掉。
  2. uboot/common/Makefile中添加上wilson.o,目的是让Make在编译时能否把cmd_wilson.c编译链接进去。
  3. 重新编译烧录。重新编译步骤是:make distclean; make x210_sd_config; make

uboot命令体系的优点

  1. uboot的命令体系本身稍微复杂,但是他写好之后就不用动了。我们后面在移植uboot时也不会去动uboot的命令体系。我们最多就是向uboot中去添加命令,就像本节课所做的这样。
  2. 向uboot中添加命令非常简单。