uboot 命令实现机制

  • 如何添加 uboot 命令及 uboot 命令实现机制

一、命令是如何被“定义”的

要搞懂U-Boot的命令,第一步当然是看它是怎么被定义的。
common/ 目录下有很多 cmd_xxx.c 格式的文件,比如 cmd_echo.ccmd_exit.c 等。随便点开一个,比如 cmd_exit.c,就会发现里面有一个关键的函数 do_exit() 和一个宏 U_BOOT_CMD

1
2
3
4
5
6
7
8
9
10
11
12
// common/cmd_exit.c
int do_exit(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
/* exit with last command's return value */
return last_common_return_value;
}

U_BOOT_CMD(
exit, 2, 1, do_exit,
"exit script",
""
);

很明显,U_BOOT_CMD 这个宏就是定义命令的核心。这个宏定义在 include/command.h 中:

1
2
3
/* 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 才是最终的实现(这里贴出的是一个典型U-Boot版本中的定义,不同版本可能略有差异,但原理一致):

1
2
3
4
5
/* include/command.h */
#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_declare 这个宏负责创建变量和设置段属性,而 U_BOOT_CMD_MKENT_COMPLETE 负责初始化结构体。最终展开后,其核心思想是:定义一个 cmd_tbl_t 类型的结构体变量,并用传入的参数去初始化它

我们再用 exit 命令的例子来具体看一下:

1
2
3
4
5
U_BOOT_CMD(
exit, 2, 1, do_exit,
"exit script",
""
);

经过层层宏展开,它就等价于定义了一个全局变量,并将其放入了特定的段中,大致如下:

1
2
3
cmd_tbl_t _u_boot_list_2_cmd_2_exit __aligned(4) \
__attribute__((unused,section(".u_boot_list_2_cmd_2_exit"))) = \
{ "exit", 2, 1, do_exit, "exit script", "", NULL };

这里面有几个关键点:

  1. 变量定义: 定义了一个 cmd_tbl_t 类型的变量,变量名叫 _u_boot_list_2_cmd_2_exit,并且是4字节对齐的。
  2. 段属性: __attribute__((unused,section(".u_boot_list_2_cmd_2_exit"))) 是一个GCC的扩展。unused 是为了防止编译器在某些情况下警告变量未被使用。section(...) 则是最关键的部分,它告诉编译器,不要把这个变量放到普通的 .data 段,而是放到一个我们自定义的、名为 .u_boot_list_2_cmd_2_exit 的段里面。这个后面提到。
  3. 结构体初始化: {...} 这部分就是用我们传入的参数来初始化这个结构体变量的各个成员。

二、命令的数据结构 cmd_tbl_t

从上面的分析我们知道,U-Boot里的每一个命令,实际上都是一个 cmd_tbl_t 结构体实例。这个结构体定义在 include/command.h 中,它就像一个命令的“身份证”,记录了这个命令的所有信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 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_CMD`宏的参数和这个结构体成员做一个对应表格:

宏参数 cmd_tbl_t 成员 含义
name name 命令的名称,直接写,不用加双引号
maxargs maxargs 命令能接受的最大参数个数
rep repeatable 是否可重复?为1时,按回车键会重复执行此命令
cmd cmd 指向命令实现函数(do_xxx)的函数指针
usage usage 简短的使用说明,比如在 help 命令列表里显示
help help 详细的帮助信息,通过 help <命令> 显示

现在再回看 exit 命令的初始化 { "exit", 2, 1, do_exit, "exit script", "", NULL },就一目了然了:它的 name 成员是 "exit"maxargs2repeatable1cmd 函数指针指向 do_exit… 所有信息都保存在了这个结构体里。

三、命令是如何被管理的

我们知道了每个命令都是一个独立的 cmd_tbl_t 变量,而且它们都被放到了一个以 .u_boot_list_2_cmd_2_ 开头的自定义段里。那么,系统是怎么找到所有这些分散的命令的。

答案就在U-Boot的链接脚本 u-boot.lds 里。在这个文件里,有一段专门处理命令表的定义:

1
2
3
4
5
/* u-boot.lds */
. = ALIGN(4);
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*)));
}

这段脚本的作用是:

  1. 收集: *(.u_boot_list*) 是一个通配符,它会找到所有编译单元中段名以 .u_boot_list 开头的所有段。
  2. 排序和合并: SORT(...) 会对找到的段进行排序(Maybe是按段名),KEEP(...) 使这些段没有被直接引用也不会被链接器优化掉。最终就是把所有命令结构体紧挨着排列在一起,形成一个连续的内存区域。
  3. 定义边界: 通常还会在这段的前后定义两个符号,比如 __u_boot_cmd_start__u_boot_cmd_end,分别指向这个连续内存区域的开始和结束地址, 方便 run_command()->find_cmd() 执行时查找。
  • 我们想添加一个新命令,只需要新建一个 cmd_xxx.c 文件,使用 U_BOOT_CMD 宏定义一下,完全不用去修改任何全局的命令列表,非常解耦。

四、命令执行流程

  • 从我们在终端敲下回车,到命令被执行,U-Boot内部都发生了什么。
  1. 用户输入: 用户在U-Boot命令行输入一条命令,比如 echo hello然后按下回车。
  2. 读取与解析: U-Boot的主循环 (main_loop in common/main.c) 会调用 run_command() 函数来处理这个输入字符串。run_command() 会对字符串进行解析,把它拆分成命令和参数,存放到一个 argv 数组里,就像C语言的 main 函数参数一样(argv[0]="echo", argv[1]="hello")。
  3. 查找命令: 接着,run_command() 会调用 find_cmd() 函数 (in common/command.c)。这个函数会从命令表的起始地址 (__u_boot_cmd_start) 开始,遍历到结束地址 (__u_boot_cmd_end),用用户输入的命令名(argv[0])去和表中每一个 cmd_tbl_t 结构体的 name 成员进行字符串比较。
  4. 找到匹配: 当 find_cmd() 找到了一个 name 成员与 "echo" 完全匹配的 cmd_tbl_t 结构体时,它会返回这个结构体的指针。
  5. 执行回调: run_command() 拿到这个指针后,就会通过这个指针去调用其 cmd 成员——也就是那个函数指针,指向的正是 do_echo() 函数。同时,把解析好的 argcargv 等参数传递给 do_echo()
  6. 完成执行: do_echo() 函数执行完毕,打印出 “hello”,然后返回。一次完整的命令执行就结束了。
作者

GoKo Mell

发布于

2024-10-01

更新于

2025-09-14

许可协议

评论

:D 一言句子获取中...