您的位置:首页 > 运维架构 > Linux

Segmentation Fault in Linux (2.指针越界和SIGSEGV)

2011-12-20 10:09 447 查看

Segmentation Fault in Linux (2.指针越界和SIGSEGV)

作者:Fancy 来源:博客园 发布时间:2010-06-07 13:20 阅读:760 次 原文链接 [收藏]

2.指针越界和SIGSEGV

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

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

1 #include <stdio.h>

2 #include <stdlib.h>

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 <stdio.h>

2 #include <stdlib.h>

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 <stdio.h>

2 #include <stdlib.h>

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 <stdio.h>

2 #include <stdlib.h>

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\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>

3

4 char* foo() {

5 char buf[8192];

6

7 memset (buf, 0x55, sizeof(buf));

8 return buf;

9 }

10

11 int main() {

12 char* c;

13

14 c = foo();

15 printf ("%#x\n", c[5000]);

16 }

虽然我们的栈溢已经超出了4K大小,可运行仍然正常。这是因为C教程中提到的“栈自动释放”实际上是改变栈指针,而其指向的内存,并不是在函数返回时就被回收了。在我们的例子中,所访问的栈溢处内存仍然存在。无效的栈内存(即栈指针范围外未被回收的栈内存)是由操作系统在需要时回收的,这是无法预测的,也就无法预测何时访问非法的栈内容会引发SIGSEGV。

好了,在上面的例子中,我们的栈溢例子,无论是大于一个页尺寸还是小于一个页尺寸,访问的都是已分配而未回收的栈内存。那么访问未分配的栈内存,是否就一定会引发SIGSEGV呢?答案是否定的。

1 #include <stdio.h>

2 #include <stdlib.h>

3

4 int main() {

5 char* c;

6

7 c = (char*)&c – 8192 *2;

8 *c = 'a';

9 printf ("%c\n", *c);

10 }

在IA32平台上,栈默认是向下增长的,我们栈溢16K,访问一块未分配的栈区域(至少从我们的程序来看,此处是未分配的)。选用16K这个值,是要让我们的溢出范围足够大,大过内核为进程分配的初始栈大小(初始大小为4K或8K)。按理说,我们应该看到期望的SIGSEGV,但结果却非如此,一切正常。

答案藏在内核的page fault处理函数中:

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)

goto bad_area;

}

if (expand_stack(vma, address))

goto bad_area;

内核为enter[*]这样的指令留下了空间,从代码来看,理论上栈溢小于64K左右都是没问题的,栈会自动扩展。令人迷惑的是,笔者用下面这个例子来测试栈溢的阈值,得到的确是70K ~ 80K这个区间,而不是预料中的65K
~ 66K。

[*]关于enter指令的详细介绍,请参考《Intel(R) 64 and IA-32 Architectures Software Developer Manual Volume 1》6.5节“PROCEDURE
CALLS FOR BLOCK-STRUCTURED LANGUAGES”

1 #include <stdio.h>

2 #include <stdlib.h>

3

4 #define GET_ESP(esp) do { \

5 asm volatile ("movl %%esp, %0\n\t" : "=m" (esp)); \

6 } while (0)

7

8

9 #define K 1024

10 int main() {

11 char* c;

12 int i = 0;

13 unsigned long esp;

14

15 GET_ESP (esp);

16 printf ("Current stack pointer is %#x\n", esp);

17 while (1) {

18 c = (char*)esp - i * K;

19 *c = 'a';

20 GET_ESP (esp);

21 printf ("esp = %#x, overflow %dK\n", esp, i);

22 i ++;

23 }

24 }

笔者目前也不能解释其中的魔术,这神奇的程序啊!上例中发生SIGSEGV时,在图2中的流程是:

1 -à 3 -à 4 -à 5 -à 11
-à 10 (注意,发生SIGSEGV时,该地址已经不属于用户态栈了,所以是5 à 11 而不是 5
-à 6)

到这里,我们至少能够知道SIGSEGV和操作系统(栈的分配和回收),编译器(谁知道它会不会使用enter这样的指令呢)有着密切的联系,而不像教科书中“函数返回后其使用的栈自动回收”那样简单。

