2022-12-08栈

栈是一种具有特殊的访问方式的存储空间。它的特殊性就在于,最后进入这个空间的数据,最先出去。

可以用一个盒子和3本书来描述栈的这种操作方式。

一个开口的盒子就可以看成一个栈空间,现在有3本书,《高等数学》、《C语言》、《软件工程》,把它们放到盒子中,操作的过程如图所示。

现在的问题是,一次只允许取一 本, 我们如何将3本书从盒子中取出来?

显然,必须从盒子的最上边取。这样取出的顺序就是:《软件工程》 、《C 语言》、《高等数学》,和放入的顺序相反,如图3.8所示。

从程序化的角度来讲,应该有一个标记,这个标记一.直指示着盒子最上边的书。

如果说,上例中的盒子就是一个栈,我们可以看出,栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个 元素。栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的这种操作规则被称为: LIFO(Last In First Out,后进先出)。

CPU提供的栈机制

现今的CPU中都有栈的设计,8086CPU也不例外。8086CPU提供相关的指令来以栈的方式访问内存空间。这意味着,在基于8086CPU编程的时候,可以将一段内存当作栈来使用。

8086CPU提供入栈和出栈指令,最基本的两个是PUSH(入栈)和POP(出栈)。比如,push ax表示将寄存器ax中的数据送入栈中,pop ax表示从栈项取出数据送入ax。8086CPU的入栈和出栈操作都是以字为单位进行的。

下面举例说明,我们可以将10000H~1000FH这段内存当作栈来使用。

mov ax, 0123H

push ax

mov bx, 2266H

push bx

mov cx, 1122H

push cx

pop ax

pop bx

pop сх

字型数据用两个单元存放,高地址单元存放高8位,低地址单元存放低8位。

其一,我们将1000H~1000FH这段内存当作栈来使用,CPU执行push和pop指令时,将对这段空间按照栈的后进先出的规则进行访问。但是,一个重要的问题是,CPU如何知道10000H~1000FH这段空间被当作栈来使用?

其二,push ax等入栈指令执行时,要将寄存器中的内容放入当前栈顶单元的上方,成为新的栈顶元素; pop ax 等指令执行时,要从栈项单元中取出数据,送入寄存器中。显然,push、 pop 在执行的时候,必须知道哪个单元是栈顶单元,如何知道呢?

CPU如何知道当前要执行的指令所在的位置?我们现在知道答案,那就是CS、IP中存放着当前指令的段地址和偏移地址。现在的问题是: CPU如何知道栈顶的位置?显然,也应该有相应的寄存器来存放栈顶的地址,8086CPU 中,有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP 指向栈顶元素。push 指令和pop指令执行时,CPU从SS和SP中得到栈顶的地址。

现在,我们可以完整地描述push和pop指令的功能了,例如push ax。

push ax的执行,由以下两步完成。

SP=SP-2, SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;

将ax中的内容送入SS:SP指向的内存单元处,SS:SP 此时指向新栈顶。

图3.10描述了8086CPU对push指令的执行过程。

push指令的执行过程

从图中我们可以看出,8086CPU中,入栈时,栈顶从高地址向低地址方向增长。

如果将10000H~ 1000FH这段空间当作栈,初始状态栈是空的,此时,SS=1000H,SP=?

将10000H~1000H这段空间当作栈段,SS=100H,栈空间大小为16字节,栈最底部的字单元地址为1000:000任意时刻,SS:SP 指向栈项,当栈中只有-个元素的时候,SS=1000H, SP=000EH。 栈为空,就相当于栈中唯 一的元素出栈, 出栈后,SP=SP+2,SP原来为000EH, 加2后SP=10H, 所以,当栈为空的时候,SS=1000H,SP=10H。

换一个角度看,任意时刻,SS:SP指向栈项元素,当栈为空的时候,栈中没有元素,也就不存在栈项元素,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2,栈最底部字单元的地址为1000:000E,所以栈空时,SP=0010H。

我们描述pop指令的功能,例如pop ax。

popax的执行过程和push ax刚好相反,由以下两步完成。

将SS:SP指向的内存单元处的数据送入ax中;

注意,图3.2中,出栈后,SS:SP指向新的栈顶1000EH,pop操作前的栈顶元素,1000CH处的2266H依然存在,但是,它已不在栈中。当再次执行push等入栈指令后,SS:SP移至1000CH并在里面写入新的数据,它将被覆盖。

