uboot源码分析——main_loop

本文详细分析了U-Boot源码中的main_loop函数,包括预编译技巧、预引导命令、启动次数限制、VFD与Modem支持等功能。通过对CONFIG系列宏的解释,揭示了U-Boot在启动过程中的关键步骤,如TFTP功能、命令自动完成功能的安装以及启动模式判断。此外,文章还探讨了默认启动和命令行模式下的不同启动方式,如hush工具和一般方式,并重点解析了run_command和parse_string_outer函数的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

接下来看/common/main.c中的main_loop ();

你会发现,这个函数相当长,但是其中有不少的编译条件,很多宏是没有定义的,也就是说,最后被编译的只是其中一部分。

在预编译的时候使用 #warning “你想要打印的字符串”,这种形式可以在编译的时候确定哪些宏是被定义的,哪些是没有定义的。

可以将没有编译的部分去掉,得到有效的代码就比较少了:

但是看代码的时候总是有种强迫症,恨不得把所有的相关代码都理解,被去掉的代码,虽然没有被编译,但是我们想了解他到底做了什么:

#ifndef CONFIG_SYS_HUSH_PARSER
static char lastcommand[CONFIG_SYS_CBSIZE] = { 0, };
int len;
int rc = 1;
int flag;
#endif

这个宏比较重要,最后再说。

 

#ifdef CONFIG_PREBOOT
char *p;
#endif
 
#ifdef CONFIG_AUTO_COMPLETE
install_auto_complete();
#endif
.
.
.
.
#ifdef CONFIG_PREBOOT
if ((p = getenv ("preboot")) != NULL) {
# ifdef CONFIG_AUTOBOOT_KEYED
int prev = disable_ctrlc(1);	/* disable Control C checking */
# endif
 
# ifndef CONFIG_SYS_HUSH_PARSER
run_command (p, 0);
# else
parse_string_outer(p, FLAG_PARSE_SEMICOLON |
    FLAG_EXIT_FROM_LOOP);
# endif
 
# ifdef CONFIG_AUTOBOOT_KEYED
disable_ctrlc(prev);	/* restore Control C checking */
# endif
}
#endif /* CONFIG_PREBOOT */

CONFIG_PREBOOT

“预引导命令:

        CONFIG_PREBOOT

        如果定义了该选项,则在进行引导延时的计时前或者运行自动引导命令前,检查环境变量"preboot"是否存在,如果存在则进入交互模式。

        该功能在"preboot"是由程序自动生成或修改的情况下比较有用。比如,LWMON单板的代码:当引导系统时,如果用户按下特定组合键,preboot会被修改。 ”

“http://linux.chinaunix.net/techdoc/desktop/2009/01/14/1058572.shtml”

 

#ifdef CONFIG_BOOTCOUNT_LIMIT
unsigned long bootcount = 0;
unsigned long bootlimit = 0;
char *bcs;
char bcs_set[16];
#endif /* CONFIG_BOOTCOUNT_LIMIT */
 
 
#ifdef CONFIG_BOOTCOUNT_LIMIT
bootcount = bootcount_load();
bootcount++;
bootcount_store (bootcount);
sprintf (bcs_set, "%lu", bootcount);
setenv ("bootcount", bcs_set);
bcs = getenv ("bootlimit");
bootlimit = bcs ? simple_strtoul (bcs, NULL, 10) : 0;
#endif /* CONFIG_BOOTCOUNT_LIMIT */

CONFIG_BOOTCOUNT_LIMIT

“启动次数限制功能,启动次数限制可以被用户设置一个启动次数,然后保存在Flash存储器的特定位置,当到达启动次数后,U-Boot无法启动。该功能适合一些商业产品,通过配置不同的License限制用户重新启动系统。

“http://www.cnblogs.com/amanlikethis/p/3555516.html”

 

#if defined(CONFIG_VFD) && defined(VFD_TEST_LOGO)
ulong bmp = 0;	 /* default bitmap */
extern int trab_vfd (ulong bitmap);
 
#ifdef CONFIG_MODEM_SUPPORT
if (do_mdm_init)
bmp = 1;	/* alternate bitmap */
#endif
trab_vfd (bmp);
#endif	/* CONFIG_VFD && VFD_TEST_LOGO */

CONFIG_VFD宏,应该是VFD屏幕方面的,我们没有用。

CONFIG_MODEM_SUPPORT宏,如果系统中有Modem,打开该功能可以接受其他用户通过电话网络的拨号请求。Modem功能通常供一些远程控制的系统使用。

 

#if defined(CONFIG_UPDATE_TFTP)
update_tftp ();
#endif /* CONFIG_UPDATE_TFTP */

这边是启动TFTP功能,但是这里有一个疑问:CONFIG_UPDATE_TFTP并没有参与编译,但是最终build出来的uImage,还是带有tftp client的功能的。

 

#ifdef CONFIG_VERSION_VARIABLE
{
    extern char version_string[];
    setenv ("ver", version_string);  /* set version variable */
}
#endif /* CONFIG_VERSION_VARIABLE */

初始化命令自动完成功能等。动态版本号功能支持代码,version_string变量是在其他文件定义的一个字符串变量,当用户改变U-Boot版本的时候会更新该变量。打开动态版本支持功能后,U-Boot在启动的时候会显示最新的版本号。

 

最后说一下CONFIG_SYS_HUSH_PARSER这个宏,在uboot刚启动的时候hit任意一个键,将进入命令行模式,此时执行循环代码,有两种方式:

