本人微信公众号"aeolian"~

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

寄存器

上一章笔记是从CPU执行指令的角度8086CPU的逻辑结构、形成物理地址的方法、相关的寄存器以及一些指令。这章节从访问内存的角度学习几个寄存器。

内存中字的存储

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

用0、1两个内存单元存放数据20000(4E20H) 。0 、1两个内存单元用来存储一个字,内存中字的存储单元可以看作一个起始地址为0的字单元(存放一个字的内存单元,由0、1两个字节单元组成)。对千这个字单元来说,0号单元是低地址单元,1号单元是高地址单元,则字型数据4E20H的低位字节存放在0号单元中,高位字节存放在1号单元中。同理,将2、3号单元看作一个字单元,它的起始地址为2。在这个字单元中存放数据18(0012H),则在2 号单元中存放低位字节12H,在3号单元中存放高位字节00H。

《汇编语言-03寄存器(内存访问)》

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

DS和[address]

CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址。

在8086PC 中,
内存地址由段地址和偏移地址组成。8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址。比如我们要读取10000H 单元的内容,可以用如下的程序段进行。

《汇编语言-03寄存器(内存访问)》

mov指令作用: 1.将数据直接送入寄存器;2.将一个寄存器中的内容送入另一个寄存器。

3.mov指令也可以将一个内存单元中的内容送入一个寄存器中。在指令中必须指明从哪一个内存单元送到哪一个寄存器。寄存器用寄存器名来指明,内存单元则需用内存单元的地址来指明。显然,此时mov指令的格式应该是:mov 寄存器名,内存单元地址。“[…]”表示一个内存单元, “[…]”中的0表示内存单元的偏移地址。只有偏移地址是不能定位一个内存单元的,在指令执行时,8086CPU自动取ds中的数据为内存单元的段地址

8086CPU不支待将数据直接送入段寄存器的操作,ds是一个段寄存器,所以mov ds,1000H这条指令是非法的。那么如何将1000H送入ds呢?只好用一个寄存器来进行中转,即先将1000H 送入一个一般的寄存器,如bx, 再将bx中的内容送入ds。

字的传送

mov指令在寄存器和内存之间进行字节型数据的传送。因为8086CPU 是16位结构,有16根数据线,所以,可以一次性传送16 位的数据,也就是说可以一次性传送一个字(两个内存单元)。只要在mov指令中给出16位的寄存器就可以进行16 位数据的传送了。

《汇编语言-03寄存器(内存访问)》

mov、add、sub指令

《汇编语言-03寄存器(内存访问)》

 此外,还有

mov 寄存器,段寄存器

例如:mov ax,ds

mov 内存单元,段寄存器 (因为段寄存器是16位,所以要占用两个字节/16位/四个十六进制,内存单元起始位为低位)

例如:

《汇编语言-03寄存器(内存访问)》

用a写好汇编指令,用t执行,用d查看。

mov [0000],cs 执行后, CS 中的数据(14E3H)被写入1000:0 处, 1000:1 单元存放14H, 1000:0 单元存放E3H 。

《汇编语言-03寄存器(内存访问)》

mov 段寄存器,内存单元

《汇编语言-03寄存器(内存访问)》

 

《汇编语言-03寄存器(内存访问)》

数据段

对千8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将一组长度为N(N~64KB) 、地址连续、起始地址为16 的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。比如用123B0H~123B9H 这段内存空间来存放数据,我们就可以认为, 123B0H~123B9H 这段内存是一个数据段,它的段地址为123BH, 长度为10 个字节。

用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。比如,将123B0H ~ 123B9H的内存单元定义为数据段。现在要累加这个数据段中的前3个单元中的数据,代码如下。

《汇编语言-03寄存器(内存访问)》

小结

(1) 字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
(2) 用mov指令访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。.
(3) [address]表示一个偏移地址为address的内存单元。
(4) 在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
(5) mov、add 、sub 是具有两个操作对象的指令。jmp是具有一个操作对象的指令。

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

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

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

