在C中生成Segfault的最简单的标准符合方式是什么?

问题描述 投票:55回答:9

我认为问题就是这么说的。涵盖从C89到C11的大多数标准的示例将是有帮助的。我虽然这个,但我猜它只是未定义的行为:

#include <stdio.h>

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

编辑:

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

c segmentation-fault iso
9个回答
67
投票

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

如果我们看一下第1段中的条款,定义和符号部分所述的C99 draft standard§3.4.3未定义行为(强调我的未来):

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

并在第2段中说:

注意可能的未定义行为包括完全忽略具有不可预测结果的情况,在转换或程序执行期间以环境特征(有或没有发出诊断消息)的特定文档执行,终止转换或执行(使用发布诊断消息)。

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

SIGSEGV无效访问存储

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

实现不需要生成任何这些信号,除非显式调用raise函数。实现还可以指定附加信号和指向不可响应函数的指针,其中宏定义分别以字母SIG和大写字母或SIG_和大写字母219开始。完整的信号集,它们的语义和默认处理是实现定义的;所有信号编号均为正数。


99
投票

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

raise(SIGSEGV);

15
投票

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

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

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

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

当然,请注意并非所有体系结构都以相同的方式工作。在其中一些,上面根本不会崩溃,而是产生其他类型的错误。或者声明可以非常好,甚至,内存位置0可以访问得很好。这是标准实际上没有定义发生的事情的原因之一。


12
投票

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

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


7
投票

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

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

main(){main();}

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

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


2
投票

在某些平台上,符合标准的C程序如果从系统请求过多资源,则可能会因分段错误而失败。例如,使用malloc分配大对象似乎可以成功,但是稍后,当访问该对象时,它将崩溃。

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

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

可以明确地引发SIGSEGV信号,但标准C库中没有SIGSEGV符号。

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


1
投票

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

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

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

1
投票

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

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

此外,将操作系统限制为'unix like'OSes,接收SIGSEGV信号的过程的可靠方法是kill(getpid(),SIGSEGV)

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

但实际上,目前的mac,lin和win操作系统将会出现问题

*(int*)0 = 0;

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

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

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

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

?

.


1
投票
 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:如果您编译为平面可执行文件,这将不起作用。相反,您将导致未定义的行为。

© www.soinside.com 2019 - 2024. All rights reserved.