U-Boot启动代码分析(MIPS)U-Boot代码分析,CPU是MIPS架构的 1) 史前时代:汇编在FLASH中运行的日子(汇编指令参见《See MIPS Run》一书):
U-Boot的开始执行始于用汇编语言编写的CPU依赖的程序,程序是从cpu/mips/start.S文件中的_start代码段开始执行的。由于 此时DRAM未初始化,所以程序是从存储U-Boot程序的FLASH中开始运行的。下面就从_start开始代码之旅。 /***************************************************************************************/ 程序一开始就出现了一大片令人迷惑的代码: _start: RVECENT(reset,0) /* U-boot entry point */ RVECENT(reset,1) /* software reboot */ RVECENT(romReserved,3) RVECENT(romReserved,4) …… /* Reserve extra space so that when we use the boot bus local memory ** segment to remap the debug exception vector we don't overwrite ** anything useful */ …… 而宏RVECENT的定义为: #define RVECENT(f,n) \ b f; nop 可见该指令只是一个简单的跳转指令b Label。 而romReserved代码为: romReserved: b romReserved nop …… 可见是没有意义的死循环代码。 再结合注释,原来程序开始的一大片令人迷惑的代码的作用如下: _start: RVECENT(reset,0) /* U-boot entry point */ /*U-Boot开始执行的代码起始地址*/ RVECENT(reset,1) /* software reboot */ /*软重启时U-Boot开始执行的起始地址*/ RVECENT(romReserved,3) /*保留本代码所在的地址,重新映射调试异常向量时可以使用该空间*/ RVECENT(romReserved,4) /*同上……*/ …… /***************************************************************************************/ 接着reset段的代码往下看: 首先是一些COP0的状态寄存器的设置:讲COP0_STATUS_REG寄存器的5-7三个bit置1。结合CPU手册可以看到三个bit的含义。 然后是调试模式下的GPIO初始化,然后是检查是否使用FAILSAFE模式加载BootLoader,接着才真正开始CPU初始化。 当看到一段注释时: /* Check what core we are - if core 0, branch to init tlb ** loop in flash. Otherwise, look up address of init tlb ** loop that was saved in the boot vector block. */ 可以发现下面这段对每个core的TLB(Translation Lookaside Buffer)进行初始化时,是对core0与其他cores的TLB初始化有区别的:如果是core0,由于DRAM没有初始化,代码只能继续在 FLASH中执行;而如果是其他cores,则可以直接调转到DRAM中相应的这段代码的地址进行TLB初始化。 接着下面的代码可以连续看到两个Label: .globl InitTLBStart InitTLBStart: InitTLBStart_local: 第一个Label是为了将下面的代码拷贝到DRAM后可以直接在C语言中用函数的方式调用,第二个Label是为了core0中执行TLB初始化时跳转。 从下面的注释中可以证实这一点: /* This code run on all cores - core 0 from flash, ** the rest from DRAM. When booting from PCI, non-zero cores ** come directly here from the boot vector - no earlier code in this ** file is executed. */ /* Some generic initialization is done here as well, as we need this done on ** all cores even when booting from PCI */ 对TLB初始化的代码中使用了很多mfc0与mtc0指令,可见是对一些COP0的寄存器的读写。 接着往下又是一些COP0的状态寄存器的设置,设置scratch memory等。 /***************************************************************************************/ 再往下可以看到一段注释: /* Check if we are core 0, if we are not then we need ** to vector to code in DRAM to do application setup, and ** skip the rest of the bootloader. Only core 0 runs the bootloader ** and sets up the tables that the other cores will use for configuration */ 可见以下的代码执行在不同的core上开始出现不同:core0继续往下执行汇编代码;而如果是其余cores,则从内存中找到BOOT_VECTOR_BASE地址,直接跳入内存执行应用程序的初始化。 假设当前仍是core0,继续往下看。看到注释: /* If we don't have working memory yet configure a bunch of ** scratch memory, and set the stack pointer to the top ** of it. This allows us to go to C code without having ** memory set up */ 可见如果内存还没有初始化,这里首先初始化一块临时内存作为栈空间,这使得程序可以在内存初始化之前用来调用C程序。 /***************************************************************************************/ 再往下是: /* Initialize GOT pointer. ** Global symbols can't be resolved before this is done, and as such we can't ** use any global symbols in this code. We use the bal/ move xxx,ra combination to access ** data in a PC relative manner to avoid this. This code will correctly set the ** gp regardless of whether the code has already been relocated or not. ** This code determines the current gp by computing the link time (gp - pc) ** and adding this to the current pc. ** runtime_gp = runtime_pc + (linktime_gp - linktime_pc) ** U-boot is running from the address it is linked at at this time, so this ** general case code is not strictly necessary here. */ 其中,GOT=Global Offset Table,GP=GOT pointer,PC=Program counter。可见程序开始为调用其他汇编文件中定义的函数或C程序中定义的函数进行准备而建立符号表指针(GOT pointer)。 /***************************************************************************************/ 初始化完GOT pointer后,接着往下就可以调用其他汇编文件中定义的函数(或代码段),可以看到初始化内存、缓存的代码: /* Initialize any external memory. */ jal memsetup /*memsetup是定义在board/tb0229/文件夹下的memsetup.S中的代码段*/ nop /* Initialize caches... */ sync cache 0, 0($0) sync cache 9, 0($0) sync jal mips_cache_reset /*mips_cache_reset也是定义在其他文件中的代码段*/ nop /* ... and enable them. */ li t0, CONF_CM_CACHABLE_NONCOHERENT mtc0 t0, CP0_CONFIG /* Set up temporary stack. */ li a0, CFG_INIT_SP_OFFSET jal mips_cache_lock /*mips_cache_lock同样是定义在其他文件中的代码段*/ nop 这段代码主要是调用依赖某个板子的对memory进行参数设置、对cache进行初始化的代码,借以完成对某个板子的内存、缓存初始化。 /***************************************************************************************/ 接着再往下可以看到代码: la t9, board_init_f /* doesn't return... */ /*board_init_f是定义在lib_mips/board.c中的C函数*/ j t9 nop 这里开始转到board_init_f代码段开始执行程序,board_init_f实质上是C语言中定义的函数,虽然后面的代码仍在flash中存 放,但是已经可以使用一部分scratch memory作为临时栈空间进行函数调用,可以用C语言进行批量初始化了,纯汇编的时代暂时告一段落。 2) 石器时代:FLASH中的C代码在临时栈空间中活跃: 这部分的代码的使命是致力于建立一个“正常”的C运行环境,主要是内存的初始化及整个寻址空间的部分初始化。而这部分代码本身所运行的环境受到较多限制,只有一个大小受限的scratch memory作为临时运行的栈空间。 /***************************************************************************************/ board_init_f()函数一开始出现一个宏,DECLARE_GLOBAL_DATA_PTR,查看该宏的定义(include/asm-mips/Global_data.h): #define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("k0") 结合注释可以了解到,这些关于系统信息的结构体(GD是指Global Data, BD是指Board info Data)应该存放于在DRAM控制器未初始化之前就能使用的空间中,比如锁定的缓存中。在这里我们可以暂时把它放在已经初始化好的临时栈空间 scratch memory中。 GD和BD是很重要的结构体,后面当DRAM初始化完成后,会将其拷贝入DRAM空间保存。 /***************************************************************************************/ 接着往下是循环调用init_sequence函数指针数组中的成员,来依次调用数组列表中的函数进行初始化。 init_sequence的定义如下(将部分预编译指令去掉后的代码): init_fnc_t * init_sequence[] = { octeon_boot_bus_init, timer_init, env_init, /* initialize environment */ early_board_init, init_baudrate, /* initialze baudrate settings */ serial_init, /* serial communications setup */ console_init_f, display_banner, /* say that we are here */ init_dram, dram_test, init_func_ram, NULL, }; /***************************************************************************************/ 从调用完init_sequence中的函数后往下看: /* * Now that we have DRAM mapped and working, we can * relocate the code and continue running from DRAM. */ #if defined(CONFIG_NO_RELOCATION) && defined(CONFIG_RAM_RESIDENT) /* If loaded into ram, we can skip relocation , and run from the spot we were loaded into */ addr = CFG_MONITOR_BASE; u_boot_mem_top = CFG_SDRAM_BASE + MIN(gd->ram_size, (1*1024*1024)); #else /* Locate at top of first Megabyte */ addr = CFG_SDRAM_BASE + MIN(gd->ram_size, (1*1024*1024)); u_boot_mem_top = addr; 其中CFG_SDRAM_BASE=0x8000 0000,是MIPS虚拟寻址空间中kseg0段的起始地址(参考《See MIPS Run》),它经过CPU TLB翻译后是DRAM内存的起始物理地址。 这里显然是将u_boot_mem_top和addr指向了DRAM中1M地址处(从DRAM起始地址到1M地址处的1M空间是为U-boot自己运行分配的), 即从0x8000 0000到0x8010 0000的1M空间是U-boot自己活跃的天下了。 /***************************************************************************************/ 现在U-boot有了对自己来说很富裕的1M字节可以自由分配的DRAM空间了,下面很大一段代码都是对这1M内存的划分和分配。 这部分代码精简后,加入详细注释如下: /* We can reserve some RAM "on top" here. */ //从0x8010 0000处开始向下划分势力范围 /* round down to next 4 kB limit. */ addr &= ~(4096 - 1); //addr &= ~0x0FFF这种计算是常用的地址对齐的算法,这里是向下4K字节对齐 /* Reserve memory for U-Boot code, data & bss*/ addr -= MAX(len, (512*1024)); //为code, data, bss段保留512K的空间 /* round down to next 64k (allow us to create image at same addr for debugging)*/ addr &= ~(64 * 1024 - 1); //向下64K字节对齐 /* Reserve memory for malloc() arena. */ addr_sp = addr - TOTAL_MALLOC_LEN; //划分malloc()使用的空间,即所谓的堆空间,大小有宏来确定 /* (permanently) allocate a Board Info struct and a permanent copy of the "global" data*/ addr_sp -= sizeof(bd_t); //分配BD结构体大小的空间 bd = (bd_t *)addr_sp; gd->bd = bd; //GD中的指针关联到此处的BD结构体地址 addr_sp -= sizeof(gd_t); //分配GD结构体大小的空间 id = (gd_t *)addr_sp; //id指针指向GD结构体地址 /* Reserve memory for boot params. */ addr_sp -= CFG_BOOTPARAMS_LEN; //分配boot param的空间,这里的宏大小是128K字节 bd->bi_boot_params = addr_sp; //在BD中记录此boot param空间的地址 /* Finally, we set up a new (bigger) stack. Leave some safety gap for SP, force alignment on 16 byte boundary */ addr_sp -= 16; //向下一帧,保证栈空间的开始没有和之前分配的空间冲突 addr_sp &= ~0xF; //栈空间16字节对齐 #define STACK_SIZE (16*1024UL) bd->bi_uboot_ram_addr = (addr_sp - STACK_SIZE) & ~(STACK_SIZE - 1); //将栈地址16K对齐后记录入BD bd->bi_uboot_ram_used_size = u_boot_mem_top - bd->bi_uboot_ram_addr; //在BD中记录使用的DRAM大小 /* Save local variables to board info struct */ bd->bi_memstart = CFG_SDRAM_BASE; /* start of DRAM memory */ //0x80000000 bd->bi_memsize = gd->ram_size; /* size of DRAM memory in bytes */ bd->bi_baudrate = gd->baudrate; /* Console Baudrate */ memcpy (id, (void *)gd, sizeof (gd_t)); //将在临时栈空间scratch memory中的GD数据拷贝入DRAM中,至此,BD和GD都已经存在于DRAM中了。 根据这部分代码可以画出U-boot这1M空间的示意图: /***************************************************************************************/ 1M空间瓜分完毕后,出现一条语句: relocate_code (addr_sp, id, addr); 该语句使程序回到cpu/mips/start.S的汇编中,在之后的汇编中,U-boot将自己的代码段、数据段、BSS段等搬到在DRAM中新家,为以后跨入速度时代而过渡。 3) 青铜时代:短暂的回归cpu/mips/start.S: /***************************************************************************************/ 重新回到汇编的天下,找到代码: .globl relocate_code .ent relocate_code relocate_code: 下面的代码就是搬家了。 直到出现代码: move a0, a1 la t9, board_init_r /* doesn't return, runs main_loop() */ j t9 程序搬家基本完成,后面的程序就可以全部在内存DRAM中执行了,速度会比之前在FLASH和scratch memory中运行的速度快上很多。这里跳入的代码段board_init_r是在C程序中定义的函数,仍然在刚才的那个C语言文件 lib_mips/board.c中。 4) 白银时代:终于有正常的C环境接着进行初始化了: /***************************************************************************************/ 进入board_init_r函数之前,有一段让人振奋的注释: /* This is the next part if the initialization sequence: we are now * running from RAM and have a "normal" C environment, i. e. global * data can be written, BSS has been cleared, the stack size in not * that critical any more, etc. */ 然后注意到该函数有两个传入的参数,参数是之前汇编中用a0寄存器传入的,在这里可以看出这两个参数的含义: id: 之前在U-boot的1M空间中分配的GD结构体的地址 dest_addr: U-boot重新定位到DRAM之后的代码起始地址 程序接着向下是将id的值用k0寄存器保存,将GD结构体中的一些字段进行设置,包括记录U-boot自身的代码在内存中的偏移地址等。 /***************************************************************************************/ 接着是重新计算命令表(cmd table)的地址。什么是命令表?因为U-boot启动完成后可以进入命令行模式,这时候用户可以从串口输入命令来指示U-boot下一步做什么,每个 命令对应的名称、用法、描述、执行的函数等信息,用一个命令表结构体保存,这样每一个命令在内存中有对应的一个命令表。结构体的定义在 include/Command.h中,定义如下: struct cmd_tbl_s { char *name; /* Command Name */ int maxargs; /* maximum number of arguments */ int repeatable; /* autorepeat allowed? */ int (*cmd)(struct cmd_tbl_s *, int, int, char *[]); /* Implementation function */ char *usage; /* Usage message (short) */ char *help; /* Help message (long) */ } __attribute__ ((aligned (8))); 而这里给命令表重新计算地址其实只是将从__u_boot_cmd_start到__u_boot_cmd_end之间的每个命令表中的成员指针的地址 加上U-boot在DRAM中的偏移地址,这样获得命令表在DRAM中的地址。看来转换之前的命令表中的地址应该是相对地址(?!)。 注:这里,注意到__attribute((XXX))比较奇特的语法,其实这个是GCC对C语言的扩充,GCC允许声明函数、变量和类型的特殊属性,以便手工的代码优化和更仔细的代码检查。要指定一个声明的属性,在声明后写 __attribute__ (( ATTRIBUTE )) 其中 ATTRIBUTE 是属性说明,多个属性以逗号分隔。GNU C 支持十几个属性,如noreturn, unused, aligned等。 /***************************************************************************************/ 然后是初始化malloc()堆空间: mem_malloc_init(); 其实是将全局变量mem_malloc_start和mem_malloc_end和mem_malloc_brk三个指针指向之前分配好的堆空间。 然后是重定位或者初始化环境变量的指针: env_relocate(); 将env_ptr指针及其指向的地址初始化,用来存放环境变量结构体,然后将flash中的环境变量拷贝到内存中。 然后是其余设备的初始化devices_init(),这是在前面的堆空间(malloc)、环境变量、PCI总线初始化后的基础之上才能进行的,这里的设备包括: i2c_init (); drv_lcd_init (); drv_video_init (); drv_keyboard_init (); drv_logbuff_init (); drv_system_init (); drv_usbtty_init (); ... 然后是将标准的输入输出std*变量由通过串口改为通过pci_console_active进行输入输出。 然后是jump table的初始化,这里似乎是将一些函数指针记录进GD结构体。 然后是console初始化。 然后是再次打印board/chip info,这里打印是为了自检和板子确认。 然后是确认loadaddr和bootfile环境变量的有效性。 然后是miscellaneous platform dependent的初始化,函数misc_init_r ()。 然后是网卡的初始化,eth_initialize()。 然后是IDE的检测和初始化,ide_init()。 然后是debugger的设置。 最后是一个空函数late_board_init(),用来添加比较晚的初始化代码。 下面就进入了一个死循环,循环调用main_loop()函数,这意味着U-boot基本启动完毕,进入命令行模式。 5) 钻石时代:专题篇 下面是一些个人学习的专题。记录备忘。 /***************************************************************************************/ MIPS CPU地址空间简介(整理自《See MIPS Run》和CPU文档): 注:首先需要明确的是CPU物理地址空间不仅仅包括RAM物理内存的空间,还包括CPU内部的一些总线、寄存器的编址。 一个MIPS CPU可以运行在两种优先级别上, 用户态和核心态。MIPS CPU从核心态到用户态的变化并不是CPU工作不一样,而是对于有些操作认为是非法的。在用户态,任何一个程序地址的首位是1的话,这个地址是非法的,对 其存取将会导致异常处理。另外,在用户态下,一些特殊的指令将会导致CPU进入异常状态。 在32位CPU下,程序地址空间划分为4个大区域。每个区域有一个传统的名字。对于在这些区域的地址,各自有不同的属性: kuseg: 虚拟空间0x0000 0000 - 0x7FFF FFFF (低端2G):这些地址是用户态可用的地址。在有MMU的机器里,这些地址将一概被MMU作转换,除非MMU的设置被建立好,否则这2G地址是不可用的。 对于没有MMU的机器,存取这2G地址的方法依具体机器相关,你的CPU具体厂商提供的手册将会告诉你关于这方面的信息。如果想要你的代码在有或没有 MMU的MIPS处理器之间有兼容性,尽量避免这块区域的存取。 kseg0: 虚拟空间0x8000 0000 - 0x9FFF FFFF(512M): 这些地址映射到物理地址简单的通过把最高位清零,然后把它们映射到物理地址低段512M(0x0000 0000 - 0x1FFF FFFF)。因为这种映射是很简单的,通常称之为“非转换的”地址区域。几乎全部的对这段地址的存取都会通过快速缓存(cache)。因此在cache设 置好之前,不能随便使用这段地址。通常一个没有MMU的系统会使用这段地址作为其绝大多数程序和数据的存放位置。对于有MMU的系统,操作系统核心会存放 在这个区域。 kseg1: 虚拟空间0xA000 0000 - 0xBFFF FFFF(512M): 这些地址通过把最高3位清零的方法来映射到相应的物理地址上,与kseg0映射的物理地址一样。但kseg1是非cache存取的。kseg1是唯一的在 系统重启时能正常工作的地址空间。这也是为什么重新启动时的入口向量是0xBFC0 0000。这个向量相应的物理地址是0x1FC0 0000。你将使用这段地址空间去存取你的初始化ROM。大多数人在这段空间使用I/O寄存器。如果你的硬件工程师要把这段地址空间映射到非低段512M 空间,你得劝说他。 kseg2: 虚拟空间0xC000 0000 - 0xFFFF FFFF (1G): 这段地址空间只能在核心态下使用并且要经过MMU的转换。在MMU设置好之前,不能存取这段区域。除非你在写一个真正的操作系统,一般来说你不需要使用这段地址空间。 综上可以看到,MIPS32 CPU下面的不经过MMU转换的内存窗口只有kseg0和kseg1 的512M的大小,而且这两个内存窗口映射到同一512M的物理地址空间。其余的3G虚拟地址空间需要经过MMU转换成物理地址,这个转换规则是由CPU 厂商实现的。还句话说,在MIPS32 CPU下面访问高于512M的物理地址空间,必须通过MMU地址转换。 在核心态下(CPU启 动时),CPU可以作任何事情。在用户态下,2G之上的地址空间是非法的,任何存取将会导致系统异常处理。注意的是,如果一个CPU有MMU,这意味着所 有的用户地址在真正访问到物理地址之前必须经过MMU的转换, 从而使得OS可以防止用户程序随便乱用。对於一个没有内存映射的OS,MIPS CPU的用户态其实是多余的。在核心态下,CPU可以存取低段地址空间,这个存取也是通过MMU的转换。 下面来谈谈MIPS64 CPU的虚拟地址空间。 64位CPU的地址空间的最低2G和最高2G区域是和32位情况下一样的,64位扩展的地址部分在这两者之间。64位下那些大块的不需要MMU转换的窗口可以克服kseg0和kseg1 512M的局限,但是32位下我们可以通过对MMU编程来同样达到这一点。 /***************************************************************************************/ MIPS CPU内存管理与TLB(整理自《See MIPS Run》): 早期的MIPS CPU定位于支持运行在UNIX工作站与服务器上的应用程序,因此内存管理硬件被构想为一个最小化的能帮助BSD UNIX——一个经过完善设计并拥有充分多虚拟存储需求的操作系统的典型——提供内存管理功能的硬件。我们将从MIPS的设计起点开始,面对着一个 unix类型的操作系统以及它的虚存系统的众多需求。我们将会展示一下MIPS的硬件是如何满足这些需求的。结尾时,我们会讨论一下在不能像通常一样使用 内存管理硬件的嵌入式系统中,您可以采取的几种使用方式。 UNIX内存管理工作的本质是为了能运行众多不同的任务(即 multitasking—— 多进程),并且每个任务各自拥有自己的内存空间。如果这个工作圆满完成,那么各任务的命运将彼此独立开来(操作系统自身也因此得以保护):一个任务自身崩 溃或者错误的做某些事不会影响整个系统。显然,对一个使用分布终端来运行学生们程序的大学而言,这是一个很有用的特性;然而不仅如此,甚至是要求最严格的 商业系统环境也需要能够在运行的同时支持实验软件或原型软件一并进行调试和测试。 MMU并不仅仅为了建立巨大而完备的虚拟存储系统,小的嵌入式程序也能在重定位和更有效的内存分配里受益。如果能把应用程序观念上的地址映射到任何可获得的物理地址,系统在不同时刻运行不同程序就会更加容易。 嵌入式应用中常常会明确的运用多进程机制,但几乎没有多少嵌入式操作系统使用隔离的地址空间。或许这归咎于这种机制在嵌入式CPU以及它们上面的操作系统上用处不大并且带来不稳定性,因而显得不那么重要。 |
|