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

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这样的指令呢)有着密切的联系,而不像教科书中“函数返回后其使用的栈自动回收”那样简单。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: