Segmentation fault in Linux(二)

来源:互联网 发布:价税合计算法 编辑:程序博客网 时间:2024/06/10 14:11

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

这也是CU常见的一个月经贴。大部分C语言教材都会告诉你,当从一个函数返回后,该函数栈上的内容会被自动“释放”。“释放”给大多数初学者的印象是free(),似乎这块内存不存在了,于是当他访问这块应该不存在的内存时,发现一切都好,便陷入了深深的疑惑。
#include <stdio.h>#include <stdlib.h>int* foo() {    int a = 10;    return &a;}int main() {    int* b;    b = foo();    printf ("%d\n", *b);}
当你编译这个程序时,会看到“warning: function returns address of local variable”,GCC已经在警告你栈溢的可能了。实际运行结果一切正常。原因是操作系统通常以“页”的粒度来管理内存,Linux中典型的页大小为4K,内核为进程栈分配内存也是以4K为粒度的。故当栈溢的幅度小于页的大小时,不会产生SIGSEGV。那是否说栈溢出超过4K,就会产生SIGSEGV呢?看下面这个例子:
#include <stdio.h>#include <stdlib.h>char* foo() {    char buf[8192];    memset (buf, 0x55, sizeof(buf));    return buf;}int main() {    char* c;    c = foo();    printf ("%#x\n", c[5000]);}

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

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

#include <stdio.h>#include <stdlib.h>int main() {    char* c;    c = (char*)&c – 8192 *2;    *c = 'a';    printf ("%c\n", *c);}

IA32平台上,栈默认是向下增长的,我们栈溢16K,访问一块未分配的栈区域(至少从我们的程序来看,此处是未分配的)。选用16K这个值,是要让我们的溢出范围足够大,大过内核为进程分配的初始栈大小(初始大小为4K8K)。按理说,我们应该看到期望的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 16.5节“PROCEDURE CALLS FOR BLOCK-STRUCTURED LANGUAGES

#include <stdio.h>#include <stdlib.h>#define GET_ESP(esp) do {   \    asm volatile ("movl %%esp, %0\n\t" : "=m" (esp));  \}  while (0)#define K 1024int main() {    char* c;    int i = 0;    unsigned long esp;    GET_ESP (esp);    printf ("Current stack pointer is %#x\n", esp);    while (1) {        c = (char*)esp -  i * K;        *c = 'a';        GET_ESP (esp);        printf ("esp = %#x, overflow %dK\n", esp, i);        i ++;    }}

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

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

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







原创粉丝点击