pkey_alloc pkey_free系统调用及示例

pkey_alloc pkey_free系统调用及示例

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

我们继续按照您的要求学习 Linux 系统编程中的重要函数。这次我们介绍 pkey_alloc 和 pkey_free。

1. 函数介绍

pkey_alloc 和 pkey_free 是一组与 内存保护键(Memory Protection Keys, MPK)相关的 Linux 系统调用(内核版本 >= 4.9, x86 架构)。它们是 Intel MPK (Memory Protection Keys) 特性的用户态接口。

**核心概念:内存保护键 **(Protection Keys)

想象你的内存是一间大房子,里面有各种不同的房间(内存页)。传统上,你只有一把总钥匙(页表项中的权限位 RWX)来控制进入这间房子的所有门(内存访问)。如果这把钥匙丢了或者被复制,坏人就能进入所有房间。

内存保护键(MPK)就像是给这间大房子加装了多把独立的锁(保护键):

获取钥匙 (pkey_alloc) 你向操作系统申请一把新的、独立的钥匙(保护键)。操作系统给你一个钥匙编号(pkey)。

**给房间上锁 **(pkey_mprotect) 你可以使用 pkey_mprotect 系统调用,将特定的房间(内存区域)与你刚申请到的那把钥匙(pkey)关联起来。这相当于给这些房间的门加上了这把新锁。

**控制钥匙 **(特殊寄存器) CPU 内部有一个特殊的寄存器(x86 上是 PKRU - Protection Key Rights User register)。这个寄存器里有 16 个插槽(对应 16 个可能的 pkey),每个插槽可以设置为允许或禁止访问。

尝试进入房间: 当程序试图访问一个与特定 pkey 关联的内存页时:

  • CPU 会检查页表项中的 pkey 编号(例如 3)。

  • 然后检查 PKRU 寄存器中对应插槽(第 3 个插槽)的权限。

  • 如果 PKRU 允许访问(例如,插槽 3 是 0b00),访问成功。

  • 如果 PKRU 禁止访问(例如,插槽 3 是 0b01 或 0b10),CPU 会立即产生一个 SIGSEGV (段错误) 信号,而无需进行昂贵的页表遍历。

pkey_alloc 和 pkey_free 的作用:

  • pkey_alloc: 申请一个可用的内存保护键(pkey)。成功时返回一个唯一的 pkey 编号(0-15)。

  • pkey_free: 释放一个之前通过 pkey_alloc 申请的 pkey,使其可以被其他部分的程序再次申请使用。

优势:

  • 快速权限切换: 通过修改 PKRU 寄存器(一个非常快的操作),可以瞬间改变对大量内存区域的访问权限,而无需修改每个内存页的页表项。

  • 细粒度保护: 可以将不同的内存区域分配给不同的 pkey,实现更细粒度的内存访问控制。

  • 硬件加速: 权限检查由 CPU 硬件直接完成,性能极高。

相关文章:pkey_alloc系统调用及示例-CSDN博客 Linux 3.0 内核系统调用 preadv2系统调用及示例

2. 函数原型

1
2
3
4
5
6
7
8
#include <sys/mman.h> // 包含 MPK 相关常量和函数声明 (需要较新的 glibc)

// 分配一个保护键
int pkey_alloc(unsigned int flags, unsigned int access_rights);

// 释放一个保护键
int pkey_free(int pkey);

注意: 这些函数需要 glibc 2.27 或更高版本。在较旧的系统上,可能需要直接使用 syscall。

3. 功能

pkey_alloc:

  • 向内核请求一个当前未被使用的内存保护键。

  • 内核会返回一个唯一的 pkey 编号(通常在 0 到 15 之间,具体取决于 CPU 实现)。

  • 这个 pkey 可以用于后续的 pkey_mprotect 调用。

