汇编网首页登录博客注册
unixandlinux的学习博客
博客首页博客互动【做检测题】论坛求助

我的博客

个人首页 |  我的文章 |  我的相册 |  我的好友 |  最新访客 |  文章收藏 |  论坛提问 |  友情链接 |  给我留言  
图片载入中
学习动态
最新评论
最新留言
好友圈
友情链接

[2023-04-19 11:05] 《汇编语言》(王爽 著) —— 学习笔记(第3章)

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

第 2 章主要从 CPU 如何执行指令的角度学习 8086 CPU 的逻辑结构、形成物理地址的方法、相关的寄存器
以及一些指令。本章将从访问内存的角度继续学习几个寄存器。

3.1 内存中字的存储

CPU 中,用 16 位寄存器来存储一个字。高 8 位存放高位字节,低 8 位存放低位字节。在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
由此提出字单元的概念:字单元,即存放一个字型数据(16 位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
起始地址为 N 的字单元简称 N 地址字单元。比如一个字单元由 2、3 两个内存单元组成(数字 2、3 是这两个连续内存单元的地址),这个字单元的起始地址为 2,则可以说存放这个字的存储单元是 2 地址字单元。
任何两个地址连续的内存单元,例如 N 号单元和 N+1 号单元,都可以将它们看成两个内存单元,也可看成一个地址为 N 的字单元中的高位字节单元和低位字节单元。

问题 3.1
(没有挑战性,不写入笔记)

3.2 DS 和 [address]

CPU 要读写一个内存单元就必须先获取它的地址;8086 PC 的内存地址由段地址和偏移地址组成。8086 CPU 的 DS 寄存器通常被用于存放要访问数据的段地址。
访问内存单元的汇编指令仍然是 mov 指令,命令格式按实现的功能不同分为三种:
(1) 将指定的内存单元存储信息传送到指定的寄存器中
mov 寄存器名, 内存单元地址
(2) 将指定的寄存器存储信息传送到指定的内存单元中
mov 内存单元地址, 寄存器名
内存单元地址只书写其偏移地址,并用一对方括号括住,8086 CPU 自动默认从段寄存器 DS 中读取其段地址。例如:
mov bx, 1000H
mov ds, bx
mov al, [0]
这 3 条指令实现将 1000H(1000:0)地址的内存单元所存储的数据读到 al 中。特别强调,方括号 [] 的使用,说明了操作对象是一个内存单元,其括住的数据表示该内存单元的偏移地址,而段地址则在指令执行时由 8086 CPU 自动从寄存器 DS 中读取,因此称 DS 为段寄存器。
注意,8086 CPU 不支持将数据直接传送入段寄存器的操作(这属于 8086 CPU 硬件设计问题),所以下面的指令是非法的:
mov ds, 1000H
为实现将段地址传送入 DS 寄存器,需要用另一个寄存器作为中转,先将地址传送入一个一般的寄存器(例如 bx),再将一般寄存器中的数据传送给段寄存器 DS。
虽然不能使用 mov、push、add、sub 等汇编指令将数据传送到段寄存器,但 Debug 的 R 命令允许将任何数据传送到任何寄存器 —— 包括段寄存器。
总结 mov 指令的功能:
(1) 将数据直接传送入寄存器。
(2) 将寄存器存储的信息传送入另一个寄存器 —— 两个寄存器之间的信息互传。
(3) 寄存器与内存单元之间的信息互传。
注意,两个内存单元之间的信息互传,不能使用下面的指令(经上机测试验证):
mov [1], [0]
也就是说,将指定的内存单元存储信息传送到另一个指定的内存单元中,不能使用下面的命令格式:
mov <内存单元地址2>, <内存单元地址1>

问题 3.2(测试)

1. 编写将 al 中的数据送入内存单元 10000H 中的汇编指令
mov ax, 4AE7
mov bx, 1000
mov ds, bx
mov [0], al

2. 在 Debug 中执行上述汇编指令,验证其能否实现本题要求的目标
(以下省略号代表计算机在执行 Debug 命令后的输出内容)
(1) 向内存单元 100000H 前后存储单元中写入无关数据并查看验证
向 0ffffH~10002H 地址的存储空间写数据 01H 23H 45H 67H,存储在 10000H 内存单元的数据是 23H。
-e 0fff:f 01 23 45 67
-d 0fff:e f
...
-d 1000:0 3
...
(2) 将编写的汇编指令写入远离 10000H 内存单元的另一块内存空间,查看未执行指令前的寄存器存储内容
-a 2000:0
2000:0 mov ax, 4AE7
2000:3 mov bx, 1000
2000:6 mov ds, bx
2000:8 mov [0], al
-r
...
(3) 执行指令前再次验证 10000H 内存单元前后存储空间的数据
-d 0fff:e f
...
-d 1000:0 3
...
(4) 执行汇编指令
-r cs
CS=0741
:2000
-r ip
IP=0100
:0
-r
...
-t
...
-t
...
-t
...
-t
...
(5) 验证执行结果
-d 0fff:e f
...
-d 1000:0 3
...

3. 参照上面的方法,将内存单元 1000:0 存储的数据传送给地址为 1000:1 的内存单元
-e 1000:0 4A
-d 1000:0 2
1000:0 4A 00 00
-r cs
CS=0741
:3000
-r ip
IP=0100
:0
-a 3000:0
3000:0 mov bx, 1000
3000:3 mov ds, bx
3000:5 mov [1], [0]
        Error
当输入 mov [1],[0] 指令时即报错。

3.3 字的传送

在 16 位的寄存器与 8 位的内存单元之间进行存储信息互传,是允许的,这进行的是字传送;但将一个占用存储空间的大小与寄存器的容量不相同的常量数据传送给寄存器,则是不允许的,并且两个容量不同的寄存器之间也不能互传数据(参 p18~p19 问题 2.2)。
示例:
mov bx, 1000H
mov ds, bx
mov ax, [0]        ; 1000:0 处的字型数据送入 ax
mov [0], cx        ; cx 中的 16 位数据送到 1000:0 处
在 Debug 实验环境中测试该示例代码:
1. 将数据 584AH 存入地址 1000:0 的存储空间,使得 1000:0 地址成为存储数据 584AH 的字的内存空间
-e 1000:0
1000:0000 00.4a 00.58
-d 1000:0 f
1000:0000 4A 58 00 00 00 ....
2. 将示例代码写入内存 2000:0 起始的空间
-a 2000:0
2000:0000 mov cx, 11C3
2000:0003 mov bx, 1000
2000:0006 mov ds, bx
2000:0008 mov ax, [0]
2000:000B mov [0], cx
为了突显代码运行后,字内存空间存储的内容变化,添加了第一行代码,将数据 11C3H 存入 CX 寄存器;而字内存空间中的内容设置为初始值 584AH。
3. 修改 CS:IP,将 CPU 执行指令的指针指向所编写的代码所在内存空间
-r cs
CS=0741
:2000
-r ip
IP=0100
:0
-r
AX=0000 BX=0000 CX=0000 ....
DS=0741 ....        CS=2000 IP=0000 ....
2000:0000 ...        MOV CX,11C3 
4. 执行示例指令
-t

AX=0000 BX=0000 CX=11C3 ....
DS=0741 ....        CS=2000 IP=0003 ....
2000:0003 ...        MOV BX,1000 
-t

AX=0000 BX=1000 CX=11C3 ....
DS=0741 ....        CS=2000 IP=0006 ....
2000:0006 ...        MOV DS,BX
-t

AX=0000 BX=1000 CX=11C3 ....
DS=1000 ....        CS=2000 IP=0008 ....
2000:0008 ...        MOV AX,[0000]
-t

AX=584A BX=1000 CX=11C3 ....
DS=1000 ....        CS=2000 IP=000B ....
2000:000B ...        MOV [0000],CX
-t

AX=584A BX=1000 CX=11C3 ....
DS=1000 ....        CS=2000 IP=000F ....
2000:000F ...        ADD [BX+SI],AL
5. 验证 1000:0 地址的字内存空间存储内容是否由初始值 584AH 变化为 11C3H
-d 1000:0 f
1000:0000 C3 11 00 00 00 ...

问题 3.3

1. 内存中的情况如下:
 地址        数据(十六进制)
10000H                23
10001H                11
10002H                22
10003H                66

2. 执行指令:
mov ax, 1000H
mov ds, ax
mov ax, [0]
mov bx, [2]
mov cx, [1]
add bx, [1]
add cx, [2]
(1) 指令 mov ax, 1000H:将数据 1000H 存储到寄存器 AX 中。
(2) 指令 mov ds, ax:将通用寄存器 AX 存储的数据传送到段寄存器 DS 中。
(3) 指令 mov ax, [0]:CPU 首先自动读取段寄存器 DS 存储的数据 1000H 作为段地址,与指令中指定的偏移地址 0000H 组合成物理地址 10000H,将该物理地址指向的内存单元所存储的字传送到寄存器 AX 中;由于指令中的内存单元偏移地址是 0,所以该偏移地址与段地址组成的物理地址就是“字”的低 8 位,即该“字”的低 8 位地址是 10000H,高 8 位地址是 10001H。已知地址 10000H 所存储的数据为 23H,它和地址 10001H 所存储的数据 11H 组成一个“字”数据 1123H,可见传送到 AX 寄存器的数据就是 1123H。
(4) 指令 mov bx, [2]:如同前述 (3) 中的分析一样,本指令将地址 10002H 中存储的字传送给寄存器 bx,该字的低 8 位地址是 10002H,高 8 位地址是 10003H,而地址 10002H 内存单元存储的是 22H,地址 10003H 内存单元存储的是 66H,所以传送给 BX 寄存器的数据就是 6622H。
(5) 指令 mov cx, [1]:本指令传送的字的低 8 位地址是 10001H,高 8 位地址是 10002H,10001H 存储的数据是 11H,10002H 存储的数据是 22H,因此传送给寄存器 CX 的是数据 2211H。
(6) 指令 add bx, [1]:本指令将地址为 10001H 存储单元所存储的字数据与 BX 寄存器中存储的数据相加后,将结果存入 BX 寄存器。已知 10001H 存储空间的字由低 8 位地址 10001H 和高 8 位地址 10002H 的内存单元组成,而 10001H 地址的内存单元存储的数据是 11H,10002H 地址的内存单元存储的数据是 22H,所以该字数据为 2211H,由上述 (4) 可知 BX 寄存器存储的数据是 6622H,所以两者相加的结果是 8833H,可见指令执行
后 BX 寄存器存储的数据变成 8833H。
(7) 指令 add cx, [2]:本指令将地址为 10002H 存储单元所存储的字数据与 CX 寄存器中存储的数据相加后,将结果存入 CX 寄存器。已知 10002H 存储空间的字由低 8 位地址 10002H 和高 8 位地址 10003H 的内存单元组成,而 10002H 地址的内存单元存储的数据是 22H,10003H 地址的内存单元存储的数据是 66H,所以该字数据为 6622H,由上述 (5) 可知 CX 寄存器存储的数据是 2211H,所以两者相加的结果是 8833H,可见指令执行
后 CX 寄存器存储的数据变成 8833H。

3. 通过在 Debug 实验环境执行上述汇编指令,以验证结果。
(1) 将数据存入地址 1000:0~1000:3 的存储空间
-e 1000:0
1000:0000 00.23 00.11 00.22 00.66
-d 1000:0 f
1000:0000 23 11 22 66 00 00 00 ...
(2) 将示例代码写入内存 2000:0 起始的空间
-a 2000:0
2000:0000 mov ax, 1000H
2000:0003 mov ds, ax
2000:0005 mov ax, [0]
2000:0008 mov bx, [2]
2000:000C mov cx, [1]
2000:0010 add bx, [1]
2000:0014 add cx, [2]
2000:0018
(3) 修改 CS:IP,将 CPU 执行指令的指针指向所编写的代码所在内存空间
-r cs
CS=0741
:2000
-r ip
IP=0100
:0
-r
AX=0000 BX=0000 CX=0000 ....
DS=0741 ....        CS=2000 IP=0000 ....
2000:0000 ...        MOV AX,1000 

(4) 执行汇编指令
-t

AX=1000 BX=0000 CX=0000 ....
DS=0741 ....        CS=2000 IP=0003 ....
2000:0003 ...        MOV DS,AX
-t

AX=1000 BX=0000 CX=0000 ....
DS=1000 ....        CS=2000 IP=0005 ....
2000:0005 ...        MOV AX,[0000]
-t

AX=1123 BX=0000 CX=0000 ....
DS=1000 ....        CS=2000 IP=0008 ....
2000:0008 ...        MOV BX,[0002]
-t

AX=1123 BX=6622 CX=0000 ....
DS=1000 ....        CS=2000 IP=000C ....
2000:000C ...        MOV CX,[0001]
-t

AX=1123 BX=6622 CX=2211 ....
DS=1000 ....        CS=2000 IP=0010 ....
2000:0010 ...        ADD BX,[0001]
-t

AX=1123 BX=8833 CX=2211 ....
DS=1000 ....        CS=2000 IP=0014 ....
2000:0014 ...        ADD CX,[0002]
-t

AX=1123 BX=8833 CX=8833 ....
DS=1000 ....        CS=2000 IP=0018 ....
2000:0018 ...        ADD [BX+SI],AL

(5) 验证 1000:0 地址的字内存空间存储内容是否由初始值 584AH 变化为 11C3H
-d 1000:0 f
1000:0000 C3 11 00 00 00 ...

问题 3.4

1. 内存中的情况如下:
 地址        数据(十六进制)
10000H                23
10001H                11
10002H                22
10003H                11

2. 执行指令:
mov ax, 1000H
mov ds, ax
mov ax, [0]
mov ax, 11316
mov [0], ax
mov bx, [0]
sub bx, [2]
mov [2], bx
(1) 指令 mov ax, 1000H:将数据 1000H 存储到寄存器 AX 中。
(2) 指令 mov ds, ax:将通用寄存器 AX 存储的数据传送到段寄存器 DS 中。
(3) 指令 mov ax, [0]:CPU 首先自动读取段寄存器 DS 存储的数据 1000H 作为段地址,与指令中指定的偏移地址 0000H 组合成物理地址 10000H,将该物理地址指向的内存单元所存储的字传送到寄存器 AX 中;由于指令中的内存单元偏移地址是 0,所以该偏移地址与段地址组成的物理地址就是“字”的低 8 位,即该“字”的低 8 位地址是 1000:0,高 8 位地址是 1000:1。已知 1000:0 所存储的数据为 23H,它和 1000:1 所存储的数据 11H 组成一个“字”数据 1123H,可见传送到 AX 寄存器的数据就是 1123H。
(4) 指令 mov ax, 11316:本指令将数据 11316 存入寄存器 AX。注意,11316 没有后缀,表示十进制数,转换为十六进制数为 2C34H。在 Debug 实验环境中输入本指令时,要输入 2C34 —— 注意,Debug 默认所输入的数据均为十六进制,因此不要在输入的 2C34 末尾添加后缀 H,否则 Debug 会将 H 认作一个十六进制数字而报错。执行后 AX 寄存器存储的数据为 2C34H。
(5) 指令 mov [0], ax:本指令将寄存器 AX 存储的包含 16 位的 2 字节数据存入内存地址 1000:0 的字存储单元中。AX 寄存器存储的数据是 2C34H,其低 8 位数据为 34H 将存入 1000:0 内存单元中,高 8 位数据 2CH 将存入 1000:1 内存单元中。执行的结果是,1000:0 存储单元存储的数据由原来的 23H 变为 34H,1000:1 存储单元存储的数据由原来的 11H 变为 2CH。
(6) 指令 mov bx, [0]:本指令将 1000:0 存储单元所存储的字数据存入 BX 寄存器中。已知 1000:0 存储空间的字由低 8 位 1000:0 和高 8 位 1000:1 的内存单元组成,1000:0 内存单元存储的数据是 34H,而 1000:1 内存单元存储的数据是 2CH,所以该字数据为 2C34H,指令执行后 BX 寄存器存储的数据变成 2C34H。
(7) 指令 sub bx, [2]:本指令将 BX 寄存器存储的数据减去 1000:2 存储单元所存储的字数据后,其计算结果存入 BX 寄存器。已知 1000:2 存储空间的字由低 8 位 1000:2 和高 8 位地址 1000:3 内存单元组成,1000:2 内存单元存储的数据是 22H,1000:3 内存单元存储的数据是 11H,所以该字数据为 1122H。由 (6) 可知 BX 寄存器存储的数据为 2C34H,因此计算结果 2C34H - 1122H = 1B12H 将存入 BX 寄存器中。
(8) 指令 mov [2], bx:本指令将寄存器 BX 存储的包含 16 位的 2 字节数据 1B12H(参见 (7))存入 1000:2 字存储空间。BX 存储的数据 1B12H,其低 8 位数据为 12H 将存入 1000:2 内存单元中,高 8 位数据 1BH 将存入 1000:3 内存单元中。执行的结果是,1000:2 存储单元存储的数据由原来的 22H 变为 12H,1000:3 存储单元存储的数据由原来的 11H 变为 1BH。

3. 最后的执行结果:
(1) 内存单元的情况
 地址        数据(十六进制)
10000H                34
10001H                2C
10002H                12
10003H                1B
(2) 寄存器的情况
AX=2C34H BX=1B12H DS=1000H
        
4. 参照“问题 3.3”,在 Debug 实验环境执行上述汇编指令,以验证结果。(略)

3.4 mov、add、sub 指令

mov 指令有两个操作对象,mov 指令可以有以下形式:
mov 寄存器, 数据        示例:mov ax, 8
mov 寄存器, 寄存器        示例:mov ax, bx
mov 寄存器, 内存单元        示例:mov ax, [0]
mov 内存单元, 寄存器        示例:mov [0], ax
mov 段寄存器, 寄存器        示例:mov ds, ax
由上述指令形式可以推测出以下有效的指令:
1. mov 寄存器, 段寄存器
功能:从段寄存器向其他寄存器传送数据。
2. mov 内存单元, 段寄存器
功能:从段寄存器向内存单元传送数据。
3. mov 段寄存器, 内存单元
功能:从字型内存单元向段寄存器传送数据。
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 和 sub 指令中,如果其中的一个寄存器是段寄存器替换,则指令报错,即段寄存器不允许参与到 add 和 sub 指令运算中。

3.5 数据段

前已述(2.8 节),8086 PC 机在编程时可以根据需要将一组内存单元定义为一个段。即将一组长度为 N(N ≤ 64 KB)、地址连续、起始地址为 16 的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。
将一段内存当作数据段,是在编程时的一种人为安排;具体操作时,用 ds 存放数据段的段地址,再根据需要用相关指令访问数据段中的具体单元。例如,将 123BOH~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 行的 mov 指令将寄存器 AL 的初始值设置为 0,只有这样才能实现累加后能得到正确的结果 —— 如果不设置其初始值为 0,则累加的结果就不是数据段前 3 个单元数据的正确累加值。

问题 3.5

认定 123B0H~123B9H 的内存单元为数据段,需要累加数据段中前 3 个字型数据,编写汇编指令如下:
mov ax, 123BH
mov ds, ax   ; 将 123BH 送入 ds 中,作为数据段的段地址
mov ax, 0   ; 用 ax 存放累加结果
add ax, [0] ; 将数据段第一个字(偏移地址为 0)加到 ax 中
add ax, [2] ; 将数据段第三个字(偏移地址为 2)加到 ax 中
add ax, [4] ; 将数据段第三个字(偏移地址为 4)加到 ax 中
注意,一个字型数据占两个单元,所以偏移地址是 0、2、4。

3.1~3.5 节及运算注意事项小结:
(1) 寄存器(包括段寄存器)之间的数据传送要求这两个寄存器具有相同的容量,否则即为非法运算。
(2) 常量数据传送到寄存器(不允许为段寄存器)时,要求常量数据占用的存储空间大小必须与寄存器的容量相同,否则即为非法运算。
(3) 字在内存中存储时,要用两个地址连续的内存单元来存放,字的低 8 位存放在低地址单元中,高 8 位存放在高地址单元中。例如字 A1E5H 存储在 1000:0 和 1000:1 两个内存单元中,字的低 8 位 E5H 存放在 1000:0 内存单元,字的高 8 位 A1H 存放在 1000:1 内存单元。
(4) 用 mov 指令访问内存单元,可以在 mov 指令中只给出单元的偏移地址,段地址默认在 DS 寄存器中。
(5) [addres] 表示一个偏移地址为 addres 的内存单元。
(6) 寄存器(包括段寄存器)与内存单元之间发生数据传送时,如果寄存器为 16 位,则进行的是字传送;如果寄存器为 8 位,则进行的是字节传送。字传送时,寄存器低 8 位与字存储单元的低 8 位进行数据互传,寄存器高 8 位与字存储单元的高 8 位进行数据互传。
(7) 不允许将常量数据直接传送给内存单元或段寄存器 —— 但在 Debug 中可以用 R 命令将常量数据传送到段寄存器,用 E 命令将常量数据传送到内存单元。
(8) 不允许在两个内存单元之间通过 mov、add、sub 进行传送数据操作。
(9) mov、add、sub 都是具有两个操作对象的指令。jmp 是具有一个操作对象的指令。

检测点 3.1

(一)
在 Debug 实验环境中执行以下操作:
(1) 将题目中要求的数据输入指定的 0000:0000~0000:001f 内存空间
-e 0000:0
0000:0000 00.70 00.80 00.f0 00.30 00.ef 00.60 00.30 00.e2
0000:0008 00.   00.80 00.80 00.12 00.66 00.20 00.22 00.60
0000:0010 00.62 00.26 00.e6 00.d6 00.cc 00.2e 00.3c 00.3b
0000:0018 00.ab 00.ba 00.   00.   00.26 00.06 00.66 00.88
(2) 验证内存空间 0000:0000~0000:001f 中存储的数据是否与题目给出的初始数据一致
-d 0000:0 10
...
(3) 输入要执行的汇编指令到内存空间 2000:0000 中
-a 2000:0
2000:0000 mov ax, 1
2000:0003 mov ds, ax
2000:0005 mov ax, [0000]
2000:0008 mov bx, [0001]
2000:000C mov ax, bx
2000:000E mov ax, [0000]
2000:0010 mov bx, [0002]
2000:0013 add ax, bx
2000:0015 add ax, [0004]
2000:0018 mov ax, 0
2000:001B mov al, [0002]
2000:001E mov bx, 0
2000:0022 mov bl, [000C]
2000:0025 add al, bl
(4) 修改 CS:IP 指令指针值,使得指令指针指向上述汇编指令所存储的内存空间中
-r cs
CS=0741
:2000
-r ip
IP=0100
:0
(5) 查看汇编指令执行前各个寄存器存储的数据
-r
AX=0000 BX=0000 ...
DS=0741 ....        CS=2000 IP=0000 ...
2000:0000 ...        MOV AX,0001
(6) 使用 T 命令执行上述汇编指令
-t
...
(7) 汇编指令执行的结果如下:
mov ax, 1                AX = 0001H
mov ds, ax                DS = 0001H
mov ax, [0000]        AX = 2662H  0001:0000~0001:0001,即 00010H~00011H,Data:62H 26H,Word:2662H
mov bx, [0001]        BX = E626H  0001:0001~0001:0002,即 00011H~00012H,Data:26H E6H,Word:E626H
mov ax, bx                AX = E626H        AX=BX=E626H
mov ax, [0000]        AX = 2662H  同于第 3 行代码
mov bx, [0002]        BX = D6E6H        0001:0002~0001:0003,即 00012H~00013H,Data:E6H D6H,Word:D6E6H
add ax, bx                AX = FD48H        AX=BX+AX=D6E6H+2662H=FD48H
add ax, [0004]        AX = 2C14H        [0004]:00014H~00015H,CCH 2EH,2ECCH        AX=2ECCH+AX=2ECCH+FD48H=2C14H
mov ax, 0                AX = 0000H
mov al, [0002]        AX = 00E6H        0001:0002 即 00012H,Data:E6H,AL=E6H,AX=00E6H
mov bx, 0                BX = 0000H
mov bl, [000C]        BX = 0026H        0001:000C 即 0001CH,Data:26H,BL=26H,BX=0026H
add al, bl                AX = 000CH        AL:E6H                BL:26H                AL=AL+BL=E6H+26H=0CH,AX=000CH

(二)

初始值:CS=2000H IP=0 DS=1000H AX=0 BX=0
指令:
 地址                指令                 机器码                         地址                指令                  机器码
10000H        mov ax, 2000H        B8 00 20                20000H        mov ax, 6622H        B8 22 66
10003H        mov ds, ax                8E D8                        20003H        jmp 0ff0:0100        EA 00 01 F0 0F
10005H        mov ax, [0008]        A1 08 00                20008H        mov bx, ax                89 C3
10008H        mov ax, [0002]        A1 02 00

注意,在执行地址 20003H 处 jmp 指令时,是先读取此处的 jmp 指令,接着 IP 自动增值,然后再执行该 jmp 指令,执行完该 jmp 指令使得 CS:IP 指向新的内存位置。

1. 解答
(1) CPU 执行的指令序列如下:
mov ax, 6622H
jmp 0ff0:0100
mov ax, 2000H
mov ds, ax
mov ax, [0008]
mov ax, [0002]
位于内存单元 20008H 的指令 mov bx, ax 不会被执行。
(2) 每条指令执行后,CS、IP 和相关寄存器存储的值如下(仅列示值有变化的寄存器):
mov ax, 6622H        ; CS=2000H IP=3 AX=6622H
jmp 0ff0:0100        ; CS=0ff0H IP=0100H 读取本指令后 IP 加 5,成为 8,执行本条 jmp 指令后使得 IP=0100H
   ; CS:IP 指令指针指向物理地址 0ff00H+0100H=10000H
mov ax, 2000H        ; IP=0103H CS:IP 指向物理地址 0ff00H+0103H=10003H,AX=2000H
mov ds, ax   ; IP=0105H CS:IP 指向物理地址0ff00H+0105H=10005H,DS=2000H
mov ax, [0008]        ; IP=0108H CS:IP 指向物理地址 0ff00H+0108H=10008H,AX=DS:[0008]=[20008]=C389H
mov ax, [0002]        ; IP=010BH CS:IP 指向物理地址 0ff00H+010BH=1000BH,AX=DS:[0002]=[20002]=EA66H
可见指令执行完毕后,各寄存器存储的数据如下:
AX=EA66H BX=0000H CS=0ff0H IP=010BH DS=2000H
注意,20008H 地址处的指令不会被执行,这使得 BX 从始至终没有改变过其存储的数据。
(3) 数据和程序(指令)在计算机中的存在状态完全相同,都是以二进制信息方式存储在计算机中。计算机将通过 CPU 的 CS 和 IP 寄存器所存储的数据组合成的物理地址来指定程序指令信息的范围,同时,包括已经被指定为程序指令信息的所有二进制信息,都可以被用作数据信息。参见下面第 2 点第 (4) 小点对指令作适当修改后进行的测试验证。

2. 在 Debug 实验环境中执行以下操作,验证上述分析:
(1) 按题目中的内存数据设置内存空间存储的内容(通过使用 A 命令输入汇编指令的方式来设置):
-a 1000:0
1000:0000 mov ax, 2000H
1000:0003 mov ds, ax
1000:0005 mov ax, [0008]
1000:0008 mov ax, [0002]
1000:000B
-a 2000:0
2000:0000 mov ax, 6622H
2000:0003 jmp 0ff0:0100
2000:0008 mov bx, ax
2000:000A
(2) 设置 CS、IP、AX、BX、DS 寄存器的初始值,以与题目要求一致,并验证之
-r cs
CS=0741
:2000
-r ip
IP=0100
:0
-r AX
AX=...
:0
-r BX
BX=...
:0
-r DS
DS=0741
:1000
-r
AX=0000 BX=0000 ...
DS=1000 ....        CS=2000 IP=0000 ...
2000:0000 ...        MOV AX,6622
(3) 使用 T 命令执行上述汇编指令以验证结果是否与分析一致
-t
...
(4) 删除指令 mov ds, ax,并修改部分指令,修改指令后成为如下形式,以验证是否在 CS:IP 指令指针指向的
内存空间范围内的内存单元也可以被视作数据信息 —— 参见前面的分析
第 1 种测试:
设置初始值:CS=2000H IP=0 DS=1000H AX=0 BX=0
 地址                指令                 机器码                         地址                指令                  机器码
10000H        mov ax, 2000H        B8 00 20                20000H        mov ax, 6622H        B8 22 66
10003H        mov ax, [0006]        A1 06 00                20003H        jmp 0ff0:0100        EA 00 01 F0 0F
10006H        mov ax, [0008]        A1 08 00                20008H        mov bx, ax                89 C3
10009H                                                                        2000AH
执行指令的结果如下:
mov ax, [0006]        ; IP=0106H CS:IP 指向物理地址 0ff00H+0106H=10006H,AX=DS:[0006]=[10006]=08A1H
mov ax, [0008]        ; IP=0109H CS:IP 指向物理地址 0ff00H+0109H=10009H,AX=DS:[0008]=[10008]=0000H
第 2 种测试:
设置初始值:CS=2000H IP=0 DS=1000H AX=0 BX=0
 地址                指令                 机器码                         地址                指令                  机器码
10000H        mov ax, 2000H        B8 00 20                20000H        mov ax, 6622H        B8 22 66
10003H        mov ax, [0006]        A1 06 00                20003H        jmp 0ff0:0100        EA 00 01 F0 0F
10006H        mov ax, [0007]        A1 07 00                20008H        mov bx, ax                89 C3
10009H                                                                        2000AH
执行的结果如下:
mov ax, [0006]        ; IP=0106H CS:IP 指向物理地址 0ff00H+0106H=10006H,AX=DS:[0006]=[10006]=07A1H
mov ax, [0007]        ; IP=0109H CS:IP 指向物理地址 0ff00H+0109H=10009H,AX=DS:[0007]=[10007]=0007H
第 3 种测试:
设置初始值:CS=2000H IP=0 DS=1000H AX=0 BX=0
 地址                指令                 机器码                         地址                指令                  机器码
10000H        mov ax, 2000H        B8 00 20                20000H        mov ax, 6622H        B8 22 66
10003H        mov ax, [0006]        A1 06 00                20003H        jmp 0ff0:0100        EA 00 01 F0 0F
10006H        mov ax, [0006]        A1 06 00                20008H        mov bx, ax                89 C3
10009H                                                                        2000AH
执行的结果如下:
mov ax, [0006]        ; IP=0106H CS:IP 指向物理地址 0ff00H+0106H=10006H,AX=DS:[0006]=[10006]=06A1H
mov ax, [0006]        ; IP=0109H CS:IP 指向物理地址 0ff00H+0109H=10009H,AX=DS:[0006]=[10006]=06A1H
上述测试的区别仅在于地址 10006H 处的指令稍有不同,而指令执行的结果则完全不同,但都充分说明,可以将当前正在执行的指令的二进制信息当作数据信息存储到指定寄存器中。也就是说,包括已经被指定为程序指令的所有二进制信息,无论该指令当前是否正在执行,都可以被用作数据信息。

3.6 栈

栈是一种具有特殊的访问方式的存储空间。它的特殊性在于,最后进入这个空间的数据,最先出去。
栈有两个基本操作:入栈和出栈。入栈是将一个新的元素放到栈顶,出栈是从栈顶取出一个元素。栈顶元素总是最后入栈,出栈时又最先被从栈中取出。栈的这种操作规则称为 LIFO(Last In First Out,后进先出)。

3.7 CPU 提供的栈机制

8086 CPU 提供相关的指令使得能以栈的方式访问内存空间。这意味着在基于 8086 CPU 编程时,可以将一段内存当作栈来使用。
8086 CPU 提供两个最基本的栈指令,是 PUSH(入栈)和 POP(出栈)。比如,push ax 表示将寄存器 ax 中的数据送入栈中,pop ax 表示从栈顶取出数据送入 ax。
8086 CPU 的入栈和出栈操作都是以字为单位进行的。
注意,字型数据用两个单元存放,高地址单元存放字的高 8 位数据,低地址单元存放字的低 8 位数据,而且总是以低 8 位地址作为该字型数据的地址。
CPU 如何知道某段地址空间会被当作栈来使用(这个问题在下一小节回答)?push、pop 在执行时如何知道哪个存储单元是栈顶单元?
8086 CPU 有两个寄存器 —— 段寄存器 SS 和寄存器 SP,栈顶的段地址存放在 SS 中,偏移地址存放在 SP 中,这与 CPU 通过 CS:IP 确认程序指令所在地址的情况类似。任意时刻,SS:SP 指向栈顶元素。push 指令和 pop 指令执行时,CPU 从 SS 和 SP 中得到栈顶的地址。
push ax 的执行,由以下两步完成:
(1) SP=SP-2,SS:SP 指向当前栈顶前面的单元,以当前栈顶相邻前 1 个存储单元为新的栈顶。
(2) 将 ax 中的内容送入 SS:SP 指向的内存单元处,SS:SP 此时指向新栈顶。
8086 CPU 中,入栈时,栈顶从高地址向低地址方向延伸。
对于字型数据,仿佛存在一个字指针,字指针总是指向字型数据的低 8 位地址单元。本节前面讲过,出入栈操作都是以字为单位进行,可见栈顶元素就是一个字型数据;出入栈的操作对象是 SS:SP 指向的内存单元,可见 SS:SP 就是栈元素字型数据的字指针;因此 SS:SP 指向的正是栈顶元素字型数据的低 8 位地址单元而不是高 8 位地址单元。字型数据的低 8 位地址代表该字型数据的地址,可见表示字型数据的 [address] 与栈顶元素字型数据一一对应,而 [address] 与寄存器名表示的字型数据一一对应,所以寄存器名表示的字型数据也与栈顶元素字型数据一一对应。
示例代码:
mov ax, 0123H
push ax
mov bx, 2266H
push bx
mov cx, 1122H
push cx
pop ax
popbx
pop cx

问题 3.6

对于一个空栈,SS:SP 将指向一个怎样的位置?
假设栈中只存在一个栈顶元素 A,那么 SS:SP 将指向栈顶元素字型数据 A 的低 8 位内存单元;由于入栈是从高地址向低地址方向延伸的,因此栈为空时 SS:SP 将指向与 A 相邻的更高位地址处另一个字数据 B 的低 8 位内存单元,该内存单元的地址等于 A 的高 8 位单元地址加 1 —— 这就是空栈时 SS:SP 指向的内存单元。

pop ax 的执行过程和 push ax 刚好相反,由以下两步完成:
(1) 将 SS:SP 指向的内存单元处的字型数据送入 AX 寄存器。
(2) SP=SP+2,SS:SP 指向当前栈顶字数据高 8 位单元的相邻更高位下一个单元,并以之为新的栈顶元素。

注意,pop 指令执行出栈操作,只是将栈顶元素字数据传送到指定寄存器,然后令 SS:SP 指向新的栈顶元素。操作前的栈顶元素依然存在于原存储单元位置(经上机测试,书中 p61 关于图 3.12 所述“出栈后,SS:SP 指向新的栈顶 1000EH,pop 操作前的栈顶元素,1000CH 处的 2266H 依然存在”这句话与测试结果不一致,地址 1000CH 的内存单元存储的数据已经改变,不再是原来的数据 2266H,所以本笔记按书本的意思记录的这句话不正确),只是它已不在栈内;当再次执行 push 入栈操作时,SS:SP 就又指向它,并向它写入新的字数据 —— 用指定寄存器的字数据覆盖它。
(以下只是本人根据上机实践结果,为理解本书知识而作的臆测,与真实、正确答案不符)实际上,栈空间内会有一组特定的数据值(我称其为栈顶标识值)被存储在 SP-1 地址内存单元,这组特定的数据会占用连续的多个内存单元,而 SP-1 单元则是这组数据的最高 8 位数据所存储的位置。这组数据会随着 SP 的变化而移动。例如,栈空间的初始设置为 SS=1000H,SP=0010H,这时栈状态为空,而栈顶标识值的最高 8 位就存储在 SP-1 内存单元(即物理地址 1000FH 内存单元,也就是栈顶元素的高 8 位)中,如果这时执行 push 操作,先执行 push 指令的第 1 步操作:SP=SP-2。栈顶标识值就会随之移动,其最高 8 位就存储到新 SP 的 SP-1 单元;接着才执行 push 指令的第 2 步操作,入栈的数据将覆盖原来栈顶标识值所在的栈顶位置。当执行 pop 指令时,在执行 SP=SP+2 操作后,同样地,栈顶标识值随着 SP 的变化而移动 —— 该值最高 8 位数据存储到新 SP 的 SP-1 单元,而原来的栈顶数据就被栈顶标识值数据所覆盖。
注意,并不是在设置 SS:SP 后就会产生栈顶标识值数据,而是在用 Debug 的 T 命令执行指令后,才会产生栈顶标识值数据。也就是说,当改变 SS:SP 的设置后,并不会立即在新设置的栈顶处(新 SP 的 SP-1 内存单元)产生栈顶标识值数据,而必须通过 Debug 的 T 命令执行指令后,才会在新设置的栈顶处生成栈顶标识值数据。

3.8 栈顶超界的问题

8086 CPU 用 SS 和 SP 指示栈顶地址,并提供 push 和 pop 指令实现入栈和出栈。但 SS 和 SP 只是记录栈顶地址,依靠 SS 和 SP 可以保证入栈和出栈时找到栈顶,却无法保证入栈、出栈时栈顶不会越过栈空间?
当越过栈空间执行 push 指令,就会把寄存器的字数据送入栈空间外的存储单元,将栈空间外所存储的数据覆盖。在栈状态为空时仍然继续使用 pop 指令,导致栈顶越过栈空间(注意,pop 指令本身是不会改变任何存储单元所存储的数据的);这时如果执行 push 指令,就会跟前述一样把寄存器的字数据送入栈空间外的存储单元,从而将栈空间外的数据覆盖。
栈顶越界是危险的,因为在栈空间之外的存储空间很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是自己程序中的,也可能是别的程序中的,改写这些数据、代码很可能会引发一连串的错误。选定栈空间的操作虽然是人为安排的,但当然是选择安全范围内的存储空间来用作栈空间 —— 不会影响系统安全和其他程序的运行。
8086 CPU 不保证对栈的操作不会越界,8086 CPU 只知道栈顶在何处(由 SS:SP 指示),而不知道所安排的栈空间有多大。正如 CPU 只知道当前要执行的指令在何处(由 CS:IP 指示),而不知道要执行的指令有多少一样。由此可以看出 8086 CPU 的工作机理 —— 它只考虑当前的情况:当前的栈顶在何处、当前要执行的指令是哪一条。
因此编程时程序员要负责防止栈顶超界的问题,要根据可能用到的最大栈空间来安排栈的大小,防止入栈数据太多导致越界;执行出栈操作时也要注意,以防栈状态为空时继续出栈而导致越界。

3.9 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、pop 和 mov 指令,会发现栈操作指令 push 和 pop 就是在栈空间中按特定的方式执行的 mov 指令。
特别注意,当 push 和 pop 指令的操作对象是内存单元时,它是在两个内存单元之间进行数据传送,而在两个内存单元之间进行数据传送操作是 mov、add、sub 指令所不允许的。

问题 3.7

将 10000H~1000FH 这段空间当作栈,初始状态为空,要把 AX、BX、DS 中的数据入栈,应该怎样编写程序指令?
由于栈空间范围是 10000H~1000FH 且初始状态为空,这时 SS:SP 指向的地址为 1000FH + 1 = 10010H(参考“问题 3.6”),设置 SS=1000H,则 SP=0010H。由于 SS 是段寄存器,因此使用下面的指令来实现该设置:
mov ax, 1000H
mov SS, ax
mov SP, 0010H
注意,不能将常量数据直接通过 mov 指令传送给段寄存器,即下面的指令是非法的:
mov SS, 1000H
把 AX、BX、DS 中的数据入栈,其指令为:
push ax
push bx
push ds

问题 3.8

(1) 将 10000H~1000FH 这段空间当作栈,初始状态栈为空
参考“问题 3.7”:
mov cx, 1000H
mov SS, cx
mov SP, 0010H
(2) 设置 AX=001AH,BX=001BH
mov ax, 001AH
mov bx, 001BH
(3) 将 AX、BX 中的数据入栈
push ax
push bx
(4) 将 AX、BX 清零
sub ax, ax 
mov bx, ax
(5) 从栈中恢复 AX、BX 原来的内容
pop bx
pop ax
注意,栈空间中出、入栈遵循的规则是“后进先出”,因此不能将这两条指令的执行序列颠倒。可见,用栈来暂存以后需要恢复的寄存器数据时,出栈顺序要和入栈顺序相反,因为最后入栈的寄存器数据在栈顶,所以在恢复时要让它先出栈。

问题 3.9

(1) 将 10000H~1000FH 这段空间当作栈,初始状态栈为空
(2) 设置 AX=001AH,BX=001BH
上述两项参考“问题 3.8”。
(3) 利用栈实现交换 AX 和 BX 中的数据
push ax
push bx
pop ax
pop bx
注意指令的顺序。也可以:
push bx
push ax
pop bx
pop ax

问题 3.10

在地址 10000H 处写入字型数据 2266H,要求不能使用“mov 内存单元, 寄存器”这类指令,所使用的指令中包含以下两条指令:
mov ax, 2266H
push ax
可以有两种方法:
(1) 将地址为 10000H+2 内存单元设置为栈顶,即让 SS:SP 指向 1000:0002 内存单元(书本所用方法)
mov ax, 1000H
mov SS, ax
mov SP, 2
最后使用提供的 mov 和 push 指令完成将数据 2266H 写入 10000H 地址内存单元的操作:
mov ax, 2266H
push ax
(2) 设置 DS=1000H,使用默认 SS:SP 将数据 2266H 入栈后再用 pop 指令传送到 10000H 内存单元。
mov ax, 1000H
mov ds, ax
mov ax, 2266H
push ax
pop [0]

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 而不会修改 SS。也就是说,栈顶的变化范围是:0~FFFFH(SP 是 16 位的寄存器,其可存储的数据范围 0~FFFFH 就是栈顶地址的变化范围)。
8086 CPU 提供的栈操作机制包括:SS、SP 指示栈顶;改变 SP 然后向内存写入数据的入栈指令;读取内存数据然后改变 SP 的出栈指令。

栈的综述:
1. 8086 CPU 提供了栈操作机制,方案如下:
分别在 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. 8086 CPU 只记录栈顶,栈空间的大小需要程序员来管理。
6. 用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。
7. push、pop 实质上是一种内存传送指令。正如不能把常量数据直接传送到内存单元的 mov、add、sub 指令一样,push 指令也不能直接将常量数据入栈(栈空间的本质就是内存单元),即下面的指令非法:
push A1E5H
A1E5H 是十六进制常量数据。

3.10 栈段

2.8 节已述,8086 PC 机在编程时,可根据需要将一组内存单元定义为一个段。将长度为 N(N ≤ 64 KB)的一组地址连续、起始地址为 16 的倍数的内存单元,当作栈空间来用,从而定义了一个栈段。
将一段内存当作栈段,只是一种编程时的安排,CPU 不会由于这种安排,就在执行栈操作指令时自动地将所定义的栈段当作栈空间来访问。栈操作指令能访问所安排的栈空间,是因为 SS:SP 指向了该存储空间。

问题 3.11、3.12

将 10000H~1FFFFH 地址空间当作栈段,初始状态为空,则 SS=1000H 时,SP 寄存器存储的数据是什么?栈空间的容量有多大?
当初始状态栈设置 SS=1000H,只要既不用手工也不通过指令对 SS 进行修改,只是任 SP 随栈指令操作而自然变动,SS 是不会因 SP 的任何改变而发生改变。SP 的变化范围是 000H~FFFFH,所以当 SP=0000H 时,再次执行 push 将使得 SP=SP-2,由于溢出效应,SP 变为 FFFEH,于是 SS:SP 由 1000:0000 变为 1000:FFFE;
当 SP=FFFFH,再次执行 pop 使得 SP=SP+2,溢出效应导致 SP 由 FFFFH 变为 0001H,SS:SP 由 1000:FFFF 变为 1000:0001。可见,栈空间 10000H~1FFFFH 形成一个闭环,让栈空间的存储单元可以循环使用而不会越界。
由于 SP 的变化范围 0000H~FFFFH 是 64KB 的容量,所以一个栈段的最大容量也就是 64KB:一个地址代表一个内存单元,一个内存单元的容量为 1B;在 0~FFFFH 范围内共有 2¹⁶个数值,一个值就是一个地址,因此也就是 2¹⁶ 个地址;可见这个栈的空间容量为 2¹⁶B,即 2⁶KB(64KB)。

段的综述:
将一段内存定义为一个段,用段地址指示段,用偏移地址访问段内的单元。这些完全是程序员自己的安排。
用一个段存放数据,将它定义为“数据段”;用一个段存放代码,将它定义为“代码段”;用一个段当作栈,将它定义为“栈段”。
若要让 CPU 按照这样的安排来访问这些段,就要:
1. 对于数据段,将它的段地址放在 DS 中,用 mov、add、sub 等访问内存单元的指令时,CPU 就将所定义的数据段中的内容当作数据来访问。
2. 对于代码段,将它的段地址放在 CS 中,将段中第一条指令的偏移地址放在 IP 中,这样 CPU 就会执行所定
义的代码段中的指令:
3. 对于栈段,将它的段地址放在 SS 中,将栈顶单元的偏移地址放在 SP 中,这样 CPU 在需要进行栈操作时,比如执行 push、pop 指令等,就将所定义的栈段当作栈空间来使用。
可见,不管如何安排,CPU 将内存中的某段内容当作代码,是因为 CS:IP 指向了该内存单元;CPU 将某段内存当作栈,则是因为 SS:SP 指向该内存单元。
例如,将 10000H~1001FH 安排为代码段,并存储如下代码:
mov ax, 1000H
mov ss, ax
mov sp, 0020H        ; 初始化栈顶
mov ax, cs
mov ds, ax        ; 设置数据段段地址
mov ax, [0]
add ax, [2]
mov bx, [4]
add bx, [6]
push ax
push bx
pop ax
pop bx
设置 CS=1000H,IP=0,这段代码将得到执行。这段代码又将 10000H~1001FH 安排为栈段和数据段,使得这段内存既是代码段,又是栈段和数据段。
一段内存,可以既是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么也不是。关键在于 CPU 中寄存器的设置,即 CS、IP、SS、SP、DS 的指向。

检测点 3.2

(1) 补全下面的程序,使其可以将 10000H~1000FH 中的 8 个字,逆序复制到 20000H~2000FH 中。逆序复制的含义如下表所示(表中内存里的数据均为假设)。

 地址   数据(十六进制) 地址   数据(十六进制)
10000H    23            20000H    33
10001H    01            20001H    11
10002H    66            20002H    FE
10003H    22            20003H    9A
...                     ...
1000CH    FE            2000CH    66
1000DH    9A            2000DH    22
1000EH    33            2000EH    23
1000FH    11            2000FH    01
        
给定指令代码:
mov ax, 1000H
mov ds,        ax
...
push [0]
push [2]
push [4]
push [6]
push [8]
push [A]
push [C]
push [E]

解题思路:将 2000:0~2000:F 设置为栈段空间,将 1000:0~1000:F 内存空间(视为数据段空间)的数据通过 push 指令逆序传送到栈段空间。
将以下代码补全到上述代码的省略号处:
mov ax, 2000H
mov ss, ax       ;设置栈顶段地址
mov sp, 0010H    ; 设置栈顶偏移地址
将上述完整的代码通过 Debug 的 A 命令写入 3000:0 代码段空间,用 R 命令设置 CS=3000H,IP=0000H,再通过 T 命令执行,以及用 D 命令来验证指令执行的结果。
注:可以将 1000:0~1000:F 数据段中的 1000:4~1000:A 部分数据设置为 a1~a8 或任意数据。

(2) 改动上题中给定的代码如下:
mov ax, 2000H
mov ds, ax
...
pop [E]
pop [C]
pop [A]
pop [8]
pop [6]
pop [4]
pop [2]
pop [0]
要求补全省略号中的代码以实现相同的逆序复制功能。
解题思路:将 1000:0~1000F 设置为栈段空间,将 2000:0~2000:F 视为数据段空间,通过 pop 指令将栈段空间存储的数据逆序传送到数据段空间。
补全的代码如下:
mov ax, 1000H
mov ss, ax    ; 设置栈顶段地址
mov sp, 0    ; 设置栈顶偏移地址
将上述完整的代码通过 Debug 的 A 命令写入 3000:0 代码段空间,用 R 命令设置 CS=3000H,IP=0000H,再通过 T 命令执行,以及用 D 命令来验证指令执行的结果。

实验 2 用机器指令和汇编指令编程

一、预备知识:Debug 的使用

1. 关于 D 命令

D 命令的功能是查看指定内存单元所存储的数据信息。其命令格式在实验 1 中给出如下:
d 段地址:偏移地址
D 命令是由 Debug 执行的,在上面的命令格式中,直接给出了段地址和偏移地址;在执行这样的命令格式的 D 命令时,Debug 会先将段地址传送到段寄存器中再执行 D 命令的查看数据操作。
Debug 执行 D 命令也是通过 CPU 启动一段程序来运行的。也就是说,CPU 通过段寄存器读取 D 命令格式中提供的段地址来查找目标内存单元,然后读取其存储的数据并显示出来。
CPU 的段寄存器共有 4 个,CS、DS、SS 和 ES。其中 CS 和 SS 都有特定的用途,因此如果要查看的不是 CS 和 SS 段地址范围的内存单元,就不应该用 CS 和 SS 来存储 D 命令格式中的段地址。通常用于存储 D 命令格式的段地址的段寄存器是 DS。
D 命令除了上述命令格式外,还提供了一种符合 CPU 机理的命令格式:
d 段寄存器:偏移地址
例如:
-r ds
DS=2000
:1000
-d ds:0
-d ds:10 17
由于段寄存器与存储于其内的段地址具有唯一对应的关系,即该段寄存器所存储的只有唯一的一个段地址,
因此当引用这个段寄存器时,与引用其所存储的唯一的这个段地址没有区别。这正是 D 命令能用段寄存器名取代段地址的原因。
注意,D 命令也可以查看 CS 和 SS 寄存器段地址的内存单元,例如:
-d cs:0
...
-d ss:0

2. 在 E、A、U 命令中使用段寄存器

由于上面第 1 点所述的段寄存器与段地址唯一对应关系,所以在 E、A、U 这些命令格式中包含内存单元地址的命令中,也可以同 D 命令一样,用段寄存器表示内存单元的段地址。例如:
-r ds
DS=2000
:1000
-e ds:0 11 22 33 44 55 66
-a ds:0
-u cs:0

3. 下一条指令执行了吗?

在用 T 命令执行设置 SS 的指令时,会在该 T 命令的执行过程中将当前的设置 SS 指令与其下一条指令依序执行。也就是说,在这种情况下执行一次 T 命令会执行设置 SS 的指令及其下一条指令,而不管这“下一条指令”是否与栈操作有关。例如:
mov ax, 2000H
mov ss, ax
mov bx, fc4d
mov cx, 6a37
sub ax, ax
mov sp, 0
add bx, cx
push bx
push cx
在这些指令中,用 T 命令执行到 mov ss, ax 指令时,该 T 命令将在执行该设置 SS 的指令后接着执行其下面的一条指令 mov bx, fc4d,而无需再次启动 T 命令来执行传送数据 fc4d 到 BX 寄存器的指令 —— 这条将数据 fc4d 传送到 BX 的指令与栈操作毫不相干,但仍然会在执行设置 SS 的 T 命令中被执行,而不用另外启动 T 命令来执行。

二、实验任务

1. 使用 Debug,将程序写入内存,逐条执行。

由于 8086 PC 机的 FFFF:0000~FFFF:0007 内存地址空间属于 ROM 存储空间,因此不能向其写入数据;用 D 命令查看该范围存储的数据如下:
  地址          数据(十六进制)
FFFF:0000        EA
FFFF:0001        C0
FFFF:0002        12
FFFF:0003        00
FFFF:0004        F0
FFFF:0005        30
FFFF:0006        31
FFFF:0007        2F
在 3000:0000 地址处写入实验程序指令代码,用 T 命令并观察执行指令后的结果(数字均为十六进制):
-a 3000:0
3000:0000 mov ax, ffff
3000:0003 mov ds, ax
3000:0005 mov ax, 2200
3000:0008 mov ss, ax
3000:000A mov sp, 0100
3000:000D mov ax, [0]    ; ax = C0EA
3000:0010 add ax, [2]    ; ax = C0EA + 0012 = C0FC
3000:0014 mov bx, [4]    ; bx = 30F0
3000:0018 add bx, [6]    ; bx = 30F0 + 2F31 = 6021
3000:001C push ax    ; sp = 00FE,修改的内存单元地址是 2200:00FE 和 2200:00FF,内容为 FC 和 C0
3000:001D push bx    ; sp = 00FC,修改的内存单元地址是 2200:00FC 和 2200:00FD,内容为 21 和 60
3000:001E pop ax    ; sp = 00FE,ax = 6021
3000:001F pop bx    ; sp = 0100,bx = C0FC
3000:0020 push [4]    ; sp = 00FE,修改的内存单元地址是 2200:00FE 和 2200:00FF,内容为 F0 和 30
3000:0024 push [6]    ; sp = 00FC,修改的内存单元地址是 2200:00FC 和 2200:00FD,内容为 31 和 2F

2. 观察实验过程,分析为什么 2000:0~2000:f 中的内容会发生改变?

指令如下:
mov ax,2000
mov ss,ax
mov sp,10
mov ax,3123
push ax
mov ax,3366
push ax

当用 T 命令执行程序指令 mov ss,ax 后,SS 寄存器存储的内容发生改变,因此 CPU 会继续执行这条指令的下一条指令(无论这“下一条指令”是否与栈操作相关,都会被执行),而无需再次启动 T 命令。
以下叙述只是本人为了理解书本知识而臆测的,与标准正确答案完全不同。
mov ss,ax 指令的下一条指令是 mov sp,10,这条指令将完整地设置了栈顶地址 SS:SP 为 2000:0100,因此栈顶标识值数据便产生了。栈顶标识值数据是标记栈段空间下一个栈顶元素所在位置的标识数据。栈顶标识值数据的高 8 位在栈段空间当前 SP 的 SP-1 处,也就是 SS:SP-1,具体地址是 2000:0100-1 = 2000:00FF,并且栈顶标识值数据占用着连续的多个字节,所以在 2000:0~2000:F 范围内会看到存储的数据在变化 —— 在执行 mov sp,10 指令前是不会生成栈顶标识值数据的(即使已经设置了 SS 也不会产生栈顶标识值数据)。栈顶标识值数据随 SP 的变化而改变其存储位置(值不会改变,只改变其存储位置)。(参考 3.7 节笔记末尾所述)
评论次数(0)  |  浏览次数(124)  |  类型(学习笔记) |  收藏此文  | 
 
 请输入验证码  (提示:点击验证码输入框,以获取验证码