1。什么是栈在计算机软件开发过程中,我们经常听到、看到和使用“栈”。那么到底什么是“栈”呢?“栈”的作用是什么?我们来看看《新华字典》是怎么定义的。栈的基本含义是存放货物或供旅客住宿的房屋,如仓库、客栈等。说到客栈,就让我想起了一部著名的武侠电影《新龙门客栈》。客栈是英雄暂时居住的房间,由于客栈也是“栈”,意思是栈的最底层同义,是用来储物的。计算机中的“堆栈”是数据存储空间中用于存储特定数据的区域。堆栈的主机实体通常是随机存取存储器(RAM)。CPU可以直接与RAM交换数据。当RAM处于工作状态时,可以随时从任意指定地址写入(存储)或读取(取出)信息。综上所述,计算机中的“栈”就是一个存储数据的存储区。一般来说,“堆”和“堆”的意思是一样的。2.栈的分类我们知道栈是用来存储数据的。在实际使用中,栈并不是只有一种形式,而是分为多种类型。下面我们来了解一下栈的分类。根据堆栈在内存中的增长方向,堆栈可分为降序堆栈和递增堆栈:降序堆栈(Descend):向堆栈写入数据时,堆栈的增长方向是从高地址到低地址。增量栈(Increase):向栈中写入数据时,栈的增长方向是从低地址向高地址。根据栈指针SP指向的位置,栈可分为满栈和空栈:全栈(FullStack):SP指针始终指向栈顶元素。向堆栈写入数据时,先移动SP指针,再将数据放入SP指向的Address中。EmptyStack:SP指针总是指向下一个要放置元素的位置。向堆栈写入数据时,先将数据放入SP指向的地址,然后移动SP指针。根据栈的增长方向和栈指针的位置,栈可以分为以下四种基本类型:全栈(FA):栈指针指向最后一次压入的数据,栈的增长方向堆栈是从低地址到高地址。Fullminusstack(FD):栈指针指向最后一次压入的数据,栈的增长方向是从高地址到低地址。空增量栈(EA):栈指针指向数据将被压入的下一个地址,栈的增长方向是从低地址到高地址。空减栈(ED):栈指针指向下一个要压入数据的地址,栈的增长方向是从高地址到低地址。其中,全减栈是最常用的栈类型。3、栈的操作计算机的结构框图如下:从图中可以看出,寄存器堆中的寄存器在处理器中直接参与运算,逻辑运算的结果可以输出到数据存储器的地址端口和寄存器文件的数据端口。可以将寄存器文件中的寄存器值输出到数据存储器的数据端口,实现临时寄存器的值。入栈操作是将寄存器的值存入数据存储器,或将数据存储器中的数据读回寄存器。重要的事情说三遍:栈是用来临时存放处理器中寄存器的值的!栈是用来临时存放处理器中寄存器的值的!栈是用来临时存放处理器中寄存器的值的!ARM架构处理器中的寄存器组如下:栈操作的两条指令:PUSH和POP。对于PUSH操作,处理器首先递减SP的值,然后将指定的寄存器存储到SP寄存器指向的内存地址。对于POP操作,处理器首先将SP指向的内存地址存入指定的寄存器,然后增加SP寄存器的值。栈的操作具有“先进后出”的特点。先入栈的数据在栈底,后入栈的数据在栈顶。栈中的数据只能从栈顶读取,故有“先进后出”。以fullminusstack为例,下图是连续两次PUSH操作,SP寄存器和数据存储器的数据变化。在PUSH操作过程中,处理器先将SP的值减1,然后将指定的寄存器存储到SP寄存器指向的内存地址中。注意:PUSH操作执行后,数据存储器(栈空间)中的数据发生变化,但指定寄存器的值仍然是原来的值(不会被清除)。以fullminusstack为例,下图是连续两次POP操作,SP寄存器和指定寄存器的数据变化。POP操作时,处理器先将SP指向的内存地址存入指定的寄存器,然后增加SP寄存器的值。注意:执行POP操作后,指定寄存器的数据发生变化,但数据存储器(堆栈空间)中的值仍然是原来的值(不会被清除)。PUSH操作后,由于保存了指定寄存器的数据,之后该寄存器可以用作其他用途,寄存器完成其他操作后可以通过POP操作恢复原来的值。由于在处理器中只能直接参与运算,所以寄存器的个数通常是有限的(通常是16个或32个),堆栈操作相当于扩展了寄存器的个数。这个操作类似于火影中鸣人的影子分身(一真身多假身)。栈操作总结:栈是一个数据存储空间(stackspace)。堆栈有一个指向当前堆栈地址的堆栈指针。栈空间中的数据在每次PUSH操作后发生变化,指定寄存器中的数据在每次POP操作后发生变化。SP栈指针会在每次PUSH和POP操作后自动调整(无需用户干预)。4.栈的作用上面描述了栈空间本质上是暂时存放处理器中寄存器的计算结果。具体来说,栈在以下4种情况下用于数据存储:1.用于保存函数执行前寄存器的值,以便函数结束时恢复。2.用于存放局部变量。3.用于调用函数时传递参数。4.用于存放中断产生时状态寄存器和通用寄存器的值。4.1函数调用前保存寄存器值当被调用函数需要使用寄存器进行数据处理时,需要使用栈来临时保存寄存器的值,函数结束时恢复寄存器的值。测试代码如下:/******************************************@Author:liwei*@Github:liyinuoman2017************************************************/voidtest(void){intl,m,n;l=9;米=8;n=l+m;}voidstack_test(void){inti,j,k;我=1;j=2;测试();k=i+j;}intmain(void){inta0,a1,a2,a3,a4,a5,a6,a7,a8,a9;a0=1;a1=3;a2=1;a3=4;a4=1;a5=7;a6=9;a7=5;a8=2;a9=0;/***调用测试函数**/stack_test();a0=a1+a2;a3=a4+a5;}mian函数中定义了很多变量,占用大量寄存器。stack_test函数定义了3个变量。在调用stack_test函数之前,处理器的寄存器已经被占满。为了给stack_test函数提供寄存器,首先要将一些寄存器存入栈中,等stack_test函数执行结束后,再从栈中恢复一些寄存器的值。main函数反汇编结果如下:stack_test函数反汇编结果如下:根据stack_test函数的汇编代码可以看出,因为stack_test函数需要用到R4、R5、R6而这3个寄存器,由于这3个寄存器已经被占用了,所以在stack_test函数使用R4、R5、R6之前,必须先将这3个寄存器存入栈,而R4这3个寄存器的值,R5、R6必须在stack_test函数执行后返回之前从栈中恢复。总结:调用子函数时,子函数需要用到寄存器。由于寄存器已经被占用,需要将函数调用者使用的寄存器保存到栈中,等调用函数结束后再恢复寄存器。4.2存放局部变量局部变量可以直接存放在寄存器中,但当局部变量较多时,有些变量会存放在栈中。测试代码如下:/******************************************@Author:liwei*@Github:liyinuoman2017************************************************/intmain(void){inta0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15;a0=1;a1=3;a2=1;a3=4;a4=1;a5=7;a6=9;a7=5;a8=2;a9=1;a10=3;a11=1;a12=4;a13=1;a14=7;a15=9;a0=a1+a2;a3=a4+a5;a6=a7+a8;a9=a10+a11;a12=a13+a14;a15=a1+a2;根据反汇编,当使用到很多局部变量时,一些局部变量会被保存到栈中。4.3保存子函数参数AAPCS(ProcedureCallStandardfortheARMArchitecture),它规定了子程序的调用规则,其中ARM寄存器在子程序调用中的参数传递规则如下:当子程序参数不超过4个时,寄存器R0-R3用于传递参数。当参数超过4个时,超出的参数使用栈传递。调用2个参数的子函数的测试代码如下:/********************************************@Author:liwei*@Github:liyinuoman2017*************************************************/intsum(inta,intb){return(a+b);}intmain(void){inta0=0;a0=sum(3,4);while(1);}代码反汇编如下:根据反汇编,在跳转到sum函数之前,R0和R1用于保存sum函数的两个参数。调用6个参数子函数的测试代码如下:/********************************************@Author:liwei*@Github:liyinuoman2017*************************************************/intsum(inta,intb,intc,intd,inte,intf){return(a+b+c+d+e+f);}int主要(无效){inta0=0;a0=sum(1,3,2,4,7,9);while(1);}代码反汇编如下:从反汇编可以看出,在跳转到sum函数之前,用R0、R1、R2、R3保存最后4个函数参数,第一个和第二个保存参数同时入栈,并将栈中的参数加载到sum函数中进行计算。所以很多编程规范都规定函数参数不能超过3个,原因是使用栈传递参数影响程序执行效率。4.4中断时存储寄存器当处理器产生异常或中断时,处理器硬件会在进入异常函数之前自动保存一些寄存器。寄存器的值。ARM架构处理器的中断时序图如下:5.栈区的优点计算机中的内存分区通常是这样的:数据内存通常分为:栈区、堆区、静态区。栈区用于存放特定用途的变量。栈区的变量是临时改变的,栈区的大小是动态改变的。堆区用于用户主动分配和存储数据。静态区用于存放静态变量。静态区变量的使用周期就是整个程序的运行周期。每个变量对应一个地址,“一个萝卜一个坑”。堆区本文不做介绍。本节着重比较堆栈区和静态区。栈区有什么好处?栈区可以换成静态区吗?5.1栈区的优点假设现在有一个函数如下:voidtest(void){charbuff[100];...}一个函数test定义了一个100字节的数组buff,所以当程序运行到test函数时,会临时占用一个100字节的栈区。假设buff应该是statictypestaticcharbuff[100],程序会一直使用100字节的静态区来存放数组buff。好像没什么优势。静态区和堆栈区都会占用100字节。如果像test这样有50个函数,每个函数需要使用100字节,如果全部使用静态变量,程序将使用5000字节的静态区。如果函数不使用静态变量,而是使用临时变量,程序会临时占用最少100字节(50个函数没有嵌套调用),最多5000字节(50个函数全部嵌套),这实际上是很难把50个函数全部嵌套起来。按照10层嵌套,只需要1000字节的栈区就可以满足程序对栈的消耗。因此,在函数较多的情况下,栈区可以显着减少数据存储区的消耗!5.2栈区的不可替代性正如上面所说,栈区可以显着降低内存消耗。如果不考虑内存消耗,取消栈区,只使用静态区,可以让数据内存中的分区更简单,更容易管理。这个可以吗?假设现在有如下函数:/*****************************************@Author:liwei*@Github:liyinuoman2017************************************************/doublefactorial(doublen){doubles;如果(n>=2){s=n*阶乘(n-1);}elseif(n==1){s=1;}returns;}factorial是一个递归函数来实现阶乘函数,递归函数会调用自己。在这种情况下,如果递归函数中的变量使用静态变量,将无法正常工作。递归函数中的变量只能使用局部变量,局部变量存放在栈区。假设现在有如下代码:/*********************************************@Author:liwei*@Github:liyinuoman2017************************************************/voidmain(void){...sum();}intsum(inta,intb){intc;c=a+b;returnc;}/*中断函数*/voidirq_handler(void){...sum();}假设main函数调用sum函数,处理器产生中断。这时程序跳转执行irq_handler函数。irq_handler函数中也调用了sum函数。这时候sum函数出现了Calledtwice,sum函数中的变量也必须使用局部变量。不仅递归函数中的变量必须使用局部变量,在一些特殊的使用场景下也只能使用局部变量,这就体现了栈区的不可替代性。综上所述,使用栈区可以节省数据存储空间,一些特殊的使用场景可以只使用栈区。6.每个硬币都有两个面上一篇文章指出堆栈区的使用是不可替代的,同时可以节省数据存储空间。那么使用栈区是不是一个完美的解决方案呢?每个硬币都有两个面(每个硬币都有两个面)。任何事物都有两个方面。有优点,就会有缺点。使用栈的缺点是:栈溢出!栈溢出的情况相信大家都遇到过。堆栈溢出会导致程序产生形式可变的BUG。这种BUG通常很难定位。下图是栈溢出的情况:从图中可以看出,当栈溢出时,可能会错误地修改静态区变量的值,导致程序出现BUG。而且堆栈溢出后修改的静态区的值体现了随机性,所以会导致程序产生表现形式多变的bug。既然栈溢出会导致严重的bug,那么有没有办法检测栈溢出呢?第一种方法是在初始化栈区时,用固定的标记字符(如0x5a5a5a5a)填充栈尾。如果发生“堆栈溢出”,那么填充在堆栈区域末尾的标记字符可能会发生变化。这样,通过检测栈区末尾的标记符是否发生变化,判断是否存在“栈溢出”。这种检测方法不是100%有效,因为末尾的标签字符可能会被跳过。第二种方法是通过栈基地址和当前栈地址计算出当前栈的大小。栈基地址减去当前栈地址就可以得到栈的大小。如果计算出的堆栈大小大于系统分配的堆栈大小,则“堆栈溢出”。