2.5 我们知道栈了,那么堆呢?

看了栈的例子,举一反三就能知道,SIGSEGV和堆的关系取决于你的内存分配器,通常这意味着取决于C库的实现。

1 #include <stdio.h>

2 #include <stdlib.h>

3

4 #define K 1024

5 int main () {

6 char* c;

7 int i = 0;

8

9 c = malloc (1);

10 while (1) {

11 c += i*K;

12 *c = 'a';

13 printf ("overflow %dK\n", i);

14 i ++;

15 }

16 }

上面这个例子在笔者机器上于15K时产生SIGSEGV。让我们改变初次malloc的内存大小,当初次分配16M时,SIGSEGV推迟到了溢出180K;当初次分配160M时,SIGSEGV推迟到了溢出571K。我们知道内存分配器在分配不同大小的内存时通常有不同的机制,这个例子从某种角度证明了这点。此例SIGSEGV在图2中的流程为:

1 -à 3 -à 4 -à 5 -à 11
-à 10

用一个野指针在堆里胡乱访问很少见,更多被问起的是“为什么我访问一块free()后的内存却没发生SIGSEGV”,比如下面这个例子:

1 #include <stdio.h>

2 #include <stdlib.h>

3

4 #define K 1024

5 int main () {

6 int* a;

7

8 a = malloc (sizeof(int));

9 *a = 100;

10 printf ("%d\n", *a);

11 free (a);

12 printf ("%d\n", *a);

13 }

SIGSEGV没有发生,但free()后a指向的内存被清零了,一个合理的解释是为了安全。相信不会再有人问SIGSEGV没发生的原因。是的,free()后的内存不一定就立即归还给了操作系统,在真正的归还发生前,它一直在那儿。

2.6如果是指向全局区的野指针呢?

看了上面两个例子,我觉得这实在没什么好讲的。

2.7 函数跳转到了一个非法的地址上执行

这也是产生SIGSEGV的常见原因,来看下面的例子:

1 #include <stdio.h>

2 #include <stdlib.h>

3 #include <string.h>

4

5 void foo () {

6 char c;

7

8 memset (&c, 0x55, 128);

9 }

10

11 int main () {

12 foo();

13 }

通过栈溢出,我们将函数foo的返回地址覆盖成了0x55555555,函数跳转到了一个非法地址执行,最终引发SIGSEGV。非法地址执行,在图2中的流程中的可能性就太多了,从1-à3
-à4 -à … -à10,从4到10之间,几乎每条路径都可能出现。当然对于此例,0x55555555所指向的页面并不在内存之中,其在图2的流程为:

1-à3 -à4 -à5--à11-à10

如果非法地址对应的页面(页面属于用户态地址空间)存在于内存中,它又是可执行的[*],则程序会执行一大堆随机的指令。在这些指令执行过程中一旦访问内存,其产生SIGSEGV的流程几乎就无法追踪了(除非你用调试工具跟进)。看到这里,一个很合理的问题是:为什么程序在非法地址中执行的是随机指令,而不是非法指令呢?在一块未知的内存上执行,遇到非法指令可能性比较大吧,这样应该收到SIGILL信号啊?

[*]如果不用段寄存器的type checking,只用页表保护,传统32bit IA32可读即可执行。在NX技术出现后页级也可以控制是否可以执行。

事实并非如此,我们的IA32架构使用了如此复杂的指令集,以至于找到一条非法指令的编码还真不容易。在下例子中:

1 #include <stdio.h>

2 #include <stdlib.h>

3

4 int main() {

5 char buf[128] = "asdfaowerqoweurqwuroahfoasdbaoseur20 234123akfhasbfqower53453";

6 sleep(1);

7 }

笔者在buf中随机的敲入了一些字符,反汇编其内容得到的结果是:

0xbffa9e00: popa

0xbffa9e01: jae 0xbffa9e67

0xbffa9e03: popaw

0xbffa9e05: outsl %ds:(%esi),(%dx)

0xbffa9e06: ja 0xbffa9e6d

0xbffa9e08: jb 0xbffa9e7b

0xbffa9e0a: outsl %ds:(%esi),(%dx)

0xbffa9e0b: ja 0xbffa9e72

