Linux内核编译与启动分析

在linux环境下,我们想运行一个应用程序,在shell交互环境下直接敲命令就可以了,操作系统给程序提供了运行环境和进程管理。那linux操作系统本身是如何运行和启动的呢?在分析之前,我们先做一个linux内核启动的实验:通过u-boot加载linux内核镜像uimage到内存不同地址,观察linux内核启动流程。
实验环境:
硬件平台:使用 qemu 仿真arm vexpress a9 开发板ram大小配置:512 mbram内存地址: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 uimagemkimage工具常见的参数说明如下:
-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

iPhone15什么时候上市 iphone15参数配置
BA50L(II)-AI/I交流剩余电流传感器
新唐科技ML51FB9AE控制器简介
简单的表征电路充电节AA电池-Simple Characte
台积电P14厂受到停电的影响,大概会有3万片晶圆受影响
Linux内核编译与启动分析
PCB设计中的器件封装问题分享
聊聊组成光纤系统的关键组件,它是如何在光纤连接中发挥作用的?
关于同轴电缆连接器基础知识的简述
全球半导体公司市值有何变化?
手机指纹触控解锁和密码解锁哪个更好用
目前32位微控制器处理器有着非常广泛的应用
加拿大蒙特利尔举办“机器人”足球世界杯
我国量子计算机超越超算还有多久?
固态继电器的用途_固态继电器的应用实例
与现代传感器接口:接口设计
Agilent安捷伦86122A光波长计优势
利用石墨烯作为生长缓冲层来实现高亮 LED 的新策略
苹果的AR眼镜将能够解决虚拟现实的一个发展难题
我国5G终端连接数超2亿,居世界第一