[TOC]

include/uapi/linux/elf.h

Elf32_Ehdr (内核中通常用 struct elfhdr): ELF文件头结构体

这个结构体是ELF文件格式的“封面”和“目录”,它必须位于任何一个ELF文件的最开头。加载器(如内核)通过读取并解析这个结构体,来了解如何处理文件的其余部分。

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
/*
* 这是32位ELF文件头的标准定义.
* 内核代码中通常会用 Elf32_Addr, Elf32_Half 等类型来保证在不同架构下的大小是固定的.
*/
typedef struct elf32_hdr {
/*
* e_ident: 一个16字节的数组, 用于识别文件. 它包含了多个子字段.
* - e_ident[EI_MAG0..EI_MAG3]: 存放着 '0x7F', 'E', 'L', 'F' 这个魔数(Magic Number).
* - e_ident[EI_CLASS]: 指定文件是32位(ELFCLASS32)还是64位(ELFCLASS64).
* - e_ident[EI_DATA]: 指定字节序是大端(ELFDATA2MSB)还是小端(ELFDATA2LSB).
* - e_ident[EI_VERSION]: ELF头的版本, 通常为 EV_CURRENT.
* - e_ident[EI_OSABI]: 指定文件所遵循的操作系统ABI, 例如 ELFOSABI_LINUX 或 ELFOSABI_ARM_FDPIC.
* - ... 其他字节通常被填充为0.
*/
unsigned char e_ident[EI_NIDENT];
/*
* e_type: ELF文件的类型. 这是区分可执行文件、共享库和目标文件的关键字段.
*/
Elf32_Half e_type;
/*
* e_machine: 目标CPU架构.
* 例如, EM_ARM 表示为ARM架构编译, EM_386 表示为x86架构编译.
* 内核通过此字段判断该文件能否在当前硬件上运行.
*/
Elf32_Half e_machine;
/*
* e_version: ELF格式的版本. 通常为1.
*/
Elf32_Word e_version;
/*
* e_entry: 程序的入口点地址.
* 当内核加载完程序后, CPU的程序计数器(PC)将跳转到这个地址开始执行.
* 对于FDPIC程序, 这个地址是相对于代码段基地址的偏移.
*/
Elf32_Addr e_entry; /* Entry point */
/*
* e_phoff: 程序头表(Program Header Table)在文件中的偏移量(以字节为单位).
* 内核根据这个值去文件中找到加载指令表.
*/
Elf32_Off e_phoff;
/*
* e_shoff: 段头表(Section Header Table)在文件中的偏移量.
* 段头表主要用于链接和调试, 在程序执行时非必需, 内核加载器通常不关心它.
*/
Elf32_Off e_shoff;
/*
* e_flags: 特定于架构的标志.
* 例如, 在ARM上, 它可以用来指示文件是否使用了硬件浮点单元等信息.
*/
Elf32_Word e_flags;
/*
* e_ehsize: ELF头自身的大小(以字节为单位).
*/
Elf32_Half e_ehsize;
/*
* e_phentsize: 程序头表中每个表项的大小(以字节为单位).
* 内核用它来验证文件格式的兼容性.
*/
Elf32_Half e_phentsize;
/*
* e_phnum: 程序头表中的表项数量.
* 内核用它和e_phentsize来计算整个程序头表的总大小.
*/
Elf32_Half e_phnum;
/*
* e_shentsize: 段头表中每个表项的大小.
*/
Elf32_Half e_shentsize;
/*
* e_shnum: 段头表中的表项数量.
*/
Elf32_Half e_shnum;
/*
* e_shstrndx: 段头表字符串表在段头表中的索引.
* 用于获取段的名称.
*/
Elf32_Half e_shstrndx;
} Elf32_Ehdr;

/* 定义了 e_type 字段可能取值的常量. */
#define ET_NONE 0 /* 未知类型 */
#define ET_REL 1 /* 可重定位文件 (.o 文件) */
#define ET_EXEC 2 /* 可执行文件 */
#define ET_DYN 3 /* 共享目标文件 (.so 文件) 或 位置无关可执行文件(PIE) */
#define ET_CORE 4 /* 核心转储文件 (Core dump) */
#define ET_LOPROC 0xff00 /* 特定于处理器的类型范围开始 */
#define ET_HIPROC 0xffff /* 特定于处理器的类型范围结束 */
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
/*
* 定义了 e_ident 数组中各个字节的索引值, 它们代表了不同的信息字段.
*/
#define EI_MAG0 0 /* e_ident[] 索引: 魔数第0字节 */
#define EI_MAG1 1 /* e_ident[] 索引: 魔数第1字节 */
#define EI_MAG2 2 /* e_ident[] 索引: 魔数第2字节 */
#define EI_MAG3 3 /* e_ident[] 索引: 魔数第3字节 */
#define EI_CLASS 4 /* e_ident[] 索引: 文件类别 (32位/64位) */
#define EI_DATA 5 /* e_ident[] 索引: 数据编码 (字节序) */
#define EI_VERSION 6 /* e_ident[] 索引: ELF 版本 */
#define EI_OSABI 7 /* e_ident[] 索引: 操作系统/ABI 标识 */
#define EI_PAD 8 /* e_ident[] 索引: 填充字节的开始, 此后的字节未使用, 必须为0 */

/*
* 定义了魔数(Magic Number)的各个字节的具体值.
* 内核通过比较文件的前四个字节与这些值来确认它是否是ELF文件.
*/
#define ELFMAG0 0x7f /* 魔数第0字节的值 (ASCII的DEL字符) */
#define ELFMAG1 'E' /* 魔数第1字节的值 ('E'的ASCII码) */
#define ELFMAG2 'L' /* 魔数第2字节的值 ('L'的ASCII码) */
#define ELFMAG3 'F' /* 魔数第3字节的值 ('F'的ASCII码) */
/*
* 将整个魔数定义为一个字符串字面量.
* "\177" 是 0x7f 的八进制表示法.
* 这个宏在内核代码中非常常用, 例如 memcmp(hdr->e_ident, ELFMAG, SELFMAG).
*/
#define ELFMAG "\177ELF"
/*
* 定义魔数的长度, 即4个字节.
*/
#define SELFMAG 4

/*
* 定义了 e_ident[EI_CLASS] 字段的可能取值, 用于表示文件的"位数".
*/
#define ELFCLASSNONE 0 /* 无效类别 */
#define ELFCLASS32 1 /* 32位目标文件 */
#define ELFCLASS64 2 /* 64位目标文件 */
#define ELFCLASSNUM 3 /* 合法类别的数量 */

/*
* 定义了 e_ident[EI_DATA] 字段的可能取值, 用于表示数据的字节序(Endianness).
*/
#define ELFDATANONE 0 /* 无效的数据编码 */
#define ELFDATA2LSB 1 /* 小端序(Least Significant Byte first). 像x86, ARM(大部分模式下)都是小端. */
#define ELFDATA2MSB 2 /* 大端序(Most Significant Byte first). 像MIPS, PowerPC可以是大端. */

/*
* 定义了 e_version 和 e_ident[EI_VERSION] 字段的可能取值.
*/
#define EV_NONE 0 /* 无效版本 */
#define EV_CURRENT 1 /* 当前版本. 几乎所有的ELF文件都使用这个值. */
#define EV_NUM 2 /* 合法版本的数量 */

/*
* 定义了 e_ident[EI_OSABI] 字段的可能取值, 用于标识文件所遵循的操作系统或ABI规范.
* 这对于区分不同Unix变体或特殊ABI下的二进制文件很有用.
*/
#define ELFOSABI_NONE 0 /* 未指定或与UNIX System V ABI兼容(通常是默认值) */
#define ELFOSABI_LINUX 3 /* 专门为Linux编译, 可能使用了Linux特有的特性 */

/*
* 这是一个预处理宏, 用于为当前编译的内核设置一个默认的OSABI.
* 如果在编译内核时没有明确定义 ELF_OSABI,
*/
#ifndef ELF_OSABI
/*
* 那么就将它默认定义为 ELFOSABI_NONE.
* 这确保了内核自身在处理某些内部事务时有一个默认的OSABI值可用.
*/
#define ELF_OSABI ELFOSABI_NONE
#endif

程序头表项类型 (p_type)

ELF文件的程序头表是一个数组,数组中的每个元素(一个elf_phdr结构体)都描述了文件中的一个“段”以及如何处理它。p_type字段就是用来指明这个段的类型的。加载器(内核)会遍历这个表,并根据p_type的值来采取不同的行动。

下面是对这些宏的逐一、详细解释:

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/*
* 这些常量用于表示存储在镜像头文件中的段类型 (Segment Types).
*/

/*
* 类型: 空段 (NULL).
* 作用: 表示这是一个未使用的或无效的程序头表项. 加载器会完全忽略这个表项.
* 它也经常作为数组的哨兵或占位符.
*/
#define PT_NULL 0

/*
* 类型: 可加载段 (LOAD).
* 作用: 这是最重要的类型. 它指示加载器将文件中的某一部分内容加载(或映射)到内存中.
* 一个典型的可执行文件至少有两个PT_LOAD段: 一个用于代码(.text, 只读可执行),
* 另一个用于数据(.data/.bss, 可读可写). 'elf_fdpic_map_file'函数的核心工作就是处理这些段.
*/
#define PT_LOAD 1

/*
* 类型: 动态链接信息 (DYNAMIC).
* 作用: 这个段包含了一系列用于动态链接的键值对信息, 例如需要链接哪些共享库、
* 符号重定位表在哪里等等. 用户空间的动态链接器(如ld.so)会读取这个段来完成链接工作.
* 内核加载器会找到这个段并将其地址通过辅助向量传递给动态链接器.
*/
#define PT_DYNAMIC 2

/*
* 类型: 解释器 (INTERP).
* 作用: 这个段只包含一个简单的、以NULL结尾的字符串, 即程序所需的动态链接器的路径
* (例如, "/lib/ld-uClibc.so.1"). 当内核加载器发现这个段时, 它就知道不能直接运行当前程序,
* 而是需要先加载并运行这个段中指定的解释器.
*/
#define PT_INTERP 3

/*
* 类型: 附加说明 (NOTE).
* 作用: 包含一些附加的、特定于供应商或系统的信息. 例如, GNU工具链用它来存储
* 构建ID (build-id), 以唯一标识一个二进制文件. coredump文件也用它来存储进程信息.
*/
#define PT_NOTE 4

/*
* 类型: 共享库 (SHLIB).
* 作用: 这个类型是保留的, 但在现代系统中已不再使用.
*/
#define PT_SHLIB 5

/*
* 类型: 程序头表 (PHDR).
* 作用: 这个段描述了程序头表本身在文件中的位置和大小.
* 这使得动态链接器可以找到并解析程序头表, 而无需依赖ELF头.
*/
#define PT_PHDR 6

/*
* 类型: 线程局部存储 (Thread Local Storage).
* 作用: 这个段描述了与线程局部存储(TLS)相关的模板. 当创建新线程时,
* 系统会使用这个模板为每个线程分配私有的TLS数据区.
*/
#define PT_TLS 7

/*
* 类型: 操作系统特定范围的开始 (OS-specific).
* 作用: 定义了一个范围, 从这个值开始, 用于操作系统特定的段类型.
* 这允许不同的操作系统(如Linux, BSD)定义自己独有的段类型而不会发生冲突.
*/
#define PT_LOOS 0x60000000

/*
* 类型: 操作系统特定范围的结束 (OS-specific).
*/
#define PT_HIOS 0x6fffffff

/*
* 类型: 处理器特定范围的开始.
* 作用: 定义了一个范围, 用于特定CPU架构的段类型.
*/
#define PT_LOPROC 0x70000000

/*
* 类型: 处理器特定范围的结束.
*/
#define PT_HIPROC 0x7fffffff

/*
* 以下是GNU扩展的、在操作系统特定范围(PT_LOOS)内的段类型.
* 它们是Linux生态中非常常见的段.
*/

/*
* 类型: GNU异常处理帧 (GNU Exception Handler Frame).
* (PT_LOOS + 0x474e550) 中的'0x474e550'实际上是"GNU"的ASCII码在特定字节序下的表现.
* 作用: 这个段指向一个用于C++等语言异常处理(try/catch)的栈回溯(unwinding)信息表.
*/
#define PT_GNU_EH_FRAME (PT_LOOS + 0x474e550)

/*
* 类型: GNU栈 (GNU STACK).
* 作用: 这个段本身不包含任何数据, 它的标志位(p_flags)被用来向操作系统传达
* 这个程序希望它的栈是可执行的还是不可执行的.
* 这是实现栈不可执行(NX bit)安全策略的关键. 'elf_fdpic_fetch_phdrs'会解析这个段.
*/
#define PT_GNU_STACK (PT_LOOS + 0x474e551)

/*
* 类型: GNU只读重定位 (GNU Read-Only Relocations).
* 作用: 这是一个安全增强特性. 它将一些在加载后就不再需要修改的动态链接数据
* (如全局偏移表GOT的一部分)所在的内存区域标记为只读.
* 这可以防止某些类型的攻击, 如GOT覆写攻击.
*/
#define PT_GNU_RELRO (PT_LOOS + 0x474e552)

/*
* 类型: GNU属性 (GNU PROPERTY).
* 作用: 一个较新的段类型, 用于向系统传递更丰富的二进制文件属性,
* 例如它是否使用了ARM的BTI(Branch Target Identification)或PAC(Pointer Authentication)等安全特性.
*/
#define PT_GNU_PROPERTY (PT_LOOS + 0x474e553)

fs/binfmt_script.c

#! 机制的本质是把执行权委托给另一个程序。下面我们来深入了解这些常见的“受委托者”以及它们各自的特长。

