http://liuyue18301.blog.163.com/blog/static/27913282009930112920220/?fromdm&fromSearch&isFromSearchEngine=yes
U-boot会给Linux Kernel传递很多参数,如:串口,RAM,videofb、MAC地址等。而Linux kernel也会读取和处理这些参数。两者之间通过struct tag来传递参数。U-boot把要传递给kernel的东西保存在struct tag数据结构中,启动kernel时,把这个结构体的物理地址传给kernel;Linux kernel通过这个地址,用parse_tags分析出传递过来的参数。
本文主要以U-boot(1.1.6)传递RAM和Linux kernel读取RAM参数为例进行说明。
1、u-boot给kernel传RAM参数
在介绍该之前,我们需要看一看几个数据结构,这些是u-boot中几个重要的数据结构:
(1)gd_t结构体
U-Boot使用了一个结构体gd_t来存储全局数据区的数据,这个结构体在U-Boot的include/asm-arm/global_data.h中定义如下:
typedef struct global_data {
bd_t *bd; //与板子相关的结构,见下面
unsigned long flags;
unsigned long baudrate;
unsigned long have_console; /* serial_init() was called */
unsigned long reloc_off; /* Relocation Offset */
unsigned long env_addr; /* Address of Environment struct */
unsigned long env_valid; /* Checksum of Environment valid? */
unsigned long fb_base; /* base address of frame buffer */
#ifdef CONFIG_VFD //我们一般没有配置这个,这个是frame buffer的首地址
unsigned char vfd_type; /* display type */
#endif
#if 0
unsigned long cpu_clk; /* CPU clock in Hz! */
unsigned long bus_clk;
unsigned long ram_size; /* RAM size */
unsigned long reset_status; /* reset status register at boot */
#endif
void **jt; /* jump table */
} gd_t;
/*
* Global Data Flags
*/
#define GD_FLG_RELOC 0x00001 /* Code was relocated to RAM */
#define GD_FLG_DEVINIT 0x00002 /* Devices have been initialized */
#define GD_FLG_SILENT 0x00004 /* Silent mode */
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")
在global_data.h中U-Boot使用了一个存储在寄存器中的指针gd来记录全局数据区的地址:
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")
DECLARE_GLOBAL_DATA_PTR定义一个gd_t全局数据结构的指针,这个指针存放在指定的寄存器r8中。这个声明也避免编译器把r8分配给其它的变量。任何想要访问全局数据区的代码,只要代码开头加入“DECLARE_GLOBAL_DATA_PTR”一行代码,然后就可以使用gd指针来访问全局数据区了。
根据U-Boot内存使用图中可以计算gd的值:
gd = TEXT_BASE -CONFIG_SYS_MALLOC_LEN - sizeof(gd_t)
2)bd_t 保存与板子相关的配置参数
bd_t在U-Boot的include/asm-arm/u-boot.h中定义如下:
typedef struct bd_info {
int bi_baudrate; /* 串口通讯波特率 */
unsigned long bi_ip_addr; /* IP地址 */
unsigned char bi_enetaddr[6]; /* Ethernet adress */
struct environment_s *bi_env; /*环境变量开始地址 */
ulong bi_arch_number; /* unique id for this board开发板的机器码 */
ulong bi_boot_params; /* where this board expects params 内核参数的开始地址*/
struct /* RAM配置信息 */
{
ulong start;
ulong size;
} bi_dram[CONFIG_NR_DRAM_BANKS]; //在我的板子上DRAM配置是1个
#ifdef CONFIG_HAS_ETH1
/* second onboard ethernet port */
unsigned char bi_enet1addr[6];
#endif
} bd_t;
#define bi_env_data bi_env->data
#define bi_env_crc bi_env->crc
U-Boot启动内核时要给内核传递参数,这时就要使用gd_t,bd_t结构体中的信息来设置标记列表。
3)启动参数的数据结构
向内核传递启动参数可保存在两种数据结构中,param_struct和tag,前者是2.4内核用的,后者是2.6以后的内核更期望用的但是,到目前为止,2.6的内核也可以兼容前一种结构,内核参数通过一个静态的param_struct或tag链表在启动的时候传递到内核。需要注意的是,这两个数据结构在uboot中和linux中分别有定义,这个定义必须一致才能正常传递参数如果实际使用中不一致的话就不能正常传递,可以自行修改 两种数据结构具体定义如下(这里说的是内核源码中的定义):
struct param_struct {
union {
struct {
unsigned long page_size; /* 0 */
unsigned long nr_pages; /* 4 */
unsigned long ramdisk_size; /* 8 */
unsigned long flags; /* 12 */
#define FLAG_READONLY 1
#define FLAG_RDLOAD 4
#define FLAG_RDPROMPT 8
unsigned long rootdev; /* 16 */
unsigned long video_num_cols; /* 20 */
unsigned long video_num_rows; /* 24 */
unsigned long video_x; /* 28 */
unsigned long video_y; /* 32 */
unsigned long memc_control_reg; /* 36 */
unsigned char sounddefault; /* 40 */
unsigned char adfsdrives; /* 41 */
unsigned char bytes_per_char_h; /* 42 */
unsigned char bytes_per_char_v; /* 43 */
unsigned long pages_in_bank[4]; /* 44 */
unsigned long pages_in_vram; /* 60 */
unsigned long initrd_start; /* 64 */
unsigned long initrd_size; /* 68 */
unsigned long rd_start; /* 72 */
unsigned long system_rev; /* 76 */
unsigned long system_serial_low; /* 80 */
unsigned long system_serial_high; /* 84 */
unsigned long mem_fclk_21285; /* 88 */
} s;
char unused[256];
} u1;
union {
char paths[8][128];
struct {
unsigned long magic;
char n[1024 - sizeof(unsigned long)];
} s;
} u2;
char commandline[COMMAND_LINE_SIZE];
};
param_struct只需要设置cmmandline,u1.s.page_size,u1.s.nr_pages三个域,下面是使用param_struct例子通过param_struct让uboot中的go命令可以传递参数
分析:go的代码在common/cmd_boot.c中,里面并没有拷贝启动参数的代码,转向内核的时候也没有传送
启动参数所在的地址,因此添加如下代码用于拷贝参数,可以看到,对于param_struct只需要设置cmmandline
u1.s.page_size,u1.s.nr_pages三个域
char *commandline = getenv("bootargs");
struct param_struct *lxy_params=(struct param_struct *)0x80000100;
printf("setup linux parameters at 0x80000100\n");
memset(lxy_params,0,sizeof(struct param_struct));
lxy_params->u1.s.page_size=(0x1<<12); //4K 这个是必须有的,否则无法启动
lxy_params->u1.s.nr_pages=(0x4000000)>>12; //64M 这个是必须有的,否则无法启动
memcpy(lxy_params->commandline,commandline,strlen(commandline)+1);
printf("linux command line is: \"%s\"\n",lxy_params->commandline);
然后还要向内核传递参数地址,将下面一行代码修改:
rc = ((ulong (*)(int, char *[]))addr) (--argc, &argv[1]); //需要被修改的代码
rc = ((ulong(*)(int,int,uint))addr) (0, gd->bd->bi_arch_number,gd->bd->bi_boot_params);//修改之后的代码
关于param_struct不是这里重点,下面主要分析tag
对于tag来说,在实际使用中是一个struct tag组成的列表,在tag->tag_header中,一项是u32 tag(重名,注意类型)其值用宏ATAG_CORE,ATAG_MEM,ATAG_CMDLINE,ATAG_NONE等等来表示,此时下面union就会使用与之相关的数据结构同时,规定tag列表中第一项必须是ATAG_CORE,最后一项必须是ATAG_NONE,比如在linux代码中,找到启动参数之后首先看tag列表中的第一项的tag->hdr.tag是否为ATAG_CORE,如果不是,就会认为启动参数不是tag结构而是param_struct结构,然后调用函数来转换.在tag->tag_header中,另一项是u32 size,表示tag的大小,tag组成列表的方式就是指针+size
tag数据结构在arch/arm/include/asm/setup.h(U-Boot的在include/asm-arm/setup.h定义,完全一样)中定义如下:
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
/*
* Acorn specific
*/
struct tag_acorn acorn;
/*
* DC21285 specific
*/
struct tag_memclk memclk;
} u;
};
其中tag_header为tag头,表明tag_xxx的类型和大小,之所以要标识tag_xxx的类型是因为不同的tag需要不同的处理函数
内核tag_header的结构(arch/arm/include/asm/setup.h)为
struct tag_header {
__u32 size;
__u32 tag;
};
U-Boot的在include/asm-arm/setup.h定义
struct tag_header {
u32 size;
u32 tag;
};
size表示tag的结构大小,tag为表示tag类型的常量。这个静态的链表必须以tag_header.tag = ATAG_CORE开始,并以tag_header.tag = ATAG_NONE结束。由于不同的tag所使用的格式可能不尽相同,所以内核又定义了一个结构tagtable来把tag和相应的操作函数关联起来
(arch/arm/include/asm/setup.h)
struct tagtable {
__u32 tag;
int (*parse)(const struct tag *);
};
其中tag为标识入ATAG_NONE,ATAG_CORE等。parse为处理函数。Linux内核将tagtable也组成了一个静态的链表放入.taglist.init节中,这是通过__tagtable宏来实现的
#define __tag __used __attribute__((__section__(".taglist.init")))
#define __tagtable(tag, fn) \
static struct tagtable __tagtable_##fn __tag = { tag, fn }
这个tagtable 列表 是怎么形成的?
如arch/arm/kernel/setup.c
556 static int __init parse_tag_mem32(const struct tag *tag)
557 {
558 return arm_add_memory(tag->u.mem.start, tag->u.mem.size);
559 }
560
561 __tagtable(ATAG_MEM, parse_tag_mem32);
607 __tagtable(ATAG_SERIAL, parse_tag_serialnr);
608
609 static int __init parse_tag_revision(const struct tag *tag)
610 {
611 system_rev = tag->u.revision.rev;
612 return 0;
613 }
614
615 __tagtable(ATAG_REVISION, parse_tag_revision);
618 static int __init parse_tag_cmdline(const struct tag *tag)
619 {
620 strlcpy(default_command_line, tag->u.cmdline.cmdline, COMMAND_LINE_SIZE);
621 return 0;
622 }
623
624 __tagtable(ATAG_CMDLINE, parse_tag_cmdline);
根据前面相关宏定义,__tagtable(ATAG_CMDLINE, parse_tag_cmdline)展开后为
static struct tagtable __tagtable_parse_tag_cmdline __used __attribute__((__section__(".taglist.init"))) = { ATAG_CMDLINE, parse_tag_cmdline }
__tagtable将ATAG_CMDLINE和parse_tag_cmdline挂钩,
再参看arch/arm/kernel/vmlinux.lds.S文件
34 __proc_info_begin = .;
35 *(.proc.info.init)
36 __proc_info_end = .;
37 __arch_info_begin = .;
38 *(.arch.info.init)
39 __arch_info_end = .;
40 __tagtable_begin = .;
41 *(.taglist.init)
42 __tagtable_end = .;
tagtable 列表编译连接后被存放在.taglist.init中。
现在再来看一下U-boot给Linux Kernel传递启动参数的传递过程
启动参数是包装在struct tag数据结构里的,在linux kernel启动的时候,bootloader把这个数据结构拷贝到某个地址,在改动PC跳向内核接口的同时,通过通用寄存器R2来传递这个地址的值,在bootm执行的流程中,会调用do_bootm_linux()在执行Linux内核,内核的起始地址如下:
void (*theKernel)(int zero, int arch, uint params);
image_header_t *hdr = &header;
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);
header是uImage的头部,通过头部,得到内核映像起始的执行地址,标识为theKernel。从中也可以看到,内核接受三个参数,第一个为0,第二个为系统的ID号,第三个是传入内核的参数。
在do_bootm_linux()的最后,会跳到内核去执行:
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
thekernel其实不是个函数,而是指向内核入口地址的指针,把它强行转化为带三个参数的函数指针,会把三个
参数保存到通用寄存器中,实现了向kernel传递信息的功能,在这个例子里,把R0赋值为0,R1赋值为机器号bd->bi_arch_number, R2赋值为启动参数数据结构的首地址bd->bi_boot_params。最后两个参数在board/smdk2410/smdk2410.c的board_init()中被初始化。
因此,要向内核传递参数很简单,只要把启动参数封装在linux预定好的数据结构里,拷贝到某个地址(一般
约定俗成是内存首地址+100dex,后面会见到) p { margin-bottom: 0.21cm; }
U-boot向内核传递参数的具体实现过程
a、在include/asm-arm/global_data.h中声名一个gd全局指针变量宏定义,并指定存放在r8寄存器中,在后面要用到gd全局指针变量时,只须要在文件开头引用这个宏就可以了。
64 #define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")
b、在start_armboot(lib_arm/board.c)主函数中计算全局数据结构的地址并赋值给指针gd,并对struct tag数据结构里参数赋值
下面是start_armboot函数部分代码
55 DECLARE_GLOBAL_DATA_PTR; //gd指针引用声名
248 gd = (gd_t*)(_armboot_start - CFG_MALLOC_LEN - sizeof(gd_t));
249 /* compiler optimization barrier needed for GCC >= 3.4 */
250 __asm__ __volatile__("": : :"memory");
251
252 memset ((void*)gd, 0, sizeof (gd_t));
253 gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));
254 memset (gd->bd, 0, sizeof (bd_t));
255
256 monitor_flash_len = _bss_start - _armboot_start;
257
258 for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
259 if ((*init_fnc_ptr)() != 0) {
260 hang ();
261 }
262 }
首先在55行对gd指针引用声名,在248行计算全局数据结构的地址并赋值给指针gd,具体计算请参看前面的说明,253行计算出结构体中bd指针的地址,然后在第258行逐个调用init_sequence初始化函数列表数组中的初始化函数对平台硬件进行初始化,这里只分析后面用到的硬件初始化函数board_init、dram_init。这两个函数都在board/smdk2410/smdk2410.c中实现
首先看board_init函数,以下是部分实现
31 DECLARE_GLOBAL_DATA_PTR;
105 /* arch number of SMDK2410-Board */
106 gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
107
108 /* adress of boot parameters */
109 gd->bd->bi_boot_params = 0x30000100;//一般约定俗成是内存首地址+100dex
可以看到,theKernel最后两个参数在这里的第106和109行被初始化,uboot传给内核的参数表存被放在内存中起始偏移0x100的位置,这里只是指定了“指针”的位置,但还没初始化其中的值,后面传递到内核的参数列表的构建才初始化其中的值,这是在 do_bootm_linux()中跳到内核前去完成的。值得注意的是, 内核的默认运行地址的0x30008000,前面就是留给参数用的。所以一般不要将内核下载到该地址之前,以免冲掉了传给内核的参数。这里在55行同样要对gd指针引用声名,MACH_TYPE_SMDK2410在include/asm-arm/mach-types.h中定义,值为192
而dram_init函数是对struct tag数据结构里内存参数赋值,后面会用到。
117 int dram_init (void)
118 {
119 gd->bd->bi_dram[0].start = PHYS_SDRAM_1;
120 gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;
121
122 return 0;
123 }
PHYS_SDRAM_1与PHYS_SDRAM_1_SIZE宏都在include/configs/smdk2410.h中定义。
#define CONFIG_NR_DRAM_BANKS 1 /* we have 1 bank of DRAM */
#define PHYS_SDRAM_1 0x30000000 /* SDRAM Bank #1 */
#define PHYS_SDRAM_1_SIZE 0x04000000 /* 64 MB */
c、传递到内核的参数列表的构建
./common/cmd_bootm.c文件中,bootm命令对应的do_bootm函数,当分析uImage中信息发现OS是Linux时,调用./lib_arm/armlinux.c文件中的do_bootm_linux函数来启动Linux kernel。在do_bootm_linux函数中(lib_arm/armlinux.c) ,以下是部分相关源码:
#if defined (CONFIG_SETUP_MEMORY_TAGS) || \
defined (CONFIG_CMDLINE_TAG) || \
defined (CONFIG_INITRD_TAG) || \
defined (CONFIG_SERIAL_TAG) || \
defined (CONFIG_REVISION_TAG) || \
defined (CONFIG_LCD) || \
defined (CONFIG_VFD)
setup_start_tag (bd); /* 设置ATAG_CORE标志 */
#ifdef CONFIG_SERIAL_TAG
setup_serial_tag (¶ms);
#endif
#ifdef CONFIG_REVISION_TAG
setup_revision_tag (¶ms);
#endif
#ifdef CONFIG_SETUP_MEMORY_TAGS
setup_memory_tags (bd); /* 设置内存标记 */
#endif
#ifdef CONFIG_CMDLINE_TAG
setup_commandline_tag (bd, commandline); /* 设置命令行标记 */
#endif
#ifdef CONFIG_INITRD_TAG
if (initrd_start && initrd_end)
setup_initrd_tag (bd, initrd_start, initrd_end);
#endif
#if defined (CONFIG_VFD) || defined (CONFIG_LCD)
setup_videolfb_tag ((gd_t *) gd);
#endif
setup_end_tag (bd); /* 设置ATAG_NONE标志 */
#endif
在uboot中,进行设置传递到内核的参数列表tag的函数都在lib_arm/armlinux.c中,在这些函数前面是有ifdef的因此,如果你的bootm命令不能传递内核参数,就应该是在你的board的config文件里没有对上述的宏进行设置,定义一下即可
这里对于setup_start_tag、setup_memory_tags和setup_end_tag函数说明如下。它们都在lib_arm/armlinux.c文件中定义,如下
static void setup_start_tag (bd_t *bd)
{
params = (struct tag *) bd->bi_boot_params; /* 内核的参数的开始地址 */
params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size (tag_core);
params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;
params = tag_next (params);
}
标记列表必须以ATAG_CORE开始,setup_start_tag函数在内核的参数的开始地址设置了一个ATAG_CORE标记。
#ifdef CONFIG_SETUP_MEMORY_TAGS
static void setup_memory_tags (bd_t *bd) //初始化内存相关tag
{
int i;
/*设置一个内存标记 */
for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size (tag_mem32);
params->u.mem.start = bd->bi_dram[i].start; //0x30000000
params->u.mem.size = bd->bi_dram[i].size; //0x04000000(64M)
params = tag_next (params);
}
}
#endif /* CONFIG_SETUP_MEMORY_TAGS */
setup_memory_tags函数设置了一个ATAG_MEM标记,该标记包含内存起始地址,内存大小这两个参数。RAM相关参数在前面的setup_memory_tags函数中已经初始化.
78
static struct tag *setup_commandline_tag(struct tag *params, char *cmdline)
{
if (!cmdline)
return params;
/* eat leading white space */
while (*cmdline == ' ') cmdline++;
/*
* Don't include tags for empty command lines; let the kernel
* use its default command line.
*/
if (*cmdline == '\0')
return params;
params->hdr.tag = ATAG_CMDLINE;
params->hdr.size =
(sizeof (struct tag_header) + strlen(cmdline) + 1 + 3) >> 2;
strcpy(params->u.cmdline.cmdline, cmdline);
return tag_next(params);
}
static void setup_end_tag (bd_t *bd)
{
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
}
这个静态的链表必须以标记ATAG_CORE开始,并以标记ATAG_NONE结束。setup_end_tag函数设置了一个ATAG_NONE标记,表示标记列表的结束。
d、最后do_bootm_linux函数调用theKernel (0, machid, bd->bi_boot_params)去启动内核并传递参数,可以看见r0是machid,r2是bi_boot_params参数的地址。
2、Kernel读取U-boot传递的相关参数
对于Linux Kernel,ARM平台启动时,先执行arch/arm/kernel/head.S,此时r2寄存器的值为参数的地址,此文件会调用arch/arm/kernel/head-common.S中的函数,并最后调用start_kernel,看下面head-common.S的源码:
14 #define ATAG_CORE 0x54410001
15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
16 #define ATAG_CORE_SIZE_EMPTY ((2*4) >> 2)
17
18 .align 2
19 .type __switch_data, %object
20 __switch_data:
21 .long __mmap_switched
22 .long __data_loc @ r4
23 .long _data @ r5
24 .long __bss_start @ r6
25 .long _end @ r7
26 .long processor_id @ r4
27 .long __machine_arch_type @ r5
28 .long __atags_pointer @ r6
29 .long cr_alignment @ r7
30 .long init_thread_union + THREAD_START_SP @ sp
31
32 /*
33 * The following fragment of code is executed with the MMU on in MMU mode,
34 * and uses absolute addresses; this is not position independent.
35 *
36 * r0 = cp#15 control register
37 * r1 = machine ID
38 * r2 = atags pointer
39 * r9 = processor ID
40 */
41 __mmap_switched:
42 adr r3, __switch_data + 4
43
44 ldmia r3!, {r4, r5, r6, r7}
45 cmp r4, r5 @ Copy data segment if needed
46 1: cmpne r5, r6
47 ldrne fp, [r4], #4
48 strne fp, [r5], #4
49 bne 1b
50
51 mov fp, #0 @ Clear BSS (and zero fp)
52 1: cmp r6, r7
53 strcc fp, [r6],#4
54 bcc 1b
55
56 ARM( ldmia r3, {r4, r5, r6, r7, sp})
57 THUMB( ldmia r3, {r4, r5, r6, r7} )
58 THUMB( ldr sp, [r3, #16] )
59 str r9, [r4] @ Save processor ID
60 str r1, [r5] @ Save machine type
61 str r2, [r6] @ Save atags pointer
62 bic r4, r0, #CR_A @ Clear 'A' bit
63 stmia r7, {r0, r4} @ Save control register values
64 b start_kernel
str r2,[r6]:因为通用寄存器2 (r2) 必须是 kernel parameter list 的物理地址(parameter list 是由boot loader传递给kernel,用来描述设备信息属性的列表),所以将uboot传递进来的tags物理地址数值存入__atags_pointer指针( [r6] )中,__atags_pointer在第28行定义并通过42、56行将其加载到r6中,在arch/arm/kernel/setup.c中的setup_arch中将引用__atags_pointer为指向参数的地址.
init/main.c中的start_kernel函数中会调用setup_arch函数来处理各种平台相关的动作
start_kernel()
{
……
setup_arch(&command_line);
……
}
包括了u-boot传递过来参数的分析和保存,对tag的处理代码也在setup_arch里面。以下是一部分的关键代码(setup_arch函数在arch/arm/kernel/setup.c文件中实现):
767 void __init setup_arch(char **cmdline_p)
768 {
769 struct tag *tags = (struct tag *)&init_tags;//tags指向默认的tag链表
770 struct machine_desc *mdesc;
771 char *from = default_command_line;
772
773 unwind_init();
774
775 setup_processor();
776 mdesc = setup_machine(machine_arch_type);// mdesc包含启动参数在内存中的地址
.....................................................................................................
782 if (__atags_pointer) //检查BootLoader是否传入参数
783 tags = phys_to_virt(__atags_pointer);//bootloader有传递启动参数到内核
784 else if (mdesc->boot_params)//如果BootLoader没有传入参数则使用内核machine descriptor中设置的启动参数地址(arch/arm/mach-s3c2410/mach-smdk2410.c),这里设置的地址与BootLoader是否传入的一般是一致的。
785 tags = phys_to_virt(mdesc->boot_params);
786
787 #if defined(CONFIG_DEPRECATED_PARAM_STRUCT)
788 /*
789 * If we have the old style parameters, convert them to
790 * a tag list.
791 */
792 if (tags->hdr.tag != ATAG_CORE)//如果是旧的启动参数结构,将其转成新的tag链表的形式,新的tag链表的形式内核参数列表第一项必须是ATAG_CORE类型,如果不是,则需要转换成新的内核参数类型。
793 convert_to_tag_list(tags);//此函数完成新旧参数结构转换,将参数结构转换为tag list结构
794 #endif
795 if (tags->hdr.tag != ATAG_CORE)//转换失败,使用内置的启动参数
796 tags = (struct tag *)&init_tags;//则选用默认的内核参数,init_tags文件中有定义。
797
798 if (mdesc->fixup) //用内核参数列表填充meminfo,fixup函数出现在注册machine_desc中,即MACHINE_START、MACHINE_END定义中,这个函数,有些板子有,但在2410中没有定义这个函数。
799 mdesc->fixup(mdesc, tags, &from, &meminfo);
800
801 if (tags->hdr.tag == ATAG_CORE) {
802 if (meminfo.nr_banks != 0) //说明内存被初始化过
803 squash_mem_tags(tags);//如果在meminfo中有配置内存tag则跳过对内存tag的处理,如果是tag list,那么如果系统已经创建了默认的meminfo.nr_banks,清除tags中关于MEM的参数,以免再次被初始化
804 save_atags(tags);
805 parse_tags(tags);//做出一些针对各个tags的处理
806 }
.....................................................................................................
851 }
第769行tags指向默认的tag链表,内核中定义了一些默认的tags
init_tags在arch/arm/kernel/setup.c文件下定义如下
662 static struct init_tags {
663 struct tag_header hdr1;
664 struct tag_core core;
665 struct tag_header hdr2;
666 struct tag_mem32 mem;
667 struct tag_header hdr3;
668 } init_tags __initdata = {
669 { tag_size(tag_core), ATAG_CORE },
670 { 1, PAGE_SIZE, 0xff },
671 { tag_size(tag_mem32), ATAG_MEM },
672 { MEM_SIZE, PHYS_OFFSET },
673 { 0, ATAG_NONE }
674 };
上述结构中一个tag_header和tag_xxx形成了tag的完整描述,tag_size返回tag_head和tag_xxx的总大小,在tag_size中我们要注意的是u32*指针加1地址值实际上地址加了4
#define tag_next(t) ((struct tag*)((u32*)(t)+(t)->hdr.size))
#define tag_size(type) ((sizeof(struct tag_header)+sizeof(struct type)) >> 2
tag_size实际上计算的是(tag_head+tag_xxx)/4。经过进一步的分析还发现每个tag在内存中的大小并不是相同的,这一点可以从tag_next看出,tag_next只是将指针移到了下一个tag的tag_header处,这种内存布局更加紧凑。
注:2.6.18内核smdk2410的meminfo没有设置nr_banks,所以必须在内核的启动参数里面传递mem=”memory size”@”memory base address”,否则系统识别内存错误,这点从系统的启动信息就可以看出来,而且在加载initrd的时候也会遇到内存溢出的错误
if (__atags_pointer)
tags = phys_to_virt(__atags_pointer);
指向各种tag起始位置的指针,定义如下:
unsigned int __atags_pointer __initdata;
此指针指向__initdata段,各种tag的信息保存在这个段中。
mdesc->fixup(mdesc, tags, &from, &meminfo):fixup函数是板级相关的,通常就是一些ram起址大小bank之类的设定函数,如果执行过了,nrbank就不为0了,那么继续执行后面的语句时:
if (tags->hdr.tag == ATAG_CORE) {
if (meminfo.nr_banks != 0)
squash_mem_tags(tags);
parse_tags(tags);
}
就会调用squash_mem_tags把你u-boot传入的值给干掉,使parse_tags函数调用时不会处理ATAG_MEM。
然后执行到parse_tags
parse_tags定义如下(arch/arm/kernel/setup.c)
static void __init parse_tags(const struct tag *t)
{
for (; t->hdr.size; t = tag_next(t))
if (!parse_tag(t)) //针对每个tag 调用parse_tag 函数
printk(KERN_WARNING
"Ignoring unrecognised tag 0x%08x\n",
t->hdr.tag);
}
parse_tags遍历tag链表调用parse_tag对tag进行处理。parse_tags在tabtable中寻找tag的处理函数(通过tag_header结构中的tag)
static int __init parse_tag(const struct tag *tag)
{
extern struct tagtable __tagtable_begin, __tagtable_end;
struct tagtable *t;
for (t = &__tagtable_begin; t < &__tagtable_end; t++) //遍历tagtable列表,并调用处理函数,
if (tag->hdr.tag == t->tag) {
t->parse(tag); //调用处理函数
break;
}
return t < &__tagtable_end;
}
处理各种tags,其中包括了RAM参数的处理。这个函数处理如下tags:
561 __tagtable(ATAG_MEM, parse_tag_mem32);
554 __tagtable(ATAG_CORE, parse_tag_core);
555
对于处理RAM的tag,调用了parse_tag_mem32函数:
556 static int __init parse_tag_mem32(const struct tag *tag)
557 {
558 return arm_add_memory(tag->u.mem.start, tag->u.mem.size);
559 }
560
561 __tagtable(ATAG_MEM, parse_tag_mem32);
如上可见,parse_tag_mem32函数调用arm_add_memory函数把RAM的start和size等参数保存到了meminfo结构的meminfo结构体中。对照uboot部分内存初始化函数,我们知道uboot传递过来的tag->u.mem.start, tag->u.mem.size分别为0x30000000,0x4000000,现在再来分析arm_add_memory
arm_add_memory定义如下(arch/arm/kernel/setup.c)
static int __init arm_add_memory(unsigned long start, unsigned long size)
{
struct membank *bank = &meminfo.bank[meminfo.nr_banks];
if (meminfo.nr_banks >= NR_BANKS) {
printk(KERN_CRIT "NR_BANKS too low, "
"ignoring memory at %#lx\n", start);
return -EINVAL;
}
/*
* Ensure that start/size are aligned to a page boundary.
* Size is appropriately rounded down, start is rounded up.
*/
size -= start & ~PAGE_MASK;
bank->start = PAGE_ALIGN(start);
bank->size = size & PAGE_MASK;
/*
* Check whether this memory region has non-zero size or
* invalid node number.
*/
if (bank->size == 0)
return -EINVAL;
meminfo.nr_banks++;
return 0;
}
经过这样的处理,setup.c文件中的meminfo可就不再是
struct meminfo meminfo = { 0, };
而是
struct meminfo meminfo = { 1,{0x30000000,0x4000000,0},{}, };
表示当前有一个内存区域,物理地址是从0x30000000开始,大小是64M
最后,在setup_arch中执行下面语句
paging_init(&meminfo, mdesc)
再来看看另一个参数处理函数
618 static int __init parse_tag_cmdline(const struct tag *tag)
619 {
620 strlcpy(default_command_line, tag->u.cmdline.cmdline, COMMAND_LINE_SIZE);
621 return 0;
622 }
623
624 __tagtable(ATAG_CMDLINE, parse_tag_cmdline);
767 void __init setup_arch(char **cmdline_p)
768 {
769 struct tag *tags = (struct tag *)&init_tags;
770 struct machine_desc *mdesc;
771 char *from = default_command_line;
771行default_command_line在setup.c文件129行中定义如下:
static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;
其中CONFIG_CMDLINE在“.config”配置文件中定义的。定义如下:
CONFIG_CMDLINE="root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M"
default_command_line 原来的内容是我们配置文件中确定的,但是现在,他被tag->u.cmdline.cmdline覆盖了。可见,从uboot传递过来的命令行参数的优先级要高于配置文件的默认命令行.
我们接着setup_arch中的parse_tags(tags)往下看:
808 init_mm.start_code = (unsigned long) _text;
809 init_mm.end_code = (unsigned long) _etext;
810 init_mm.end_data = (unsigned long) _edata;
811 init_mm.brk = (unsigned long) _end;
812
813 /* parse_early_param needs a boot_command_line */
814 strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);
815
816 /* populate cmd_line too for later use, preserving boot_command_line */
817 strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
818 *cmdline_p = cmd_line;
819
820 parse_early_param();
821
822 arm_memblock_init(&meminfo, mdesc);
823
824 paging_init(mdesc);
825 request_standard_resources(&meminfo, mdesc);
init_mm.brk = (unsigned long) _end:从这儿之后的内存可以动态的分配了。填充 init_mm 的成员,这些数值在lds里面。分别是代码段,数据段和bss段。
strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
上面的代码先把uboot传递过来的命令行参数保存起来,以备后用。
linux内核commandline参数解析过程
前面详细分析了u-boot与linux内核间的tag参数传递及解析过程,但对命令行参数没做详细的分析,在setup_arch函数的第817行我们看到把uboot传递过来的命令行参数保存起来,以备后用。这里的后用就是linux内核commandline参数解析,也就是给第820行代码备用的,对2.6.36以前版本没用 parse_early_param而是用parse_cmdline函数,在分析这两个函数前,我们先来看一下从u-boot到内核命令行参数设置及传递过程,以便更好的理解后面的分析。
在u-boot的include/configs/smdk2410.h配置文件中我们可以找到CONFIG_BOOTARGS配置项,在这里我们可以设置要传递的到内核的命令行参数,如:
*#define CONFIG_BOOTARGS "root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M"
再看u-boot的common/env_common.c文件
static uchar env_get_char_init (int index);
uchar (*env_get_char)(int) = env_get_char_init;
/************************************************************************
* Default settings to be used when no valid environment is found
*/
#define XMK_STR(x) #x
#define MK_STR(x) XMK_STR(x)
uchar default_environment[] = {
#ifdef CONFIG_BOOTARGS
"bootargs=" CONFIG_BOOTARGS "\0"
#endif
#ifdef CONFIG_BOOTCOMMAND
"bootcmd=" CONFIG_BOOTCOMMAND "\0"
#endif
#ifdef CONFIG_RAMBOOTCOMMAND
"ramboot=" CONFIG_RAMBOOTCOMMAND "\0"
#endif
#ifdef CONFIG_NFSBOOTCOMMAND
"nfsboot=" CONFIG_NFSBOOTCOMMAND "\0"
#endif
#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)
"bootdelay=" MK_STR(CONFIG_BOOTDELAY) "\0"
#endif
#if defined(CONFIG_BAUDRATE) && (CONFIG_BAUDRATE >= 0)
"baudrate=" MK_STR(CONFIG_BAUDRATE) "\0"
#endif
#ifdef CONFIG_LOADS_ECHO
"loads_echo=" MK_STR(CONFIG_LOADS_ECHO) "\0"
#endif
#ifdef CONFIG_ETHADDR
"ethaddr=" MK_STR(CONFIG_ETHADDR) "\0"
#endif
#ifdef CONFIG_ETH1ADDR
"eth1addr=" MK_STR(CONFIG_ETH1ADDR) "\0"
#endif
#ifdef CONFIG_ETH2ADDR
"eth2addr=" MK_STR(CONFIG_ETH2ADDR) "\0"
#endif
#ifdef CONFIG_ETH3ADDR
"eth3addr=" MK_STR(CONFIG_ETH3ADDR) "\0"
#endif
#ifdef CONFIG_IPADDR
"ipaddr=" MK_STR(CONFIG_IPADDR) "\0"
#endif
#ifdef CONFIG_SERVERIP
"serverip=" MK_STR(CONFIG_SERVERIP) "\0"
#endif
#ifdef CFG_AUTOLOAD
"autoload=" CFG_AUTOLOAD "\0"
#endif
#ifdef CONFIG_PREBOOT
"preboot=" CONFIG_PREBOOT "\0"
#endif
#ifdef CONFIG_ROOTPATH
"rootpath=" MK_STR(CONFIG_ROOTPATH) "\0"
#endif
#ifdef CONFIG_GATEWAYIP
"gatewayip=" MK_STR(CONFIG_GATEWAYIP) "\0"
#endif
#ifdef CONFIG_NETMASK
"netmask=" MK_STR(CONFIG_NETMASK) "\0"
#endif
#ifdef CONFIG_HOSTNAME
"hostname=" MK_STR(CONFIG_HOSTNAME) "\0"
#endif
#ifdef CONFIG_BOOTFILE
"bootfile=" MK_STR(CONFIG_BOOTFILE) "\0"
#endif
#ifdef CONFIG_LOADADDR
"loadaddr=" MK_STR(CONFIG_LOADADDR) "\0"
#endif
。。。。。。。。。。。。。。。
可以知道CONFIG_BOOTARGS被转化为
"bootargs=""root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M"
u-boot引导内核为调用u-boot的lib_arm/armlinux.c文件的do_bootm_linux函数
void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
ulong addr, ulong *len_ptr, int verify)
{
ulong len = 0, checksum;
ulong initrd_start, initrd_end;
ulong data;
void (*theKernel)(int zero, int arch, uint params);
image_header_t *hdr = &header;
bd_t *bd = gd->bd;
#ifdef CONFIG_CMDLINE_TAG
char *commandline = getenv ("bootargs");
#endif
245
#ifdef CONFIG_CMDLINE_TAG
setup_commandline_tag (bd, commandline);
#endif
..........................
}
在这里它首先调用getenv ("bootargs")函数获得命令行参数并让commandline指向它,然后调用setup_commandline_tag函数将命令行参数放到tag参数例表,
static struct tag *setup_commandline_tag(struct tag *params, char *cmdline)
{
if (!cmdline)
return params;
/* eat leading white space */
while (*cmdline == ' ') cmdline++;
/*
* Don't include tags for empty command lines; let the kernel
* use its default command line.
*/
if (*cmdline == '\0')
return params;
params->hdr.tag = ATAG_CMDLINE;
params->hdr.size =
(sizeof (struct tag_header) + strlen(cmdline) + 1 + 3) >> 2;
strcpy(params->u.cmdline.cmdline, cmdline);
return tag_next(params);
}
关于tag参数例表前面己有详细分析,这里我只对u-boot取命令行环境参数函数getenv进行分析,它定义在common/cmd_nvedit.c文件中
char *getenv (char *name)
{
int i, nxt;
WATCHDOG_RESET();
for (i=0; env_get_char(i) != '\0'; i=nxt+1) {
int val;
for (nxt=i; env_get_char(nxt) != '\0'; ++nxt) {
if (nxt >= CFG_ENV_SIZE) {
return (NULL);
}
}
if ((val=envmatch((uchar *)name, i)) < 0)
continue;
return ((char *)env_get_addr(val));
}
return (NULL);
}
这里重点理解env_get_char函数,它定义在common/env_common.c中:
static uchar env_get_char_init (int index);
uchar (*env_get_char)(int) = env_get_char_init;
/************************************************************************
* Default settings to be used when no valid environment is found
*/
#define XMK_STR(x) #x
#define MK_STR(x) XMK_STR(x)
uchar default_environment[] = {
#ifdef CONFIG_BOOTARGS
"bootargs=" CONFIG_BOOTARGS "\0"
#endif
.................
static uchar env_get_char_init (int index)
{
uchar c;
/* if crc was bad, use the default environment */
if (gd->env_valid)
{
c = env_get_char_spec(index);
} else {
c = default_environment[index];
}
return (c);
}
这里gd->env_valid参数在start_armboot函数中的初始化函数例表中的env_init函数中设置,如果配置参数保存在flash中,gd->env_valid被设置为1,这里就通过env_get_char_spec函数从flash中取参数,否则gd->env_valid设置为0,使用默认环境变量参数,默认环境变量参数定义在u-boot的common/env_common.c文件uchar default_environment[] ,也就是include/configs/smdk2410.h配置文件中配置的参数。这里针对不同的flash存储芯片有不同的env_get_char_spec定义
common/env_flash.c
uchar env_get_char_spec (int index)
{
return ( *((uchar *)(gd->env_addr + index)) );
}
common/env_nand.c
DECLARE_GLOBAL_DATA_PTR;
uchar env_get_char_spec (int index)
{
return ( *((uchar *)(gd->env_addr + index)) );
}
common/env_nvram.c
#ifdef CONFIG_AMIGAONEG3SE
uchar env_get_char_spec (int index)
{
#ifdef CFG_NVRAM_ACCESS_ROUTINE
uchar c;
nvram_read(&c, CFG_ENV_ADDR+index, 1);
return c;
#else
uchar retval;
enable_nvram();
retval = *((uchar *)(gd->env_addr + index));
disable_nvram();
return retval;
#endif
}
#else
uchar env_get_char_spec (int index)
{
#ifdef CFG_NVRAM_ACCESS_ROUTINE
uchar c;
nvram_read(&c, CFG_ENV_ADDR+index, 1);
return c;
#else
return *((uchar *)(gd->env_addr + index));
#endif
}
#endif
为确定gd->env_addr,我们来看一下env_init函数,这里以flash为例,它在common/env_flash.c中
int env_init(void)
{
#ifdef CONFIG_OMAP2420H4
int flash_probe(void);
if(flash_probe() == 0)
goto bad_flash;
#endif
if (crc32(0, env_ptr->data, ENV_SIZE) == env_ptr->crc) {
gd->env_addr = (ulong)&(env_ptr->data);
gd->env_valid = 1;
return(0);
}
#ifdef CONFIG_OMAP2420H4
bad_flash:
#endif
gd->env_addr = (ulong)&default_environment[0];
gd->env_valid = 0; 使用默认环境变量参数,gd->env_valid设置为0
return (0);
}
而在include/configs/smdk2410.h配置文件中关于flsah的配置如下:
#define PHYS_FLASH_1 0x00000000 /* Flash Bank #1 */
#define CFG_FLASH_BASE PHYS_FLASH_1
/*-----------------------------------------------------------------------
* FLASH and environment organization
*/
#define CONFIG_AMD_LV400 1 /* uncomment this if you have a LV400 flash */
#if 0
#define CONFIG_AMD_LV800 1 /* uncomment this if you have a LV800 flash */
#endif
#define CFG_MAX_FLASH_BANKS 1 /* max number of memory banks */
#ifdef CONFIG_AMD_LV800
#define PHYS_FLASH_SIZE 0x00100000 /* 1MB */
#define CFG_MAX_FLASH_SECT (19) /* max number of sectors on one chip */
#define CFG_ENV_ADDR (CFG_FLASH_BASE + 0x0F0000) /* addr of environment */
#endif
#ifdef CONFIG_AMD_LV400
#define PHYS_FLASH_SIZE 0x00080000 /* 512KB */
#define CFG_MAX_FLASH_SECT (11) /* max number of sectors on one chip */
#define CFG_ENV_ADDR (CFG_FLASH_BASE + 0x070000) /* addr of environment */
#endif
/* timeout values are in ticks */
#define CFG_FLASH_ERASE_TOUT (5*CFG_HZ) /* Timeout for Flash Erase */
#define CFG_FLASH_WRITE_TOUT (5*CFG_HZ) /* Timeout for Flash Write */
#define CFG_ENV_IS_IN_FLASH 1
#define CFG_ENV_SIZE 0x10000 /* Total Size of Environment Sector */
#endif /* __CONFIG_H */
在common/env_flash.c中对env_ptr定义如下:
char * env_name_spec = "Flash";
#ifdef ENV_IS_EMBEDDED
extern uchar environment[];
env_t *env_ptr = (env_t *)(&environment[0]);
#ifdef CMD_SAVEENV
/* static env_t *flash_addr = (env_t *)(&environment[0]);-broken on ARM-wd-*/
static env_t *flash_addr = (env_t *)CFG_ENV_ADDR;
#endif
#else /* ! ENV_IS_EMBEDDED */
env_t *env_ptr = (env_t *)CFG_ENV_ADDR;
#ifdef CMD_SAVEENV
static env_t *flash_addr = (env_t *)CFG_ENV_ADDR;
#endif
#endif /* ENV_IS_EMBEDDED */
通过上面几个文件相关定义,我们很容易知道env_get_char_init函数功能就是如果保存了参数到flsah中就调用env_get_char_spec从指定的flash地址中读取参数字符,否则就从默认环境变量参数中读取参数字符。
理解完env_get_char_init函数后,再来看envmatch函数,定义在common/cmd_nvedit.c
/************************************************************************
* Match a name / name=value pair
*
* s1 is either a simple 'name', or a 'name=value' pair.
* i2 is the environment index for a 'name2=value2' pair.
* If the names match, return the index for the value2, else NULL.
*/
static int
envmatch (uchar *s1, int i2)
{
while (*s1 == env_get_char(i2++))
if (*s1++ == '=')
return(i2);
if (*s1 == '\0' && env_get_char(i2-1) == '=')
return(i2);
return(-1);
}
这个函数功能是查找符号变量,如果找到则返回等号后面的字符串指针,即为变量的值,环境变量表是一个字符串数组,而其中的变量之间通过’\0’符号隔开,即是当遇到该符号时,则表示一个变量结束而另一个变量开始。
common/env_common.c
uchar *env_get_addr (int index)
{
if (gd->env_valid) {
return ( ((uchar *)(gd->env_addr + index)) );
} else {
return (&default_environment[index]);
}
}
这个函数功能是返回找到的环境变量字符串数组地址。
此至,命令行参数在u-boot中设置及传递过程分析完了,下面我们再来看linux内核commandline参数解析过程,内核在start_kernel函数调用start_arch获取tag参数地址后,再调用parse_tags完成了tag参数解释,之后就是linux内核commandline参数解析,也就调用parse_early_param或parse_cmdline(对2.6.36以前版本)函数来完成对命令行参数的解释。
linux内核commandline参数解析过程
前面使用
strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
*cmdline_p = cmd_line;
将命令行参数保存到了cmd_line中
parse_early_param();
现在再来看看start_arch函数中第820行的parse_early_param函数
init/main.c
void __init parse_early_options(char *cmdline)
{
parse_args("early options", cmdline, NULL, 0, do_early_param);
}
/* Arch code calls this early on, or if not, just before other parsing. */
void __init parse_early_param(void)
{
static __initdata int done = 0;
static __initdata char tmp_cmdline[COMMAND_LINE_SIZE];
if (done)
return;
/* All fall through to do_early_param. */
strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
parse_early_options(tmp_cmdline);
done = 1;
}
在上面我们可以看到最终调用的是 parse_args("early options", cmdline, NULL, 0, do_early_param);parse_args在kernel/params.c中定义,注意它与前面的parse_tags(const struct tag *t)区别。
/* Args looks like "foo=bar,bar2 baz=fuz wiz". */
int parse_args(const char *name,
char *args,
const struct kernel_param *params,
unsigned num,
int (*unknown)(char *param, char *val))
{
char *param, *val;
DEBUGP("Parsing ARGS: %s\n", args);
/* Chew leading spaces 跳过前面的空格*/
args = skip_spaces(args);
while (*args) {
int ret;
int irq_was_disabled;
args = next_arg(args, ¶m, &val);
irq_was_disabled = irqs_disabled();
ret = parse_one(param, val, params, num, unknown);
if (irq_was_disabled && !irqs_disabled()) {
printk(KERN_WARNING "parse_args(): option '%s' enabled "
"irq's!\n", param);
}
switch (ret) {
case -ENOENT:
printk(KERN_ERR "%s: Unknown parameter `%s'\n",
name, param);
return ret;
case -ENOSPC:
printk(KERN_ERR
"%s: `%s' too large for parameter `%s'\n",
name, val ?: "", param);
return ret;
case 0:
break;
default:
printk(KERN_ERR
"%s: `%s' invalid for parameter `%s'\n",
name, val ?: "", param);
return ret;
}
}
/* All parsed OK. */
return 0;
}
#define isspace(c) ((c) == ' ')
char *skip_spaces(const char *str) 跳过前面的空格函数
{
while (isspace(*str))
++str;
return (char *)str;
}
EXPORT_SYMBOL(skip_spaces);
对于next_arg就在parse_args前定义如下:
它的功能解释"root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M"参数表,以第一个root=/dev/mtdblock3为例说明:
static char *next_arg(char *args, char **param, char **val)
{
unsigned int i, equals = 0;
int in_quote = 0, quoted = 0; //in_quote字符串结束标志
char *next;
// [args] 指向内容"root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M"
if (*args == '"') {
args++; //[args]指向内容root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M"
in_quote = 1; //in_quote字符串开始标志
quoted = 1;
}
for (i = 0; args[i]; i++) { //循环完后,
if (isspace(args[i]) && !in_quote) //空格或没有字符了,退出。
break;
if (equals == 0) { //查找到第一个=号位置
if (args[i] == '=')
equals = i;
}
if (args[i] == '"') //最后一个结束字符'"'吗?是的话设置in_quote = !in_quote
in_quote = !in_quote;
}
*param = args;
//[args] 指向内容root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M"
if (!equals)
*val = NULL;
else {
args[equals] = '\0';
//[args]指向内容root /dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M"
*val = args + equals + 1;
// *val指向内容/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M"
/* Don't include quotes in value. 去掉引号*/
if (**val == '"') {
(*val)++;
if (args[i-1] == '"')
args[i-1] = '\0';
}
if (quoted && args[i-1] == '"')
args[i-1] = '\0';
}
if (args[i]) {
args[i] = '\0';
next = args + i + 1;
} else
next = args + i;
/* Chew up trailing spaces. */
return skip_spaces(next);
}
第一次执行后,[*param] = root [*val] = /dev/mtdblock3
next指向init=/linuxrc console=ttySAC0,115200 mem=64M
现在再看parse_one,它定义在同一文件下:
此时相当于执行:parse_one("root", "/dev/mtdblock3",NULL, 0, do_early_param);
static int parse_one(char *param,
char *val,
const struct kernel_param *params,
unsigned num_params,
int (*handle_unknown)(char *param, char *val))
{
unsigned int i;
int err;
/* Find parameter */
for (i = 0; i < num_params; i++) {
if (parameq(param, params[i].name)) { //因为传入的params为NULL,这下面不执行
/* Noone handled NULL, so do it here. */
if (!val && params[i].ops->set != param_set_bool)
return -EINVAL;
DEBUGP("They are equal! Calling %p\n",
params[i].ops->set);
mutex_lock(¶m_lock);
err = params[i].ops->set(val, ¶ms[i]);
mutex_unlock(¶m_lock);
return err;
}
}
if (handle_unknown) { //调用do_early_param函数
DEBUGP("Unknown argument: calling %p\n", handle_unknown);
return handle_unknown(param, val);
}
DEBUGP("Unknown argument `%s'\n", param);
return -ENOENT;
}
以下只作了解:
kernel/params.c
static inline char dash2underscore(char c)
{
if (c == '-')
return '_';
return c;
}
static inline int parameq(const char *input, const char *paramname)
{
unsigned int i;
for (i = 0; dash2underscore(input[i]) == paramname[i]; i++)
if (input[i] == '\0')
return 1;
return 0;
}
我们再来看一下do_early_param函数,它在init/main.c中
static int __init do_early_param(char *param, char *val)
{
const struct obs_kernel_param *p;
这里的__setup_start和_-setup_end分别是.init.setup段的起始和结束的地址
for (p = __setup_start; p < __setup_end; p++) { 如果没有early标志则跳过
if ((p->early && strcmp(param, p->str) == 0) ||
(strcmp(param, "console") == 0 &&
strcmp(p->str, "earlycon") == 0)
) {
if (p->setup_func(val) != 0) 调用处理函数
printk(KERN_WARNING
"Malformed early option '%s'\n", param);
}
}
/* We accept everything at this stage. */
return 0;
}
在do_early_param函数中for循环遍历obs_kernel_param数组,这里首先要说明一下struct obs_kernel_param结构及这个数组的由来。
obs_kernel_param结构定义在include/linux/init.h文件中
218 struct obs_kernel_param {
219 const char *str;
220 int (*setup_func)(char *);
221 int early;
222 };
前两个参数很简单,一个是key,一个是处理函数。最后一个参数其实也就是类似于优先级的一个标志flag,因为传递给内核的参数中,有一些需要比另外的一些更早的解析。(这也就是为什么early_param和setup传递的最后一个参数的不同的原因了,后面会讲到)。
先说一下系统启动时对bootloader传递参数的初始化,即linux启动参数的实现,启动参数的实现,我们知道boot传递给内核的参数都是 "name_varibale=value"这种形式的,如下:
CONFIG_CMDLINE="root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M"
那么内核如何知道传递进来的参数该怎么去处理呢?
内核是通过__setup宏或者early_param宏来将参数与参数所处理的函数相关联起来的。我们接下来就来看这个宏以及相关的结构的定义:
include/linux/init.h
230 #define __setup_param(str, unique_id, fn, early) \
231 static const char __setup_str_##unique_id[] __initconst \
232 __aligned(1) = str; \
233 static struct obs_kernel_param __setup_##unique_id \
234 __used __section(.init.setup) \
235 __attribute__((aligned((sizeof(long))))) \
236 = { __setup_str_##unique_id, fn, early }
237
238 #define __setup(str, fn) \
239 __setup_param(str, fn, fn, 0)
240
241 /* NOTE: fn is as per module_param, not __setup! Emits warning if fn
242 * returns non-zero. */
243 #define early_param(str, fn) \
244 __setup_param(str, fn, fn, 1)
可见,__setup宏的作用是使用str值和函数句柄fn初始化一个static结构体 obs_kernel_param。该结构体在链接后存在于.init.setup段。其实该段也就是所有内核参数所在的处。该段的起始地址是__setup_start,结束地址为__setup_end。同样的还有一个early_param宏,也是设置一个内核参数,不过改参数是早期启动时相关的。
看起来很复杂。 首先setup宏第一个参数是一个key。比如"netdev="这样的,而第二个参数是这个key对应的处理函数。这里要注意相同的handler能联系到不同的key。__setup与early_param不同的是,early_param宏注册的内核选项必须要在其他内核选项之前被处理。在函数 start_kernel中,parse_early_param处理early_param定义的参数,parse_args处理__setup定义的参数。
early_param和setup唯一不同的就是传递给__setup_param的最后一个参数,这个参数下面会说明,而接下来 _setup_param定义了一个struct obs_kernel_param类型的结构,然后通过_section宏,使这个变量在链接的时候能够放置在段.init.setup(后面会详细介绍内核中的这些初始化段).
比如说定义一个内核参数来实现对init程序的指定。见init/main.c中
1,所有的系统启动参数都是由形如
static int __init init_setup(char *str)的函数来支持的
static int __init init_setup(char *str)
{
unsigned int i;
execute_command = str;
for (i = 1; i < MAX_INIT_ARGS; i++)
argv_init[i] = NULL;
return 1;
}
__setup("init=", init_setup);
注:(include/linux/init.h):
#define __init __section(.init.text) __cold notrace申明所有的启动参数支持函数都放入.init.text段
2.1,用__setup宏来导出参数的支持函数
__setup("init=", init_setup);
展开后就是如下的形式
static const char __setup_str_init_setup[] __initdata = "init=";
static struct obs_kernel_param __setup_init_setup
__used __section__(".init.setup")
__attribute__((aligned((sizeof(long)))))
= { __setup_str_init_setup, init_setup, 0 };//"init=",init_setup,0
也就是说,启动参数(函数指针)被封装到obs_kernel_param结构中,
所有的内核启动参数形成内核映像.init.setup段中的一个
obs_kernel_param数组,而在do_early_param函数中for循环就是遍历obs_kernel_param数组,首先看是否
设置early,如果有设置并查找到到对就的key,就调用相关early_param函数处理。
用early_param宏来申明需要'早期'处理的启动参数,例如在
arch/arm/kernel/setup.c就有如下的申明:
468 early_param("mem", early_mem);
展开后和__setup是一样的只是early参数不一样,因此会在do_early_param
中被处理
443 static int __init early_mem(char *p)
444 {
445 static int usermem __initdata = 0;
446 unsigned long size, start;
447 char *endp;
448
449 /*
453 */
454 if (usermem == 0) {
455 usermem = 1;
456 meminfo.nr_banks = 0;
457 }
458
459 start = PHYS_OFFSET;
460 size = memparse(p, &endp);
461 if (*endp == '@')
462 start = memparse(endp + 1, NULL);
463
464 arm_add_memory(start, size);
465
466 return 0;
467 }
init/main.c中启动参数申明列表:
early_param("nosmp", nosmp);
early_param("nr_cpus", nrcpus);
early_param("maxcpus", maxcpus);
__setup("reset_devices", set_reset_devices);
early_param("debug", debug_kernel);
early_param("quiet", quiet_kernel);
early_param("loglevel", loglevel);
__setup("init=", init_setup);
__setup("rdinit=", rdinit_setup);
arch/arm/kernel/setup.c中启动参数申明列表:
__setup("fpe=", fpe_setup);
early_param("mem", early_mem);
early_param("elfcorehdr", setup_elfcorehdr);
注意在2.6.36版本中,除在start_kernel中的start_arch函数中第820调用parse_early_param()外,还在start_arch函数后的580行和581行分别执行下面代码再次解释命令行参数
579 printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
580 parse_early_param();
581 parse_args("Booting kernel", static_command_line, __start___param,
582 __stop___param - __start___param,
583 &unknown_bootoption);
对于parse_early_param我们已经很熟悉,在这里不知道为什么它再做的一次,下面看第581行,我们再来看看parse_args函数,见前面的代码分解出命令行的参数后,会调用
ret = parse_one(param, val, params, num, unknown);
相当于:
ret = parse_one(param, val, __start___param,__stop___param - __start___param,&unknown_bootoption);
这里提到了两个参数__start___param和__stop___param,它涉及到kernel_param 结构体和参数的存储
关于内核参数结构体的定义,见include/linux/moduleparam.h
99 #define module_param(name, type, perm) \
100 module_param_named(name, name, type, perm)
113 #define module_param_named(name, value, type, perm) \
114 param_check_##type(name, &(value)); \
115 module_param_cb(name, ¶m_ops_##type, &value, perm); \
116 __MODULE_PARM_TYPE(name, #type)
126 #define module_param_cb(name, ops, arg, perm) \
127 __module_param_call(MODULE_PARAM_PREFIX, \
128 name, ops, arg, __same_type((arg), bool *), perm )
142 #define __module_param_call(prefix, name, ops, arg, isbool, perm) \
143 /* Default value instead of permissions? */ \
144 static int __param_perm_check_##name __attribute__((unused)) = \
145 BUILD_BUG_ON_ZERO((perm) < 0 || (perm) > 0777 || ((perm) & 2)) \
146 + BUILD_BUG_ON_ZERO(sizeof(""prefix) > MAX_PARAM_PREFIX_LEN); \
147 static const char __param_str_##name[] = prefix #name; \
148 static struct kernel_param __moduleparam_const __param_##name \
149 __used \
150 __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \
151 = { __param_str_##name, ops, perm, isbool ? KPARAM_ISBOOL : 0, \
152 { arg } }
这里也就是填充了 struct kernel_param的结构体,并将这个变量标记为__param段,以便于链接器将此变量装载到指定的段,和结构体 obs_kernel_param类似,该宏函数保持所有实例存在于__param段。该段的起始地址是__start___param,结束地址为__stop___param。具体链接脚本在include/asm-generic/vmlinux.lds.h
350 /* Built-in module parameters. */ \
351 __param : AT(ADDR(__param) - LOAD_OFFSET) { \
352 VMLINUX_SYMBOL(__start___param) = .; \
353 *(__param) \
354 VMLINUX_SYMBOL(__stop___param) = .; \
355 . = ALIGN((align)); \
356 VMLINUX_SYMBOL(__end_rodata) = .; \
357 } \
358 . = ALIGN((align));
这里给个例子net/ipv4/netfilter/nf_nat_irc.c
static int warn_set(const char *val, struct kernel_param *kp)
{
printk(KERN_INFO KBUILD_MODNAME
": kernel >= 2.6.10 only uses 'ports' for conntrack modules\n");
return 0;
}
module_param_call(ports, warn_set, NULL, NULL, 0);
处理module_param_call之外,还有core_param也可以定义内核参数,不过内核参数不可以模块化,也不可以使用前缀命名(如“printk.”)。
接下来我们来看struct kernel_param这个结构:
include/linux/moduleparam.h
struct kernel_param;
/* Flag bits for kernel_param.flags */
#define KPARAM_ISBOOL 2
struct kernel_param {
const char *name;
const struct kernel_param_ops *ops;
u16 perm;
u16 flags;
union { 传递给上面kernel_param_ops中两个函数的参数
void *arg;
const struct kparam_string *str;
const struct kparam_array *arr;
};
};
其中,联合体内定义的三个成员,第一个其实是字符类型的封装。
/* Special one for strings we want to copy into */
struct kparam_string {
unsigned int maxlen;
char *string;
};
第二个是数组类型的封装。
/* Special one for arrays */
struct kparam_array
{
unsigned int max;
unsigned int *num;
const struct kernel_param_ops *ops;
unsigned int elemsize;
void *elem;
};
还剩下的常字符串类型成员name为内核参数的名称,而perm为权限????。
同时数组类型的封装kernel_param_ops中还定义了两个方法,以函数指针存在。分别是设置和读取操作。
struct kernel_param_ops {
/* Returns 0, or -errno. arg is in kp->arg. */
int (*set)(const char *val, const struct kernel_param *kp);设置参数的函数
/* Returns length written or -errno. Buffer is 4k (ie. be short!) */
int (*get)(char *buffer, const struct kernel_param *kp);读取参数的函数
/* Optional function to free kp->arg when module unloaded. */
void (*free)(void *arg);
};
现在我们再回到parse_one函数中,我们前面有部分没有分析,现在再来看一下
static int parse_one(char *param,
char *val,
const struct kernel_param *params,
unsigned num_params,
int (*handle_unknown)(char *param, char *val))
{
unsigned int i;
int err;
如果是early_param则直接跳过这步,而非early的,则要通过这步来设置一些内置模块的参数。
/* Find parameter */
for (i = 0; i < num_params; i++) {
if (parameq(param, params[i].name)) { //如果是内置模块的参数
/* Noone handled NULL, so do it here. */
if (!val && params[i].ops->set != param_set_bool)
return -EINVAL;
DEBUGP("They are equal! Calling %p\n",
params[i].ops->set);
mutex_lock(¶m_lock);
err = params[i].ops->set(val, ¶ms[i]); //调用参数设置函数来设置对应的内置模块的参数。
mutex_unlock(¶m_lock);
return err;
}
}
if (handle_unknown) {
DEBUGP("Unknown argument: calling %p\n", handle_unknown);
return handle_unknown(param, val);
}
DEBUGP("Unknown argument `%s'\n", param);
return -ENOENT;
}
parse_one它调用unknown_bootoption函数处理__setup定义的参数,unknown_bootoption函数在init/main.c中定义如下:
static int __init unknown_bootoption(char *param, char *val)
{
/* Change NUL term back to "=", to make "param" the whole string. */
if (val) {
/* param=val or param="val"? */
if (val == param+strlen(param)+1)
val[-1] = '=';
else if (val == param+strlen(param)+2) {
val[-2] = '=';
memmove(val-1, val, strlen(val)+1);
val--;
} else
BUG();
}
/* Handle obsolete-style parameters */
if (obsolete_checksetup(param))
return 0;
/* Unused module parameter. */
if (strchr(param, '.') && (!val || strchr(param, '.') < val))
return 0;
if (panic_later)
return 0;
if (val) {
/* Environment option */
unsigned int i;
for (i = 0; envp_init[i]; i++) {
if (i == MAX_INIT_ENVS) {
panic_later = "Too many boot env vars at `%s'";
panic_param = param;
}
if (!strncmp(param, envp_init[i], val - param))
break;
}
envp_init[i] = param;
} else {
/* Command line option */
unsigned int i;
for (i = 0; argv_init[i]; i++) {
if (i == MAX_INIT_ARGS) {
panic_later = "Too many boot init vars at `%s'";
panic_param = param;
}
}
argv_init[i] = param;
}
return 0;
}
在这个函数中它调用了obsolete_checksetup函数,该函数也定义在init/main.c中
static int __init obsolete_checksetup(char *line)
{
const struct obs_kernel_param *p;
int had_early_param = 0;
p = __setup_start;
do {
int n = strlen(p->str);
if (!strncmp(line, p->str, n)) {
if (p->early) {
/* Already done in parse_early_param?
* (Needs exact match on param part).
* Keep iterating, as we can have early
* params and __setups of same names 8( */
if (line[n] == '\0' || line[n] == '=')
had_early_param = 1;
} else if (!p->setup_func) {
printk(KERN_WARNING "Parameter %s is obsolete,"
" ignored\n", p->str);
return 1;
} else if (p->setup_func(line + n)) //调用支持函数
return 1;
}
p++;
} while (p < __setup_end);
return had_early_param;
}
在这里parse_early_param()主要的作用是处理内核命令行(boot_command_line)的内核参数。也就是处理在内核命令行中有定义的早期参数值(early=1),特别的还包括内核参数console和earlycon。都和输出流有关,内核启动时的打印信息就要求该设备的正确配置。
总结一下:
内核分三类参数的传递与设置
1、内置模块的参数设置
2、高优先级命令行参数设置
3、一般命令行参数设置
三种参娄的设置都由parse_args函数来实现,对第一种内置模块的参数设置在parse_args函数中调用parse_one函数遍历__start___param和__stop___param,如果查找到对应的key,就调用相关参数设置函数处理。
对第二种高优先级命令行参数设置通过parse_args函数传递调用函数do_early_param到parse_one函数中由do_early_param实现遍历obs_kernel_param数组(__setup_start,__setup_end),首先看是否设置early,如果有设置并查找到对应的key,就调用相关early_param函数处理。
对第三种一般命令行参数设置通过parse_args函数传递调用函数unknown_bootoption到parse_one函数中由unknown_bootoption实现遍历 obs_kernel_param数组(__setup_start,__setup_end),首先看是否设置early,如果查找到对应的key,就调用相关__setup函数处理。
对2.6.36以前版本没用 parse_early_param而是用parse_cmdline函数,我们来看看这个函数
static void __init parse_cmdline(char **cmdline_p, char *from)
{
char c = ' ', *to = command_line;
int len = 0;
for (;;) {
if (c == ' ') {
//寻找c=空格 的条件,空格表示一个新的命令行选项,假设:mem=xxx noinitrd root=yyy init=/linuxrc console=ttySAC0,这个扫描是一个一个的扫描命令行里的参数的。
extern struct early_params __early_begin, __early_end; //这些变量在lds中
struct early_params *p;
for (p = &__early_begin; p < &__early_end; p++) { //扫描这个区间的所有early_params结构。
int len = strlen(p->arg); //测量这个字符串的长度。比如"mem="长度是4
if (memcmp(from, p->arg, len) == 0) { //这里也pass,这里不pass就意味着不能解析。
if (to != command_line) //防止得到两个空格
to -= 1;
from += len; //跳过这个选项,得到具体数据,现在from指向“xxx noinitrd...“
p->fn(&from); //调用这个函数处理这个xxx
while (*from != ' ' && *from != '\0') //跳过xxx部分,因为这是mem=xxx已经处理完了,可以扔掉了。
from++;
break; // 终止这次处理,针对mem=xxx的处理,现在from指向“ noinitrd ...“,注意最前面的空格。
}
}
}
c = *from++; //这次c又得到的是空格。第2次,取到的是noinitrd的n
if (!c) //如果到了uboot命令行参数的结尾,或者 命令行参数太长,都会结束扫描
break;
if (COMMAND_LINE_SIZE <= ++len)
break;
*to++ = c; //保存空格,第2此保存了n,依次类推。
}
*to = '\0';
*cmdline_p = command_line; //给cmdline_p赋值,这个指针是start_kernel里的。
}
struct early_params {
const char *arg; //字符串指针
void (*fn)(char **p); //私有的函数
};
在System.map中
c0027cdc T __early_begin
c0027cdc t __early_early_mem
c0027cdc T __setup_end
c0027ce4 t __early_early_initrd
c0027cec t __early_early_vmalloc
c0027cf4 t __early_early_ecc
c0027cfc t __early_early_nowrite
c0027d04 t __early_early_nocache
c0027d0c t __early_early_cachepolicy
c0027d14 t __early_uart_parent_setup
c0027d1c t __early_jtag_wfi_setup
c0027d24 t __early_system_rev_setup
c0027d2c T __early_end
比如arch/arm/kernel/setup.c中
static void __init early_mem(char **p)
{
static int usermem __initdata = 0;
unsigned long size, start;
/*
* If the user specifies memory size, we
* blow away any automatically generated
* size.
*/
if (usermem == 0) {
usermem = 1;
meminfo.nr_banks = 0;
}
start = PHYS_OFFSET;
size = memparse(*p, p);
if (**p == '@')
start = memparse(*p + 1, p);
arm_add_memory(start, size);
}
__early_param("mem=", early_mem);
可以发现能够在这里处理的命令行参数有mem initrd ecc cachepolicy nowrite nocache这六个。
parse_cmdline做了三件事,首先它解析了from所指向的完整的内核参数中关于内存的部分,其次它将没有解析的部分复制到command_line中,最后它将start_kernel()传进来的内核参数指针指向command_line。
内核参数中的将会被parse_cmdline解析,并根据结果设置meminfo,而其余部分则被复制到command_line中就是说,能解析的都解析了,不能解析的留下来,等着以后解析,保存在command_line中,可以发现uboot的命令行参数具有最高的优先级,如果指定的话,将覆盖掉标记列表的mem32配置,如果多次使用mem= mem=的话,将得到多个内存bank,通过代码来看是这样,没有验证过。
最后。通过上面的分析,我们可以自定义通过命令行传入一个参数设置,这里以uboot向内核传递MAC地址为例说明添加过程
我们使用的系统中的CS8900a没有外接eeprom,所以在默认的情况,Linux下的CS8900a的驱动使用的是一个伪MAC地址。在单一的系统中,这是没有问题的,但是当我们在同一个子网中使用或测试多个设备是,就会产生冲突了。所以我们需要能够方便的改变网卡的MAC地址,而不是将MAC地址硬编码进内核中,每次修改都得重新编译内核。
一、添加内核参数的处理函数
向Linux驱动传递参数的方式有两种,一为在系统启动的时候由bootloader传入,还有一种是将驱动编译成模块,将参数作为模块加载的参数传入。
这里使用的是由bootloader传入的方式。内核通过setup接口接受Bootloader传入的参数。方式如下:
static int __init param_mac_setup(char *str)
{
……
}
__setup("mac=", param_mac_setup);
这样,当在Bootloader中指定“mac=00:2E:79:38:6D:4E”,系统在加载这个模块的时候,就会执行相应的param_mac_setup()函数,而传入给它的参数就是等号后面的物理地址 “00:2E:79:38:6D:4E”。这样,该函数就可以对它进行相应的处理。
二、将MAC地址参数传入命令行参数
为了传入命令行参数,uboot所作的是:
char *commandline = getenv ("bootargs");
setup_commandline_tag (bd, commandline);
现在想要把MAC地址也加入到命令行参数中,只需要修改配置文件include/configs/smdk2410.h中的CONFIG_BOOTARGS:
#define CONFIG_BOOTARGS "root=ramfs devfs=mount console=ttySA0,9600"
在后面添加mac参数如下:
#define CONFIG_BOOTARGS "root=ramfs devfs=mount console=ttySA0,9600 mac=00:2E:79:38:6D:4E"
这样就不需要每次在命令行后面手工追加MAC地址参数了