在C语言中,产生Segmentation Fault(段错误)的最简单的标准符合方式是什么?

11 浏览
0 Comments

在C语言中,产生Segmentation Fault(段错误)的最简单的标准符合方式是什么?

我认为问题已经说得很清楚了。一个涵盖从C89到C11大部分标准的示例将会很有帮助。我想到了这个例子,但我猜它只是未定义的行为:

#include 
int main( int argc, char* argv[] )
{
  const char *s = NULL;
  printf( "%c\n", s[0] );
  return 0;
}

编辑:

由于一些投票要求澄清:我想要一个有常见编程错误的程序(我能想到的最简单的是段错误),这是由标准保证会中止的。这与最小段错误问题有点不同,后者不关心这种保证。

0
0 Comments

在C语言中,产生一个Segmentation Fault(段错误)的最简单标准符合的方法是使用raise()函数来触发一个SIGSEGV信号,即raise(SIGSEGV)。然而,根据标准规定,raise()函数的行为是实现定义的,并没有明确定义在7.14.1.1中与段错误相关的内容。虽然这种方法可能在某些操作系统上产生段错误,但并不一定是标准的。这种方法可以告诉操作系统发生了一个段错误,而不依赖于未定义的行为。

需要注意的是,SIGSEGV代表的是对存储空间的无效访问,而不一定是指段错误。根据C11标准7.14的例子,即使没有内存段,一些系统在奇数地址上进行偶数对齐的内存访问时也可能引发该信号。总之,raise(SIGSEGV)虽然不能真正产生段错误,但可以模拟出发生段错误的情况。

关于段错误的定义,它已经演变成一个通用的术语,用来表示各种无效的内存访问。一般来说,段错误涉及到无效的分页、模式(写入只读内存)或范围错误(地址过高或为0),即在一个段或一段内存范围上的操作成功或失败,这种情况很少能够恢复。而对于内存访问不对齐,我更倾向于将其称为“总线错误”。有些总线错误是可以恢复的。另一种无效访问是由于故障内存(奇偶校验、ECC故障)引起的,有时是可以恢复的,虽然并不一定是段错误的问题。

产生段错误通常是由CPU(尤其是MMU)来检测,而不是由内核来检测。具体而言,内核代码不需要执行任何指令来检测段错误。当然,CPU会跳转到内核代码来处理段错误。raise(SIGSEGV)会跳转到内核来处理SIGSEGV信号,这是相当可比较的。

此外,需要注意的是,raise(SIGSEGV)并不等于真正的段错误,它只是调用了任何SIGSEGV信号的处理程序。根据标准规定,如果处理程序返回,行为是未定义的。许多实现会简单地重新执行导致段错误的指令,但如果使用raise,就不会重新执行该指令。在这种情况下,处理程序仅仅被执行一次,然后程序继续正常执行。而在真正的段错误中,会导致无限循环。

0
0 Comments

在C语言中,产生Segfault(分段错误)的最简单标准合规方法是什么?

分段错误是一种实现定义的行为。标准并未定义实现应如何处理未定义行为,实际上,实现可以优化掉未定义行为并仍然符合标准。需要明确的是,实现定义的行为是指标准未指定但实现应该记录的行为。未定义行为是指不可移植或错误的代码,其行为是不可预测的,因此不能依赖它。

如果我们看一下C99草案标准的第3.4.3节“未定义行为”,它属于“术语、定义和符号”部分的第1段,其中提到(以后的强调是我的):

行为,在使用不可移植或错误的程序结构或错误的数据时,对于此国际标准不强制要求的行为。

在第2段中提到:

注意,可能的未定义行为范围从完全忽略情况并产生不可预测结果,到在翻译或程序执行期间以环境的文档方式表现(可能会发出诊断消息),到终止翻译或执行(并发出诊断消息)。

另一方面,如果您只是想在大多数类Unix系统上使用标准定义的方法来导致分段错误,那么raise(SIGSEGV)应该能够实现这个目标。尽管严格来说,SIGSEGV定义如下:

SIGSEGV - 无效访问存储

并且标准的第7.14节“信号处理”(signal.h)中提到:

一个实现不需要生成这些信号,除非是由于对raise函数的显式调用的结果。实现还可以指定其他信号和指向无法声明的函数的指针,宏定义以SIG和大写字母开头,或以SIG_和大写字母开头的信号,219)。“完整的信号集、其语义和默认处理是实现定义的”;所有信号编号都必须是正数。

虽然msam的回答提供了确切的解决方案,但这个答案给了我最多的见解。现在通过编辑,还提到了raise的可能性,我认为它值得接受。无论如何,感谢所有的贡献者,让我对这个问题有了更深入的了解。

你说“实际上,实现可以优化掉未定义行为并仍然符合标准。”那么,在C++中,双重删除是未定义的。那么,C++的实现是否可以对其进行优化并仍然符合标准?

一旦存在未定义行为,编译器可以做任何事情。

所以你的意思是我在评论中写的那个东西也是可能的。

你能否给我指出一个具体的例子,无论是SO问题还是实时演示?据我所知,答案是肯定的,但是抽象地讨论总是容易忽略重要细节。我的回答提供了一个关于UB和优化的完美例子,并且我提供了很多链接来详细解释这些概念。我的回答提供了一个关于UB和优化的极端例子,并演示了结果可能有多么令人惊讶。

还请参阅“未定义行为”是否真的允许发生任何事情?

0
0 Comments

C语言中最简单的标准符合的产生段错误(Segfault)的方法是什么?

标准只提到了未定义行为,并不了解内存分段。另外需要注意的是,产生错误的代码并不符合标准。你的代码不能同时调用未定义行为和符合标准。

然而,在生成段错误的架构中,产生段错误的最短方法是:

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

为什么这一定会产生段错误?因为对内存地址0的访问总是被系统捕获;它永远不可能是有效的访问(至少不是由用户空间代码实现的)。

当然,并不是所有的架构都工作方式相同。在其中一些架构上,上述代码可能根本不会崩溃,而是产生其他类型的错误。或者该语句可能完全正常,而且内存位置0也可以正常访问。这也是标准没有定义实际发生情况的原因之一。

我曾经使用过用C语言编写的嵌入式系统,其中地址0的内存不仅存在,而且必须被写入。例如,中断向量表通常位于该位置。不过,写下类似于`((unsigned long *)0)[1] = (unsigned long)main;`的代码仍然感觉非常错误。

68000系列的CPU可以寻址内存位置零,从中读取是可以的,但是写入可能导致不可预测的行为。

将`0`转换为指针的结果不一定是地址零。另一方面,将`0`转换为指针必须是一个与任何对象的地址不同的地址,并且您只能写入标准C中的对象。

被赞同的“Your code cannot invoke undefined behavior and be standard conformant at the same time”,但是`*(volatile int *)0`在我看来更安全。

嵌入式系统的开发人员在历史上通常对标准持非常实用的观点。最重要的是具体的实现,在小型CPU上,实现通常是硬件到语言的最自然映射。这毕竟是C语言起源所在。与具有完整库和预期标准遵从性和可移植性的托管环境相比,裸机编程是非常不同的。

例如,在某些68k系统上,写入地址0也是支持的。例如,康柏Amiga内核("exec")在重新启动前,如果发现自身被搞得一团糟,以至于无法显示错误消息(著名的“Guru Meditation”框),会将0x48454C50(ASCII中的“HELP”)写入地址0。ROM引导代码然后会检查这个特殊值,并在该点显示错误消息。当然,所有这些通常都是在汇编语言中编写的内核代码中完成的,但至少在没有MMU的低端Amiga上,原则上任何程序都可以这样做。

但是必须注意的是,编译器(例如gcc)通常会假设空指针不能被解引用而终止程序,并基于这个假设进行优化。因此,在允许解引用空指针的环境中,必须关闭优化。

如果你尝试`((unsigned long *)-1)[1] = (unsigned long)main;`会发生什么?

MacOS(System 1-9)有时会写入地址0,因此苹果公司警告你不要使用它,因为这可能导致系统崩溃。我不认为实际使用地址0是有文档记录的,只是警告不要使用它。通常情况下,MacOS使用真实的内存地址,因此可以随时读写任何地址。

0