1. /bin/sh/bin/bash (Bourne Shell 和 Bourne-Again Shell)

  • 脚本作用:
    这是Linux/Unix世界中最基础、最核心的“胶水语言”。它的主要作用是自动化执行一系列命令,管理文件系统,控制进程,以及配置系统环境。sh是遵循POSIX标准的最小集,保证了极高的可移植性;bashsh的超集,提供了更多便利的功能,如命令历史、更强大的数组、更复杂的条件判断等。

  • 使用场景:
    它们是嵌入式Linux系统中无可替代的工具。

    • 系统启动脚本: 几乎所有的初始化脚本(位于/etc/init.d/或被systemd调用的脚本)都是shell脚本。它们负责挂载文件系统、配置网络、启动后台服务(守护进程)等。
    • 任务自动化 (Cron Jobs): 定时执行的任务,如夜间日志清理、数据备份、系统状态检查等,通常都由shell脚本完成。
    • 设备管理: 编写一个简单的脚本来配置GPIO、设置串口参数或重启一个外设驱动。
    • 简单的应用程序启动器: 编写一个包装脚本,在运行主程序前设置好必要的环境变量或检查依赖项。
  • 示例 (/usr/local/bin/start_myapp):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #!/bin/sh

    # 脚本功能:一个健壮的应用程序启动器

    APP_BIN="/opt/myapp/my_application"
    PID_FILE="/var/run/myapp.pid"

    # 检查应用程序二进制文件是否存在且可执行
    if [ ! -x "$APP_BIN" ]; then
    echo "错误: 应用程序 '$APP_BIN' 不存在或不可执行。"
    exit 1
    fi

    echo "正在启动 my_application..."

    # 在后台启动应用程序,并将它的进程ID(PID)写入文件
    # > /dev/null 2>&1 将所有输出重定向到空设备,实现静默运行
    $APP_BIN > /dev/null 2>&1 &
    echo $! > $PID_FILE

    echo "my_application 已启动, PID 为: $(cat $PID_FILE)"

2. /usr/bin/python3 (Python 3)

  • 脚本作用:
    Python是一种高级、通用的编程语言,以其清晰的语法和强大的标准库而闻名。与Shell脚本相比,它更适合处理复杂的逻辑、数据结构、网络通信和算法

  • 使用场景:

    • 数据采集与处理: 从传感器(如I2C、SPI接口的温湿度传感器)读取数据,进行解析、计算,然后存储到文件或数据库中。
    • 轻量级Web服务器: 使用Flask或Bottle等微型框架,在STM32H750上运行一个Web服务,提供一个用于设备监控和远程控制的API接口或Web页面。
    • 网络客户端: 主动连接到远程服务器,通过HTTP/HTTPS上传数据,或通过MQTT协议与物联网平台通信。
    • 替代复杂的Shell脚本: 当一个自动化任务的逻辑变得非常复杂,需要用到字典、列表、类等高级数据结构时,用Python实现会比用Shell更清晰、更易于维护。
  • 示例 (/opt/sensors/log_temp.py):

    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
    #!/usr/bin/python3

    # 脚本功能:模拟从传感器读取温度并记录到日志文件

    import datetime
    import random # 用于模拟传感器数据

    LOG_FILE = "/var/log/temperature.log"

    def get_temperature():
    # 在真实场景中, 这里会是与硬件交互的代码 (例如, 通过 spidev 或 smbus2)
    # 我们用一个随机数来模拟
    return 20.0 + random.uniform(-2.0, 2.0)

    def main():
    try:
    temp = get_temperature()
    timestamp = datetime.datetime.now().isoformat()
    log_entry = f"{timestamp} - Temperature: {temp:.2f} C\n"

    with open(LOG_FILE, "a") as f:
    f.write(log_entry)

    print(f"成功记录温度: {temp:.2f} C")

    except Exception as e:
    # 简单的错误处理
    print(f"错误: 无法记录温度 - {e}")

    if __name__ == "__main__":
    main()

3. /usr/bin/perl (Perl)

  • 脚本作用:
    Perl被誉为“文本处理的瑞士军刀”。它是一种非常强大的脚本语言,尤其擅长正则表达式和字符串操作。虽然近年来Python的使用更为广泛,但在系统管理和日志分析领域,Perl依然宝刀不老。

  • 使用场景:

    • 日志分析: 实时监控或离线分析系统日志(如 dmesg, /var/log/messages), 从大量文本中提取出特定的错误模式或关键信息。
    • 配置文件解析与生成: 读取复杂的、非标准格式的配置文件,解析其内容,并根据需要生成新的配置文件。
    • 报告生成: 从各种系统命令的输出中抓取数据,并将其格式化为人类可读的报告。
  • 示例 (/usr/local/bin/parse_auth_log):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #!/usr/bin/perl

    # 脚本功能:从auth.log文件中提取所有失败的SSH登录尝试

    use strict;
    use warnings;

    my $log_file = '/var/log/auth.log';

    open(my $fh, '<', $log_file) or die "无法打开 $log_file: $!";

    print "检测到失败的SSH登录尝试:\n";
    print "--------------------------\n";

    while (my $line = <$fh>) {
    # 使用正则表达式匹配包含 "Failed password for" 的行
    if ($line =~ /Failed password for (\S+) from (\S+)/) {
    my $user = $1;
    my $ip = $2;
    print "用户: $user, 来源IP: $ip\n";
    }
    }

    close($fh);

4. /usr/bin/awk -f

  • 脚本作用:
    AWK是一种优秀的面向列的文本处理工具。它逐行读取文本,并能自动将每一行按分隔符(默认为空格)拆分成多个字段($1, $2, $3…)。它非常适合从格式规整的文本数据中提取信息、进行计算和重新格式化输出。-f标志告诉awk命令,执行逻辑的脚本就写在当前文件中。

  • 使用场景:

    • 处理命令输出: 对ls -l, ps aux, df -h等命令的输出进行二次处理,只提取你关心的列。
    • 数据汇总: 计算文件中某一列的总和、平均值等。
    • 格式转换: 将一种格式的文本文件(如空格分隔)转换为另一种格式(如CSV逗号分隔)。
  • 示例 (/usr/local/bin/disk_usage_report.awk):

    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
    #!/usr/bin/awk -f

    # 脚本功能:处理'df -h'的输出,只显示使用率超过80%的文件系统
    # 使用方法: df -h | /usr/local/bin/disk_usage_report.awk

    # BEGIN块在处理任何输入行之前执行一次, 用于打印表头
    BEGIN {
    print "高使用率的文件系统 (>80%):"
    print "文件系统\t\t已用%"
    }

    # 这个模式块对每一行都执行
    # NR > 1 表示跳过第一行(表头)
    # $5 ~ /%/ 表示只处理第五列包含'%'符号的行
    NR > 1 && $5 ~ /%/ {
    # gsub函数用于替换, 这里去掉第五列的'%'符号, 以便进行数值比较
    gsub(/%/, "", $5)

    # 如果第五列(使用率)大于80
    if ($5 > 80) {
    # 打印第一列(文件系统名称)和第五列(使用率)
    printf "%-20s\t%s%%\n", $1, $5
    }
    }

    # END块在所有输入行处理完毕后执行一次
    END {
    print "检查完毕."
    }

5. #!/usr/bin/env 的巧妙用法

这不是一个解释器, 而是一个寻找解释器的工具env是一个标准的系统程序, 它的作用是在当前用户的PATH环境变量所指定的目录列表中, 查找并执行一个程序。

  • 问题: 如果你在脚本中硬编码 #!/usr/bin/python3, 但在另一台机器上, Python被安装在了 /usr/local/bin/python3。那么这个脚本在那台机器上就会因为找不到解释器而执行失败。

  • 解决方案: 使用 #!/usr/bin/env python3

  • 工作流程:

    1. 内核看到 #! 行, 它执行 /usr/bin/env 程序。
    2. 内核将 python3 和脚本路径 /home/user/test.py 作为参数传递给 env
    3. env 程序启动, 它看到第一个参数是 python3
    4. env 开始在 PATH 环境变量(如/usr/local/bin:/usr/bin:/bin)的所有目录中依次搜索一个名为 python3 的可执行文件。
    5. 一旦找到(比如在/usr/bin/python3), env 就会执行它, 并将脚本路径传递给它。
  • 优点: 极大地提高了脚本的可移植性。它不关心解释器到底安装在哪里, 只要它存在于用户的PATH中, 脚本就能正确执行。这在使用Python虚拟环境(venv)时尤其重要, 因为虚拟环境会修改PATH, 使得python3指向环境内部的解释器。

#! (Shebang) 支持哪些脚本?

这是一个非常好的问题, 也是一个常见的误区。内核的 #! 机制本身不关心、也不识别任何特定的脚本语言(如Shell, Python, Perl等)。

它的机制是完全通用的, 其原理是:“不要执行这个文件, 而是去执行 #! 后面指定的那一个程序, 并把这个脚本文件的路径作为参数传给它。”

因此, #! 支持任何已经安装了解释器的脚本语言。只要系统中存在一个可以接收脚本文件路径作为参数并执行其内容的可执行程序, 你就可以在 #! 后面使用它。

常见的解释器包括:

  • /bin/sh (标准的Bourne Shell)
  • /bin/bash (更强大的Bash Shell)
  • /usr/bin/python/usr/bin/python3
  • /usr/bin/perl
  • /usr/bin/awk -f
  • /usr/bin/node (用于Node.js脚本)

甚至一些不常见的程序也可以, 比如 #!/usr/bin/env 就是一个巧妙的用法, 它会利用 env 程序在用户的PATH环境变量中去查找真正的解释器(如 python), 增加了脚本的可移植性。


工作流程示例

下面我们通过两个具体的例子, 详细追踪 load_script 函数是如何工作的。

示例1: 一个简单的Shell脚本

场景: 用户在命令行执行一个带参数的shell脚本。

  1. 脚本文件内容 (/home/user/test.sh):

    1
    2
    #!/bin/sh
    echo "Hello from script! You gave me the argument: $1"
  2. 用户执行的命令:

    1
    /home/user/test.sh my_arg
  3. 内核 load_script 函数介入前的状态:

    • bprm->filename: “/home/user/test.sh”
    • bprm->argc: 2
    • 内核看到的初始参数列表 (argv):
      • argv[0]: /home/user/test.sh
      • argv[1]: my_arg
  4. load_script 函数执行步骤详解:

    • 行31: (bprm->buf[0] != '#') || (bprm->buf[1] != '!') 判断为 false, 因为文件以 #! 开头, 匹配成功。
    • 行39-57: 解析 #! 行。
      • i_name 被设置为指向字符串 /bin/sh
      • i_argNULL, 因为 /bin/sh 后面没有其他内容。
    • 行80: retval = remove_arg_zero(bprm);
      • 移除原始的 argv[0], 即 “/home/user/test.sh”。
      • bprm->argc 变为 1。
    • 行83-86: retval = copy_string_kernel(bprm->interp, bprm);
      • 将原始文件名 “/home/user/test.sh” 作为新的参数压入。
      • bprm->argc 变为 2。
      • 参数列表现在逻辑上是: (my_arg, /home/user/test.sh)
    • 行89-95: if (i_arg) 判断为 false, 跳过。
    • 行96-99: retval = copy_string_kernel(i_name, bprm);
      • 将解释器名 “/bin/sh” 作为新的参数压入。
      • bprm->argc 变为 3。
      • 参数列表现在逻辑上是: (/bin/sh, /home/user/test.sh, my_arg)
    • 行100-103: retval = bprm_change_interp(i_name, bprm);
      • bprm->interp 从 “/home/user/test.sh” 更改为 “/bin/sh”。
    • 行108-111: file = open_exec(i_name);
      • 内核尝试打开 /bin/sh 这个文件以供执行。
    • 行113: bprm->interpreter = file;
      • bprm 结构体中要执行的程序文件对象, 设置为刚刚打开的 /bin/sh
    • 行115: return 0; 函数成功返回。
  5. 最终结果:

    • load_script 函数修改了 bprm 结构, 告诉内核的 execve 流程:“不要执行原来的脚本了, 请执行 /bin/sh 这个程序”。
    • 内核最终执行的命令是: /bin/sh /home/user/test.sh my_arg
    • /bin/sh 解释器启动后, 看到它的第一个参数是 /home/user/test.sh, 于是它打开这个文件, 读取并执行里面的指令。第二个参数 my_arg 则成为脚本内部的 $1

示例2: #! 行带有参数

场景: 一个python脚本, 在 #! 行上就为解释器提供了一个参数。

  1. 脚本文件内容 (/home/user/test.py):

    1
    2
    3
    4
    5
    #!/usr/bin/python3 -u
    # The -u flag forces unbuffered output for stdout and stderr
    import sys
    print("Script running")
    print("Argv is:", sys.argv)
  2. 用户执行的命令:

    1
    /home/user/test.py
  3. 内核 load_script 函数介入前的状态:

    • bprm->filename: “/home/user/test.py”
    • bprm->argc: 1
    • 内核看到的初始参数列表 (argv):
      • argv[0]: /home/user/test.py
  4. load_script 函数执行步骤详解:

    • 行31: 匹配 #! 成功。
    • 行39-57: 解析 #! 行。
      • i_name 被设置为指向字符串 /usr/bin/python3
      • i_sep 指向 python3-u 之间的空格。
      • i_arg 被设置为指向字符串 -u
    • 行80: retval = remove_arg_zero(bprm);
      • 移除原始的 argv[0], 即 “/home/user/test.py”。
      • bprm->argc 变为 0。
    • 行83-86: retval = copy_string_kernel(bprm->interp, bprm);
      • 将 “/home/user/test.py” 作为新参数压入。
      • bprm->argc 变为 1。
      • 参数列表逻辑上是: (/home/user/test.py)
    • 行89-95: if (i_arg) 判断为 true
      • retval = copy_string_kernel(i_arg, bprm);-u 作为新参数压入。
      • bprm->argc 变为 2。
      • 参数列表逻辑上是: (-u, /home/user/test.py)
    • 行96-99: retval = copy_string_kernel(i_name, bprm);
      • 将 “/usr/bin/python3” 作为新参数压入。
      • bprm->argc 变为 3。
      • 参数列表逻辑上是: (/usr/bin/python3, -u, /home/user/test.py)
    • 后续步骤同示例1, bprm->interp 被改为 /usr/bin/python3, 并被成功打开。
  5. 最终结果:

    • 内核最终执行的命令是: /usr/bin/python3 -u /home/user/test.py
    • 这样, -u 这个参数就成功地从脚本的 #! 行传递给了 python3 解释器。

