为什么找个程序在OJ上提示Segmentationpage faultt

2011年8月 移动平台大版内专家分月排行榜第二2011年7月 移动平台大版内专家分月排行榜第二2011年3月 移动平台大版内专家分月排行榜第二
2012年8月 移动平台大版内专家分月排行榜第三2012年7月 移动平台大版内专家分月排行榜第三
2011年6月 移动平台大版内专家分月排行榜第二
2011年8月 移动平台大版内专家分月排行榜第二2011年7月 移动平台大版内专家分月排行榜第二2011年3月 移动平台大版内专家分月排行榜第二
2012年8月 移动平台大版内专家分月排行榜第三2012年7月 移动平台大版内专家分月排行榜第三
2011年8月 移动平台大版内专家分月排行榜第二2011年7月 移动平台大版内专家分月排行榜第二2011年3月 移动平台大版内专家分月排行榜第二
2012年8月 移动平台大版内专家分月排行榜第三2012年7月 移动平台大版内专家分月排行榜第三
2011年6月 移动平台大版内专家分月排行榜第二
2011年8月 移动平台大版内专家分月排行榜第二2011年7月 移动平台大版内专家分月排行榜第二2011年3月 移动平台大版内专家分月排行榜第二
2012年8月 移动平台大版内专家分月排行榜第三2012年7月 移动平台大版内专家分月排行榜第三
2011年8月 移动平台大版内专家分月排行榜第二2011年7月 移动平台大版内专家分月排行榜第二2011年3月 移动平台大版内专家分月排行榜第二
2012年8月 移动平台大版内专家分月排行榜第三2012年7月 移动平台大版内专家分月排行榜第三
本帖子已过去太久远了,不再提供回复功能。原作者(&ZX_WING()写得很好,加上之前的确遇到过很多,产生了很多疑问,原创的两张图失效了,转贴补充之。
1.什么是&Segmentation fault in Linux&?
我们引用上的一段话来回答这个问题。
A&segmentation fault&(often shortened to&SIGSEGV) is a particular error condition that can occur during the operation of&. A segmentation fault occurs when a program attempts to access a memory location that it is not allowed to access, or attempts to access a memory location in a way that is not allowed (for example, attempting to write to a read-only location, or to overwrite part of the operating system).
&is one approach to&&and protection in the&. It has been superseded by&&for most purposes, but much of the terminology of segmentation is still used, "segmentation fault" being an example. Some operating systems still have segmentation at some logical level although paging is used as the main memory management policy.
On&&operating systems, a process that accesses an invalid memory address receives the&&. On&, a process that accesses invalid memory receives the STATUS_ACCESS_VIOLATION&.
上述文字没有给出的定义,仅仅说它是&计算机软件操作过程中的一种错误情况&。文字描述了在何时发生,即&当程序试图访问不被允许访问的内存区域(比如,尝试写一块属于操作系统的内存),或以错误的类型访问内存区域(比如,尝试写一块只读内存)。这个描述是准确的。为了加深理解,我们再更加详细的概括一下。
&O&&是在访问内存时发生的错误,它属于内存管理的范畴
&O&&是一个用户态的概念,是操作系统在用户态程序错误访问内存时所做出的处理。
&O&&当用户态程序访问(访问表示读、写或执行)不允许访问的内存时,产生。
&O&&当用户态程序以错误的方式访问允许访问的内存时,产生。
从用户态程序开发的角度,我们并不需要理解操作系统复杂的内存管理机制,这是和硬件平台相关的。但是,了解内核发送信号的流程,对我们理解是很有帮助的。在《》和《》相关章节都有一幅总图对此描述,对比之下,笔者认为的图更为直观。
图红色部分展示了内核发送信号给用户态程序的总体流程。当用户态程序访问一个会引发的地址时,硬件首先产生一个,即&缺页异常&。在内核的处理函数中,首先判断该地址是否属于用户态程序的地址空间。以的架构的为例,用户态程序的地址空间为,,内核地址空间为,。如果该地址属于用户态地址空间,检查访问的类型是否和该内存区域的类型是否匹配,不匹配,则发送信号;如果该地址不属于用户态地址空间,检查访问该地址的操作是否发生在用户态,如果是,发送信号。
这里的用户态程序地址空间,特指程序可以访问的地址空间范围。如果广义的说,一个进程的地址空间应该包括内核空间部分,只是它不能访问而已。
图更为详细的描绘了内核发送信号的流程。在这里我们不再累述图中流程,在后面章节的例子中,笔者会结合实际,描述具体的流程。
图2 SIGSEGV detailed flow
2.指针越界和SIGSEGV
经常看到有帖子把两者混淆,而这两者的关系也确实微妙。在此,我们把指针运算(加减)引起的越界、野指针、空指针都归为指针越界。在很多时候是由于指针越界引起的,但并不是所有的指针越界都会引发。一个越界的指针,如果不解引用它,是不会引起的。而即使解引用了一个越界的指针,也不一定会引起。这听上去让人发疯,而实际情况确实如此。涉及到操作系统、库、编译器、链接器各方面的内容,我们以一些具体的例子来说明。
2.1错误的访问类型引起的SIGSEGV
1 #include &stdio.h&
2 #include &stdlib.h&
4 int main() {
char* s = "hello world";
s[1] = 'H';
这是最常见的一个例子。想当年俺对语言懵懂的时候,也在校内的上发帖问过,当时还以为这是指针和数组的区别。此例中,作为一个常量字符串,在编译后会被放在节(),最后链接生成目标程序时节会被合并到与代码段放在一起,故其所处内存区域是只读的。这就是错误的访问类型引起的。
其在图中的顺序为:
2.2访问了不属于进程地址空间的内存
1 #include &stdio.h&
2 #include &stdlib.h&
4 int main() {
int* p = (int*)0xC0000fff;
在这个例子中,我们访问了一个属于内核的地址(,)。当然,很少会有人这样写程序,但你的程序可能在不经意的情况下做出这样的行为(这个不经意的行为在后面讨论)。此例在图的流程:
2.3访问了不存在的内存
最常见的情况不外乎解引用空指针了,如:
1 #include &stdio.h&
2 #include &stdlib.h&
4 int main () {
int *a = NULL;
在实际情况中,此例中的空指针可能指向用户态地址空间,但其所指向的页面实际不存在。其产生在图中的流程为:
2.4栈溢出了,有时SIGSEGV,有时却啥都没发生
这也是常见的一个月经贴。大部分语言教材都会告诉你,当从一个函数返回后,该函数栈上的内容会被自动&释放&。&释放&给大多数初学者的印象是,似乎这块内存不存在了,于是当他访问这块应该不存在的内存时,发现一切都好,便陷入了深深的疑惑。
1 #include &stdio.h&
2 #include &stdlib.h&
4 int* foo() {
int a = 10;
return &a;
10 int main() {
b = foo();
printf ("%d\n", *b);
当你编译这个程序时,会看到&&,已经在警告你栈溢的可能了。实际运行结果一切正常。原因是操作系统通常以&页&的粒度来管理内存,中典型的页大小为,内核为进程栈分配内存也是以为粒度的。故当栈溢的幅度小于页的大小时,不会产生。那是否说栈溢出超过,就会产生呢?看下面这个例子:
1 #include &stdio.h&
2 #include &stdlib.h&
4 char* foo() {
char buf[8192];
memset (buf, 0x55, sizeof(buf));
11 int main() {
c = foo();
printf ("%#x\n", c[5000]);
虽然我们的栈溢已经超出了大小,可运行仍然正常。这是因为教程中提到的&栈自动释放&实际上是改变栈指针,而其指向的内存,并不是在函数返回时就被回收了。在我们的例子中,所访问的栈溢处内存仍然存在。无效的栈内存(即栈指针范围外未被回收的栈内存)是由操作系统在需要时回收的,这是无法预测的,也就无法预测何时访问非法的栈内容会引发。
好了,在上面的例子中,我们的栈溢例子,无论是大于一个页尺寸还是小于一个页尺寸,访问的都是已分配而未回收的栈内存。那么访问未分配的栈内存,是否就一定会引发呢?答案是否定的。
1 #include &stdio.h&
2 #include &stdlib.h&
4 int main() {
c = (char*)&c & 8192 *2;
printf ("%c\n", *c);
在平台上,栈默认是向下增长的,我们栈溢,访问一块未分配的栈区域(至少从我们的程序来看,此处是未分配的)。选用这个值,是要让我们的溢出范围足够大,大过内核为进程分配的初始栈大小(初始大小为或)。按理说,我们应该看到期望的,但结果却非如此,一切正常。
答案藏在内核的处理函数中:
if (error_code & PF_USER) {
* Accessing the stack below %sp is always a bug.
* The large cushion allows instructions like enter
* and pusha to work.
("enter $65535,$31" pushes
* 32 pointers and then decrements %sp by 65535.)
if (address + 65536 + 32 * sizeof(unsigned long) & regs-&sp)
if (expand_stack(vma, address))
内核为这样的指令留下了空间,从代码来看,理论上栈溢小于左右都是没问题的,栈会自动扩展。令人迷惑的是,笔者用下面这个例子来测试栈溢的阈值,得到的确是这个区间,而不是预料中的。
关于指令的详细介绍,请参考《》节&&
1 #include &stdio.h&
2 #include &stdlib.h&
4 #define GET_ESP(esp) do {
asm volatile ("movl %%esp, %0\n\t" : "=m" (esp));
9 #define K 1024
10 int main() {
int i = 0;
unsigned long
GET_ESP (esp);
printf ("Current stack pointer is %#x\n", esp);
while (1) {
c = (char*)esp -
GET_ESP (esp);
printf ("esp = %#x, overflow %dK\n", esp, i);
笔者目前也不能解释其中的魔术,这神奇的程序啊!上例中发生时,在图中的流程是:
(注意,发生时,该地址已经不属于用户态栈了,所以是&而不是&)
到这里,我们至少能够知道和操作系统(栈的分配和回收),编译器(谁知道它会不会使用这样的指令呢)有着密切的联系,而不像教科书中&函数返回后其使用的栈自动回收&那样简单。
2.5&我们知道栈了,那么堆呢?
1 #include &stdio.h&
2 #include &stdlib.h&
4 #define K 1024
5 int main () {
int i = 0;
c = malloc (1);
while (1) {
printf ("overflow %dK\n", i);
看了栈的例子,举一反三就能知道,和堆的关系取决于你的内存分配器,通常这意味着取决于库的实现。
上面这个例子在笔者机器上于时产生。让我们改变初次的内存大小,当初次分配时,推迟到了溢出;当初次分配时,推迟到了溢出。我们知道内存分配器在分配不同大小的内存时通常有不同的机制,这个例子从某种角度证明了这点。此例在图中的流程为:
用一个野指针在堆里胡乱访问很少见,更多被问起的是&为什么我访问一块后的内存却没发生&,比如下面这个例子:
1 #include &stdio.h&
2 #include &stdlib.h&
4 #define K 1024
5 int main () {
a = malloc (sizeof(int));
printf ("%d\n", *a);
printf ("%d\n", *a);
没有发生,但后指向的内存被清零了,一个合理的解释是为了安全。相信不会再有人问没发生的原因。是的,后的内存不一定就立即归还给了操作系统,在真正的归还发生前,它一直在那儿。
2.6如果是指向全局区的野指针呢?
看了上面两个例子,我觉得这实在没什么好讲的。
2.7&函数跳转到了一个非法的地址上执行
这也是产生的常见原因,来看下面的例子:
1 #include &stdio.h&
2 #include &stdlib.h&
3 #include &string.h&
5 void foo () {
memset (&c, 0x55, 128);
11 int main () {
通过栈溢出,我们将函数的返回地址覆盖成了,函数跳转到了一个非法地址执行,最终引发。非法地址执行,在图中的流程中的可能性就太多了,从,从到之间,几乎每条路径都可能出现。当然对于此例,所指向的页面并不在内存之中,其在图的流程为
如果非法地址对应的页面(页面属于用户态地址空间)存在于内存中,它又是可执行的,则程序会执行一大堆随机的指令。在这些指令执行过程中一旦访问内存,其产生的流程几乎就无法追踪了(除非你用调试工具跟进)。看到这里,一个很合理的问题是:为什么程序在非法地址中执行的是随机指令,而不是非法指令呢?在一块未知的内存上执行,遇到非法指令可能性比较大吧,这样应该收到信号啊?
如果不用段寄存器的,只用页表保护,传统可读即可执行。在技术出现后页级也可以控制是否可以执行。
事实并非如此,我们的架构使用了如此复杂的指令集,以至于找到一条非法指令的编码还真不容易。在下例子中:
1 #include &stdio.h&
2 #include &stdlib.h&
4 int main() {
char buf[128] = "asdfaowerqoweurqwuroahfoasdbaoseur20
234123akfhasbfqower53453";
笔者在中随机的敲入了一些字符,反汇编其内容得到的结果是:
0xbffa9e00:
0xbffa9e01:
0xbffa9e67
0xbffa9e03:
0xbffa9e05:
%ds:(%esi),(%dx)
0xbffa9e06:
0xbffa9e6d
0xbffa9e08:
0xbffa9e7b
0xbffa9e0a:
%ds:(%esi),(%dx)
0xbffa9e0b:
0xbffa9e72
0xbffa9e0d:
0xbffa9e81
0xbffa9e0f:
0xbffa9e88
0xbffa9e11:
0xbffa9e85
0xbffa9e13:
%ds:(%esi),(%dx)
0xbffa9e14:
0xbffa9e15:
$0x73616f66
0xbffa9e1a:
%esp,%fs:0x6f(%ecx)
0xbffa9e1e:
0xbffa9e85
0xbffa9e20:
0xbffa9e94
0xbffa9e22:
(%eax),%dh
0xbffa9e24:
%ah,(%eax)
0xbffa9e26:
%dh,(%edx)
0xbffa9e28:
(%ecx,%esi,1),%esi
0xbffa9e2b:
(%ebx),%dh
0xbffa9e2d:
0xbffa9e2e:
$0x61,0x68(%esi),%esp
0xbffa9e32:
0xbffa9e96
0xbffa9e34:
0xbffa9e35:
0xbffa9ea6
0xbffa9e37:
0xbffa9e9e
0xbffa9e39:
0xbffa9e70
0xbffa9e3b:
0x33(,%esi,1),%esi
0xbffa9e42:
%al,(%eax)
0xbffa9e44:
%al,(%eax)
0xbffa9e46:
%al,(%eax)
0xbffa9e48:
%al,(%eax)
0xbffa9e4a:
%al,(%eax)
0xbffa9e4c:
%al,(%eax)
0xbffa9e4e:
%al,(%eax)
0xbffa9e50:
%al,(%eax)
0xbffa9e52:
%al,(%eax)
0xbffa9e54:
%al,(%eax)
0xbffa9e56:
%al,(%eax)
0xbffa9e58:
%al,(%eax)
0xbffa9e5a:
%al,(%eax)
0xbffa9e5c:
%al,(%eax)
0xbffa9e5e:
%al,(%eax)
&&&&&&&&&&&&&&&&&&&&&&&&&&&&
一条非法指令都没有!大家也可以自己构造一些随机内容试试,看能得到多少非法指令。故在实际情况中,函数跳转到非法地址执行时,遇到的概率是远远大于的。
我们来构造一个遭遇的情况,如下例:
#include &stdio.h&
#include &stdlib.h&
#include &string.h&
#define GET_EBP(ebp)
asm volatile ("movl %%ebp, %0\n\t" : "=m" (ebp));
} while (0)
char buf[128];
void foo () {
printf ("Hello world\n");
void build_ill_func() {
int i = 0;
memcpy (buf, foo, sizeof(buf));
while (1) {
* Find *call* instruction and replace it with
* *ud2a* to generate a #UD exception
if ( buf[i] == 0xffffffe8 ) {
buf[i] = 0x0f;
buf[i+1] = 0x0b;
void overflow_ret_address () {
unsigned long
unsigned long addr = (unsigned long)
GET_EBP (ebp);
for ( i=0; i&16; i++ )
memcpy ((void*)(ebp + i*sizeof(addr)), &addr, sizeof(addr));
printf ("ebp = %#x\n", ebp);
int main() {
printf ("%p\n", buf);
build_ill_func ();
overflow_ret_address ();
我们在一块全局的里填充了一些指令,其中有一条是,它是指令集中用来构造一个非法指令陷阱。在中,我们通过栈溢出覆盖函数的返回地址,使得函数返回时跳转到执行,最终执行到指令产生一个信号。注意此例使用了框架指针寄存器,在编译时不能使用参数,否则得不到期望的结果。
2.8非法的系统调用参数
这是一种较为特殊的情况。特殊是指前面的例子访问非法内存都发生在用户态。而此例中,对非法内存的访问却发生在内核态。通常是执行或时。其流程在图中为:
内核使用的技巧来处理在处理此类错误。说通常的处理是发送一个信号,但实际大多数系统调用都可以返回()码,从而避免用户态程序被终结。这种情况就不举例了,笔者一时间想不出哪个系统调用可以模拟此种情况而不返回错误。
关于的技巧可以参考笔者另一篇文章《》,
2.9还有什么?
我们已经总结了产生的大多数情况,在实际编程中,即使现象不一样,最终发生的原因都可以归到上述几类。掌握了这些基本例子,我们可以避免大多数的。
3.如何避免SIGSEGV
良好的编程习惯永远是最好的预防方法。良好的习惯包括:
尽量按照标准写程序。之所以说是尽量,是因为标准有太多平台相关和无定义的行为,而其中一些实际上已经有既成事实的标准了。例如标准中,一个越界的指针导致的是无定义的行为,而在实际情况中,一个越界而未解引用的指针是不会带来灾难后果的。借用的一个例子,如下:
1 #include &stdio.h&
2 #include &stdlib.h&
4 int main () {
char a[] = "hello";
for ( p = a+5; p&=a; p-- )
printf ("%c\n", *p);
虽然循环结束后,指向了数组前一个元素,在标准中这是一个无定义的行为,但实际上程序却是安全的,没有必要为了不让成为一个野指针而把程序改写为:
1 #include &stdio.h&
2 #include &stdlib.h&
4 int main () {
char a[] = "hello";
for ( p = a+5; p!=a; p-- ) {
printf ("%c\n", *p);
printf ("%c\n", *p);
当然,或许世界上真有编译器会对&越界但未解引用&的野指针进行处理,例如引发一个。笔者无法保证,所以大家在实践中还是各自斟酌吧。
彻底的懂得你的程序。和其它程序员不同的是,程序员需要对自己的程序完全了解,做到精确控制。尤其在内存的分配和释放方面。在操作每一个指针前,你都应该清楚它所指向内存的出处(栈、堆、全局区),并清楚此内存的生存周期。只有明白的使用内存,才能最大限度的避免的产生。
大量使用。笔者偏好在程序中使用大量的,凡是有认为不该出现的情况,笔者就会加入一个做检查。虽然无法直接避免,但它却能尽早的抛出错误。离错误越近,就越容易。很多时候出现时,程序已经跑飞很远了。
打开编译选项。如果程序是自己写的,应该始终是一项指标(不包括因为编译器版本不同而引起的)。一种常见的来源于向函数传入了错误的参数类型。例如:
1 #include &stdio.h&
2 #include &stdlib.h&
3 #include &string.h&
5 int main () {
char buf[12];
strcpy (buff, "hello");
这个例子中,本意是要向拷贝一个字符串,但由于有一个和名称很相近的变量,由于一个笔误(这个笔误很可能就来自你编辑器的自动补全,例如的),如愿的引发了。实际在编译期间,编译器就提示我们,但我们忽略了。
这就进一步要求我们尽量使用编译器的类型检查功能,包括多用函数少用宏(特别是完成复杂功能的宏),函数参数多用带类型的指针,少用指针等。此例就是我们在节提到的不经意的行为。
少用奇技淫巧,多用标准方法。好的程序应该逻辑清楚,干净整洁,像一篇朗朗上口的文章,让人一读就懂。那种充满晦涩语法、怪异招数的试验作品,是不受欢迎的。很多人喜欢把性能问题做为使用不标准方法的借口,实际上他们根本不知道对性能的影响如何,拿不出具体指标,全是想当然尔。笔者曾经在项目中,将一个执行频繁的异常处理函数用汇编重写,使该函数的执行周期从多个机器周期下降到多个。满心欢喜的提交了一个给该项目的,得到的答复是:&张,你具体测试过你的能带来多大的性能提升吗?如果没有明显的数据,我是不愿意将优雅的代码替换成这晦涩的汇编的。&于是我做了一个内核编译来测试,耗时分钟,我的带来的整体性能提升大约为。所以,尽量写清楚明白的代码,不仅有利于避免,也利于在出现后进行调试。
当你的一个需求,标准的方法不能满足时,只有两种可能:从一开始的设计就错了,才会导致错误的需求;你读过的代码太少,不知道业界解决该问题的标准方法是什么。计算机已经发展了几十年,如果你不是在做前沿研究,遇到一定得用非标准方法解决的问题的机会实在太小了。正如我们经常用跟踪发现发生在库里,不要嚷嚷说库有,大部情况是一开始你传入的参数就错了。
无论如何我们应该感谢,是它让我们能在不重启机器的情况下调试程序。相比那些由于内存使用错误而不得不一次又一次重启机器来的内核工程师,让我们的生活变得轻松。理解同时,我们也更加理解程序。希望这篇文档对初学语言的同志有些许帮助。
PS:原文有些代码部分多了写#include 的我就不改了。可以看懂。
阅读(...) 评论()2016年5月 总版技术专家分月排行榜第二
2016年10月优秀大版主2016年8月论坛优秀大版主
2016年5月 总版技术专家分月排行榜第二
2016年10月优秀大版主2016年8月论坛优秀大版主
匿名用户不能发表回复!|
每天回帖即可获得10分可用分!小技巧:
你还可以输入10000个字符
(Ctrl+Enter)
请遵守CSDN,不得违反国家法律法规。
转载文章请注明出自“CSDN(www.csdn.net)”。如是商业用途请联系原作者。

我要回帖

更多关于 segmentfault 的文章

 

随机推荐