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_loop
incommon/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”,然后返回。一次完整的命令执行就结束了。