在Linux环境下,我们想运行一个应用程序,在shell交互环境下直接敲命令就可以了,操作系统给程序提供了运行环境和进程管理。那Linux操作系统本身是如何运行和启动的呢?在分析之前,我们先做一个Linux内核启动的实验:通过u-boot加载Linux内核镜像uImage到内存不同地址,观察Linux内核启动流程。
实验环境:
实验过程:
- 编译内核镜像,将uImage加载地址设置为0x60003000,编译生成uImage
- 将内核加载到0x60003000地址,然后bootm 0x60003000
- 将内核加载到0x60004000地址 ,然后bootm 0x60004000
通过实验我们可以看到:虽然 uImage 被U-boot加载到了内存 0x60003000 和 0x60004000 内存不同地址,但是通过U-boot的bootm命令都可以正常引导和启动运行。bootm到底有什么魔法,即使我们把镜像文件加载到了未指定的内存地址,也能让Linux神奇般地启动起来呢?要想一探究竟,还得溯本求源:从Linux内核的编译链接说起。我们从编译Linux内核镜像 uImage 的Log信息为切入点分析:
$ make uImage LOADADDR=0x60003000
CC arch/arm/mm/mmu.o //上面省略的是编译过程:将.c编译为.o文件
… //前方高能预警
LD vmlinux
SYSMAP System.map
OBJCOPY arch/arm/boot/Image
Kernel: arch/arm/boot/Image is ready
Kernel: arch/arm/boot/Image is ready
LDS arch/arm/boot/compressed/vmlinux.lds
AS arch/arm/boot/compressed/head.o
GZIP arch/arm/boot/compressed/piggy.gzip
AS arch/arm/boot/compressed/piggy.gzip.o
CC arch/arm/boot/compressed/misc.o
CC arch/arm/boot/compressed/decompress.o
LD arch/arm/boot/compressed/vmlinux
OBJCOPY arch/arm/boot/zImage
Kernel: arch/arm/boot/zImage is ready
Kernel: arch/arm/boot/Image is ready
Kernel: arch/arm/boot/zImage is ready
UIMAGE arch/arm/boot/uImage
Image Name: Linux-4.4.0+
Created: Fri Apr 24 19:11:09 2020
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 3460776 Bytes = 3379.66 kB = 3.30 MB
Load Address: 60003000
Entry Point: 60003000
Image arch/arm/boot/uImage is ready
编译Linux内核镜像整个过程比较漫长,大概需要5分钟左右,并有大量的编译信息打印出来。前期的打印信息比较简单,就是分别使用编译器和汇编器将对应的.c文件、.S文件编译成 .o 格式可重定位目标文件。真正高能核心的过程在最后的链接和镜像文件格式处理部分,编译信息已经截取如上。
结合编译信息和上面的编译流程图我们可以看到,编译器将所有的源文件编译成对应的目标文件后,接下来就是链接过程:将所有的目标文件链接成ELF格式的可执行文件:vmlinux。ELF文件格式是Linux环境下的可执行文件格式,无论是 gcc 还是 arm-linux-gcc 编译器,生成的都是ELF这种格式的文件。在Linux环境下,加载器根据ELF文件里的地址信息,就可以把它加载到内存指定的地址运行,但是系统启动过程中并没有ELF文件的执行环境,需要将ELF文件转换为二进制纯指令文件。编译器接着会调用objdump命令删除不必要的section,只保留代码段、数据段等必要的section,将ELF格式的vmlinux文件转换为原始的二进制内核镜像Image。Image可以在裸机环境下运行,体积也比较大,我们可以使用gzip工具对其进行压缩,生成piggz.gzip压缩的二进制内核镜像。这样做的好处是可提高程序的启动速度:因为内核加载运行时,从Flash 上读取镜像的速度是很慢的,我们通过先压缩,加载到内存后再解压这种操作,不仅可以节省Flash存储空间(尤其是Nor Flash还是很贵的),还可以节省了镜像的加载时间。
因为piggz.gzip是压缩文件无法运行,所以我们还需要给它链接上一段解压缩代码。链接器只能处理ELF格式的目标文件,因此在链接之前,要先将压缩文件piggz.gzip转换为可重定位的目标文件:piggy.gzip.o。在ARM平台下,解压缩代码是在arch/arm/boot/compressed/目录下面的head.o、misc.o、 decompress.o,这部分使用 -fpic 参数编译生成的指令是与位置无关的,放到哪里都可以执行,它们通过链接器与piggy.gzip.o一起组装成新的ELF文件vmlinux,然后再使用objcopy工具转换为纯二进制镜像zImage,就可以直接烧写到Nor或nand flash上,随系统启动后加载到内存运行了。
不同的嵌入式系统平台可能会使用不同的BootLoader来加载Linux内核镜像的运行,常见的BootLoader有U-boot、vivi、g-bios等。使用U-boot的嵌入式平台通常会对zImage进一步转换,给它添加一个64字节的数据头,用来记录镜像文件的加载地址、入口地址、文件大小、CPU架构等信息。我们可以使用U-boot提供的mkimage工具将zImage镜像转换为uImage:
$ mkimage –A arm -O linux –T kernel –C none –a 0x60003000
–e 0x60003000 -d zImage uImage
mkimage工具常见的参数说明如下:
- -A:指定CPU架构类型
- -O:指定操作系统类型
- -T:指定image类型
- -C:采用的压缩方式:none、gzip、bzip2等
- -a:内核加载地址
- -e:内核镜像入口地址
走到这一步,U-boot可以引导的uImage内核镜像生成,这个Linux内核镜像编译就完美结束了。接下来我们继续分析U-boot是如何加载uImage运行的:
U-boot加载的 dtb 文件和 bootargs 这里暂不考虑,我们重点关注uImage:当uImage被加载到内存不同的位置时,为什么都可以正常启动。我们先考虑上面的第一种情况,当加载到内存中的地址等于编译时指定的地址时:
U-boot提供的bootm机制用来启动内核的运行。bootm会解析uImage文件64字节的数据头,解析出指定的加载地址,并跟自己的参数进行对比:若发现bootm参数地址和编译时-a指定的加载地址0x60003000相同,就会直接跳过数据头,跳到zImage的入口地址0x60003040执行。
如果bootm发现自己的参数地址跟-a指定的加载地址0x60003000不同时,它会将去掉64个字节数据头的内核镜像zImage复制到编译时 -a 指定的加载地址处,然后再跳到该地址处执行。如上图所示,zImage镜像被加载到了编译时指定的0x60003000地址处,然后跳过来,就可以直接执行zImage了。
zImage是一个压缩文件,在运行之前要先解出真正要执行的内核镜像Image,然后才能跳到内核镜像真正的入口处去启动Linux内核。解压缩代码head.o、decompress.o是一段与位置无关的代码,放到内存的任何位置都可以运行。大家有兴趣可以做一个实验,使用U-boot的bootz命令直接引导内核镜像zImage运行:将zImage加载到内存的不同地址,你会发现zImage都可以正常启动。
解压缩代码的主要作用就是将从zImage文件出解压出真正的内核镜像Image,并将其重定位到Image内核编译时指定的链接地址0x80008000上。Linux运行使用的是虚拟地址,需要CPU硬件管理单元MMU的支持,MMU会将虚拟地址转换为对应的物理地址。在ARM vexpress平台上,内核的链接地址0x80008000会映射到物理内存0x60008000的地方。zImage的解压缩代码会将Image解压到0x60008000处,然后跳过去就可以直接启动Linux内核了。
在zImage运行解压缩代码的过程中会遇到这么一种情况:zImage自身刚好占据了0x60008000这片地址空间,那么当zImage的重定位代码将解压出来的Image拷贝到指定的0x60008000处时,可能就会冲掉自身正在运行的代码。为了避免这种情况发生,zImage会将这部分重定位拷贝到一个安全的地方,比如Image的后面,然后再跳到这片重定位代码处执行,这样就可以将Image镜像安全地拷贝到0x60008000地址上了。
拷贝成功后,就可以直接跳到 0x60008000 地址去运行Linux内核真正的代码了。因为Image镜像链接时使用的是虚拟地址,所以在运行Linux内核的C语言函数之前,首先会有一段汇编代码用来初始化堆栈环境,使能MMU。代码跟踪就不具体分析了,有兴趣大家可以去看视频教程:《C语言嵌入式Linux高级编程》第3期:程序的编译、链接和运行,或者参考下面的提示自行分析:
- 运行入口:arch/arm/kernel/head.S
- 使能MMU:__create_page_tables
- 跳入C语言函数:__mmap_switched/start_kernel
-
Linux
+关注
关注
87文章
11219浏览量
208872 -
应用程序
+关注
关注
37文章
3240浏览量
57594 -
Shell
+关注
关注
1文章
363浏览量
23287
发布评论请先 登录
相关推荐
评论