CPU提供的栈机制

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

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

《汇编语言-03寄存器(内存访问)》 

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

《汇编语言-03寄存器(内存访问)》

8086CPU中,有两个寄存器,段寄存器SS和寄存器SP, 栈顶的段地址存放在SS中,偏移地址存放在SP中任意时刻,SS:SP指向栈顶元素。push指令和pop指令执行时,CPU从SS和SP中得到栈顶的地址。

push命令

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

(1) SP = SP – 2, SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
(2) 将ax中的内容送入SS:SP 指向的内存单元处, SS:SP 此时指向新栈顶。

《汇编语言-03寄存器(内存访问)》

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

栈空,SS:SP指向栈空间最高地址单元的下一个单元。执行push ax后, SS : SP指向栈中的第一个元素。10000H – 1000FH 这段空间当作栈,初始状态栈是空的,此时, SS=1000H,SP=0010H。

pop命令

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

(I) 将SS:SP 指向的内存单元处的数据送入ax 中;
(2) SP=SP+2, SS : SP 指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。

《汇编语言-03寄存器(内存访问)》

 

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

栈顶超界的问题

push后栈顶超出

将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单元处,将栈空间外的数据覆盖。

《汇编语言-03寄存器(内存访问)》

 

pop后栈顶超出

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

《汇编语言-03寄存器(内存访问)》

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

8086CPU不保证我们对栈的操作不会超界。这也就是说,8086CPU只知道栈顶在何处(由SS:SP 指示),而不知道我们安排的栈空间有多大。这点就好像CPU只知道当前要执行的指令在何处(由CS:IP 指示),而不知道要执行的指令有多少。从这两点上我们可以看出8086CPU的工作机理,它只考虑当前的情况: 当前的栈顶在何处、当前要执行的指令是哪一条。我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。

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

利用栈交换AX和BX数据

《汇编语言-03寄存器(内存访问)》

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。

栈总结

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

(2)push指令的执行步骤:1. SP=SP – 2。2. 向SS:SP指向的字单元中送入数据

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

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

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

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

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

栈段

在编程时,可以根据需要将一组内存单元定义为一个段。我们可以将长度为N(N~64KB)的一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来用,从而定义了一个栈段。比如,我们将10010H-1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以称为一
个栈段,段地址为1001H, 大小为16 字节。

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

栈段最大容量

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将内存中的某段内容当作数据,是因DS:[偏移量]指向了那里CPU将内存中的某段内容当作代码,是因CS:IP指向了那里;CPU将某段内存当作栈,是因为SS:SP指向了那里。我们一定要清楚,什么是我们的安排,以及如何让CPU按我们的安排行事。要非常清楚CPU的工作机理,才能在控制CPU按照我们的安排运行的时候做到游刃有余。

实验

段地址是放在段寄存器中的,在D命令后面直接给出段地址,是Debug提供的一种直观的操作方式。D命令是由Debug 执行的,Debug在执行“d 1000:0″这样的命令时,也会先将段地址1000H 送入段寄存器中。

访问内存的指令如“ mov ax,[]” 等一般都默认段地址在ds 中,所以Debug 在执行如“ d 段地址:偏移地址”这种D命令时,将段地址送入ds中比较方便。

D命令也提供了一种符合CPU机理的格式:“d 段寄存器:偏移地址”

《汇编语言-03寄存器(内存访问)》

E、A 、U这些可以带有内存单元地址的命令中,也可以同D命令一样,用段寄存器表示内存单元的段地址。

《汇编语言-03寄存器(内存访问)》

 

在Debug中,用A 命令写一段程序:

《汇编语言-03寄存器(内存访问)》

 

用t执行

《汇编语言-03寄存器(内存访问)》

 

点赞

Leave a Reply

Your email address will not be published. Required fields are marked *