是否有可能在C语言中预先检查任意内存的取消引用是否会导致程序崩溃?
是否有可能在C语言中预先检查任意内存的取消引用是否会导致程序崩溃?
我想编写一个交互式解释型C shell,它允许您访问任意内存并对该内存地址执行命令。
例如(运行程序shell):
prompt> 10 bytes starting 0x400000
这个指令将尝试访问地址0x400000并显示从那里开始的10个字节,例如范围[0x400000,0x400009]。
并且会产生以下输出:
{0x00, 0x01, 0x02, 0x03, 0x04, , , 0x07, 0x08, 0x09, 0x0a}
其中“bad”表示尝试访问“非法”的内存。
我想知道是否有一种C标准方式来检查程序是否被允许访问我正在尝试访问的内存,或者访问这个内存是否会导致程序崩溃(在它真正崩溃之前),并向用户报告程序无法访问该内存的信息。
我之所以问这个问题,是因为大多数关于这个主题的问题往往被回答为“你无法确定检查指针是否有效”,但是我确信必须有一种方法来检查指针是否至少是“绝对无效且会崩溃”或“可能无效但不会崩溃”,遗憾的是我找不到答案。
提前谢谢。
我不认为可以仅使用标准的C语言来完成这个任务。
但是,你可以使用一些邪恶的平台特定技巧来获取你的内存映射的情况。在Linux中,文件/proc/(pid)/maps
将列出进程pid
的内存映射情况,包括读/写权限状态。这是我机器上一个简单的cat
进程的情况:
00400000-0040c000 r-xp 00000000 00:13 1237228 /usr/bin/cat 0060b000-0060c000 r--p 0000b000 00:13 1237228 /usr/bin/cat 0060c000-0060d000 rw-p 0000c000 00:13 1237228 /usr/bin/cat 01864000-01885000 rw-p 00000000 00:00 0 [heap] 7fe7a5e0b000-7fe7a6121000 r--p 00000000 00:13 1487092 /usr/lib/locale/locale-archive 7fe7a6123000-7fe7a62ba000 r-xp 00000000 00:13 1486770 /usr/lib/libc-2.23.so 7fe7a62ba000-7fe7a64ba000 ---p 00197000 00:13 1486770 /usr/lib/libc-2.23.so 7fe7a64ba000-7fe7a64be000 r--p 00197000 00:13 1486770 /usr/lib/libc-2.23.so 7fe7a64be000-7fe7a64c0000 rw-p 0019b000 00:13 1486770 /usr/lib/libc-2.23.so 7fe7a64c0000-7fe7a64c4000 rw-p 00000000 00:00 0 7fe7a64cb000-7fe7a64ee000 r-xp 00000000 00:13 1486769 /usr/lib/ld-2.23.so 7fe7a66cc000-7fe7a66ee000 rw-p 00000000 00:00 0 7fe7a66ee000-7fe7a66ef000 r--p 00023000 00:13 1486769 /usr/lib/ld-2.23.so 7fe7a66ef000-7fe7a66f0000 rw-p 00024000 00:13 1486769 /usr/lib/ld-2.23.so 7fe7a66f0000-7fe7a66f1000 rw-p 00000000 00:00 0 7fe7a66f5000-7fe7a66f8000 rw-p 00000000 00:00 0 7ffe398e8000-7ffe39909000 rw-p 00000000 00:00 0 [stack] 7ffe3999b000-7ffe3999e000 r--p 00000000 00:00 0 [vvar] 7ffe3999e000-7ffe399a0000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
因此,你可以看到程序本身的映像图被映射到虚拟内存的开头附近,堆位于稍微更高的位置,栈被映射到7ffe398e8000-7ffe39909000
,C库和动态链接器也被加载到内存中。
请注意,每个文件都被映射了多次。例如,/usr/bin/cat既有只读、可执行和读写段。这是为了防止进程将数据写入const
内存并执行数据。
从映射表中,你可以对内存的布局以及这些内存部分可能进行的操作有一个相对准确的了解。
这是一个好主意吗?不。
除非你正在编写调试器或类似的开发工具,大多数情况下不是好主意。
顺便一提,你正在考虑编写的“shell”非常类似于调试器。诸如gdb之类的调试器可以完成你谈论的所有事情,包括求值C表达式和检查内存。
作为第二个附言,因为我觉得这非常有趣,这里有一个小练习:
你可以看到,有一些内核内存映射到ffffffffff600000
。如果这个理论正确,即使在一般情况下我们无法访问内核的内存,我们也应该能够读取这个内存。让我们试试:
int main(void) { unsigned long *p = 0xffffffffff600000; for (;;) printf("0x%lx, ", *p++); }
我们得到:
0xf00000060c0c748,0xccccccccccccc305,0xcccccccccccccccc
...段错误
如果你想知道为什么用户空间进程可以读取这个内存,那是因为它可以加速某些系统调用,如gettimeofday,并使它们无需像其它系统调用那样必须切换到内核模式运行。例如,请参见这个问题。