有什么方法可以保证段错误吗?

Is there any way to guarantee a segfault?

我知道段错误是未定义行为的常见表现形式。但是我有两个小问题:

  1. 所有段错误都是未定义的行为吗?

  2. 如果不是,有什么方法可以确保出现段错误吗?

What is a segmentation fault? is far more general than my question and none of the answers answers any of my questions.

分段错误仅表示您对内存进行了无效访问——要么是因为请求的地址未映射(映射错误),要么是因为您没有访问它的权限(访问错误)。

  1. 存在有意的分段错误。可以找到一个这样的例子 -- 一个迷你应用程序,它故意使用内存页面的权限来检测给定函数在何处进行写入。

  2. 最简单的方法是使用raise函数。

来源:

#include <signal.h>    
int main() {
    raise(SIGSEGV);
    return 0;
}

取消引用 NULL 指针永远不会出错。

int main() {
  int *a = 0;
  *a = 0;
  return 0;
}

正如评论中正确提到的那样,这不会在 100% 的时间内起作用,并且是特定于平台的。但它应该适用于大多数常见平台。

  1. Are ALL segfaults undefined behavior?

这个问题比看起来更棘手,因为 "undefined behavior" 是对 C 源程序的描述,或者是 运行 在 "abstract machine" 中使用 C 程序的结果一般描述 C 程序的行为;但是 "segmentation fault" 是特定操作系统的可能行为,通常需要特定 CPU 功能的帮助。

C 标准根本没有说明段错误。它所说的一个几乎相关的事情是,如果程序执行没有未定义的行为,那么程序的实际实现执行将具有与抽象机器执行相同的可观察行为。并且"observable behavior"被定义为仅包括对易失性对象的访问、写入文件的数据以及交互设备的输入和输出。

如果我们可以假设 "segmentation fault" 总是阻止程序的进一步操作,那么任何不存在未定义行为的分段错误都只会在所有可观察到的行为按预期完成后发生。 (但请注意,有效的优化有时会导致事情以与明显顺序不同的顺序发生。)

因此,尽管没有未定义的行为(根据 C 标准),程序导致分段错误(对于 OS)的情况对于真正的编译器来说意义不大,OS,但也不能完全排除。

而且,所有这些都假设有完美的计算机。如果 RAM 坏了,预期的地址值可能最终会改变。甚至有非常罕见但可测量的事件,其中宇宙射线可以在其他良好的 RAM 内发生一点变化。对于几乎任何完美编写的 C 程序,像这样的软错误可能会导致分段错误(在 "segmentation fault" 是一回事的系统上),在任何实现或输入上都不可能出现未定义的行为。

  1. If no, is there any way to ensure a segfault?

这取决于上下文,以及您所说的 "ensure"。

你能写一个总是会导致段错误的 C 程序吗?没有,因为有的电脑可能连这个概念都没有

如果在计算机上可能的话,你能写一个总是导致段错误的 C 程序吗?不,因为在某些情况下某些编译器可能会做一些事情来避免实际问题。由于程序的行为是未定义的,因此不导致段错误与导致段错误一样有效。特别是,你可能 运行 遇到的一个真正障碍,即使是做一些简单的事情,比如故意解除对空指针值的引用,编译器优化有时会假设输入和逻辑总是会出现,这样未定义的行为就不会发生,因为对于确实会导致未定义行为的输入,可以不执行程序所说的操作。

了解了一个特定的 OS 以及 CPU 如何处理内存并有时会产生段错误的详细信息,您能否编写出总是会导致段错误的汇编指令?当然,如果段错误处理有任何价值的话。你能写一个 C 程序来以大致相同的方式触发段错误吗?很有可能。

假设平台完全支持段错误,这里有一些可能性:

  • 使用raise。这将产生明显不同的 siginfo_t,但通常这无关紧要。
  • 取消引用非易失性指针。这很可能被优化为 "unreachable".
  • 取消引用可变指针。这应该可以防止编译器优化访问。
  • 使用asm volatile 并取消引用指针。请务必包含稍后使用的虚假输出,否则编译器仍会执行不需要的优化。这需要针对每个平台的特殊代码。
  • 构建一个内核模块,直接生成一个 SIGSEGV 和适当的 siginfo_t。这假设内核模块甚至 可以 加载(甚至编译)。

如果我们取消对指针的引用,我们是否:

  • 通读一遍,或者
  • 写一遍,或者
  • 两者都有?

而且,它应该是什么指针?

  • NULL 是一个流行的选择,但有时可能会映射该页面。如今,具有安全意识的内核通常不允许这样做。
  • 使用有效但未对齐的指针。这高度依赖于平台,并且可能会产生不同的 siginfo_t。甚至可能是 SIGBUS 之类的东西。
  • 使用跨页的未对齐指针,其中一页未映射或受保护,如下所示。
  • 调用 mmap 保护措施禁止您尝试访问。我不记得这个 siginfo_t 是否可以区分。可以竞争,但前提是程序中的某些其他线程是敌对的。 mmap 也有可能失败。
  • 调用 mmap,紧接着调用 munmap,然后取消引用指针。在信号被阻塞的单线程程序中,这会产生一个保证不会被映射的地址。但是,它可能会在多线程程序中竞争,并且初始 mmap 可能会失败。
  • 循环调用 mmap 直到它失败,因为您已经用尽了内核对映射内存区域数量的 64k 限制。然后解析 /proc/self/maps 并找到一个没有被其中任何一个覆盖的地址。如果其他线程当前正在 munmaping,这可能是活泼的。如果其他线程当前正在 mmaping,它们可能会在您的段错误终止进程之前以神秘的方式失败。

总而言之,没有完美 reliable/portable的方法,但有很多足够好。