第一种是下面代码的红字部分,采用“hush”方式的主循环。

什么是hush?可以理解成一种shell工具,用于命令的接收和解析。集成在busybox中。

 

第二种为一般的循环方式。

如果定义了CONFIG_SYS_HUSH_PARSER,那么将采用第一种循环方式,否则用第二种。

 

其实这里说的不准确,不仅是在命令行模式下,其实只要是涉及到执行命令、解析命令的语句,都是分Hush”一般”的两种情况,随后会讲到。

 

我参考的uboot代码中是采用hush的方式,相对一般的方法更为复杂,hush的代码比较复杂,研究了一下没看懂,没有相关的资料,hush简略提一下,着重分析一般的方法。

 

下面来具体分析main_loop

void main_loop (void)
{
#ifndef CONFIG_SYS_HUSH_PARSER
static char lastcommand[CONFIG_SYS_CBSIZE] = { 0, };
int len;
int rc = 1;
int flag;
#endif
 
#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)    //defined
char *s;
int bootdelay;
#endif 
 
#ifdef CONFIG_SYS_HUSH_PARSER                  //defined                        
u_boot_hush_start ();
#endif
 
#ifdef CONFIG_AUTO_COMPLETE                           //defined
install_auto_complete();
#endif
 
#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)   //defined
s = getenv ("bootdelay");
bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;
 
debug ("### main_loop entered: bootdelay=%d\n\n", bootdelay);
 
s = getenv ("bootcmd");
 
debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");
 
if (bootdelay >= 0 && s && !abortboot (bootdelay)) {
# ifndef CONFIG_SYS_HUSH_PARSER
run_command (s, 0);
# else
parse_string_outer(s, FLAG_PARSE_SEMICOLON |
    FLAG_EXIT_FROM_LOOP);
# endif
 
}
#endif	/* CONFIG_BOOTDELAY */
 
/*
 * Main Loop for Monitor Command Processing
 */
#ifdef CONFIG_SYS_HUSH_PARSER
parse_file_outer();
/* This point is never reached */
for (;;);
#else
for (;;) {
len = readline (CONFIG_SYS_PROMPT);
 
flag = 0;	/* assume no special flags for now */
if (len > 0)
strcpy (lastcommand, console_buffer);
else if (len == 0)
flag |= CMD_FLAG_REPEAT;
if (len == -1)
    puts ("<INTERRUPT>\n");
else
    rc = run_command (lastcommand, flag);
 
if (rc <= 0) {
    /* invalid command or not repeatable, forget it */
    lastcommand[0] = 0;
}
}
#endif /*CONFIG_SYS_HUSH_PARSER*/
}
/common/main.c

#ifdef CONFIG_SYS_HUSH_PARSER
u_boot_hush_start ();
#endif
/common/hush.c
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;
#ifndef CONFIG_RELOC_FIXUP_WORKS
u_boot_hush_reloc();
#endif
}
return 0;
}

这里是一些简单的赋值操作。

看一下top_vars这个变量:

struct variables shell_ver = { "HUSH_VERSION", "0.01", 1, 1, 0 };
struct variables *top_vars = &shell_ver;

看一下struct variables结构体

struct variables {
char *name;
char *value;
int flg_export;
int flg_read_only;
struct variables *next;
};

 

接下来安装命令自动补全函数

#ifdef CONFIG_AUTO_COMPLETE
install_auto_complete();
#endif
/common/command.c
void install_auto_complete(void)
{
install_auto_complete_handler("printenv", var_complete);
install_auto_complete_handler("setenv", var_complete);
#if defined(CONFIG_CMD_RUN)
install_auto_complete_handler("run", var_complete);
#endif
}
 

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;
}

来看一下如何安装自动补全函数。

首先在install_auto_complete函数中调用了三次install_auto_complete_handler,实参分别为"printenv""setenv""run"var_complete

install_auto_complete_handler函数中,首先调用find_cmd函数找到该字符串对应的命令结构体,命令结构体的定义如下:

/include/command.h

/*
 * Monitor Command Table
 */
 
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 *[]);
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 *argv[], char last_char, int maxv, char *cmdv[]);
#endif
};
 
typedef struct cmd_tbl_s	cmd_tbl_t;

让后将结构体中的complete函数指针设置为complete,即var_complete

所以install_auto_complete_handler做了两件事

寻找命令结构体

对结构体的函数指针赋值。

 

来看一下如何寻找结构体,即函数find_cmd

/common/command.c

cmd_tbl_t *find_cmd (const char *cmd)
{
int len = &__u_boot_cmd_end - &__u_boot_cmd_start;
return find_cmd_tbl(cmd, &__u_boot_cmd_start, len);
}


/***************************************************************************
 * find command table entry for a command
 */
cmd_tbl_t *find_cmd_tbl (const char *cmd, cmd_tbl_t *table, int table_len)
{
cmd_tbl_t *cmdtp;
cmd_tbl_t *cmdtp_temp = table;	/*Init value */
const char *p;
int len;
int n_found = 0;
 
/*
 * Some commands allow length modifiers (like "cp.b");
 * compare command name only until first dot.
 */
len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);
 
for (cmdtp = table;
     cmdtp != table + table_len;
     cmdtp++) {
if (strncmp (cmd, cmdtp->name, len) == 0) {
if (len == strlen (cmdtp->name))
return cmdtp;	/* full match */
 
cmdtp_temp = cmdtp;	/* abbreviated command ? */
n_found++;
}
}
if (n_found == 1) {	 /* exactly one match */
return cmdtp_temp;
}
 
