联合、数据对齐和缓冲区溢出攻击

阅读经典——《深入理解计算机系统》05

本文讲述三个比较冷门的话题:联合、数据对齐和缓冲区溢出攻击。

  1. 联合体
  2. 数据对齐
  3. 栈帧为什么必须16字节对齐?
  4. 缓冲区溢出攻击

联合体

在C语言中有这么一个不常用的数据类型union,往往被人们遗忘。它就是联合体。

与结构体类似,都是用来封装多种数据类型,但含义不同。结构体会将各个字段按顺序分配各自独立的内存空间。而联合体则是只申请一块内存空间,由所有字段共用。听起来有些不可思议,共用一块内存空间的字段岂不是只能有一个值,那那些字段还怎么区分?下面给出一个使用联合体的经典案例。

我们想要实现一个二叉树结构,所有的内部结点具有左孩子和右孩子,但没有数据;所有的叶子结点既没有左孩子也没有右孩子,但有数据。很容易想到可以用如下的结构体来实现:

struct NODE_S {
    struct NODE_S *left;
    struct NODE_S *right;
    double data;
};

这样的话每个结点需要16字节,是不是有点浪费?因为总是有一半的空间处于无用状态,当作为内部结点时,data字段为空,当作为叶子结点时,leftright结点都为空。

这时候就可以用联合体来节约空间,原型如下:

union NODE_U {
    struct {
        union NODE_U *left;
        union NODE_U *right;
    } internal;
    double data;
};

这样的话每个结点只需要8个字节了,因为internal大小为8个字节,double大小也是8个字节,取最大值还是8个字节。对于该联合体类型的指针n,我们可以用n->internal.left来访问内部结点的左孩子,也可以用n->data来访问叶子结点。

可是,这个结果仍然不够令人满意,因为我们无法分辨出当前结点是内部结点还是叶子结点。那么我们可以增加一个枚举字段来表示结点类型:

typedef enum { N_LEAF, N_INTERNAL } nodetype_t;

struct NODE_T {
    nodetype_t type;
    union {
        struct {
            struct NODE_T *left;
            struct NODE_T *right;
        } internal;
        double data;
    } info;
};

这样一来每个结点需要12个字节了,type字段占用4个字节,info字段占用8个字节。当internaldata占用空间非常大时,该方案可以极大地降低内存消耗。

数据对齐

IA32并不要求数据对齐,但不同的平台有着额外的要求。Linux要求2字节数据类型(例如short)必须2字节对齐(意思是该数据的地址必须是2的整数倍),大于2字节的数据类型必须4字节对齐。而Windows要求K字节数据类型必须K字节对齐,除了long double要求4字节对齐。

对齐的数据有利于提高CPU的存取效率,更详细的说明见参考资料。

值得一提的是,为了对齐数据,结构体中往往会采取增加间隙的措施。例如对于如下结构体:

struct S1 {
    int i;
    char c;
    int j;
};

如果以完全紧密放置的方式保存的话,内存空间分配如下:

未对齐的数据

这导致j不满足4字节对齐的要求。因此编译器会在c后面插入一个3字节的间隙,如下所示:

对齐的数据

此时所有数据都满足了对齐要求。

栈帧为什么必须16字节对齐?

现在我们来解释上一篇文章《函数调用栈》中提出的问题,栈帧为什么必须16字节对齐。

Intel从Pentium III处理器开始推出的SSE指令集(Streaming SIMD Extensions,单指令多数据流扩展)要求操作对象为16字节对齐的数据。因此,栈帧为了支持该指令集,必须使自己16字节对齐,从而栈帧内部的数据才可能16字节对齐。否则即使数据相对于栈顶对齐,地址也不是16的整数倍。

缓冲区溢出攻击

缓冲区溢出的含义是为缓冲区提供了多于其存储容量的数据,就像往杯子里倒入了过量的水一样。通常情况下,缓冲区溢出的数据只会破坏程序数据,造成意外终止。但是如果有人精心构造溢出数据的内容,那么就有可能获得系统的控制权!例如,对于如下的简单程序:

void echo()
{
    char buf[8];
    gets(buf);
    puts(buf);
}

该函数的功能是读取输入的字符串,并输出。对应的栈帧结构如下:

联合、数据对齐和缓冲区溢出攻击_第1张图片
echo函数的栈帧结构

如果gets函数中给buf赋予了长度超过8的字符串,可以想象,这个字符串将覆盖buf上方的内容。随着字符串长度的增大,echo栈帧中的“保存的%ebx”、“保存的%ebp”,以及调用者的栈帧中的“返回地址”将被依次覆盖。大部分情况下,覆盖会使程序紊乱并出错,从而导致程序终止,但如果有人巧妙地设计覆盖的内容,就实现了所谓的缓冲区溢出攻击。

简单来讲,黑客可以把恶意代码通过buf传入内存,并恰好使返回地址指向恶意代码的起始位置。这样的话,一旦echo函数返回,程序将立即跳转到恶意代码段,如果程序具有管理员权限,恶意代码就有了任意操作整个计算机的能力,后果不堪设想。

当然,时至今日,缓冲器溢出攻击已经不能通过这种简单的方式实现了。人们在编译器、处理器中切断了缓冲区溢出攻击的必经之路。但是,道高一尺魔高一丈,黑客们总能找到系统的漏洞,让系统安全人员防不胜防。正应了那句话:没有绝对安全的系统。在参考资料提到的另一篇博文中,详细讲述了缓冲区溢出攻击的细节,并给出了简单的实现代码,感兴趣的读者可以前往阅读。

参考资料

数据对齐详解 bakari
缓冲区溢出攻击 范志东

你可能感兴趣的:(联合、数据对齐和缓冲区溢出攻击)