汇编学习教程:关于栈,遗留的问题

引言

在我们这几篇内存访问的博文中,你会发现栈这个东西,关于它的讲解占了差不多有一半左右,足以见识到它的重要性。随着我们后面深入学习,你会发现栈在开发中往往承担了很重要的角色,所以我们势必要好好学习栈,彻底把它搞明白。

在上篇博文中,我们已经学习了两个操作栈的指令,分别是:PUS、POP,学习了两个访问栈需要的寄存器,分别是:SS段寄存器、SP寄存器。我们通过在Debug上进行实际操作和观察内存数据变化,明白了PUSH、POP指令背后,CPU都做了什么样的工作。

那么本篇博文,我们将探讨栈一些遗留的问题,下面就让我们开始今天的学习吧!

栈顶越界问题

栈顶超界?什么界?当然是栈顶和栈底!谁超了界?当然是SP寄存器中记录的偏移地址!

我们之前在讲述栈空间的时候就已经说过,栈空间(也就是栈段),是一段有明确范围的内存地址空间,也就是说该空间有既定且明确的起始地址和结束地址。越界就是指:SP寄存器中记录的当前栈顶元素位置,不在栈段的明确空间范围内,即认为此时发生了栈顶越界。为什么要表述为“栈顶”越界?是因为,SP寄存器中的值为当前栈中栈顶元素位置,所以称呼为“栈顶”越界。

还是老样子,我们先在Debug中实际操作,去看下如果发生栈顶越界,将会出现什么样的情况:

首先,我们还是将内存地址 10000H~1000FH 这段范围当作一个栈,共有16个字节、8个字,也就是说,当我们连续 PUSH 8次以上,将会发生栈顶越界

我们使用A命令,向内存中写入下面汇编指令:

mov ax,0123H

push ax    ; 第一次PUSH

push ax    ; 第二次PUSH

push ax    ; 第三次PUSH

push ax    ; 第四次PUSH

push ax    ; 第五次PUSH

push ax    ; 第六次PUSH

push ax    ; 第七次PUSH

push ax    ; 第八次PUSH   此时栈满

push ax    ; 第九次PUSH   此时将会发生栈顶越界

 

修改 SS 段寄存器为1000H,SP寄存器为0010H 

 接着就让我们先把栈堆满,执行8次push:

我们发现,执行8次后栈满,此时SP寄存器为0,1000H:0H正式栈顶位置,如果继续执行,将会发生栈顶越界,那么我们继续执行push:

我们发现,再次push后,按照push过程,SP寄存器值减2,也就是SP=0-2,则SP寄存器的值为FFFE。

为什么0减去2,会变成FFFEH?还值得我们之前讲过的一个例子吗?FFFFH+1,结果变成了0,这是因为发生了一个进位,实际结果肯定为 10000H,但是寄存器是十六位,放不下二十位的数据,就舍弃了最前面的1,保留了0000H到寄存器中。

同样,这是一样的道理:0-2,我们都知道,0比2小,所以0需要向前一位借位,这里拿10进制举例,0向前借位变成10,10比2大,所以相减得8,因为0是借位相减,所以结果是负数,为-8。

十进制的加减乘除我们都会,那么十六进制也是一样的,只不过借的那一位数变成了16而已。由于寄存器是十六位,所以相减的话就要看成下面的形式:

从右向左算起,首先减数第一位是0,被减数第一位是1,不够减所以要向前借一位;减数第二位也是0,那么就继续向前借,第三位和第四位都是0,所以第四位就继续向前借,也就是是事实不存在的第五位,最终借得1,那么就相当于:10000-1

 这下就清楚了,十六进制10000H-1,答案等于FFFFH,那么减2,答案就是FFFEH了。由于这是借位,所以FFFEH在这里实际上表达的是一个负数,按照补码规则,FFFEH也就是负数:-2。你当然也可以把它看成一个正数,那么FFFEH看成正数就是数字:65534。

由于CPU本身无法区分数字的正负,但是又要保证计算的准确性,需要按照人类已知的计算法则进行计算,所以它就需要兼顾到负数,于是便有了源码、反码、补码的诞生。

 这里就先简单的讲述一下,有关补码方面的,感兴趣的小伙伴可以自行搜索学习。

回到我们的问题中,我们发现此时SP中的栈顶元素偏移实际上是已经超过了栈顶位置1000H:0H,那么也就意味着此时栈属于越界状态。你可能会问,怎么没有发生任何异常呢?除了SP栈顶元素指针超界外,也没发生什么问题啊!

NONONO,确实我们发现没有发生任何的异常和问题,那是因为我们现处于Debug下的T单步调试中,所以没感受到什么问题。如果这个栈顶越界发生在一个程序中,那么此时很有可能程序就已经崩溃掉了。为什么会崩溃掉?因为越界导致改写了栈之外的未知数据

我们发现,由于越界的产生,我们将内存地址1000H:FFFEH、内存地址1000H:FFFFH,这两个字节单元下的数据修改为了AX寄存器中的值,这个修改是不可逆的,也就是说这两个字节单元的原先数据已经无法恢复了。如果此时其他的程序(尤其是系统程序),需要使用到这两个字节单元的数据,那么就会读取到错误值,导致程序问题或者崩溃。如果是系统程序,那么不止崩溃那么简单,还可能面临电脑屏幕黑屏甚至死机。这才是栈顶越界的危害之处!

通过上述分析我们明白,栈顶的越界将会改写栈外未知内存单元数据,导致可能存在电脑运行异常问题。

