关于达芬奇DM6446,里面内部有两个部分,一个是ARM926ejs的核,还有一个是C64+DSP的视频处理核,而我需要关心的重点是arm926ejs的核(bootload和linux内核)
从bootloader可知, 第一阶段主要是负责检测arm926ejs的相关硬件平台(主要是内存等),而第二阶段主要将内核映象以及根文件映象拷贝进入到RAM中运行!
硬件存储地址说明:
Flash地址存放:
利用EMIF(5.10—External Memory Interface)外部存储接口,一共128M空间
起始地址是0x02000000开始,分别存放bootloader, linux-kernel, Ramdisk
0x2030000---存放image---DDR2 内存进入点是0x80008000
0x21b0000---存放ramdisk----DDR2内存进入点是0x80800000
DDR2地址说明:
利用DDR2专用接口, 地址是0x8000 0000开始(看2.5—Memory Map Summary接口),这个是程序运行的RAM接口
程序运行过程:
根据达芬奇手册的3.3(BootMode)说明,和mx27一样,达芬奇也有自带的的一个类似BIOS的固化启动程序,根据pin脚的选择来选择相应的启动口!
查看表table 3-4可得到相关的启动的详细信息!
我们看到ARM启动模式:
当系统复位时,保存在片内ROM的RBL(rom boot load)程序开始运行,该程序根据读写BTSEL[1:0]脚来判断相应的启动方式,如果选择了EMIFA(NOR Flash)启动方式,那么立即从地址0x02000000开始运行,否则运行RBL程序,RBL程序首先便将flash的开始位置的UBL(用户user boot loader)拷贝到ARM内部的RAM(AIM)中,并运行该UBL程序(最多14K)大小!
UBL主要完成系统时钟、DDR频率的初始化,准备好加载u-boot镜像的环境。然后加载u-boot代码到DDR中,并跳转到u-boot上,然后u-boot启动内核!
DDR2内核启动内存分配:(TFTP 启动模式)
0x8000 0000-0x8000 8000这个32k是存放linux内核的全局变量,如命令参数、页表等
0x8000 8000-0x807f ffff这个8M左右的空间是存放真正的内核的
0x8080 0000-0x8180 0000这个是存放ramdisk的镜像的
0x8180 0000—这个存放uImage镜像文件
具体的内核运行过程为:
1.u-boot启动拷贝ramdisk镜像到0x8080 0000
2.u-boot启动拷贝uImage镜像到0x81800000中,如果在镜像在Flash的话,那么
3.运行uImage镜像在0x8000 8000开始处,那么开始内核解压,然后运行vmlinux
根据相关的资料,可以得出,bootloader的过程比较容易理解,直接将其CPU看成一个单片机类型的应用程序即可,有RAM和ROM(Flash)可用,并且ARM资源很吩咐,底层的初始化基本上用汇编实现,定义好了相关的地址启动CPU之后,那么可以用c语言实现相关的bootloader功能,比较简单,复杂的是linux的内核运行!
关于bootloader的相关详细分析,可以参考一篇强文《嵌入式系统bootloader技术内幕》
由上可知,关于内核运行,不管是TFTP模式还是flash模式:
内核运行的情况都是拷贝到0x8000 8000处,然后进行解压缩,解压后开始运行内核
内核挂载根文件系统是在0x8180 0000
接下来,利用u-boot中的两个有趣的函数实现的命令来做两个实验
1. do_go()函数,u-boot中的go命令
2. do_bootm()函数,u-boot对应的bootm命令(包含了写入内核相关全局启动参数)
这里我们内核进行编译后,可以看到生成zImge ,uImage 以及compressed/Image 三个内核文件,在u-boot中,利用命令
1.Tftp 81800000 zImage(其实也可以是任意的,82000000都可以,利用u-boot的tftp协议,将zImage文件,下载到81800000的地址处)
然后运行 go 81800000 ----可以看到我们的程序开始运行了吧,看到没?
程序马上进行解压内核,然后进入到内核,内核根据boot传递的参数开始执行!
2.tftp 8180000 uImage(这个也可以是8180000后的任意地址,因为前面需要用ramdisk),因此我们将uImage下载到0x8180000内存地址处
利用bootm 81800000来解压uImage
上面的两个方法都可以实现内核的启动,这里顺便说一下initrd参数,如果我们是挂载ramdisk类型,那么我们设定boot参数的时候,设定initrd=0x80800000这个地址来挂载系统的根文件系统,那么内核启动的时候,初始化之后,在挂载真正的根文件系统之前,会挂载一个初始根文件系统,该系统就是参数initrd指定地址开始,如果是yjttfs2类型的文件系统,那么直接用rootfs=/dev/来挂载,并且前面定义noinitrd来进行说明!
因此我们在boot的参数可以直接指定initrd=0x8080000 ,也就是前面
利用命令tftp 80800000 ramdisk 将ramdisk存放到指定位置后的地址
总结,上面我们运行实验知道了一点,就是利用u-boot中的tftp功能,将相关的内核可执行映象文件zImage存放到后面的ram中,然后直接运行!
接下来我们看看zImage是什么,它为什么可以直接运行?
关于两个起始文件:linux-2.6.*/arch/arm/kernel/*.S
linux-2.6.*/arch/arm/boot/compressed/*.S
内核编译流程:
如上,首先编译生成的是未压缩过的内核文件,然后再将其拷贝进入boot/compressed/下进行压缩,生成相应的zImage或者是其他的uImage之类的(uImage就是在zImage前面加上一个64字节大小的信息头,包含了linux版本、内核大小等信息,是需要对应uboot的do_bootm命令来解压,如果你的bootloader不是u-boot,那么还是使用zImage,利用地址来直接跳转吧),从上可以大概明确一点:
1. 利用kernel/*.S vmlinux.lds等相关文件, 将编译好的相关内核代码进行分段链接,首先,生成一个叫vmlinux(虚拟内存内核elf格式文件),该文件包含了sections等信息,各个分段标识都可见 ,这个才是其实才是真正的内核执行文件Image,但是需要内存重映射才行,这个内核定义的运行地址和bootloader定义的基地址一定要匹配(如同这里的80008000才是真正的内核运行起始地址,8000 0000-80007fff是内核存放全局变量参数)
因为内核定义的运行地址其实是0x0000 0000,所以才需要一个基地址,因为不同的系统定义的SDRAM的物理地址是不一样的,如同MX27中是A000 0000是SDRAM的首物理地址,而S3C2440是0xC000 0000才是首物理地址,而这里规定的是0x8000 0000才是首地址!(如果你不相信, 可以试着做个实验,利用tftp 80008000 Image先下载到板子上,然后直接运行80008000 ,看看是不是直接可以运行下去,但是如果你不是将该Image下载到bootloader匹配的80008000处,那么不管是其它任何地址,都不会运行Image, 但是zImage就不同)
2. 利用objcopy把上面的文件进行处理,去掉sections等信息,生成一个vmlinux.bin文件
3. 利用gzip将其压缩成vmlinux.bin.gz, 并删除vmlinux.bin文件
4. 将vmlinux.bin.gz文件设为数据段文件,生成一个叫piggy.o的文件
5. 链接:compressed/vmlinux=head.o+misc.o+piggy.o,其中head.o+misc.o主要用于解压用
6. 再次利用objcopy将vmlinux去掉相关的section等信息,生成vmlinux.bin
7. 最后链接_start,生成zImage
zImage 的入口程序即为 arch/arm/boot/compressed/head.S。它依次完成以下工作:开启 MMU 和 Cache,调用 decompress_kernel()解压内核,最后通过调用 call_kernel()进入非压缩内核 Image 的启动
明白了上面大概的内核生成的过程,如果需要,请自行参考boot/compressed/下的相关代码
如果需要详细的明白该流程,可以相关参考资料:百度文库中《内核阅读心得》
那么真正的内核入口地址是0x80008000, 才是运行Image真正的入口地址!
一般情况下,不同的平台,文件下初始化linux-2.6/arch/arm/kernel/下的文件是不同的
下面我们分析Image的真正的执行情况!
Linux内核入口 (转)
Linux 非压缩内核的入口位于文件/arch/arm/kernel/head-armv.S 中的 stext 段。该段的基地址就是压缩内核解压后的跳转地址。如果系统中加载的内核是非压缩的 Image,那么bootloader将内核从 Flash中拷贝到 RAM 后将直接跳到该地址处,从而启动 Linux 内核。不同体系结构的 Linux 系统的入口文件是不同的,而且因为该文件与具体体系结构有关,所以一般均用汇编语言编写[3]。对基于 ARM 处理的 Linux 系统来说,该文件就是head-armv.S。该程序通过查找处理器内核类型和处理器类型调用相应的初始化函数,再建立页表,最后跳转到 start_kernel()函数开始内核的初始化工作。
检测处理器内核类型是在汇编子函数__lookup_processor_type中完成的。通过以下代码可实现对它的调用:bl __lookup_processor_type。__lookup_processor_type调用结束返回原程序时,会将返回结果保存到寄存器中。其中r8 保存了页表的标志位,r9 保存了处理器的 ID 号,r10 保存了与处理器相关的 struproc_info_list 结构地址。
检测处理器类型是在汇编子函数 __lookup_architecture_type 中完成的。与__lookup_processor_type类似,它通过代码:“bl __lookup_processor_type”来实现对它的调用。该函数返回时,会将返回结构保存在 r5、r6 和 r7 三个寄存器中。其中 r5 保存了 RAM 的起始基地址,r6 保存了 I/O基地址,r7 保存了 I/O的页表偏移地址。当检测处理器内核和处理器类型结束后,将调用__create_page_tables 子函数来建立页表,它所要做的工作就是将 RAM 基地址开始的 4M 空间的物理地址映射到 0xC0000000 开始的虚拟地址处。对笔者的 S3C2410 开发板而言,RAM 连接到物理地址 0x30000000 处,当调用 __create_page_tables 结束后 0x30000000 ~ 0x30400000 物理地址将映射到0xC0000000~0xC0400000 虚拟地址处。
当所有的初始化结束之后,使用如下代码来跳到 C 程序的入口函数 start_kernel()处,开始之后的内核初始化工作:
b SYMBOL_NAME(start_kernel)
3.2 start_kernel函数
start_kernel是所有 Linux 平台进入系统内核初始化后的入口函数,它主要完成剩余的与硬件平台相关的初始化工作,在进行一系列与内核相关的初始化后,调用第一个用户进程-init 进程并等待用户进程的执行,这样整个 Linux 内核便启动完毕。该函数所做的具体工作有[4][5]
:
1) 调用 setup_arch()函数进行与体系结构相关的第一个初始化工作;
对不同的体系结构来说该函数有不同的定义。对于 ARM 平台而言,该函数定义在arch/arm/kernel/Setup.c。它首先通过检测出来的处理器类型进行处理器内核的初始化,然后通过 bootmem_init()函数根据系统定义的 meminfo 结构进行内存结构的初始化,最后调用paging_init()开启 MMU,创建内核页表,映射所有的物理内存和 IO空间。
2) 创建异常向量表和初始化中断处理函数;
3) 初始化系统核心进程调度器和时钟中断处理机制;
4) 初始化串口控制台(serial-console);
ARM-Linux 在初始化过程中一般都会初始化一个串口做为内核的控制台,这样内核在启动过程中就可以通过串口输出信息以便开发者或用户了解系统的启动进程。
5) 创建和初始化系统 cache,为各种内存调用机制提供缓存,包括;动态内存分配,虚拟文件系统(VirtualFile System)及页缓存。
6) 初始化内存管理,检测内存大小及被内核占用的内存情况;
7) 初始化系统的进程间通信机制(IPC);
当以上所有的初始化工作结束后,start_kernel()函数会调用 rest_init()函数来进行最后的初始化,包括创建系统的第一个进程-init 进程来结束内核的启动。Init 进程首先进行一系列的硬件初始化,然后通过命令行传递过来的参数挂载根文件系统。最后 init 进程会执行用 户传递过来的“init=”启动参数执行用户指定的命令,或者执行以下几个进程之一:
execve("/sbin/init",argv_init,envp_init);
execve("/etc/init",argv_init,envp_init);
execve("/bin/init",argv_init,envp_init);
execve("/bin/sh",argv_init,envp_init)。
当所有的初始化工作结束后,cpu_idle()函数会被调用来使系统处于闲置(idle)状态并等待用户程序的执行。至此,整个 Linux 内核启动完毕。