数组越界访问会发生什么错误?怎样避免该错误?_SEGMENTATION FAULT IN LINUX 原因与避免...

原作者( ZX_WING([email protected])写得很好,加上之前的确遇到过很多信号问题,产生了很多疑问。

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 the STATUS_ACCESS_VIOLATION exception.

上述文字没有给出SIGSEGV的定义,仅仅说它是“计算机软件操作过程中的一种错误情况”。文字描述了SIGSEGV在何时发生,即“当程序试图访问不被允许访问的内存区域(比如,尝试写一块属于操作系统的内存),或以错误的类型访问内存区域(比如,尝试写一块只读内存)。这个描述是准确的。为了加深理解,我们再更加详细的概括一下SIGSEGV。

Ø SIGSEGV是在访问内存时发生的错误,它属于内存管理的范畴

Ø SIGSEGV是一个用户态的概念,是操作系统在用户态程序错误访问内存时所做出的处理。

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

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

从用户态程序开发的角度,我们并不需要理解操作系统复杂的内存管理机制,这是和硬件平台相关的。但是,了解内核发送SIGSEGV信号的流程,对我们理解SIGSEGV是很有帮助的。在《Understanding Linux Kernel Edition 3》和《Understanding the Linux Virtual Memory Manager》相关章节都有一幅总图对此描述,对比之下,笔者认为ULK的图更为直观。

数组越界访问会发生什么错误?怎样避免该错误?_SEGMENTATION FAULT IN LINUX 原因与避免..._第1张图片

图1. SIGSEGV Overview

图1红色部分展示了内核发送SIGSEGV信号给用户态程序的总体流程。当用户态程序访问一个会引发SIGSEGV的地址时,硬件首先产生一个page fault,即“缺页异常”。在内核的page fault处理函数中,首先判断该地址是否属于用户态程序的地址空间[*]。以Intel的32bit IA32架构的CPU为例,用户态程序的地址空间为[0,3G],内核地址空间为[3G,4G]。如果该地址属于用户态地址空间,检查访问的类型是否和该内存区域的类型是否匹配,不匹配,则发送SIGSEGV信号;如果该地址不属于用户态地址空间,检查访问该地址的操作是否发生在用户态,如果是,发送SIGSEGV信号。

[*]这里的用户态程序地址空间,特指程序可以访问的地址空间范围。如果广义的说,一个进程的地址空间应该包括内核空间部分,只是它不能访问而已。

图2更为详细的描绘了内核发送SIGSEGV信号的流程。在这里我们不再累述图中流程,在后面章节的例子中,笔者会结合实际,描述具体的流程。

数组越界访问会发生什么错误?怎样避免该错误?_SEGMENTATION FAULT IN LINUX 原因与避免..._第2张图片

图2 SIGSEGV detailed flow

2.指针越界和SIGSEGV

经常看到有帖子把两者混淆,而这两者的关系也确实微妙。在此,我们把指针运算(加减)引起的越界、野指针、空指针都归为指针越界。SIGSEGV在很多时候是由于指针越界引起的,但并不是所有的指针越界都会引发SIGSEGV。一个越界的指针,如果不解引用它,是不会引起SIGSEGV的。而即使解引用了一个越界的指针,也不一定会引起SIGSEGV。这听上去让人发疯,而实际情况确实如此。SIGSEGV涉及到操作系统、C库、编译器、链接器各方面的内容,我们以一些具体的例子来说明。

2.1错误的访问类型引起的SIGSEGV

1 #include  2 #include  3  4 int main() { 5 char* s = "hello world"; 6 7 s[1] = 'H';8 }

这是最常见的一个例子。想当年俺对C语言懵懂的时候,也在校内的BBS上发帖问过,当时还以为这是指针和数组的区别。此例中,”hello world”作为一个常量字符串,在编译后会被放在.rodata节(GCC),最后链接生成目标程序时.rodata节会被合并到text segment与代码段放在一起,故其所处内存区域是只读的。这就是错误的访问类型引起的SIGSEGV。

其在图2中的顺序为:

1 -> 3 -> 4 -> 6 -> 8 -> 11 ->10

2.2访问了不属于进程地址空间的内存

 1 #include  2 #include  3  4 int main() { 5 int* p = (int*)0xC0000fff; 6 7 *p = 10; 8 }

在这个例子中,我们访问了一个属于内核的地址(IA32,32bit)。当然,很少会有人这样写程序,但你的程序可能在不经意的情况下做出这样的行为(这个不经意的行为在后面讨论)。此例在图2的流程:

1 -> 2 -> 11 -> 10

2.3访问了不存在的内存

最常见的情况不外乎解引用空指针了,如:

 1 #include  2 #include  3  4 int main () { 5 int *a = NULL; 6 7 *a = 1; 8 }

在实际情况中,此例中的空指针可能指向用户态地址空间,但其所指向的页面实际不存在。其产生SIGSEGV在图2中的流程为:

1 -> 3 -> 4 -> 5 -> 11 ->10

2.4栈溢出了,有时SIGSEGV,有时却啥都没发生

这也是CU常见的一个月经贴。大部分C语言教材都会告诉你,当从一个函数返回后,该函数栈上的内容会被自动“释放”。“释放”给大多数初学者的印象是free(),似乎这块内存不存在了,于是当他访问这块应该不存在的内存时,发现一切都好,便陷入了深深的疑惑。

 1 #include  2 #include  3 4 int* foo() { 5 int a = 10; 6 7 return &a; 8 } 9 10 int main() { 11 int* b; 12 13 b = foo(); 14 printf ("%d

你可能感兴趣的:(数组越界访问会发生什么错误?怎样避免该错误?_SEGMENTATION FAULT IN LINUX 原因与避免...)