return NULL;	/* not found or ambiguous command */
}

find_cmd函数的返回值是一个cmd_tbl_t *指针。

定义了一个int len变量,它的值为 &__u_boot_cmd_end - &__u_boot_cmd_start;

/u-boot.lds文件中有

. = .;
 __u_boot_cmd_start = .;
 .u_boot_cmd : { *(.u_boot_cmd) }
 __u_boot_cmd_end = .;

所以len是整个section的长度,我们查找的字符串所对应的命令结构体,就是在这个section中。

然后运行find_cmd_tbl函数,它的实参cmd对应字符串"printenv""setenv""run"&__u_boot_cmd_start表示section的起始地址,lensection的长度。

find_cmd_tbl 中,有一段注释:

/*
 * Some commands allow length modifiers (like "cp.b");
 * compare command name only until first dot.
 */
len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);

有些命令允许使用长度修饰语比如cp.b,比较时只比较点之前的部分。

那么这样就好理解了,首先判断一个表示命令的字符串中有没有点,用strchr函数返回cmd字符串中第一次出现字符’.’的地址,如果没有则返回NULL

/**
 * strchr - Find the first occurrence of a character in a string
 * @s: The string to be searched
 * @c: The character to search for
 */
char * strchr(const char * s, int c)
{
for(; *s != (char) c; ++s)
if (*s == '\0')
return NULL;
return (char *) s;
}

这个函数很简单。回到find_cmd_tbl函数。

接下来就是遍历&__u_boot_cmd_start为起始地址,table_len为长度的,u_boot_cmd section

将字符串与命令结构体指针的cmdtp->name进行比较,并且比较的是前len个字符,如果前len个字符相同,那么可能是完全匹配(full match,立即返回)或者字符串是简短的命令,这个可以根据cmdtp->name的长度和len的比较来确定。

如果是简短的命令,那么存在两种可能,1.这个section中只有一个命令结构体的名字与字符串匹配2.有多个

如何区分呢?通过一个整型变量n_found,每次strncmp成功时n_found就会加1,如果有多个匹配,那么最后n_found必然不为1,返回NULL,否则只有一个匹配,返回指向该结构体的指针。

 

现在已经把var_complete函数“安装”到结构体变量的函数指针处,var_complete用于实现命令补充:

/common/command.c

int var_complete(int argc, char *argv[], char last_char, int maxv, char *cmdv[])
{
static char tmp_buf[512];
int space;
 
space = last_char == '\0' || last_char == ' ' || last_char == '\t';
 
if (space && argc == 1)
return env_complete("", maxv, cmdv, sizeof(tmp_buf), tmp_buf);
 
if (!space && argc == 2)
return env_complete(argv[1], maxv, cmdv, sizeof(tmp_buf), tmp_buf);
 
return 0;
}

这个函数之后讲解到命令的时候再说~~

 

然后执行

s = getenv ("bootdelay");
bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;

首先得到环境变量bootdelay的地址,如果为空则使用默认的CONFIG_BOOTDELAY,在mini2440.h中被定义为1,否则使用环境变量的值一般是3

 

 

然后获取环境变量bootcmd的地址

s = getenv ("bootcmd");

bootcmd的内容及作用随后会讲到。

 

继续向下看

if (bootdelay >= 0 && s && !abortboot (bootdelay)) {
# ifdef CONFIG_AUTOBOOT_KEYED
int prev = disable_ctrlc(1);	/* disable Control C checking */
# endif
 
# ifndef CONFIG_SYS_HUSH_PARSER
run_command (s, 0);
# else
parse_string_outer(s, FLAG_PARSE_SEMICOLON |
    FLAG_EXIT_FROM_LOOP);
# endif
 
# ifdef CONFIG_AUTOBOOT_KEYED
disable_ctrlc(prev);	/* restore Control C checking */
# endif
}

我们在实际操作的时候,uboot启动初期,如果在短时间内hit任意的按键,那么就会进入uboot的命令行模式,否则就会默认启动linux,这里要做的就是这样一个判断。

首先判断bootdelay 是否非负,这个是预先设置好的,一般是1.

然后看 abortboot,顾名思义,是打断启动

/common/main.c

static __inline__ int abortboot(int bootdelay)
{
int abort = 0;
 
#ifdef CONFIG_MENUPROMPT
printf(CONFIG_MENUPROMPT);
#else
printf("Hit any key to stop autoboot: %2d ", bootdelay);
#endif
 
#if defined CONFIG_ZERO_BOOTDELAY_CHECK
/*
 * Check if key already pressed
 * Don't check if bootdelay < 0
 */
if (bootdelay >= 0) {
if (tstc()) {	/* we got a key press	*/
(void) getc();  /* consume input	*/
puts ("\b\b\b 0");
abort = 1;	/* don't auto boot	*/
}
}
#endif
 
while ((bootdelay > 0) && (!abort)) {
int i;
 
--bootdelay;
/* delay 100 * 10ms */
for (i=0; !abort && i<100; ++i) {
if (tstc()) {	/* we got a key press	*/
abort  = 1;	/* don't auto boot	*/
bootdelay = 0;	/* no more delay	*/
# ifdef CONFIG_MENUKEY
menukey = getc();
# else
(void) getc();  /* consume input	*/
# endif
break;
}
udelay(10000);
}
 
printf("\b\b\b%2d ", bootdelay);
}
 
putc('\n');
 
#ifdef CONFIG_SILENT_CONSOLE
if (abort)
gd->flags &= ~GD_FLG_SILENT;
#endif
 
return abort;
}