0xbffa9e0d: jne 0xbffa9e81

0xbffa9e0f: jno 0xbffa9e88

0xbffa9e11: jne 0xbffa9e85

0xbffa9e13: outsl %ds:(%esi),(%dx)

0xbffa9e14: popa

0xbffa9e15: push $0x73616f66

0xbffa9e1a: bound %esp,%fs:0x6f(%ecx)

0xbffa9e1e: jae 0xbffa9e85

0xbffa9e20: jne 0xbffa9e94

0xbffa9e22: xor (%eax),%dh

0xbffa9e24: and %ah,(%eax)

0xbffa9e26: and %dh,(%edx)

0xbffa9e28: xor (%ecx,%esi,1),%esi

0xbffa9e2b: xor (%ebx),%dh

0xbffa9e2d: popa

0xbffa9e2e: imul $0x61,0x68(%esi),%esp

0xbffa9e32: jae 0xbffa9e96

0xbffa9e34: data16

0xbffa9e35: jno 0xbffa9ea6

0xbffa9e37: ja 0xbffa9e9e

0xbffa9e39: jb 0xbffa9e70

0xbffa9e3b: xor 0x33(,%esi,1),%esi

0xbffa9e42: add %al,(%eax)

0xbffa9e44: add %al,(%eax)

0xbffa9e46: add %al,(%eax)

0xbffa9e48: add %al,(%eax)

0xbffa9e4a: add %al,(%eax)

0xbffa9e4c: add %al,(%eax)

0xbffa9e4e: add %al,(%eax)

0xbffa9e50: add %al,(%eax)

0xbffa9e52: add %al,(%eax)

0xbffa9e54: add %al,(%eax)

0xbffa9e56: add %al,(%eax)

0xbffa9e58: add %al,(%eax)

0xbffa9e5a: add %al,(%eax)

0xbffa9e5c: add %al,(%eax)

0xbffa9e5e: add %al,(%eax)

…………………………………………………………………………

一条非法指令都没有!大家也可以自己构造一些随机内容试试,看能得到多少非法指令。故在实际情况中,函数跳转到非法地址执行时,遇到SIGSEGV的概率是远远大于SIGILL的。

我们来构造一个遭遇SIGILL的情况,如下例:

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#define GET_EBP(ebp) \

do { \

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;

break;

}

i ++;

}

}

void overflow_ret_address () {

unsigned long ebp;

unsigned long addr = (unsigned long)buf;

int i;

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 ();

}

我们在一块全局的buf里填充了一些指令,其中有一条是ud2a,它是IA32指令集中用来构造一个非法指令陷阱。在overflow_ret_address()中,我们通过栈溢出覆盖函数的返回地址,使得函数返回时跳转到buf执行,最终执行到ud2a指令产生一个SIGILL信号。注意此例使用了ebp框架指针寄存器,在编译时不能使用-fomit-frame-pointer参数,否则得不到期望的结果。

2.8非法的系统调用参数

这是一种较为特殊的情况。特殊是指前面的例子访问非法内存都发生在用户态。而此例中,对非法内存的访问却发生在内核态。通常是执行copy_from_user()或copy_to_user()时。其流程在图2中为:

1 -à …. -à 11 -à 12 -à 13

内核使用fixup[*]的技巧来处理在处理此类错误。ULK说通常的处理是发送一个SIGSEGV信号,但实际大多数系统调用都可以返回EFAULT(bad
address)码,从而避免用户态程序被终结。这种情况就不举例了,笔者一时间想不出哪个系统调用可以模拟此种情况而不返回EFAULT错误。

[*]关于fixup的技巧可以参考笔者另一篇文章《Linker Script in Linux》,http://linux.chinaunix.net/bbs/viewthread.php?tid=1032711&extra=page%3D2%26amp%3Bfilter%3Ddigest

2.9还有什么?

我们已经总结了产生SIGSEGV的大多数情况,在实际编程中,即使现象不一样,最终发生SIGSEGV的原因都可以归到上述几类。掌握了这些基本例子,我们可以避免大多数的SIGSEGV。

转载:http://my.chinaunix.net/space-20499746-do-blog-id-832.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: