分析了uboot命令解析和执行的过程
uboot命令体系基础
使用uboot命令
uboot启动后进入命令行环境下,在此输入命令按回车结束,uboot会收取这个命令然后解析,然后执行。
uboot命令体系实现代码在哪里
uboot命令体系的实现代码在 uboot/common/cmd_xxx.c 中。有若干个 .c 文件和命令体系有关。(还有 command.c, main.c 也是和命令有关的)
每个命令对应一个函数
- 每一个uboot的命令背后都对应一个函数。这就是uboot实现命令体系的一种思路和方法。
- 我们要找到每一个命令背后所对应的那个函数,而且要分析这个函数和这个命令是怎样对应起来的。
命令参数以argc&argv传给函数
-
有些uboot的命令还支持传递参数。也就是说命令背后对应的函数接收的参数列表中有argc和argv,然后命令体系会把我们执行命令时的命令+参数(
md 30000000 10)以argc(3)和argv(argv[0]=md, argv[1]=30000000 argv[2]=10)的方式传递给执行命令的函数。 -
举例分析,以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说起
- uboot启动的第二阶段,在初始化了所有该初始化的东西后,进入了一个死循环,死循环的循环体就是
main_loop。 main_loop函数执行一遍,就是一个获取命令、解析命令、执行命令的过程。run_command函数就是用来执行命令的函数。
main_loop 函数详解
main_loop()函数开始设置了一些变量- 然后对关键字构建了一个 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;
}
}
reserved_list在SDRAM中和DDR中都有一份,重定位之前的reserved_list是SDRAM中的,也就是说这里r=reserved_list的reserved_list是在SDRAM中的- NRES 是
reserved_list数组的个数 r->literal是结构体数组中某个结构体的首地址,这个首地址加上DDR中重定位的地址reloc_off,就是reserved_list在DDR中重定位后的地址r->literal = (char *)addr就是让 r 指向 DDR 中的那份关键字数组,于是从这之后reserved_list中的结构体元素就是DDR中的那份了
install_auto_complete
完成了关键字重定位之后,就来到了自动补全命令的绑定函数 install_auto_complete()。虽然在uboot中不使用自动补全,当我们还是来分析一下这个函数的具体实现
-
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()函数上 -
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)又做了什么呢?我们继续分析。 -
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){
}
-
查找分隔符
;或字符串尾,但允许简单的转义字符\;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 就是
;所在的位置 -
把
;替换成\0token = str; if (*sep) { // str 指向 ; 后面的内容 str = sep + 1; *sep = '\0'; } else { str = sep;/* no more commands for next pass */ } -
把命令行分解到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\0的e,argv[1]指向abcd\0的a,argv[2] = "1234" -
从uboot命令列表中查找命令并执行
cmdtp = find_cmd(argv[0]); if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) { rc = -1; } -
是否要重复执行,以及收尾处理
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 的命令处理循环,知道用户输入终止或者启动内核为止
关键点分析
- 控制台命令获取
- 命令解析。
parse_line函数把md 30000000 10解析成argv[0]=md, argv[1]=30000000 argv[2]=10 - 命令集中查找命令。
find_cmd(argv[0])函数去uboot的命令集合当中搜索有没有argv[0]这个命令 - 执行命令。最后用函数指针的方式调用执行了对应函数。
uboot如何处理命令集
可能的管理方式
- 数组。结构体数组,数组中每一个结构体成员就是一个命令的所有信息。
- 链表。链表的每个节点data段就是一个命令结构体,所有的命令都放在一条链表上。这样就解决了数组方式的不灵活。坏处是需要额外的内存开销,然后各种算法(遍历、插入、删除等)需要一定复杂度的代码执行。
- 有第三种吗?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个结构体实例构成一个命令
- 给命令结构体实例附加特定段属性(用户自定义段),链接时将带有该段属性的内容链接在一起排列(挨着的,不会夹杂其他东西,也不会丢掉一个带有这种段属性的,但是顺序是乱序的)。
- uboot重定位时将该段整体加载到DDR中。加载到DDR中的uboot镜像中带有特定段属性的这一段其实就是命令结构体的集合,有点像一个命令结构体数组。
- 段起始地址和结束地址(链接地址、定义在u-boot.lds中)决定了这些命令集的开始和结束地址。
uboot中增加自定义命令
在已有的c文件中直接添加命令
- 在
uboot/common/command.c中添加一个命令,叫:mycmd - 在已有的.c文件中添加命令比较简单,直接使用
U_BOOT_CMD宏即可添加命令,给命令提供一个do_xxx的对应的函数这个命令就齐活了。 - 添加完成后要重新编译工程(
make distclean; make x210_sd_config; make),然后烧录新的uboot去运行即可体验新命令。 - 还可以在函数中使用argc和argv来验证传参。
自建一个c文件并添加命令
- 在
uboot/common目录下新建一个命令文件,叫cmd_wilson.c(对应的命令名就叫wilson,对应的函数就叫do_wilson函数),然后在c文件中添加命令对应的U_BOOT_CMD宏和函数。注意头文件包含不要漏掉。 - 在
uboot/common/Makefile中添加上wilson.o,目的是让Make在编译时能否把cmd_wilson.c编译链接进去。 - 重新编译烧录。重新编译步骤是:
make distclean; make x210_sd_config; make
uboot命令体系的优点
- uboot的命令体系本身稍微复杂,但是他写好之后就不用动了。我们后面在移植uboot时也不会去动uboot的命令体系。我们最多就是向uboot中去添加命令,就像本节课所做的这样。
- 向uboot中添加命令非常简单。