Chinaunix首页 | 论坛 | 博客
  • 博客访问: 369625
  • 博文数量: 44
  • 博客积分: 2060
  • 博客等级: 上尉
  • 技术积分: 528
  • 用 户 组: 普通用户
  • 注册时间: 2008-04-17 20:50
文章分类
文章存档

2011年(1)

2010年(28)

2008年(15)

分类:

2010-03-08 13:57:16

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

这也是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这个值,是要让我们的溢出范围足够大,大过内核为进程分配的初始栈大小(初始大小为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

  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这样的指令呢)有着密切的联系,而不像教科书中“函数返回后其使用的栈自动回收”那样简单。

阅读(5245) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~