在上一篇文章:简说-编译Linux内核中,我们概括了Linux内核的编译流程。整个编译过程的核心产出,其实就是生成 vmlinux 和内核模块文件。
今天,我们就来深入聊聊这个核心产物——vmlinux。本文会聚焦于它的基本概念、作用以及一些关键特性,暂时不深入其复杂的生成细节和加载机制,旨在帮助你快速建立起对vmlinux的清晰认知。

简单来说,vmlinux是一个未经压缩的、可直接引导的Linux内核镜像文件。它是由编译脚本 scripts/link-vmlinux.sh 将众多编译好的内核子模块(比如调度器、内存管理、文件系统等)链接在一起而形成的。可以说,vmlinux就是最“纯粹”、最完整的Linux内核本体。
那么,名字里的“vm”代表什么?它指的是 Virtual Memory(虚拟内存),这直接点明了Linux内核在虚拟内存管理层面的核心地位。
在编译内核时,你可能还见过 make bzImage 这个命令。它的作用,正是对编译好的vmlinux进行压缩。为什么要多此一举进行压缩呢?
核心原因在于:vmlinux文件太大了。 作为一个标准的ELF文件,vmlinux是编译后的可执行内核。理论上,它确实可以被直接放到启动分区,由引导程序加载并执行,从而启动系统。但现实情况是,未经压缩的vmlinux体积动辄几百MB,直接占用宝贵的启动分区空间显然不划算。因此,实际存储在启动分区中的,是压缩后的内核镜像(如bzImage)。经过压缩,内核镜像大小可能骤降至几MB,极大地节省了空间。
既然vmlinux也是一个ELF文件,它和我们平时在用户态编译生成的ELF程序(比如一个简单的C程序)有什么区别呢?
如果单从ELF文件自身的结构来看,答案是:没有本质区别。它们都遵循相同的ELF格式规范。真正的差异在于运行环境。
用户态程序在生成和运行时,操作系统内核已经就绪,提供了完整的运行环境,包括构建好的页表等。加载器只需根据ELF头信息,将各个段(Section)加载到内存,然后跳转到入口地址(Entry Point)即可运行。
但vmlinux本身是要创造这个运行环境的“奠基者”。在它被执行之前,没有现成的页表,也没有内存管理单元(MMU)开启的虚拟地址空间。因此,vmlinux无法“自力更生”,它的加载需要外部引导程序的辅助。例如,在ARM平台上通常由U-Boot负责,在x86平台上则由GRUB等引导加载器完成。
我们可以通过 readelf 工具查看vmlinux的ELF头信息,来验证它的结构。下面的输出显示,这个vmlinux是针对x86-64架构的,其程序入口地址是 0x1000000。
root@ubuntu:/usr/src/linux-6.1.1# readelf -h vmlinux
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2’s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1000000
Start of program headers: 64 (bytes into file)
Start of section headers: 1096716808 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 5
Size of section headers: 64 (bytes)
Number of section headers: 79
Section header string table index: 78
当引导程序将压缩的内核镜像(内含vmlinux)解压并加载到内存的正确位置后,CPU就会跳转到上述的入口地址开始执行。内核执行的第一步,就是进行最基础的环境构建:初始化CPU寄存器、创建初始页表、开启MMU、切换到虚拟地址模式等等。一旦这些最基础的操作系统运行环境准备就绪,代码便会跳转到著名的 start_kernel() 函数,从这里开始,Linux内核的完整初始化之旅才正式拉开帷幕。
希望这篇简析能帮助你理解vmlinux在内核世界中的角色与特殊性。对技术细节的探讨,欢迎在云栈社区交流。
|