深入理解Linux内存保护:mprotect函数源码解析
前言
在Linux系统编程中,内存管理是一个核心话题。今天我们来深入分析musl libc中mprotect函数的实现,看看它是如何工作的。
源码分析
#include <sys/mman.h> #include "libc.h" #include "syscall.h" int __mprotect(void *addr, size_t len, int prot) { size_t start, end; start = (size_t)addr & -PAGE_SIZE; end = (size_t)((char *)addr + len + PAGE_SIZE-1) & -PAGE_SIZE; return syscall(SYS_mprotect, start, end-start, prot); } weak_alias(__mprotect, mprotect);
核心要点解析
1️⃣ 为什么需要页对齐?
Linux内核的内存保护是以页(Page)为单位的,通常是4KB(4096字节)。
start = (size_t)addr & -PAGE_SIZE;
这行代码的作用是向下取整到页边界。
-PAGE_SIZE在补码表示下是0xFFFFF000(假设PAGE_SIZE=4096)& -PAGE_SIZE相当于保留高位,低位清零,实现页对齐
举例:
- addr = 0x1005,PAGE_SIZE = 0x1000
- start = 0x1005 & 0xFFFFF000 = 0x1000 ✅
2️⃣ 结束地址的向上取整
end = (size_t)((char *)addr + len + PAGE_SIZE-1) & -PAGE_SIZE;
这里需要向上取整到页边界,技巧是先加上PAGE_SIZE-1再对齐。
举例:
- addr = 0x1005,len = 100,PAGE_SIZE = 0x1000
- addr + len = 0x1069
- 加上 PAGE_SIZE-1 = 0x1069 + 0xFFF = 0x2068
- 对齐后 end = 0x2068 & 0xFFFFF000 = 0x2000 ✅
这样就覆盖了 [0x1000, 0x2000) 整个页范围。
3️⃣ 系统调用封装
return syscall(SYS_mprotect, start, end-start, prot);
调用真正的系统调用,参数为:
- start: 起始地址(已对齐)
- end-start: 实际保护的内存大小
- prot: 保护标志(PROT_READ/WRITE/EXEC等)
4️⃣ weak_alias 的妙用
weak_alias(__mprotect, mprotect);
这创建了一个弱符号别名,允许用户自定义mprotect函数来覆盖默认实现,常用于:
- 调试 Hook
- 安全沙箱
- 内存检测工具
实际应用场景
表格
| 场景 | 用法 | 示例 | |
|---|---|---|---|
| 🛡️ 数据保护 | 禁止写入代码段 | `mprotect(code, len, PROT_READ | PROT_EXEC)` |
| 🔒 安全加固 | 栈不可执行 | 启动时设置栈为 PROT_READ | PROT_WRITE |
| 🐛 调试 | 标记内存为只读检测溢出 | mprotect(buf, size, PROT_READ) |
完整示例
#include <sys/mman.h> #include <stdio.h> #include <string.h> int main() { char buf; // 先可写 strcpy(buf, "Hello"); printf("%s\n", buf); // 改为只读(会保护整个页) if (mprotect(buf, 4096, PROT_READ) == 0) { // buf = 'X'; // ❌ 段错误! printf("内存已保护\n"); } return 0; }
总结
表格
| 知识点 | 说明 |
|---|---|
| 页对齐 | 内核以页为单位管理权限 |
| 位运算技巧 | & -PAGE_SIZE快速对齐 |
| 向上取整 | +PAGE_SIZE-1再对齐 |
| weak_alias | 允许函数覆盖,灵活性强 |
这个看似简单的函数,体现了系统编程的精髓:理解硬件约束,用最少的代码高效解决问题!💡
参考资料:
- musl libc源码
- Linux man page:
man 2 mprotect
觉得有用就点个赞吧👍 欢迎评论区讨论!
#Linux #系统编程 #mprotect #musl #内存管理
