Segmentation fault in linux

  • 格式:pdf
  • 大小:309.26 KB
  • 文档页数:20

Segmentation Fault in Linux——原因与避免Author:ZX_WING(xing5820@)写在前面的话最近CU(chinaunix)出现了很多问segmentation fault 的帖子,其实这也是个“月经贴”了,泡CU几年,每个月都有人问。

为了减少重复回帖,笔者结合自己的经验,总结了SIGSEGV在Linux中产生的机理,并用实际例子概括哪些编程错误容易引发SIGSEGV。

由于本人经验有限,文中难免有疏漏和错误,请发现的朋友发信到xing5820@ 指正,笔者好即使修改。

(版权声明:本文欢迎转载,但未经允许不得用于商业目的)内容提要本文简单介绍了Segmentation fault发生的原因,结合实际例子描述了内核向用户态程序发送SIGSEGV信号的流程。

文中以实例回答了常见的一些SIGSEGV问题,例如“为什么函数返回了栈还可以访问?”、“为什么free()后的内存仍然可以使用”、“为什么我遇到的是SIGSEGV而不是SIGILL信号”等。

最后笔者结合自己的经验,列举了一些预防SIGSEGV的编程习惯,供大家参考。

SIGSEGV严格依赖操作系统、编译器、硬件平台,本文基于Linux、GCC、32bit IA32架构,但对其他平台操作系统也有借鉴意义。

Revision History日期版本描述2009.12.21 1.0初次发表版本1.什么是“Segmentation fault in Linux”?我们引用wiki上的一段话来回答这个问题。

A segmentation fault(often shortened to SIGSEGV)is a particular error condition that can occur during the operation of computer software.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).Segmentation is one approach to memory management and protection in the operating system.It has been superseded by paging 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 Unix-like operating systems,a process that accesses an invalid memory address receives the SIGSEGV signal.On Microsoft Windows,a process that accesses invalid memory receives theSTATUS_ACCESS_VIOLATION exception.上述文字没有给出SIGSEGV的定义,仅仅说它是“计算机软件操作过程中的一种错误情况”。

文字描述了SIGSEGV在何时发生,即“当程序试图访问不被允许访问的内存区域(比如,尝试写一块属于操作系统的内存),或以错误的类型访问内存区域(比如,尝试写一块只读内存)。

这个描述是准确的。

为了加深理解,我们再更加详细的概括一下SIGSEGV。

�SIGSEGV是在访问内存时发生的错误,它属于内存管理的范畴�SIGSEGV是一个用户态的概念,是操作系统在用户态程序错误访问内存时所做出的处理。

�当用户态程序访问(访问表示读、写或执行)不允许访问的内存时,产生SIGSEGV。

�当用户态程序以错误的方式访问允许访问的内存时,产生SIGSEGV。

从用户态程序开发的角度,我们并不需要理解操作系统复杂的内存管理机制,这是和硬件平台相关的。

但是,了解内核发送SIGSEGV信号的流程,对我们理解SIGSEGV是很有帮助的。

在《Understanding Linux Kernel Edition3》和《Understanding the Linux Virtual Memory Manager》相关章节都有一幅总图对此描述,对比之下,笔者认为ULK的图更为直观。

图1.SIGSEGV Overview图1红色部分展示了内核发送SIGSEGV信号给用户态程序的总体流程。

当用户态程序访问一个会引发SIGSEGV的地址时,硬件首先产生一个page fault,即“缺页异常”。

在内核的page fault处理函数中,首先判断该地址是否属于用户态程序的地址空间[*]。

以Intel 的32bit IA32架构的CPU为例,用户态程序的地址空间为[0,3G],内核地址空间为[3G,4G]。

如果该地址属于用户态地址空间,检查访问的类型是否和该内存区域的类型是否匹配,不匹配,则发送SIGSEGV信号;如果该地址不属于用户态地址空间,检查访问该地址的操作是否发生在用户态,如果是,发送SIGSEGV信号。

[*]这里的用户态程序地址空间,特指程序可以访问的地址空间范围。

如果广义的说,一个进程的地址空间应该包括内核空间部分,只是它不能访问而已。

图2更为详细的描绘了内核发送SIGSEGV信号的流程。

在这里我们不再累述图中流程,在后面章节的例子中,笔者会结合实际,描述具体的流程。

Page Fault图2SIGSEGV detailed flow2.指针越界和SIGSEGV经常看到有帖子把两者混淆,而这两者的关系也确实微妙。

在此,我们把指针运算(加减)引起的越界、野指针、空指针都归为指针越界。

SIGSEGV在很多时候是由于指针越界引起的,但并不是所有的指针越界都会引发SIGSEGV。

一个越界的指针,如果不解引用它,是不会引起SIGSEGV的。

而即使解引用了一个越界的指针,也不一定会引起SIGSEGV。

这听上去让人发疯,而实际情况确实如此。

SIGSEGV涉及到操作系统、C库、编译器、链接器各方面的内容,我们以一些具体的例子来说明。

2.1错误的访问类型引起的SIGSEGV1#include<stdio.h>2#include<stdlib.h>34int main(){5char*s="hello world";67s[1]='H';8}这是最常见的一个例子。

想当年俺对C语言懵懂的时候,也在校内的BBS上发帖问过,当时还以为这是指针和数组的区别。

此例中,”hello world”作为一个常量字符串,在编译后会被放在.rodata节(GCC),最后链接生成目标程序时.rodata节会被合并到text segment与代码段放在一起,故其所处内存区域是只读的。

这就是错误的访问类型引起的SIGSEGV。

其在图2中的顺序为:1-�3-�4-�6-�8-�11-�102.2访问了不属于进程地址空间的内存1#include<stdio.h>2#include<stdlib.h>34int main(){5int*p=(int*)0xC0000fff;67*p=10;8}在这个例子中,我们访问了一个属于内核的地址(IA32,32bit)。

当然,很少会有人这样写程序,但你的程序可能在不经意的情况下做出这样的行为(这个不经意的行为在后面讨论)。

此例在图2的流程:1-�2-�11-�102.3访问了不存在的内存最常见的情况不外乎解引用空指针了,如:1#include<stdio.h>2#include<stdlib.h>34int main(){5int*a=NULL;67*a=1;8}在实际情况中,此例中的空指针可能指向用户态地址空间,但其所指向的页面实际不存在。

其产生SIGSEGV在图2中的流程为:1�3-�4-�5-�11-�102.4栈溢出了,有时SIGSEGV,有时却啥都没发生这也是CU常见的一个月经贴。

大部分C语言教材都会告诉你,当从一个函数返回后,该函数栈上的内容会被自动“释放”。

“释放”给大多数初学者的印象是free(),似乎这块内存不存在了,于是当他访问这块应该不存在的内存时,发现一切都好,便陷入了深深的疑惑。

1#include<stdio.h>2#include<stdlib.h>34int*foo(){5int a=10;67return&a;8}910int main(){11int*b;1213b=foo();14printf("%d\n",*b);15}当你编译这个程序时,会看到“warning:function returns address of local variable”,GCC 已经在警告你栈溢的可能了。

实际运行结果一切正常。

原因是操作系统通常以“页”的粒度来管理内存,Linux中典型的页大小为4K,内核为进程栈分配内存也是以4K为粒度的。

故当栈溢的幅度小于页的大小时,不会产生SIGSEGV。

那是否说栈溢出超过4K,就会产生SIGSEGV呢?看下面这个例子:1#include<stdio.h>2#include<stdlib.h>34char*foo(){5char buf[8192];67memset(buf,0x55,sizeof(buf));8return buf;9}1011int main(){12char*c;1314c=foo();15printf("%#x\n",c[5000]);16}虽然我们的栈溢已经超出了4K大小,可运行仍然正常。