如何在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 } 

这解决了我的直接问题,测试用程序“工作”(又名崩溃)与gccclang

我猜因为解除引用空指针是未定义的行为, 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; }