精华内容
下载资源
问答
  • 什么是缓冲区(buffer),什么是缓存(cache)

    千次阅读 多人点赞 2020-11-17 22:31:03
    为什么要引入缓冲区 我们为什么要引入缓冲区呢? 比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再...

    缓冲区

    缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区

    为什么要引入缓冲区

    我们为什么要引入缓冲区呢?

    比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度

    又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。现在您基本明白了吧,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

    缓冲区的类型

    缓冲区 分为三种类型:全缓冲、行缓冲和不带缓冲。

    1、全缓冲

    在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写

    2、行缓冲

    在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据

    3、不带缓冲

    也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。

    缓冲区的刷新

    下列情况会引发缓冲区的刷新:

    1、缓冲区时;

    2、执行flush语句;

    3、执行endl语句;

    4、关闭文件。

    可见,缓冲区满或关闭文件时都会刷新缓冲区,进行真正的I/O操作。另外,在C++中,我们可以使用flush函数来刷新缓冲区(执行I/O操作并清空缓冲区),如:cout << flush; //将显存的内容立即输出到显示器上进行显示。刷新字面上的意思是用刷子刷,把原来旧的东西变新了,这里就是改变的意思,例如像缓冲区溢出的时候,多余出来的数据会直接将之前的数据覆盖,这样缓冲区里的数据就发生了改变。

    endl控制符的作用是将光标移动到输出设备中下一行开头处,并且清空缓冲区

    cout < < endl;

    相当于

    cout < < ”\n”< < flush;

    通过实例演示说明

    1、文件操作演示全缓冲

    创建一个控制台工程,输入如下代码:

    #include<fstream>
    using namespace std;
     
    int main()
     
    {
        //创建文件test.txt并打开
        ofstream outfile("test.txt");
        //向test.txt文件中写入4096个字符’a’
        for(int n=0;n< 4096;n++)
        {
            outfile << 'a';
        }
        //暂停,按任意键继续
        system("PAUSE");
        //继续向test.txt文件中写入字符’b’,也就是说,第4097个字符是’b’
        outfile << 'b';
        //暂停,按任意键继续
        system("PAUSE");
        return 0;
    }

    上面这段代码很容易理解,已经在代码内部作了注释。

    编写这段小代码的目的是验证WindowsXP下全缓冲的大小是4096个字节,并验证缓冲区满后会刷新缓冲区,执行真正的I/O操作。

    编译并执行,运行结果如下:

     

    此时打开工程所在文件夹下的test.txt文件,您会发现该文件是空的,这说明4096个字符“a”还在缓冲区,并没有真正执行I/O操作。敲一下回车键,窗口变为如下:

     

    此时再打开test.txt文件,您就会发下该文件中已经有了4096个字符“a”。这说明全缓冲区的大小是4K(4096),缓冲区满后执行了I/O操作,而字符“b”还在缓冲区。
    再次敲一下回车键,窗口变为如下:

     

    此时再打开test.txt文件,您就会发现字符“b”也在其中了。这一步验证了文件关闭时刷新了缓冲区。

    2、键盘操作演示行缓冲

    先介绍getchar()函数。

    函数原型:int getchar(void);

    说明:当程序调用getchar()函数时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar()函数才开始从键盘缓冲区中每次读入一个字符。也就是说,后续的getchar()函数调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才重新等待用户按键。

    不知道您明白了没有,再通俗一点讲,当程序调用getchar()函数时,程序就等着用户按键,并等用户按下回车键返回。期间按下的字符存放在缓冲区,第一个字符作为函数返回值。继续调用getchar()函数,将不再等用户按键,而是返回您刚才输入的第2个字符;继续调用,返回第3个字符,直到缓冲区中的字符读完后,才等待用户按键。

    如果您还没有明白,只能怨我表达能力有限,您可以结合以下实例体会。

    创建一个控制台工程,输入如下代码:

    #include <iostream>
    using namespace std;
    
    int main()
    {
        char c;
        //第一次调用getchar()函数
        //程序执行时,您可以输入一串字符并按下回车键,按下回车键后该函数才返回
        c=getchar();
        //显示getchar()函数的返回值
        cout << c << endl;
        //暂停
        system("PAUSE");
        //循环多次调用getchar()函数
        //将每次调用getchar()函数的返回值显示出来
        //直到遇到回车符才结束
        while((c=getchar())!='\n')
        {
            printf("%c",c);
        }
        //暂停
        system("PAUSE");
        return 0;
    }

    这段小代码也很简单,同样在代码内部都有注释。

    getchar()函数的执行就是采用了行缓冲。第一次调用getchar()函数,会让程序使用者(用户)输入一行字符并直至按下回车键 函数才返回。此时用户输入的字符和回车符都存放在行缓冲区。

    再次调用getchar()函数,会逐步输出行缓冲区的内容。

    好了,本人表达能力有限,还是编译运行程序,通过运行结果自己领会吧。

    编译运行程序,会提示您输入字符,您可以交替按下一些字符,如下:

     

    您一直按下去,您就会发现当您按到第4094个字符时,不允许您继续输入字符。这说明行缓冲区的大小也是4K。

    此时您按下回车键,返回第一个字符’a’,如下图:

     

    继续敲一下回车键,将缓冲区的其它的字符全部输出,如下图:

     

    3、标准错误输出不带缓冲

    如错误输出时使用:

    cerr<<”错误,请检查输入的参数!”;

    这条语句等效于:

    fprintf(stderr, ”错误,请检查输入的参数!”);

    缓存(cache)

    cache是一个非常大的概念。

    一、CPU的Cache

    CPU的Cache,它中文名称是高速缓冲存储器读写速度很快,几乎与CPU一样。由于CPU的运算速度太快,内存的数据存取速度无法跟上CPU的速度,所以在cpu与内存间设置了cache为cpu的数据快取区。当计算机执行程序时,数据与地址管理部件会预测可能要用到的数据和指令,并将这些数据和指令预先从内存中读出送到Cache。一旦需要时,先检查Cache,若有就从Cache中读取,若无再访问内存,现在的CPU还有一级cache,二级cache。简单来说,Cache就是用来解决CPU与内存之间速度不匹配的问题,避免内存与辅助内存频繁存取数据,这样就提高了系统的执行效率。

    二、磁盘的Cache
    磁盘也有cache,硬盘的cache作用就类似于CPU的cache,它解决了总线接口的高速需求和读写硬盘的矛盾以及对某些扇区的反复读取

    三、浏览器的Cache

    浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览,并且可以减少服务器的压力。这个过程与下载非常类似,不过下载是用户的主动过程,并且下载的数据一般是长时间保存,游览器的缓存的数据只是短时间保存,可以人为的清空

    四、Cache的大小

    同样cache也有大小,例如现在市面上购买的CPU的cache越大,级数越多,CPU的访问速度越快。cache在很多方面都有应用,就不一一列举了。

    缓存(cache)与缓冲(buffer)的主要区别

    Buffer的核心作用是用来缓冲,缓和冲击。比如你每秒要写100次硬盘,对系统冲击很大,浪费了大量时间在忙着处理开始写和结束写这两件事嘛。用个buffer暂存起来,变成每10秒写一次硬盘,对系统的冲击就很小,写入效率高了,日子过得爽了。极大缓和了冲击。

    Cache的核心作用是加快取用的速度。比如你一个很复杂的计算做完了,下次还要用结果,就把结果放手边一个好拿的地方存着,下次不用再算了。加快了数据取用的速度。

    简单来说就是buffer偏重于写,而cache偏重于读。

    有时候大家要好好理解这些专有名词字面上的意思,对理解这些概念有好处,缓冲:缓解冲击,缓存:临时存储

    展开全文
  • Java NIO是jdk1.4引入的,官方给出的定义是NEW IO,是一种新的IO,也可以理解为no-block io(非阻塞io),NIO的出现和bio有相同的作用和目的,都是为了数据的输入和输出,但是方式有所不同,BIO是基于流的,而NIO是...

     一、NIO缓冲区

    引言:Java NIO 是jdk1.4引入的,官方给出的定义是NEW IO ,是一种新的IO ,也可以理解为no-block io(非阻塞io),NIO 的出现和bio有相同的作用和目的,都是为了数据的输入和输出,但是方式有所不同,BIO是基于流的,而NIO 是基于通道和缓冲区的,nio具有更高的效率。

    Bio和Nio的对比

     通道和缓冲区  Channel 负责传输, Buffer 负责存储

    Nio的两个核心:通道和缓冲区,缓冲区用于存储数据通道,通道用于连接数据源,将缓冲区从一端运输到另一端,完成数据的传输。

    缓冲区(Buffer):一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类。NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。

    Buffer 就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean 除外) ,有以下 Buffer 常用子类:

    • ByteBuffer
    • CharBuffer
    • ShortBuffer
    •  IntBuffer
    • LongBuffer
    • FloatBuffer
    • DoubleBuffer

    上述 Buffer 类 他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个 Buffer 
    对象:static XxxBuffer allocate(int capacity) : 创建一个容量为 capacity 的 XxxBuffer 对象。

    ByteBuffer是最常用的一种类型的缓冲区,因为其他几种类型都可以转化为byte类型。

    缓冲区的几个属性: 

     capacity、limit、position,mark 的关系为 :0 <= mark <= position <= limit <= capacity。

    测试一下上面的属性申请一个10个字节的缓冲区。

     

    • 容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。就是在缓冲区allocate时申请的容量。
    • 限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。说白了就是存入数据的长度。
    • 位置 (position):下一个要读取或写入的数据的索引。就是指向现在正在操作的位置的后一个位置。缓冲区的位置不能为负,并且不能大于其限制。
    • 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position。
    public class Demo1 {
    	public static void main(String[] args) {
    		ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    		System.out.println("----------------allocate------------");
    		System.out.println("capacity: "+byteBuffer.capacity());
    		System.out.println("limit: "+byteBuffer.limit());
    		System.out.println("position: "+byteBuffer.position());
    	}
    }

    结果

    ----------------allocate------------
    capacity: 10
    limit: 10
    position: 0

     此时缓冲区为:

    此时的 capacity=limit,因为缓冲区没有数据所一以position指向0位置。

     

    添加代码往缓冲区放入数据使用 put方法

            //往缓存中存入数据
    		System.out.println("----------------put------------");
    		byteBuffer.put("abcde".getBytes());
    		System.out.println("capacity: "+byteBuffer.capacity());
    		System.out.println("limit: "+byteBuffer.limit());
    		System.out.println("position: "+byteBuffer.position());

    此时又输出了

    ----------------put------------
    capacity: 10
    limit: 10
    position: 5

    此时的缓冲区:

     

     

    在写模式下position指向了正在操作的后一个位置,也是我们即将操作的位置。 

     

    切换缓冲区为读模式:使用Buffer.flip()方法。

    		//切换到读mo模式
    		System.out.println("----------------flip------------");
    		byteBuffer.flip();
    		System.out.println("capacity: "+byteBuffer.capacity());
    		System.out.println("limit: "+byteBuffer.limit());
    		System.out.println("position: "+byteBuffer.position());

    打印结果:

    ----------------flip------------
    capacity: 10
    limit: 5
    position: 0

     此时缓冲区:

     

     此时position指向0位置,因为要读数据肯定是从第一个字节位置开始读。

     

    //取数据 
    		System.out.println("----------------get------------");
    		byte[] b = new byte[byteBuffer.limit()];
    		byteBuffer.get(b, 0, byteBuffer.limit());
    		System.out.println("capacity: "+byteBuffer.capacity());
    		System.out.println("limit: "+byteBuffer.limit());
    		System.out.println("position: "+byteBuffer.position());
    		System.out.println(new String(b));

    结果: 

    ----------------get------------
    capacity: 10
    limit: 5
    position: 5
    abcde

    此时的 

     

    此时position处于limit处,从这里开始后面是不能操作的区域。

    清空缓冲区(数据只是被遗忘并没有被真正的清除,position指向0索引处,再次put数据会将原来的数据覆盖)

    		byteBuffer.clear();
    		System.out.println("----------------clear------------");
    		System.out.println("capacity: "+byteBuffer.capacity());
    		System.out.println("limit: "+byteBuffer.limit());
    		System.out.println("position: "+byteBuffer.position());

    结果:

    ----------------clear------------
    capacity: 10
    limit: 10
    position: 0

     此时缓冲区为:

     

    只是将position指向索引0处,limit指向capacity。看一下源码

        /**
         * Clears this buffer.  The position is set to zero, the limit is set to
         * the capacity, and the mark is discarded.
         *
         * <p> Invoke this method before using a sequence of channel-read or
         * <i>put</i> operations to fill this buffer.  For example:
         *
         * <blockquote><pre>
         * buf.clear();     // Prepare buffer for reading
         * in.read(buf);    // Read data</pre></blockquote>
         *
         * <p> This method does not actually erase the data in the buffer, but it
         * is named as if it did because it will most often be used in situations
         * in which that might as well be the case. </p>
         *
         * @return  This buffer
         */
        public final Buffer clear() {
            position = 0;
            limit = capacity;
            mark = -1;
            return this;
        }

    可以看出源码也是只是将几个属性所指向的位置变化,并没有对缓冲区的的内容进行任何操作。

     Buffer常用的方法:

     

    Buffer 所有子类提供了两个用于数据操作的方法:get() 
    与 put() 方法
    获取 Buffer 中的数据

    • get() :读取单个字节
    • get(byte[] dst):批量读取多个字节到 dst 中
    • get(int index):读取指定索引位置的字节(不会移动 position)

     放入数据到 Buffer 中

    • put(byte b):将给定单个字节写入缓冲区的当前位置
    • put(byte[] src):将 src 中的字节写入缓冲区的当前位置
    • put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position) 

    二、缓冲区的分类:非直接缓冲区和直接缓冲区别

    在了解直接缓冲区和非直接缓冲区之前先说几个概念:

    Java虚拟机分配的内存是物理内存,不是虚拟内存。 

    先看物理内存和虚拟内存的概念:

    物理内存:物理内存(Physical memory)是相对于虚拟内存而言的。物理内存指通过物理内存条而获得的内存空间。

    虚拟内存 : 虚拟内存则是指将硬盘的一块区域划分来作为内存。

    内核地址空间和用户地址空间区别和联系:操作系统和驱动程序运行在内核空间,应用程序运行在用户空间在电脑开机之前,内存就是一块原始的物理内存。什么也没有。开机加电,系统启动后,就对物理内存进行了划分。当然,这是系统的规定,物理内存条上并没有划分好的地址和空间范围。这些划分都是操作系统在逻辑上的划分。不同版本的操作系统划分的结果都是不一样的。为什么要划分用户空间和系统空间呢?当然是有必要的。操作系统的数据都是存放于系统空间的,用户进程的数据是存放于用户空间的。这是第一点,不同的身份,数据放置的位置必然不一样,否则大混战就会导致系统的数据和用户的数据混在一起,系统就不能很好的运行了。分开来存放,就让系统的数据和用户的数据互不干扰,保证系统的稳定性。分开存放,管理上很方便,而更重要的是,将用户的数据和系统的数据隔离开,就可以对两部分的数据的访问进行控制。这样就可以确保用户程序不能随便操作系统的数据,这样防止用户程序误操作或者是恶意破坏系统。处于用户态的程序只能访问用户空间,而处于内核态的程序可以访问用户空间和内核空间。

    非直接缓冲区:我们之前说过NIO通过通道连接磁盘文件与应用程序,通过缓冲区存取数据进行双向的数据传输。物理磁盘的存取是操作系统进行管理的,与物理磁盘的数据操作需要经过内核地址空间;而我们的Java应用程序是通过JVM分配的内存空间,属于应用程序的内存空间。数据需要在内核地址空间和用户地址空间,在操作系统和JVM之间进行数据的来回拷贝,无形中增加的中间环节使得效率与后面要提的之间缓冲区相比偏低。

    读操作:当有数据读取的时候,os系统现将数据先读入内核地址空间中,然后将内核空间的数据复制一份到用户地址空间中,然后再读入用户程序。

    写操作:当有数据要写入的时候,现将数据通过管道写入用户地址空间中,然后将用户地址空间的数据复制一份到内核地址空间中,然后再由os写入磁盘。(复制到内核地址空间后数据什么时候写入磁盘,不是程序所决定的)

     

     

    直接缓冲区:直接缓冲区则不再通过内核地址空间和用户地址空间的缓存数据的复制传递,而是在物理内存中申请了一块空间,这块空间映射到应用程序和物理磁盘,不再经过内核地址空间和用户地址空间,应用程序与磁盘之间的数据存取之间通过这块直接申请的物理内存进行,起到了中间媒介的作用

    可以通过 FileChannel 的 map() 方法获得:直接字节缓冲区可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 java中提供了3种内存映射模式,即:只读(readonly)、读写(read_write)、专用(private) ,对于只读模式来说,如果程序试图进行写操作,则会抛出ReadOnlyBufferException异常。 第二种的读写模式表明了通过内存映射文件的方式写或修改文件内容的话是会立刻反映到磁盘文件中去的。 专用(private)模式采用的是OS的“写时拷贝”原则,即在没有发生写操作的情况下,多个进程之间都是共享文件的同一块物理内存(进程各自的虚拟地址指向同一片物理地址),一旦某个进程进行写操作,那么将会将要写的物理内存中的文件数据单独拷贝一份到进程的私有缓冲区中,然后在私有缓冲区中对文件进行操作,不会反映到物理文件中去,只是改变的进程内存空间的副本。

    allocateDirect() 工厂方法来创建:直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。

    使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,在垃圾回收时并不能用程序区控制堆外内存的回收,因为不属于虚拟机,只能是垃圾回收机制按需对堆内的DirectByteBuffer对象进行回收,回收后将与直接缓存区失去联系,也就意味着直接缓冲区被回收。

    在直接缓冲区谨慎使用原因:

    (1)不安全;

    (2)消耗更多,因为它不是在JVM中直接开辟空间。这部分内存的回收只能依赖于垃圾回收机制,垃圾什么时候回收不受我们控制;

    (3)数据写入物理内存缓冲区中,程序就失去了对这些数据的管理,即什么时候这些数据被最终写入从磁盘只能由操作系统来决定,应用程序无法再干涉。

    (4)堆外空间分配比较耗时。

     

    最后看一下jdk对于缓冲区和非缓冲区的说明:

    直接 非直接缓冲区

     

    字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。

    直接字节缓冲区可以通过调用此类的 allocateDirect 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。

    直接字节缓冲区还可以通过 mapping 将文件区域直接映射到内存中来创建。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。

    字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。

     

    展开全文
  • 前面发了两篇都是关于C语言缓冲区溢出的文章,有的同行问,是否C#、Java语言有缓冲区溢出问题吗?答案是否定的。由于C#、Java语言需要虚拟机去执行,属于托管语言,虚拟机会自动检查边界。一般不会出现缓冲区溢出。...

    前面发了两篇都是关于C语言缓冲区溢出的文章,有的同行问,是否C#、Java语言有缓冲区溢出问题吗?答案是否定的。由于C#、Java语言需要虚拟机去执行,属于托管语言,虚拟机会自动检查边界。一般不会出现缓冲区溢出。但是通过JNI调用本机代码、以及JVM/CLR/ETC等虚拟机可能出现这些问题。所以缓冲区溢出最突出的语言是C和C++。

    下面我们再深一步了解缓冲区溢出的原理。

    缓冲区是一块连续的计算机中的内存区域,可保存相同数据类型的多个实例,例如代码段、数据段、堆和栈。栈存放函数变量和实参、堆是提供动态申请内存、静态数据区存放全局或静态数据。C/C++语言中,通常使用字符数组和malloc/new之类内存分配函数申请缓冲区。缓冲区溢出是指数据被添加到分配给该缓冲区的内存块之外。缓冲区溢出是最常见的程序缺陷,严重程序缺陷导致可能被攻击者利用,成为安全漏洞。

    按照冯·诺依曼存储程序原理,程序代码是作为二进制数据存储在内存的,同样程序的数据也在内存中,因此直接从内存的二进制形式上是无法区分哪些是数据哪些是代码的,这也为缓冲区溢出攻击提供了可能。 恶意攻击者利用缓冲区溢出攻击的最终目的就是希望系统能执行这块可读写内存中已经被蓄意设定好的恶意代码。

    栈帧结构的引入为高级语言中实现函数或过程调用提供直接的硬件支持,但由于将函数返回地址保存在程序员可见的堆栈中,这样给系统安全带来隐患。攻击者可以将函数返回地址修改为指向一段精心安排的恶意代码,则可达到危害系统的目的。此外,堆栈的正确恢复依赖于压栈的EBP值的正确性,但EBP域邻近局部变量,若编程中有意无意地通过局部变量的地址偏移窜改EBP值,则程序的行为将变得非常危险。

    栈存取一般是按照向下增长的方式存取的,例如push 0x0021fd67(0x0021fd67是一个变量的值)到栈,按照如图进入栈空间,高字节在高位,低字节在低位,这种方式就是所谓的“大端”方式。为什么栈是这种存取方式呢?其实原因就是当初设计计算机内存如何高效使用时,堆和栈公用相同的空间,最大利用方式是堆从某一个地址增长,而栈从某一个高地址减小,这样向中间增长,保证堆和栈都能先申请先利用,直到最后没有空间(当然这时候发生堆栈溢出,程序会崩溃),来保证最大的内存利用率。

     

    这里补充一下大端和小端的知识。大端/小端就是Big-Endian/Little-Endian问题。大端:高位字节存在高地址上,低位字节存在低地址上
    小端:低位字节存在高地址上,高位字节存在低地址上 
    有两种常见的方法来判断是大端还是小端,下面代码来自于网络。
    方法一:使用指针
     

    int x=1;
    if(*(char*)&x==1)
        printf("little-endian\n");
    else
        printf("big-endian\n");

    方法二:使用联合
     

    union{
        int i;
        char c;
    }x;
    x.i=1;
    if(x.c==1)
        printf("little-endian\n");
    else
        printf("big-endian\n");

    由于C/C++语言没有数组越界检查机制,当向局部数组缓冲区里写入的数据超过为其分配的大小时,就会发生缓冲区溢出。攻击者可利用缓冲区溢出来窜改进程运行时栈,从而改变程序正常流向,轻则导致程序崩溃,重则系统特权被窃取。

     除通过使堆栈缓冲区溢出而更改返回地址外,还可改写局部变量(尤其函数指针)以利用缓冲区溢出缺陷。

    缓冲区溢出有常见3种:

    (1)激活记录(Activation Records)

    这是一种比较常见的溢出方式,每当调用一个函数时,在堆栈中留下一个激活记录,它包含了函数结束时返回的地址。攻击者通过溢出这些自动变量,使这个返回地址指向攻击代码。通过改变程序的返回地址,当函数调用结束时,程序就跳转到攻击者设定的地址,而不是原先的地址。

    (2)函数指针(Function Pointers)

    在C语言中,void(*foo)()声明了一个返回值为void函数指针的变量foo。函数指针可以用来定位任何地址空间,所以攻击者只需在任何空间内的函数指针附近找到一个能够溢出的缓冲区,然后溢出这个缓冲区来改变函数指针。在某一时刻,当程序通过函数指针调用函数时,程序的流程就按攻击者的意图实现转移。

    (3)长跳转缓冲区(Longjmp buffers)

    在C语言中包含了一个简单的检验/恢复系统,即setjmp/longjmp,其作用是在检验点设定setjmp(buffer),而用longjmp(buffer)来恢复检验点。但是如果攻击者能够进入缓冲区的空间,longjmp(buffer)实际是跳转到攻击者的代码。和函数指针一样,longjmp缓冲区能够指向任何地方,所以攻击者所要做的就是找到一个可供溢出的缓冲区。

    在进行溢出缓冲区攻击时,常常需要在一个字符串里综合了代码植入和激活记录。攻击者需要定位一个可供溢出的自动变量,然后向程序传递一个很大的字符串,在引发缓冲区溢出改变激活记录的同时植入代码。代码植入和缓冲区溢出不一定要在一次动作内完成。攻击者可以在一个缓冲区内放置代码,这时不能溢出缓冲区。然后,攻击者通过溢出另外一个缓冲区来转移程序的指针。这种方法一般用来解决可供溢出的缓冲区不够大的情况。

    如果攻击者想使用已经驻留的代码而不是从外部植入代码,就必须先把代码参数化。如在libc中的部分代码段会执行exec(some),其中some就是参数。攻击者使用缓冲区溢出改变程序的参数,再利用另一个缓冲区溢出使程序指针指向libc中特定的代码段

    下面举个例子:

    // wshell.cpp : 定义控制台应用程序的入口点。

    //

     

    #include "stdafx.h"

    int fun(void)

    {

        int a=10,*p=NULL;

        a=20;

        p=(int*)((char *)&a + 16);

        *p+=16;

        return a;

    }

    int _tmain(int argc, _TCHAR* argv[])

    {

        int b=11;

        b = fun();

        printf("printf aaa\n");

        printf("printf bbb\n");

        return 0;

    }

    通过VS2012编辑编译、在调试模式下,可以查看到汇编代码如下:

     

    ESP扩展栈指针寄存器,存放的是函数栈顶顶指针。fun函数调用返回地址是0x0021149A,而fun函数执行完成后,返回地址是调用函数指令后跟的指令地址,要跳转到0x002114AA地址,两条指令之间相差16。变量a的地址是0x004bf9b8,fun函数返回地址存放在0x004bf9b8+16=0x004bf9C8中,所以在fun函数中,要取变量a地址,相加16之后,得到返回地址。

    由于Vistual  C++编译后会检查指针越界等缺陷,所以会抛出异常,可以忽略掉异常,继续执行,只打你出printf bbb字符串。也就是第一个字符串通过fun函数地址跳转,跳转到第二个打印语句。

     

     

    如何防范缓冲区溢出呢?

    防范缓冲区溢出问题的准则是:

    确保对输入数据做边界检查。相对于系统安全,不要担心因为添加检查而影响程序效率。不要为接收数据预留相对过小的缓冲区,大量数据处理应通过malloc/new分配堆空间来解决;

    在将数据读入或复制到目标缓冲区前,检查数据长度是否超过缓冲区空间。

    检查以确保不会将过大的数据传递给别的程序,尤其是第三方商用软件库——不要设想关于其他人软件行为的任何事情。因为很多商用库也是程序员编写的,早期对于风险防范关注较少,难免出现各种漏洞。

    若有可能,改用具备防止缓冲区溢出内置机制的高级语言(Java、C#等)。但许多语言依赖于C库,或具有关闭该保护特性的机制(为速度而牺牲安全性)。

    可以借助某些底层系统机制或检测工具(如对C数组进行边界检查的编译器)。许多操作系统(包括Linux和Solaris)提供非可执行堆栈补丁,但该方式不适于这种情况:攻击者利用堆栈溢出使程序跳转到放置在堆上的执行代码。此外,存在一些侦测和去除缓冲区溢出漏洞的静态分析工具(例如北大CoBOT、Checkmarx等)和动态工具,甚至采用grep命令自动搜索源代码中每个有问题函数的实例。

    但是即使采用上面这些保护手段,程序员自身也可能犯其他许多错误,导致引入缺陷。例如,当使用有符号数存储缓冲区长度或某个待读取内容长度时,攻击者可将其变为负值,从而使该长度被解释为很大的正值。经验丰富的程序员还容易过于自信地使用某些危险的库函数,如对其添加自己总结编写的检查,或错误地推论出使用的具有潜在危险的函数在某些特殊情况下是"安全"的。

     

    关注安全,关注作者

    展开全文
  • 对于引入缓冲的原因,缓冲的类型,缓冲的组成,缓冲池的操作及工作流程进行了介绍!

    凡数据到达速度和离去速度不匹配的地方都可以采用缓冲技术!

    一、操作系统中引入缓冲的主要原因:

    ——缓和CPU与I/O设备间速度不匹配的矛盾。

    ——减少对CPU的中断频率,放宽对中断响应时间的限制。

    ——提高CPU和I/O设备的并行性。


    二、缓冲的类型:

    按照缓冲区存在的位置分类:

    按照缓冲区的个数以及缓冲区的组织形式分类:


    三、缓冲池的组成及操作:

    ——在缓冲池中存在由三类缓冲区组成的三条队列:空缓冲队列emq 、输入队列inq 、输出队列outq。

    ——系统(或者用户进程)可以从这三种队列中申请和取出缓冲区,用得到的缓冲区进行存数、取数操作,在存数、取数操作完成之后,再将缓冲区挂到相应的队列,这些缓冲区被称为工作缓冲区

    ——缓冲池中有四种工作缓冲区

    • 用于收容设备输入数据的收容输入缓冲区hin。
    • 用于提取设备输入数据的提取输入缓冲区sin。
    • 用于收容CPU输出数据的收容输出缓冲区hout。
    • 用于提取CPU输出数据的提取输出缓冲区sout。
    缓冲区的工作缓冲区

    缓冲池的操作:

    • 从指定缓冲区队列中取出一个缓冲区的过程:Take_buf(type)
    • 把缓冲区插入到相应的缓冲区队列的过程:Add_buf(type,  buf)
    • 进程申请一个指定类型缓冲区的过程:Get_buf(type)
    • 进程将工作缓冲区插入到相应缓冲区队列的过程:Put_buf(type, work_buf)

    ——type表示缓冲队列的类型。

    ——work_buf表示工作缓冲区类型。

     

    在看到这里的时候,可能会奇怪,上面的几个函数的功能有些许重叠!这是为什么呢?

     

    因为缓冲池中的队列本身是一种临界资源,当多个进程在访问同一个队列时,既应当互斥,又需同步。所以不能直接使用Take_buf和Add_buf对缓冲池中的队列进行操作,而是使用对这两个过程改造加工之后的Get_buf和Put_buf。

     

    Get_buf和Put_buf的构建:

    为了使得各个进程能互斥的访问缓冲池队列,可以为每一队列设置一个互斥信号量MS(type),初始值为1。此时,为了保证各个进程同步地使用缓冲区,为每一个缓冲队列设置一个资源信号量RS(type)初始值为 n (n为type队列长度)。

    缓冲池的工作过程:

    ——收容输入:

    • 在输入进程需要输入数据时,调用Get_buf(emq)过程,从空缓冲队列emq的队首摘下一空缓冲,把它作为收容输入工作缓冲区hin,把数据输入其中,装满后再调用Put_buf(inq, hin)过程,将该缓冲区挂在输入队列inq上。

    ——提取输入:

    • 当计算进程需要输入数据时,调用Get_buf(inq)过程,从输入队列inq的队首取得一缓冲区作为提取输入工作缓冲区sin,计算进程从中提取数据。计算进程用完该数据后,再调用Put_buf(emq, sin)过程,将该缓冲区挂到空缓冲队列emq上。

    ——收容输出:

    • 当计算进程需要输出时,调用Get_buf(emq)过程,从空缓冲队列emq的队首取得一空缓冲作为收容输出工作缓冲区hout。当其中装满输出数据后,又调用Put_buf(outq, hout)过程,将该缓冲区挂在输出队列outq末尾。

    ——提取输出:

    • 输出进程调用Get_buf(outq)过程,从输出队列的队首取得一装满输出数据的缓冲区作为提取输出工作缓冲区sout。在数据提取完后,再调用Put_buf(emq, sout)过程,将该缓冲区挂在空缓冲队列末尾

     

    Ending... ...

    展开全文
  • 基于堆栈的缓冲区溢出问题记录

    千次阅读 2019-09-25 18:52:48
    缓冲区溢出问题本质其实就是复制一个内存区域的内容到另一个内存区域,而目标内存区域容量太小无法容纳 函数A在调用函数B时,A的函数地址会存储在堆栈中,若函数B中的代码存在堆栈溢出,一般就是内存拷贝时,目标...
  • 什么是缓冲区

    2018-10-15 12:27:00
    什么是缓冲区 缓冲区又称为缓存,它是内存...为什么要引入缓冲区 我们为什么要引入缓冲区呢? 比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再...
  • 操作系统——缓冲区溢出

    千次阅读 2019-05-12 22:46:18
    一、缓冲区溢出介绍 1988年,世界上第一个缓冲区溢出攻击–Morris蠕虫在互联网上泛滥,短短一夜的时间全世界6000多台网络服务器瘫痪或半瘫痪,不计其数的数据和资料被毁。造成一场损失近亿美元的空前大劫难! 那么,...
  • 堆栈缓冲区溢出实验

    2020-10-30 23:07:44
    实验二 堆栈缓冲区溢出实验 一、 实验目的  了解堆栈缓冲区溢出原理  掌握vc++6.0使用方法  熟悉堆栈缓冲区溢出防范常用的方法 二、 实验原理 堆栈的概念 堆栈是一个在计算机科学中经常使用的抽象数据类型。...
  •   字节缓冲区跟其他缓冲区类型最明显的不同在于,它们可以成为通道所执行的 I/O 的源头和/或目标。其实发现通道只接收ByteBuffer作为参数这个将Channel的时候会发现。 类型 优点 缺点 直接缓冲区 在虚拟机...
  • 10 WebGL的缓冲区对象使用

    千次阅读 2017-05-07 00:12:22
    前面的方法只能绘制一个点,而不能绘制多个顶点组成的图形...缓冲区对象是WebGL系统中的一块内存区域,我们可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用。案例查看地...
  • 缓冲区

    千次阅读 2016-04-15 15:30:51
    缓冲区像前篇文章讨论的那样被写满和释放,对于每个非布尔原始数据类型都有一个缓冲区类,尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节,非字节缓冲区可以再后台执行从字节或到字节的转换,...
  • 详解缓冲区溢出

    千次阅读 2019-09-14 17:49:04
    段错误的原理缓冲区溢出的原理攻击手段导致缓冲区溢出的常见 C 和 C++ 错误防止缓冲区溢出的一些技术重要选择:静态和动态分配的缓冲区实例研究 内存对齐 内存对齐系数 说道内存对齐,就不得不说内存对齐系数, 对齐...
  • linux缓冲区概念梳理

    2019-12-15 15:49:21
    缓冲区 我们在linux的开发过程中经常会接触到缓冲的概念。缓冲一般与输入输出联系在一起,因为我们知道I/O...故而需要引入缓冲区这一中间层,相当于CPU,内存 与 I/O设备之间的缓冲地带。对应用程序而言,将数据放...
  • 防止缓冲区溢出

    千次阅读 2019-05-15 09:39:19
    防止缓冲区溢出 通过防御性编程保护代码 https://www.ibm.com/developerworks/cn/security/buffer-defend/index.html#ibm-pcon C 中大多数缓冲区溢出问题可以直接追溯到标准 C 库。最有害的罪魁祸首是不进行自...
  • 缓冲区溢出1.原理2.攻击方式2.1 利用shellcode2.2 跳到其它函数2.3 其它情况3.防护3.1 canary(栈保护)3.2 NX3.3 RELRO3.4 PIE 缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢出的数据覆盖...
  • C++缓冲区的理解

    千次阅读 多人点赞 2018-03-05 17:23:32
    原文链接下面介绍缓冲区的知识。一、什么是缓冲区缓冲区又称为缓存,...二、为什么要引入缓冲区我们为什么要引入缓冲区呢?比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,...
  • 字节缓冲区和其他缓冲区最明显的区别在于,他们可以成为通道所执行的IO的源头和目标。通道只接收ByteBuffer作为参数。 直接缓冲区: 在jvm中,字节数组可能不会在内存中连续存储,或者无用存储单元收集可能随时对...
  • C语言缓冲区(缓存)详解

    千次阅读 多人点赞 2015-07-26 10:59:00
    1.概念 缓冲区又称为缓存,它是内存空间的一部分。...2.为什么要引入缓冲区 比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取
  • 缓冲区(Buffer)又称为缓存(Cache),是内存空间的一部分。也就是说,计算机在内存中预留了一定的存储空间,用来暂时保存输入或输出的数据,这部分预留的空间就叫做缓冲区(缓存)。 有时候,从键盘输入的内容,...
  • 文件缓冲区介绍与理解

    千次阅读 2020-01-16 18:18:16
    何为文件缓冲区? 文件缓冲区     缓冲文件系统是系统自动在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。     从内存向磁盘输出的数据会先送到内存的缓冲区,等装满缓冲区后一起送到磁盘上...
  • 解析C语言编程对缓冲区的理解

    千次阅读 2018-03-20 23:08:35
     一、什么是缓冲区 缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。 缓冲区根据其对应的是输入...
  • 堆溢出是缓冲区溢出的一种情形,分配到内存的堆部分的缓冲区被覆盖的,通常意味着缓冲区是使用malloc()等函数分配的内存。 相关视图 与“研究层面”视图(CWE-1000)相关 与“开发层面”视图(CWE-699)相关 引入...
  • D3D12渲染技术之常量缓冲区

    千次阅读 2018-09-16 13:45:56
    常量缓冲区是GPU资源(ID3D12Resource)的示例,其数据内容可以在着色器程序中引用。 正如我们将在博客中学到的,纹理和其他类型的缓冲区资源也可以在着色器程序中引用。顶点着色器具有以下代码: cbuffer ...
  • 通过一个绘制三角形的具体实例,详解了WebGL中缓冲区对象(buffer object)的使用。
  • C++对缓冲区的理解

    千次阅读 2017-08-20 14:01:51
    什么是缓冲区 缓冲区又称为缓存,它是内存空间的一部分。...为什么要引入缓冲区 我们为什么要引入缓冲区呢? 比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据
  • 缓冲引入

    千次阅读 2015-01-02 19:38:10
    引入缓冲的原因有很多(也可以说成引入缓冲目的),可以归结为以下几点: (1)缓和CPU和io设备减速度不匹配的矛盾。 (2)减少CPU的中断频率,放宽CPU中断响应时间的限制。 (3)解决数据粒度不匹配的问题。 ...
  • 进入缓冲区(缓存)的世界

    千次阅读 2019-07-27 17:33:46
    缓冲区(Buffer)又称为缓存(Cache),是内存空间的一部分。也就是说,计算机在内存中预留了一定的存储空间,用来暂时保存输入或输出的数据,这部分预留的空间就叫做缓冲区(缓存)。 有时候,从键盘输入的内容,...
  • [操作系统]I/O管理 缓冲区/SPOOLing

    千次阅读 2020-08-29 16:03:31
    主要功能:缓冲区管理,设备分配,设备处理,虚拟设备,实现设备独立性 I/O管理概述 I/O管理目标 提供统一界面、方便用户使用 使用逻辑操作和逻辑设备名掩盖设备的物理细节。 程序对设备的独立性:在源程序和目标...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 71,782
精华内容 28,712
关键字:

引入缓冲区的主要目的是