Linux用户态下如何检测C函数对共享内存的重复读取?
解决方案:检测函数对同一内存地址的重复读取
针对你的需求,在Linux用户态下无需修改库代码,可通过以下几种可靠方案实现运行时检测:
方案一:内存保护+信号处理(轻量无依赖)
利用Linux内存权限控制结合SIGSEGV信号处理,拦截重复读取操作:
实现步骤
初始化监控环境
- 将目标共享内存区域的权限临时改为
PROT_NONE(不可读不可写)。 - 分配一块与监控区域大小一致的影子位图,用于记录每个内存地址的访问状态(已读/未读)。
- 将目标共享内存区域的权限临时改为
注册信号处理逻辑
当库函数尝试读取PROT_NONE区域时会触发SIGSEGV信号,在信号处理函数中:- 通过
siginfo_t获取触发错误的内存地址。 - 校验地址是否在监控范围内:
- 若未被访问:临时将地址所在页设为
PROT_READ,允许本次读取;读取完成后改回PROT_NONE,并标记该地址为已读。 - 若已被访问:直接触发运行时错误(如打印日志后调用
abort())。
- 若未被访问:临时将地址所在页设为
- 通过
核心代码片段
#include <signal.h> #include <sys/mman.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #define PAGE_SIZE 4096 static void* monitored_start; static size_t monitored_size; static unsigned char* access_bitmap; static void segv_handler(int sig, siginfo_t* info, void* ctx) { void* addr = info->si_addr; if (addr >= monitored_start && addr < (char*)monitored_start + monitored_size) { size_t offset = (size_t)(addr - monitored_start); if (access_bitmap[offset]) { fprintf(stderr, "ERROR: Repeated read at address %p\n", addr); abort(); } access_bitmap[offset] = 1; // 临时设置页为可读 void* page_addr = (void*)((size_t)addr & ~(PAGE_SIZE - 1)); mprotect(page_addr, PAGE_SIZE, PROT_READ); // 注:实际场景需确保读取完成后恢复权限,可结合线程同步或钩子实现 } else { // 非监控区域的段错误,按默认逻辑处理 signal(sig, SIG_DFL); raise(sig); } } void BEGIN_READ_ONCE(void* input, size_t size) { monitored_start = input; monitored_size = size; access_bitmap = calloc(size, 1); mprotect(input, size, PROT_NONE); struct sigaction sa = {0}; sa.sa_sigaction = segv_handler; sa.sa_flags = SA_SIGINFO; sigaction(SIGSEGV, &sa, NULL); } void END_READ_ONCE(void* input, size_t size) { mprotect(input, size, PROT_READ | PROT_WRITE); // 恢复原内存权限 free(access_bitmap); signal(SIGSEGV, SIG_DFL); }
优缺点
- ✅ 无额外依赖,轻量高效,适合CI环境
- ❌ 需处理页对齐问题,恢复权限的时机需精准控制
方案二:Intel Pin动态插桩(精准无遗漏)
Intel Pin是用户态动态插桩工具,可拦截所有内存读取指令,精准统计地址访问次数:
实现步骤
编写Pin工具
- 拦截所有内存读取指令,注册回调函数。
- 用哈希表记录监控范围内每个地址的读取计数。
- 每次读取时校验计数:若≥1则触发错误,否则计数+1。
运行目标程序
通过Pin加载工具启动程序:pin -t your_tool.so -- ./your_program
核心逻辑示例(C++)
#include "pin.H" #include <unordered_map> #include <iostream> static void* g_monitor_start; static size_t g_monitor_size; static std::unordered_map<void*, int> g_access_count; VOID OnMemRead(VOID* addr) { if (addr >= g_monitor_start && addr < (char*)g_monitor_start + g_monitor_size) { if (g_access_count[addr] >= 1) { std::cerr << "ERROR: Repeated read at address " << addr << std::endl; abort(); } g_access_count[addr]++; } } VOID Instruction(INS ins, VOID* v) { if (INS_IsMemoryRead(ins)) { INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)OnMemRead, IARG_MEMORYREAD_EA, IARG_END); } } INT32 Usage() { std::cerr << "Usage: pin -t read_once_check.so -start <addr> -size <size> -- ./program" << std::endl; return -1; } int main(int argc, char* argv[]) { if (PIN_Init(argc, argv)) return Usage(); // 从命令行参数获取监控区域的起始地址和大小 g_monitor_start = (void*)strtoul(PIN_GetCommandLineArgument("-start", NULL), NULL, 16); g_monitor_size = strtoul(PIN_GetCommandLineArgument("-size", NULL), NULL, 10); INS_AddInstrumentFunction(Instruction, NULL); PIN_StartProgram(); return 0; }
优缺点
- ✅ 精准拦截所有内存读取,无需处理内存权限问题
- ✅ 支持预编译库,无需修改目标代码
- ❌ 需要提前安装Intel Pin,CI环境需配置依赖
方案三:ptrace跟踪(无额外工具但性能低)
利用Linuxptrace系统调用编写跟踪程序,逐指令监控目标进程的内存读取操作:
实现步骤
- 编写tracer程序
- 用
ptrace(PTRACE_ATTACH)附加到目标进程。 - 逐单步执行指令(
PTRACE_SINGLESTEP),解析每条指令是否为内存读取操作。 - 提取目标地址,统计访问次数,重复读取则触发错误。
- 用
优缺点
- ✅ 无额外工具依赖,纯用户态实现
- ❌ 性能极低,仅适合小范围短时间测试
- ❌ 需处理不同架构的指令解析,实现复杂度高
方案选择建议
- 追求轻量高效:优先选方案一,适配大多数测试场景
- 需要精准无遗漏检测:优先选方案二,适合复杂库函数测试
- 无法安装额外工具:可尝试方案三,仅适合简单场景
内容的提问来源于stack exchange,提问作者Gilles 'SO- stop being evil'
相关产品推荐
相关产品推荐

