Email: BuddyZhang1 buddy.zhang@aliyun.com
目录
内核镜像介绍
vmlinux, Image, zImage, 以及 uImage,各种名词对各位开发者来说是内核开发中比较混淆
的地方,本文就给各位开发者理顺一下各个名词之间的关系以及构建构成,为内核学习打下扎实
基础。正如上图所示,ARM linux 内核是一个运行在 arm 平台上的 Linux 操作系统,该操作
系统由不同的源代码制作而成,这里称一个可在目标板上运行的操作系统文件为内核镜像。
内核镜像是由不同的源代码经过编译汇编,链接而成,在不同的阶段,内核镜像有不同的称呼,
因此本节介绍不同阶段内核镜像的名字。
vmlinux
内核镜像的第一个名字称为 vmlinux,其位于源码目录下。vmlinux 就是通过源码经过编译汇编,
链接而成的 ELF 文件,因此这个 vmlinux 文件包含了 ELF 的属性,以及各种调试信息等,因此
这个阶段的内核镜像 vmlinux 特别大,而且不能直接在 arm 上直接运行。
Image
由于 vmlinux 镜像体积巨大而且不能在 arm 上运行,因此需要使用 OBJCOPY 工具将不需要
的 section 从 vmlinux 里面剥离出来,最终在 arch/arm/boot/ 目录下生成了 Image 文件,
此时 Image 是可以在 arm 平台上运行的,但是由于历史原因,当年制作出 Image 的大小
正好比一个软盘大一点,为了让内核镜像能够装在一张软盘上,所以就将 Image 进行压缩,
生成 piggy.gz 或者 piggy_data.
piggy.gz/piggy_data
一开始只支持 gzip 压缩方法,所以将压缩之后的 Image 称为 piggy.gz,但随着内核的不断
发展,内核支持更多的压缩算法,因此把压缩之后的 Image 称为 piggy_data.
piggy.o
之前说过 Image 可以在 arm 上运行,当不能直接运行,因为 Image 运行前需要一些已知
初始化环境,这就需要特定功能的代码实现这些功能,这里称这些代码为 bootstrap。
于是内核在 arch/arm/boot/compressed/ 目录下增加了 bootstrap 功能的代码。和制作
vmlinux 一样,需要将这个目录下的源文件编译汇编成目标文件,然后再链接成一个文件。
为了构造这个,内核将 piggy_data 直接塞到了一个汇编文件 piggy.S 中,然后这个文件
经过汇编之后,就生成了 piggy.o
vmlinux (compress kernel)
为了构建能直接在 ARM 上运行的内核,压缩之后的内核与 bootstrap 功能的目标文件经过链接
生成了一个 ELF 文件 vmlinux,这个 vmlinux 位于 arch/arm/boot/compressed/ 目录下,
这个 vmlinux 与内核源码顶层目录下的 vmlinux 不是同一个文件。该目录下的 vmlinux
是包含 bootstrap 和压缩内核的内核镜像文件,是一个 ELF,所以不能在 arm 上直接运行,
于是和之前一样,使用 OBJCOPY 工具将 vmlinux 中不必要的段全部丢弃,最后生成二进制文件
zImage。
zImage
zImage 是可以直接在 arm 直接运行的内核镜像。zImage 的主要任务就是将被压缩的 Image
解压到指定位置,然后将控制权交给 Image 执行。因此,只要将 zImage 加载到内存指定位置
之后,内核就能正常启动。
uImage
uboot 为了启动内核,将 zImage 经过 mkimage 工具在 zImage 头部添加一些 uboot 加载
内核需要的信息。
vmlinux 构建过程
vmlinux 文件是 Kbuild 编译系统将源码经过编译链接所获得的目标文件,所以它是一个 ELF
文件,因此 vmlinux 文件包含了各种调试信息和各种有用的 section。(注意!这里的 vmlinux
文件位于内核源码的顶层目录,可能其他目录也有名为 vmlinux 的文件)。vmlinux 文件的链接
过程由 arch/$(ARCH)/kernel/vmlinux.lds.S 链接脚本决定,可以通过该文件知道 vmlinux
文件的内部布局。vmlinux 生成的更多信息可以查看:
Kbuild 构建 Linux 内核
Image 构建过程
Image 文件是 vmlinux 使用 objcopy 工具转换后得到的二进制文件。由于 vmlinux 不能
直接在 arm 上运行,需要丢弃一些与运行无关的 section,所以使用 objcopy 工具正好
可以完成这个任务。Image 文件相比 vmlinux,除了格式不同之外,vmlinux 的调试信息和
许多注释以及与运行无关的 section 都被移除,所以体积会变小很多。开发者可以在
arch/arm/boot/Makefile 中查看这个过程:
从上面可以看出 Image 就是通过 vmlinux objcopy 获得,这里 objcopy 对应的命令是
位于 scripts/Makefile.lib 文件中获得,定义如下:
通过上面的代码,开发者可以在 Image 生成过程中添加打印消息,以此查看整个 object 过程,
添加调试代码如下:
然后编译内核时可以看到如下消息:
从上面的调试可知,vmlinux ELF 文件使用 object 工具变成 Image 时,使用的参数
是 “-O binary -R .comment -S”,这个参数的意思是:
piggy.gz/piggy_data 构建过程
piggy.gz 是 Image 经过压缩之后得到的压缩文件,在高版本中,piggy.gz 被命名为
piggy_data,指代码压缩内核,Image 压缩过程位于 arch/arm/boot/compressed/Makefile
里面,具体如下:
通过上面的内容可知,内核采用的压缩方法由 compress-y 变量决定,其定义在
arch/arm/boot/compressed/Makefile 里面,如下:
因此内核支持 gzip,lzo,lzma,xzkern, 和 lz4 的压缩方法,具体使用哪种,因此开发者可以在
命令执行处添加调试代码如下:
编译内核,获得如下调试信息:
所以 Image 采用了 gzip 方法,因此开发者可以在 scripts/Makefile.lib 文件中获得
具体的 gzip 过程,如下:
gizp 的参数含义如下:
其他压缩方法同理分析。经过上面分析,Image 压缩成 piggy.gz 或者 piggy_data 的过程已经
分析完毕。
piggy.o 构建过程
piggy.o 文件是通过 piggy_data/piggy.gz 汇编之后的可链接的目标文件,其汇编命令位于
arch/arm/boot/compressed/Makefile,具体内容如下:
从上面的命令来看,piggy.o 是通过 piggy.S 汇编生成,其依赖 piggy_data,那么 piggy.S
的内容如下 (arch/arm/boot/compressed/piggy.S):
从上面的汇编代码可以知道,piggy.S 汇编中调用 incbin 指令将 arch/arm/boot/compressed/piggy_data
到汇编文件中,成为汇编的一部分,这样 piggy_data 就能被汇编最后成为一个可链接的目标文件。
这里定义了两个全局符号: input_data 和 input_data_end。这两个符号标记了压缩内核在
piggy.o 中的起始地址和终止地址,对链接脚本有用。至此,piggy.o 的构建过程分析完毕,内核
到此已经被汇编成一个可链接的目标文件。
Bootstrap ELF kernel (vmlinux) 构建过程
只有纯粹的内核是无法启动的,所以需要在内核的头部加入一些用于 bootstrap loader 功能的代码。
Kbuild 编译系统在 arch/arm/boot/compressed/ 目录下,将 head.S, misc.S,
compressed.S 等多个汇编文件汇编成多个可链接的 ELF 目标文件,以此作为内核的
bootstrap loader。在这个步骤,Kbuid 编译系统将这些可链接的目标文件与 piggy.o 文件按
链接脚本的内容进行链接,制作出一个带 bootstrap loader 的内核ELF 文件。对于的过程要参考
arch/arm/boot/compressed/ 目录下的 Makefile 和 vmlinux.lds.S 文件。
首先通过分析 Makefile 知道链接的文件,具体源码如下:
从上面的代码可知,这里将可链接之后的 ELF 目标文件也成为 vmlinux,开发者要将这个
vmlinux 与源码顶层目录的 vmlinux 区分开来。这里的 vmlinux 是添加了 bootstrap loader。
从上面的代码可以看出,vmlinux 的链接过程通过 vmlinux.lds 进行链接,这里开发者可以好好分析
一下这个链接脚本,以此知道 vmlinux 如何布局,以及系统运行之后,vmlinux 如何在内存中布局,
在下一节重点分析 vmlinux.lds 链接脚本。vmlinux 的具体构建过程,在下面的章节会详细介绍。
至此,一个带 bootstrap loader 的内核 ELF 文件已经制作完成,但由之前分析可知,ELF 文件
是不能直接在 arm 上运行的,需要制作成 bin 文件才能在 arm 上运行,所以下一步就是 zImage
的制作。
vmlinux.lds
这里所介绍的 vmlinux.lds 是位于 arch/arm/boot/compressed/ 目录下的 vmlinux.lds,
这个 vmlinux.lds 用于将压缩内核 ELF 文件 piggy.o 与其他目标文件链接成一个带 bootstrap
loader 的 ELF 目标文件。
链接之后的 ELF 文件成为 vmlinux,用于在 arm 上解压被压缩的内核。那么接下来分析一下这个 ELF
的构建过程。由于源码比较长,这里分段解析:
首先通过判断宏 CONFIG_CPU_ENDIAN_BE8 是否定义,以此定义了一个宏操作 ZIMAGE_MAGIC,
这个宏主要用于调整字节序。
链接脚本首先定义了 ELF 目标文件运行在 arm,并且 vmlinux 的入口函数为 _start。
链接脚本使用 “/DISCARD/” 关键字,将所有输入文件的 “.ARM.exidx”, “.ARM.extab”,
以及可读写的 “.data” 数据段。因此如果 vmlinux ELF 的目标文件的数据最好放到代码段
里作为只读数据。
链接脚本定义了 vmlinux ELF 目标文件的代码段 .text,其包含了所有输入文件的 “.start”,
“.text”, “.text.*”, “.fixup”, “.gnu.warning”, “.glue_7t”, “.glue_7” sections。
并定义了 .text section 的起始地址是 TEXT_START, 并且 _text 指向 .text section
开始的地方。
接下来定义了 .table section, 按 4 字节对齐,.table section 中首先定义了 _table_start
变量,用于指向 .table section 的起始位置。 _table_end 变量指向 .table section 的结束
地址,所以在代码中使用这两个变量就可以确定 .table 的位置。这里用于创建一个 table,table
内首先定义了一个 long 字节用于存储 2,第二个 long 直接存储 MAGIC 0x5a534c4b。对于第三个
字节,首先 __piggy_size_addr 地址对应的值用于存储压缩内核的大小,也就是 piggy_data 的
大小。所以第三个 long 字节用于存储 __piggy_size_addr 与 _start 之间的偏移,用于运行
时确定 __piggy_size_addr 正确位置。第四个 long 字节用于存储内核的 bss section 的大小。
第五个 long 字节存储一个 0, 用于结尾。通过这个 table,基本确定了带 bootstrap loader
的 vmlinux 内存布局了。
定义了 .rodata section, 用于存储所有输入文件的 .rodata, .rodata.*, .data.rel.ro sections,
这个 section 基本就是随机数的 section。
这里比较重要的是建立了 .piggydata section, 这个 section 就是用于存储被压缩的内核镜像,
之前 piggy_data 经过汇编生成 piggy.o 之后,由于 piggy_data 是二进制文件,所以
piggy_data 内的 section 没有被 vmlinux.lds.S 拆分。这里还定义了一个变量 __piggy_size_addr
用于存储 piggy_data 的大小。 __piggy_size_addr 指向当前地址前 4 字节。这样 piggy_data
的最后 4 个字节就是存储 piggy_data 的大小。
创建了 .got.plt 和 .got section, 用于存储输入文件的 GOT 表和 PLT 表。PLT 表可以称为
内部函数表,GOT 表称为全局函数表。
这里由于没有定义 CONFIG_EFI_STUB 宏,则不做讲解。
_edata 指向了 data 段的结束地址,由于 .image_end section 定义了 NOLOAD,所以
这个 section 不会被链接到 vmlinux。
接着定义了 4 个变量。_maigc_sig 存储 0x016f2818;_magic_start 指向了 vmlinux
的起始地址 _start; _magic_end 指向了 _edata; _magic_table 指向了.table 的相对
地址。
使用 BSS_START 定义了 BSS section 在内存中的地址。其定义如下:
所以这里用 CONFIG_ZBOOT_ROM 来设置 BSS_START 的起始地址。以此,当 CONFIG_ZBOOT_ROM
没有使用的情况下,ZBSSADDR 指向 ALIGN(8), 所以 BSS 段的起始地址是从当前地址进行 8 字节
对齐之后,作为 BSS 的起始地址。并且这里也定义了 vmlinux 的 _end 地址以及 __bss_start
地址。
最后定义了 .stack section, 将所有的输入文件的 .stack 存储到 .stack section。并定义
了 __pecoff_data_size 和 __pecoff_end 地址。
这些 section 基本不需要讨论。最后使用 ASSERT 关键字确定 _edata_real 和 _edata 之间
的关系是否满足。如果不满足。则 zImage 的 size 不正确。
以此,通过上面 vmlinux.lds.S 链接脚本链接之后,生成的带 bootstrap 的 vmlinux ELF
文件。可以使用 objdump 工具查看此时的 ELF 布局:
zImage 构建过程
zImage 是通过带 bootstrap loader 的内核 ELF 文件经过 objcopy 命令之后制作生成
的二进制文件,用于在 arm 上直接运行,其生成过程可以查看 arch/arm/boot/Makefile:
同原始 vmlinux 转换为 Image 过程一致,具体细节可以参考上面章节。制作完 zImage 之后,
可以将 zImage 在 arm 上运行。
uImage 构建过程
uboot 为加载 zImage,会使用 mkimage 工具对 zImage 进行相应处理生成 uImage,
因此 uImage 用于 uboot 加载。其构建如下:
附录
Linux ARM Debugging
Debugging ARM Linux from first code
BiscuitOS Home
BiscuitOS Kernel Build
Linux Kernel
Bootlin: Elixir Cross Referencer
赞赏一下吧 🙂