pkey_free:

  • 将一个之前分配的 pkey 返还给内核。

  • 释放后,该 pkey 可以被后续的 pkey_alloc 调用再次分配。

  • 重要: 在调用 pkey_free 之前,应该确保没有内存区域仍然通过 pkey_mprotect 与该 pkey 关联,否则这些区域的访问权限可能会变得不可预测。

4. 参数

pkey_alloc

  • unsigned int flags: 目前必须设置为 0。保留供将来扩展。

unsigned int access_rights: 指定该 pkey 的初始访问权限。这是一个位掩码,定义在 <sys/mman.h> 中。

  • PKEY_DISABLE_ACCESS: 禁止所有访问(读、写、执行)。这是最严格的权限。

  • PKEY_DISABLE_WRITE: 禁止写入,但允许读取和执行。

  • 0: 允许所有访问(读、写、执行)。这是最宽松的权限。

  • 注意: 这个初始权限是设置在内核内部的,与 PKRU 寄存器中的权限是分开的。pkey_alloc 返回的 pkey,其在 PKRU 中的初始状态通常是允许访问的。这个 access_rights 参数更多是作为一种内核层面的标记或备用机制。

pkey_free

  • int pkey: 这是之前通过成功的 pkey_alloc 调用返回的保护键编号。

5. 返回值

pkey_alloc:

  • 成功时: 返回一个非负整数,即新分配的保护键编号(pkey)。

  • 失败时: 返回 -1,并设置 errno(例如 ENOSPC 没有可用的 pkey,EINVAL flags 或 access_rights 无效,EOPNOTSUPP 硬件不支持)。

pkey_free:

  • 成功时: 返回 0。

  • 失败时: 返回 -1,并设置 errno(例如 EINVAL pkey 无效,EOPNOTSUPP 硬件不支持)。

6. 相似函数,或关联函数

  • pkey_mprotect: 用于将一个内存区域与特定的 pkey 关联起来,是使用 pkey 进行内存保护的核心函数。

  • mprotect: 传统的内存保护函数,修改内存区域的 RWX 权限。pkey_mprotect 是其增强版,增加了 pkey 功能。

  • mmap: 用于分配和映射内存区域,这些区域后续可以用 pkey_mprotect 来关联 pkey。

  • syscall(SYS_pkey_alloc, …) / syscall(SYS_pkey_free, …): 在 glibc 不支持时,直接调用系统调用的方式。

  1. 示例代码

重要提示:

硬件和内核支持: MPK 仅在支持该特性的 CPU(如 Intel x86_64 Skylake 及更新架构)和 Linux 内核(>= 4.9)上可用。

glibc 版本: 需要 glibc 2.27 或更高版本。

复杂性: 使用 MPK 需要结合 pkey_mprotect 和对 PKRU 寄存器的操作(通常通过内联汇编或专用库函数),下面的示例主要演示 pkey_alloc/free 的基本用法。

示例 1:检查支持并基本使用 pkey_alloc/free

这个例子演示了如何检查系统是否支持 MPK,然后分配和释放保护键。

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
// pkey_basic_example.c
#define _GNU_SOURCE // For pkey functions
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

// 如果 glibc 版本过低,可能需要手动定义系统调用号
#ifndef SYS_pkey_alloc
#define SYS_pkey_alloc 330
#endif
#ifndef SYS_pkey_free
#define SYS_pkey_free 331
#endif

// 用于手动调用系统调用的包装函数 (如果需要)
// #include <sys/syscall.h>
// static inline int my_pkey_alloc(unsigned int flags, unsigned int access_rights) {
// return syscall(SYS_pkey_alloc, flags, access_rights);
// }
// static inline int my_pkey_free(int pkey) {
// return syscall(SYS_pkey_free, pkey);
// }

