Linux内核copy_string_kernel接口的作用

copy_string_kernel 是 Linux 内核中一个核心但非公开导出的函数(未包含在头文件中供模块使用),主要在 execve 系统调用的实现路径 (fs/exec.c) 中扮演关键角色。它的核心作用如下:

核心作用:

安全、高效地将用户空间提供的字符串参数(argv)和环境变量(envp)复制到内核空间新分配的内存中,并正确地组织它们,为后续执行新的程序镜像做好准备。

详细解析其职责和过程:

  1. 处理用户空间输入:

    • 当用户程序调用 execve(pathname, argv, envp) 时,argv(命令行参数数组)和 envp(环境变量数组)是用户空间提供的指针数组。
    • 这些指针指向用户空间内存中的字符串(以 '\0' 结尾)。
  2. 内核空间的安全复制:

    • 内核代码不能直接解引用用户空间指针。必须先将用户空间的数据复制到内核空间,以避免:
      • 页错误导致内核崩溃: 用户指针可能无效或指向未映射的内存。
      • 恶意用户空间程序篡改内核: 在解引用过程中用户空间数据被改变。
      • 时间差攻击 (TOCTOU): 在检查和使用的间隙数据被改变。
    • copy_string_kernel 负责执行这个关键的复制操作。
  3. 针对 execve 的特殊组织:

    • execve 需要将 argvenvp 中的所有字符串打包成一个连续的内核内存块。这个内存块的布局通常是:
      • 所有字符串 (argv[0], argv[1], …, envp[0], envp[1], …) 按顺序、以 '\0' 结尾紧密排列。
      • 在这个字符串块的末尾,紧接着存储一个指向每个字符串起始地址的 指针数组 (char * 数组)。
      • 这个指针数组的最后一项是 NULL
      • 这个布局使得新进程的用户空间栈在初始化时,可以高效地设置 argvenvp 指针。
    • copy_string_kernel 不仅仅简单地复制单个字符串,它在复制每个字符串时,会:
      • 分配内核内存: 为新字符串分配内核空间的内存。
      • 复制内容: 使用类似 copy_from_user() 的机制(内部可能调用 strncpy_from_user),安全地将用户空间字符串内容复制到这块内核内存。
      • 记录指针: 将新分配的内核内存地址(即新字符串在内核空间的起始地址)记录到 struct linux_binprm 结构 (bprm) 的 p 成员指向的位置。
      • 更新位置指针: 递增 bprm->p,使其指向内存块中下一个字符串应该存放的位置(考虑对齐),同时也会在指针数组中为下一个指针预留位置。
      • 错误处理: 严格检查用户指针有效性、字符串长度(防止超长参数攻击)、内存分配是否成功等。遇到错误立即返回错误码。
  4. struct linux_binprm (bprm) 的交互:

    • bprm 结构体 (include/linux/binfmts.h) 贯穿整个 execve 执行过程,包含了加载新程序所需的所有信息。
    • bprm 的关键成员 bprm->p 是一个游标指针
      • 初始时指向一块预先分配好的内核内存 (bprm->buf 或后续扩展的内存) 的末尾
      • copy_string_kernelbprm->p 开始向前(递减地址)写入字符串内容和构建指针数组。
      • 每复制一个字符串或添加一个指针,bprm->p 就向前移动相应距离(考虑对齐)。
    • 这种从后向前填充的方式是为了高效构建最终的 argv/envp 内存布局。最终,bprm->p 会指向整个打包块(指针数组 + 字符串)的起始地址