那么还有一种越界,则是栈底越界。我们同样实际操作下:

使用A命令向内存中写入下面的汇编指令:

mov ax,0123H

push ax    ; 第一次PUSH

pop ax    ; 第一次POP

pop ax    ;第二次POP,此时将会发生栈底越界

  

修改SS、SP值,使SS:SP指向栈底:

 使用T命令进行执行:

 我们发现最后一次POP后,SP寄存器值变成12H,而AX寄存器的值被改为0H,因为内存地址1000H:10H字单元数据为0。此时SP指向的栈顶元素偏移越过了栈底位置,那么就发生了栈底越界。当然我们看不到什么异常发生,因为我们处在Debug调试下。

这次的栈底越界导致AX寄存器被修改为栈外未知数据,对于我们自己的程序而言,AX作为极其重要的通用寄存器被修改为一个未知数据,那么很有可能就导致了我们程序异常和崩溃。如果这时再进行一次PUSH,那么就会出现栈外未知数据被修改,从而引发系统程序崩溃以及更加严重的后果。

我们总结一下:

栈顶越界,往往会导致栈外未知内存单元数据被修改,可能会发生系统程序异常问题;亦会导致栈外未知内存单元数据被读入我们的程序中,使我们的程序发生异常问题。

 所以,我们在平常的开发中,一定要认真且谨慎的主要栈的使用,程序中PUSH和POP指令为对等数量,切勿出现栈顶越界问题。

修改SS、SP

现在我们知道SS段寄存器和SP寄存器它们两个用来访问栈很重要,在Debug中我们可以通过R命令来修改SS段寄存器和SP寄存器的值,那么我们在开发中,写汇编指令,如何修改这两个寄存器的值呢?

答案很简单,当然是使用MOV指令。

和DS寄存器一样,直接使用MOV指令去修改SS段寄存器是不可行的,所以我们需要使用通用寄存器来间接的修改:具体指令如下:

mov ax,1000H

mov ss,ax

 先将AX寄存器值修改为1000H,然后通过AX寄存器将1000H赋值给SS段寄存器,这样就实现了SS修改。基本和修改DS段寄存器一致。

修改SP寄存器就很简单了,直接使用MOV指令即可:

mov sp,10H

直接将SP赋值为10H。

在开发中,我们通常将这两个寄存器的赋值放在一起,即:

mov ax,1000H

mov ss ax

mov sp,10H

 这样书写起来能够使我们比较直观的看到SS:SP的指向,方便了在代码中对栈的操作。

下面我们就来亲自操作下:

使用A命令向内存中写入下面的汇编指令:

mov ax,1000H
mov ss,ax
mov sp,10H
push ax

为了方便观察结果,我们先修改SS、SP为不同的值:

 

 下面执行代码:

 我们发现,SS段寄存器被修改为1000H,符合预期,但是很奇怪的一点,SP也同时被修改为10H。我们使用T执行MOV SS,AX,执行后CS:IP直接就指向了:PUSH AX,而不是:MOV SP,10H。

既然SP也被修改了,那就说明MOV SP,10确实被执行了,什么时候执行的?答案就是在执行MOV SS,AX之后,自动执行的。

怎么会自动执行?Debug的T命令不是单步执行调试的嘛?答案还是由于中断!

因为T指令会产生中断,现在我们已经大致清楚了中断的过程,其中就涉及到寄存器入栈出栈操作。入栈和出栈需要什么?那肯定需要SS:SP呀,CPU根据这两个才能操作栈。修改了SS段寄存器,那么就要立刻对SP寄存器进行调整,因为它们两个合体兄弟,同生共死的那种。

我们试想一下,如果不会自动执行,那么会发生什么样的事情呢?

首先执行完:MOV SS,AX后,此时SS值为1000H,SP值为20H,那么中断过程中就会将数据写到1000H:20H处;这就意味着发生了什么?发生了栈顶越界啊同学们!因为我们规定的栈的范围为10000H~1000FH,结果现在SP中栈顶元素偏移超过了这个范围,应该要写入指定空间范围的数据被写到了栈外未知内存空间,影响了栈外内存数据,势必可能会对其他的运行程序产生一个不可逆转的影响!

所以,为了保证并避免栈顶越界问题出现,Debug 会自动执行MOV SP,10H指令,确保数据正确写入栈中,而不影响别处内存数据。

栈的总结

1、对于8086PC机来说,提供SS段寄存器用来存放栈段的段地址,提供SP寄存器用来存放栈顶元素的偏移地址;

2、提供入栈指令PUSH、出栈指令POP,指令执行的方式就是8086CPU根据SS:SP指向的地址,按照栈的特殊访问方式(LIFO)来访问内存单元;

3、任意时刻,SS:SP指向栈顶元素;

4、8086CPU只记录栈顶,实际栈空间需要我们开发人员自己关注并管理

5、用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈顺序要和入栈顺序相反(重要牢记)

6、PUSH、POP是成对出现的,代码中要注意PUSH、POP数量是否相等(重要牢记)

本篇结束语 

在本片博文中,我们补充讲解了栈顶越界问题和SS、SP赋值相关,并总结了栈相关的知识点,希望对大家理解学习栈有所帮助。

至此我们有关栈方面的学习就到此为止,接下来就正式步入上手开发环节。在实际的开发中,我们还会涉及到许多有关栈方面的操作和知识,希望小伙伴们能认真学习。

感谢围观,转发分享请标明出处,谢谢! 

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值