首先定义一个int变量abort.

CONFIG_MENUPROMPT没有定义

在串口上打印"Hit any key to stop autoboot:  1"      (假设bootdelay被定义成1

CONFIG_ZERO_BOOTDELAY_CHECK没有定义

然后进入while循环,每一秒bootdelay1并打印到屏幕上,如果在这期间hit任意按键,那么tstc()函数返回1,进入if语句,把abort1bootdelay0

CONFIG_MENUKEY没有定义,(void) getc(); 只是为了消耗输入的字符,没有什么实际意义,跳出for循环,打印bootdelay的值,此时应该为0

所以如果默认启动被打断的话,你最后见到的一定是Hit any key to stop autoboot: 0,然后跳出while循环。

CONFIG_SILENT_CONSOLE没有没定义。

最后说一下tstc()函数,追到最后是这样的形式:

/drivers/serial/serial_s3c24x0.c
int _serial_tstc(const int dev_index)
{
struct s3c24x0_uart *uart = s3c24x0_get_base_uart(dev_index);
 
return readl(&uart->UTRSTAT) & 0x1;
}

可以看到,这里是判断UTRSTAT寄存器的第0位是否为1,如果为1的话,表示有数据传入,即有按键被hit,默认启动被打断,反之,则不会被打断。

所以分析代码的时候面临两种情况:默认启动、被打断进入命令行模式。

我们先看默认启动,即(bootdelay >= 0 && s && !abortboot (bootdelay))为真。

这里又出现了两种情况(=简直要疯了),还记得之前说过进行命令解析、命令执行的时候分hush和“一般”两种情况不?

所以这里出现了

# ifndef CONFIG_SYS_HUSH_PARSER
run_command (s, 0);
# else
parse_string_outer(s, FLAG_PARSE_SEMICOLON |
    FLAG_EXIT_FROM_LOOP);
# endif

如果用hush工具,那么执行parse_string_outer函数,否则,执行run_command,这两个函数都是用来启动Linux

 

下面来看run_command函数:

首先定义了一堆局部变量。

检验命令字符串cmd是否合法,检验字符串长度是不是太长。

然后将命令字符串拷贝到cmdbufcmdbuf是一个指针变量,用来存放命令字符串,这样做比较安全。

接下来就是一个大的while循环,进入循环之前先要有一个概念。

cmdbuf存储的是一串字符串,代表我们想要执行的命令,启动时传进来的就是环境变量bootcmd对应的值(他的值是以字符串形式存储的。)

字符串对应命令,一次性可能输入多个命令,命令和命令之间以’;’来区分。

比如:bootcmd=nfs 0x30008000 192.168.0.1:/home/tekkaman/working/nfs/zImage.img;bootm,这里就对应两个命令,以分号区分。

函数的目的是要执这两个命令,uboot的机制是,将两个命令分开执行,先解析出一个命令,执行命令1,然后解析出命令2,执行命令2.

这样看代码的思路就比较清晰了,

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;
}

这段代码是从命令行中解析出一个命令,即每次for循环break后,都会有一个命令被解析出来,此时sep指向命令末尾。所以这里执行bootcmd应该是要走两次while循环。

但是具体代码我看不是很明白,还要在研究一下......

然后把被解析出来的命令的首地址赋给token

然后将str指向下一条命令,如果没有下一条命令,就将str指向命令尾部。

然后执行process_macros,看注释就知道是将命令中包含的宏特换为真正的值。

然后执行parse_line,返回的是参数的个数给变量argc。这个函数挺简单的,不用说了。

然后执行find_cmd(argv[0]),这里以argv[0]作为命令,即以第一个参数作为命令,是OK的。返回.u_boot_cmd段中bootcmd命令对应的结构体地址,并赋值给cmdtp

补充一下,关于find_cmd函数,之前有研究过,它的作用是在.u_boot_cmd段中寻找命令字符串所对应的结构体,并返回地址。

这就存在一些疑问:

首先,这些命令结构体是哪来来的?

其次,是通过什么手段,将所有的命令结构体都放入.u_boot_cmd段中?

第一个问题:U_BOOT_CMD这个宏用来定义命令结构体

/include/command.h

#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}

在很多cmd_xxx.c文件中定义结构体,

定义的方式用U_BOOT_CMD宏,比如version命令

U_BOOT_CMD(
version,	1,	 1,	do_version,
"print monitor version",
""
);

那么这里的name就是versionmaxargs最大参数个数就是1repeatable的值是1,执行命令时调用的函数就是do_version,usage"print monitor version"help为空,自动补全函数为空。

第二个问题:

/include/command.h

#define Struct_Section  __attribute__ ((unused,section (".u_boot_cmd")))

告诉编译器将此部分语句划作.u_boot_cmd段。

所以上面的version定义部分展开后就得到

cmd_tbl_t __u_boot_cmd_version __attribute__ = {version, 1, 1, do_version, "print monitor version", “”}

解决了这个疑惑,让我们回到run_command函数继续向下执行。

然后检验,输入的命令行的参数个数是否大于命令所规定的最大参数个数。

检验cmdtp->cmd的值是否是do_bootd,然后通过flagCMD_FLAG_BOOTD的值来判断是否发生do_bootd递归,不太明白。

下面就是最关键的部分,真正运行命令

/* OK - call function to do the command */
if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) {
rc = -1;
}

这个很容易理解。

那么现在回顾一下run_command做了什么工作?

拷贝命令字符串到一个临时的内存;

