全部博文(44)
分类:
2010-03-08 13:57:16
这也是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\n", *b);
15 }
当你编译这个程序时,会看到“warning: function returns address of local variable”,GCC已经在警告你栈溢的可能了。实际运行结果一切正常。原因是操作系统通常以“页”的粒度来管理内存,Linux中典型的页大小为4K,内核为进程栈分配内存也是以4K为粒度的。故当栈溢的幅度小于页的大小时,不会产生SIGSEGV。那是否说栈溢出超过4K,就会产生SIGSEGV呢?看下面这个例子:
1 #include
2 #include
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
2 #include
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
2 #include
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这样的指令呢)有着密切的联系,而不像教科书中“函数返回后其使用的栈自动回收”那样简单。