王爽《汇编语言》第四版 超级笔记



目录



第3章 寄存器(内存访问)

3.1 内存中的存储:字(word)

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

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

[汇编]《汇编语言》第3章 寄存器(内存访问)_寄存器

在图3.1中,我们用0、1两个内存单元存放数据 20000(4E20H)。

0、1两个内存单元用来存储一个字,这两个单元可以看作一个起始地址为0的字单元(存放一个字的内存单元,由0、1两个字节单元组成)。

对于这个字单元来说,0号单元是低地址单元,1号单元是高地址单元,则字型数据4E20H的低位字节存放在0号单元中,高位字节存放在1号单元中。

同理,将2、3号单元看作一个字单元,它的起始地址为2,在这个字单元中存放数18(0012H),则在2号单元中存放低位字节12H,在3号单元中存放高位字节00H。

我们提出字单元的概念:字单元,即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。

高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。

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

3.2 DS 和 [address]

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

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

mov bx,1000H
mov ds,bx
mov al,[0]

上面的3条指令将10000H(1000:0)中的数据读到al中。

下面详细说明指令的含义:

mov al,[0]

前面我们使用mov指令,可完成两种传送:

①将数据直接送入寄存器;

②将一个寄存器中的内容送入另一个寄存器。

也可以使用mov指令将一个内存单元中的内容送入一个寄存器中。

从哪一个内存单元送到哪一个寄存器中呢?

在指令中必须指明。寄存器用寄存器名来指明,内存单元则需用内存单元的地址来指明。

显然,此时mov指令的格式应该是:mov 寄存器名,内存单元地址。

“[•••]”表示一个内存单元,“[•••]”中的0表示内存单元的偏移地址。

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

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

如何把一个数据送入寄存器呢?

我们以前用类似“mov ax,1”这样的指令来完成,从理论上讲,我们可以用相似的方式:mov ds,1000H,来将1000H送入ds。

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

那么如何将1000H送入ds呢?

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

为什么8086CPU不支持将数据直接送入段寄存器的操作?

这属于8086CPU硬件设计的问题,我们只要知道这一点就行了。

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

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

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

完整的几条指令是:

mov bx,1000H
mov ds,bx
mov [0],al

3.3 字的传送

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

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

mov bx,1000H
mov ds,bx
mov ax,[0] ;1000:0处的字型数据送入ax
mov [0],ex ;cx中的16位数据送到1000:0处

[汇编]《汇编语言》第3章 寄存器(内存访问)_汇编语言_02

[汇编]《汇编语言》第3章 寄存器(内存访问)_出栈_03

内存中的情况如图3.3所示,写出下面的指令执行后内存中的值,思考后看分析。

[汇编]《汇编语言》第3章 寄存器(内存访问)_数据_04

[汇编]《汇编语言》第3章 寄存器(内存访问)_出栈_05

3.4 mov、add、sub 指令

mov、add、sub指令,它们都带有两个操作对象。

到现在,我们知道,mov指令可以有以下几种形式。

mov 寄存器,数据 比如:mov ax,8
mov 寄存器,寄存器 比如:mov ax,bx
mov 寄存器,内存单元 比如:mov ax,[0]
mov 内存单元,寄存器 比如:mov [0],ax
mov 段寄存器,寄存器 比如:mov ds,ax

我们可以根据这些己知指令进行下面的推测。

(1)既然有“mov 段寄存器,寄存器”,从寄存器向段寄存器传送数据,那么也应该有“mov 寄存器,段寄存器”,从段寄存器向寄存器传送数据。

一个合理的设想是: 8086CPU内部有寄存器到段寄存器的通路,那么也应该有相反的通路。

有了推测,我们还要验证一下。进入Debug,用A命令,如图3.4所示。

[汇编]《汇编语言》第3章 寄存器(内存访问)_学习笔记_06

图3.4中,用A命令在一个预设的地址0B39:0100处,用汇编的形式mov ax,ds写入指令,再用T命令执行,可以看到执行的结果,段寄存器ds中的值送到了寄存器ax中。 通过验证我们知道,“mov 寄存器,段寄存器”是正确的指令。

(2)既然有“mov 内存单元,寄存器”,从寄存器向内存单元传送数据,那么也应该有“mov 内存单元,段寄存器”,从段寄存器向内存单元传送数据。

比如我们可以将段寄存器cs中的内容送入内存10000H处,指令如下。

mov ax,1000H
mov ds,ax
mov [0],cs

在Debug中进行试验,如图3.5所示。

[汇编]《汇编语言》第3章 寄存器(内存访问)_寄存器_07

图3.5中,当CS:IP指向0B39:0105的时候,Debug显示当前的指令mov [0000],cs,因为这是一条访问内存的指令,Debug还显示出指令要访问的内存单元中的内容。

由于指令中的CS是一个16位寄存器,所以耍访问(写入)的内存单元是一个字单元,它的偏移地址为0,段地址在ds中,Debug在屏幕右边显示岀“DS:0000=0000”,我们可以知道这个字单元中的内容为0。

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

最后,用D命令从1000:0开始查看指令执行后内存中的情况,注意1000:0、1000:1两个单元的内容。

(3)“mov 段寄存器,内存单元”也应该可行,比如我们可以用10000H处存放的字型数据设置ds(即将10000H处存放的字型数据送入ds),指令如下。

mov ax,1000H
mov ds,ax
mov ds,[0]

可以自行在Debug中进行试验。

add和sub指令同mov一样,都有两个操作对象。

它们也可以有以下几种形式。

add 寄存器,数据 比如:add ax,8

add 寄存器,寄存器 比如:add ax,bx

add 寄存器,内存单元 比如:add ax,[0]

add 内存单元,寄存器 比如:add [0],ax

sub 寄存器,数据 比如:sub ax,9

sub 寄存器,寄存器 比如:sub ax,bx

sub 寄存器,内存单元 比如:sub ax,[0]

sub 内存单元,寄存器 比如:sub [0],ax

它们可以对段寄存器进行操作吗?

比如“add ds,ax”,请自行在Debug中试验。

3.5 数据段

对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。

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

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

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

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

比如,将123E0H~123B9H的内存单元定义为数据段。现在要累加这个数据段中的前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章 寄存器(内存访问)_学习笔记_08

3.1~3.5 小 结

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

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

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

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

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

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

3.6 栈、CPU提供的栈机制

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

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

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

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

[汇编]《汇编语言》第3章 寄存器(内存访问)_寄存器_09

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

显然,必须从盒子的最上边取。

这样取出的顺序就是:《软件工程》、《C语言》、《高等数学》,和放入的顺序相反,如图3.8所示。

[汇编]《汇编语言》第3章 寄存器(内存访问)_学习笔记_10

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

如果说,上例中的盒子就是一个栈,我们可以看出,栈有两个基本的操作:入栈和出栈。

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

栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的这种操作规则被称为:LIFO(Last In First Out,后进先出)。

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

8086CPU提供相关的指令来以栈的方式访问内存空间。这意味着,在基于8086CPU编程的时候,可以将一段内存当作栈来使用。

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

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

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

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

[汇编]《汇编语言》第3章 寄存器(内存访问)_数据_11

mov ax,0123H
push ax
mov bx,2266H
push bx
mov ex,1122H
push ex
pop ax
pop bx
pop ex

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

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指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。

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

[汇编]《汇编语言》第3章 寄存器(内存访问)_学习笔记_12

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

3.7 栈顶超界的问题

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

但还有一个问题需要讨论,就是SS和SP只是记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶。

可是,如何能够保证在入栈、出栈时,栈顶不会超出栈空间?

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

[汇编]《汇编语言》第3章 寄存器(内存访问)_出栈_13

图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

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

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

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

上面描述了执行push、pop指令时,发生的栈顶超界问题。

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

栈顶超界是危险的,因为我们既然将一段空间安排为栈,那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序中的,也可能是别的程序中的(毕竟一个计算机系统中并不是只有我们自己的程序在运行)。

但是由于我们在入栈出栈时的不小心,而将这些数据、代码意外地改写,将会引发一连串的错误。

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

不过,对于8086CPU,这只是我们的一个设想(我们当然可以这样设想,如果CPU是我们设计的话,这也就不仅仅是一个设想)。

实际的情况是,8086CPU中并没有这样的寄存器。

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

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

这点就好像CPU只知道当前要执行的指令在何处(由CS:IP指示),而不知道要执行的指令有多少。

从这两点上我们可以看出8086CPU的工作机理,它只考虑当前的情况:当前的栈顶在何处、当前要执行的指令是哪一条。

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

执行岀栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。

3.8 push、pop 指令

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中取得。

push ax是入栈指令,它将在栈顶之上压入新的数据。

一定要注意:它的执行过程是,先将记录栈顶偏移地址的SP寄存器中的内容减2,使得SS:SP指向新的栈顶单元,然后再将寄存器中的数据送入SS:SP指向的新的栈顶单元。

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提供的栈操作机制。

栈的综述

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

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

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

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

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

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

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

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

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

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

3.9 栈 段

对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。

我们可以将长度为N(N<=64KB)的一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来用,从而定义了一个栈段。

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

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

如何使得如push、pop等栈操作指令访问我们定义的栈段呢?

前面我们己经讨论过,就是要将SS:SP指向我们定义的栈段。

任意时刻,SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的地址为栈最底部的字单元的地址+2。栈最底部字单元的地址为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中寄存器的设置,即CS、IP,SS、SP,DS的指向。

要非常清楚CPU的工作机理,才能在控制CPU按照我们的安排运行的时候做到游刃有余。