分析了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 就是
;
所在的位置 -
把
;
替换成\0
token = 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中添加命令非常简单。