引言
在上篇博文中,我们学习了栈的概念和意义,以及栈空间的定义和栈空间在内存空间中存在的方式。那我们如何去操作栈?存在两个指令:PUSH、POP。PUSH指令是将数据送入栈中,POP指令是取出当前栈顶的数据。
那么在这两个指令的背后,CPU是如何访问并操作栈空间呢?CPU究竟做了什么样的工作来处理栈数据?SS段寄存器在其中又扮演了什么样的角色呢?
那么,就快快让我们开启本篇博文的学习吧!
访问栈
SS段寄存器
SS,全称:Stack Segment。Stack的意思就是“栈”,这也就表明了SS段寄存器,与栈空间那不可分割的关系。
SS段寄存器也是十六位寄存器,它和CS、DS它们存放的数据类型一样,也是一个地址,SS中存放的地址就是栈段的段地址。栈段,就是我们之前说过的栈空间。
和通过DS访问内存数据一样,CPU在访问栈数据时,会默认把SS段寄存器中的地址当作栈段的段地址来进行寻址。我们都知道寻址不仅仅需要基地址,还需要一个偏移地址才能完成,就像与CS段寄存器配套使用的IP寄存器一样,要想对栈完成寻址,势必还需要另外一个寄存器的配合,这个帮手就是:SP寄存器。
SP寄存器
SP寄存器也是一个十六位寄存器,它里面存放的是栈的栈顶元素偏移地址。栈顶元素就是栈中当前“最上面”的字数据,注意,栈中存放的数据长度都是一个字的长度。
我们可以拿上篇博文中的向盒子内放书本的例子来加深理解栈顶元素:
比如我们将《高等数学》书本放入盒子内:如图
那么,将盒子看成栈段,《高等数学》就是栈顶元素。
我们再放一本书进去,如图:
此时最上面的是《C 语言》,那么《C 语言》就是栈顶元素。
从上面的举例中,我们可以得到下面的结论:
1、栈顶元素是指栈中最上面的字数据,栈顶元素会随着栈中数据变化而发生变化。
2、“最上面”的字数据,对于CPU来说,就是从栈顶开始寻址最先寻到的数据。
SS段寄存器和SP寄存器加在一起,则是栈顶元素的地址,也就是说:任意时刻,CPU将 SS:SP 指向的数据,当作栈顶元素。
PUSH和POP的过程
了解了SS段寄存器和SP寄存器,下面我们将详细讲述PUSH指令在执行的全过程。
如图,我们将内存地址10000H~1000FH这段内存地址空间当作栈段:
那么此时,SS段寄存器中值为1000H,SP寄存器中值为10010FH。
我们可以理解SS段寄存器值为1000H,但是SP寄存器值为什么会是10010FH?
在上面的讲解中我们已经知道SP中存放的栈顶元素在栈中的偏移地址,它是一个字数据,需要占用两个字节单元。如果栈中没有任何数据,也就是栈中为空,此时我们向栈中放入一字数据,按照栈的访问方式,那么此时的栈顶元素地址就为1000EH,数据占用1000H:EH、1000H:FH两个字节单元,SP寄存器的值为:000EH。
已知放入一个字数据后SP寄存器值为000EH,一个字数据占用两个字节单元,那么求没有放入字数据之前,SP寄存器值位多少?这就是一个很简单的数学题目了,解题:SP = 000EH+2,SP = 0010H。所以,栈为空的情况下,SP寄存器中值为0010H。
我们也可以这样认为:栈为空的情况下,SP指向栈底的下一个内存单元地址,即SP = 栈底+1。
明白了SP寄存器的值,下面我们就通过Debug,来实际操作并执行PUSH指令,观察内存中数据变化和分布情况:
打开Debug,使用R命令修改SS段寄存器值为1000H,修改SP段寄存器值为0010H:
下面使用A命令向内存中写入下面汇编指令:
mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx
下面使用T命令进行单步执行,执行过程中观察栈段中数据详情:
我们先执行前两条指令:mov ax,0123H,push ax;使用D命令查看此时栈中数据:
我们发现执行前两条指令后,SP寄存器的值变成了:000EH,变化为:SP=SP-2。那么此时栈中栈顶元素为 1000H:EH、1000H:FH 两个字节单元组成的字数据:0123H,说明我们确实将AX的内容push进了栈中。
我们继续执行接下来的两行指令:mov bx,2266H,push bx。执行后再次观察栈中数据详情:
我们发现执行后,SP寄存器的值变成了:000CH,变化为:SP=SP-2。此时栈顶元素为1000H:CH、1000H:DH两个字节单元组成的字数据:2266H,证明BX的值确实被push进了栈。
我们再执行下面的两句指令:mov cx,1122H,push cx。执行后再次观察栈中数据:
和我们预料的一样,SP寄存器的值变成了:000AH,变化为:SP=SP-2。此时栈顶元素为 1000H:AH、1000H:BH 这两个字节单元组成的字数据:1122H。证明CX的值被push进了栈。
好,接下来就是出栈的环节,我们继续执行下条指令:pop ax,这条指令的意思是将当前栈顶数据出栈,送到AX寄存器中。我们执行后观察SP寄存器值变化和栈中数据变化:
我们发现,SP寄存器的值变成了:000CH,变化为:SP=SP+2,此时栈顶元素就变成了由 1000H:CH、1000H:DH 这两个字节单元组成的字数据:2266H,于此同时,AX中寄存器的值变成了1122H。
下面我们继续POP出栈:pop bx,观察执行后寄存器值变化和栈中数据变化
执行后,SP寄存器的值变成:000EH,变化为:SP=SP+2,此时栈顶元素就变成了由 1000H:EH、1000H:FH 这两个字节单元组成的字数据:0123H。BX寄存器之前的数据就是2266H,所以本次 pop bx 后,BX寄存器值并未变化。
下面就执行最后一条指令:pop cx:
执行后,SP寄存器的值变成:000FH,变化为:SP=SP+2。因为SP寄存器中记录的偏移地址已经在栈底下面,所以此时栈中数据为空。你可能会说,D查看内存中数据,这不是有数据的嘛,栈怎么就空了呢?
这里我们需要明白的是,我们判断栈为空还是栈为满,是看SP中的偏移位置,并不是根据栈中是否存在数据来判断的。SP 中偏移在栈底下一个字节单元,说明栈为空;SP 中偏移为栈顶,说明栈为满。之所以这样,是因为CPU在操作栈的时候,是按照 SS:SP 的地址来进行栈的数据处理,它才不会去关系栈中之前的数据是什么,反正 SS:SP 指到哪,它就干到哪。
通过上面的实操和观察,我们可以得出下面几个结论:
PUSH 的过程:
1、首先 SP寄存器的值先减去2,也就是 SP=SP-2;
2、对内存地址 SS:SP 下的字单元进行赋值操作,相当于:MOV SS:SP,字数据。
完成入栈的操作。
POP 的过程:
1、首先将内存地址 SS:SP 下的字单元数据赋值给一个操作对象,相当于:MOV 操作对象,SS:SP ;
2、将SP寄存器的值加上2,也就是 SP=SP+2。
完成出栈的操作。
一个疑惑:来自实验2中的问题
相信在上面的实操和观察栈中数据变化,已经让我们深刻理解了CPU是如何访问并操作栈中数据的。不过有细心的小伙伴可能发现了,我们在观察栈中数据变化时,栈顶到栈顶元素之间的数据在每次被PUSH、POP时,都会发生一个变化,这是怎么回事?按道理我们执行了一次PUSH、POP指令应该只操作了当前栈顶元素,栈中空余的元素怎么会变化呢?
答案明显是,是还有别的程序在操作这个栈!也就是说,不单单只有我们的指令在向这个栈中 PUSH、POP,别处的也有~
那么到底是谁在操作?我们这里好好的探讨一下这个问题,这同时也是 《汇编语言 第四版》实验2中的第二个问题。
首先我们先清空 10000H~1000FH 之间的数据:
可以看到我们现在已经将 10000H~1000FH 之间清零,下面我们修改IP地址,重新执行上述指令,这次主要观察栈顶到栈顶元素之间的数据变化:
下面我们就执行前两条指令数据:mov ax,0123,push ax,观察栈中数据:
我们能看到,本来之前我们已经对栈段进行了清零处理,只不过执行了 push ax后,栈顶到栈顶元素之间的数据(1000H:0H~1000H:EH之间的内存空间)就从之前的0,发生了变化。
比如,1000H:CH 字单元数据变成了:01A3H,1000H:AH 字单元数据变成了:073FH,1000H:8H 字单元数据变成了 0104H。
这些数据有什么意义么?数据好眼熟啊,我们想了一下,发现 073FH 这个不就正是此时 CS段寄存器的值么!这个0104H,不就是此时的 IP寄存器值么!还有 1000H:4H 字单元下的数据:0123H,这不就是此时AX寄存器的值么!
很好,现在我们已经搞明白了这几个数据的意义,它们分别是 CS段寄存器值、IP寄存器值、AX寄存器值。不过还有一个 01A3H,这个则是标志寄存器中的值。
其实这些会在后面进行详细的说明和解释,这里博主先大致说一下,解释大家心中的疑惑。
首先,DOS下只存在一个栈,这是肯定的,因为作为单核CPU,栈就只能通过 SS:SP 这一种形式进行定义和访问,大家当然就共用一个栈啦!
其次,标志寄存器、CS段寄存器、IP寄存器中的值为什么会被push进栈?这还是由于中断过程导致的。是谁引发了这个中断?是Debug的T命令引发了一个中断,T命令中断的目的是为了使程序暂停,然后Debug界面打印执行某条指令后各个寄存器的值和状态,以方便开发成员更好的观察程序运行中数据的变化和处理,说白了T命令就好比是我们熟悉的单步调试。关于中断,我们之前就已经提及到了,比如我们做实验,使用MOV指令修改CS值,结果就发生了一个中断。
中断过程中,首先会把标志寄存器的值入栈,这样做的目的为了保存中断发生之前的“现场”;紧接着就会把CS、IP入栈,这样做的目的是为了保留回去的路,执行完中断程序后,程序还需要回到中断之前的地方继续执行。
下面我们就详细说下,当我们使用T命令执行 push ax 时,此时整个DOS系统中做了哪些操作:
1、首先,我们输入T,按下 Enter键,CPU开始执行指令:push ax,将AX寄存器中的值 0123H 送入栈中,此时SP值减去2,SP值为:000EH;
2、执行完成后,Debug程序便引发了一个中断,暂停当前程序执行;
3、由于Debug引发了一个中断,所以程序进入中断过程中,系统便将标志寄存器的值入栈,保留此时的运行现场。由于我们前面已经将AX入栈,所以标志寄存器值 01A3H 便放在了 1000H:CH 下的字单元,此时SP值再减2,SP值为:000CH;
4、接下来,系统将CS、IP这两个寄存器依次入栈,073FH、0104H 放入栈中,此时SP值为:0008H;
5、接下来就是执行中断程序啦,这个中断程序是Debug提供并完成的,就是打印出当前CPU中各个寄存器的值,以及需要展示的下条指令数据等。这个过程中需要使用到AX寄存器和别的一些寄存器,所以中断程序中就先将这些使用到的寄存器入栈保存,就是为了保护数据不被修改,在执行完成后做出栈恢复,也就是保留运行现场。所以我们就在此时的栈中看到了 AX寄存器的值。这里我们暂且认为Debug的T命令中断程序只push了两个寄存器的值,那么此时SP值为:0004H;
6、中断程序执行结束,界面打印各个寄存器值和状态完毕后,就需要恢复当时的现场数据啦,也就是按照之前PUSH进来的顺序,开始POP出栈。首先恢复的是AX寄存器和另外一个不知道是谁的寄存器,0123H,0000H,这两个出栈,SP值连续加两个2,此时 SP值为:0008H;
7、接下来就是 POP IP、POP CS;使程序回到中断之前的地址继续执行,此时SP值为:000CH;
8、最后就是 POP 标志寄存器,恢复当时的运行状态,此时SP值为:000EH,又恢复了中断之前的偏移地址。
至此整个的过程结束。
这里注意,由于内存中的数据只会不断地被新数据覆盖而改变,所以POP出栈的操作已经无法使栈恢复之前的数据 0。这也就是为什么,我们通过D命令查看栈中的数据,发现了10000H~1000EH 之间出现了非0值。
好了,经过上述详细的步骤讲解,相信你现在已经完全明白了,其根本原因就是DOS下各个程序公用同一个栈段导致的。
不过我们在开发中无需关系栈中的数据发生了怎么样的变化,我们只需要关注一个东西,那就是当前的栈顶元素是谁,也就是关注 SS:SP 指向了谁,毕竟这对CPU来说才是真正有意义的东西。
本篇结束语
在本篇博文中,我们学习了SS段寄存器的作用和意义,以及它的小伙伴SP寄存器。同时通过上手操作并观察栈顶元素变化,明白了PUSH、POP指令在执行时,CPU是如何访问并操作栈中数据。
此外,我们搞清楚了栈中数据变化是为何而来,通过详细的步骤分析,掌握栈中每个时刻都在发生了什么事情。
内存访问系列博文到此完结,希望大家都已经中分掌握了访问内存要领,期待上手代码开发来展露拳脚。
那么接下来的博文中,我们将正式进入开发环节,讲解汇编语言开发中所需要掌握的知识和技巧,还希望大家好好学习~
感谢围观,转发分享请标明出处,谢谢!