// 检查系统是否支持内存保护键
int check_pkey_support() {
int pkey;
// 尝试分配一个 pkey 来检查支持
pkey = pkey_alloc(0, 0);
if (pkey == -1) {
if (errno == EOPNOTSUPP) {
return 0; // Not supported
} else {
// Other error, but might still support it in general
// Let's assume it does and let the main code handle specific errors
return 1;
}
}
// Allocation succeeded, support confirmed. Free it.
if (pkey_free(pkey) == -1) {
perror("pkey_free after check");
}
return 1;
}

int main() {
printf("Checking for Memory Protection Key (MPK) support...\n");

if (!check_pkey_support()) {
fprintf(stderr, "Memory Protection Keys (MPK) are NOT supported on this system/CPU.\n");
fprintf(stderr, "This might be because:\n");
fprintf(stderr, " 1. The CPU does not support MPK (e.g., older than Intel Skylake).\n");
fprintf(stderr, " 2. The Linux kernel version is older than 4.9.\n");
fprintf(stderr, " 3. The feature is disabled.\n");
exit(EXIT_FAILURE);
}

printf("MPK support detected.\n");

// --- 分配保护键 ---
printf("\n--- Allocating Protection Keys ---\n");

int pkey1, pkey2;

// 分配第一个 pkey,初始权限为允许所有访问
pkey1 = pkey_alloc(0, 0);
if (pkey1 == -1) {
perror("pkey_alloc 1 failed");
exit(EXIT_FAILURE);
}
printf("Successfully allocated pkey 1: %d\n", pkey1);

// 分配第二个 pkey,初始权限为禁止写入
pkey2 = pkey_alloc(0, PKEY_DISABLE_WRITE);
if (pkey2 == -1) {
perror("pkey_alloc 2 failed");
// Cleanup previously allocated pkey
pkey_free(pkey1);
exit(EXIT_FAILURE);
}
printf("Successfully allocated pkey 2: %d (initially write-disabled)\n", pkey2);

// 尝试分配更多 pkey (系统通常限制为 16 个, 0-15)
printf("\nAttempting to allocate more pkeys...\n");
int pkeys&#91;20];
int allocated_count = 0;
for (int i = 0; i < 20; i++) {
pkeys&#91;i] = pkey_alloc(0, 0);
if (pkeys&#91;i] == -1) {
if (errno == ENOSPC) {
printf(" Allocation %d failed: No space left (ENOSPC). Max pkeys reached.\n", i);
break;
} else {
printf(" Allocation %d failed with errno %d (%s)\n", i, errno, strerror(errno));
break;
}
} else {
printf(" Allocated pkey %d: %d\n", i, pkeys&#91;i]);
allocated_count++;
}
}

// --- 释放保护键 ---
printf("\n--- Freeing Protection Keys ---\n");

// 释放前两个
if (pkey_free(pkey1) == -1) {
perror("pkey_free pkey1 failed");
} else {
printf("Successfully freed pkey 1 (%d)\n", pkey1);
}

if (pkey_free(pkey2) == -1) {
perror("pkey_free pkey2 failed");
} else {
printf("Successfully freed pkey 2 (%d)\n", pkey2);
}

// 释放之前批量分配的
for (int i = 0; i < allocated_count; i++) {
if (pkey_free(pkeys&#91;i]) == -1) {
printf("Failed to free pkey %d (%d)\n", i, pkeys&#91;i]);
} else {
printf("Successfully freed pkey %d (%d)\n", i, pkeys&#91;i]);
}
}

printf("\nAll pkey operations completed.\n");
return 0;
}

如何编译和测试:

1
2
3
4
# 需要较新的 glibc (>= 2.27)
gcc -o pkey_basic_example pkey_basic_example.c
./pkey_basic_example

代码解释:

定义了必要的头文件。

check_pkey_support: 通过尝试调用 pkey_alloc 来粗略检查系统是否支持 MPK。如果返回 EOPNOTSUPP,则说明不支持。

在 main 函数中,首先调用 check_pkey_support。

分配 pkey:

  • 调用 pkey_alloc(0, 0) 分配第一个 pkey,初始权限为允许所有访问。

  • 调用 pkey_alloc(0, PKEY_DISABLE_WRITE) 分配第二个 pkey,初始权限为禁止写入。

  • 通过一个循环尝试分配更多 pkey,直到系统报告 ENOSPC(没有空间,即达到上限)。

释放 pkey:

  • 调用 pkey_free 释放之前分配的所有 pkey。

打印相关信息。

示例 2:结合 mmap, pkey_mprotect 使用 pkey_alloc/free (概念性)

这个例子展示了一个更完整的、但概念性的用法,结合了分配内存、分配 pkey、关联内存与 pkey 以及通过 PKRU 控制访问。请注意:直接操作 PKRU 寄存器需要内联汇编,这比较复杂且依赖于架构。

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
// pkey_conceptual_example.c
#define _GNU_SOURCE
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>

static jmp_buf jmp_env;
static volatile sig_atomic_t sigsegv_caught = 0;

// 信号处理函数
void sigsegv_handler(int sig) {
sigsegv_caught = 1;
longjmp(jmp_env, 1);
}

// 概念性的 PKRU 操作函数 (实际需要内联汇编)
// 这里用伪代码表示
void write_pkru(unsigned int pkru_value) {
// In real code, this would be inline assembly like:
// asm volatile(".byte 0x0f,0x01,0xef\n\t" : : "a" (pkru_value), "d" (0), "c" (0) : "memory");
printf(" &#91;Concept] Writing PKRU with value: 0x%08x\n", pkru_value);
// WARNING: This is NOT real code, just for illustration.
// Real code needs inline assembly.
}

unsigned int read_pkru() {
// In real code:
// unsigned int pkru;
// asm volatile(".byte 0x0f,0x01,0xee\n\t" : "=a" (pkru) : "c" (0) : "rdx", "memory");
// return pkru;
printf(" &#91;Concept] Reading PKRU\n");
return 0; // Dummy return
}

int main() {
if (sysconf(_SC_MPKEY) <= 0) { // Check if supported
fprintf(stderr, "MPK not supported by sysconf.\n");
exit(EXIT_FAILURE);
}

struct sigaction sa;
sa.sa_handler = sigsegv_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGSEGV, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}

size_t len = 4096; // 1 page
void *addr;

// 1. 分配内存
addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
printf("Allocated memory at %p\n", addr);

// 2. 写入一些数据
strcpy((char*)addr, "Initial data in protected memory.");
printf("Written initial data.\n");

// 3. 分配一个 pkey
int pkey = pkey_alloc(0, 0);
if (pkey == -1) {
perror("pkey_alloc");
munmap(addr, len);
exit(EXIT_FAILURE);
}
printf("Allocated pkey: %d\n", pkey);

// 4. 将内存区域与 pkey 关联 (概念性)
// 实际需要调用 pkey_mprotect(addr, len, PROT_READ|PROT_WRITE, pkey);
printf("--- Conceptually associating memory with pkey %d ---\n", pkey);
// printf("Would call: pkey_mprotect(%p, %zu, PROT_READ|PROT_WRITE, %d)\n", addr, len, pkey);

// 5. 通过修改 PKRU 来禁止访问 pkey (概念性)
printf("\n--- Disabling access to pkey %d via PKRU ---\n", pkey);
// 计算新的 PKRU 值以禁用 pkey 的访问
// 每个 pkey 在 PKRU 中占 2 位: 00(allow), 01(deny access), 10(deny write), 11(deny access)
// 假设 pkey 是 1, 那么它在 PKRU 的 bit 2-3
// unsigned int current_pkru = read_pkru();
unsigned int new_pkru = 0; // Start with allowing all
// Set bits for pkey to 01 (deny access)
// new_pkru |= (1 << (pkey * 2));
// write_pkru(new_pkru);
printf(" &#91;Concept] Would set PKRU bits for pkey %d to deny access.\n", pkey);

// 6. 尝试访问受保护的内存 (应该触发 SIGSEGV)
printf("\n--- Attempting to access protected memory ---\n");
sigsegv_caught = 0;

if (setjmp(jmp_env) == 0) {
// This block will be executed first
printf(" Trying to read from %p...\n", addr);
char first_char = *((char*)addr); // This should trigger SIGSEGV
printf(" ERROR: Read succeeded (first char: %c). This should not happen!\n", first_char);
} else {
// This block will be executed if longjmp is called from signal handler
if (sigsegv_caught) {
printf(" SUCCESS: SIGSEGV caught as expected. Access denied by pkey.\n");
} else {
printf(" Unexpected longjmp.\n");
}
}

// 7. 重新允许访问
printf("\n--- Re-enabling access to pkey %d via PKRU ---\n", pkey);
// write_pkru(0); // Allow all access again
printf(" &#91;Concept] Would reset PKRU to allow all access.\n");

// 8. 再次尝试访问 (应该成功)
printf("\n--- Attempting to access memory again (should succeed now) ---\n");
printf(" Reading from %p: %s\n", addr, (char*)addr);

// 9. 清理
if (pkey_free(pkey) == -1) {
perror("pkey_free");
}
if (munmap(addr, len) == -1) {
perror("munmap");
}

printf("\nConceptual pkey example finished.\n");
return 0;
}

**代码解释 **(概念性):

设置了 SIGSEGV 信号处理函数和 setjmp/longjmp 机制来捕获预期的段错误。

使用 mmap 分配了一块匿名内存。

向内存写入了初始数据。

调用 pkey_alloc(0, 0) 分配一个 pkey。

概念性步骤: 描述了将内存与 pkey 关联(实际需要 pkey_mprotect)和通过修改 PKRU 寄存器禁止访问的操作。这部分用 printf 和伪函数 write_pkru/read_pkru 代替,因为真实的实现需要内联汇编。

尝试读取受保护的内存。在真实场景下,这会触发 SIGSEGV,信号处理函数会设置标志并 longjmp 回来。

概念性步骤: 描述了重新允许访问(重置 PKRU)。

再次尝试访问,这次应该成功。

释放 pkey 和内存。

重要提示与注意事项:

硬件和内核依赖: MPK 是 x86_64 架构(Intel Skylake 及更新)的特性,需要 Linux 内核 4.9+。

glibc 版本: 需要 glibc 2.27+ 才有原生支持。

复杂性: 真正使用 MPK 需要结合 pkey_mprotect 和对 PKRU 寄存器的精确控制(通常通过内联汇编),这比示例中展示的要复杂得多。

pkey_mprotect 是关键: pkey_alloc/free 只是管理 pkey 编号,真正将内存和权限联系起来的是 pkey_mprotect。

PKRU 操作: 直接读写 PKRU 寄存器是使用 MPK 功能的核心,但需要内联汇编知识。

错误处理: 始终检查 pkey_alloc 是否返回 EOPNOTSUPP(不支持)或 ENOSPC(pkey 耗尽)。

性能优势: MPK 的主要优势在于权限切换的极低延迟,因为它避免了修改页表的开销。

应用场景: 适用于需要快速、动态地改变大量内存区域访问权限的场景,如沙箱、内存安全库、调试器等。

总结:

pkey_alloc 和 pkey_free 是 Linux 内存保护键(MPK)机制的一部分,用于分配和回收独立的内存访问控制键。它们本身只是 pkey 生命周期管理的第一步。要真正利用 MPK 提供的快速、细粒度内存保护能力,还需要结合 pkey_mprotect 来关联内存区域,以及通过直接操作 PKRU 寄存器来动态启用或禁用访问权限。虽然使用起来比较底层和复杂,但对于需要极致内存安全和性能控制的应用来说,MPK 是一个强大的工具。

data-ad-format="auto" data-full-width-responsive="true">