分割字符串,取出命令;

替换其中的宏;

得到参数个数;

查找命令对应的结构体;

跳转到结构体中的函数指针,即执行函数。

 

分析完run_command再回到main_loop

# ifndef CONFIG_SYS_HUSH_PARSER
run_command (s, 0);
# else
parse_string_outer(s, FLAG_PARSE_SEMICOLON |
    FLAG_EXIT_FROM_LOOP);
# endif

前面说了,启动方式有两种,一种是一般的启动方式,调用run_command,另一种就是hush的方式,调用parse_string_outer函数,下面来看hush的方式。

/commom/hush.c

#ifndef __U_BOOT__
static int parse_string_outer(const char *s, int flag)
#else
int parse_string_outer(char *s, int flag)
#endif	/* __U_BOOT__ */
{
struct in_str input;
#ifdef __U_BOOT__
char *p = NULL;
int rcode;
if ( !s || !*s)
return 1;
if (!(p = strchr(s, '\n')) || *++p) {
p = xmalloc(strlen(s) + 2);
strcpy(p, s);
strcat(p, "\n");
setup_string_in_str(&input, p);
rcode = parse_stream_outer(&input, flag);
free(p);
return rcode;
} else {
#endif
setup_string_in_str(&input, s);
return parse_stream_outer(&input, flag);
#ifdef __U_BOOT__
}
#endif
}

__U_BOOT__没有被定义,所以真正执行的只有两句:

setup_string_in_str(&input, s);
return parse_stream_outer(&input, flag);

先看setup_string_in_str

/common/hush.c

static void setup_string_in_str(struct in_str *i, const char *s)
{
i->peek = static_peek;
i->get = static_get;
i->__promptme=1;
i->promptmode=1;
i->p = s;
}

setup_string_in_str主要对结构体变量input进行一些赋值,将命令行字符串s赋给input->p,已经其他一些变量的赋值。

看一下struct in_str的定义

/common/hush.c

struct in_str {
const char *p;
#ifndef __U_BOOT__
char peek_buf[2];
#endif
int __promptme;
int promptmode;
#ifndef __U_BOOT__
FILE *file;
#endif
int (*get) (struct in_str *);
int (*peek) (struct in_str *);
};

看一下static_get函数和static_peek函数

/common/hush.c

static int static_get(struct in_str *i)
{
int ch = *i->p++;
if (ch=='\0') return EOF;
return ch;
}
 
static int static_peek(struct in_str *i)
{
return *i->p;
}

我自己做了实验,发现:

static_peek返回*i->pi->p指向字符串的首地址,那么*i->p应该是第一个字符。

static_getch不等于0的情况下也是指向第一个字符。

那么这两个函数的作用是什么?

 

然后看parse_stream_outer函数,去掉无用的宏定义

/common/hush.c

/* most recursion does not come through here, the exeception is
 * from builtin_source() */
int parse_stream_outer(struct in_str *inp, int flag)
{
 
struct p_context ctx;
o_string temp=NULL_O_STRING;
int rcode;
do {
ctx.type = flag;
initialize_context(&ctx);
update_ifs_map();
if (!(flag & FLAG_PARSE_SEMICOLON) || (flag & FLAG_REPARSING)) mapset((uchar *)";$&|", 0);
inp->promptmode=1;
<span style="color:#ff6666;">rcode = parse_stream(&temp, &ctx, inp, '\n');</span>
if (rcode != 1 && ctx.old_flag != 0) {
syntax();
}
if (rcode != 1 && ctx.old_flag == 0) {
done_word(&temp, &ctx);
done_pipe(&ctx,PIPE_SEQ);
#ifndef __U_BOOT__
<span style="color:#ff6666;">run_list(ctx.list_head);</span>
#endif
} else {
if (ctx.old_flag != 0) {
free(ctx.stack);
b_reset(&temp);
}
temp.nonnull = 0;
temp.quote = 0;
inp->p = NULL;
free_pipe_list(ctx.list_head,0);
}
b_free(&temp);
} while (rcode != -1 && !(flag & FLAG_EXIT_FROM_LOOP));   /* loop on syntax errors, return on EOF */
#ifndef __U_BOOT__
return 0;
#endif /* __U_BOOT__ */
}

Hush的代码过于复杂,水平有限,很多代码看不懂,标红色的部分parse_stream对命令行进行解析,run_list执行代码。

 

以上就是uboot的默认启动,之后会运行启动linux的一些函数,以后会讲到,先把main_loop看完。

如果在启动初期按下键盘上的任意键,那么就会进入命令行模式。

/*
 * Main Loop for Monitor Command Processing
 */
#ifdef CONFIG_SYS_HUSH_PARSER
parse_file_outer();
/* This point is never reached */
for (;;);
#else
for (;;) {
len = readline (CONFIG_SYS_PROMPT);
 
flag = 0;	/* assume no special flags for now */
if (len > 0)
strcpy (lastcommand, console_buffer);
else if (len == 0)
flag |= CMD_FLAG_REPEAT;
if (len == -1)
    puts ("<INTERRUPT>\n");
else
    rc = run_command (lastcommand, flag);
 
if (rc <= 0) {
    /* invalid command or not repeatable, forget it */
    lastcommand[0] = 0;
}
}
#endif /*CONFIG_SYS_HUSH_PARSER*/

首先判断有没有定义CONFIG_SYS_HUSH_PARSER,有就以hush模式来解析执行命令,否则用一般的方式。

hush的方法中 parse_file_outer最终也是调用parse_stream_outer