希望这两个详细的例子能帮助您彻底理解 load_script 的工作原理和 #! 机制的强大之处。

binfmt_script.c: 内核脚本执行支持

此文件的作用是实现一个内核级的二进制格式(binfmt)处理器, 专门用于识别并处理以 #! (Shebang) 开头的脚本文件。当用户尝试执行一个脚本时, 内核通过此文件中实现的 load_script 函数, 解析脚本的第一行, 找出指定的解释器(如/bin/sh), 然后加载并运行该解释器来执行脚本内容。这个机制是所有现代Linux/Unix系统能够原生执行shell脚本、Python脚本等的基础。

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/* 一个内联辅助函数, 用于判断一个字符是否是空格或制表符. */
static inline bool spacetab(char c) { return c == ' ' || c == '\t'; }
/*
* 一个内联辅助函数, 用于从指定范围 [first, last] 内查找并返回第一个非空格/制表符的字符的指针.
* 如果找不到, 返回 NULL.
*/
static inline const char *next_non_spacetab(const char *first, const char *last)
{
for (; first <= last; first++)
if (!spacetab(*first))
return first;
return NULL;
}
/*
* 一个内联辅助函数, 用于从指定范围 [first, last] 内查找并返回第一个终止符(空格/制表符/字符串结束符)的指针.
* 如果找不到, 返回 NULL.
*/
static inline const char *next_terminator(const char *first, const char *last)
{
for (; first <= last; first++)
if (spacetab(*first) || !*first)
return first;
return NULL;
}

/*
* load_script: 二进制格式处理器的核心函数.
* 当内核认为一个文件可能是脚本时, 会调用此函数.
* @bprm: 指向 linux_binprm 结构体的指针, 它包含了要执行的文件的信息,
* 如文件的前128字节(bprm->buf), 参数列表等.
* @return: 成功时返回0, 失败时返回负值的错误码.
*/
static int load_script(struct linux_binprm *bprm)
{
/*
* i_name: 指向解释器名称的指针.
* i_sep: 指向解释器名称和参数之间分隔符的指针.
* i_arg: 指向解释器参数的指针.
* i_end: 指向'#!'行有效内容末尾的指针.
* buf_end: 指向bprm->buf缓冲区末尾的指针, 用于防止越界.
*/
const char *i_name, *i_sep, *i_arg, *i_end, *buf_end;
/* file: 指向解释器可执行文件的文件对象. */
struct file *file;
/* retval: 用于存储函数调用的返回值. */
int retval;

/* 检查文件的起始两个字节是否为'#!'. 如果不是, 则这不是一个脚本文件, 我们不处理. */
/* 返回 -ENOEXEC 告知内核尝试下一个二进制格式处理器. */
if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))
return -ENOEXEC;

/*
* 下面的代码块负责解析'#!'行, 将其分解为解释器路径和可选的参数字符串.
* 必须小心处理, 因为bprm->buf中的内容不保证是以NUL(空字符)结尾的.
* 我们不希望执行一个被截断的解释器路径, 所以我们要么找到一个换行符(说明行是完整的),
* 要么在解释器路径后找到一个空格/制表符/NUL. 参数被截断是可以接受的.
*/
/* 计算出bprm->buf缓冲区的末尾地址, 用于边界检查. */
buf_end = bprm->buf + sizeof(bprm->buf) - 1;
/* 在缓冲区中查找第一个换行符'\n', 它标志着'#!'行的结束. */
i_end = strnchr(bprm->buf, sizeof(bprm->buf), '\n');
/* 如果没有找到换行符, 说明'#!'行可能因为文件太小而被截断了. */
if (!i_end) {
/* 从'#!'之后开始查找第一个非空格/制表符的字符. */
i_end = next_non_spacetab(bprm->buf + 2, buf_end);
/* 如果整个缓冲区都是空格/制表符, 这是一个无效的脚本. */
if (!i_end)
return -ENOEXEC;
/*
* 如果在找到的第一个非空字符之后, 再也找不到任何空格/制表符/NUL,
* 我们必须假设解释器的路径被截断了, 这是一个不安全的情况.
*/
if (!next_terminator(i_end, buf_end))
return -ENOEXEC;
/* 如果没有换行符, 我们将行的末尾视为缓冲区的末尾. */
i_end = buf_end;
}
/* 从后向前, 修剪掉行末尾的所有空格或制表符. */
while (spacetab(i_end[-1]))
i_end--;

/* 跳过'#!'后面可能存在的前导空格/制表符, 找到解释器名称的起始位置. */
i_name = next_non_spacetab(bprm->buf+2, i_end);
/* 如果没有找到解释器名称 (例如, '#! '后面全是空格), 则返回错误. */
if (!i_name || (i_name == i_end))
return -ENOEXEC;

/* 检查是否存在可选的参数. */
i_arg = NULL;
/* 查找解释器名称后面的第一个终止符 (空格/制表符/NUL). */
i_sep = next_terminator(i_name, i_end);
/* 如果找到了一个非NUL的终止符, 说明后面可能有参数. */
if (i_sep && (*i_sep != '\0'))
/* 在这个终止符之后, 查找下一个非空格/制表符的字符, 那就是参数的开始. */
i_arg = next_non_spacetab(i_sep, i_end);

/*
* 如果脚本文件名在exec之后将变得不可访问(例如, 它是一个指向将要被关闭的文件描述符的路径),
* 那么现在就放弃执行. 因为我们假定解释器自己也需要读取这个脚本文件.
*/
if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
return -ENOENT;

/*
* 我们已经解析出了解释器名称和(可选的)参数.
* 现在我们需要重新组织bprm中的参数列表, 以便执行解释器.
* 新的参数列表将是:
* argv[0]: 解释器名称 (例如, "/bin/sh")
* argv[1]: (可选的)'#!'行中的参数
* argv[2]: 脚本文件的路径 (它替换了旧的argv[0])
* ...: 原始的其他参数
*
* 这个过程是反向操作的, 因为用户空间参数是以栈的形式存储的.
*/
/* 移除原始的argv[0] (即脚本文件的名称). */
retval = remove_arg_zero(bprm);
if (retval)
return retval;
/* 将原始的脚本文件名(bprm->interp)作为最后一个参数压入. */
retval = copy_string_kernel(bprm->interp, bprm);
if (retval < 0)
return retval;
bprm->argc++;
/* 在i_end位置写入NUL, 确保从bprm->buf中复制字符串时能正确终止. */
*((char *)i_end) = '\0';
/* 如果存在'#!'行中的参数. */
if (i_arg) {
/* 在解释器和参数的分隔符处写入NUL, 将它们分开. */
*((char *)i_sep) = '\0';
/* 将'#!'行中的参数压入参数列表. */
retval = copy_string_kernel(i_arg, bprm);
if (retval < 0)
return retval;
bprm->argc++;
}
/* 将解释器名称压入参数列表, 它将成为新的argv[0]. */
retval = copy_string_kernel(i_name, bprm);
if (retval)
return retval;
bprm->argc++;
/* 更改bprm中的解释器字段, 以记录新的解释器路径. */
retval = bprm_change_interp(i_name, bprm);
if (retval < 0)
return retval;

/*
* 现在用解释器的路径来重新开始执行流程.
*/
/* 调用 open_exec 尝试打开解释器的可执行文件. */
file = open_exec(i_name);
/* 检查文件打开是否成功. IS_ERR宏用于判断返回的指针是否是错误码. */
if (IS_ERR(file))
/* PTR_ERR宏从指针中提取出负值的错误码. */
return PTR_ERR(file);

/* 将打开的解释器文件对象存入bprm->interpreter. */
bprm->interpreter = file;
/* 返回0表示成功, 内核的execve流程将继续, 但这次加载的是bprm->interpreter指向的文件. */
return 0;
}

/*
* 定义一个静态的 'linux_binfmt' 结构体实例, 名为 script_format.
* 它将'#!'脚本这种"格式"与它的加载函数'load_script'绑定在一起.
*/
static struct linux_binfmt script_format = {
.module = THIS_MODULE,
.load_binary = load_script,
};

/*
* init_script_binfmt: 内核模块的初始化函数.
* __init 属性表示此函数仅在内核启动时执行.
*/
static int __init init_script_binfmt(void)
{
/* 调用 register_binfmt 将 script_format 注册到内核的二进制格式处理器列表中. */
register_binfmt(&script_format);
return 0;
}

/*
* exit_script_binfmt: 内核模块的退出函数.
* __exit 属性表示此函数仅在模块卸载时执行.
*/
static void __exit exit_script_binfmt(void)
{
/* 调用 unregister_binfmt 从内核列表中注销 script_format 处理器. */
unregister_binfmt(&script_format);
}

/* 将 init_script_binfmt 注册为核心初始化调用, 确保在系统启动早期被执行. */
core_initcall(init_script_binfmt);
/* 将 exit_script_binfmt 注册为模块卸载时的退出函数. */
module_exit(exit_script_binfmt);
/* 模块的描述信息, 可通过 modinfo 工具查看. */
MODULE_DESCRIPTION("Kernel support for scripts starting with #!");
/* 模块的许可证. */
MODULE_LICENSE("GPL");

fs/binfmt_elf_fdpic.c

ELF FDPIC Binary Format Support: 在无MMU系统上实现多进程

此代码片段的作用是为Linux内核注册一个二进制格式处理器(binfmt),专门用于加载和执行一种特殊类型的ELF(Executable and Linkable Format)文件,即 FDPIC(Function-Descriptor Position-Independent Code) 格式的可执行文件。

这个功能对于在没有内存管理单元(MMU)的微控制器(如STM32H750)上运行一个功能完整的、支持多进程的Linux系统(通常称为uClinux)是绝对核心和必需的

FDPIC的原理与在STM32H750上的关键作用

  1. 无MMU系统面临的挑战:
    在一个标准的、有MMU的系统上,当您运行两个相同的程序实例(例如,两个httpd进程)时,MMU会为每个进程创建一个独立的虚拟地址空间。这意味着,虽然它们共享同一份物理代码(.text段),但每个进程都有自己私有的、位于相同虚拟地址的数据段(.data.bss)。它们可以自由地修改自己的全局变量,而不会影响到对方。

    在没有MMU的STM32H750上,地址就是物理地址。如果两个进程直接加载同一个普通ELF文件,它们将共享同一份物理代码同一份物理数据段。如果一个进程修改了一个全局变量,这个修改会立即对另一个进程可见,这将导致灾难性的后果和系统崩溃。

  2. FDPIC的解决方案:
    FDPIC是一种为解决这个问题而设计的、精巧的编译和加载方案。

    • 编译: 编译器会生成一种特殊的位置无关代码。在这种代码中,对全局变量或静态变量的访问不是通过绝对地址,而是通过一个基地址寄存器加上一个偏移量来进行的。
    • 加载 (load_elf_fdpic_binary): 当load_elf_fdpic_binary函数加载一个FDPIC程序时,它会:
      • 将程序的代码段(.text)加载到内存中。这份代码段只会被加载一次,并由所有运行该程序的进程共享
      • 对于每一个要创建的新进程,加载器都会在内存中分配一套全新的、私有的数据段(.data)和BSS段(.bss)。
      • 在创建进程和进行上下文切换时,内核会负责将这个进程私有数据段的基地址加载到那个专用的基地址寄存器中。
  3. 最终效果:
    当CPU执行共享的代码时,无论它访问哪个全局变量,实际上都是通过“基地址寄存器(指向私有数据区) + 偏移量”来找到正确的位置。这样,进程A的指令访问的是A的数据区,进程B的指令访问的是B的数据区,它们之间完美隔离,互不干扰。FDPIC巧妙地用软件和编译器约定,模拟出了MMU对数据段的隔离效果

因此,binfmt_elf_fdpic是使真正的多任务处理在像STM32H750这样的高性能MCU上成为可能的基石。


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
/*
* 定义一个静态的 'linux_binfmt' 结构体实例, 名为 elf_fdpic_format.
* 这个结构体向内核注册了一个处理 FDPIC ELF 格式二进制文件的处理器.
*/
static struct linux_binfmt elf_fdpic_format = {
/*
* .module: 指向当前模块的指针 (THIS_MODULE).
* 用于管理模块的引用计数, 确保在处理器还在使用时, 模块不会被卸载.
*/
.module = THIS_MODULE,
/*
* .load_binary: 一个函数指针, 指向加载此格式二进制文件的核心函数.
* 当内核识别出一个文件是 FDPIC ELF 格式时, 就会调用 load_elf_fdpic_binary.
* 这个函数会执行我们上面描述的加载逻辑: 加载共享的代码段, 并为新进程分配私有的数据段.
*/
.load_binary = load_elf_fdpic_binary,
/*
* 仅当内核配置了CONFIG_ELF_CORE (支持生成ELF格式的crash dump)时, 才编译以下部分.
*/
#ifdef CONFIG_ELF_CORE
/*
* .core_dump: 一个函数指针, 指向当此格式的程序崩溃时, 用于生成核心转储(core dump)文件的函数.
* 这对于事后调试非常重要.
*/
.core_dump = elf_fdpic_core_dump,
/*
* .min_coredump: 指定生成核心转储所需的最小文件大小.
* 通常设置为一个页的大小 (ELF_EXEC_PAGESIZE).
*/
.min_coredump = ELF_EXEC_PAGESIZE,
#endif
};

