Segmentation fault in Linux(二)
2017-07-19 21:32
435 查看
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这个值,是要让我们的溢出范围足够大,大过内核为进程分配的初始栈大小(初始大小为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”
#include <stdio.h> #include <stdlib.h> #define GET_ESP(esp) do { \ asm volatile ("movl %%esp, %0\n\t" : "=m" (esp)); \ } while (0) #define K 1024 int 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时,该地址已经不属于用户态栈了,所以是5 à 11 而不是 5
-à 6)
到这里,我们至少能够知道SIGSEGV和操作系统(栈的分配和回收),编译器(谁知道它会不会使用enter这样的指令呢)有着密切的联系,而不像教科书中“函数返回后其使用的栈自动回收”那样简单。
相关文章推荐
- Segmentation Fault in Linux 原因与避免(SIGSEGV)
- Segmentation fault in Linux
- Segmentation Fault in Linux 原因与避免
- About Segmentation Fault in Linux ( SIGSEGV )
- Segmentation fault in Linux
- Segmentation Fault in Linux (2.指针越界和SIGSEGV)
- Segmentation fault in Linux(一)
- About Segmentation Fault in Linux ( SIGSEGV )
- [转载]Segmentation fault in Linux
- Segmentation Fault in Linux 原因与避免
- Segmentation fault in linux
- Segmentation Fault in Linux 原因与避免
- Segmentation Fault in Linux(3.如何避免SIGSEGV)
- About Segmentation Fault in Linux ( SIGSEGV )
- Segmentation Fault in Linux 原因与避免
- Segmentation Fault in Linux 原因与避免
- Segmentation fault in Linux (1.什么是“Segmentation fault in Linux”?)
- Segmentation Fault in Linux (2.指针越界和SIGSEGV)
- Segmentation Fault in Linux (2.指针越界和SIGSEGV)
- Segmentation Fault in Linux(3.如何避免SIGSEGV)