[英]What is the simplest standard conform way to produce a Segfault in C?
我认为这个问题说明了一切。 涵盖从 C89 到 C11 的大多数标准的示例将很有帮助。 我虽然是这个,但我想这只是未定义的行为:
#include <stdio.h>
int main( int argc, char* argv[] )
{
const char *s = NULL;
printf( "%c\n", s[0] );
return 0;
}
编辑:
正如一些投票要求澄清的那样:我想要一个程序有一个通常的编程错误(我能想到的最简单的是一个段错误),它(按标准)保证中止。 这与最小的段错误问题有点不同,它不关心这个保险。
raise()
可用于引发段错误:
raise(SIGSEGV);
分段错误是实现定义的行为。 该标准没有定义实现应该如何处理未定义的行为,实际上实现可以优化未定义的行为并且仍然是合规的。 需要明确的是,实现定义的行为是标准未指定但实现应该记录的行为。 未定义的行为是不可移植或错误的代码,其行为不可预测,因此不能依赖。
如果我们查看C99 草案标准§3.4.3未定义的行为,它属于第1段中的术语、定义和符号部分,它说(强调我的未来):
使用不可移植或错误程序结构或错误数据时的行为,本国际标准对此没有要求
在第2段中说:
注意 可能的未定义行为范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(使用发出诊断消息)。
另一方面,如果您只是想要标准中定义的方法,该方法会在大多数类 Unix系统上导致分段错误,那么raise(SIGSEGV)
应该可以实现该目标。 虽然严格来说, SIGSEGV
的定义如下:
SIGSEGV 对存储的无效访问
和 §7.14信号处理<signal.h>
说:
实现不需要生成任何这些信号,除非是对 raise 函数的显式调用。 附加的信号和指向不可声明函数的指针,宏定义分别以字母 SIG 和一个大写字母或 SIG_ 和一个大写字母开头,219)也可以由实现指定。 完整的信号集、它们的语义和它们的默认处理是实现定义的; 所有信号编号都应为正数。
该标准仅提及未定义的行为。 它对内存分段一无所知。 另请注意,产生错误的代码不符合标准。 您的代码不能同时调用未定义的行为并符合标准。
尽管如此,在确实产生此类故障的架构上产生分段错误的最短方法是:
int main()
{
*(int*)0 = 0;
}
为什么这肯定会产生段错误? 因为访问内存地址0总是被系统困住; 它永远不可能是有效的访问(至少不是通过用户空间代码。)
当然请注意,并非所有架构都以相同的方式工作。 在其中一些上,上述内容根本不会崩溃,而是会产生其他类型的错误。 或者该语句可能非常好,甚至可以很好地访问内存位置 0。 这就是该标准实际上并未定义会发生什么的原因之一。
正确的程序不会产生段错误。 而且您无法描述不正确程序的确定性行为。
“分段错误”是 x86 CPU 所做的事情。 您可以通过尝试以不正确的方式引用内存来获得它。 它还可以指内存访问导致页面错误(即尝试访问未加载到页表中的内存)并且操作系统决定您无权请求该内存的情况。 要触发这些条件,您需要直接为您的操作系统和硬件进行编程。 它不是 C 语言指定的。
如果我们假设我们没有发出调用raise
的信号,则分段错误很可能来自未定义的行为。 未定义的行为是未定义的,编译器可以自由拒绝翻译,因此未定义的任何答案都不能保证在所有实现上都失败。 此外,调用未定义行为的程序是错误程序。
但这是我能在我的系统上得到该段错误的最短时间:
main(){main();}
(我用gcc
和-std=c89 -O0
编译)。
顺便说一句,这个程序真的会调用未定义的行为吗?
main;
而已。
真的。
本质上,它的作用是将main
定义为variable 。 在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:如果您编译为平面可执行文件,这将不起作用。 相反,您将导致未定义的行为。
在某些平台上,如果从系统请求太多资源,符合标准的 C 程序可能会因分段错误而失败。 例如,使用malloc
分配一个大对象可能看起来成功,但稍后,当访问该对象时,它会崩溃。
请注意,这样的程序并不严格符合; 符合该定义的程序必须保持在每个最低实施限制内。
否则,符合标准的 C 程序不会产生分段错误,因为唯一的其他方式是通过未定义的行为。
SIGSEGV
信号可以显式引发,但标准 C 库中没有SIGSEGV
符号。
(在此答案中,“符合标准”的意思是:“仅使用 ISO C 标准的某些版本中描述的功能,避免未指定、实现定义或未定义的行为,但不一定限于最低实现限制。”)
考虑最少字符数的最简单形式是:
++*(int*)0;
这个问题的大部分答案都围绕着一个关键点,即: C标准不包含分段错误的概念。 (自 C99 以来,它包括信号编号SIGSEGV
,但它没有定义传递该信号的任何情况,除了raise(SIGSEGV)
,如其他答案中所讨论的不计算在内。)
因此,没有保证会导致分段错误的“严格符合”程序(即仅使用行为完全由 C 标准定义的结构的程序)。
分段错误由不同的标准POSIX定义。 该程序保证在任何完全符合 POSIX.1-2008 的系统(包括内存保护和高级实时选项)上引发分段错误或功能等效的“总线错误”( SIGBUS
),前提是调用sysconf
、 posix_memalign
和mprotect
成功。 我对 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-faulting 定义。
但实际上,当前的 mac、lin 和 win 操作系统会出现 segfault on
*(int*)0 = 0;
此外,引起段错误也不是坏行为。 assert()
的一些实现会导致一个 SIGSEGV 信号,该信号可能会产生一个核心文件。 当您需要尸检时非常有用。
比导致段错误更糟糕的是隐藏它:
try
{
anyfunc();
}
catch (...)
{
printf("?\n");
}
它隐藏了错误的根源,你所要做的就是:
?
.
这是我在这里没有提到的另一种方式:
int main() {
void (*f)(void);
f();
}
在这种情况下, f
是一个未初始化的函数指针,当您尝试调用它时会导致分段错误。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.