跟涛哥一起学习嵌入式 32:Linux内核编译和启动分析
发布日期:2021-06-29 04:23:54 浏览次数:2 分类:技术文章

本文共 4447 字,大约阅读时间需要 14 分钟。

在Linux环境下,我们想运行一个应用程序,在shell交互环境下直接敲命令就可以了,操作系统给程序提供了运行环境和进程管理。那Linux操作系统本身是如何运行和启动的呢?在分析之前,我们先做一个Linux内核启动的实验:通过u-boot加载Linux内核镜像uImage到内存不同地址,观察Linux内核启动流程。

实验环境:

  • 硬件平台:使用 QEMU 仿真ARM vexpress A9 开发板
  • RAM大小配置:512 MB
  • RAM内存地址:0x60000000 ~ 0x7FFFFFFF

实验过程:

  • 编译内核镜像,将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/uImageImage Name:   Linux-4.4.0+Created:      Fri Apr 24 19:11:09 2020Image Type:   ARM Linux Kernel Image (uncompressed)Data Size:    3460776 Bytes = 3379.66 kB = 3.30 MBLoad Address: 60003000Entry Point:  60003000Image 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

宅学部落官方微信群开通了,嵌入式技术分享、交流。有兴趣加入请先加微信:brotau,然后拉你进群。

转载地址:https://blog.csdn.net/zhaixuebuluo/article/details/106146842 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:跟涛哥一起学习嵌入式 33:《嵌入式C语言自我修养》书稿V1.0版
下一篇:跟涛哥一起学嵌入式 31:深入浅出CPU流水线工作原理

发表评论

最新留言

很好
[***.229.124.182]2024年04月21日 20时25分02秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章