总结流程(简化):

  1. do_execveat_common -> prepare_binprm -> copy_strings_kernel (该函数会循环调用 copy_string_kernel) 的路径中调用。
  2. 对于用户空间 argv/envp 数组中的每一个字符串
    • 检查用户空间指针有效性。
    • 计算字符串长度(包含结尾 '\0')。
    • 在内核内存块(由 bprm->p 指示当前末尾)中分配空间。
    • 安全地从用户空间复制字符串内容到分配的内核空间。
    • 将新字符串的内核地址记录在 bprm->p 指向的位置(这是指针数组的一部分)。
    • 向前移动 bprm->p(减去字符串长度和可能的对齐填充),使其指向下一个可用位置(用于存放下一个字符串内容)。
    • 同时向前移动 bprm->p(减去一个指针的大小),使其指向指针数组的下一个槽位(用于存放下一个字符串的指针)。
  3. 当所有字符串复制完成后:
    • 在指针数组的最后一个槽位(此时 bprm->p 指向的位置)存入 NULL
    • bprm->p 最终指向了整个打包好的参数/环境块(包含指针数组和所有字符串)在内核内存中的起始地址
  4. 这个打包好的内核内存块稍后会被复制到新进程的用户空间栈上,成为新程序的 argvenvp

为什么需要它?为什么不直接用 strndup_user

  • 特殊的内存布局要求: execve 要求参数和环境变量以特定的连续方式组织(字符串块 + 指针数组),copy_string_kernel 直接负责构建这种布局。
  • bprm 状态管理集成: 它紧密地与 bprm 结构及其游标指针 bprm->p 协同工作,管理内存分配和布局的进度。
  • 性能与效率:execve 这个高频且关键的操作路径上,需要高度优化的代码来处理大量字符串。copy_string_kernel 被设计为满足这个特定场景的高效和安全需求。

简单来说,copy_string_kernelexecve 系统调用内部的一个关键助手,负责安全、高效地将用户命令行参数和环境变量“搬运”并“打包”到内核空间,为启动新进程奠定基础。它是连接用户空间意图和内核执行新程序的桥梁中至关重要的一环。

Linux内核中从bprm->p中获取进程的参数和环境变量

在 Linux 内核中,struct linux_binprm(简称 bprm)结构体用于管理 execve 系统调用加载新程序的过程。bprm->p 是一个关键成员,它指向内核空间中打包好的参数和环境变量块的起始位置。下面是其工作原理和获取参数的详细说明:


1. bprm->p 指向的内容

execve 执行过程中,copy_string_kernel(通过 copy_strings_kernel 调用)会将用户空间的 argvenvp 打包成一个连续的内核内存块,其结构如下:

1
2
3
4
5
6
低地址 (起始位置)                                     高地址
+----------------------+----------------------+----------------------+
| 指针数组 (char*[]) | 字符串数据区 | (未使用空间) |
+----------------------+----------------------+----------------------+
↑ ↑ ↑
bprm->p 指针数组结束位置 字符串结束位置

具体包含两部分:

  • 指针数组:存储所有参数字符串的内核地址
    • 格式:[argv[0], argv[1], ..., argv[n], NULL, envp[0], envp[1], ..., envp[m], NULL]
    • 以两个 NULL 结尾(分隔 argvenvp)。
  • 字符串数据区:紧跟在指针数组后,存储所有参数字符串的实际内容(以 \0 结尾)。

2. 如何从 bprm->p 获取参数