/*
* init_elf_fdpic_binfmt: 内核模块的初始化函数.
* 标记为 __init, 表示它仅在内核启动期间执行.
*/
static int __init init_elf_fdpic_binfmt(void)
{
/*
* 调用 register_binfmt, 将我们定义的 elf_fdpic_format 处理器注册到内核的二进制格式处理器列表中.
* 从此, 内核在执行 execve 系统调用时, 就会考虑 FDPIC ELF 这种格式.
*/
register_binfmt(&elf_fdpic_format);
return 0;
}

/*
* exit_elf_fdpic_binfmt: 内核模块的退出函数.
* 标记为 __exit, 表示它仅在模块卸载时执行.
*/
static void __exit exit_elf_fdpic_binfmt(void)
{
/*
* 调用 unregister_binfmt, 从内核列表中注销 elf_fdpic_format 处理器.
* 这是注册操作的反向过程, 用于安全地移除该功能.
*/
unregister_binfmt(&elf_fdpic_format);
}

/*
* 使用 core_initcall() 将 init_elf_fdpic_binfmt 注册为一个核心初始化调用.
* 这确保了对 FDPIC ELF 格式的支持在系统启动的早期阶段就已经可用,
* 以便系统能够执行用这种格式编译的用户空间程序.
*/
core_initcall(init_elf_fdpic_binfmt);
/*
* 使用 module_exit() 宏指定当模块被卸载时要调用的退出函数.
*/
module_exit(exit_elf_fdpic_binfmt);

is_elf: 验证一个文件是否为可执行的ELF文件

此函数是一个验证函数,它的核心作用是根据ELF文件格式规范,对一个文件的头部信息进行一系列快速检查,以判断该文件是否是一个当前系统可以理解并尝试执行的ELF二进制文件。它像是一个门卫,只有通过了它所有检查的文件,才会被内核的ELF加载器进一步处理。

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
/*
* is_elf: 检查一个文件是否是当前系统可以处理的有效ELF文件.
*
* @hdr: 指向从文件头部读出的 elfhdr 结构体的指针.
* @file: 指向该文件的 'struct file' 对象的指针.
* @return: 如果是有效的、可处理的ELF文件, 返回1 (true); 否则返回0 (false).
*/
static int is_elf(struct elfhdr *hdr, struct file *file)
{
/*
* 第1步: 检查魔数 (Magic Number). 这是最首要、最关键的检查.
* memcmp: 是一个内存比较函数, 它比较两块内存区域的内容.
* hdr->e_ident: 这是ELF头中一个16字节的数组, 其前4个字节包含了魔数.
* ELFMAG: 是一个宏, 定义了标准的ELF魔数 {'\x7f', 'E', 'L', 'F'}.
* SELFMAG: 是一个宏, 定义了魔数的长度 (4).
* 这行代码的意思是: "比较文件头的前4个字节是否与标准的ELF魔数完全相同".
* 如果不相同 (memcmp返回值不为0), 说明这不是一个ELF文件.
*/
if (memcmp(hdr->e_ident, ELFMAG, SELFMAG) != 0)
return 0; /* 立即返回false. */

/*
* 第2步: 检查文件类型.
* hdr->e_type: 这个字段描述了ELF文件的类型.
* ET_EXEC: 表示这是一个可执行文件.
* ET_DYN: 表示这是一个共享目标文件, 在现代Linux中, 它也用于位置无关可执行文件(PIE).
* 在STM32H750的uClinux上, 几乎所有的可执行文件都是 ET_DYN 类型的PIE/FDPIC文件.
* 这行代码的意思是: "我们只尝试加载可执行文件或PIE, 不加载.o之类的目标文件".
*/
if (hdr->e_type != ET_EXEC && hdr->e_type != ET_DYN)
return 0; /* 如果类型不匹配, 返回false. */

/*
* 第3步: 检查目标架构.
* 调用 elf_check_arch 辅助函数. 这个函数会检查ELF头中的 e_machine 字段.
* e_machine 字段记录了这个文件是为哪种CPU架构编译的.
* elf_check_arch 会判断这个架构是否与当前系统正在运行的CPU架构(例如, ARM)相匹配.
* 你不能在ARM架构的STM32H750上运行一个为x86编译的程序.
*/
if (!elf_check_arch(hdr))
return 0; /* 如果架构不匹配, 返回false. */

/*
* 第4步: 检查文件系统是否支持内存映射(mmap).
* file->f_op: 指向与该文件所在文件系统相关的文件操作函数表.
* file->f_op->mmap: 是这个函数表中的一个函数指针. mmap是加载ELF文件的核心操作,
* 它将文件的代码段和数据段映射到内存中.
* 这行代码的意思是: "如果这个文件所在的文件系统(例如一个特殊的虚拟文件系统)
* 根本不支持mmap操作, 那么我们就无法加载它".
* 在STM32H750上, 存放可执行文件的文件系统 (如FAT32, JFFS2, Ext4等) 都会支持mmap.
*/
if (!file->f_op->mmap)
return 0; /* 如果不支持mmap, 返回false. */

/*
* 如果以上所有检查都通过了, 说明这是一个当前系统可以尝试加载的ELF文件.
*/
return 1; /* 返回true. */
}

elf_fdpic_fetch_phdrs: 读取并解析ELF程序头表

此函数是FDPIC ELF加载器中的一个关键步骤。它的核心作用是根据ELF头中提供的信息,从可执行文件中读取整个程序头表(Program Header Table)到内核内存中,并对表中的特定项进行初步解析,以获取重要信息,如请求的栈大小和栈的可执行权限

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/*
* read the program headers table into memory
* 将程序头表读入内存
*/
/*
* @params: 指向 elf_fdpic_params 结构体的指针, 函数将把读取和解析的结果存入其中.
* @file: 指向要读取的ELF文件的 'struct file' 对象.
* @return: 成功时返回0, 失败时返回负值的错误码.
*/
static int elf_fdpic_fetch_phdrs(struct elf_fdpic_params *params,
struct file *file)
{
/*
* phdr: 一个指针, 用于在读取的程序头表中进行遍历.
*/
struct elf_phdr *phdr;
/*
* size: 用于存储整个程序头表所需的总字节数.
*/
unsigned long size;
/*
* retval: 用于存储函数调用的返回值.
* loop: 用于循环的计数器.
*/
int retval, loop;
/*
* pos: 文件读取的偏移量.
* 从ELF头中的 e_phoff 字段获取程序头表的起始位置在文件中的偏移量.
*/
loff_t pos = params->hdr.e_phoff;

/*
* 安全性检查 1: 检查ELF头中记录的每个程序头表项的大小(e_phentsize)
* 是否与内核定义的 'struct elf_phdr' 大小完全一致.
* 如果不一致, 说明文件格式有误或与内核不兼容.
*/
if (params->hdr.e_phentsize != sizeof(struct elf_phdr))
return -ENOMEM; /* 返回错误. -ENOMEM在这里实际上意味着格式不兼容. */
/*
* 安全性检查 2: 检查程序头表的总数(e_phnum)是否过大.
* 65536U 是一个合理的上限值, 防止因文件中一个畸大的数值而导致内核分配过多内存.
*/
if (params->hdr.e_phnum > 65536U / sizeof(struct elf_phdr))
return -ENOMEM;

/*
* 计算读取整个程序头表所需的总内存大小.
* 总大小 = 表项数量 * 每个表项的大小.
*/
size = params->hdr.e_phnum * sizeof(struct elf_phdr);
/*
* 使用 kmalloc 为程序头表分配内核内存.
* GFP_KERNEL 是标准的内核内存分配标志.
*/
params->phdrs = kmalloc(size, GFP_KERNEL);
/*
* 检查内存是否分配成功.
*/
if (!params->phdrs)
return -ENOMEM; /* 如果失败, 返回内存不足错误. */

/*
* 调用 kernel_read, 从文件的指定位置(pos)读取整个程序头表(大小为size)到新分配的内存中.
*/
retval = kernel_read(file, params->phdrs, size, &pos);
/*
* 检查读取操作的结果. unlikely() 是一个编译器优化提示.
* 必须确保实际读取的字节数与期望的完全一致.
*/
if (unlikely(retval != size))
/*
* 如果不一致, 如果 retval < 0, 说明发生了I/O错误, 直接返回该错误码.
* 否则 (retval >= 0 但 < size), 说明文件被意外截断, 返回 -ENOEXEC (无效的执行格式).
*/
return retval < 0 ? retval : -ENOEXEC;

/*
* 遍历刚刚读入的程序头表, 以确定此二进制文件请求的栈属性.
*/
/* phdr 指针指向程序头表的起始位置. */
phdr = params->phdrs;
/* 遍历所有表项. */
for (loop = 0; loop < params->hdr.e_phnum; loop++, phdr++) {
/*
* 检查当前表项的类型是否是 PT_GNU_STACK.
* 这是一个由GNU工具链添加的特殊段, 专门用来向操作系统传达栈的属性.
*/
if (phdr->p_type != PT_GNU_STACK)
continue; /* 如果不是, 继续检查下一个表项. */

/*
* 如果找到了 PT_GNU_STACK 段, 检查其 p_flags 字段.
* PF_X 标志位代表"可执行"(eXecutable).
* 如果 PF_X 被设置, 说明程序请求一个可执行的栈.
*/
if (phdr->p_flags & PF_X)
/* 在 params->flags 中记录下这个请求. */
params->flags |= ELF_FDPIC_FLAG_EXEC_STACK;
else
/* 否则, 记录下栈是不可执行的. */
params->flags |= ELF_FDPIC_FLAG_NOEXEC_STACK;

/*
* PT_GNU_STACK 段的 p_memsz 字段通常被用来建议一个默认的栈大小.
* 将这个值存入 params->stack_size.
*/
params->stack_size = phdr->p_memsz;
/*
* 按照规范, 一个ELF文件中最多只有一个 PT_GNU_STACK 段.
* 既然已经找到了, 就可以提前结束循环.
*/
break;
}

/*
* 所有操作成功, 返回0.
*/
return 0;
}

elf_fdpic_map_file_constdisp_on_uclinux: 在uClinux上映射固定位移的文件

此函数专门用于处理那些其内部段(segment)之间需要保持固定相对位置的FDPIC ELF文件。它的核心策略是:一次性地分配一块足够大的、连续的物理内存块,然后像“贴图”一样,将文件中的各个PT_LOAD段精确地复制到这块大内存的不同偏移处。 这块大内存就成为了新进程的代码和私有数据区。

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/*
* map a file with constant displacement under uClinux
* 在uClinux下映射一个具有固定位移的文件
*/
/*
* @params: 指向 elf_fdpic_params 结构体的指针.
* @file: 指向要加载的文件的 file 对象.
* @mm: 指向当前进程的内存描述符 (mm_struct) 的指针.
* @return: 成功时返回0, 失败时返回负值的错误码.
*/
static int elf_fdpic_map_file_constdisp_on_uclinux(
struct elf_fdpic_params *params,
struct file *file,
struct mm_struct *mm)
{
/*
* seg: 用于填充加载映射表(loadmap)中段信息的指针.
* phdr: 用于遍历程序头表的指针.
* load_addr: 期望的加载地址.
* base: 所有LOAD段中最小的虚拟地址(vaddr).
* top: 所有LOAD段中最大的虚拟地址 + 大小.
* maddr: 分配到的大块连续物理内存的起始地址.
* loop, ret: 循环计数器和返回值.
*/
struct elf_fdpic_loadseg *seg;
struct elf_phdr *phdr;
unsigned long load_addr, base = ULONG_MAX, top = 0, maddr = 0;
int loop, ret;

/*
* 从参数结构体中获取期望的加载地址和加载映射表的指针.
*/
load_addr = params->load_addr;
seg = params->loadmap->segs;

/*
* 第一步: 扫描程序头表, 计算出需要分配的连续内存块的总大小.
* 这个大小由所有PT_LOAD段所覆盖的虚拟地址范围决定.
*/
phdr = params->phdrs;
for (loop = 0; loop < params->hdr.e_phnum; loop++, phdr++) {
/*
* 我们只关心需要被加载到内存的段.
*/
if (params->phdrs[loop].p_type != PT_LOAD)
continue;

/*
* 寻找所有LOAD段中最小的虚拟地址, 作为整个内存块的虚拟基地址.
*/
if (base > phdr->p_vaddr)
base = phdr->p_vaddr;
/*
* 寻找所有LOAD段覆盖范围的最高点, 作为整个内存块的虚拟顶地址.
*/
if (top < phdr->p_vaddr + phdr->p_memsz)
top = phdr->p_vaddr + phdr->p_memsz;
}

/*
* 第二步: 根据计算出的总大小 (top - base), 分配一块大的、匿名的、连续的物理内存.
* vm_mmap 在无MMU系统上是一个物理内存分配器.
* @ NULL, load_addr: 提示期望的加载地址.
* @ top - base: 要分配的总大小.
* @ PROT_...|PROT_EXEC: 请求的内存权限为可读、可写、可执行.
* @ MAP_PRIVATE: 这是一个私有映射, 对内存的修改不会写回文件.
* @ 0: 文件偏移量, 对于匿名映射为0.
*/
maddr = vm_mmap(NULL, load_addr, top - base,
PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE, 0);
/*
* 检查内存分配是否成功. IS_ERR_VALUE 用于判断一个无符号长整型是否是一个错误码.
*/
if (IS_ERR_VALUE(maddr))
return (int) maddr; /* 如果失败, 返回错误码. */

/*
* 这行代码用于共享库的加载, 如果有多个库, 确保它们被加载到不同的地址.
*/
if (load_addr != 0)
load_addr += PAGE_ALIGN(top - base);

/*
* 第三步: 再次遍历程序头表, 将每个PT_LOAD段的内容复制到大内存块(maddr)中对应的位置.
*/
phdr = params->phdrs;
for (loop = 0; loop < params->hdr.e_phnum; loop++, phdr++) {
if (params->phdrs[loop].p_type != PT_LOAD)
continue;

/*
* 关键计算: 确定当前段在物理内存块(maddr)中的最终位置.
* 物理地址 = 物理基地址 + (段的虚拟地址 - 虚拟基地址).
* 这保证了所有段在物理内存中的相对位移和它们在虚拟地址空间中的完全一致.
*/
seg->addr = maddr + (phdr->p_vaddr - base);
/*
* 填充加载映射表(loadmap)的当前表项.
*/
seg->p_vaddr = phdr->p_vaddr;
seg->p_memsz = phdr->p_memsz;

/*
* 调用 read_code, 从文件中读取段的内容(大小为p_filesz)到计算出的物理地址(seg->addr).
*/
ret = read_code(file, seg->addr, phdr->p_offset,
phdr->p_filesz);
if (ret < 0) /* 如果读取失败, 返回错误. */
return ret;

/*
* 如果当前段包含了文件的开头(p_offset == 0), 那么ELF头也被加载进来了.
* 记录下这个地址, 动态链接器可能需要它.
*/
if (phdr->p_offset == 0)
params->elfhdr_addr = seg->addr;

/*
* 处理 .bss 段: 如果段在内存中的大小(p_memsz)大于在文件中的大小(p_filesz),
* 那么多出来的部分就是未初始化的全局变量(.bss).
*/
if (phdr->p_filesz < phdr->p_memsz) {
/*
* 调用 clear_user 将这部分内存区域清零, 这是C语言规范所要求的.
*/
if (clear_user((void *) (seg->addr + phdr->p_filesz),
phdr->p_memsz - phdr->p_filesz))
return -EFAULT; /* 如果清零失败, 返回错误. */
}

/*
* 如果mm不为NULL(即正在加载主可执行文件), 更新进程内存描述符中的代码段和数据段范围.
*/
if (mm) {
/*
* 如果段是可执行的(PF_X), 它就是代码段.
*/
if (phdr->p_flags & PF_X) {
if (!mm->start_code) { /* 只记录第一个代码段的范围. */
mm->start_code = seg->addr;
mm->end_code = seg->addr +
phdr->p_memsz;
}
} else if (!mm->start_data) { /* 否则, 它是数据段. */
mm->start_data = seg->addr;
mm->end_data = seg->addr + phdr->p_memsz;
}
}

/*
* 移动到加载映射表的下一个表项, 准备填充下一个LOAD段的信息.
*/
seg++;
}

/*
* 所有段都成功加载, 返回0.
*/
return 0;
}

