pkey_mprotect系统调用及示例
data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">我们来深入学习 pkey_mprotect 系统调用
相关文章:pkey_mprotect系统调用及示例-CSDN博客 pkey_mprotect系统调用及示例 pkey_alloc pkey_free系统调用及示例 preadv2系统调用及示例
1. 函数介绍
在 Linux 系统中,内存保护是一个核心的安全机制。我们使用 mprotect 系统调用(或者 C 标准库的 mprotect 函数)来设置一块内存区域的访问权限,比如:
只读 (PROT_READ)
可写 (PROT_WRITE)
可执行 (PROT_EXEC)
例如,你可以将包含程序代码的内存页设置为“只读可执行”,防止程序意外修改自己的代码;或者将包含数据的内存页设置为“只读”,防止意外写入。
然而,mprotect 有一个限制:整个进程都遵循同一套内存保护规则。如果一个进程有权限修改某块内存的保护属性(通常需要特殊权限),它就可以修改任何内存页的权限。
Memory Protection Keys (MPK) 是 Intel 和 ARM 等 CPU 架构引入的一种更细粒度的内存保护机制。它允许将内存区域与一个密钥 (Protection Key) 关联起来。这个密钥就像一把锁,控制着与之关联的内存区域是否可以被访问。
pkey_mprotect 系统调用就是用来在设置内存区域访问权限的同时,将这块内存与一个特定的保护密钥 (pkey) 关联起来。
简单来说:
mprotect(addr, len, prot): 设置 addr 开始的 len 字节内存的权限为 prot。
pkey_mprotect(addr, len, prot, pkey): 做同样的事,外加将这块内存和密钥 pkey 绑定。
要访问一块由 pkey_mprotect 保护的内存,不仅需要满足 prot 指定的权限(如 PROT_READ),当前线程的密钥权限掩码 (PKRU) 中对应的 pkey 也必须允许这种访问。
这提供了一种额外的、硬件加速的、线程级的内存访问控制。即使程序通过 mprotect 获得了写权限,如果线程的 pkey 设置禁止写入,访问仍然会失败。
典型应用场景:
沙箱/安全容器:为不同来源或信任级别的代码分配不同的 pkey,防止它们互相干扰或越权访问。
调试器/分析器:保护关键数据结构不被被调试的程序意外修改。
高性能库:在复杂的内存管理库中,使用 pkey 来防止用户代码错误地访问库的内部数据。
2. 函数原型
1 | #define _GNU_SOURCE // 启用 GNU 扩展以使用 pkey_mprotect |
注意:此函数需要 glibc 2.27 或更高版本,并且运行在支持 Memory Protection Keys 的 CPU 上(如 Intel Haswell 及更新的处理器,或支持相应特性的 ARM 处理器)。
3. 功能
为从 addr 开始、长度为 len 字节的内存区域设置访问权限 (prot),并将其与保护密钥 (pkey) 关联。
4. 参数
addr:
void * 类型。
指向要修改保护属性的内存区域的起始地址。这个地址必须是页对齐的(通常是 4KB 边界)。
len:
size_t 类型。
要修改保护属性的内存区域的长度(字节数)。
prot:
- int 类型。
指定要设置的内存访问权限。可以是以下值的按位或 (|) 组合:
PROT_NONE: 内存无法访问。
PROT_READ: 页面可读。
PROT_WRITE: 页面可写。
PROT_EXEC: 页面可执行。
pkey:
int 类型。
指定要与该内存区域关联的保护密钥。
有效的 pkey 值是 0 到 PKEY_MAX (通常是 15) 之间的整数。
pkey 为 -1 时,表示不改变该内存区域当前关联的 pkey(如果有的话)。
特殊的 pkey 值 PKEY_DISABLE_ACCESS 和 PKEY_DISABLE_WRITE 可用于 pkey_set 函数,而不是 pkey_mprotect。
5. 返回值
成功: 返回 0。
失败: 返回 -1,并设置全局变量 errno 来指示具体的错误原因。
6. 错误码 (errno)
pkey_mprotect 可能返回的错误码与 mprotect 相同或类似,并增加了一些与 pkey 相关的:
EINVAL: addr 不是页对齐的,或者 prot 或 pkey 参数无效。
ENOMEM: 地址范围 (addr to addr+len) 无效,或者包含了未映射的页面。
EFAULT: addr 指向了调用进程无法访问的内存地址。
EACCES: 调用进程没有权限修改指定内存区域的保护属性。
ENOSYS: 系统调用不被当前内核或硬件支持(例如,CPU 不支持 MPK)。
7. 相似函数或关联函数
mprotect: 不使用保护密钥的标准内存保护函数。
pkey_alloc: 分配一个新的保护密钥。
pkey_free: 释放一个之前分配的保护密钥。
pkey_get: 获取当前线程的密钥权限掩码 (PKRU) 的值。
pkey_set: 设置当前线程的密钥权限掩码 (PKRU) 的值。
mmap: 用于分配和映射内存区域。
sysconf(_SC_PAGESIZE): 获取系统页大小,用于确保地址对齐。
8. 示例代码
下面的示例演示了如何使用 pkey_mprotect 来保护内存区域。请注意,此代码需要在支持 MPK 的硬件和内核上运行。
1 | #define _GNU_SOURCE |
9. 编译和运行
1 | # 假设代码保存在 pkey_mprotect_example.c 中 |
10. 预期输出 (在支持 MPK 的系统上)
1 | --- Demonstrating pkey_mprotect --- |
在不支持 MPK 的系统上的输出:
1 | --- Demonstrating pkey_mprotect --- |
11. 总结
pkey_mprotect 是一个利用现代 CPU 硬件特性(Memory Protection Keys)来提供更精细内存访问控制的系统调用。
核心优势:
额外保护层:在传统的 mprotect 权限之上增加了一层由硬件支持的、基于密钥的访问控制。
线程级控制:每个线程可以通过 pkey_set 独立控制自己对不同 pkey 保护区域的访问权限。
高性能:由 CPU 硬件直接处理,检查开销极小。
工作流程:
使用 pkey_alloc 获取一个 pkey。
使用 pkey_mprotect 将内存区域与 pkey 和访问权限 (prot) 关联。
使用 pkey_get 和 pkey_set 控制当前线程的 PKRU 寄存器,决定哪些 pkey 允许访问/写入。
当线程尝试访问内存时,CPU 会同时检查 mprotect 权限和 PKRU 中对应的 pkey 权限。
使用前提:
CPU 支持 MPK (如 Intel RDPID + Protection Keys for User-mode pages)。
Linux 内核支持 (通常 4.9+)。
glibc 版本足够新 (2.27+)。
典型应用:沙箱、安全容器、调试器、高性能库。
对于 Linux 编程新手来说,pkey_mprotect 是一个高级特性,理解其概念和潜在用途有助于学习现代系统安全和内存管理的前沿技术。但在日常开发中,mprotect 仍然是管理内存权限的主要工具。