使用mprotect定位踩内存故障

本文介绍了一种利用mprotect函数定位内存被踩问题的方法。通过在易被踩的内存前添加一段设置为只读的替死鬼内存,当程序尝试非法写入时触发保护机制,从而定位到问题源头。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

对于 C 语言来说,内存被踩是比较常见的问题,轻则普通变量被改写程序逻辑出错,重则指针变量被改写引发指针解引用出现未定义行为风险;

定位内存被踩一直是棘手的难题,如果出现程序跑死,一般可以通过堆栈信息来定位:
1)查看跑死的调用链,确定跑死代码的位置;
2)根据pc指针找到具体代码;
3)走查代码分析问题;

但是这种方法有个先天的劣势:程序跑死的点和内存被踩的点往往不在同一个地方,需要分析代码寻找真正的问题点。如果程序只是逻辑出错没有跑死,定位起来会更加困难。

有没有方法可以让程序告诉我们是谁踩了内存呢?

这里分享一种借助 mprotect 函数定位内存被踩的方法。

1.mprotect介绍

mprotect 是 linux 系统中用于修改一段指定内存区域保护属性的函数,其原型是:

#include <unistd.h>
#include <sys/mman.h>

int mprotect(const void* start, size_t len, int prot)

其中 start 是被保护内存的起始地址,len 是被保护内存的长度,prot 是内存的保护属性,常见的属性有:

保护属性说明
PROT_READ内存可读
PROT_WRITE内存可写
PROT_EXEC内存可执行
PROT_NONE内存不可访问

需要注意的是,mprotect 函数在使用上有限制:

  • start 指向的内存地址要求是一个内存页的首地址;
  • len 需要是内存页的整数倍;

关于内存页的这里不多做介绍,有兴趣的可以看看其他博文的介绍,需要知道的是一般内存页是按 4096 字节(4KB)为单位对齐的。

2.举个栗子

下面以一个实际的例子来说明 mprotect 的使用方法。
定义以下结构体、变量和函数:

#define  MAX_ARRAY_SIZE (4096)

typedef struct SubInst
{
	unsigned char flag;
}SubInst;

typedef struct Inst
{
	unsigned char array[MAX_ARRAY_SIZE];
	SubInst*      subInst;
}Inst;

Inst* gInst = NULL;

void CreateInst()
{
	// 假设 malloc 不会失败,假设 gInst 和 gInst->subInst 不会为 NULL;
	gInst = (Inst*)malloc(sizeof(Inst));
	gInst->subInst = (SubInst*)malloc(sizeof(SubInst)); 
}

void DoSomething()
{
	unsigned char* ptr1 = (unsigned char*)gInst;
	unsigned int*  ptr2 = (unsigned int*)(ptr1 + MAX_ARRAY_SIZE);
	*(ptr2) = 0;
}

void PrintInst()
{
	printf("[Inst] flag : %u\n", gInst->subInst->flag);
}

int main()
{
	CreateInst();
	DoSomething();
	PrintInst();
	return 0;
}

很容易就可以看出在 DoSomething() 函数中由于指针偏移错误,改写了指针 subInst 的值为 0, 所以在 PrintInst() 中打印时出现空指针访问,引起程序跑死。

根据调用链可以得到以下段错误信息:
在这里插入图片描述
显然,根据 coredump 信息只能看到程序跑死在 subInst 解引用时出现问题。

如果提示缺少 glibc 但安装不上,需要修改一下 etc/yum.repos.d/CentOS-Linux-Debuginfo.repo 中enabled 的值为1。

coredump 信息缺失的话请检查 ulimit -c,可以修改 etc/profile,添加 ulimit -S -c 0 > /dev/null 2>&1,记得 source etc/profile;

3.mprotect使用方法

下面来看看 mprotect 是如何帮助我们找到问题点的。

首先改写代码如下

#define  MAX_ARRAY_SIZE (4096)

typedef struct SubInst
{
	unsigned char flag;
}SubInst;

typedef struct Inst
{
	unsigned char array[MAX_ARRAY_SIZE];
	unsigned char pzone[MAX_ARRAY_SIZE];
	SubInst*      subInst;
}Inst;

Inst* gInst = NULL;

void CreateInst()
{
	// 假设 posix_memalign, malloc, mprotect 不会失败
	// 假设 gInst 和 gInst->subInst 不会为 NULL;
	size_t pagesize = sysconf(_SC_PAGESIZE);
	posix_memalign((void**)gInst, pagesize, sizeof(Inst));
	gInst->subInst = (SubInst*)malloc(sizeof(SubInst)); 
	
	mprotect(gInst->pzone, pagesize, PROT_READ);
}

void DoSomething()
{
	unsigned char* ptr1 = (unsigned char*)gInst;
	unsigned int*  ptr2 = (unsigned int*)(ptr + MAX_ARRAY_SIZE);
	*(ptr2) = 0;
}

void PrintInst()
{
	printf("[Inst] flag : %u\n", gInst->subInst->flag);
}

int main()
{
	CreateInst();
	DoSomething();
	PrintInst();
	return 0;
}

解释一下几个关键点

sysconf(_SC_PAGESIZE) 返回当前操作系统的内存页大小,一般是 4096 字节;

posix_memalign 函数申请内存,它与 malloc 的区别是会将申请的内存按要求的长度对齐并且返回的内存地址是一个内存页的首地址,函数原型:

#include <stdlib.h>
int posix_memalign(void** memptr, size_t alignment, size_t size);

