《uboot与linux内核间的参数传递过程分析》
1.内核中对boot loader描述
(源码位于kernel中的Documentation/arm/booting)
4. Setup boot data ------------------ Existing boot loaders: OPTIONAL, HIGHLY RECOMMENDED New boot loaders: MANDATORY The boot loader must provide either a tagged list or a dtb image for passing configuration data to the kernel. The physical address of the boot data is passed to the kernel in register r2. 4a. Setup the kernel tagged list -------------------------------- The boot loader must create and initialise the kernel tagged list. A valid tagged list starts with ATAG_CORE and ends with ATAG_NONE. The ATAG_CORE tag may or may not be empty. An empty ATAG_CORE tag has the size field set to '2' (0x00000002). The ATAG_NONE must set the size field to zero. Any number of tags can be placed in the list. It is undefined whether a repeated tag appends to the information carried by the previous tag, or whether it replaces the information in its entirety; some tags behave as the former, others the latter. The boot loader must pass at a minimum the size and location of the system memory, and root filesystem location(bootloader必须传递一个系统内存的位置和最小值,以及根文件系统位置).
Therefore, the minimum tagged list should look: +-----------+ base -> | ATAG_CORE | | +-----------+ | | ATAG_MEM | | increasing address +-----------+ | | ATAG_NONE | | +-----------+ v The tagged list should be stored in system RAM. The tagged list must be placed in a region of memory where neither the kernel decompressor nor initrd 'bootp' program will overwrite it. The recommended placement is in the first 16KiB of RAM.
linux2.4x以后的内核都期望以标记列表(tagged list)的形式来传递启动参数,标记是一种数据结构;标记列表就是挨着存放的多个标记。标记列表以标记ATAG_CORE开始,以标记ATAG_NONE结束。uboot会给Linux Kernel传递很多参数,如:串口,RAM,videofb、MAC地址等。而Linux kernel也会读取和处理这些参数。两者之间通过struct tag来传递参数。uboot把要传递给kernel的东西保存在struct tag数据结构中,启动kernel时,把这个结构体的物理地址传给kernel;Linux kernel通过这个地址,用parse_tags分析出传递过来的参数。
标记的数据结构为tag,它由一个tag_header结构和一个联合(union)组成。Tag_header结构表示标记的类型及长度。对于不同类型的标记使用不同的联合,比如表示内存时使用tag_mem32,表示命令行时使用tag_cmdline。
数据结构tag和tag_header定义在linux(uboot)源码的arch/arm/include/asm/setup.h, uboot的定义是从内核中拷贝过来的,要和内核一致的。
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; //command line字符串标签,我们平时设置的启动参数cmdline字符串(uboot中为bootargs环境变量)就放在这个标签中。 /* * Acorn specific */ struct tag_acorn acorn; /* * DC21285 specific */ struct tag_memclk memclk; } u; };
struct tag_header {
u32 size; //标签的大小,包括header本身
u32 tag; //tag的类型
};
2.参数从uboot到特定的内存地址
2.1 使用uboot来启动一个Linux内核,通常情况下我们会按照如下步骤执行:
- 设置内核启动的command line,也就是设置uboot的环境变量“bootargs”(非必须,如果你要传递给内核cmdline才要设置)
- 加载内核映像文到内存指定位置(从SD卡、u盘、网络或flash),linux内核一定要加载到内存的。
- 使用“bootm (内核映像基址)”命令来启动内核
而uboot将参数按照协议处理好并放入指定内存地址的过程就发生在“bootm”命令中。
/*******************************************************************/ /* bootm - boot application image from image in memory */ /*******************************************************************/ int do_bootm (cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[]) { ulong iflag; ulong load_end = 0; int ret; boot_os_fn *boot_fn; #ifdef CONFIG_SECURE_BOOT #ifndef CONFIG_SECURE_BL1_ONLY security_check(); #endif #endif #ifdef CONFIG_ZIMAGE_BOOT #define LINUX_ZIMAGE_MAGIC 0x016f2818 image_header_t *hdr; ulong addr; /* find out kernel image address */ if (argc < 2) { addr = load_addr; debug ("* kernel: default image load address = 0x%08lx\n", load_addr); } else { addr = simple_strtoul(argv[1], NULL, 16); } if (*(ulong *)(addr + 9*4) == LINUX_ZIMAGE_MAGIC) { u32 val; printf("Boot with zImage\n"); //addr = virt_to_phys(addr); hdr = (image_header_t *)addr; hdr->ih_os = IH_OS_LINUX; hdr->ih_ep = ntohl(addr); memmove (&images.legacy_hdr_os_copy, hdr, sizeof(image_header_t)); /* save pointer to image header */ images.legacy_hdr_os = hdr; images.legacy_hdr_valid = 1; goto after_header_check; } #endif #ifdef CONFIG_NEEDS_MANUAL_RELOC static int relocated = 0; /* relocate boot function table */ if (!relocated) { int i; for (i = 0; i < ARRAY_SIZE(boot_os); i++) if (boot_os[i] != NULL) boot_os[i] += gd->reloc_off; relocated = 1; } #endif /* determine if we have a sub command */ if (argc > 1) { char *endp; simple_strtoul(argv[1], &endp, 16); /* endp pointing to NULL means that argv[1] was just a * valid number, pass it along to the normal bootm processing * * If endp is ':' or '#' assume a FIT identifier so pass * along for normal processing. * * Right now we assume the first arg should never be '-' */ if ((*endp != 0) && (*endp != ':') && (*endp != '#')) return do_bootm_subcommand(cmdtp, flag, argc, argv); } if (bootm_start(cmdtp, flag, argc, argv)) return 1; /* * We have reached the point of no return: we are going to * overwrite all exception vector code, so we cannot easily * recover from any failures any more... */ iflag = disable_interrupts(); #if defined(CONFIG_CMD_USB) /* * turn off USB to prevent the host controller from writing to the * SDRAM while Linux is booting. This could happen (at least for OHCI * controller), because the HCCA (Host Controller Communication Area) * lies within the SDRAM and the host controller writes continously to * this area (as busmaster!). The HccaFrameNumber is for example * updated every 1 ms within the HCCA structure in SDRAM! For more * details see the OpenHCI specification. */ usb_stop(); #endif ret = bootm_load_os(images.os, &load_end, 1); if (ret < 0) { if (ret == BOOTM_ERR_RESET) do_reset (cmdtp, flag, argc, argv); if (ret == BOOTM_ERR_OVERLAP) { if (images.legacy_hdr_valid) { if (image_get_type (&images.legacy_hdr_os_copy) == IH_TYPE_MULTI) puts ("WARNING: legacy format multi component " "image overwritten\n"); } else { puts ("ERROR: new format image overwritten - " "must RESET the board to recover\n"); show_boot_progress (-113); do_reset (cmdtp, flag, argc, argv); } } if (ret == BOOTM_ERR_UNIMPLEMENTED) { if (iflag) enable_interrupts(); show_boot_progress (-7); return 1; } } lmb_reserve(&images.lmb, images.os.load, (load_end - images.os.load)); if (images.os.type == IH_TYPE_STANDALONE) { if (iflag) enable_interrupts(); /* This may return when 'autostart' is 'no' */ bootm_start_standalone(iflag, argc, argv); return 0; } show_boot_progress (8); #if defined(CONFIG_ZIMAGE_BOOT) after_header_check: images.os.os = hdr->ih_os; images.ep = image_get_ep (&images.legacy_hdr_os_copy); #endif #ifdef CONFIG_SILENT_CONSOLE if (images.os.os == IH_OS_LINUX) fixup_silent_linux(); #endif boot_fn = boot_os[images.os.os]; if (boot_fn == NULL) { if (iflag) enable_interrupts(); printf ("ERROR: booting os '%s' (%d) is not supported\n", genimg_get_os_name(images.os.os), images.os.os); show_boot_progress (-8); return 1; } arch_preboot_os(); boot_fn(0, argc, argv, &images); show_boot_progress (-9); #ifdef DEBUG puts ("\n## Control returned to monitor - resetting...\n"); #endif do_reset (cmdtp, flag, argc, argv); return 1; }
do_bootm()中最重要的就是bootm_start(),这是bootm主要功能的开始。
1 static int bootm_start(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[]) 2 { 3 void *os_hdr; 4 int ret; 5 6 memset ((void *)&images, 0, sizeof (images)); 7 images.verify = getenv_yesno ("verify"); 8 9 bootm_start_lmb(); 10 11 /* get kernel image header, start address and length */ 12 os_hdr = boot_get_kernel (cmdtp, flag, argc, argv, 13 &images, &images.os.image_start, &images.os.image_len); 14 if (images.os.image_len == 0) { 15 puts ("ERROR: can't get kernel image!\n"); 16 return 1; 17 } 18 19 /* get image parameters */ 20 switch (genimg_get_format (os_hdr)) { 21 case IMAGE_FORMAT_LEGACY: 22 images.os.type = image_get_type (os_hdr); 23 images.os.comp = image_get_comp (os_hdr); 24 images.os.os = image_get_os (os_hdr); 25 26 images.os.end = image_get_image_end (os_hdr); 27 images.os.load = image_get_load (os_hdr); 28 break; 29 #if defined(CONFIG_FIT) 30 case IMAGE_FORMAT_FIT: 31 if (fit_image_get_type (images.fit_hdr_os, 32 images.fit_noffset_os, &images.os.type)) { 33 puts ("Can't get image type!\n"); 34 show_boot_progress (-109); 35 return 1; 36 } 37 38 if (fit_image_get_comp (images.fit_hdr_os, 39 images.fit_noffset_os, &images.os.comp)) { 40 puts ("Can't get image compression!\n"); 41 show_boot_progress (-110); 42 return 1; 43 } 44 45 if (fit_image_get_os (images.fit_hdr_os, 46 images.fit_noffset_os, &images.os.os)) { 47 puts ("Can't get image OS!\n"); 48 show_boot_progress (-111); 49 return 1; 50 } 51 52 images.os.end = fit_get_end (images.fit_hdr_os); 53 54 if (fit_image_get_load (images.fit_hdr_os, images.fit_noffset_os, 55 &images.os.load)) { 56 puts ("Can't get image load address!\n"); 57 show_boot_progress (-112); 58 return 1; 59 } 60 break; 61 #endif 62 default: 63 puts ("ERROR: unknown image format type!\n"); 64 return 1; 65 } 66 67 /* find kernel entry point */ 68 if (images.legacy_hdr_valid) { 69 images.ep = image_get_ep (&images.legacy_hdr_os_copy); 70 #if defined(CONFIG_FIT) 71 } else if (images.fit_uname_os) { 72 ret = fit_image_get_entry (images.fit_hdr_os, 73 images.fit_noffset_os, &images.ep); 74 if (ret) { 75 puts ("Can't get entry point property!\n"); 76 return 1; 77 } 78 #endif 79 } else { 80 puts ("Could not find kernel entry point!\n"); 81 return 1; 82 } 83 84 if (((images.os.type == IH_TYPE_KERNEL) || 85 (images.os.type == IH_TYPE_MULTI)) && 86 (images.os.os == IH_OS_LINUX)) { 87 /* find ramdisk */ 88 ret = boot_get_ramdisk (argc, argv, &images, IH_INITRD_ARCH, 89 &images.rd_start, &images.rd_end); 90 if (ret) { 91 puts ("Ramdisk image is corrupt or invalid\n"); 92 return 1; 93 } 94 95 #if defined(CONFIG_OF_LIBFDT) 96 /* find flattened device tree */ 97 ret = boot_get_fdt (flag, argc, argv, &images, 98 &images.ft_addr, &images.ft_len); 99 if (ret) { 100 puts ("Could not find a valid device tree\n"); 101 return 1; 102 } 103 104 set_working_fdt_addr(images.ft_addr); 105 #endif 106 } 107 108 images.os.start = (ulong)os_hdr; 109 images.state = BOOTM_STATE_START; 110 111 return 0; 112 }
bootm_start()作用:
- (boot_get_kernel())获取内核uImage的文件头(也就是在用uboot的mkiamge工具处理内核zImage时添加的那64Byte的数据),以及开始地址和长度
- 获取内核映像的参数
- 找到内核入口点
os_hdr = boot_get_kernel (cmdtp, flag, argc, argv, &images, &images.os.image_start, &images.os.image_len);
static bootm_headers_t images; /* pointers to os/initrd/fdt images */
image_header_t *legacy_hdr_os; /* image header pointer */
image_header_t legacy_hdr_os_copy; /* header copy */
ulong legacy_hdr_valid;
image_info_t os; /* os image info */
/* 略 */
ulong start, end; /* start/end of blob */
ulong image_start, image_len; /* start of image within blob, len of image */
ulong load; /* load addr for the image */
uint8_t comp, type, os; /* compression, type of image, os type */
} image_info_t;
平常我们在用bootm驱动内核的时候所看到的如下信息:
## Booting kernel from Legacy Image at 50008000 ...
Image Name: Linux-2.6.37.1
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 3800644 Bytes = 3.6 MiB
Load Address: 50008000
Entry Point: 50008040
Verifying Checksum ... OK
就是这个函数所调用的 boot_get_kernel函数及其子函数根据uImage的文件头打印出来的。
注意此时linux内核已经处于内存之中了。核对并显示出其中包含的信息,并填充一个全局的static bootm_headers_timages结构体的image_info_tos域:
接下来分析do_bootm()中调用的bootm_load_os()函数。
这个函数的作用是通过获取的文件头信息,将文件头后面所跟的内核映像放置到文件头信息规定的内存加载地址(如果是压缩内核,还在此函数中解压。但这个和zImage压缩内核不是一个概念,不要混淆)。
平常我们在用bootm驱动内核的时候所看到的如下信息:
XIPKernel Image ... OK
OK
就是这个函数打印出来的。
do_bootm()中的boot_fn = boot_os[images.os.os]
其功能是根据全局staticbootm_headers_t images结构体的image_info_t os域中记录的os类型来将一个特定OS的内核引导函数入口赋给boot_fn变量。比如我引导的是Linux内核,那么boot_fn就是do_bootm_linux。
do_bootm()中的boot_fn(0, argc, argv, &images)
如果不出错的话,这个函数应该是不会在返回了,因为在这个函数中会将控制权交由OS的内核。对于引导Linux内核来说,这里其实就是调用do_bootm_linux。
综上所述,之前所做的动作是解析uImage的前64 Byte信息,解析出kernel的一系列信息并填充在一个数据结构中,接着根据上述结构的信息将kernel加载到kernel的内存加载地址,再根据kernel的类型,调用相应的boot_fn函数,这里离应该就是do_bootm_linux()。
2.2 分析do_bootm_linux()
do_bootm_linux()函数在uboot源码目录arch/arm/lib/bootm.c中
int do_bootm_linux(int flag, int argc, char *argv[], bootm_headers_t *images) { bd_t *bd = gd->bd; char *s; int machid = bd->bi_arch_number; void (*kernel_entry)(int zero, int arch, uint params); int ret; #ifdef CONFIG_CMDLINE_TAG char *commandline = getenv ("bootargs"); //这是从uboot的环境中获取的boogargs。 #endif
这里获取了生成cmdline标签所需要的字符串,相对于其他操作系统,这是与linux相关的。
if ((flag != 0) && (flag != BOOTM_STATE_OS_GO)) return 1; s = getenv ("machid"); //这是从uboot的环境中获取的machid。 if (s) { machid = simple_strtoul (s, NULL, 16); printf ("Using machid 0x%x from environment\n", machid); } 注意:这里设备ID号可以从环境变量中获得!如果环境变量中有,就会覆盖之前赋值过的设备ID(最终通过r1传递给内核)。
ret = boot_get_ramdisk(argc, argv, images, IH_ARCH_ARM, &(images->rd_start), &(images->rd_end)); if(ret) printf("[err] boot_get_ramdisk\n"); show_boot_progress (15); #ifdef CONFIG_OF_LIBFDT //支持FDT会执行这里。 if (images->ft_len) return bootm_linux_fdt(machid, images); #endif kernel_entry = (void (*)(int, int, uint))images->ep; //linux的入口点,为某个物理地址。 这里让函数指针指向内核映像的入口物理地址
debug ("## Transferring control to Linux (at address %08lx) ...\n", (ulong) kernel_entry);
以上代码主要获取了三个值:commandline,machid,内核入口地址kernel_entry。
以下就是我们一直在找的内核标签列表生成代码,在这里uboot会为linux建立内核标签列表,从这里看出:U-boot原生只支持部分标签。当然,如果要加的话也很简单。
#if defined (CONFIG_SETUP_MEMORY_TAGS) || \ defined (CONFIG_CMDLINE_TAG) || \ defined (CONFIG_INITRD_TAG) || \ defined (CONFIG_SERIAL_TAG) || \ defined (CONFIG_REVISION_TAG) setup_start_tag (bd); #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); //设置ATAG_MEM,依赖于uboot的全局变量bd->bi_dram[i]中的数据 #endif #ifdef CONFIG_CMDLINE_TAG setup_commandline_tag (bd, commandline); //设置ATAG_CMDLINE,依赖上面的字符串commandline中的数据 #endif #ifdef CONFIG_INITRD_TAG if (images->rd_start && images->rd_end) setup_initrd_tag (bd, images->rd_start, images->rd_end); #endif setup_end_tag(bd); //设置ATAG_NONE #endif announce_and_cleanup(); 在进入内核前配置好芯片状态,以符合内核启动要求。
主要是关闭和清理缓存
#ifdef CONFIG_ENABLE_MMU theLastJump((void *)virt_to_phys(kernel_entry), machid, bd->bi_boot_params); #else kernel_entry(0, machid, bd->bi_boot_params); /* does not return */
跳入内核入口地址:r1=0、r1=machid、r2=启动参数指针
#endif return 1; }
分析announce_and_cleanup()函数
static void announce_and_cleanup(void) { printf("\nStarting kernel ...\n\n"); #ifdef CONFIG_USB_DEVICE { extern void udc_disconnect(void); udc_disconnect(); } #endif cleanup_before_linux(); }
3. 调用内核映像
-----------------------------------------
现有的引导加载程序: 强制
新开发的引导加载程序: 强制
调用内核映像zImage有两个选择。如果zImge是保存在flash中的,且其为了在flash中直接运行而被正确链接。这样引导加载程序就可以在flash中直接调用zImage。
zImage也可以被放在系统RAM(任意位置)中被调用。注意:内核使用映像基地址的前16KB RAM空间来保存页表。建议将映像置于RAM的32KB处。
对于以上任意一种情况,都必须符合以下启动状态:
Ø 停止所有DMA设备,这样内存数据就不会因为虚假网络包或磁盘数据而被破坏。这可能可以节省你许多的调试时间。
Ø CPU 寄存器配置
r0 = 0,
r1 = (在上面 (3) 中获取的)机器类型码.
r2 = 标签列表在系统RAM中的物理地址,或
设备树块(dtb)在系统RAM中的物理地址
Ø CPU 模式
所有形式的中断必须被禁止 (IRQs 和 FIQs)
CPU 必须处于 SVC 模式。 (对于 Angel 调试有特例存在)
Ø 缓存, MMUs
MMU 必须关闭。
指令缓存开启或关闭都可以。
数据缓存必须关闭。
Ø 引导加载程序应该通过直接跳转到内核映像的第一条指令来调用内核映像。
4. 标签生成的函数举例分析: //理解如何往内存中写入ATAG参数的
所有标签生成函数都在arch/arm/lib/bootm.c文件中,其实原理很简单,就是直接往指定的内存地址中写入标签信息。以下以setup_start_tag和setup_memory_tags为例分析:
1 static void setup_start_tag (bd_t *bd) 2 { 3 params = (struct tag *) bd->bi_boot_params; //params指向内存中标签列表中的基地址 4 //直接往内存中按照内核定义的标签结构写入信息 5 params->hdr.tag = ATAG_CORE; 6 params->hdr.size = tag_size (tag_core); 7 8 params->u.core.flags = 0; 9 params->u.core.pagesize = 0; 10 params->u.core.rootdev = 0; 11 12 params = tag_next (params);//根据本标签的大小数据,params跳到下一标签的起始地址 13 }
1 #ifdef CONFIG_SETUP_MEMORY_TAGS 2 int nr_dram_banks = -1; 3 static void setup_memory_tags (bd_t *bd) 4 { 5 int i; 6
//上一个标签已经将params指向了下一标签的基地址,所以这里可以直接使用
7 for (i = 0; i < nr_dram_banks; i++) { 8 params->hdr.tag = ATAG_MEM; 9 params->hdr.size = tag_size (tag_mem32); 10 //根据配置信息和uboot全局变量中的信息创建标签数据 11 params->u.mem.start = bd->bi_dram[i].start; 12 params->u.mem.size = bd->bi_dram[i].size; 13 14 params = tag_next (params);//根据本标签的大小数据,params跳到下一标签的起始地址 15 } 16 } 17 #endif /* CONFIG_SETUP_MEMORY_TAGS */
bootloader完成了引导Linux内核所需要的准备之后将通过直接跳转,将控制权交由内核zImage。
以上是Uboot将参数放入内存的过程,下面介绍linux如何从内存中获取这些参数并解析这些参数。
5.内核从特定内存获取参数
start_kernel-->setup_arch(&command_line)-->mdesc= setup_machine_tags(machine_arch_type);