uboot 命令实现机制
- 如何添加 uboot 命令及 uboot 命令实现机制
一、命令是如何被“定义”的
要搞懂U-Boot的命令,第一步当然是看它是怎么被定义的。
在 common/ 目录下有很多 cmd_xxx.c 格式的文件,比如 cmd_echo.c、cmd_exit.c 等。随便点开一个,比如 cmd_exit.c,就会发现里面有一个关键的函数 do_exit() 和一个宏 U_BOOT_CMD。
1 | |
很明显,U_BOOT_CMD 这个宏就是定义命令的核心。这个宏定义在 include/command.h 中:
1 | |
这还只是第一层封装,U_BOOT_CMD_COMPLETE 才是最终的实现(这里贴出的是一个典型U-Boot版本中的定义,不同版本可能略有差异,但原理一致):
1 | |
ll_entry_declare 这个宏负责创建变量和设置段属性,而 U_BOOT_CMD_MKENT_COMPLETE 负责初始化结构体。最终展开后,其核心思想是:定义一个 cmd_tbl_t 类型的结构体变量,并用传入的参数去初始化它。
我们再用 exit 命令的例子来具体看一下:
1 | |
经过层层宏展开,它就等价于定义了一个全局变量,并将其放入了特定的段中,大致如下:
1 | |
这里面有几个关键点:
- 变量定义: 定义了一个
cmd_tbl_t类型的变量,变量名叫_u_boot_list_2_cmd_2_exit,并且是4字节对齐的。 - 段属性:
__attribute__((unused,section(".u_boot_list_2_cmd_2_exit")))是一个GCC的扩展。unused是为了防止编译器在某些情况下警告变量未被使用。section(...)则是最关键的部分,它告诉编译器,不要把这个变量放到普通的.data段,而是放到一个我们自定义的、名为.u_boot_list_2_cmd_2_exit的段里面。这个后面提到。 - 结构体初始化:
{...}这部分就是用我们传入的参数来初始化这个结构体变量的各个成员。
二、命令的数据结构 cmd_tbl_t
从上面的分析我们知道,U-Boot里的每一个命令,实际上都是一个 cmd_tbl_t 结构体实例。这个结构体定义在 include/command.h 中,它就像一个命令的“身份证”,记录了这个命令的所有信息。
1 | |
为了方便理解,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",maxargs 是 2,repeatable 是 1,cmd 函数指针指向 do_exit… 所有信息都保存在了这个结构体里。
三、命令是如何被管理的
我们知道了每个命令都是一个独立的 cmd_tbl_t 变量,而且它们都被放到了一个以 .u_boot_list_2_cmd_2_ 开头的自定义段里。那么,系统是怎么找到所有这些分散的命令的。
答案就在U-Boot的链接脚本 u-boot.lds 里。在这个文件里,有一段专门处理命令表的定义:
1 | |
这段脚本的作用是:
- 收集:
*(.u_boot_list*)是一个通配符,它会找到所有编译单元中段名以.u_boot_list开头的所有段。 - 排序和合并:
SORT(...)会对找到的段进行排序(Maybe是按段名),KEEP(...)使这些段没有被直接引用也不会被链接器优化掉。最终就是把所有命令结构体紧挨着排列在一起,形成一个连续的内存区域。 - 定义边界: 通常还会在这段的前后定义两个符号,比如
__u_boot_cmd_start和__u_boot_cmd_end,分别指向这个连续内存区域的开始和结束地址, 方便 run_command()->find_cmd() 执行时查找。
- 我们想添加一个新命令,只需要新建一个
cmd_xxx.c文件,使用U_BOOT_CMD宏定义一下,完全不用去修改任何全局的命令列表,非常解耦。
四、命令执行流程
- 从我们在终端敲下回车,到命令被执行,U-Boot内部都发生了什么。
- 用户输入: 用户在U-Boot命令行输入一条命令,比如
echo hello然后按下回车。 - 读取与解析: U-Boot的主循环 (
main_loopincommon/main.c) 会调用run_command()函数来处理这个输入字符串。run_command()会对字符串进行解析,把它拆分成命令和参数,存放到一个argv数组里,就像C语言的main函数参数一样(argv[0]="echo",argv[1]="hello")。 - 查找命令: 接着,
run_command()会调用find_cmd()函数 (incommon/command.c)。这个函数会从命令表的起始地址 (__u_boot_cmd_start) 开始,遍历到结束地址 (__u_boot_cmd_end),用用户输入的命令名(argv[0])去和表中每一个cmd_tbl_t结构体的name成员进行字符串比较。 - 找到匹配: 当
find_cmd()找到了一个name成员与"echo"完全匹配的cmd_tbl_t结构体时,它会返回这个结构体的指针。 - 执行回调:
run_command()拿到这个指针后,就会通过这个指针去调用其cmd成员——也就是那个函数指针,指向的正是do_echo()函数。同时,把解析好的argc和argv等参数传递给do_echo()。 - 完成执行:
do_echo()函数执行完毕,打印出 “hello”,然后返回。一次完整的命令执行就结束了。