步骤示例(伪代码逻辑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 获取指针数组的起始地址
char **kargv = (char **)bprm->p;

// 2. 遍历 argv
int argc = 0;
while (kargv[argc] != NULL) { // 遇到第一个 NULL 结束 argv
printk("argv[%d] = %s\n", argc, kargv[argc]);
argc++;
}

// 3. 跳过 argv 的 NULL 分隔符
char **kenvp = &kargv[argc + 1]; // 指向 envp 开始

// 4. 遍历 envp
int envc = 0;
while (kenvp[envc] != NULL) { // 遇到第二个 NULL 结束 envp
printk("envp[%d] = %s\n", envc, kenvp[envc]);
envc++;
}

3. 关键实现位置

该逻辑主要在内核的 execve 路径中实现,尤其是在以下函数中:

  • create_elf_tables() (位于 fs/binfmt_elf.c)
    bprm->p 指向的内核参数块复制到新进程的用户空间栈,并设置用户态的 argc/argv/envp 指针。
  • setup_arg_pages() (位于 fs/exec.c)
    准备新进程的栈内存,为复制参数块提供空间。

4. 用户空间视角

最终,当新程序启动时,其用户空间栈布局如下(以 x86_64 为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
高地址
+-----------------+
| ... |
| envp[0] (用户地址) | ← 用户空间的 envp 指针指向这里
| ... |
| argv[0] (用户地址) | ← 用户空间的 argv 指针指向这里
| argc |
+-----------------+
| 辅助向量 (auxv) |
+-----------------+
| NULL (envp 结束) |
| envp[m] |
| ... |
| envp[0] |
| NULL (argv 结束) | ← bprm->p 中的指针数组复制到这里
| argv[n] |
| ... |
| argv[0] |
+-----------------+
| "value" (环境变量)|
| "PATH=..." |
| "program_name" | ← 字符串数据区复制到这里
+-----------------+
低地址

用户进程通过 main(int argc, char **argv, char **envp) 访问这些参数。


5. 注意事项

  1. 内核专用
    bprm->p 仅在 execve 上下文(内核态)有效,不能直接暴露给用户空间。
  2. 安全性
    所有字符串均经过安全复制(如 copy_from_user),防止用户空间恶意指针。
  3. 架构差异
    栈布局(如参数顺序、对齐)因架构(x86/ARM/RISC-V)而异,由 create_elf_tables() 处理。

总结

bprm->p 是内核在 execve 中打包参数/环境变量的临时内核地址

  • 内容 = 指针数组 + 字符串数据。
  • 作用:为复制到用户空间栈提供源数据。
  • 生命周期:在 execve 执行期间有效,新程序启动后释放。

通过遍历 bprm->p 的指针数组,内核可以安全访问所有参数和环境变量,最终将其传递给新进程的用户空间栈。

arm64架构 Linux内核中从bprm->p中获取进程的参数和环境变量

arm64架构 Linux内核中从bprm->exec中获取进程的参数和环境变量

在 ARM64 架构的 Linux 内核中,从 bprm->exec 直接获取参数和环境变量是错误的理解bprm->exec 实际上是一个函数指针,而非存储参数的位置。正确获取参数的方式是通过 struct linux_binprm 的其他成员和机制,以下是详细说明:


1. 关键概念澄清

  • bprm->exec 的作用
    这是 struct linux_binprm 中的一个成员,类型为 int (*exec)(struct linux_binprm *)
    它指向二进制格式的加载函数(如 ELF 格式的 load_elf_binary),负责解析可执行文件并设置新进程的上下文。
    它不存储参数或环境变量!

  • 参数/环境变量的实际存储位置
    参数和环境变量存储在 bprm->p 指向的内核内存块中(与 x86 等架构相同),其结构为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    低地址
    +------------------------+
    | 指针数组 (char*[]) | ← bprm->p 指向这里
    | [argv0_ptr, ..., NULL,
    | envp0_ptr, ..., NULL]
    +------------------------+
    | 字符串数据区 |
    | "program\0arg1\0... |
    | PATH=...\0HOME=...\0" |
    +------------------------+
    高地址

2. ARM64 架构下获取参数的流程

参数访问发生在二进制加载函数(如 ELF 的 load_elf_binary)中,核心步骤如下:

(1) 在 load_elf_binary 中访问参数

文件位置:fs/binfmt_elf.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int load_elf_binary(struct linux_binprm *bprm)
{
// 1. 获取参数指针数组
char **kargv = (char **)bprm->p;

// 2. 遍历 argv
for (int i = 0; kargv[i] != NULL; i++) {
printk("argv[%d] = %s\n", i, kargv[i]);
}

// 3. 跳过 NULL 分隔符获取 envp
char **kenvp = kargv;
while (*kenvp++ != NULL); // 找到 envp 起始位置

// 4. 遍历 envp
for (int j = 0; kenvp[j] != NULL; j++) {
printk("envp[%d] = %s\n", j, kenvp[j]);
}

// 5. 将参数复制到用户空间栈 (ARM64 特定)
retval = create_elf_tables(bprm, ...);
}

(2) ARM64 用户空间栈设置 (create_elf_tables)

文件位置:fs/binfmt_elf.c
这是将内核参数复制到 ARM64 用户空间栈的关键函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int create_elf_tables(struct linux_binprm *bprm, ...)
{
// 计算参数块总大小
unsigned long sp = STACK_TOP; // ARM64 栈起始地址 (0x8000000000)
sp -= sizeof(unsigned long); // 对齐预留

// 将 bprm->p 指向的参数块复制到用户栈
ret = copy_to_user((void __user *)sp, (void *)bprm->p, total_size);

// 设置用户空间寄存器:
// x0 = argc
// x1 = argv 用户地址
// x2 = envp 用户地址
start_thread(regs, elf_entry, sp);
}

3. ARM64 用户空间栈布局

当新进程启动时,其用户空间栈布局如下(符合 ARM64 ABI):

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
高地址 (0x7ffffffff000)
+---------------------+
| 环境变量字符串 |
| "HOME=/user\0" |
| "PATH=...\0" |
+---------------------+
| 参数字符串 |
| "program\0" |
| "arg1\0" |
+---------------------+
| 辅助向量 (auxv) |
| [AT_NULL, 0] |
| [AT_PAGESZ, 4096] |
+---------------------+
| NULL (envp 结束) | // envp[envc] = NULL
| 0x00007ffffee000 | → 指向 "PATH=..."
| 0x00007fffff0000 | → 指向 "HOME=/user"
+---------------------+
| NULL (argv 结束) | // argv[argc] = NULL
| 0x00007ffffe0000 | → 指向 "arg1"
| 0x00007ffffd0000 | → 指向 "program"
+---------------------+
| argc = 2 | // 参数个数
+---------------------+
| 栈底 (sp 寄存器指向这里)
低地址 (sp)
  • 寄存器初始化
    • x0 (argc): 参数个数
    • x1 (argv): 指向 argv[0] 的地址(上图中 0x00007ffffd0000
    • x2 (envp): 指向 envp[0] 的地址(上图中 0x00007fffff0000

4. 从内核模块访问参数的注意事项

若需要在自定义内核代码(如 LSM 模块)中访问参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在 security_bprm_check 钩子中
int my_bprm_check(struct linux_binprm *bprm)
{
char **argv = (char **)bprm->p;

// 遍历 argv
for (int i = 0; argv[i] != NULL; i++) {
pr_info("argv[%d]: %s\n", i, argv[i]);
}

// 获取 envp (跳过 argv 的 NULL 分隔符)
char **envp = argv;
while (*envp++ != NULL);

// 遍历 envp
for (int j = 0; envp[j] != NULL; j++) {
pr_info("envp[%d]: %s\n", j, envp[j]);
}
return 0;
}

重要限制

  1. 仅能在 execve 执行路径的钩子(如 security_bprm_check)中访问
  2. bprm->p 只在当前 execve 上下文有效
  3. 直接访问用户空间指针需用 copy_from_user(但此处已是内核副本)

总结:ARM64 的参数处理流程

  1. 存储位置
    参数/环境变量存储在 bprm->p 指向的内核内存块中(**非 bprm->exec**)。

  2. 访问方式

    • 在内核中:通过 (char **)bprm->p 遍历指针数组
    • 在用户空间:通过寄存器 x0(argc), x1(argv), x2(envp) 访问
  3. ARM64 特定处理

    • create_elf_tables 负责构建符合 ARM64 ABI 的栈布局
    • 栈起始地址 sp = 0x8000000000(用户空间高地址)
    • 参数通过寄存器 x0-x2 传递
  4. 关键函数

    • load_elf_binary → 解析 ELF 文件时访问参数
    • create_elf_tables → 设置 ARM64 用户栈和寄存器