汇编语言–寄存器(内存访问)

  • Post author:
  • Post category:其他


知识点:内存中字的存储、DS和[address]、字的传送、mov,add,sub指令、数据段、栈、CPU提供的栈机制、栈顶超界的问题、push,pop指令、栈段。

内存中字的存储

CPU中,用16位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。

在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连接的内存单元来存放,这个字的低位字节放在低地址单元中,高位字节存放在高地址单元中。

字单元:即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。

任何两个地址连续的内存单元,N号单元和N+1号单元,可以将它们看成两个内存单元,也可看成一个地址为N的字单元中的高位字节单元和低位字节单元。

DS和[address]

CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086CPU中,内存地址由段地址和偏移地址组成。

8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址。

mov指令,可完成三种传送:

(1)将数据直接送入寄存器;

mov 寄存器名,数据

(2)将一个寄存器中的内容送入另一个寄存器;

mov 寄存器名,寄存器名

(3)将一个内存单元中的内容送入一个寄存器中。

mov 寄存器,[内存单元的偏移地址]

“[…]”表示一个内存单元的偏移地址,我们知道,只有偏移地址是不能定位一个内存单元的,那么内存单元的段地址是多少呢?

指令执行时,8086CPU自动取ds中的数据为内存单元的段地址。

所以,我们需要根据情况,改变ds中的数据。

比如 mov ds, 1000H

但是,8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器,所以mov ds, 1000H这条指令是非法的。

那么如何将1000H送入ds呢?只好用一个寄存器来进行中转,即先将1000H送入一个一般的寄存器,如bx,再将bx中的内容送入ds。

怎样将数据从寄存器送入内存单元?

从内存单元到寄存器的格式是:mov 寄存器名,内存单元地址

从寄存器到内存单元则是:mov 内存单元地址,寄存器名。

将al中的数据送入内存单元10000H。

10000H可表示为1000:0,用ds存放段地址1000H,偏移地址是0,则:mov [0], al可完成从al到10000H的数据传送。

mov bx, 1000H

mov ds, bx

mov [0], al

字的传送

mov指令在寄存器和内存之间进行字节型数据的传送。

因为8086CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是说可以一次性传送一个字。

我们只要在mov指令中给出16位的寄存器就可以进行16位数据的传送了。

mov, add, sub指令

mov指令目前可以有以下几种形式:

mov 寄存器,数据 mov ax,8

mov 寄存器,寄存器 mov ax,bx

mov 寄存器,内存单元 mov ax,[0]

mov 内存单元,寄存器 mov [0],ax

mov 段寄存器,寄存器 mov ds,ax

add和sub指令同mov一样,都有两个操作对象,它们也可以有上面的几种形式。

这些形式中有些,两个操作数可以相互交换操作,这些需要用debug中的a命令和t命令多实践实践。

数据段

前面讲过,对于8086CPU,在编程时,我们可以根据需要,将一组内在单元定义为一个段。

我们可以将一组长度为N(N≤64K)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。

比如我们用:123B0H~123BAH这段内存空间来存放数据,我们就可以认为,123B0H~123BAH这段内存是一个数据段,它的段地址为123B,长度为10字节。

如何访问数据段中的数据呢?

将一段内存当作数据段,是我们在编程时的一种安排,我们可以在具体操作的时候,用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。

比如,我们将123B0H~123BAH的内存单元定义为数据段。

我们现在要累加这个数据段中的前3个单元中的数据。

mov ax, 123BH

mov ds, ax ;将123BH送入ds中,作为数据段的段地址。

mov al, 0 ;用al存放累加结果,先把它清零。

add al, [0] ;将数据段第一个单元(偏移地址为0)中的数值加到al中。

add al, [1] ;将数据段第一个单元(偏移地址为1)中的数值加到al中。

add al, [2] ;将数据段第一个单元(偏移地址为2)中的数值加到al中。

累加数据段中的前3个字型数据。

mov ax, 123BH

mov ds, ax ;将123BH送入ds中,作为数据段的段地址。

mov ax, 0 ;用ax存放累加结果,先把它清零。

add al, [0] ;将数据段第一个字(偏移地址为0)加到ax中。

add al, [2] ;将数据段第一个字(偏移地址为2)加到ax中。

add al, [4] ;将数据段第一个字(偏移地址为4)加到ax中。

小结

(1)字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。

(2)用mov指令要访问内存单元,可以在mov指令中只给出单元的偏移地址。此时,段地址默认在DS寄存器中。

(3)[address]表示一个偏移地址为address的内存单元。

(4)在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。

(5)mov, add, sub是具有两个操作对象的指令,jmp是具有一个操作对象的指令。

(6)可以根据自己的推测,在Debug中实验指令的新格式。

在这里,我们对栈的研究仅限于这个角度:栈是一种具有特殊的访问方式的存储空间。