hush模式下,首先运行readline ,用于读取命令行:

CONFIG_SYS_PROMPT被定义为”[u-boot@MINI2440]#”字符串,提示输入命令。

/include/configs/mini2440.h

#define	CONFIG_SYS_PROMPT	 "[u-boot@MINI2440]# "	/* Monitor Command Prompt	*/
#define	CONFIG_SYS_CBSIZE	 256	 /* Console I/O Buffer Size	*/

/common/main.c

/*
 * Prompt for input and read a line.
 * If  CONFIG_BOOT_RETRY_TIME is defined and retry_time >= 0,
 * time out when time goes past endtime (timebase time in ticks).
 * Return:	number of read characters
 *	 -1 if break
 *	 -2 if timed out
 */
int readline (const char *const prompt)
{
/*
 * If console_buffer isn't 0-length the user will be prompted to modify
 * it instead of entering it from scratch as desired.
 */
console_buffer[0] = '\0';
 
return readline_into_buffer(prompt, console_buffer);
}

同时还定义了,用来存放console接收的命令字符串。

char        console_buffer[CONFIG_SYS_CBSIZE];	 /* console I/O buffer	*/

然后调用readline_into_buffer函数

/common/main.c

int readline_into_buffer (const char *const prompt, char * buffer)
{
.....
}

CONFIG_CMDLINE_EDITING如果被定义的话,使用键盘上的上下方向键就能调出以前执行过的命令,这里没有定义,先不看。

接着定义了一些变量。

要理清一个关系:p_buf指向的是console_buffer的起始位置,console_buffer用来存放输入的命令字符串,p刚开始也是指向console_buffer的起始位置,但随着命令的输入,p会不断增加,指向console_buffer内部的某一个位置,对*p的操作导致console_buffe的内容发生改变。

每次提示输入命令之前先打印[u-boot@MINI2440]#

Col用于标记光标的位置,初始的时候是在[u-boot@MINI2440]#之后。

CONFIG_BOOT_RETRY_TIMECONFIG_SHOW_ACTIVITY都没有定义。

然后从串口接收输入的字符,getc的实现方法相信写过串口逻辑编程的应该很熟悉了。

接下来是对得到的字符c进行判断:

在得到’\r’或者’\n’的情况下,uboot认为命令已经输入完成了,将*p设置为’\0’,即在console_buffer中命令是以’\0’结尾的,然后返回命令字符串的长度,即p - p_buf

如果没有得到任何字符(用‘\0’表示),则继续进行下一次的for循环。

如果得到的字符是0x03,则说明输入了ctr+c,表示用户想取消命令,此时清空console_buffer,然后返回-1.

如果得到的字符是0x15,则说明输入了ctr+u,表示用户想删除一整行,uboot将不停的向串口发送”\b\b”,同时光标的位置向行首移动(也就是--col),直到光标移到[u-boot@MINI2440]#的末尾,重新将p指向console_buffer的起始位置,然后继续下一次for循环。

如果得到的字符是0x17,则说明输入了ctr+w,表示用户想删除一个词,调用delete_char函数,这个函数还是蛮好理解的。

如果得到的字符是0x08或者0x7F,则说明输入了ctr+H或者del,表示用户想删除一个字符,调用delete_char函数。

除了以上几种情况,剩下的就是默认情况——命令字符串中的普通字符(或者是’\t’).

首先判断console_buffer有没有满,没有满的话就直接输出到串口,满的话就打印字符’\a’,表示响铃.

console_buffer没有满的情况下,输入的字符全都要执行*p++ = c; ++n;

在通过*p改变console_buffer之前,先判断该字符是不是tab

如果不是,”++n;putc (c);”,打印到串口上;如果是,那么就将console_buffer*p对应的字符设为’\0’(本来应该是输入的’\t’),然后调用cmd_auto_complete函数,进行命令补全,补全完以后p = p_buf + n,这就改变了p的指向,nbuffer index.

下面来看一下cmd_auto_complete究竟做了什么:

首先strcpy(tmp_buf, buf);,为了安全你懂得。

argc = make_argv(tmp_buf, sizeof(argv)/sizeof(argv[0]), argv);

make_argv/common/command.c中,static int make_argv(char *s, int argvsz, char *argv[])s指向tmp_bufargvsz20argv是定义的char *argv[CONFIG_SYS_MAXARGS + 1];

假设我们输入的命令行是“123h kiuahdi   ahf”,123hkiuahdi之间有一个空格,kiuahdiahf之间有两个空格和一个tabmake_argv如何处理。

Uboot中控制匹配命令的个数不能超过18,所以先要满足while (argc < argvsz - 1),在这个循环里面先判断:

/* skip any white space */

while ((*s == ' ') || (*s == '\t'))

++s;

if (*s == '\0') /* end of s, no more args */

break;

这两句的作用是去除命令或者参数前面的空格和tab,然后判断是否到达整个命令行的末尾也就是例子中字符f的后一位,如果到末尾,直接break,循环结束。

然后执行argv[argc++] = s; /* begin of argument string */,开始时s指向字符串的手字符sargv[0] = sargv是一个指针数组,它的第0个元素指向了字符1;

然后将通过++ss移动到“123h”字符串的结尾,下段代码的作用就是这个。

while (*s && (*s != ' ') && (*s != '\t'))

++s;

跳出这个循环的条件是什么?是*s的值是空格或者’\t’,跳出循环说明s指向的是h后面的那一个空格。

然后*s++ = '\0';h后面的那一个空格设置成’\0’,s指向字符k

