如何在C中强制崩溃,取消引用空指针(相当)可移植的方式?
我正在为我当前的项目编写自己的测试运行器。 一个特性(对于测试运行者来说可能很常见)是每个测试用例都在子进程中执行,因此测试运行器可以正确检测并报告崩溃的测试用例。
我还要测试测试运行器本身,因此一个测试用例必须强制崩溃。 我知道“崩溃”不是C标准所涵盖的,只是可能由于未定义的行为而发生。 所以这个问题更多的是关于实际实现的行为。
我的第一次尝试是取消引用空指针 :
int c = *((int *)0);
这在GNU / Linux和Windows上的调试版本中有效,但在发布版本中未能崩溃,因为未使用的变量c
已经过优化,所以我添加了
printf("%d", c); // to prevent optimizing away the crash
我以为我已经安顿下来了。 但是,使用clang
而不是gcc
尝试我的代码会在编译期间发现一个惊喜:
[CC] obj/x86_64-pc-linux-gnu/release/src/test/test/test_s.o src/test/test/test.c:34:13: warning: indirection of non-volatile null pointer will be deleted, not trap [-Wnull-dereference] int c = *((int *)0); ^~~~~~~~~~~ src/test/test/test.c:34:13: note: consider using __builtin_trap() or qualifying pointer with 'volatile' 1 warning generated.
事实上, clang
编译的测试用例没有崩溃。
所以,我遵循警告的建议,现在我的测试用例看起来像这样:
PT_TESTMETHOD(test_expected_crash) { PT_Test_expectCrash(); // crash intentionally int *volatile nptr = 0; int c = *nptr; printf("%d", c); // to prevent optimizing away the crash }
这解决了我的直接问题,测试用程序“工作”(又名崩溃)与gcc
和clang
。
我猜因为解除引用空指针是未定义的行为, clang
可以自由地将我的第一个代码编译成不会崩溃的东西。 volatile
限定符删除了在编译时确定这实际上将取消引用null的能力。
现在我的问题是:
- 这个最终代码是否保证在运行时实际发生null解除引用?
- 取消引用null确实是一种在大多数平台上崩溃的相当便携的方式吗?
如果我是你,我不会依赖那种方法。
你不能使用abort()
,这是C标准的一部分,并保证导致exception的程序终止事件?
回答abort()
的答案很棒,我真的没有想到这一点,它确实是一种强制程序终止exception的便携式方式。
尝试使用我的代码,我遇到msvcrt
(Microsoft的C运行时abort()
以一种特殊的聊天方式实现abort()
,它将以下内容输出到stderr
:
此应用程序已请求Runtime以不寻常的方式终止它。 请联系应用程序的支持团队以获取更多信息。
这不太好,至少它不必要地混乱了完整测试运行的输出。 所以我看了一下__builtin_trap()
,这也是在clang
的警告中引用的。 事实certificate这给了我正是我想要的东西:
如果目标ISA支持,LLVM代码生成器会将__builtin_trap()转换为陷阱指令。 否则,内置转换为中止调用。
从版本4.2.4开始,它也可以在gcc
使用:
此function导致程序exception退出。 GCC通过使用依赖于目标的机制(例如故意执行非法指令)或通过调用abort来实现此function。
由于这类似于真正的崩溃,我更喜欢它而不是简单的abort()
。 对于后备,它仍然是一个尝试执行自己的非法操作的选项,如空指针取消引用,但只是添加一个调用abort()
以防程序以某种方式使它在那里没有崩溃。
总而言之,解决方案看起来像这样,测试最小的GCC版本并使用clang提供的更方便的__has_builtin()
宏:
#undef HAVE_BUILTIN_TRAP #ifdef __GNUC__ # define GCC_VERSION (__GNUC__ * 10000 \ + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) # if GCC_VERSION > 40203 # define HAVE_BUILTIN_TRAP # endif #else # ifdef __has_builtin # if __has_builtin(__builtin_trap) # define HAVE_BUILTIN_TRAP # endif # endif #endif #ifdef HAVE_BUILTIN_TRAP # define crashMe() __builtin_trap() #else # include # define crashMe() do { \ int *volatile iptr = 0; \ int i = *iptr; \ printf("%d", i); \ abort(); } while (0) #endif // [...] PT_TESTMETHOD(test_expected_crash) { PT_Test_expectCrash(); // crash intentionally crashMe(); }
你可以写内存而不是阅读它。
*((int *)0) = 0;
不,取消引用NULL指针不是崩溃程序的可移植方式。 这是未定义的行为,这意味着,你无法保证会发生什么。
实际上,在大多数情况下,在台式计算机上使用的三个主要操作系统中的任何一个,即MacOS,Linux和Windows NT(*)取消引用NULL指针将立即崩溃您的程序。
这就是说:“未定义行为的最坏结果是让它做你期望的事情。”
我故意在Windows NT旁边放一颗星,因为在Windows 95/98 / ME下,我可以制作一个具有以下来源的程序:
int main() { int *pointer = NULL; int i = *pointer; return 0; }
这将运行而不会崩溃。 将它编译为16位DOS下的TINY模式.COM文件,你就可以了。
同上,在CP / M下使用几乎任何C编译器运行相同的源代码。
同上在某些嵌入式系统上运行它。 我没有在Arduino上测试它,但我不想在结果上打赌。 我确实知道这是一个可用于8051系统的C编译器我开始关注,该程序可以正常运行。
下面的程序应该有效。 但是,它可能会造成一些附带损害。
#include void crashme( char *str) { char *omg; for(omg=strtok(str, "" ); omg ; omg=strtok(NULL, "") ) { strcat(omg , "wtf"); } *omg =0; // always NUL-terminate a NULL string !!! } int main(void) { char buff[20]; // crashme( "WTF" ); // works! // crashme( NULL ); // works, too crashme( buff ); // Maybe a bit too slow ... return 0; }