69

我认为这个问题说明了一切。涵盖从 C89 到 C11 的大多数标准的示例将很有帮助。我虽然是这个,但我想这只是未定义的行为:

#include <stdio.h>

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

编辑:

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

4

10 回答 10

107

raise()可用于引发段错误:

raise(SIGSEGV);
于 2013-09-24T15:57:38.823 回答
72

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

如果我们查看C99 草案标准§3.4.3未定义的行为,它属于第1段中的术语、定义和符号部分,它说(强调我的未来):

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

在第2段中说:

注意 可能的未定义行为范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(使用发出诊断消息)。

另一方面,如果您只是想要标准中定义的一种方法,该方法会在大多数类 Unix系统上导致分段错误,那么raise(SIGSEGV)应该实现该目标。虽然,严格来说,SIGSEGV定义如下:

SIGSEGV 对存储的无效访问

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

实现不需要生成任何这些信号,除非是对 raise 函数的显式调用。附加的信号和指向不可声明函数的指针,宏定义分别以字母 SIG 和一个大写字母或 SIG_ 和一个大写字母开头,219)也可以由实现来指定。完整的信号集、它们的语义和它们的默认处理是实现定义的;所有信号编号都应为正数。

于 2013-09-24T15:51:44.057 回答
27

该标准仅提及未定义的行为。它对内存分段一无所知。另请注意,产生错误的代码不符合标准。您的代码不能同时调用未定义的行为并符合标准。

尽管如此,在确实产生此类故障的架构上产生分段错误的最短方法是:

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

为什么这肯定会产生段错误?因为访问内存地址0总是被系统困住;它永远不可能是有效的访问(至少不是通过用户空间代码。)

当然请注意,并非所有架构都以相同的方式工作。在其中一些上,上述内容根本不会崩溃,而是会产生其他类型的错误。或者该语句可能非常好,甚至可以很好地访问内存位置 0。这就是该标准实际上并未定义会发生什么的原因之一。

于 2013-09-24T15:54:32.517 回答
13

正确的程序不会产生段错误。而且您无法描述不正确程序的确定性行为。

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

于 2013-09-24T15:51:43.470 回答
8

如果我们假设我们没有发出信号调用 raise,则分段错误可能来自未定义的行为。未定义的行为是未定义的,编译器可以自由拒绝翻译,因此未定义的任何答案都不能保证在所有实现上都失败。此外,调用未定义行为的程序是错误程序。

但这是我能在我的系统上得到该段错误的最短时间:

main(){main();}

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

顺便说一句,这个程序真的会调用未定义的行为吗?

于 2013-09-24T21:05:30.027 回答
4
 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:如果您编译为平面可执行文件,这将不起作用。相反,您将导致未定义的行为。

于 2018-09-05T15:22:13.143 回答
2

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

请注意,这样的程序并不严格符合;符合该定义的程序必须保持在每个最低实施限制内。

否则,符合标准的 C 程序不会产生分段错误,因为唯一的其他方式是通过未定义的行为。

SIGSEGV可以显式发出信号,但标准 C 库中没有符号SIGSEGV

(在此答案中,“符合标准”的意思是:“仅使用 ISO C 标准的某些版本中描述的功能,避免未指定、实现定义或未定义的行为,但不一定限于最低实现限制。”)

于 2013-09-24T19:08:46.593 回答
2

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

++*(int*)0;
于 2019-08-28T20:18:55.457 回答
1

这个问题的大部分答案都围绕着一个关键点,即:C标准不包含分段错误的概念。 (自 C99 起,它包括信号编号 SIGSEGV,但它没有定义传递该信号的任何情况,除了raise(SIGSEGV),如其他答案中讨论的不计算在内。)

因此,没有保证会导致分段错误的“严格符合”程序(即仅使用行为完全由 C 标准定义的结构的程序)。

分段错误由不同的标准POSIX定义。该程序保证SIGBUS在任何完全符合 POSIX.1-2008 的系统(包括内存保护和高级实时选项)上引发分段错误或功能等效的“总线错误”(),前提是调用sysconf,posix_memalignmprotect成功。我对 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;
}
于 2016-05-04T15:29:46.583 回答
1

在未定义的平台上很难定义一种对程序进行分段错误的方法。分段错误是一个松散的术语,并未针对所有平台(例如简单的小型计算机)定义。

仅考虑支持进程的操作系统,进程可以接收到发生分段错误的通知。

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

与大多数跨平台问题的情况一样,每个平台可能(通常会)有不同的 seg-faulting 定义。

但实际上,当前的 mac、lin 和 win 操作系统会出现 segfault on

*(int*)0 = 0;

此外,引起段错误也不是坏行为。导致 SIGSEGV 信号的一些实现assert()可能会产生核心文件。当您需要尸检时非常有用。

比导致段错误更糟糕的是隐藏它:

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

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

?

.

于 2017-07-29T14:46:41.890 回答