elf_fdpic_map_file_by_direct_mmap: 逐个映射PT_LOAD

此函数的核心策略是遍历程序头表,并为每一个 PT_LOAD 类型的段单独调用vm_mmap。这种方法更加灵活,因为它不要求所有段都被加载到一块连续的物理内存中。

在这种模式下:

  • 独立分配: 内核会为代码段分配一块物理内存,为数据段分配另一块(可能不相邻的)物理内存。
  • vm_mmap的行为: 在无MMU系统上,vm_mmap从文件中映射内存的行为简化为:
    1. 调用物理内存分配器(如kmalloc)分配一块大小合适的、私有的物理内存。
    2. 调用文件系统的read操作,将文件中的数据复制到这块新分配的物理内存中。
  • FDPIC的魔力: 即使代码段和数据段被加载到了物理上不相邻的内存区域,FDPIC机制依然能正常工作。因为对全局变量的访问是通过“基地址寄存器(指向私有数据段) + 偏移量”来完成的,这个基地址寄存器的值是在程序加载时根据数据段的实际物理位置确定的。所以无论数据段在哪里,程序总能找到它。

这种方式的缺点是可能会产生更多的内存碎片,并且无法保证段之间的固定相对位置,但优点是为链接器和加载器提供了更大的灵活性。


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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/*
* map a binary by direct mmap() of the individual PT_LOAD segments
* 通过对单个PT_LOAD段直接进行mmap()来映射一个二进制文件
*/
/*
* @params: 指向 elf_fdpic_params 结构体的指针.
* @file: 指向要加载的文件的 file 对象.
* @mm: 指向当前进程的内存描述符 (mm_struct) 的指针.
* @return: 成功时返回0, 失败时返回负值的错误码.
*/
static int elf_fdpic_map_file_by_direct_mmap(struct elf_fdpic_params *params,
struct file *file,
struct mm_struct *mm)
{
/*
* seg: 用于填充加载映射表(loadmap)中段信息的指针.
* phdr: 用于遍历程序头表的指针.
* load_addr: 期望的加载基地址.
* delta_vaddr: 用于计算固定位移模式下的地址偏移.
* loop: 循环计数器.
* dvset: 一个标志, 用于标记delta_vaddr是否已被设置.
*/
struct elf_fdpic_loadseg *seg;
struct elf_phdr *phdr;
unsigned long load_addr, delta_vaddr;
int loop, dvset;

/*
* 初始化变量.
*/
load_addr = params->load_addr;
delta_vaddr = 0;
dvset = 0;

/*
* 指针指向加载映射表的第一个段.
*/
seg = params->loadmap->segs;

/*
* 核心部分: 遍历程序头表, 对每个PT_LOAD段分别进行处理.
*/
phdr = params->phdrs;
for (loop = 0; loop < params->hdr.e_phnum; loop++, phdr++) {
/*
* maddr: 将要映射到的内存地址.
* disp: 段的虚拟地址(vaddr)相对于页边界的偏移.
* excess: .bss段的大小.
* prot: 内存保护标志 (读/写/执行).
* flags: mmap的标志.
*/
unsigned long maddr, disp, excess;
int prot = 0, flags;

/*
* 我们只关心需要被加载到内存的PT_LOAD段.
*/
if (phdr->p_type != PT_LOAD)
continue;

/* 内核调试打印: 显示当前正在处理的LOAD段的虚拟地址、文件偏移、文件大小和内存大小. */
kdebug("[LOAD] va=%lx of=%lx fs=%lx ms=%lx",
(unsigned long) phdr->p_vaddr,
(unsigned long) phdr->p_offset,
(unsigned long) phdr->p_filesz,
(unsigned long) phdr->p_memsz);

/*
* 根据程序头中的p_flags, 确定内存的保护属性.
*/
if (phdr->p_flags & PF_R) prot |= PROT_READ;
if (phdr->p_flags & PF_W) prot |= PROT_WRITE;
if (phdr->p_flags & PF_X) prot |= PROT_EXEC;

/*
* 默认的mmap标志为私有映射.
*/
flags = MAP_PRIVATE;
maddr = 0; /* 默认让内核自动选择加载地址. */

/*
* 根据FDPIC的排列方式标志, 调整mmap的参数.
*/
switch (params->flags & ELF_FDPIC_FLAG_ARRANGEMENT) {
case ELF_FDPIC_FLAG_INDEPENDENT:
/* PT_LOAD段可独立重定位. 无需特殊操作. */
break;

case ELF_FDPIC_FLAG_HONOURVADDR:
/* 必须遵守指定的虚拟地址. */
maddr = phdr->p_vaddr; /* 将期望地址设置为vaddr. */
flags |= MAP_FIXED; /* 使用 MAP_FIXED 强制映射到该地址. */
break;

case ELF_FDPIC_FLAG_CONSTDISP:
/* 固定位移模式. */
if (!dvset) { /* 如果是第一个LOAD段 */
maddr = load_addr; /* 使用传入的基地址. */
delta_vaddr = phdr->p_vaddr; /* 记录第一个段的虚拟地址作为基准. */
dvset = 1; /* 设置标志. */
} else { /* 对于后续的段 */
/* 计算期望地址, 保持与第一个段的相对位移不变. */
maddr = load_addr + phdr->p_vaddr - delta_vaddr;
flags |= MAP_FIXED;
}
break;

case ELF_FDPIC_FLAG_CONTIGUOUS:
/* 连续模式, 地址将在后面计算. */
break;

default:
BUG(); /* 不应该出现其他情况. */
}

/*
* 将期望的映射地址按页对齐.
*/
maddr &= PAGE_MASK;

/*
* 调用 vm_mmap 来执行实际的内存映射.
* disp: 计算段的vaddr没有对齐到页边界的部分, 这部分也要包含在映射中.
* phdr->p_offset - disp: 调整文件偏移, 以匹配对齐后的内存地址.
*/
disp = phdr->p_vaddr & ~PAGE_MASK;
maddr = vm_mmap(file, maddr, phdr->p_memsz + disp, prot, flags,
phdr->p_offset - disp);

/* 内核调试打印: 显示mmap的详细参数和返回值. */
kdebug("mmap[%d] <file> sz=%llx pr=%x fl=%x of=%llx --> %08lx",
loop, (unsigned long long) phdr->p_memsz + disp,
prot, flags, (unsigned long long) phdr->p_offset - disp,
maddr);

/* 检查mmap是否失败. */
if (IS_ERR_VALUE(maddr))
return (int) maddr;

/* 如果是连续模式, 更新下一个段的期望加载地址. */
if ((params->flags & ELF_FDPIC_FLAG_ARRANGEMENT) ==
ELF_FDPIC_FLAG_CONTIGUOUS)
load_addr += PAGE_ALIGN(phdr->p_memsz + disp);

/*
* 填充加载映射表(loadmap)的当前项.
* seg->addr 是段在内存中的真正起始地址 (maddr + disp).
*/
seg->addr = maddr + disp;
seg->p_vaddr = phdr->p_vaddr;
seg->p_memsz = phdr->p_memsz;

/* 如果当前段包含了ELF头, 记录其在内存中的地址. */
if (phdr->p_offset == 0)
params->elfhdr_addr = seg->addr;

/*
* 清理页内偏移(disp)产生的、在段内容开始之前的"空隙".
*/
if (prot & PROT_WRITE && disp > 0) {
kdebug("clear[%d] ad=%lx sz=%lx", loop, maddr, disp);
if (clear_user((void __user *) maddr, disp))
return -EFAULT;
maddr += disp;
}

/*
* 清理 .bss 段 (内存大小 > 文件大小的部分).
*/
excess = phdr->p_memsz - phdr->p_filesz;

#ifdef CONFIG_MMU
/* 在有MMU的系统上, .bss段的处理比较复杂, 因为它可能跨越页,
* 并且需要为超出文件映射范围的部分创建匿名的、清零的内存页. */
// ... (MMU特定处理逻辑)
#else
/* 在无MMU系统上, 因为整个段都在一块连续的物理内存中, 处理很简单. */
if (excess > 0) {
kdebug("clear[%d] ad=%llx sz=%lx", loop,
(unsigned long long) maddr + phdr->p_filesz,
excess);
/* 直接调用clear_user将这部分内存清零. */
if (clear_user((void *) maddr + phdr->p_filesz, excess))
return -EFAULT;
}
#endif

/*
* 更新进程内存描述符中的代码段和数据段范围.
*/
if (mm) {
if (phdr->p_flags & PF_X) { /* 可执行段是代码 */
if (!mm->start_code) {
mm->start_code = maddr;
mm->end_code = maddr + phdr->p_memsz;
}
} else { /* 否则是数据 */
if (!mm->start_data) {
mm->start_data = maddr;
mm->end_data = maddr + phdr->p_memsz;
}
}
}

/*
* 移动到加载映射表的下一个表项.
*/
seg++;
}

/*
* 所有段都成功加载, 返回0.
*/
return 0;
}

elf_fdpic_map_file: 将FDPIC程序映射到内存

