在嵌入式开发中,最令人头疼的莫过于内存踩踏问题。它们像幽灵一样时隐时现,让系统在测试时正常运行,却在客户现场莫名崩溃。
这次我们来介绍一个高效定位内存踩踏问题的利器——mprotect。
mprotect简介
mprotect是Linux/Unix系统调用,通过操纵MMU(内存管理单元)的页表权限,实现对指定内存区域的保护。当发生非法访问时,系统立即抛出SIGSEGV信号,让我们能够精确定位问题发生的第一现场。
mprotect()函数的原型如下:
#include <unistd.h>
#include <sys/mmap.h>
int mprotect(const void *start, size_t len, int prot);
mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。
prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:
PROT_READ:表示内存段内的内容可读;
PROT_WRITE:表示内存段内的内容可写;
PROT_EXEC:表示内存段中的内容可执行;
PROT_NONE:表示内存段中的内容根本没法访问。
与valgrind等工具相比,mprotect有三大优势:
实时性:在问题发生瞬间捕获,无需事后分析
低开销:只在检测时开启,不影响正常业务性能
精准防护:可针对关键内存区域进行重点保护
mprotect的局限性:
- 保护粒度较粗(页级别)频繁切换有性能开销
mprotect核心原理
mprotect本质上是Linux内核提供的内存页保护机制。它的工作原理可以用一个简单的比喻来理解:
想象你的内存是一栋大楼,mprotect就是给每个房间安装不同级别的门禁系统。当有人试图违规进入时,门禁立即报警并通知保安(信号处理函数)。
权限检查流程:
实战代码解析
让我们深入分析一个完整的mprotect内存保护实现:
1. 信号处理机制
说明:
siginfo_t结构体的si_addr字段精确记录了违规地址信号处理函数必须是异步安全的,避免使用malloc等函数
2. 保护页设计
内存布局图:
说明:
三明治结构:用两个不可访问的保护页包围数据区
页面对齐:所有操作都基于4KB页面边界,这是MMU的最小管理单位
精确捕获:任何越界访问都会立即触发SIGSEGV
3. 动态权限控制
说明:
页面地址对齐计算 addr & ~(PAGE_SIZE - 1) 是一个位运算技巧,相当于 (addr / PAGE_SIZE) * PAGE_SIZE,但效率更高。
4. 越界写入测试
5. 安全写入测试
本文完整代码例子可在嵌入式大杂烩公众号聊天界面输入关键字获取:mprotect_demo
总结
mprotect不是万能的,它有明显的局限性——保护粒度较粗(页级别)、频繁切换有性能开销。但在定位诡异的内存踩踏问题时,它提供的实时精确打击能力是其他工具难以替代的。
另外,我们之前也有分享过类似的内存检测的思路:
我们在申请内存时,在申请内存的前后增加两块标识区(红区),里面写入固定数据。申请、释放内存的时候去检测这两块标识区有没有被破坏(检测操作堆内存时是否踩到高压红区)。
感兴趣的小伙伴可阅读:谈谈嵌入式C语言踩内存问题!
1254