在 C 中生成 Segfault 最简单的标准一致性方法是什么?

我觉得问题说明了一切。一个涵盖从 C89到 C11的大多数标准的例子将是有帮助的。我想到了这一点,但我想这只是一种不明确的行为:

#include <stdio.h>


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

编辑:

正如一些投票要求澄清的那样: 我希望有一个程序有一个常见的编程错误(我能想到的最简单的错误是 Segfault) ,即 保证(按标准)中止。这与最小 Segfault 问题稍有不同,后者不关心这个保险。

30105 次浏览

一个正确的程序不会产生一个 Segfault,而且你也不能描述一个不正确的程序的确定性行为。

“内存区段错误”是一个 x86 CPU 所做的事情。您可以通过尝试以不正确的方式引用内存来获得它。它还可以引用这样一种情况: 内存访问导致页面错误(即试图访问没有加载到页表中的内存) ,而操作系统决定您无权请求该内存。要触发这些条件,您需要直接为您的操作系统和硬件编程。它不是由 C 语言指定的。

内存区段错误就是 实现定义的行为。该标准没有定义实现应该如何处理 未定义行为,实际上实现可以优化出 未定义行为,并且仍然是兼容的。需要说明的是,实现定义的行为是一种行为,按照标准来说它不是 指明,但是实现应该记录在案。未定义行为是不可移植或错误的代码,其行为是不可预测的,因此不能依赖。

如果我们看一下 C99草案标准 & section; 3.4.3 未定义行为,它在 1段的 术语、定义和符号部分下面,它说(强调我的未来) :

行为,在使用不可移植的或错误的程序构造或错误的数据时,本国际标准没有任何要求

2段中写道:

注意可能出现的未定义行为包括: 完全忽视这种情况,导致不可预测的结果; 在翻译或程序执行过程中,采取有记录的环境特征方式(发布或不发布诊断信息) ; 终止翻译或执行(发布诊断信息)。

另一方面,如果你只是想在标准中定义一种方法,这种方法会在大多数 像 Unix 一样系统上引起内存区段错误,那么 raise(SIGSEGV)就应该实现这个目标。尽管严格来说,SIGSEGV的定义如下:

对存储的无效访问

7.14 信号处理 <signal.h>说:

实现不需要生成这些信号中的任何一个,除非是显式调用 rise 函数 的结果。实现还可以指定指向不可声明函数的其他信号和指针,宏定义分别以字母 SIG 和大写字母或 SIG _ 和大写字母219开始。所有信号号码必须为正。

标准只提到未定义行为。它对记忆体区段一无所知。还要注意,产生错误的代码不符合标准。您的代码不能同时调用未定义行为和标准一致性。

尽管如此,要了解导致 出现此类错误的体系结构的内存区段错误,最简单的方法是:

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

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

当然要注意,并非所有的体系结构都以相同的方式工作。对于其中一些错误,上述方法根本不会崩溃,而是会产生其他类型的错误。或者语句完全可以访问,甚至内存位置0也可以访问。这就是为什么标准没有真正定义发生了什么的原因之一。

raise()可以用来产生一个 Segfault:

raise(SIGSEGV);

在某些平台上,如果一个符合标准的 C 程序从系统中请求了太多的资源,那么它可能会因为一个内存区段错误而失败。例如,使用 malloc分配一个大型对象可能看起来成功,但是稍后,当访问该对象时,它将崩溃。

注意,这样的程序不符合 严格来说; 符合该定义的程序必须保持在每个最小实现限制内。

一个符合标准的 C 程序不可能产生一个内存区段错误,因为唯一的其他途径是通过未定义行为。

SIGSEGV信号可以显式提升,但是在标准 C 库中没有 SIGSEGV符号。

(在这个答案中,“符合标准”的意思是: “只使用某些版本的国际标准化组织 C 标准中描述的特性,避免未指明、实施定义或未定义行为,但不一定限于最低实施限制。”)

如果我们假设我们没有发出一个叫做 abc0的信号,那么内存区段错误很可能来自未定义行为。未定义行为是未定义的,编译器可以自由地拒绝翻译,所以没有未定义的答案会在所有实现中都失败。此外,一个调用未定义行为的程序是一个错误的程序。

但是这是我在 天啊系统中能找到的最短的一段:

main(){main();}

(我用 gcc-std=c89 -O0编译)。

顺便问一下,这个程序真的会引起未定义的行为吗?

这个问题的大部分答案都围绕着关键点展开讨论,那就是: C 标准并不包括内存区段错误的概念。(自 C99以来,它包括了 信号编号 SIGSEGV,但是它没有定义任何情况,除了 raise(SIGSEGV),其他答案中讨论的不算数)

因此,没有“严格一致”的程序(即只使用行为完全由 C 标准定义的构造的程序)能够保证引起内存区段错误。