此函数的核心职责是:根据程序头表(phdrs)的指示,为所有需要加载的段(PT_LOAD)在物理内存中分配空间,并将文件中的相应内容复制进去。它会创建一个加载映射表(loadmap),详细记录每个段被加载到了哪个物理地址,并将这个映射表提供给后续步骤和用户空间的动态链接器使用。

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/*
* load the appropriate binary image (executable or interpreter) into memory
* 将合适二进制镜像(可执行文件或解释器)加载到内存中
* - we assume no MMU is available
* 我们假定没有MMU可用 (这是uClinux FDPIC加载器的核心思想)
* - if no other PIC bits are set in params->hdr->e_flags
* - we assume that the LOADable segments in the binary are independently relocatable
* 我们假定二进制文件中的可加载(LOADable)段是可独立重定位的
* - we assume R/O executable segments are shareable
* 我们假定只读的可执行段是可共享的
* - else
* - we assume the loadable parts of the image to require fixed displacement
* 我们假定镜像的可加载部分需要固定的位移
* - the image is not shareable
* 镜像是不可共享的
*/
/*
* @params: 指向 elf_fdpic_params 结构体的指针, 包含了ELF文件的所有解析信息.
* @file: 指向要加载的文件的 file 对象.
* @mm: 指向当前进程的内存描述符 (mm_struct) 的指针.
* @what: 一个描述性字符串 ("executable" 或 "interpreter"), 用于调试打印.
* @return: 成功时返回0, 失败时返回负值的错误码.
*/
static int elf_fdpic_map_file(struct elf_fdpic_params *params,
struct file *file,
struct mm_struct *mm,
const char *what)
{
/*
* loadmap: 指向将要创建的FDPIC加载映射表的指针.
*/
struct elf_fdpic_loadmap *loadmap;
#ifdef CONFIG_MMU
/* 在有MMU的系统上, 这些变量用于内存段的合并优化. */
struct elf_fdpic_loadseg *mseg;
unsigned long load_addr;
#endif
/*
* seg: 用于遍历加载映射表中各个段的指针.
* phdr: 用于遍历程序头表的指针.
* nloads:记录文件中PT_LOAD段的总数.
* tmp: 一个临时变量.
* stop: 用于地址范围计算的临时变量.
* loop: 用于循环的计数器.
* ret: 用于存储函数调用的返回值.
*/
struct elf_fdpic_loadseg *seg;
struct elf_phdr *phdr;
unsigned nloads, tmp;
unsigned long stop;
int loop, ret;

/*
* 第一步: 统计文件中PT_LOAD段的数量, 以便为加载映射表分配大小刚好的内存.
*/
nloads = 0;
/* 遍历程序头表. */
for (loop = 0; loop < params->hdr.e_phnum; loop++)
/* 如果一个程序头项的类型是PT_LOAD, 计数器加1. */
if (params->phdrs[loop].p_type == PT_LOAD)
nloads++;

/* 如果一个可执行文件没有任何PT_LOAD段, 这是一个无效的文件. */
if (nloads == 0)
return -ELIBBAD; /* 返回库格式错误. */

/*
* 使用 kzalloc 分配加载映射表(loadmap)所需的内存.
* struct_size 是一个宏, 用于计算一个包含柔性数组成员的结构体的总大小.
* 这里它计算 'struct elf_fdpic_loadmap' 头部加上 'nloads' 个 'elf_fdpic_loadseg' 元素所需的总字节数.
*/
loadmap = kzalloc(struct_size(loadmap, segs, nloads), GFP_KERNEL);
/* 检查内存分配是否成功. */
if (!loadmap)
return -ENOMEM;

/* 将新创建的loadmap与params关联起来. */
params->loadmap = loadmap;

/* 初始化loadmap的版本号和段的数量. */
loadmap->version = ELF_FDPIC_LOADMAP_VERSION;
loadmap->nsegs = nloads;

/*
* 第二步: 根据FDPIC的排列方式标志, 调用不同的映射函数将文件段加载到内存.
*/
switch (params->flags & ELF_FDPIC_FLAG_ARRANGEMENT) {
/* 如果ELF文件要求其各段之间保持固定位移或需要连续加载. */
case ELF_FDPIC_FLAG_CONSTDISP:
case ELF_FDPIC_FLAG_CONTIGUOUS:
#ifndef CONFIG_MMU
/*
* 在无MMU的uClinux上, 这是最常见也是最关键的路径.
* 调用一个专门的函数, 它会一次性地分配一块足够大的连续物理内存,
* 然后根据程序头表, 将文件的各个段(代码、数据)精确地复制到这块大内存的不同偏移处.
* 这个函数是实现FDPIC数据段隔离的核心.
*/
ret = elf_fdpic_map_file_constdisp_on_uclinux(params, file, mm);
if (ret < 0)
return ret;
break;
#endif
/* 对于其他情况 (如段可独立重定位). */
default:
/*
* 调用另一个映射函数. 在无MMU系统上, 这通常意味着为每个段独立分配物理内存.
*/
ret = elf_fdpic_map_file_by_direct_mmap(params, file, mm);
if (ret < 0)
return ret;
break;
}

/*
* 第三步: 在所有段都加载完毕后, 计算一些关键地址的最终物理位置.
*/
/* 映射程序的入口点地址. */
if (params->hdr.e_entry) { /* 如果ELF头中指定了入口点 */
seg = loadmap->segs; /* 指针指向加载映射表的第一个段. */
/* 遍历加载映射表中的所有段. */
for (loop = loadmap->nsegs; loop > 0; loop--, seg++) {
/* 检查入口点的虚拟地址(e_entry)是否落在当前段的虚拟地址范围内. */
if (params->hdr.e_entry >= seg->p_vaddr &&
params->hdr.e_entry < seg->p_vaddr + seg->p_memsz) {
/*
* 如果找到了, 计算出入口点的最终物理地址.
* 公式: 物理地址 = (入口点虚拟地址 - 段虚拟基地址) + 段物理基地址.
*/
params->entry_addr =
(params->hdr.e_entry - seg->p_vaddr) +
seg->addr;
break; /* 找到后即可退出循环. */
}
}
}

/*
* 确定程序头表(如果它本身也被加载了)在内存中的位置.
*/
stop = params->hdr.e_phoff; /* 计算程序头表在文件中的范围. */
stop += params->hdr.e_phnum * sizeof (struct elf_phdr);
phdr = params->phdrs; /* 指针指向内核中缓存的程序头表. */

/* 遍历程序头表. */
for (loop = 0; loop < params->hdr.e_phnum; loop++, phdr++) {
if (phdr->p_type != PT_LOAD) /* 我们只关心被加载的段. */
continue;

/* 检查程序头表是否位于当前这个LOAD段所加载的文件范围内. */
if (phdr->p_offset > params->hdr.e_phoff ||
phdr->p_offset + phdr->p_filesz < stop)
continue;

/* 如果是, 遍历加载映射表找到这个LOAD段被加载到了哪里. */
seg = loadmap->segs;
for (loop = loadmap->nsegs; loop > 0; loop--, seg++) {
/* 通过虚拟地址进行匹配. */
if (phdr->p_vaddr >= seg->p_vaddr &&
phdr->p_vaddr + phdr->p_filesz <=
seg->p_vaddr + seg->p_memsz) {
/*
* 计算出程序头表在内存中的最终物理地址.
* 公式复杂一些, 因为要考虑文件内和内存内的多重偏移.
*/
params->ph_addr =
(phdr->p_vaddr - seg->p_vaddr) +
seg->addr +
params->hdr.e_phoff - phdr->p_offset;
break;
}
}
break;
}

/*
* 确定动态链接段(PT_DYNAMIC)在内存中的位置.
*/
phdr = params->phdrs;
/* 遍历程序头表, 寻找PT_DYNAMIC段. */
for (loop = 0; loop < params->hdr.e_phnum; loop++, phdr++) {
if (phdr->p_type != PT_DYNAMIC)
continue;

/* 遍历加载映射表, 找到PT_DYNAMIC段所在的LOAD段的加载信息. */
seg = loadmap->segs;
for (loop = loadmap->nsegs; loop > 0; loop--, seg++) {
/* 通过虚拟地址进行匹配. */
if (phdr->p_vaddr >= seg->p_vaddr &&
phdr->p_vaddr + phdr->p_memsz <=
seg->p_vaddr + seg->p_memsz) {
Elf_Dyn __user *dyn;
Elf_Sword d_tag;

/* 计算出PT_DYNAMIC段在内存中的最终物理地址. */
params->dynamic_addr =
(phdr->p_vaddr - seg->p_vaddr) +
seg->addr;

/*
* 对动态段进行一个简单的完整性检查.
* 确保它的大小不为0, 且是Elf_Dyn结构体大小的整数倍.
*/
if (phdr->p_memsz == 0 ||
phdr->p_memsz % sizeof(Elf_Dyn) != 0)
goto dynamic_error;

/* 检查动态段的最后一项是否是DT_NULL(d_tag为0), 这是规范要求. */
tmp = phdr->p_memsz / sizeof(Elf_Dyn);
dyn = (Elf_Dyn __user *)params->dynamic_addr;
if (get_user(d_tag, &dyn[tmp - 1].d_tag) ||
d_tag != 0)
goto dynamic_error;
break;
}
}
break;
}

/*
* 在有MMU的Linux上, 可以对加载映射表进行优化, 将物理上相邻的段合并成一个.
* 在无MMU的uClinux上, 因为段之间的空隙可能被其他进程或系统占用, 所以不能进行这种合并.
* 因此这部分代码被 #ifdef CONFIG_MMU 包围, 在STM32H750的no-MMU配置下不会被编译.
*/
#ifdef CONFIG_MMU
// ... (合并逻辑)
#endif

/*
* 第四步: 打印调试信息, 显示所有映射结果.
*/
kdebug("Mapped Object [%s]:", what);
kdebug("- elfhdr : %lx", params->elfhdr_addr);
kdebug("- entry : %lx", params->entry_addr);
kdebug("- PHDR[] : %lx", params->ph_addr);
kdebug("- DYNAMIC[]: %lx", params->dynamic_addr);
seg = loadmap->segs;
for (loop = 0; loop < loadmap->nsegs; loop++, seg++)
kdebug("- LOAD[%d] : %08llx-%08llx [va=%llx ms=%llx]",
loop,
(unsigned long long) seg->addr,
(unsigned long long) seg->addr + seg->p_memsz - 1,
(unsigned long long) seg->p_vaddr,
(unsigned long long) seg->p_memsz);

/* 所有操作成功, 返回0. */
return 0;

dynamic_error: /* 动态段错误处理标签. */
/* 打印一条详细的错误信息, 指出哪个文件以及哪个inode出了问题. */
printk("ELF FDPIC %s with invalid DYNAMIC section (inode=%lu)\n",
what, file_inode(file)->i_ino);
/* 返回库格式错误. */
return -ELIBBAD;
}

create_elf_fdpic_tables: 在新进程的栈上创建初始信息表

此函数的主要职责是将execve系统调用传递进来的参数(argv)、环境变量(envp)以及内核需要告知用户空间动态链接器的一些关键信息(辅助向量),从内核空间复制并排列到新进程的用户空间栈上。这个过程必须精确无误,因为动态链接器和C库的启动代码将依赖这个布局来初始化程序。

  1. 确定栈顶: sp = mm->start_stack;。在无MMU系统上,start_stack就是物理内存块的最高地址。
  2. 传递参数/环境: transfer_args_to_stack()这个函数(在此代码片段中未显示,但逻辑相似)会把之前准备好的argvenvp字符串从内核的临时缓冲区复制到这个新栈的较高地址处。
  3. 压入FDPIC加载映射表: 这是FDPIC机制的核心。它将exec_params->loadmap(以及解释器的interp_params->loadmap)这个描述了“哪个段被加载到了哪个物理地址”的映射表,完整地复制到栈上。
  4. 构建辅助向量 (Auxiliary Vector): 这是最关键的部分。辅助向量是一个由{类型, 值}键值对组成的列表,它在栈上传递了内核与用户空间动态链接器之间的“秘密情报”。
    • NEW_AUX_ENT(AT_FDPIC_LOADMAP, ...) (此函数中没有直接出现,但由AT_BASE等间接实现): 这条信息会把刚刚压入栈的加载映射表的地址告知动态链接器。链接器收到后,就会去这个地址读取映射表,从而知道代码段和数据段的真实物理位置。
    • NEW_AUX_ENT(AT_ENTRY, exec_params->entry_addr): 告知链接器,原始可执行文件的入口点在哪里。
    • NEW_AUX_ENT(AT_PHDR, exec_params->ph_addr): 告知链接器,程序头表被加载到了哪里。
    • 其他如AT_PAGESZ(页大小)、AT_UID(用户ID)等提供了必要的系统信息。
  5. 构建argvenvp指针数组: 在栈的更低地址处,它会创建两个指针数组。argv数组的每个元素指向栈上对应参数字符串的起始地址。envp数组也一样。这两个数组都以一个NULL指针结尾。
  6. 压入argc: 最后,在栈的最底部(最低地址处),压入参数的个数argc

