0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Linux内核编译与启动分析

汽车电子技术 来源:宅学部落 作者:王利涛 2023-02-17 09:43 次阅读

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/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:

$ mkimageA arm  -O linuxT kernelC nonea 0x60003000e 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

    文章

    10994

    浏览量

    206772
  • 应用程序
    +关注

    关注

    37

    文章

    3138

    浏览量

    56423
  • Shell
    +关注

    关注

    1

    文章

    358

    浏览量

    22903
收藏 人收藏

    评论

    相关推荐

    Linux编译驱动、内核及应用程序分析

    作为一名嵌入式Linux新手,在学习的过程中会遇到很多问题。写了一个驱动程序怎么编译?怎么加载进内核
    的头像 发表于 01-17 13:46 6234次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>编译</b>驱动、<b class='flag-5'>内核</b>及应用程序<b class='flag-5'>分析</b>

    Linux内核编译主要过程

    Linux内核编译主要过程: 配置、编译、安装 。
    发表于 08-08 16:02 508次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>的<b class='flag-5'>编译</b>主要过程

    linux内核编译

    Linux内核编译与安装1从www.kernel.org 下载内核linux-2.6.29.1.tar.bz2包解压。并执行清理命令mak
    发表于 10-26 14:14

    Linux内核编译启动的相关资料分享

    Linux环境下,我们想运行一个应用程序,在shell交互环境下直接敲命令就可以了,操作系统给程序提供了运行环境和进程管理。那Linux操作系统本身是如何运行和启动的呢?在分析之前,
    发表于 12-20 06:28

    交叉编译linux内核(raspberry_3.6.y)

    一步一步教你交叉编译linux内核,RPI的内核编译教程,小众的东西了
    发表于 11-03 17:58 0次下载

    linux内核启动内核解压过程分析

    linux启动内核解压过程分析,一份不错的文档,深入了解内核必备
    发表于 03-09 13:39 1次下载

    Linux内核编译详谈

    Linux内核编译详谈
    发表于 10-30 09:51 7次下载
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b><b class='flag-5'>编译</b>详谈

    基于Arm的Linux内核编译指导

    基于Arm的Linux内核编译指导
    发表于 10-30 10:13 15次下载
    基于Arm的<b class='flag-5'>Linux</b><b class='flag-5'>内核</b><b class='flag-5'>编译</b>指导

    如何编译Linux内核

    不断更新。新的内核修订了旧内核的bug,并增加了许多新的特性。如果用户想要使用这些新特性,或想根据自己的系统度身定制一个更高效,更稳定的内核,就需要重新编译
    发表于 04-02 14:46 474次阅读

    Linux内核配置编译分析的设计方案

    Linux内核配置编译分析的设计方案
    发表于 07-08 16:53 18次下载
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>配置<b class='flag-5'>编译</b><b class='flag-5'>分析</b>的设计方案

    如何才能编译Linux内核

    了旧内核的bug ,并增加了许多新的特性。如果用户想要使用这些新特性, 或想根据自己的系统度身定制一个更高效, 更稳定的内核, 就需要重新编译内核。本文将以RedHat
    发表于 11-04 18:04 8次下载

    Linux内核编译与运行

    本文档的主要内容详细介绍的是Linux内核编译与运行免费下载。
    发表于 03-25 13:48 10次下载

    嵌入式Linux内核编译

    实验环境VMware Workstation PlayerUbuntu16.04kernel-3.2.tar.bz2Linux内核编译在ubuntu上编译嵌入式
    发表于 11-01 17:07 16次下载
    嵌入式<b class='flag-5'>Linux</b>的<b class='flag-5'>内核</b><b class='flag-5'>编译</b>

    Linux内核编译和运行

    想让Linux内核代码跑起来,得先搭建编译和运行代码的环境。
    发表于 06-23 11:56 361次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>的<b class='flag-5'>编译</b>和运行

    Linux内核编译脚本

    获得编译命令及选项 编译linux时,默认不会显示编译的命令,如果你要获得编译命令及其选项,可以在make命令后面加上宏定义: make V
    的头像 发表于 09-27 11:52 369次阅读