它的特殊性就在于,最后进入这个空间的数据,最先出去。

栈有两个基本的操作:入栈和出栈。

入栈就是将一个新的元素到栈顶,出栈就是从栈顶取出一个元素。

栈顶的元素总是最后入栈,需要出栈和时,又最先被从栈中取出。

栈的这种操作规则被称为:LIFO(Last In First Out, 后进先出)。

CPU提供的栈机制

现今的CPU中都有栈的设计,8086CPU也不例外。

8086CPU提供相关的指令来以栈的方式访问内存空间。

这意味着,我们在基于8086CPU编程的时候,可以将一段内存当作栈来使用。

8086CPU提供入栈和出栈指令,最基本的两个是PUSH(入栈)和POP(出栈)。

比如:push ax 表示将寄存器ax中的数据据送入栈中,pop ax表示从栈顶取出数据送入ax。

8086CPU的入栈和出栈操作都是以字为单位进行的。

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

8086CPU中,有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。

任意时刻,SS:SP指向栈顶元素。push指令和pop指令执行时,CPU从SS和SP中得到栈顶的地址。

push ax的执行:

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

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

pop ax的执行过程和push ax刚好相反:

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

2)SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。

栈顶超界的问题

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

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

当栈满的时候再使用push指令入栈,或栈空的时候再使用pop指令出栈,都将发生栈顶超界问题。

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

8086CPU不保证我们对栈的操作不会超界。

也就是说,8086CPU只知道栈顶在何处(由SS:SP指示),而不知道读者安排的栈空间有多大。

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

push、pop指令

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

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

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

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

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

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

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

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

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

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

栈的综述

1)8086CPU提供了栈操作机制,方案如下:

在SS、SP中存放栈顶的段地址和偏移地址;

提供入栈和出栈指令,它们根据SS:SP指示的地址,按照栈的方式访问内存单元。

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

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

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

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

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

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

栈是一种非常重要的机制,一定要深入理解,灵活掌握。

栈段

前面讲过,对于8086CPU,在编程时,我们可以根据需要,将一组内存单元定义为一个段。我们可以将长度为N(N<=64K)的一组地址连接、起始地址为16的倍数的内存单元,当作栈空间来用,从而定义了一个栈段。

比如,我们将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以称为一个栈段,段地址为1000H,大小为16字节。

将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就在执行push、pop等栈操作指令时就自动地将我们定义的栈段当作栈空间来访问。

如何使得如push、pop等栈操作指令访问我们定义的栈段呢?就是要将SS:SP指向我们定义的栈段。

任意时刻,SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的地址为栈最底部的字单元的地址+2。

如果将10000H~1FFFFH这段空间当作栈段,栈最底部字单元的地址为1000:FFFE,所以栈空时,SP=0000H。

一个栈段最大可以设为多少?

从栈操作指令所完成的功能的角度上来看,push、pop等指令在执行的时候只修改SP,所以栈顶的变化范围是0~FFFFH,人栈空时候的SP=0,一直压栈,直到栈满时SP=0;如果再次压栈,栈顶将循环,覆盖了原来栈中的内容。所以一个栈段的容量最大为64KB。

段的综述

我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。这完全是我们自己的安排。

我们可以用一个段丰放数据,将它定义为“数据段”;

我们可以用一个段存放代码,将它定义为“代码段”;

我们可以用一个段当作栈,将它定义为“栈段”;

我们可以这样安排,但若要让CPU按照我们的安排来访问这些段,就要:

对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据来访问;

对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令;

对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需要进行栈操作的时候,比如执行push、pop指令等,就将我们定义的栈段当作栈空间来用。

可见,不管我们如何安排,CPU将的某段内容当作代码,是为因为CS:IP指向了那里;CPU将某段内存当作栈,是为因SS:SP指向了那里。

我们一定要清楚,什么是我们的安排,以及如何让CPU按我们的安排行事。要非常地清楚CPU的工作机理,才能在控制CPU来按照我们的安排运行的时候做到游刃有余。

比如我们将10000H~1001FH安排为代码段,并在这里存储如下代码:

mov ax,1000H

mov ss,ax

mov sp,0020H ;初始化栈顶

mov ax,cs

mov ds,ax ;设置数据段段地址

mov ax,[0]

mov ax,[2]

mov bx,[4]

mov bx,[6]

push ax

push bx

pop bx

pop ax

设置CS=1000H,IP=0,这段代码将得到执行,可以看到,在这段代码中,我们双将10000H~1001FH安排为栈段和数据段,10000H~1001FH这段内存,即是代码段,又是栈段和数据段。

一段内存,可骒既是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么也不是。

关键在于CPU中寄存器的设置,即:CS、IP、SS、SP、DS的指向。



版权声明:本文为forcj原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。