当所有这些都完成后,新进程的用户空间栈就完全准备好了。start_thread函数会将CPU的栈指针(SP)设置为sp的最终值。当动态链接器开始执行时,它会从这个栈指针开始,按照ABI规范,准确地找到argcargvenvp和至关重要的辅助向量,从而完成最后的符号重定位和程序初始化,最终跳转到main函数。


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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
/*
* create_elf_fdpic_tables: present useful information to the program by
* shovelling it onto the new process's stack
* 通过将有用的信息“铲到”新进程的栈上, 来呈现给程序
*/
/*
* @bprm: 指向 linux_binprm 结构体的指针, 包含了argv/envp等信息.
* @mm: 指向新进程的内存描述符.
* @exec_params: 主可执行文件的FDPIC参数.
* @interp_params: 解释器(动态链接器)的FDPIC参数.
* @return: 成功时返回0, 失败时返回负值的错误码.
*/
static int create_elf_fdpic_tables(struct linux_binprm *bprm,
struct mm_struct *mm,
struct elf_fdpic_params *exec_params,
struct elf_fdpic_params *interp_params)
{
/*
* cred: 指向当前进程的凭证结构体.
* sp: 栈指针(Stack Pointer), 它将从栈顶(高地址)向下移动.
* csp: 一个临时的栈指针, 用于计算.
* nitems: 辅助向量(auxv)中的项目总数.
* argv, envp: 指向用户空间中argv和envp数组的指针.
* ...其他变量用于字符串和长度计算.
*/
const struct cred *cred = current_cred();
unsigned long sp, csp, nitems;
elf_caddr_t __user *argv, *envp;
size_t platform_len = 0, len;
char *k_platform, *k_base_platform;
char __user *u_platform, *u_base_platform, *p;
int loop;
unsigned long flags = 0;
int ei_index;
elf_addr_t *elf_info;

#ifdef CONFIG_MMU
/* 在有MMU的系统上, 栈指针需要进行架构特定的对齐. */
sp = arch_align_stack(bprm->p);
#else
/* 在无MMU系统上, 栈指针直接从内存描述符中记录的栈顶开始. */
sp = mm->start_stack;

/*
* 将参数和环境变量的字符串本身从内核临时缓冲区复制到新栈的顶部.
* transfer_args_to_stack会更新sp的值.
*/
if (transfer_args_to_stack(bprm, &sp) < 0)
return -EFAULT;
/* 确保栈指针是16字节对齐的, 这是许多ABI的要求. */
sp &= ~15;
#endif

/*
* 将平台标识字符串(如"armv7l")复制到栈上.
* 这允许程序知道自己正在什么平台上运行.
*/
k_platform = ELF_PLATFORM;
u_platform = NULL;
if (k_platform) {
platform_len = strlen(k_platform) + 1;
sp -= platform_len; /* 栈向下增长, "分配"空间. */
u_platform = (char __user *) sp;
/* 将内核空间的字符串复制到用户空间的栈上. */
if (copy_to_user(u_platform, k_platform, platform_len) != 0)
return -EFAULT;
}

/*
* 复制"基础"平台字符串 (如果存在).
*/
k_base_platform = ELF_BASE_PLATFORM;
u_base_platform = NULL;
if (k_base_platform) {
platform_len = strlen(k_base_platform) + 1;
sp -= platform_len;
u_base_platform = (char __user *) sp;
if (copy_to_user(u_base_platform, k_base_platform, platform_len) != 0)
return -EFAULT;
}

/* 确保栈指针是8字节对齐的. */
sp &= ~7UL;

/*
* 将FDPIC加载映射表(loadmap)复制到栈上. 这是FDPIC的关键步骤.
*/
/* 计算主可执行文件的loadmap大小. */
len = sizeof(struct elf_fdpic_loadmap);
len += sizeof(struct elf_fdpic_loadseg) * exec_params->loadmap->nsegs;
sp = (sp - len) & ~7UL; /* 向下分配空间并对齐. */
exec_params->map_addr = sp; /* 记录下loadmap在栈上的最终地址. */

/* 将内核中的loadmap复制到用户空间的栈上. */
if (copy_to_user((void __user *) sp, exec_params->loadmap, len) != 0)
return -EFAULT;

/* 在进程上下文中记录下这个地址. */
current->mm->context.exec_fdpic_loadmap = (unsigned long) sp;

/* 如果存在解释器(动态链接器), 也将其loadmap复制到栈上. */
if (interp_params->loadmap) {
len = sizeof(struct elf_fdpic_loadmap);
len += sizeof(struct elf_fdpic_loadseg) *
interp_params->loadmap->nsegs;
sp = (sp - len) & ~7UL;
interp_params->map_addr = sp;

if (copy_to_user((void __user *) sp, interp_params->loadmap,
len) != 0)
return -EFAULT;

current->mm->context.interp_fdpic_loadmap = (unsigned long) sp;
}

/*
* 在栈上为辅助向量(auxv), argv指针数组, envp指针数组和argc分配空间.
* 这个计算是反向的, 先计算总共需要多少项.
*/
#define DLINFO_ITEMS 15 /* 一个预估的基本辅助向量项目数 */

nitems = 1 + DLINFO_ITEMS + (k_platform ? 1 : 0) +
(k_base_platform ? 1 : 0) + AT_VECTOR_SIZE_ARCH;

if (bprm->have_execfd)
nitems++;
#ifdef ELF_HWCAP2
nitems++;
#endif

csp = sp; /* 临时保存当前栈顶. */
sp -= nitems * 2 * sizeof(unsigned long); /* 为辅助向量分配空间 (每项包含类型和值, 都是unsigned long). */
sp -= (bprm->envc + 1) * sizeof(char *); /* 为envp指针数组分配空间 (加1是为了结尾的NULL). */
sp -= (bprm->argc + 1) * sizeof(char *); /* 为argv指针数组分配空间 (加1是为了结尾的NULL). */
sp -= 1 * sizeof(unsigned long); /* 为argc分配空间. */

/* 进行最终的16字节对齐. */
csp -= sp & 15UL;
sp -= sp & 15UL;

/*
* 在内核的一个临时缓冲区(mm->saved_auxv)中创建辅助向量.
*/
elf_info = (elf_addr_t *)mm->saved_auxv;
/* 一个用于方便地添加辅助向量项的宏. */
#define NEW_AUX_ENT(id, val) \
do { \
*elf_info++ = id; \
*elf_info++ = val; \
} while (0)

/* 添加架构特定的辅助向量项. */
#ifdef ARCH_DLINFO
ARCH_DLINFO;
#endif
/* 添加硬件能力(HWCAP), 页面大小(PAGESZ), 时钟频率(CLKTCK)等标准信息. */
NEW_AUX_ENT(AT_HWCAP, ELF_HWCAP);
NEW_AUX_ENT(AT_PAGESZ, PAGE_SIZE);
NEW_AUX_ENT(AT_CLKTCK, CLOCKS_PER_SEC);
/* 添加ELF程序头表的信息: 地址, 每项大小, 项数. */
NEW_AUX_ENT(AT_PHDR, exec_params->ph_addr);
NEW_AUX_ENT(AT_PHENT, sizeof(struct elf_phdr));
NEW_AUX_ENT(AT_PHNUM, exec_params->hdr.e_phnum);
/*
* 添加动态链接器的基地址. 动态链接器看到AT_BASE, 就知道自己被加载到了哪里.
* 对于FDPIC, 真正的基地址信息在loadmap里, 但这里通常会提供解释器的加载基地址.
*/
NEW_AUX_ENT(AT_BASE, interp_params->elfhdr_addr);
if (bprm->interp_flags & BINPRM_FLAGS_PRESERVE_ARGV0)
flags |= AT_FLAGS_PRESERVE_ARGV0;
NEW_AUX_ENT(AT_FLAGS, flags);
/* 添加主可执行文件的入口点地址. */
NEW_AUX_ENT(AT_ENTRY, exec_params->entry_addr);
/* 添加真实的和有效的用户/组ID. */
NEW_AUX_ENT(AT_UID, (elf_addr_t) from_kuid_munged(cred->user_ns, cred->uid));
NEW_AUX_ENT(AT_EUID, (elf_addr_t) from_kuid_munged(cred->user_ns, cred->euid));
NEW_AUX_ENT(AT_GID, (elf_addr_t) from_kgid_munged(cred->user_ns, cred->gid));
NEW_AUX_ENT(AT_EGID, (elf_addr_t) from_kgid_munged(cred->user_ns, cred->egid));
/* 添加安全执行标志(AT_SECURE). */
NEW_AUX_ENT(AT_SECURE, bprm->secureexec);
/* 添加可执行文件的路径名. */
NEW_AUX_ENT(AT_EXECFN, bprm->exec);
/* 如果有平台字符串, 添加其在栈上的地址. */
if (k_platform)
NEW_AUX_ENT(AT_PLATFORM,
(elf_addr_t)(unsigned long)u_platform);
if (k_base_platform)
NEW_AUX_ENT(AT_BASE_PLATFORM,
(elf_addr_t)(unsigned long)u_base_platform);
/* 如果有execfd, 添加它. */
if (bprm->have_execfd)
NEW_AUX_ENT(AT_EXECFD, bprm->execfd);
#undef NEW_AUX_ENT
/* 用0填充剩余的缓冲区, 最后的0对 {0, 0} 就是 AT_NULL, 表示辅助向量结束. */
memset(elf_info, 0, (char *)mm->saved_auxv +
sizeof(mm->saved_auxv) - (char *)elf_info);

/*
* 将在内核缓冲区中准备好的辅助向量, 整体复制到用户空间的栈上.
*/
ei_index = elf_info - (elf_addr_t *)mm->saved_auxv;
csp -= ei_index * sizeof(elf_addr_t);
if (copy_to_user((void __user *)csp, mm->saved_auxv,
ei_index * sizeof(elf_addr_t)))
return -EFAULT;

/*
* 计算出用户空间中argv和envp指针数组的位置.
*/
csp -= (bprm->envc + 1) * sizeof(elf_caddr_t);
envp = (elf_caddr_t __user *) csp;
csp -= (bprm->argc + 1) * sizeof(elf_caddr_t);
argv = (elf_caddr_t __user *) csp;

/*
* 在栈上写入argc的值.
*/
csp -= sizeof(unsigned long);
if (put_user(bprm->argc, (unsigned long __user *) csp))
return -EFAULT;

/* 这是一个断言, 确保我们对栈空间的计算是精确的. */
BUG_ON(csp != sp);

/*
* 填充argv和envp指针数组.
*/
/* 获取参数字符串区域在用户空间的起始地址. */
#ifdef CONFIG_MMU
current->mm->arg_start = bprm->p;
#else
/* 在无MMU系统上, 需要从栈顶反向计算. */
current->mm->arg_start = current->mm->start_stack -
(MAX_ARG_PAGES * PAGE_SIZE - bprm->p);
#endif

p = (char __user *) current->mm->arg_start;
/* 遍历所有参数. */
for (loop = bprm->argc; loop > 0; loop--) {
/* 将当前参数字符串的地址写入argv数组的当前项. */
if (put_user((elf_caddr_t) p, argv++))
return -EFAULT;
/* 获取字符串长度, 并将p指针移动到下一个字符串的开头. */
len = strnlen_user(p, MAX_ARG_STRLEN);
if (!len || len > MAX_ARG_STRLEN)
return -EINVAL;
p += len;
}
/* 写入argv数组结尾的NULL. */
if (put_user(NULL, argv))
return -EFAULT;
current->mm->arg_end = (unsigned long) p;

/* 用同样的方式填充envp数组. */
current->mm->env_start = (unsigned long) p;
for (loop = bprm->envc; loop > 0; loop--) {
if (put_user((elf_caddr_t)(unsigned long) p, envp++))
return -EFAULT;
len = strnlen_user(p, MAX_ARG_STRLEN);
if (!len || len > MAX_ARG_STRLEN)
return -EINVAL;
p += len;
}
if (put_user(NULL, envp))
return -EFAULT;
current->mm->env_end = (unsigned long) p;

/*
* 最后, 更新内存描述符中的栈顶指针为我们计算出的最终值.
*/
mm->start_stack = (unsigned long) sp;
return 0;
}

load_elf_fdpic_binary: 加载FDPIC ELF可执行文件