此时argv[0]指向了字符1,而字符h后面的空格被改为’\0’,那么123h被分割(也就是注释中的separate)成一个字符串,首地址保存在argv[0]中。此时argc的值是1.

然后看第二次循环此时s指向k,和之前一样,kiuahdi后的第一个空格被改写成’\0’,此时s指向kiuahdi的第二个空格,而argv[1]指向字符串”kiuahdi”,此时argc的值是2.

看第三次循环,s指向kiuahdi的第二个空格,空格后面是一个tab,经过判断后,空格和tab会全部被跳过,然后s指向字符a,然后argv[2]指向字符a,此时argc的值是3

到达整个字符串末尾,判断if (*s == '\0'),然后break,跳出while循环,argc[3] = NULL。这就解释了为什么大的循环的条件是argc < argvsz - 1argvsz20,所以最大的有效参数下标只能是argc[18]argc[19] = NULL.

最后返回argc,即参数的个数,例子中是3.

所以make_argv的作用就是把命令行的命令和各个参数分隔到char *argv[CONFIG_SYS_MAXARGS + 1];指针数组中,并且返回参数的个数(包括命令).

回到cmd_auto_complete,接下来执行complete_cmdv

它的参数:

argc:参数个数

argv:参数的字符串数组

last_char:最后一个字符

sizeof(cmdv)/sizeof(cmdv[0])20,表示最多匹配的命令的个数

cmdvchar指针,存放匹配的命令

返回值:

匹配的命令的个数

为保持思路的连贯性,先完成cmd_auto_complete再研究complete_cmdv

我们现在已经找到了匹配的命令并保存在cmdv二级指针指向的位置,并且将匹配的命令的个数保存在i中。

接着对i的值进行判断:

i=0,没有匹配的命令,bell,返回。

i=1,这是最好的情况,只有一个命令匹配:

实际输入过程中,我们什么情况下按下tab键?肯定是输入命令的过程中,这时候命令的一部分保存在console_buffer中,然后tab键被替换为’\0’,这个前面已经讲过了。

那么这时候输入的“残缺的命令”在哪里?是argv[argc - 1],我刚看代码的时候没有意识到这一点,错误的以为是argv[0]

那么k就是这些“残缺的命令”的长度了。

回过头来看cmdv,前面说这个二级char指针用来存放匹配的命令,其实不够准确,它存放的是cmdtp->name,也就是匹配的结构体的name成员,这个后面讲complete_cmdv的时候会说。

所以s = cmdv[0] + k;这里s指向的就是需要补充的第一个字符的位置!len就是需要补充的字符的长度,补充完字符以后加一个空格,方便下次输入。

3.i>1,有多个命令匹配:

还有一个判断条件j = find_common_prefix(cmdv)) != 0

看一下这个函数:/commom/command.c

首先说一下公有前缀的问题,我在网上没有查到,但是我觉得公有前缀的并不是”abc”、”abcd”公有前缀是”abc”,这种事错误的。

举个例子:nand writenand read是有公有前缀的,为nand

nmnbootnandnfs是没有公有前缀的。

不知道这种理解是否正确。

static int find_common_prefix(char *argv[])
{
int i, len;
char *anchor, *s, *t;
 
if (*argv == NULL)
return 0;
 
/* begin with max */
anchor = *argv++;
len = strlen(anchor);
while ((t = *argv++) != NULL) {
s = anchor;
for (i = 0; i < len; i++, t++, s++) {
if (*t != *s)
break;
}
len = s - anchor;
}
return len;
}

这里的char *argv[]cmdvanchor永远都指向第一个第一个匹配的命令的名字,然后剩下的所有的匹配的命令的名字与之相比较,得出相同的部分。

我一开始觉得代码有问题,以为最后返回的len只是最后一个匹配的命令与第一个的共有部分的长度。

实际上因为for (i = 0; i < len; i++, t++, s++),所以len的值只可能越来越小。代码果然很精妙~~

现在我们知道find_common_prefix返回了所有匹配的命令的公有前缀的长度。接下来同样是计算“残缺的命令”的长度,k

比较kj的大小,显然如果k大于等于j的话,是没有意义的。

然后s = cmdv[0] + k;len = j;s还是指向需要补充的第一个字符,len则为共有前缀的长度。

 

前面列出了命令匹配的所有可能情况,并做了一些布置,接下来就要把缺失的字符补充好。

先判断if (s != NULL)s != NULL有两种可能:

1.i == 1i > 1且有公有前缀。

K是要补充的字符的长度,nconsole_bufferindexn+k判断有没有溢出。

我们输入的“残缺的命令”之后的tab被变为’\0’,buf + cnt就指向’\0’,即t指向紧接着“残缺的命令”之后的那个字符。

然后用一个for循环把缺少的字符从cmdv[0] + k处复制到console_buffer中。

接下来就是输出puts(t - k);以及一些重定位npcolp的重定位。

i>1且没有公有前缀,那么调用print_argv把所有匹配的命令都打印出来。print_argv就不去研究了。

cmd_auto_complete 运行结束后返回readline_into_buffer函数,我们把剩下的代码看完。

if (n < CONFIG_SYS_CBSIZE-2) {
if (c == '\t') {	/* expand TABs	 */
#ifdef CONFIG_AUTO_COMPLETE
/* if auto completion triggered just continue */
*p = '\0';
if (cmd_auto_complete(prompt, console_buffer, &n, &col)) {
p = p_buf + n;	/* reset */
continue;
}
#endif
puts (tab_seq+(col&07));
col += 8 - (col&07);
} else {
++col;	 /* echo input	 */
putc (c);
}
*p++ = c;
++n;
} else {	 /* Buffer full	 */
putc ('\a');
}

