在C中生成Segfault的最简单的标准符合方式是什么?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在C中生成Segfault的最简单的标准符合方式是什么?相关的知识,希望对你有一定的参考价值。
我认为问题就是这么说的。涵盖从C89到C11的大多数标准的示例将是有帮助的。我虽然这个,但我猜它只是未定义的行为:
#include <stdio.h>
int main( int argc, char* argv[] )
{
const char *s = NULL;
printf( "%c
", s[0] );
return 0;
}
编辑:
正如一些投票要求澄清:我希望有一个程序通常有编程错误(我能想到的最简单的是段错误),这是保证(按标准)中止。这与最小的段错误问题有点不同,它不关心这种保险。
分段错误是实现定义的行为。该标准没有定义实现应该如何处理undefined behavior,事实上,实现可以优化未定义的行为并且仍然是合规的。需要明确的是,实现定义的行为是标准不是specified的行为,但实现应该记录。未定义的行为是不可移植或错误的代码,其行为是不可预测的,因此无法依赖。
如果我们看一下第1段中的条款,定义和符号部分所述的C99 draft standard§3.4.3未定义行为(强调我的未来):
使用不可移植或错误的程序结构或错误数据时的行为,本国际标准不对此要求
并在第2段中说:
注意可能的未定义行为包括完全忽略具有不可预测结果的情况,在转换或程序执行期间以环境特征(有或没有发出诊断消息)的特定文档执行,终止转换或执行(使用发布诊断消息)。
另一方面,如果你只是想要一个在标准中定义的方法,它会在大多数类Unix系统上导致分段错误,那么raise(SIGSEGV)
应该实现这个目标。虽然严格来说,SIGSEGV
的定义如下:
SIGSEGV无效访问存储
和§7.14信号处理<signal.h>
说:
实现不需要生成任何这些信号,除非显式调用raise函数。实现还可以指定附加信号和指向不可响应函数的指针,其中宏定义分别以字母SIG和大写字母或SIG_和大写字母219开始。完整的信号集,它们的语义和默认处理是实现定义的;所有信号编号均为正数。
raise()
可用于引发段错误:
raise(SIGSEGV);
该标准仅提到未定义的行为。它对内存分段一无所知。另请注意,产生错误的代码不符合标准。您的代码无法同时调用未定义的行为并且符合标准。
尽管如此,在产生此类故障的体系结构上产生分段错误的最短方法是:
int main()
{
*(int*)0 = 0;
}
为什么这肯定会产生段错误?因为对内存地址0的访问总是被系统捕获;它永远不会是有效的访问(至少不是用户空间代码。)
当然,请注意并非所有体系结构都以相同的方式工作。在其中一些,上面根本不会崩溃,而是产生其他类型的错误。或者声明可以非常好,甚至,内存位置0可以访问得很好。这是标准实际上没有定义发生的事情的原因之一。
正确的程序不会产生段错误。而且您无法描述错误程序的确定性行为。
“分段错误”是x86 CPU所做的事情。你通过尝试以不正确的方式引用内存来获得它。它还可以指内存访问导致页面错误(即尝试访问未加载到页表中的内存)的情况,并且操作系统决定您无权请求该内存。要触发这些条件,您需要直接为您的操作系统和硬件编程。它不是C语言指定的。
如果我们假设我们没有提出调用raise
的信号,则分段错误可能来自未定义的行为。未定义的行为是未定义的,并且编译器可以自由拒绝转换,因此在所有实现上都不会保证未定义的答案都会失败。此外,调用未定义行为的程序是错误的程序。
但这个是我能在系统上得到的最短的段错误:
main(){main();}
(我用gcc
和-std=c89 -O0
编译)。
顺便说一下,这个程序真的会调用未定义的行为吗?
在某些平台上,符合标准的C程序如果从系统请求过多资源,则可能会因分段错误而失败。例如,使用malloc
分配大对象似乎可以成功,但是稍后,当访问该对象时,它将崩溃。
请注意,这样的程序并不严格符合要求;符合该定义的程序必须保持在每个最低实施限制范围内。
符合标准的C程序否则不会产生分段错误,因为唯一的其他方法是通过未定义的行为。
可以明确地引发SIGSEGV
信号,但标准C库中没有SIGSEGV
符号。
(在本回答中,“符合标准”意味着:“仅使用某些版本的ISO C标准中描述的功能,避免未指定的,实现定义的或未定义的行为,但不一定限于最小实现限制。”)
这个问题的大部分答案都在谈论关键点,即: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
", strerror(errno));
return 1;
}
void *page;
int err = posix_memalign(&page, pagesize, pagesize);
if (err || !page) {
fprintf(stderr, "posix_memalign: %s
", strerror(err));
return 1;
}
if (mprotect(page, pagesize, PROT_NONE)) {
fprintf(stderr, "mprotect: %s
", strerror(errno));
return 1;
}
*(long *)page = 0xDEADBEEF;
return 0;
}
很难定义一种在未定义平台上对程序进行分段故障的方法。分段错误是一个松散的术语,没有为所有平台定义(例如,简单的小型计算机)。
仅考虑支持进程的操作系统,进程可以接收发生分段错误的通知。
此外,将操作系统限制为'unix like'OSes,接收SIGSEGV信号的过程的可靠方法是kill(getpid(),SIGSEGV)
与大多数跨平台问题一样,每个平台可能(通常都是)具有不同的seg-faulting定义。
但实际上,目前的mac,lin和win操作系统将会出现问题
*(int*)0 = 0;
此外,导致段错也不错。 assert()
的某些实现会导致SIGSEGV信号,这可能会产生核心文件。当您需要尸检时非常有用。
更糟糕的是导致段错误隐藏它:
try
{
anyfunc();
}
catch (...)
{
printf("?
");
}
它隐藏了错误的起源,你必须要做的就是:
?
.
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:如果您编译为平面可执行文件,这将不起作用。相反,您将导致未定义的行为。
以上是关于在C中生成Segfault的最简单的标准符合方式是什么?的主要内容,如果未能解决你的问题,请参考以下文章