zoukankan      html  css  js  c++  java
  • 深入理解计算机操作系统(六)

    阅读经典——《深入理解计算机系统》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);
    }
    

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

     
    echo函数的栈帧结构

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

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

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

    参考资料

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



    作者:金戈大王
    链接:https://www.jianshu.com/p/b20c8838b929
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    8086汇编--1--通用寄存器和简单汇编指令
    写在前面的吐槽!
    汇编/Debug学习计划
    开篇-引文
    看懂别人的代码,和自己能写代码是两回事
    内存管理
    Flink基本原理及应用场景
    IDEA Maven Dependencies标红报错
    Spark Streaming流式处理
    Kafka
  • 原文地址:https://www.cnblogs.com/zzdbullet/p/9354586.html
Copyright © 2011-2022 走看看