此函数是 binfmt_elf_fdpic 模块的心脏。当 execve 系统调用发现一个文件是FDPIC ELF格式时,就会调用此函数。它的职责是:读取ELF文件的元数据,解析程序头,加载代码段和数据段到内存,设置新进程的内存布局(特别是栈和堆),准备好CPU寄存器,并最终启动新程序的执行。

  1. 验证和解析
  2. 处理动态链接器 (Interpreter)
  3. 核心:加载和映射文件 (FDPIC的关键)
  4. 创建栈和堆 (no-MMU的特殊处理)
  5. 最后的准备与启动
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
/*
* load an fdpic binary into various bits of memory
* 将一个FDPIC二进制文件加载到内存的各个部分
*/
/*
* @bprm: 指向 linux_binprm 结构体的指针, 它包含了要执行的文件的所有上下文信息.
* @return: 成功时返回0, 失败时返回负值的错误码.
*/
static int load_elf_fdpic_binary(struct linux_binprm *bprm)
{
/*
* exec_params: 用于存储可执行文件本身的ELF参数 (如头部信息, 加载映射表等).
* interp_params: 用于存储其动态链接器(解释器)的ELF参数.
*/
struct elf_fdpic_params exec_params, interp_params;
/*
* regs: 指向当前进程的内核栈上保存的寄存器集合. 我们将修改它来启动新程序.
*/
struct pt_regs *regs = current_pt_regs();
/*
* phdr: 用于遍历程序头表的指针.
*/
struct elf_phdr *phdr;
/*
* stack_size: 新进程栈的大小.
* entryaddr: 新程序(或其解释器)的入口点地址.
*/
unsigned long stack_size, entryaddr;
#ifdef ELF_FDPIC_PLAT_INIT
/*
* dynaddr: 指向动态链接段的地址, 用于特定平台的初始化.
*/
unsigned long dynaddr;
#endif
#ifndef CONFIG_MMU
/*
* stack_prot: 在无MMU系统上, 用于设置栈区域的保护标志 (读/写/执行).
*/
unsigned long stack_prot;
#endif
/*
* interpreter: 指向动态链接器文件的 'struct file' 对象.
* interpreter_name: 指向从文件中读出的动态链接器路径名字符串.
*/
struct file *interpreter = NULL;
char *interpreter_name = NULL;
/*
* executable_stack: 记录请求的栈是否需要可执行权限.
*/
int executable_stack;
/*
* retval: 用于存储函数调用的返回值.
* i: 用于循环的计数器.
*/
int retval, i;
/*
* pos: 用于在文件中定位的偏移量.
*/
loff_t pos;

/*
* kdebug: 内核调试打印宏, 打印当前进程的PID.
*/
kdebug("____ LOAD %d ____", current->pid);

/*
* 使用 memset 将两个参数结构体清零, 确保没有垃圾数据.
*/
memset(&exec_params, 0, sizeof(exec_params));
memset(&interp_params, 0, sizeof(interp_params));

/*
* 将 bprm->buf (文件的前128字节) 中的内容强制转换为 elfhdr 结构体, 并复制到 exec_params.hdr.
*/
exec_params.hdr = *(struct elfhdr *) bprm->buf;
/*
* 为可执行文件设置标志, 表示它存在且是一个可执行文件.
*/
exec_params.flags = ELF_FDPIC_FLAG_PRESENT | ELF_FDPIC_FLAG_EXECUTABLE;

/*
* 检查这是否是一个我们能处理的二进制文件.
*/
retval = -ENOEXEC; /* 默认返回码: 不支持的执行格式 */
/*
* 调用 is_elf 检查文件幻数等, 确认它是一个ELF文件.
*/
if (!is_elf(&exec_params.hdr, bprm->file))
goto error; /* 如果不是, 跳转到错误处理. */
/*
* 调用 elf_check_fdpic 检查ELF头部是否表明这是一个FDPIC兼容的文件.
*/
if (!elf_check_fdpic(&exec_params.hdr)) {
#ifdef CONFIG_MMU
/* 在有MMU的系统上, 普通的ELF文件由 binfmt_elf 处理, 所以我们直接退出. */
goto error;
#else
/* 在无MMU系统上, 我们只接受 ET_DYN 类型的ELF文件(即位置无关可执行文件 PIE). */
if (exec_params.hdr.e_type != ET_DYN)
goto error; /* 如果不是, 跳转到错误处理. */
#endif
}

/*
* 读取程序头表(Program Header Table).
*/
retval = elf_fdpic_fetch_phdrs(&exec_params, bprm->file);
if (retval < 0) /* 如果读取失败, 跳转到错误处理. */
goto error;

/*
* 扫描程序头表, 寻找一个指定了动态链接器(解释器)的段.
*/
phdr = exec_params.phdrs; /* 指针指向程序头表的开始. */

/*
* 遍历所有的程序头表项.
*/
for (i = 0; i < exec_params.hdr.e_phnum; i++, phdr++) {
/*
* 根据程序头项的类型(p_type)进行处理.
*/
switch (phdr->p_type) {
case PT_INTERP: /* 如果这是一个解释器段 */
retval = -ENOMEM; /* 预设错误码: 内存不足 */
/* 检查解释器路径名是否过长. */
if (phdr->p_filesz > PATH_MAX)
goto error;
retval = -ENOENT; /* 预设错误码: 文件不存在 */
/* 检查路径名长度是否有效. */
if (phdr->p_filesz < 2)
goto error;

/* 分配内核内存以存储解释器的路径名. */
interpreter_name = kmalloc(phdr->p_filesz, GFP_KERNEL);
if (!interpreter_name) /* 如果分配失败... */
goto error;

/* 设置文件读取的起始偏移量. */
pos = phdr->p_offset;
/* 从可执行文件中读取解释器的路径名到分配的内存中. */
retval = kernel_read(bprm->file, interpreter_name,
phdr->p_filesz, &pos);
/* 检查读取的字节数是否与期望的完全一致. */
if (unlikely(retval != phdr->p_filesz)) {
if (retval >= 0) /* 如果读取成功但字节数不对, 这是个无效格式. */
retval = -ENOEXEC;
goto error;
}

retval = -ENOENT; /* 预设错误码: 文件不存在 */
/* 检查读出的路径名是否以空字符'\0'结尾. */
if (interpreter_name[phdr->p_filesz - 1] != '\0')
goto error;

/* 内核调试打印: 显示找到的ELF解释器名称. */
kdebug("Using ELF interpreter %s", interpreter_name);

/* 用解释器替换掉当前要执行的程序. */
interpreter = open_exec(interpreter_name);
/* 检查 open_exec 是否返回了错误. */
retval = PTR_ERR(interpreter);
if (IS_ERR(interpreter)) {
interpreter = NULL;
goto error;
}

/*
* 如果原始的二进制文件不可读, 那么无论解释器的权限如何,
* 都要强制新进程不可被dump.
*/
would_dump(bprm, interpreter);

/* 重置文件偏移量为0, 准备读取解释器的文件头. */
pos = 0;
/* 将解释器的前128字节读入bprm->buf, 覆盖掉原始程序的内容. */
retval = kernel_read(interpreter, bprm->buf,
BINPRM_BUF_SIZE, &pos);
/* 检查是否成功读取了完整的缓冲区大小. */
if (unlikely(retval != BINPRM_BUF_SIZE)) {
if (retval >= 0)
retval = -ENOEXEC;
goto error;
}

/* 将解释器的ELF头部信息保存到 interp_params.hdr. */
interp_params.hdr = *((struct elfhdr *) bprm->buf);
break; /* 退出 switch */

case PT_LOAD: /* 如果这是一个需要加载到内存的段 */
#ifdef CONFIG_MMU
/* 在有MMU的系统上, 记录第一个加载段的虚拟地址作为基地址. */
if (exec_params.load_addr == 0)
exec_params.load_addr = phdr->p_vaddr;
#endif
break;
} /* switch 结束 */
} /* for 循环结束 */

/* 检查ELF头部是否指定了"常量位移"(constant displacement)加载模式. */
if (is_constdisp(&exec_params.hdr))
exec_params.flags |= ELF_FDPIC_FLAG_CONSTDISP;

/* 对解释器进行完整性检查. */
if (interpreter_name) { /* 如果我们找到了一个解释器 */
retval = -ELIBBAD; /* 预设错误码: 库格式错误 */
/* 检查解释器本身是否是一个有效的ELF文件. */
if (!is_elf(&interp_params.hdr, interpreter))
goto error;

/* 为解释器设置'存在'标志. */
interp_params.flags = ELF_FDPIC_FLAG_PRESENT;

/* 读取解释器的程序头表. */
retval = elf_fdpic_fetch_phdrs(&interp_params, interpreter);
if (retval < 0)
goto error;
}

/* 确定新进程的栈大小, 优先使用可执行文件指定的. */
stack_size = exec_params.stack_size;
/* 检查可执行文件是否指定了栈的可执行权限. */
if (exec_params.flags & ELF_FDPIC_FLAG_EXEC_STACK)
executable_stack = EXSTACK_ENABLE_X;
else if (exec_params.flags & ELF_FDPIC_FLAG_NOEXEC_STACK)
executable_stack = EXSTACK_DISABLE_X;
else
executable_stack = EXSTACK_DEFAULT;

/* 如果可执行文件没有指定栈大小, 且有解释器, 则使用解释器指定的. */
if (stack_size == 0 && interp_params.flags & ELF_FDPIC_FLAG_PRESENT) {
stack_size = interp_params.stack_size;
/* 同时, 栈的可执行权限也由解释器决定. */
if (interp_params.flags & ELF_FDPIC_FLAG_EXEC_STACK)
executable_stack = EXSTACK_ENABLE_X;
else if (interp_params.flags & ELF_FDPIC_FLAG_NOEXEC_STACK)
executable_stack = EXSTACK_DISABLE_X;
else
executable_stack = EXSTACK_DEFAULT;
}

retval = -ENOEXEC;
/* 如果最终栈大小仍为0, 则使用一个默认值 (128KB). */
if (stack_size == 0)
stack_size = 131072UL;

/* 检查解释器是否也使用了"常量位移"模式. */
if (is_constdisp(&interp_params.hdr))
interp_params.flags |= ELF_FDPIC_FLAG_CONSTDISP;

/* 清理当前进程的执行上下文, 准备加载新程序. 这是无法回头的一步. */
retval = begin_new_exec(bprm);
if (retval)
goto error;

/* 从现在开始, 旧的用户空间镜像已经死亡. */
/* 设置新进程的"个性化"标志, 以反映其ELF类型. */
SET_PERSONALITY(exec_params.hdr);
/* 如果是FDPIC, 添加 PER_LINUX_FDPIC 标志. */
if (elf_check_fdpic(&exec_params.hdr))
current->personality |= PER_LINUX_FDPIC;
/* 如果架构上读等同于执行, 添加 READ_IMPLIES_EXEC 标志. */
if (elf_read_implies_exec(&exec_params.hdr, executable_stack))
current->personality |= READ_IMPLIES_EXEC;

/* 设置新的执行上下文, 例如清除信号处理器等. */
setup_new_exec(bprm);

/* 在当前进程的task_struct中记录下我们正在使用的二进制格式处理器. */
set_binfmt(&elf_fdpic_format);

/* 初始化新进程的内存映射(mm_struct)中的各个段地址为0. */
current->mm->start_code = 0;
current->mm->end_code = 0;
current->mm->start_stack = 0;
current->mm->start_data = 0;
current->mm->end_data = 0;
/* FDPIC特定的加载映射表指针也清零. */
current->mm->context.exec_fdpic_loadmap = 0;
current->mm->context.interp_fdpic_loadmap = 0;

#ifdef CONFIG_MMU
/* 在有MMU的系统上, 规划内存布局, 设置栈和堆的起始地址. */
elf_fdpic_arch_lay_out_mm(&exec_params,
&interp_params,
¤t->mm->start_stack,
¤t->mm->start_brk);

/* 创建参数和环境变量所在的内存页. */
retval = setup_arg_pages(bprm, current->mm->start_stack,
executable_stack);
if (retval < 0)
goto error;
#ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGES
/* 特定架构可能需要设置一些额外的页. */
retval = arch_setup_additional_pages(bprm, !!interpreter_name);
if (retval < 0)
goto error;
#endif
#endif

/* 将可执行文件和解释器映射到内存. 这是FDPIC加载的核心步骤. */
retval = elf_fdpic_map_file(&exec_params, bprm->file, current->mm,
"executable");
if (retval < 0)
goto error;

/* 如果有解释器, 也将其映射到内存. */
if (interpreter_name) {
retval = elf_fdpic_map_file(&interp_params, interpreter,
current->mm, "interpreter");
if (retval < 0) {
printk(KERN_ERR "Unable to load interpreter\n");
goto error;
}

/* 允许对解释器文件的写访问(之前被我们禁止了). */
exe_file_allow_write_access(interpreter);
/* 释放对解释器文件的引用. */
fput(interpreter);
interpreter = NULL;
}

#ifdef CONFIG_MMU
/* 在有MMU系统上, 设置并对齐堆的起始地址. */
if (!current->mm->start_brk)
current->mm->start_brk = current->mm->end_data;

current->mm->brk = current->mm->start_brk =
PAGE_ALIGN(current->mm->start_brk);

#else
/* 在无MMU系统上, 显式地为栈分配物理内存. */
/* 将栈大小对齐到页边界. */
stack_size = (stack_size + PAGE_SIZE - 1) & PAGE_MASK;
/* 确保栈至少有2个页大小. */
if (stack_size < PAGE_SIZE * 2)
stack_size = PAGE_SIZE * 2;

/* 设置栈的保护标志: 可读, 可写. */
stack_prot = PROT_READ | PROT_WRITE;
/* 根据之前的判断, 如果需要, 添加可执行标志. */
if (executable_stack == EXSTACK_ENABLE_X ||
(executable_stack == EXSTACK_DEFAULT && VM_STACK_FLAGS & VM_EXEC))
stack_prot |= PROT_EXEC;

/*
* 调用 vm_mmap (在no-MMU下是物理内存分配器)来分配栈空间.
* MAP_PRIVATE | MAP_ANONYMOUS: 私有的、匿名的内存.
* MAP_UNINITIALIZED: 内核无需清零, 提高效率.
* MAP_GROWSDOWN: 这是一个向下增长的栈.
*/
current->mm->start_brk = vm_mmap(NULL, 0, stack_size, stack_prot,
MAP_PRIVATE | MAP_ANONYMOUS |
MAP_UNINITIALIZED | MAP_GROWSDOWN,
0);

/* 检查内存分配是否成功. */
if (IS_ERR_VALUE(current->mm->start_brk)) {
retval = current->mm->start_brk;
current->mm->start_brk = 0;
goto error;
}

/* 在no-MMU系统上, 堆(brk)的起始点紧接着栈的末尾. */
current->mm->brk = current->mm->start_brk;
current->mm->context.end_brk = current->mm->start_brk;
/* 栈顶地址在分配区域的最高处. */
current->mm->start_stack = current->mm->start_brk + stack_size;
#endif

/*
* 在新创建的栈上创建ELF表(主要是辅助向量, Auxiliary Vector).
* 这些信息将传递给用户空间的动态链接器.
*/
retval = create_elf_fdpic_tables(bprm, current->mm, &exec_params,
&interp_params);
if (retval < 0)
goto error;

/* 内核调试打印: 显示最终确定的内存布局. */
kdebug("- start_code %lx", current->mm->start_code);
kdebug("- end_code %lx", current->mm->end_code);
kdebug("- start_data %lx", current->mm->start_data);
kdebug("- end_data %lx", current->mm->end_data);
kdebug("- start_brk %lx", current->mm->start_brk);
kdebug("- brk %lx", current->mm->brk);
kdebug("- start_stack %lx", current->mm->start_stack);

#ifdef ELF_FDPIC_PLAT_INIT
/*
* 特定于平台的初始化宏.
* 根据ABI要求, 设置某些寄存器的初始值. 例如, FDPIC约定用一个特定寄存器
* 来存放私有数据段的基地址, 这个宏会完成这个设置.
*/
dynaddr = interp_params.dynamic_addr ?: exec_params.dynamic_addr;
ELF_FDPIC_PLAT_INIT(regs, exec_params.map_addr, interp_params.map_addr,
dynaddr);
#endif

/*
* 最后的收尾工作.
*/
finalize_exec(bprm);
/*
* 一切就绪...准备启动用户空间上下文.
* 确定入口点地址, 优先使用解释器的.
*/
entryaddr = interp_params.entry_addr ?: exec_params.entry_addr;
/*
* 调用 start_thread, 修改内核栈上保存的寄存器状态(regs),
* 将程序计数器(PC)设置为入口点地址, 栈指针(SP)设置为新栈的栈顶.
*/
start_thread(regs, entryaddr, current->mm->start_stack);

/* 如果执行到这里, 说明一切成功. */
retval = 0;

error: /* 错误处理标签 */
/* 如果解释器文件被打开了但未被正常处理, 释放它. */
if (interpreter) {
exe_file_allow_write_access(interpreter);
fput(interpreter);
}
/* 释放所有在函数执行过程中动态分配的内存. */
kfree(interpreter_name);
kfree(exec_params.phdrs);
kfree(exec_params.loadmap);
kfree(interp_params.phdrs);
kfree(interp_params.loadmap);
/* 返回最终的错误码. */
return retval;
}