其中
memptr 是个2级指针,指向存放申请内存地址的指针变量的指针;
alignment 是期望对齐的内存长度;
size 是申请的内存大小。

前面说过,mprotect 要求被保护的内存是完整的内存页且 4KB 对齐,所以我们在被踩的内存 subInst 指针前加入了一段 4KB 大小的内存 pzone,并且使用 mprotect 将这段内存设置为 只读

typedef struct Inst
{
	unsigned char array[MAX_ARRAY_SIZE];
	unsigned char pzone[MAX_ARRAY_SIZE];
	SubInst*      subInst;
}Inst;

再次执行上面的程序,这次程序很直接的就告诉了我们内存被踩的案发现场。
在这里插入图片描述

4.总结

上面结合例子分享了一种使用 mprotect 定位被踩的方法,例子举的比较简单,所以在一些更为复杂的代码中效果会更明显,核心思想是:

  • 在被踩的内存前添加一段“替死鬼”内存,并在上面设置“陷阱”揪出踩内存的罪魁祸首:被保护的内存是只读属性,发生内存被写则中断操作。

当然这种方法也有它的局限性:
1)对内存的分配更为严格,对于非动态申请的内存存在修改代码上的困难;
2)占用更多的内存;

根据不同情况选择合适的定位方法才是我们需要掌握的技巧,有方法总比没有方法好:D

<think>好的,用户遇到了页面故障执行错误的问题,需要了解其原因和解决方法。首先,我需要回忆操作系统相关的知识,特别是内存管理和页面错误的部分。 页面故障(Page Fault)通常发生在进程访问的虚拟地址没有映射到物理内存时,这时候操作系统需要处理这个异常。但执行错误(Execute Error)可能涉及到权限问题,比如尝试执行不可执行的页面。 可能的原因包括: 1. 访问了未映射的地址,导致缺页错误。但如果是执行错误,可能页面存在但权限不足,比如代码段被标记为不可执行。 2. 内存权限设置错误,例如数据段或堆栈段被错误地标记为可执行,而操作系统启用了NX位(不可执行位)保护。 3. 内存损坏,比如缓冲区溢出覆盖了页表项,导致权限位被修改。 4. 驱动程序或内核模块错误地修改了内存映射,导致权限冲突。 5. 硬件故障,比如内存条损坏,但这种情况比较少见。 解决方法方面: 首先检查应用程序是否有内存访问越界或执行非法操作,使用调试工具如GDB分析崩溃点。如果是权限问题,需要检查页表项中的权限位是否正确设置。对于启用了NX位的系统,确保代码段可执行而数据段不可执行。如果是驱动程序问题,更新或回滚驱动版本。硬件问题则需要运行内存检测工具。 另外,用户提供的引用中有提到延迟失效漏洞和集群节点固定命令,可能和分布式系统中的内存一致性或缓存失效有关,但不确定是否直接相关。需要确认页面错误是否发生在集群环境下,可能涉及节点间内存同步问题,此时按照引用[2]的方法固定节点可能有助于稳定内存状态。 最后,生成相关问题时要覆盖原因、排查工具、系统保护机制、硬件问题以及集群环境下的处理。</think>### 页面故障执行错误的原因及解决方法 #### 一、核心原因分析 1. **权限冲突** 进程尝试执行没有执行权限的内存区域(如数据段或堆栈段),触发硬件保护机制。现代处理器支持$NX$位(No-eXecute)技术,操作系统通过$$PAGE_EXECUTE_READWRITE$$等标志控制内存区域权限[^1]。 2. **无效地址访问** 访问未映射到物理内存的虚拟地址(如空指针解引用),或访问已被释放的内存区域,引发缺页异常(Page Fault)。此时若错误类型为$0xC0000005$(ACCESS_VIOLATION),通常伴随STATUS_IN_PAGE_ERROR[^2]。 3. **内存映射异常** 驱动程序或内核模块错误修改页表项(PTE)导致权限位异常,例如将代码页误标记为不可执行。此类问题常见于$$mmap()$$系统调用参数配置错误。 4. **硬件故障** 内存条损坏或总线错误导致物理地址读写异常,但此类情况仅占故障总量的$<5\%$。 #### 二、排查与解决方法 1. **调试工具定位** 使用GDB/WinDbg分析崩溃堆栈: ```bash gdb -ex "bt full" --args ./your_program ``` 重点关注$$si/eip$$寄存器指向的故障地址权限属性。 2. **检查内存权限配置** 对于Linux系统,通过$$/proc/[pid]/maps$$查看进程内存映射: ```bash cat /proc/$(pidof your_program)/maps | grep -i [故障地址所在区间] ``` 验证目标地址区间是否具有$x$(可执行)权限。 3. **系统级保护设置** 在集群环境中,若出现跨节点内存同步问题,可参考引用[2]的节点固定方法: ```bash crsctl pin css -n <node1> <node2> ``` 4. **代码层面修复** - 使用AddressSanitizer检测内存越界访问 ```bash gcc -fsanitize=address -g your_code.c ``` - 对动态生成代码(如JIT编译)需显式设置可执行权限: ```c mprotect(code_ptr, size, PROT_READ | PROT_EXEC); ``` #### 三、预防措施 1. 开启DEP(Data Execution Prevention)保护机制 2. 定期使用memtest86+检测物理内存完整性 3. 在分布式系统中遵循引用[1]的延迟失效处理规范,确保缓存一致性
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值