Linux内核copy_string_kernel接口作用解析
Linux内核copy_string_kernel接口的作用
copy_string_kernel
是 Linux 内核中一个核心但非公开导出的函数(未包含在头文件中供模块使用),主要在 execve
系统调用的实现路径 (fs/exec.c
) 中扮演关键角色。它的核心作用如下:
核心作用:
安全、高效地将用户空间提供的字符串参数(argv
)和环境变量(envp
)复制到内核空间新分配的内存中,并正确地组织它们,为后续执行新的程序镜像做好准备。
详细解析其职责和过程:
处理用户空间输入:
- 当用户程序调用
execve(pathname, argv, envp)
时,argv
(命令行参数数组)和envp
(环境变量数组)是用户空间提供的指针数组。 - 这些指针指向用户空间内存中的字符串(以
'\0'
结尾)。
- 当用户程序调用
内核空间的安全复制:
- 内核代码不能直接解引用用户空间指针。必须先将用户空间的数据复制到内核空间,以避免:
- 页错误导致内核崩溃: 用户指针可能无效或指向未映射的内存。
- 恶意用户空间程序篡改内核: 在解引用过程中用户空间数据被改变。
- 时间差攻击 (TOCTOU): 在检查和使用的间隙数据被改变。
copy_string_kernel
负责执行这个关键的复制操作。
- 内核代码不能直接解引用用户空间指针。必须先将用户空间的数据复制到内核空间,以避免:
针对
execve
的特殊组织:execve
需要将argv
和envp
中的所有字符串打包成一个连续的内核内存块。这个内存块的布局通常是:- 所有字符串 (
argv[0]
,argv[1]
, …,envp[0]
,envp[1]
, …) 按顺序、以'\0'
结尾紧密排列。 - 在这个字符串块的末尾,紧接着存储一个指向每个字符串起始地址的 指针数组 (
char *
数组)。 - 这个指针数组的最后一项是
NULL
。 - 这个布局使得新进程的用户空间栈在初始化时,可以高效地设置
argv
和envp
指针。
- 所有字符串 (
copy_string_kernel
不仅仅简单地复制单个字符串,它在复制每个字符串时,会:- 分配内核内存: 为新字符串分配内核空间的内存。
- 复制内容: 使用类似
copy_from_user()
的机制(内部可能调用strncpy_from_user
),安全地将用户空间字符串内容复制到这块内核内存。 - 记录指针: 将新分配的内核内存地址(即新字符串在内核空间的起始地址)记录到
struct linux_binprm
结构 (bprm
) 的p
成员指向的位置。 - 更新位置指针: 递增
bprm->p
,使其指向内存块中下一个字符串应该存放的位置(考虑对齐),同时也会在指针数组中为下一个指针预留位置。 - 错误处理: 严格检查用户指针有效性、字符串长度(防止超长参数攻击)、内存分配是否成功等。遇到错误立即返回错误码。
与
struct linux_binprm
(bprm
) 的交互:bprm
结构体 (include/linux/binfmts.h
) 贯穿整个execve
执行过程,包含了加载新程序所需的所有信息。bprm
的关键成员bprm->p
是一个游标指针:- 初始时指向一块预先分配好的内核内存 (
bprm->buf
或后续扩展的内存) 的末尾。 copy_string_kernel
从bprm->p
开始向前(递减地址)写入字符串内容和构建指针数组。- 每复制一个字符串或添加一个指针,
bprm->p
就向前移动相应距离(考虑对齐)。
- 初始时指向一块预先分配好的内核内存 (
- 这种从后向前填充的方式是为了高效构建最终的
argv
/envp
内存布局。最终,bprm->p
会指向整个打包块(指针数组 + 字符串)的起始地址。
总结流程(简化):
- 在
do_execveat_common
->prepare_binprm
->copy_strings_kernel
(该函数会循环调用copy_string_kernel
) 的路径中调用。 - 对于用户空间
argv
/envp
数组中的每一个字符串:- 检查用户空间指针有效性。
- 计算字符串长度(包含结尾
'\0'
)。 - 在内核内存块(由
bprm->p
指示当前末尾)中分配空间。 - 安全地从用户空间复制字符串内容到分配的内核空间。
- 将新字符串的内核地址记录在
bprm->p
指向的位置(这是指针数组的一部分)。 - 向前移动
bprm->p
(减去字符串长度和可能的对齐填充),使其指向下一个可用位置(用于存放下一个字符串内容)。 - 同时向前移动
bprm->p
(减去一个指针的大小),使其指向指针数组的下一个槽位(用于存放下一个字符串的指针)。
- 当所有字符串复制完成后:
- 在指针数组的最后一个槽位(此时
bprm->p
指向的位置)存入NULL
。 bprm->p
最终指向了整个打包好的参数/环境块(包含指针数组和所有字符串)在内核内存中的起始地址。
- 在指针数组的最后一个槽位(此时
- 这个打包好的内核内存块稍后会被复制到新进程的用户空间栈上,成为新程序的
argv
和envp
。
为什么需要它?为什么不直接用 strndup_user
?
- 特殊的内存布局要求:
execve
要求参数和环境变量以特定的连续方式组织(字符串块 + 指针数组),copy_string_kernel
直接负责构建这种布局。 - 与
bprm
状态管理集成: 它紧密地与bprm
结构及其游标指针bprm->p
协同工作,管理内存分配和布局的进度。 - 性能与效率: 在
execve
这个高频且关键的操作路径上,需要高度优化的代码来处理大量字符串。copy_string_kernel
被设计为满足这个特定场景的高效和安全需求。
简单来说,copy_string_kernel
是 execve
系统调用内部的一个关键助手,负责安全、高效地将用户命令行参数和环境变量“搬运”并“打包”到内核空间,为启动新进程奠定基础。它是连接用户空间意图和内核执行新程序的桥梁中至关重要的一环。
Linux内核中从bprm->p中获取进程的参数和环境变量
在 Linux 内核中,struct linux_binprm
(简称 bprm
)结构体用于管理 execve
系统调用加载新程序的过程。bprm->p
是一个关键成员,它指向内核空间中打包好的参数和环境变量块的起始位置。下面是其工作原理和获取参数的详细说明:
1. bprm->p
指向的内容
在 execve
执行过程中,copy_string_kernel
(通过 copy_strings_kernel
调用)会将用户空间的 argv
和 envp
打包成一个连续的内核内存块,其结构如下:
1 | 低地址 (起始位置) 高地址 |
具体包含两部分:
- 指针数组:存储所有参数字符串的内核地址。
- 格式:
[argv[0], argv[1], ..., argv[n], NULL, envp[0], envp[1], ..., envp[m], NULL]
- 以两个
NULL
结尾(分隔argv
和envp
)。
- 格式:
- 字符串数据区:紧跟在指针数组后,存储所有参数字符串的实际内容(以
\0
结尾)。
2. 如何从 bprm->p
获取参数
步骤示例(伪代码逻辑)
1 | // 1. 获取指针数组的起始地址 |
3. 关键实现位置
该逻辑主要在内核的 execve
路径中实现,尤其是在以下函数中:
create_elf_tables()
(位于fs/binfmt_elf.c
)
将bprm->p
指向的内核参数块复制到新进程的用户空间栈,并设置用户态的argc/argv/envp
指针。setup_arg_pages()
(位于fs/exec.c
)
准备新进程的栈内存,为复制参数块提供空间。
4. 用户空间视角
最终,当新程序启动时,其用户空间栈布局如下(以 x86_64 为例):
1 | 高地址 |
用户进程通过 main(int argc, char **argv, char **envp)
访问这些参数。
5. 注意事项
- 内核专用:
bprm->p
仅在execve
上下文(内核态)有效,不能直接暴露给用户空间。 - 安全性:
所有字符串均经过安全复制(如copy_from_user
),防止用户空间恶意指针。 - 架构差异:
栈布局(如参数顺序、对齐)因架构(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 | static int load_elf_binary(struct linux_binprm *bprm) |
(2) ARM64 用户空间栈设置 (create_elf_tables
)
文件位置:fs/binfmt_elf.c
这是将内核参数复制到 ARM64 用户空间栈的关键函数:
1 | static int create_elf_tables(struct linux_binprm *bprm, ...) |
3. ARM64 用户空间栈布局
当新进程启动时,其用户空间栈布局如下(符合 ARM64 ABI):
1 | 高地址 (0x7ffffffff000) |
- 寄存器初始化:
x0 (argc)
: 参数个数x1 (argv)
: 指向argv[0]
的地址(上图中0x00007ffffd0000
)x2 (envp)
: 指向envp[0]
的地址(上图中0x00007fffff0000
)
4. 从内核模块访问参数的注意事项
若需要在自定义内核代码(如 LSM 模块)中访问参数:
1 | // 在 security_bprm_check 钩子中 |
重要限制:
- 仅能在
execve
执行路径的钩子(如security_bprm_check
)中访问 bprm->p
只在当前execve
上下文有效- 直接访问用户空间指针需用
copy_from_user
(但此处已是内核副本)
总结:ARM64 的参数处理流程
存储位置:
参数/环境变量存储在bprm->p
指向的内核内存块中(**非bprm->exec
**)。访问方式:
- 在内核中:通过
(char **)bprm->p
遍历指针数组 - 在用户空间:通过寄存器
x0
(argc),x1
(argv),x2
(envp) 访问
- 在内核中:通过
ARM64 特定处理:
create_elf_tables
负责构建符合 ARM64 ABI 的栈布局- 栈起始地址
sp = 0x8000000000
(用户空间高地址) - 参数通过寄存器
x0
-x2
传递
关键函数:
load_elf_binary
→ 解析 ELF 文件时访问参数create_elf_tables
→ 设置 ARM64 用户栈和寄存器