[TOC]

kernel/bounds.c 内核边界定义(Kernel Bounds Definition) 为汇编代码提供C语言的宏常量

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术以及其背后独特的构建过程,是为了解决一个根本性的、存在于所有操作系统内核开发中的问题:如何在底层汇编代码(Assembly Code)中,安全、准确地访问C语言定义的内核数据结构(structs)的成员偏移量和大小

  • 汇编代码的局限性:汇编语言是一种低级语言,它没有struct#define的概念。汇编代码需要知道一个结构体中某个成员的确切字节偏移量才能访问它。例如,为了访问task_struct结构中的state字段,汇编代码需要知道state字段距离task_struct结构体起始地址有多少个字节。
  • C语言编译器的不确定性:这些偏移量并不是固定的。它们会因为以下原因而改变:
    1. 架构差异:同一个结构体在32位和64位系统上的布局可能完全不同。
    2. 配置选项:不同的内核配置选项(Kconfig)可能会启用或禁用某些结构体成员,导致其后所有成员的偏移量发生变化。
    3. 编译器行为:不同的编译器或同一编译器的不同版本,可能会因为对齐(alignment)策略的差异而产生不同的结构体布局。
  • 硬编码的脆弱性:如果在汇编代码中硬编码这些偏移量(例如,#define TASK_STATE_OFFSET 8),那么一旦C语言中的结构体定义发生任何变化,汇编代码就会访问到错误的数据,导致难以调试的、灾难性的系统崩溃。

kernel/bounds.c及其构建机制就是为了在每次内核编译时,自动地、动态地生成这些偏移量,从而彻底解决了这个问题。

它的发展经历了哪些重要的里程碑或版本迭代?

这种“编译时计算偏移量”的技术是内核开发早期的基础创新之一。其发展主要体现在构建过程的优化和标准化上。

  • 早期脚本:最初可能是一些临时的、由awksed驱动的脚本来解析C头文件。
  • bounds.c的标准化:内核开发社区将这个过程标准化。创建了一个专门的C文件(kernel/bounds.c),并设计了一个巧妙的构建规则。这个C文件本身不包含任何逻辑,它只包含一系列DEFINE宏,用于打印出需要的信息。
  • 构建系统的集成:这个机制被深度集成到内核的Kbuild/Makefile系统中。Makefile中有一条规则,它会先编译bounds.c生成一个可执行文件,然后运行这个可执行文件,将其标准输出重定向到一个头文件(通常是include/generated/bounds.h)中。

目前该技术的社区活跃度和主流应用情况如何?

这是一个极其稳定、成熟且不可或缺的内核构建基础设施。

  • 社区活跃度kernel/bounds.c本身的代码几乎永远不会改变。社区的活动体现在添加新的宏定义。当一个新的内核特性需要在汇编代码中访问某个新的结构体成员时,开发者就会向bounds.c中添加一个新的DEFINE宏。
  • 主流应用:它是所有需要底层汇编代码的体系结构(x86, ARM, RISC-V等)的强制性构建步骤
    • 上下文切换:在切换任务时,汇编代码需要保存和恢复寄存器到task_structthread_struct的特定位置。
    • 系统调用入口:汇编代码需要从thread_info结构中获取信息。
    • 中断处理:汇编代码需要访问pt_regs结构来处理寄存器状态。

核心原理与设计

它的核心工作原理是什么?

其核心是一个非常巧妙的“两步构建”过程:

第一步:生成一个计算程序

  1. C源文件 (kernel/bounds.c):这个文件包含了一系列特殊的宏调用,看起来像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    #include <linux/kbuild.h>
    #include <linux/sched.h> // 包含 task_struct 的定义

    void foo(void) {
    DEFINE(TASK_STATE_OFS, offsetof(struct task_struct, state));
    DEFINE(TASK_STRUCT_SIZE, sizeof(struct task_struct));
    /* ... 更多定义 ... */
    }

    这里的DEFINE宏(定义在linux/kbuild.h中)非常关键,它实际上是一个printf的包装,会打印出类似#define TASK_STATE_OFS 8这样的字符串到标准输出。offsetofsizeof是C语言的编译时运算符。

  2. 编译:内核的Makefile会使用主机编译器(Host C Compiler)将kernel/bounds.c编译成一个主机上可以运行的可执行文件(例如,kernel/bounds)。

第二步:运行计算程序并生成头文件

  1. 执行Makefile会立即执行上一步生成的可执行文件kernel/bounds
  2. 重定向输出:执行的结果(即所有printf的输出)被重定向到一个新的头文件中,例如include/generated/bounds.h
  3. 生成头文件内容:这个生成的bounds.h文件内容看起来会是这样:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #ifndef __ASM_OFFSETS_H__
    #define __ASM_OFFSETS_H__
    /*
    * DO NOT MODIFY.
    *
    * This file was generated by Kbuild
    */

    #define TASK_STATE_OFS 8
    #define TASK_STRUCT_SIZE 8704
    /* ... 更多定义 ... */

    #endif

第三步:在汇编代码中使用

  1. 包含头文件:内核的底层汇编文件(.S文件)会#include <generated/bounds.h>
  2. 使用宏:现在,汇编代码就可以安全地使用这些宏了,例如(x86汇编示例):
    1
    movl TASK_STATE_OFS(%rbx), %eax  // %rbx 存储了 task_struct 的地址
    由于这个过程在每次内核配置或编译器改变后都会重新运行,所以bounds.h中的值总是与当前编译环境下的C结构体布局完全同步

它的主要优势体现在哪些方面?

  • 绝对的准确性:偏移量由编译器在编译时亲自计算,保证了100%的准确性。
  • 自动化:整个过程由make自动处理,开发者无需手动干预。
  • 健壮性和可维护性:C语言开发者可以自由地修改结构体,而无需担心破坏汇编代码。这极大地降低了维护成本,提高了代码的健壮性。
  • 跨平台/跨配置:同一套bounds.c和汇编代码可以无缝地在不同架构和不同内核配置下工作。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 构建复杂性:为内核的构建过程增加了一些额外的步骤和复杂性。但这点复杂性与其带来的巨大好处相比,是完全值得的。
  • 无法处理运行时信息:它只能定义编译时就能确定的常量。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

这是在内核中连接C和汇编世界的唯一、标准且首选的解决方案。

  • 任务切换代码:在arch/x86/entry/entry_64.S等文件中,__switch_to汇编函数需要保存当前任务的栈指针到task_struct->thread.sp,并从下一个任务的task_struct->thread.sp加载新的栈指针。THREAD_SP_OFS这个偏移量就是由bounds.c生成的。
  • 系统调用入口:当一个系统调用发生时,汇编入口代码需要访问当前进程的thread_info结构来检查标志位(例如,是否需要被追踪_TIF_SYSCALL_TRACE)。
  • 中断和异常处理:硬件在发生中断时,会将CPU寄存器保存在栈上的一个pt_regs结构中。汇编处理程序需要知道每个寄存器在该结构中的确切偏移量才能访问它们。

是否有不推荐使用该技术的场景?为什么?

  • 纯C代码:在纯C代码中,应该直接使用offsetof()sizeof(),而不需要经过这个间接的宏生成过程。bounds.c是专门为服务汇编代码而存在的。

对比分析

请将其 与 其他相似技术 进行详细对比。

对比 bounds.c机制 vs. 硬编码常量

特性 kernel/bounds.c 机制 硬编码常量 (Hard-coding)
准确性 100% 准确。与每次编译都同步。 极度脆弱。任何C结构体或配置的改变都可能使其失效。
可维护性 。C和汇编解耦。 极低。修改C代码后,必须手动检查并更新所有相关的汇编代码,极易出错。
可移植性 。自动适应不同架构、配置和编译器。 。为特定配置硬编码的值在其他配置下几乎肯定是错误的。
开发实践 现代内核开发的标准实践 绝对的反模式,在任何严肃的、可维护的项目中都应被禁止。

对比 bounds.c机制 vs. 脚本解析头文件

特性 bounds.c 机制 脚本解析 (e.g., awk/perl)
实现方式 利用C编译器自身的sizeof/offsetof能力。 尝试用脚本(如正则表达式)去“猜测”C代码的结构。
准确性 100% 准确。由权威的编译器计算。 不可靠。C语言的语法非常复杂,脚本解析很难处理所有边缘情况(如#ifdef, 嵌套结构, 位域, 对齐属性等),非常容易出错。
健壮性 。不受C代码格式或注释的影响。 。代码格式的微小改变都可能破坏脚本的解析。
依赖 仅依赖于构建链中的C编译器。 依赖于外部脚本解释器(如awk, perl, python)及其版本。
开发实践 内核的标准、健壮方案 一种临时的、脆弱的、不推荐的方案。

include/linux/page-flags.h

pageflags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/*
* 不要直接使用 pageflags。 使用 PageFoo 宏。
* Page flags 字段分为两部分,主标志区域从低位向上延伸,字段区域从高位向下延伸。
*
* |FIELD |... |FLAGS |
* N-1 ^ 0
* (NR_PAGEFLAGS)
* 字段区域保留给字段映射区域、节点(用于 NUMA)和 SPARSEMEM 部分(用于需要部分 ID 的 SPARSEMEM 变体,如 SPARSEMEM_EXTREME 与 !SPARSEMEM_VMEMMAP)。
*/
enum pageflags {
PG_locked, /* 页面被锁定。不要操作。 */
PG_writeback, /* 页面正在写回 */
PG_referenced, /* 页面被引用 */
PG_uptodate, /* 页面是最新的 */
PG_dirty, /* 页面已被修改 */
PG_lru, /* 页面在LRU链表中 */
PG_head, /* 必须是第6位 */
PG_waiters, /* 页面有等待者,请检查其等待队列。必须是第7位,并且与“PG_locked”在同一个字节中 */
PG_active, /* 页面是活动的 */
PG_workingset, /* 页面属于工作集 */
PG_owner_priv_1, /* 所有者使用。如果是页面缓存,文件系统可能会使用 */
PG_owner_2, /* 所有者使用。如果是页面缓存,文件系统可能会使用 */
PG_arch_1, /* 架构特定的标志 */
PG_reserved, /* 特殊页面,通常不应被触碰 */
PG_private, /* 如果是页面缓存,包含文件系统私有数据 */
PG_private_2, /* 如果是页面缓存,包含文件系统辅助数据 */
PG_reclaim, /* 应尽快回收 */
PG_swapbacked, /* 页面由RAM/交换支持 */
PG_unevictable, /* 页面是“不可驱逐的” */
PG_dropbehind, /* 在IO完成时丢弃页面 */
#ifdef CONFIG_MMU
PG_mlocked, /* 页面被vma锁定 */
#endif
#ifdef CONFIG_MEMORY_FAILURE
PG_hwpoison, /* 硬件损坏的页面。不要操作 */
#endif
#if defined(CONFIG_PAGE_IDLE_FLAG) && defined(CONFIG_64BIT)
PG_young, /* 页面最近被访问 */
PG_idle, /* 页面处于空闲状态 */
#endif
#ifdef CONFIG_ARCH_USES_PG_ARCH_2
PG_arch_2, /* 架构特定的标志 */
#endif
#ifdef CONFIG_ARCH_USES_PG_ARCH_3
PG_arch_3, /* 架构特定的标志 */
#endif
__NR_PAGEFLAGS, /* 页面标志的总数 */

PG_readahead = PG_reclaim, /* 预读页面 */

/* 匿名内存(和shmem) */
PG_swapcache = PG_owner_priv_1, /* 交换页面:swp_entry_t在private字段中 */
/* 一些文件系统 */
PG_checked = PG_owner_priv_1, /* 页面已检查 */

/*
* 根据匿名folio映射到页面表的方式(例如,头页面的单个PMD/PUD/CONT映射与PTE映射的THP),
* PG_anon_exclusive可能仅设置在头页面或匿名folio的尾页面上。目前,我们仅期望它在PTE映射的THP的尾页面上设置。
*/
PG_anon_exclusive = PG_owner_2,

/*
* 如果folio中的所有缓冲区头都已映射,则设置。
* 不使用缓冲区头的文件系统可以将其用于自己的目的。
*/
PG_mappedtodisk = PG_owner_2,

/* FS-Cache使用两个页面位来维护本地缓存状态。
* 当这些页面属于netfs的inode时,这些位会被设置。
*/
PG_fscache = PG_private_2, /* 页面由缓存支持 */

/* XEN */
/* 在Xen中作为只读页表页面固定。 */
PG_pinned = PG_owner_priv_1,
/* 作为域保存的一部分固定(参见xen_mm_pin_all())。 */
PG_savepinned = PG_dirty,
/* 具有另一个(外部)域页面的授权映射。 */
PG_foreign = PG_owner_priv_1,
/* 由swiotlb-xen重新映射。 */
PG_xen_remapped = PG_owner_priv_1,

/* 非LRU隔离的可移动页面 */
PG_isolated = PG_reclaim,

/* 仅对伙伴系统中的页面有效。用于跟踪已报告的页面 */
PG_reported = PG_uptodate,

#ifdef CONFIG_MEMORY_HOTPLUG
/* 用于自托管内存映射页面 */
PG_vmemmap_self_hosted = PG_owner_priv_1,
#endif

/*
* 仅对复合页面有效的标志。存储在第一个尾页面的flags字段中。
* 不能使用前8个标志或任何标记为PF_ANY的标志。
*/

/* folio中至少有一个页面设置了hwpoison标志 */
PG_has_hwpoisoned = PG_active,
PG_large_rmappable = PG_workingset, /* 匿名或文件支持 */
PG_partially_mapped = PG_reclaim, /* 被标识为部分映射 */
};

include/generated/bounds.h

  • 通过kernel/bounds.s生成的include/generated/bounds.h文件,定义了内核中使用的最大值和最小值。
  • 该文件包含了内核中使用的各种常量和限制的定义,例如最大页数、最大区域数等。
1
2
3
4
5
6
7
8
9
10
11
/*
* DO NOT MODIFY.
*
* This file was generated by Kbuild
*/

#define NR_PAGEFLAGS 20 /* __NR_PAGEFLAGS */
#define MAX_NR_ZONES 2 /* __MAX_NR_ZONES */
#define SPINLOCK_SIZE 0 /* sizeof(spinlock_t) */
#define LRU_GEN_WIDTH 0 /* 0 */
#define __LRU_REFS_WIDTH 0 /* 0 */
  • 查看Kbuild文件,可以看到kernel/bounds.s的编译规则
1
2
3
4
5
6
bounds-file := include/generated/bounds.h

targets := kernel/bounds.s

$(bounds-file): kernel/bounds.s FORCE
$(call filechk,offsets,__LINUX_BOUNDS_H__)

kernel/bounds.c

  • 生成.s文件的kernel/bounds.c文件,该文件主要用于生成内核中使用的各种常量和限制的定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//include/linux/kbuild.h
#define DEFINE(sym, val) \
asm volatile("\n.ascii \"->" #sym " %0 " #val "\"" : : "i" (val))

#define __GENERATING_BOUNDS_H
/* Include headers that define the enum constants of interest */
#include <linux/page-flags.h>
#include <linux/mmzone.h>
#include <linux/kbuild.h>
#include <linux/log2.h>
#include <linux/spinlock_types.h>

int main(void)
{
/* The enum constants to put into include/generated/bounds.h */
DEFINE(NR_PAGEFLAGS, __NR_PAGEFLAGS);
DEFINE(MAX_NR_ZONES, __MAX_NR_ZONES);
#ifdef CONFIG_SMP
DEFINE(NR_CPUS_BITS, order_base_2(CONFIG_NR_CPUS));
#endif
DEFINE(SPINLOCK_SIZE, sizeof(spinlock_t));
#ifdef CONFIG_LRU_GEN
DEFINE(LRU_GEN_WIDTH, order_base_2(MAX_NR_GENS + 1));
DEFINE(__LRU_REFS_WIDTH, MAX_NR_TIERS - 2);
#else
DEFINE(LRU_GEN_WIDTH, 0);
DEFINE(__LRU_REFS_WIDTH, 0);
#endif
/* End of constants */

return 0;
}