分段故障由不同的标准 POSIX定义。这个程序保证在任何完全符合 POSIX.1-2008标准的系统上,包括内存保护和高级实时选项,只要对 sysconfposix_memalignmprotect的调用成功,就会引发内存区段错误或功能等同的“总线错误”(SIGBUS)。我对 C99的理解是这个程序有 实现定义(不是没有定义!)行为只考虑该标准,因此它是 符合而不是 严格遵守

#define _XOPEN_SOURCE 700
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>


int main(void)
{
size_t pagesize = sysconf(_SC_PAGESIZE);
if (pagesize == (size_t)-1) {
fprintf(stderr, "sysconf: %s\n", strerror(errno));
return 1;
}
void *page;
int err = posix_memalign(&page, pagesize, pagesize);
if (err || !page) {
fprintf(stderr, "posix_memalign: %s\n", strerror(err));
return 1;
}
if (mprotect(page, pagesize, PROT_NONE)) {
fprintf(stderr, "mprotect: %s\n", strerror(errno));
return 1;
}
*(long *)page = 0xDEADBEEF;
return 0;
}

在未定义的平台上定义一个方法来 内存区段错误程序是很困难的。内存区段错误是一个松散的术语,没有为所有平台(例如简单的小型计算机)定义。

考虑到只有支持 程序的操作系统,进程才能接收到内存区段错误发生的通知。

此外,将操作系统限制为“ Unix 样”的操作系统,进程接收 SIGSEGV 信号的可靠方法是 kill(getpid(),SIGSEGV)

与大多数跨平台问题的情况一样,每个平台可能(通常确实如此)对 seg 错误有不同的定义。

但为了实际起见,当前的 Mac、 Lin 和 Win 操作系统将出现分裂

*(int*)0 = 0;

此外,导致 Segfault 也不是什么坏行为。assert()的一些实现会导致 SIGSEGV 信号,这可能会产生一个核心文件。当你需要解剖的时候很有用。

比导致 sefault 更糟糕的是隐藏它:

try
{
anyfunc();
}
catch (...)
{
printf("?\n");
}

它隐藏了错误的起源,你所要做的就是:

?

.

 main;

就是这样。

真的。

本质上,它将 main定义为 变量。 在 C 语言中,变量和函数都是内存中的 符号指针,因此编译器不会区分它们,而且这段代码不会抛出错误。

然而,问题在于 系统如何运行可执行文件。简而言之,C 标准要求所有 C 可执行程序都内置一个准备环境的入口点,这基本上归结为“调用 main”。

然而,在这种特殊情况下,main是一个变量,因此它被放置在称为 .bss不可执行文件内存区域中,用于变量(与代码的 .text相反)。试图在 .bss中执行代码违反了它的特定分割,因此系统抛出了一个内存区段错误。

为了说明这一点,下面是结果文件的 objdump的一部分:

# (unimportant)


Disassembly of section .text:


0000000000001020 <_start>:
1020:   f3 0f 1e fa             endbr64
1024:   31 ed                   xor    %ebp,%ebp
1026:   49 89 d1                mov    %rdx,%r9
1029:   5e                      pop    %rsi
102a:   48 89 e2                mov    %rsp,%rdx
102d:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
1031:   50                      push   %rax
1032:   54                      push   %rsp
1033:   4c 8d 05 56 01 00 00    lea    0x156(%rip),%r8        # 1190 <__libc_csu_fini>
103a:   48 8d 0d df 00 00 00    lea    0xdf(%rip),%rcx        # 1120 <__libc_csu_init>


# This is where the program should call main
1041:   48 8d 3d e4 2f 00 00    lea    0x2fe4(%rip),%rdi      # 402c <main>
1048:   ff 15 92 2f 00 00       callq  *0x2f92(%rip)          # 3fe0 <__libc_start_main@GLIBC_2.2.5>
104e:   f4                      hlt
104f:   90                      nop


# (nice things we still don't care about)


Disassembly of section .data:


0000000000004018 <__data_start>:
...


0000000000004020 <__dso_handle>:
4020:   20 40 00                and    %al,0x0(%rax)
4023:   00 00                   add    %al,(%rax)
4025:   00 00                   add    %al,(%rax)
...


Disassembly of section .bss:


0000000000004028 <__bss_start>:
4028:   00 00                   add    %al,(%rax)
...


# main is in .bss (variables) instead of .text (code)


000000000000402c <main>:
402c:   00 00                   add    %al,(%rax)
...


# aaand that's it!

PS: 如果你编译成一个平面的可执行文件,这将不起作用。相反,你会导致未定义的行为。

考虑到字符数最少的最简单形式是:

++*(int*)0;

还有一种我没有在这里提到的方式:

int main() {
void (*f)(void);
f();
}

在这种情况下,f是一个未初始化的函数指针,当你试图调用它时,它会导致一个内存区段错误。