Linux下运行时检测C/C++指针指向只读内存的适配方案探讨
Linux下只读内存指针检测的问题与适配
传统检测方式的局限性
早期Linux环境中,C/C++底层代码通过以下方式判断指针ptr是否指向只读内存:
extern char etext, edata; ... (ptr > &etext) && (ptr < &edata);
这类代码目前仍有实际应用,但传统的连续内存布局text -> data -> bss -> heap -> ... -> stack -> kernel已无法保证,且现代内存布局的权威资料稀缺,难以判断传统代码的适用性及适配方案。
另有静态内存检测方案:
(void*)(x) <= (void*)&end || (void*)(x) <= (void*)&edata
该方案假设堆始终位于BSS和只读数据之后,由此引出两个技术疑问:
技术疑问
- 该堆位置假设在现代Linux系统中是否安全?
- 若假设成立,结合text段居前特性,如何调整逻辑以准确检测指针是否指向排除BSS的只读内存?
尝试性检测代码
本人编写的检测代码在Ubuntu 22.04传统布局环境下测试有效,但不确定非传统布局场景的适用性:
#include <iostream> extern char etext, edata; // 检测指针是否在栈中 void *stack_bottom; bool __attribute__((noinline)) inStack(void *x) { void *stack_top = &stack_top; return x <= stack_bottom && x >= stack_top; } // 检测指针是否在排除BSS的只读内存中 bool roMem(void* c) { if(&etext < &edata && &end > &edata) { return (c > &etext) && (c < &edata) && (!inStack((void*)c)); } else if(&etext > &edata && &end > &edata) { return (void*)(c) <= (void*)&edata && !inStack((void*)c); } else if(&end < &edata) { return (void*)(c) > (void*)&end && (void*)(c) <= (void*)&edata && !inStack((void*)c); } } class Test { public: const char* d; Test(const char* c) { d = c; } }; static const char* str5 = "short"; int main() { const char* str1 = "short"; char* str2 = const_cast<char*>("short"); const char str3[6] = {'s', 'h', 'o', 'r', 't', 0}; const char* str4 = "longgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg"; std::cout << roMem((void*)str1) << std::endl; // 输出1 std::cout << roMem((void*)(const char*)str2) << std::endl; // 输出1 std::cout << roMem((void*)str3) << std::endl; // 输出0 std::cout << roMem((void*)str4) << std::endl; // 输出1 std::cout << roMem((void*)str5) << std::endl; // 输出1 std::cout << roMem((void*)"short") << std::endl; // 输出1 std::cout << roMem((void*)(const char*)2) << std::endl; // 输出0 Test x((const char*)3); std::cout << roMem((void*)x.d) << std::endl; // 输出0 }
问题解答
1. 堆位置假设的安全性
该假设在现代Linux系统中完全不安全,核心原因如下:
- ASLR(地址空间布局随机化)默认开启,堆、栈、共享库的加载地址会随机偏移,传统连续布局被彻底打破,堆可能出现在任意地址区间。
- 即使关闭ASLR,部分内核配置、架构或嵌入式场景下,堆可能与其他内存区域交错;进程还可通过
mmap创建多个匿名内存区域(类堆区域),位置无规律。 - 容器、虚拟化环境的内存布局可能进一步偏离传统模式。
2. 排除BSS的只读内存检测逻辑调整
若仅针对关闭ASLR的传统连续布局场景(堆在BSS之后),可基于标准符号调整逻辑,核心是区分只读段(text、rodata)与可写段(data、bss):
- 明确各段的标准边界(基于GCC默认符号):
.text(代码段,只读):从程序入口地址&_start到&etext.rodata(只读数据段,如字符串常量、const静态变量):从&etext到.data段起始地址(无公开默认符号,需间接判断或自定义链接脚本导出).data(可写初始化全局变量):从.data起始到&edata.bss(可写未初始化全局/静态变量):从&edata到&end
- 调整后的检测逻辑:
- 先排除栈中的指针,避免栈地址与其他段重叠导致误判
- 直接检测指针是否在
.text区间 - 若需检测
.rodata,最可靠的方式是通过自定义链接脚本导出其边界符号,示例如下:// 自定义链接脚本中添加: // PROVIDE(rodata_start = .); // .rodata : { *(.rodata) *(.rodata.*) } // PROVIDE(rodata_end = .); // 代码中引用符号 extern char _start, etext, rodata_start, rodata_end; bool roMem(void* c) { if (inStack(c)) return false; // 检测text段 if (c >= &_start && c < &etext) return true; // 检测rodata段 if (c >= &rodata_start && c < &rodata_end) return true; return false; }
注:上述逻辑仅适用于传统连续布局场景,现代系统中更可靠的方式是通过查询内存页权限(如解析/proc/self/maps或使用内存管理系统调用)实现检测,而非依赖段地址范围假设。
内容的提问来源于stack exchange,提问作者S08
相关产品推荐
相关产品推荐

