栈溢出导致脏数据

最近遇到一个很奇怪的指针引用问题。局部指针指向一个全局互斥锁变量,通过一层封装引用后,互斥锁unlock的时候失败。原因类型以下的这种分析情况。

记得之前看过一篇文章说,最好查的bug是语法错误,因为编译器会告诉你,最不好查的bug是栈溢出,因为啥,因为不仅编译器不会告诉你,连你自己有可能都找不到原因出在哪。

经过了一段时间的摸索,算是基本搞清楚了栈溢出的原理,写下来以防日后出现问题无从下手。

前言

开发过单片机的同学应该不陌生这个名词,一般我们也说堆栈,其实这里有两个意思:一般我们说堆栈其实指的就是帧本身,而说堆指的就是堆。这是两个不同的分区。便于理解给出一张典型的C语言在linux系统下的占区图:

这里写图片描述

可以看出,对于Linux系统下的,存储空间的分配有着较为层次清晰的分层。单片机大概也遵循这个分区架构。

二进制代码以及常量(CONST修饰)以及全局变量在最底层,存储空间最靠前的部分 
然后是堆区,堆区向上增长,我们常用到的molloc()、free()等函数操作的就是这个区,这也是芯片系统中唯一可以让程序员通过代码操作的一片存储空间 
再然后是动态链接库 
在往上(高地址)便是栈区。 最高地址一般为操作系统内核,用户无法访问

了解了这个之后我们开始详解何为栈、栈为什么会溢出以及在代码级如何预防栈溢出,最后说一下栈溢出攻击的事情。

那么什么是栈呢

在计算机中,栈可以理解为一个特殊的容器,用户可以将数据依次放入栈中,然后再将数据按照相反的顺序从栈中取出。也就是说,先放入的数据最后才能取出,而最后放入的数据必须先取出。这称为先进后出(First In Last Out)原则。

放入数据常称为入栈或压栈(Push),取出数据常称为出栈或弹出(Pop)。

这里写图片描述

可以发现,栈底始终不动,出栈入栈只是在移动栈顶,当栈中没有数据时,栈顶和栈底重合。

这里需要注意标识栈顶和栈底的两个寄存器: ebp寄存器指向栈底,esp寄存器指向栈顶。从本质上来讲,栈是一段连续的内存,需要同时记录栈底和栈顶,才能对当前的栈进行定位。

栈溢出是怎么回事

了解了栈实际上也是一块内存后,栈溢出就好理解了。

当我们定义的数据所需要占用的内存超过了栈的大小时,就会发生栈溢出。编译器会报栈溢出错误。

如一块芯片的内存RAM大小为4k,当我们定义了一个大数组,如下:

int buf[1024*5] = {0};
  •  

很明显定义的数组超过了内存大小,这就导致了栈溢出。

预防栈溢出需要我们在编程时了解内存使用,尽可能不要定义特别大的数组,尽可能不要定义特别复杂的函数,如多个形参等。

函数调用栈

定义的数组会占用栈空间,同样,定义的函数也会占用栈空间,一个简单的例子便是函数的入栈和出栈。

举个例子:

void func(int a, int b)
{
    int p =12, q = 345;
}
int main()
{
    func(90, 26);
    return 0;
}
  •  

函数的进栈出栈过程如下图所示: 
这里写图片描述

函数进栈

1) main() 是主函数,也需要进栈,如步骤①所示。

2) 在步骤②中,执行语句func(90, 26);,先将实参 90、26 压入栈中,再将返回地址压入栈中,这些工作都由 main() 函数(调用方)完成。这个时候 ebp 的值并没有变,仅仅是改变 esp 的指向。

3) 到了步骤③,就开始执行 func() 的函数体了。首先将原来 ebp 寄存器的值压入栈中(也即图中的 old ebp),并将 esp 的值赋给 ebp,这样 ebp 就从 main() 函数的栈底指向了 func() 函数的栈底,完成了函数栈的切换。由于此时 esp 和ebp 的值相等,所以它们也就指向了同一个位置。

4) 为局部变量、返回值等预留足够的内存,如步骤④所示。由于栈内存在函数调用之前就已经分配好了,所以这里并不是真的分配内存,而是将 esp 的值减去一个整数,例如 esp - 0XC0,就是预留 0XC0 字节的内存。

5) 将 ebp、esi、edi 寄存器的值依次压入栈中。

6) 将局部变量的值放入预留好的内存中。

至此,func() 函数的活动记录就构造完成了。可以发现,在函数的实际调用过程中,形参是不存在的,不会占用内存空间,内存中只有实参,而且是在执行函数体代码之前、由调用方压入栈中的。

未初始化的局部变量的值为什么是垃圾值

为局部变量分配内存时,仅仅是将 esp 的值减去一个整数,预留出足够的空白内存,不同的编译器在不同的模式下会对这片空白内存进行不同的处理,可能会初始化为一个固定的值,也可能不进行初始化。

函数出栈

步骤⑦到⑨是函数 func() 出栈过程: 
7) 函数 func() 执行完成后开始出栈,首先将 edi、esi、ebx 寄存器的值出栈。

8) 将局部变量、返回值等数据出栈时,直接将 ebp 的值赋给 esp,这样 ebp 和 esp 就指向了同一个位置。

9) 接下来将 old ebp 出栈,并赋值给现在的 ebp,此时 ebp 就指向了 func() 调用之前的位置,即 main() 活动记录的 old ebp 位置,如步骤⑨所示。

这一步很关键,保证了还原到函数调用之前的情况,这也是每次调用函数时都必须将 old ebp 压入栈中的原因。

最后根据返回地址找到下一条指令的位置,并将返回地址和实参都出栈,此时 esp 就指向了 main() 活动记录的栈顶, 这意味着 func() 完全出栈了,栈被还原到了 func() 被调用之前的情况。

函数执行完局部变量的值真的不存在了?

经过上面的分析可以发现,函数出栈只是在增加 esp 寄存器的值,使它指向上一个数据,并没有销毁之前的数据。

栈上的数据只有在后续函数继续入栈时才能被覆盖掉,这就意味着,只要时机合适,在函数外部依然能够取得局部变量的值。请看下面的代码:

#include <stdio.h>
int *p;
void func(int m, int n)
{
    int a = 18, b = 100;
    p = &a;
}
int main()
{
    int n;
    func(10, 20);
    n = *p;
    printf("n = %d\n", n);
    return 0;
}
  •  

运行结果:

n = 18

在 func() 中,将局部变量 a 的地址赋给 p,在 main() 函数中调用 func(),函数刚刚调用结束,还没有其他函数入栈,局部变量 a 所在的内存没有被覆盖掉,所以通过语句n = *p;能够取得它的值。

通常出现栈溢出的情况

一、局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。

二、递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。

三、指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。

core.dump生成方法

1)编译(加上 -g )、执行程序,发现同级目录并没有生产 core 文件:

2)core文件的生成跟你当前系统的环境设置有关系,可以用下面的语句设置让其生成 core 文件:ulimit -c unlimited。core 文件生成的位置一般于运行程序的路径相同,在ubuntu下文件名一般为core。

3)用gdb来调试core文件:gdb a.out core

猜你喜欢

转载自blog.csdn.net/qinglongzhan/article/details/88908916
今日推荐