如果cmd_auto_complete成功就返回1,重定位P,终止本次循环,进入下次循环,继续等待输入。

如果匹配失败运行

puts (tab_seq+(col&07));

col += 8 - (col&07);

关于这两行代码,引用网上一位大神的解释:http://blog.youkuaiyun.com/vermilliontear/article/details/8350641

如果用户输入了多个命令且未找到匹配命令,那么这里cmd_auto_complete返回0,此时会认定为输出这个\t。输出\t则会将光标移动到下一个制表符的位置,而制表符之间的宽度一般被认定为8个空格。这里有很有意思的实现,col&07得到了当前光标位置与8的余数,那么也就是得到了当前光标距前一个制表符多远。tab_seq是一个存有8个空格的字符数组,所以tab_seq+(col&07)正好得到光标移到下一个制表符需要输出几个空格。之后更新当前光标位置,将\t存入缓冲区,更新n的值

 

到这里readline_into_buffer也分析完了,接下来看之前没有研究的complete_cmdv

它的参数:

argc:参数个数

argv:参数的字符串数组

last_char:最后一个字符

sizeof(cmdv)/sizeof(cmdv[0])20,表示最多匹配的命令的个数

cmdvchar指针,存放匹配的命令

返回值:

匹配的命令的个数

继续引用大神的解释:

根据用户按下TAB键时已输入的命令个数以及在TAB前输入的最后一个字符,分为3种情况进行匹配命令的搜索。

函数正常情况下返回匹配命令的个数。

--------------------------------------------------------------------------------------------------------------------------------------

cmd_tbl_t*cmdtp;

       在函数开始便定义了一个cmd_tbl_t指针,它定义在include/command.h62行,实际上是struct cmd_tbl_s的重命名。不记得这个结构体?到文章的开始处看看。

if(argc == 0) {
       /* output full list of commands */
       for (cmdtp = &__u_boot_cmd_start;cmdtp != &__u_boot_cmd_end; cmdtp++) {
              if (n_found >= maxv - 2) {
                     cmdv[n_found++] ="...";
                     break;
              }
              cmdv[n_found++] = cmdtp->name;
       }
       cmdv[n_found] = NULL;
       retur n n_found;
}

第一种情况,未输入任何命令时直接按下TAB键,此时应当列出所有u-boot命令(此时可理解为所有命令均匹配)。这个__u_boot_cmd_start__u_boot_cmd_end是定义在各平台的u-boot.lds文件中,用于标明u-boot命令域,它们分别代表命令域的起始和结束,在它们之间依次存放着每个u-boot命令所对应的cmd_tbl_s结构体。所以这个for循环的作用就是遍历u-boot命令域中每个命令的cmd_tbl_s结构体,并将每个命令名字的首地址存入cmdv指针数组中,该数组最后一个元素为NULL。在for循环中还可以看到,如果命令过多(多余20 – 2个),则其他命令的名字均以“...”代替。最后返回匹配命令的个数。

 

/*more than one arg or one but the start of the next */
if(argc > 1 || (last_char == '\0' || isblank(last_char))) {
       cmdtp = find_cmd(argv[0]);
       if (cmdtp == NULL || cmdtp->complete== NULL) {
              cmdv[0] = NULL;
              return 0;
       }
       return (*cmdtp->complete)(argc, argv,last_char, maxv, cmdv);
}

   第二种情况,输入了多个命令,或者正在准备输入第二个命令(last_char\0、空格或者\t时)时按下TAB键,此时应当根据第一个命令补全其余命令,即执行第一个命令(因为它是主命令)的complete函数。如果执行了complete函数,则函数返回匹配命令个数,同时将匹配命令名字(此时是针对于主命令的匹配命令)的首地址存入cmdv指针数组中。

 

cmd= argv[0];
/*
 * Some commands allow length modifiers (like"cp.b");
 * compare command name only until first dot.
 */
p= strchr(cmd, '.');
if(p == NULL)
       len = strlen(cmd);
else
       len = p - cmd;
 
/*return the partial matches */
for(cmdtp = &__u_boot_cmd_start; cmdtp != &__u_boot_cmd_end; cmdtp++) {
 
       clen = strlen(cmdtp->name);
       if (clen < len)
              continue;
 
       if (memcmp(cmd, cmdtp->name, len) !=0)
              continue;
 
       /* too many! */
       if (n_found >= maxv - 2) {
              cmdv[n_found++] = "...";
              break;
       }
 
       cmdv[n_found++] = cmdtp->name;
}
 
cmdv[n_found] = NULL;
return n_found;

       第三种情况,当正在输入第一个命令时(last_char不是\0、空格或者\t)按下TAB键,此时应当补全该命令。此种情况与find_cmd函数的工作方式非常类似,只不过此处当命令域中命令名长度(clen)比用户输入的命令长度(len)短时,不再进行命令匹配比较,此处是个优化。最后结果依旧是将匹配命令名字首地址放入cmdv指针数组中,并且返回匹配命令个数。

回到main_loop函数:

strcpy (lastcommand, console_buffer);

console_buffer中输入的命令(和已经补充的字符)拷贝到lastcommand中,然后运行

run_command (lastcommand, flag);函数,这个函数之前分析过了。

 

到这里为止,uboot的整个运行流程就算结束了。

 

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值