Linux内核分析之基础知识-03

韩乔落

4.1 Kbuild 系统概述

什么是 Kbuild

Kbuild 是 Linux 内核专用的构建系统,它构建在 GNU Make 之上,但绝非简单的 Makefile 集合。面对数万个源文件、数百个目录、数千个配置选项的复杂性,Kbuild 通过一套精心设计的递归构建机制,实现了高效、灵活、可扩展的编译流程。

Kbuild 的核心设计理念是:每个目录自治管理自己的编译规则,顶层 Makefile 负责全局调度。这种递归 make 模式使得各子系统可以独立演进,互不干扰。

核心文件解析

顶层 Makefile

顶层 Makefile(位于内核源码根目录)是整个构建系统的入口点,它负责:

  • 版本信息定义:内核版本号由 Makefile 顶部的 VERSIONPATCHLEVEL、SUBLEVELEXTRAVERSION` 四个变量组合而成
  • 架构设定:通过 ARCH 变量指定目标架构(如 x86arm64),默认为当前主机架构
  • 交叉编译配置:通过 CROSS_COMPILE 变量指定交叉编译工具链前缀
  • 全局编译标志:定义 -O2-fno-strict-aliasing 等全局编译选项
  • 递归构建入口:将构建任务分发到各子目录
1
2
3
4
5
6
7
8
9
# 顶层 Makefile 中的关键变量
VERSION = 7
PATCHLEVEL = 0
SUBLEVEL = 10
EXTRAVERSION =
NAME = ...

ARCH ?= $(shell uname -m | sed -e s/i.86/x86/ ...)
CROSS_COMPILE ?=

scripts/Kbuild.include

这是一个被所有 Makefile 共同包含的公共规则文件。它定义了通用的编译规则、标志处理函数、文件检查逻辑等。比如将 cc-option 函数用于检测编译器是否支持某个选项:

1
2
# 如果 gcc 支持 -fno-stack-protector,则使用该选项
cflags-y += $(call cc-option, -fno-stack-protector)

scripts/Makefile.build

这是 Kbuild 的真正引擎。每当 make 递归进入一个子目录时,实际上就是调用这个文件:

1
$(MAKE) -f scripts/Makefile.build obj=<目标目录>

它会读取该目录下的 KbuildMakefile,解析其中的 obj-yobj-m 等变量,然后执行编译、链接等操作。

arch/x86/Makefile

架构相关的 Makefile 定义了 x86 特有的编译标志和规则:

1
2
3
4
5
6
7
8
9
# x86 32 位与 64 位的不同编译选项
ifeq ($(CONFIG_X86_64),y)
KBUILD_CFLAGS += -mno-red-zone -mcmodel=kernel -fno-pic ...
else
KBUILD_CFLAGS += -march=i386 ...
endif

# 链接 vmlinux 时使用的特殊标志
LDFLAGS_vmlinux := -e startup_64

arch/x86/boot/Makefile

启动映像的构建规则在此定义,包括 setup.elf 的链接、compressed 目录的递归构建,以及最终 bzImage 的生成。这个 Makefile 中的规则体现了从编译产物到可引导映像的最后加工步骤。

构建流程

第一步:配置 (make config)

1
2
3
make menuconfig    # 基于 ncurses 的菜单式配置
make defconfig # 使用默认配置
make olddefconfig # 基于现有 .config 自动应用新默认值

配置的结果是生成 .config 文件,它是一个键值对列表:

1
2
3
4
CONFIG_X86_64=y
CONFIG_SMP=y
CONFIG_MODULES=y
CONFIG_DEBUG_INFO=y

第二步:编译 (make)

执行 make 时,Kbuild 会:

  1. .config 转换为 include/generated/autoconf.h 头文件
  2. 递归遍历所有包含 obj-y/obj-m 条目的目录
  3. 编译所有需要的 .c 文件为 .o 文件
  4. 将所有 obj-y 对应的 .o 文件链接为 vmlinux
  5. 编译所有 obj-m 对应的 .o 文件为独立的 .ko 模块

第三步:生成 bzImage

x86 架构的 Makefile 中定义了从 vmlinuxbzImage 的特殊规则链,这个流程将在下一节详细分析。

obj-y / obj-m 机制

这是 Kbuild 最核心的设计模式。每个子目录的 Makefile 通过这两个变量声明该目录需要编译的内容:

1
2
3
4
5
6
7
8
9
10
# 直接编译并链接进内核
obj-y += sched.o
obj-y += fork.o
obj-y += exec.o

# 编译为可加载模块
obj-m += mydriver.o

# 条件编译:CONFIG_FOO=y 时链接进内核,=m 时编译为模块,未设置时跳过
obj-$(CONFIG_FOO) += foo.o

obj-y 的行为foo.c 被编译为 foo.o,最终链接进 built-in.a(该目录的静态库),再逐级汇总到 vmlinux 中。

obj-m 的行为bar.c 被编译为 bar.o,然后单独链接为 bar.ko 内核模块文件,不会进入 vmlinux

条件编译obj-$(CONFIG_FOO) += foo.o 会展开为三种情况之一:

  • CONFIG_FOO=yobj-y += foo.o(编入内核)
  • CONFIG_FOO=mobj-m += foo.o(编为模块)
  • # CONFIG_FOO is not setobj- += foo.o(被忽略,不编译)

autoconf.h:从配置到代码的桥梁

include/generated/autoconf.h 是由 .config 自动生成的 C 头文件,它将 Makefile 层面的配置选项转化为 C 预处理器可用的宏定义:

1
2
3
4
#define CONFIG_X86_64 1
#define CONFIG_SMP 1
#define CONFIG_DEBUG_INFO 1
/* #undef CONFIG_FOO */

内核源码中大量使用 #ifdef CONFIG_XXX 来实现条件编译,这些宏的值全部来自 autoconf.h。这条从 .configautoconf.h 再到源码的链路,是内核可配置性的基石。

常用 make 目标

目标 说明
vmlinux 仅编译并链接生成未压缩的 ELF 内核
bzImage 生成完整的可引导压缩映像(x86 默认目标)
modules 编译所有配置为模块的代码
modules_install 将编译好的模块安装到 /lib/modules/
install 将 bzImage 安装到 /boot
clean 清除大部分构建产物
mrproper 清除所有构建产物,包括 .config
distclean mrproper + 清除编辑器备份等杂项文件

目录遍历顺序

Kbuild 遍历目录的顺序由 obj-y 中条目的排列顺序决定。对于链接顺序敏感的场景(如初始化调用顺序),这一点至关重要。内核通过 include/linux/init.h 中定义的各级 initcall 级别来保证初始化顺序,而非依赖链接顺序本身。4.2 bzImage 生成流程

构建流水线总览

从源代码到最终可引导的 bzImage,经历了一条精心设计的多阶段流水线。每一步都是一个格式转换过程,逐步将原始编译产物转化为引导加载程序能够识别和加载的格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
源代码(.c/.S)
↓ gcc / clang
目标文件(.o)
↓ ld (链接器)
vmlinux (完整 ELF 可执行文件,含符号表和调试信息)
↓ objcopy -O binary
vmlinux.bin (原始二进制,ELF 头已剥离)
↓ gzip / xz / lz4 / zstd
vmlinux.bin.gz (压缩后的内核负载)
↓ piggy.S (汇编包装)
piggy.o (包含压缩数据的目标文件)
↓ ld (与解压代码链接)
arch/x86/boot/compressed/vmlinux (自解压 ELF)
↓ objcopy -O binary
arch/x86/boot/compressed/vmlinux.bin (自解压原始二进制)
↓ tools/build.c (拼接)
arch/x86/boot/bzImage (最终可引导映像)

逐步详解

阶段一:编译(.c → .o)

每个 .c 源文件被独立编译为对应的目标文件。Kbuild 根目录递归遍历所有子目录,对 obj-y 列出的源文件执行编译:

1
2
3
gcc -c -o kernel/sched/core.o kernel/sched/core.c
gcc -c -o kernel/fork.o kernel/fork.c
# ... 数千个文件

同目录下的 .o 文件被合并为 built-in.a 静态归档。

阶段二:链接生成 vmlinux(.o → vmlinux)

所有 built-in.a 文件被链接为一个完整的 ELF 可执行文件 vmlinux。这是整个内核在未压缩状态下的完整二进制表示,包含:

  • 所有内核代码和数据段(.text, .data, .rodata, .bss 等)
  • 完整的 ELF 头和段头表
  • 符号表(可用 nm vmlinux 查看)
  • 调试信息(如果启用了 CONFIG_DEBUG_INFO

链接过程由 vmlinux.lds.S(x86_64 为 arch/x86/kernel/vmlinux.lds.S)生成的链接脚本控制,它精确安排了各段的布局:

1
2
3
4
5
6
7
8
9
/* 简化的链接脚本结构 */
SECTIONS
{
. = 0xffffffff81000000; /* 内核虚拟基地址 */
.text : { *(.text) } /* 代码段 */
.rodata : { *(.rodata) } /* 只读数据 */
.data : { *(.data) } /* 可写数据 */
.bss : { *(.bss) } /* 未初始化数据 */
}

此时生成的 vmlinux 文件体积可达数百 MB(含调试信息时),但它还不能被 BIOS 或 UEFI 直接引导。

阶段三:生成原始二进制(vmlinux → vmlinux.bin)

1
objcopy -O binary -R .note -R .comment -S vmlinux vmlinux.bin

objcopy 执行以下操作:

  • -O binary:将输出格式设为原始二进制(剥离所有 ELF 结构)
  • -R .note:移除 .note 段
  • -R .comment:移除编译器注释段
  • -S:移除调试符号

结果是一个纯粹的二进制映像,只包含需要加载到内存中的原始字节。文件体积大幅缩小。

阶段四:压缩(vmlinux.bin → vmlinux.bin.gz)

内核支持多种压缩算法,通过 CONFIG_KERNEL_* 选项选择:

1
2
3
4
5
6
7
8
# arch/x86/boot/compressed/Makefile 中的压缩规则
suffix_$(CONFIG_KERNEL_GZIP) = gz
suffix_$(CONFIG_KERNEL_BZIP2) = bz2
suffix_$(CONFIG_KERNEL_LZMA) = lzma
suffix_$(CONFIG_KERNEL_XZ) = xz
suffix_$(CONFIG_KERNEL_LZO) = lzo
suffix_$(CONFIG_KERNEL_LZ4) = lz4
suffix_$(CONFIG_KERNEL_ZSTD) = zstd

默认使用 gzip,压缩率与解压速度的平衡较好。zstd 是较新的选择,在大内核上表现优秀。

阶段五:制作 piggy(压缩数据 → piggy.o)

这是整个流程中最精巧的一步。内核使用一个汇编文件 piggy.S 将压缩后的二进制数据包装成一个目标文件:

1
2
3
4
5
6
7
8
9
10
11
/* arch/x86/boot/compressed/piggy.S */
.section ".rodata..compressed","a",@progbits
.globl z_input_len
z_input_len:
.long <压缩数据大小>
.globl z_output_len
z_output_len:
.long <解压后大小>
.globl input_data
input_data:
.incbin "arch/x86/boot/compressed/vmlinux.bin.gz"

.incbin 伪指令直接将压缩后的二进制文件作为原始字节嵌入到目标文件的 .rodata..compressed 段中。同时导出 input_data(压缩数据起始地址)、z_input_len(压缩大小)和 z_output_len(解压后大小)三个符号,供解压代码使用。

阶段六:链接自解压映像(piggy.o + 解压代码 → compressed/vmlinux)

arch/x86/boot/compressed/ 目录下,piggy.o 与多个解压相关模块链接为自解压的 ELF 映像:

1
2
3
4
5
6
7
8
piggy.o          ← 包含压缩的内核数据
head_64.o ← 16 位/32 位到 64 位模式切换,自解压入口
misc.o ← 解压主逻辑,调用解压函数
kaslr.o ← 内核地址随机化(KASLR)相关处理
efi_thunk_64.o ← EFI 兼容层(如需要)
string.o ← 基本字符串操作(解压过程中使用)
cmdline.o ← 命令行解析
error.o ← 错误处理

这些代码构成了一个微型自举程序,它的工作是:

  1. 从引导加载程序手中接管 CPU
  2. 设置基本的 64 位执行环境
  3. 将嵌入的压缩内核解压到正确的内存位置
  4. 跳转到解压后的内核入口点 startup_64

阶段七:生成压缩二进制(compressed/vmlinux → compressed/vmlinux.bin)

再次使用 objcopy 剥离 ELF 头:

1
objcopy -O binary -R .note -R .comment -S compressed/vmlinux compressed/vmlinux.bin

阶段八:合成 bzImage(setup + compressed → bzImage)

最终的合成由内核源码中的 arch/x86/boot/tools/build.c 工具完成。这个 C 程序将两个部分拼接为 bzImage:

  1. setup.elf:实模式启动代码,包含引导参数处理、硬件探测等
  2. compressed/vmlinux.bin:包含自解压代码和压缩内核的负载

tools/build.c 执行的关键操作:

  • 读取 setup 部分,计算大小并填充头部字段
  • 将 PE/COFF 头部附加到映像开头(使 bzImage 可被 UEFI 固件识别为合法的可执行文件)
  • 追加压缩的内核负载
  • 写入校验和和大小信息

bzImage 最终结构

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────┐
│ PE/COFF Header │ ← UEFI 可识别的头部
├─────────────────────────────────┤
│ Boot Sector (512B) │ ← 传统 BIOS 引导扇区
├─────────────────────────────────┤
│ Setup Code │ ← 实模式设置代码
│ (video, e820, cmdline) │
├─────────────────────────────────┤
│ Compressed Kernel Payload │ ← 自解压代码 + 压缩的 vmlinux
│ (piggy.o + decompressor) │
└─────────────────────────────────┘

arch/x86/boot/Makefile 中的关键规则

1
2
3
4
5
6
7
8
9
10
# 链接实模式 setup 代码
$(obj)/setup.elf: $(setup-objs) FORCE
$(call if_changed,ld)

# 生成 bzImage:调用 tools/build 拼接
$(obj)/bzImage: $(obj)/setup.elf $(obj)/compressed/vmlinux.bin \
$(obj)/tools/build FORCE
$(call if_changed,image)
$(Q)$(obj)/tools/build $(obj)/setup.elf $(obj)/compressed/vmlinux.bin \
> $(obj)/bzImage

整个流程体现了 Unix 哲学:每个工具做好一件事,通过管道(pipeline)串联完成复杂任务。从编译器到链接器,从 objcopy 到压缩工具,再到自定义的 build 工具,每一步都在为最终的可引导映像添加必要的层次。4.3 vmlinux、vmlinuz 与 bzImage 的区别

Linux 内核构建过程中会产生多种中间映像和最终映像文件,它们各有不同的用途和特征。理解这些格式之间的区别,对于内核调试、系统部署和故障排查都至关重要。

vmlinux:原始未压缩 ELF 内核

vmlinux 是内核构建的第一个完整产物,位于源码树根目录。它是一个标准的 ELF(Executable and Linkable Format)可执行文件,具有以下特征:

格式:ELF 64-bit LSB executable, x86-64

内容完整

  • 完整的 ELF 头和段头表(program headers & section headers)
  • 所有内核代码(.text 段)
  • 所有内核数据(.data、.rodata、.bss 段)
  • 完整的符号表(可用 nm vmlinuxreadelf -s vmlinux 查看)
  • 调试信息(若启用 CONFIG_DEBUG_INFO,包含 DWARF 格式的源码级调试数据)

不可直接引导:BIOS 和 UEFI 固件无法识别 ELF 格式,因此 vmlinux 不能被固件直接加载和执行。

核心用途

  • GDB 调试gdb vmlinux /proc/kcore 可以对运行中的内核进行源码级调试
  • 崩溃分析:配合 crash 工具和 vmcore 转储文件分析内核崩溃原因
  • 符号解析:内核 Oops 信息中的地址需要通过 vmlinux 的符号表转换为函数名
  • 代码审查:使用 objdump -d vmlinux 反汇编查看最终生成的机器码

典型体积:100MB - 800MB(取决于配置,启用完整调试信息时更大)

vmlinux.bin:剥离 ELF 头的原始二进制

vmlinux.bin 是通过 objcopy 从 vmlinux 生成的原始二进制映像:

1
objcopy -O binary -R .note -R .comment -S vmlinux vmlinux.bin

这个过程做了什么:

  • 去除 ELF 头、段头表等元数据结构
  • 去除符号表和调试信息(-S 标志)
  • 去除 .note 和 .comment 段
  • 只保留需要加载到内存中的纯净代码和数据

结果是包含所有内核代码和数据的原始字节流,没有任何文件格式包装。这个文件是压缩的输入原料。

典型体积:30MB - 50MB

vmlinuz:一个约定俗成的名称

vmlinuz 这个名字需要特别澄清——它不是构建过程中产生的一个特定文件格式,而是一个历史沿用的命名约定:

  • vmlinux + z(compressed)= vmlinuz
  • 在现代 x86 构建中,bzImage 文件被安装到 /boot/ 目录时被命名为 vmlinuz-<version>
  • 这是为了与传统命名习惯保持一致
1
2
3
# 安装后的文件命名
/boot/vmlinuz-7.0.10
/boot/vmlinuz-7.0.10-old

所以当你看到 /boot/vmlinuz-* 文件时,它实际上就是一个 bzImage 格式的文件,只是换了名字。

bzImage:”big zImage”

bzImage 是 x86 平台上最终的、可引导的内核映像。名称中的 “bz” 代表 “big zImage”,而不是 “bzip2 压缩”——这是一个常见的误解。

为什么叫 “big”

在早期的 Linux 内核中,zImage 格式的内核必须被加载到低于 640KB 的内存区域(实模式内存限制)。随着内核体积不断增长,这个限制变得越来越不合理。bzImage 格式打破了这一限制,它可以将内核负载放置在 1MB 以上的地址,因此被称为”big zImage”。

bzImage 的组成

bzImage 并非只是一个压缩文件,它是一个多层封装的复合映像:

1
2
3
4
PE/COFF Header      ← UEFI 固件可识别的可执行文件头部
Boot Sector ← 传统 BIOS 引导扇区(前 512 字节)
Setup Code ← 实模式设置代码(硬件探测、模式切换)
Kernel Payload ← 自解压代码 + 压缩的原始内核二进制

bzImage 的引导过程

  1. BIOS/UEFI 固件加载引导加载程序(GRUB、systemd-boot 等)
  2. 引导加载程序读取 bzImage
  3. 将 setup 代码加载到实模式地址(通常 0x90000)
  4. 将内核负载加载到受保护的内存区域(通常 1MB 以上)
  5. 跳转到 setup 代码入口
  6. setup 代码完成硬件初始化后,跳转到压缩负载入口
  7. 自解压代码将内核解压到最终位置
  8. 跳转到解压后的内核入口 startup_64

典型体积:8MB - 30MB(压缩后)

zImage:已淘汰的旧格式

zImage 是早期 Linux 内核使用的压缩映像格式,现已完全淘汰:

  • 内核负载必须加载到 640KB 以下的实模式内存
  • 无法支持大于约 512KB 的压缩内核
  • 在 Linux 2.3.x 之后被 bzImage 取代
  • 现代内核构建不再生成 zImage

体积对比

以一个典型的 x86_64 allmodconfig 构建为例:

文件 格式 典型体积 说明
vmlinux ELF 200-800MB 含符号表和调试信息
vmlinux.bin Raw binary 30-50MB 剥离 ELF 元数据
vmlinux.bin.gz gzip compressed 8-15MB 纯压缩的内核二进制
bzImage Bootable image 8-30MB 含 setup + 压缩内核

从 vmlinux 到 bzImage,体积缩小了 10-50 倍,这正是压缩的威力。

其他映像格式

vmlinux.gz:gzip 压缩的 vmlinux ELF 文件。常用于 kdump 和 kexec 场景——kexec 工具可以直接加载 vmlinux 格式的内核,跳过 BIOS 的完整引导流程。

vmlinux.zst:zstd 压缩的 vmlinux,部分发行版使用此格式减小 /boot 分区占用。

install image:某些架构(如 ARM)的安装映像会额外包含 initramfs,形成一个自包含的引导包。

实用参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看 vmlinux 的 ELF 信息
file vmlinux
readelf -h vmlinux
nm vmlinux | grep startup_64

# 查看 bzImage 的头部信息
file arch/x86/boot/bzImage
# 输出:arch/x86/boot/bzImage: Linux kernel x86 boot executable

# 解压 bzImage 中的内核(使用 extract-vmlinux 脚本)
scripts/extract-vmlinux arch/x86/boot/bzImage > extracted-vmlinux

# 从 /boot 中的 vmlinuz 提取原始 vmlinux
/scripts/extract-vmlinux /boot/vmlinuz-$(uname -r)

4.4 内核链接脚本分析

链接脚本(Linker Script)是控制内核最终内存映像布局的核心文件。它决定了各个代码段和数据段的排列顺序、对齐方式以及在内存中的具体位置,并定义了大量供内核运行时使用的关键符号。理解链接脚本是深入理解内核内存布局的必经之路。

4.4.1 链接脚本的作用

链接脚本的三大核心职责:

  1. 控制内存布局:指定内核映像在虚拟地址空间中的起始位置,以及各段的排列顺序和对齐方式。
  2. 段排序与合并:将所有目标文件中同名段(如 .text)合并为一个大段,并按指定顺序排列。
  3. 符号定义:在段的起始和结束位置定义符号变量,供 C 代码引用以获取段边界地址。

4.4.2 主链接脚本

x86_64 架构的主链接脚本位于 arch/x86/kernel/vmlinux.lds.S。注意其扩展名为 .lds.S(大写 S),这意味着它是一个经过 C 预处理器(CPP)处理的链接脚本。预处理阶段会展开 #include#define 以及条件编译 #ifdef 等指令,最终生成 .lds 文件供链接器 ld 使用。

该脚本首先通过 #include <asm-generic/vmlinux.lds.h> 引入通用的链接脚本辅助宏,如 INIT_TEXT_SECTIONPERCPU_SECTION 等,然后定义 x86_64 特有的内存布局。

4.4.3 关键段定义

.text 段

.text 段存放内核的可执行机器码。其中 .head.text 子段具有特殊地位——它包含内核的引导入口点代码(如 startup_64),必须位于整个内核映像的最前面。这是因为 BIOS 或引导加载器在加载内核时,总是从映像的起始位置开始执行。

1
2
3
4
5
6
7
.text : AT(ADDR(.text) - LOAD_OFFSET) {
_text = .;
*(.head.text) /* 引导代码,必须最先出现 */
*(.text) /* 普通内核代码 */
...
_etext = .;
}

.text 段通常要求 2MB 对齐,以利用大页(huge page)减少 TLB 未命中。

.data 段

.data 段存放已初始化的全局变量和静态变量。_sdata_edata 分别标记该段的起始和结束地址。

.rodata 段

.rodata(read-only data)段存放只读数据,包括 const 修饰的常量、跳转表(jump table)、格式字符串等。该段在运行时通过页表设置为只读权限,任何写入尝试都会触发页故障异常。

.bss 段

.bss(Block Started by Symbol)段存放零初始化的全局变量和静态变量。内核启动时将该段清零。__bss_start__bss_stop 标记其边界。BSS 段不占用磁盘空间,仅在内存中分配。

.init 段

.init.text.init.data 存放仅在内核初始化期间使用的代码和数据。典型的例子包括驱动程序的 xxx_init() 函数和 __initdata 变量。内核完成引导后,从 __init_begin__init_end 之间的内存会被回收并释放给伙伴分配器使用。这是通过 free_initmem() 函数实现的。

.init 段配套的是一系列 initcall 段:

  • __initcall_start__initcall_end:存放 initcall 函数指针数组
  • 按优先级分为 early_initcallpure_initcallcore_initcallpostcore_initcallarch_initcallsubsys_initcallfs_initcalldevice_initcalllate_initcall 等级别
  • 内核按顺序遍历这些函数指针数组,依次调用每个初始化函数

.exit 段

.exit.text.exit.data 存放模块卸载时执行的代码和数据。对于内建(built-in)驱动,这些代码在启动后永远不会被执行,因此会被链接器丢弃以节省空间。对于可加载模块,这些代码保留在模块中,在 rmmod 时执行。

__setup 段

__setup_start__setup_end 之间的区域存放内核启动参数处理器的注册表。每个 __setup() 宏定义的参数处理器在此段中占一个条目,内核在解析命令行参数时遍历此表进行匹配。

__ex_table 段

__ex_table(异常表)段存放 copy_to_usercopy_from_user 等函数的故障恢复地址对。当用户空间访问发生页故障时,内核通过查找异常表跳转到对应的修复代码,而不是触发 oops。每个表项包含(故障指令地址, 修复指令地址)二元组。

__per_cpu 段

__per_cpu_start__per_cpu_end 定义了 per-CPU 数据的模板区域。PER-CPU 变量通过 DEFINE_PER_CPU() 宏定义,在编译时存放在此段中。系统启动时,内核为每个 CPU 分配一份独立的拷贝,并通过 per_cpu() 宏加上 CPU 编号偏移来访问对应 CPU 的实例。

__param 段

__start___param__stop___param 存放模块参数(module_param)的元数据。每个内核模块参数在此段中注册一个 struct kernel_param 结构体,包含参数名、类型、权限和读写回调。

4.4.4 关键符号

链接脚本定义的符号在 C 代码中通过 extern 声明后即可使用。以下是 x86_64 内核中最重要的符号:

符号 含义
_text / _etext 内核代码段的起始和结束
_sdata / _edata 数据段的起始和结束
__bss_start / __bss_stop BSS 段的起始和结束
_end 整个内核映像的结束地址
__init_begin / __init_end init 段的起始和结束(引导后释放)
phys_base 内核加载的物理基地址
__START_KERNEL_map 内核代码的虚拟地址映射起点(0xFFFFFFFF80000000

其中 __START_KERNEL_map 的值 0xFFFFFFFF80000000 是 x86_64 内核文本段在直接映射区之上的固定映射地址,属于内核地址空间的高地址区域。

4.4.5 段排序的重要性

段的排列顺序不是随意的,而是经过精心设计的:

  1. .head.text 必须排在最前面,因为引导加载器从映像头部开始执行
  2. .text 紧随其后,包含主体内核代码
  3. .rodata 放在代码段之后,可以与 .text 共享同一个大页映射
  4. .data.bss 放在只读数据之后
  5. .init 段单独放在末尾区域,便于启动后整块释放

这种排序确保了:

  • 引导代码能被正确找到和执行
  • 相同页权限的段可以连续排列,减少页表开销
  • init 段可以作为一个连续区域被整体回收

4.4.6 Per-CPU 段的详细机制

per-CPU 机制是内核实现高性能数据访问的关键技术。其工作原理如下:

  1. 编译期:所有 per-CPU 变量在链接时被集中放置在 __per_cpu_start__per_cpu_end 之间的模板区域
  2. 启动期:内核为每个 CPU 分配一段与模板等大的内存,并将模板内容复制到每份拷贝中
  3. 运行期:通过 GS 段寄存器(x86_64)保存当前 CPU 的 per-CPU 区域偏移,使用 per_cpu() 宏访问时自动加上该偏移

这种方式避免了锁竞争——每个 CPU 只访问自己的数据副本,无需加锁同步,是内核中实现无锁算法的重要基础设施。

4.5 内核配置系统(Kconfig)

Linux 内核包含数以千计的可配置选项,从架构支持到驱动程序、从调试功能到安全机制,几乎每个子系统都提供了丰富的配置点。Kconfig 是内核的配置描述语言和配置管理框架,它提供了一套完整的机制来定义配置选项、管理选项间的依赖关系,并为用户提供多种配置界面。

4.5.1 Kconfig 文件的组织

内核源码树的几乎每个子目录都包含一个 Kconfig 文件,用于描述该目录所对应子系统或模块的配置选项。顶层 Kconfig 文件通过 source 指令引入各子目录的 Kconfig 文件,形成一棵完整的配置选项树。

例如,arch/x86/Kconfig 定义了 x86 架构特有的选项,而 drivers/net/Kconfig 定义了网络驱动相关的选项。这种分散式的组织方式使得每个子系统可以独立维护自己的配置描述。

4.5.2 配置选项类型

Kconfig 支持以下几种基本类型:

  • bool:布尔型,取值为 y(启用)或 n(禁用)。用于只能编译为内建或完全不编译的选项。
  • tristate:三态型,取值为 y(内建)、m(模块)或 n(禁用)。这是内核特有的类型,用于既可内建也可编译为可加载模块的选项。
  • int:整型,取值为十进制整数。
  • hex:十六进制整型,取值为十六进制数。
  • string:字符串型,取值为任意字符串。

Kconfig 还提供了简化写法:def_booldef_tristate 可以在单行内同时定义类型和默认值。

4.5.3 依赖关系

配置选项之间可以建立依赖关系,这是 Kconfig 最强大的特性之一:

  • depends on:声明直接依赖。被依赖的选项未启用时,当前选项不可见也不可选。
  • select:强制启用另一个选项。当当前选项被启用时,被 select 的选项会被自动启用(无论其依赖是否满足)。这种”反向依赖”使用时需谨慎,因为可能导致违反依赖约束。
  • imply:弱式反向依赖。当当前选项启用时,建议启用另一个选项,但用户仍可手动关闭。这比 select 更安全。
  • if:条件表达式,可在选项定义行内使用,等效于 depends on

4.5.4 Choice 分组

choice 语句定义一组互斥选项,同一时刻只能选择其中一个。典型的应用场景是选择不同的调度策略、不同的 I/O 调度器或不同的定时器频率。

1
2
3
4
5
6
7
8
9
10
11
12
13
choice
prompt "Timer frequency"
default HZ_250

config HZ_100
bool "100 HZ"

config HZ_250
bool "250 HZ"

config HZ_1000
bool "1000 HZ"
endchoice

4.5.5 配置界面

内核提供了多种配置界面,适应不同的使用场景:

命令 界面类型 说明
make menuconfig ncurses TUI 最常用的文本菜单界面
make xconfig Qt GUI 图形化配置界面(需 Qt 库)
make gconfig GTK GUI 图形化配置界面(需 GTK 库)
make config 逐行问答 最基本的交互式配置方式
make oldconfig 仅新选项 在已有 .config 基础上只询问新增选项
make defconfig 默认配置 生成当前架构的默认配置
make olddefconfig 默认新选项 对新选项自动采用默认值,无需交互

对于内核开发者而言,make menuconfig 是最常用的配置方式。升级内核版本后,make olddefconfig 可以快速将旧配置迁移到新版本。

4.5.6 配置文件 .config

用户通过配置界面所做的选择保存在源码树根目录的 .config 文件中。该文件的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 未启用的选项
# CONFIG_FOO is not set

# 内建选项
CONFIG_X86_64=y

# 编译为模块
CONFIG_NFS=m

# 整数值
CONFIG_HZ=250

# 字符串值
CONFIG_DEFAULT_HOSTNAME="localhost"

.config 文件由内核配置系统自动生成和修改,不建议直接手动编辑。如果需要手动调整,建议使用 scripts/config 工具脚本,或者在修改后运行 make olddefconfig 来验证和补全依赖。

4.5.7 生成的文件

配置过程会生成以下关键文件,分别服务于不同的消费者:

include/generated/autoconf.h

该头文件将配置选项转换为 C 预处理器宏,供内核 C 代码使用:

1
2
3
4
#define CONFIG_X86_64 1
#define CONFIG_SMP 1
#define CONFIG_MODULES 1
/* #undef CONFIG_FOO */ /* 未启用的选项不定义宏 */

所有 C 文件都通过 include/linux/kconfig.h 间接包含此文件。代码中可以使用 #ifdef CONFIG_xxxIS_ENABLED(CONFIG_xxx) 宏来测试配置选项。

include/config/auto.conf

该文件采用 Makefile 语法,供顶层 Makefile 和各子目录的 Makefile 使用:

1
2
3
CONFIG_X86_64=y
CONFIG_SMP=y
CONFIG_NFS=m

构建系统根据这些变量决定哪些目录需要编译、哪些对象需要链接。

include/config/tristate.conf

该文件专门处理三态选项的值,用于 Makefile 中判断某个选项是 y 还是 m

1
2
3
CONFIG_X86_64=Y
CONFIG_SMP=Y
CONFIG_NFS=M

4.5.8 重要的 x86_64 配置选项

以下列举 x86_64 架构下一些关键配置选项及其作用:

架构与处理器支持

  • CONFIG_X86_64:64 位 x86 内核,必须启用。这是整个 x86_64 架构支持的总开关。
  • CONFIG_SMP:对称多处理(Symmetric Multi-Processing),启用多核/多处理器支持。现代系统均应启用。
  • CONFIG_X86_5LEVEL:5 级页表支持。当物理内存超过 64TB(4 级页表上限)时需要启用此选项。
  • CONFIG_GENERIC_CPU / CONFIG_MCORE2 / CONFIG_MATOM 等:处理器微架构优化选项,选择与目标 CPU 匹配的选项可获得最佳性能。

抢占模型

  • CONFIG_PREEMPT_NONE:无抢占,服务器场景默认值,吞吐量最优
  • CONFIG_PREEMPT_VOLUNTARY:自愿抢占,在显式抢占点检查,是桌面场景的推荐选项
  • CONFIG_PREEMPT:完全抢占,除临界区外均可抢占,降低调度延迟
  • CONFIG_PREEMPT_RT:实时抢占(需 PREEMPT_RT 补丁),提供确定性延迟保证

定时器频率

  • CONFIG_HZ_100:100 Hz,适合服务器负载
  • CONFIG_HZ_250:250 Hz,桌面默认值
  • CONFIG_HZ_300:300 Hz(少见)
  • CONFIG_HZ_1000:1000 Hz,低延迟场景首选,但会增加系统开销

安全与加密

  • CONFIG_RANDOMIZE_BASE:内核地址空间布局随机化(KASLR),使内核虚拟地址在每次启动时随机偏移,是重要的安全防护机制
  • CONFIG_AMD_MEM_ENCRYPT:AMD SEV/SME(Secure Encrypted Virtualization / Secure Memory Encryption)支持,利用 AMD 处理器的内存加密功能保护虚拟机隔离
  • CONFIG_KASAN:内核地址消毒器(Kernel Address SANitizer),运行时检测越界访问和释放后使用漏洞
  • CONFIG_KMSAN:内核内存消毒器(Kernel Memory SANitizer),检测未初始化内存的使用

引导与固件

  • CONFIG_EFI_STUB:EFI Stub 支持,允许内核作为 EFI 应用程序直接被 UEFI 固件加载,无需中间引导加载器
  • CONFIG_EFI_MIXED:混合 EFI 模式,支持在 32 位 EFI 固件上启动 64 位内核
  • Title: Linux内核分析之基础知识-03
  • Author: 韩乔落
  • Created at : 2026-05-28 17:11:53
  • Updated at : 2026-05-28 18:39:58
  • Link: https://jelasin.github.io/2026/05/28/Linux内核分析之基础知识-03/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
Linux内核分析之基础知识-03