你了解u-boot与linux内核间的参数传递过程?

U-boot会给     Linux   Kernel传递很多参数,如:串口,     RAM   ,videofb、MAC地址等。而Linux kernel也会读取和处理这些参数。两者之间通过struct tag来传递参数。U-boot把要传递给kernel的东西保存在struct tag数据结构中,启动kernel时,把这个结构体的物理地址传给kernel;Linux kernel通过这个地址,用pa     rs   e_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    baudra     te   ;
unsigned long    have_console;    /* serial_init() was called */
unsigned long    reloc_off;    /* Reloca     ti   on 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 ini  TI alized    */
#define    GD_FLG_SILENT    0x00004        /* Silent mode                */

#define DECLARE_GLOBAL_DATA_PTR     register vola  TI le gd_t *gd asm ("r8")
在global_data.h中U-Boot使用了一个存储在     寄存器   中的指针gd来记录全局数据区的地址:
#define DECLARE_GLOBAL_DATA_PTR     register vola  TI le 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)

 你了解u_boot与linux内核间的参数传递过程?_设计制作_测量仪表

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_par     ams   ;    /* 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 r     amd   isk_size;        /*  8 */
unsigned long flags;        /* 12 */
#define FLAG_READONLY    1
#define FLAG_RDLOAD    4
#define FLAG_RDP     ROM   PT    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 m     emc   _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_     vr   am;    /* 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__((__sec  TI on__(".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         /* com     pi   ler opti     mi   zation 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 le     adi   ng 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/m     ai   n.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) _     eda   ta;
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_I     PAD   DR
"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

45
135
0
32

相关资讯

  1. 1、从紧急起飞到敢达争锋对决高达动作游戏编年史4523
  2. 2、《纯情女仆坏少爷》终极境地来袭为了自由而战2419
  3. 3、《口袋妖怪复刻》新版下周上线更多新宠新玩法2468
  4. 4、《天天打波利》「神格」面世来自众神之力的加持2702
  5. 5、冰火魔厨手游特色内容介绍深度还原小说世界726
  6. 6、《最佳阵容》私人助教超神转生带你走向巅峰273
  7. 7、百变神器立体换装《暴风城之怒》神器系统介绍1340
  8. 8、《大掌门》萌猴闹新春海量福利庆佳节4251
  9. 9、主播彩彩力荐《魔灵幻想》视频首曝12.17日萌动公测5001
  10. 10、《率土之滨》超精美人设来袭历史名将各领风骚2980
全部评论(0)
我也有话说
0
收藏
点赞
顶部