好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 mprotect
函数,它用于修改由 mmap
创建的内存映射区域(或其他内存区域)的保护属性(例如,从可读写改为只读,或添加执行权限)。
1. 函数介绍 Link to heading
mprotect
是一个 Linux 系统调用,用于更改调用进程虚拟地址空间中指定内存区域的访问权限(保护模式)。这个内存区域通常是由 mmap
映射的,但也可能是由 malloc
分配的堆内存或其他通过系统调用获得的内存区域。
在程序运行时,有时需要动态地调整内存区域的访问权限。例如:
- 安全: 你可能希望先在一个区域写入一些代码或数据,然后将其锁定为只读,以防止后续意外修改。
- 执行代码: 动态生成的代码需要被标记为可执行 (
PROT_EXEC
) 才能运行。 - 调试: 临时禁用对某块内存的访问以检测非法访问。
mprotect
提供了一种机制来实现这些需求。
2. 函数原型 Link to heading
#include <sys/mman.h> // 必需
int mprotect(void *addr, size_t len, int prot);
3. 功能 Link to heading
- 修改保护: 尝试将从地址
addr
开始、长度为len
字节的内存区域的访问权限修改为由prot
参数指定的新权限。 - 页对齐:
addr
参数通常需要与系统内存页边界对齐。len
不需要对齐,但内核会将其向上舍入到最近的页边界。 - 权限应用: 新的保护属性
prot
会应用于指定范围内的所有内存页。
4. 参数 Link to heading
void *addr
: 指向要修改保护属性的内存区域的起始地址。- 重要: 这个地址必须是系统内存页大小的整数倍(页对齐)。可以使用
getpagesize()
获取页大小。
- 重要: 这个地址必须是系统内存页大小的整数倍(页对齐)。可以使用
size_t len
: 要修改保护属性的内存区域的长度(以字节为单位)。- 这个长度不需要页对齐,但内核会作用于包含该区域的所有页面。
int prot
: 指定新的保护模式。这是一个位掩码,由以下值按位或组合:PROT_NONE
: 页面不可访问。PROT_READ
: 页面可读。PROT_WRITE
: 页面可写。PROT_EXEC
: 页面可执行。 例如:PROT_READ
: 设置为只读。PROT_READ | PROT_WRITE
: 设置为可读可写。PROT_NONE
: 禁止所有访问。
5. 返回值 Link to heading
- 成功时: 返回 0。
- 失败时: 返回 -1,并设置全局变量
errno
来指示具体的错误原因(例如EINVAL
addr
未对齐或prot
无效,ENOMEM
地址范围无效或包含不可修改保护的区域,EACCES
操作不被允许等)。
6. 相似函数,或关联函数 Link to heading
mmap
: 通常在mmap
创建映射区域时指定初始保护属性,而mprotect
用于后续修改。munmap
: 用于完全解除内存映射,而mprotect
只是改变访问权限。malloc
/free
: 虽然mprotect
主要与mmap
关联,但理论上也可以尝试修改malloc
分配的内存区域的保护(但这比较复杂且不推荐,因为malloc
的内部实现可能不兼容)。
7. 示例代码 Link to heading
示例 1:修改 mmap
映射区域的权限
Link to heading
这个例子演示如何先创建一个可读写的内存映射,然后使用 mprotect
将其改为只读。
#include <sys/mman.h> // mmap, munmap, mprotect
#include <unistd.h> // getpagesize
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit
#include <string.h> // strcpy
int main() {
size_t length = 4096; // 通常至少一页大小
char *mapped_memory;
long pagesize;
pagesize = getpagesize();
printf("System page size: %ld bytes\n", pagesize);
// 1. 创建一个可读写的私有匿名映射
mapped_memory = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapped_memory == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
printf("Memory mapped successfully at %p (length %zu bytes)\n", (void*)mapped_memory, length);
// 2. 在映射区域中写入数据 (因为初始是可写的)
strcpy(mapped_memory, "Initial data written to mapped memory.");
printf("Data written: %s\n", mapped_memory);
// 3. 使用 mprotect 将映射区域改为只读
if (mprotect(mapped_memory, length, PROT_READ) == -1) {
perror("mprotect to read-only");
// 清理并退出
munmap(mapped_memory, length);
exit(EXIT_FAILURE);
}
printf("Protection changed to read-only using mprotect.\n");
// 4. 尝试读取 (应该成功)
printf("Reading data again: %s\n", mapped_memory);
// 5. 尝试写入 (应该失败,导致段错误 Segmentation fault)
printf("Attempting to write to read-only memory...\n");
// 为了安全演示,我们注释掉这行会导致崩溃的代码
// strcpy(mapped_memory, "This will crash!"); // <-- 取消注释这行会导致程序崩溃
printf("Write operation skipped to prevent crash in example.\n");
// 如果你真的想测试,可以取消上面 strcpy 的注释,编译运行,程序会因段错误退出。
// 6. 清理
if (munmap(mapped_memory, length) == -1) {
perror("munmap");
}
printf("Memory unmapped.\n");
return 0;
}
代码解释:
- 获取系统页大小。
- 使用
mmap
创建一个大小为一页(4KB)的可读写 (PROT_READ | PROT_WRITE
) 私有匿名 (MAP_PRIVATE | MAP_ANONYMOUS
) 映射。 - 向映射区域写入数据,这会成功,因为初始权限是可写的。
- 调用
mprotect(mapped_memory, length, PROT_READ)
将整个映射区域的权限修改为只读。 - 检查
mprotect
的返回值,失败则处理错误。 - 尝试读取数据(成功)。
- 注释说明: 尝试再次写入会导致段错误(Segmentation fault),因为内存现在是只读的。示例中为了避免崩溃,没有执行写入操作。
- 最后使用
munmap
释放内存。
示例 2:动态修改权限以实现简单状态机 Link to heading
这个例子展示了一个更抽象的概念:使用 mprotect
来控制程序的状态或资源访问。
#include <sys/mman.h> // mmap, munmap, mprotect
#include <unistd.h> // getpagesize
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit
#include <string.h> // memset
// 定义几种状态
#define STATE_INITIAL 0
#define STATE_LOCKED 1
#define STATE_READONLY 2
int current_state = STATE_INITIAL;
char *protected_data;
size_t data_length;
// 初始化受保护的数据区域
int init_protected_data() {
long pagesize = getpagesize();
data_length = pagesize; // 使用一页大小
protected_data = mmap(NULL, data_length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (protected_data == MAP_FAILED) {
perror("mmap in init_protected_data");
return -1;
}
// 初始化数据
memset(protected_data, 'A', data_length / 2);
memset(protected_data + data_length / 2, 'B', data_length / 2);
printf("Protected data initialized.\n");
return 0;
}
// 锁定数据区域 (禁止访问)
int lock_data() {
if (mprotect(protected_data, data_length, PROT_NONE) == -1) {
perror("mprotect to lock (PROT_NONE)");
return -1;
}
current_state = STATE_LOCKED;
printf("Data locked (PROT_NONE).\n");
return 0;
}
// 设置数据为只读
int set_readonly() {
if (mprotect(protected_data, data_length, PROT_READ) == -1) {
perror("mprotect to readonly (PROT_READ)");
return -1;
}
current_state = STATE_READONLY;
printf("Data set to read-only (PROT_READ).\n");
return 0;
}
// 恢复数据为可读写 (初始状态)
int set_readwrite() {
if (mprotect(protected_data, data_length, PROT_READ | PROT_WRITE) == -1) {
perror("mprotect to readwrite (PROT_READ | PROT_WRITE)");
return -1;
}
current_state = STATE_INITIAL;
printf("Data set to read-write (PROT_READ | PROT_WRITE).\n");
return 0;
}
// 尝试访问数据 (根据当前状态)
void access_data() {
switch(current_state) {
case STATE_INITIAL:
printf("Accessing data (read-write state):\n");
protected_data[0] = 'X'; // 写入
printf(" First byte: %c\n", protected_data[0]); // 读取
break;
case STATE_READONLY:
printf("Accessing data (read-only state):\n");
printf(" First byte: %c\n", protected_data[0]); // 读取 (成功)
// protected_data[0] = 'Y'; // 写入 (会段错误)
break;
case STATE_LOCKED:
printf("Accessing data (locked state):\n");
// 任何访问 (读或写) 都会段错误
// printf(" First byte: %c\n", protected_data[0]);
// protected_data[0] = 'Z';
printf(" Access is locked. Any access would cause Segmentation Fault.\n");
break;
default:
printf("Unknown state.\n");
}
}
// 清理资源
void cleanup() {
if (protected_data && protected_data != MAP_FAILED) {
if (munmap(protected_data, data_length) == -1) {
perror("munmap in cleanup");
}
}
}
int main() {
if (init_protected_data() == -1) {
exit(EXIT_FAILURE);
}
printf("\n--- Initial State (Read-Write) ---\n");
access_data();
printf("\n--- Changing to Read-Only ---\n");
if (set_readonly() == -1) {
cleanup();
exit(EXIT_FAILURE);
}
access_data();
printf("\n--- Changing to Locked (PROT_NONE) ---\n");
if (lock_data() == -1) {
cleanup();
exit(EXIT_FAILURE);
}
access_data();
printf("\n--- Unlocking back to Read-Write ---\n");
if (set_readwrite() == -1) {
cleanup();
exit(EXIT_FAILURE);
}
access_data();
cleanup();
printf("\nAll operations completed and memory cleaned up.\n");
return 0;
}
代码解释:
- 定义了几个状态常量和全局变量来跟踪内存区域的状态。
init_protected_data
: 创建一个可读写的匿名映射,并初始化一些数据。lock_data
: 使用mprotect(..., PROT_NONE)
完全锁住内存区域。set_readonly
: 使用mprotect(..., PROT_READ)
设置为只读。set_readwrite
: 使用mprotect(..., PROT_READ | PROT_WRITE)
恢复为可读写。access_data
: 根据current_state
尝试访问内存。在STATE_LOCKED
和STATE_READONLY
下,写入操作被注释掉以防止崩溃,但代码说明了会发生什么。main
函数演示了在不同状态之间切换并尝试访问数据。- 最后调用
cleanup
释放内存。
这个例子虽然简化了,但展示了 mprotect
如何作为一种底层机制来实现更高级别的程序逻辑控制。
理解 mprotect
的关键是记住它操作的是内存页,地址需要页对齐,并且要仔细处理其返回值以确保权限修改成功。它是一个强大的工具,常用于需要精细控制内存访问权限的场景。