如何在 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 真的是一种在大多数平台上崩溃的相当便携的方式吗?

如果我是你,我不会依赖这种方法是健壮的。

不能用abort(),这是C标准的一部分,保证会导致程序异常终止事件吗?

提到abort()的答案很棒,我真的没有想到这一点,它确实是一种强制异常程序终止的完美可移植方式。

用我的代码尝试它,我遇到了msvcrt(Microsoft的 C 运行时)以一种特殊的聊天方式实现abort(),它输出以下内容给stderr

此应用程序已请求运行时以异常方式终止它。 请联系应用程序的支持团队以获取更多信息。

这不太好,至少它不必要地弄乱了完整测试运行的输出。所以我看了一下clang警告中也引用__builtin_trap()。事实证明,这给了我我一直在寻找的东西:

LLVM 代码生成器将 __builtin_trap() 转换为陷阱指令(如果目标 ISA 支持)。否则,内置将转换为中止调用。

从版本 4.2.4 开始,它也在gcc中可用:

此功能会导致程序异常退出。GCC 通过使用目标相关机制(例如有意执行非法指令)或调用 abort 来实现此功能。

由于这类似于真正的崩溃,因此我更喜欢它而不是简单的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 <stdio.h>
#  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上测试过它,但我不想以任何一种方式押注结果。 我确实知道,如果一个C编译器可用于我切齿的8051系统,该程序可以在这些系统上运行良好。

下面的程序应该可以工作。但是,它可能会造成一些附带损害。


#include <string.h>
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;
}

最新更新