好的,我们继续学习 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;
}

代码解释:

  1. 获取系统页大小。
  2. 使用 mmap 创建一个大小为一页(4KB)的可读写 (PROT_READ | PROT_WRITE) 私有匿名 (MAP_PRIVATE | MAP_ANONYMOUS) 映射。
  3. 向映射区域写入数据,这会成功,因为初始权限是可写的。
  4. 调用 mprotect(mapped_memory, length, PROT_READ) 将整个映射区域的权限修改为只读
  5. 检查 mprotect 的返回值,失败则处理错误。
  6. 尝试读取数据(成功)。
  7. 注释说明: 尝试再次写入会导致段错误(Segmentation fault),因为内存现在是只读的。示例中为了避免崩溃,没有执行写入操作。
  8. 最后使用 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;
}

代码解释:

  1. 定义了几个状态常量和全局变量来跟踪内存区域的状态。
  2. init_protected_data: 创建一个可读写的匿名映射,并初始化一些数据。
  3. lock_data: 使用 mprotect(..., PROT_NONE) 完全锁住内存区域。
  4. set_readonly: 使用 mprotect(..., PROT_READ) 设置为只读。
  5. set_readwrite: 使用 mprotect(..., PROT_READ | PROT_WRITE) 恢复为可读写。
  6. access_data: 根据 current_state 尝试访问内存。在 STATE_LOCKEDSTATE_READONLY 下,写入操作被注释掉以防止崩溃,但代码说明了会发生什么。
  7. main 函数演示了在不同状态之间切换并尝试访问数据。
  8. 最后调用 cleanup 释放内存。

这个例子虽然简化了,但展示了 mprotect 如何作为一种底层机制来实现更高级别的程序逻辑控制。

理解 mprotect 的关键是记住它操作的是内存页,地址需要页对齐,并且要仔细处理其返回值以确保权限修改成功。它是一个强大的工具,常用于需要精细控制内存访问权限的场景。