栈顶超界的问题

8086CPU用SS和SP指示栈顶的地址,并提供push和pop指令实现入栈和出栈。

SS和SP只是记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶。可是,如何能够保证在入栈、出栈时,栈顶不会超出栈空间?

图3.13描述了在执行push指令后,栈顶超出栈空间的情况。

图3.13中,将10010H~1001FH当作栈空间,该栈空间容量为16字节(8字),初始状态为空,SS=1000H、 SP=0020H,SS:SP指向10020H;

在执行8次push ax后,向栈中压入8个字,栈满,SS:SP 指向10010H;

再次执行push ax: sp=sp-2, SS:SP指向1000EH,栈顶超出了栈空间,ax 中的数据送入1000EH单元处,将栈空间外的数据覆盖。

图3.14描述了在执行pop指令后,栈顶超出栈空间的情况。

图3.14中,将10010H~1001FH 当作栈空间,该栈空间容量为16字节(8字),当前状态为满,SS=1000H、SP=0010H,SS:SP 指向10010H;

在执行8次popax后,从栈中弹出8个字,栈空,SS=SP指向10020H;

再次执行pop ax: sp sp+2, SS:SP指向1022H栈顶超出了栈空间。此后,如果再执行push指令,10020H、 10021H 中的数据将被覆盖。

上面描述了执行push、 pop指令时,发生的栈顶超界问题。可以看到,当栈满的时候再使用push指令入栈,或栈空的时候再使用pop指令出栈,都将发生栈顶超界问题。

栈顶超界是危险的,因为我们既然将一段空间安排为栈, 那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序中的,也可能是别的程序中的(毕竞一个计算机系统中并不是只有我们自己的程序在运行)。但是由于我们在入栈出栈时的不小心,而将这些数据、代码意外地改写,将会引发连串的错误。

我们当然希望CPU可以帮我们解决这个问题,比如说在CPU中有记录栈顶上限和栈底的寄存器,我们可以通过填写这些寄存器米指定栈空间的范围,然后,CPU在执行push指令的时候靠检测栈顶上限寄存器、在执行pop指令的时候靠检测栈底寄存器保证不会超界。

不过,对于8086CPU,这只是我们的一个设想(我们当然可以这样设想,如果CPU是我们设计的话,这也就不仅仅是一个设想)。实际的情况是,8086CPU 中并没有这样的寄存器。

8086CPU不保证我们对栈的操作不会超界。这也就是说,8086CPU只知道栈顶在何处(由SS:SP指示),而不知道我们安排的栈空间有多大。这点就好像CPU只知道当前要执行的指令在何处(由CS:IP 指示),而不知道要执行的指令有多少。从这两点上我们可以看出8086CPU的工作机理,它只考虑当前的情况:当前的栈顶在何处、当前要执行的指令是哪一条。

我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。

push、pop指令

前面我们一直在使用push ax和pop ax,显然push和pop指令是可以在寄存器和内存(栈空间当然也是内存空间的一部分, 它只是段可以以一种特殊的方式进行访问的内存空间。)之间传送数据的。

push和pop指令的格式可以是如下形式:

push寄存器  ;将一个寄存器中的数据入栈

pop寄存器  ;出栈,用一个寄存器接收出栈的数据

当然也可以是如下形式:

push段寄存器  ;将一个段寄存器中的数据入栈

pop段寄存器  ;出栈,用一个段寄存器接收出栈的数据

push和pop也可以在内存单元和内存单元之间传送数据,我们可以:

push内存单元  ;将一个内存字单元处的字入栈(注意:栈操作都是以字为单位)

pop内存单元  ;出栈,用一个内存字单元接收出栈的数据

mov ax, 1000H

mov ds,ax  ;内存单元的段地址要放在ds中

push [0]  ;将1000:0处的字压入栈中

pop [2]  ;出栈,出栈的数据送入1000:2处

指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU 从ds中取得。

编程,将10000H~1000FH这段空间当作栈,初始状态栈是空的,将AX、BX、DS中的数据入栈。

mov ax, 1000H

mov ss, ax  ;设置栈的段地址,Ss=1000H, 不能直接向段寄存器 SS中送入;数据, 所以用ax中转。

mov sp, 0010H  ;设置栈顶的偏移地址,因栈为空,所以sp=0010H。上面的3条指令设置栈顶地址。编程中要自己注意栈的大小。

