接下来看/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:
/common/main.cvoid 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*/ }
#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的起始地址,len是section的长度。
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循环,每一秒bootdelay减1并打印到屏幕上,如果在这期间hit任意按键,那么tstc()函数返回1,进入if语句,把abort置1,bootdelay清0,
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是否合法,检验字符串长度是不是太长。
然后将命令字符串拷贝到cmdbuf,cmdbuf是一个指针变量,用来存放命令字符串,这样做比较安全。
接下来就是一个大的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就是version,maxargs最大参数个数就是1,repeatable的值是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,然后通过flag与CMD_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->p,i->p指向字符串的首地址,那么*i->p应该是第一个字符。
static_get在ch不等于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_TIME和CONFIG_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的指向,n是buffer 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_buf,argvsz是20,argv是定义的char *argv[CONFIG_SYS_MAXARGS + 1];
假设我们输入的命令行是“123h kiuahdi ahf”,123h和kiuahdi之间有一个空格,kiuahdi和ahf之间有两个空格和一个tab看make_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指向字符串的手字符s,argv[0] = s,argv是一个指针数组,它的第0个元素指向了字符1;
然后将通过++s将s移动到“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 - 1,argvsz是20,所以最大的有效参数下标只能是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,表示最多匹配的命令的个数
cmdv:char指针,存放匹配的命令
返回值:
匹配的命令的个数
为保持思路的连贯性,先完成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 write和nand read是有公有前缀的,为nand。
nm、nboot、nand、nfs是没有公有前缀的。
不知道这种理解是否正确。
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[]是cmdv。anchor永远都指向第一个第一个匹配的命令的名字,然后剩下的所有的匹配的命令的名字与之相比较,得出相同的部分。
我一开始觉得代码有问题,以为最后返回的len只是最后一个匹配的命令与第一个的共有部分的长度。
实际上因为for (i = 0; i < len; i++, t++, s++),所以len的值只可能越来越小。代码果然很精妙~~
现在我们知道find_common_prefix返回了所有匹配的命令的公有前缀的长度。接下来同样是计算“残缺的命令”的长度,k。
比较k和j的大小,显然如果k大于等于j的话,是没有意义的。
然后s = cmdv[0] + k;len = j;,s还是指向需要补充的第一个字符,len则为共有前缀的长度。
前面列出了命令匹配的所有可能情况,并做了一些布置,接下来就要把缺失的字符补充好。
先判断if (s != NULL),s != NULL有两种可能:
1.i == 1或i > 1且有公有前缀。
K是要补充的字符的长度,n是console_buffer的index,n+k判断有没有溢出。
我们输入的“残缺的命令”之后的tab被变为’\0’,buf + cnt就指向’\0’,即t指向紧接着“残缺的命令”之后的那个字符。
然后用一个for循环把缺少的字符从cmdv[0] + k处复制到console_buffer中。
接下来就是输出puts(t - k);以及一些重定位np和colp的重定位。
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,表示最多匹配的命令的个数
cmdv:char指针,存放匹配的命令
返回值:
匹配的命令的个数
继续引用大神的解释:
根据用户按下TAB键时已输入的命令个数以及在TAB前输入的最后一个字符,分为3种情况进行匹配命令的搜索。
函数正常情况下返回匹配命令的个数。
--------------------------------------------------------------------------------------------------------------------------------------
cmd_tbl_t*cmdtp;
在函数开始便定义了一个cmd_tbl_t指针,它定义在include/command.h的62行,实际上是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的整个运行流程就算结束了。