push ax

push bx

push ds

将10000H~1000FH这段空间当作栈,初始状态栈是空的;

设置AX=001AH,BX=001BH;

将AX、BX中的数据入栈;

然后将AX、BX清零;

从栈中恢复AX、BX原来的内容。

mov ax, 1000Hmov ss, ax

mov sp, 0010H  ;初始化栈顶,栈的情况如图3.15(a)所示

mov ax, 001AH

mov bx, 001BH

push ax

push bx  ;ax、bx入栈,栈的情况如图3.15(b)所示

sub ax, ax   ;将ax清零,也可以用mov ax,0,

;sub ax,ax 的机器码为2个字节,

;mov ax,0 的机器码为3个字节。

sub bx,bx

pop bx  ;从栈中恢复 ax、bx原来的数据, 当前栈顶的内容是bx

pop ax;中原来的内容: 001BH,ax 中原来的内容001AH在栈顶

;的下面,所以要先pop bx,然后再pop ax。

从上面的程序我们看到,用栈来暂存以后需要恢复的寄存器中的内容时,出栈的顺序要和入栈的顺序相反,因为最后入栈的寄存器的内容在栈顶,所以在恢复时,要最先出栈。

将10000H~1000FH这段空间当作栈,初始状态栈是空的;

设置AX=001AH,BX=001BH;

利用栈,交换AX和BX中的数据。

mov ax, 1000H

mov ss, ax

mov sp, 0010H  ;初始化栈顶,栈的情况如图3.16(a)所示

mov ax, 001AH

mov bx, 001BH

push ax

push bx  ;ax、bx入栈,栈的情况如图3.16(b)所示

pop ax  ;当前栈项的数据是bx中原来的数据: 001BH;

;所以先pop ax, ax=001BH;

pop bx   ;执行pop ax后,栈顶的数据为ax原来的数据:

;所以再pop bx, bx=001AH;

push、 pop实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov指令不同的是,push和pop指令访问的内存单元的地址不是在指令中给出的,而是由SS:SP指出的。同时,push和pop指令还要改变SP中的内容。

我们要十分清楚的是,push 和pop指令同mov指令不同,CPU执行mov指令只需一步操作,就是传送,而执行push、 pop指令却需要两步操作。执行push 时,CPU的两步操作是:先改变SP,后向SS:SP 处传送。执行pop时,CPU的两步操作是:先读取SS:SP处的数据,后改变SP。

注意,push, pop 等栈操作指令,修改的只是SP。也就是说,栈顶的变化范围最大为: 0~FFFFH。

提供:SS、SP指示栈顶:改变SP后写内存的入栈指令;读内存后改变SP的出栈指令。这就是8086CPU提供的栈操作机制。

栈的综述

8086CPU提供了栈操作机制,方案如下。

在SS、SP中存放栈顶的段地址和偏移地址;提供入栈和出栈指令,它们根据SS:SP指示的地址,按照栈的方式访问内存单元。

push指令的执行步骤:SP=SP-2;向SS:SP指向的字单元中送入数据。

pop 指令的执行步骤:从SS:SP指向的字单元中读取数据:SP=SP+2。

任意时刻,SS:SP 指向栈顶元素。

8086CPU只记录栈项,栈空间的大小我们要自己管理。

用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。

push、pop 实质上是一种内 存传送指令,注意它们的灵活应用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 第3章、寄存器(内存访问) 3.1 内存中字的存储 字单元:存放一个字型数据(16位)的内存单元,由两个连续的内存...
    BoL0150阅读 962评论 0 0
  • CPU中,用16位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。在内存中存储时,由于内存单元是字节单...
    guanjianhe阅读 144评论 0 0
  • 1. CPU中用16位寄存器来存储一个字,高8位存放高位字节,低8位存放低位字节。在内存中存储时,由于内存单元是字...
    八斗道人阅读 529评论 0 1
  • 栈结构:栈是一种只能在一端插入或删除操作的数据结构 栈有两个基本操作: 入栈和出栈 入栈: 将一个新的元素放到栈...
    有机会一起坐牢阅读 1,084评论 0 1
  • 8086的寻址方式 CPU访问内存单元时,要给出内存单元的地址,所有的内存单元都有唯一的地址,叫做物理地址 808...
    hfzhangzhang阅读 973评论 0 0