在之前的章节中我们使用 [0]、[bx] 作为偏移地址来访问内存单元。在本章中,我们将学习一些更加灵活的定位内存单元的方法,以及相关的编程方法。
and 指令(“逻辑与”运算)
1)and 指令,即“逻辑与”指令,按位进行“与”运算。
2)and 运算的规则是:同真为真,否则都为假。
3)1 and 1 结果为 1,0 and 0 结果为 0,1 and 0 结果为 0,0 and 1 结果为 0。如下示例:
mov al, 01100011B
and al, 00111011B
执行后的结果是:(al) = 00100011B
4)使用 and 指令,可以把操作对象的指定位设为0,其它位不变。如下示例:
把 al 寄存器的第6位设为0,其它位不变: and al, 10111111B
把 al 寄存器的第0位设为0,其它位不变: and al, 11111110B
or 指令(“逻辑或”运算)
1)or 指令,即“逻辑或”指令,按位进行“或”运行。
2)or 运算的规则是:同假为假,否则都为真。
3)0 or 0 结果为 0,1 or 1 结果为 1,1 or 0 结果为 1,0 or 1 结果为 1。如下示例:
mov al, 01100011B
or al, 00111011B
执行后的结果是:(al) = 01111011B
4)使用 or 指令,可以把操作对象的指定位设为1,其它位不变。如下示例:
把 al 寄存器的第7位设为1,其它位不变: or al, 10000000B
把 al 寄存器的第0位设为1,其它位不变: or al, 00000001B
ASCII 码
世界上有很多种编码方案,ASCII 就是其中的一种,它在计算机系统中使用最为广泛。简单来说,所谓编码方案就是一套规则,它约定了用什么样的信息来表示现实对象。还可以把编码方案理解成我们与CPU之间的语言翻译官。
ASCII(American Standard Code for Information Interchange,美国信息互换标准代码,ASCⅡ)是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语和其他西欧语言。它是现今最通用的单字节编码系统,并等同于国际标准ISO/IEC 646。
ASCII第一次以规范标准的型态发表是在1967年,最后一次更新则是在1986年,至今为止共定义了128个字符,其中33个字符无法显示(这是以现今操作系统为依归,但在DOS模式下可显示出一些诸如笑脸、扑克牌花式等8-bit符号),且这33个字符多数都已是陈废的控制字符,控制字符的用途主要是用来操控已经处理过的文字,在33个字符之外的是95个可显示的字符,包含用键盘敲下空白键所产生的空白字符也算1个可显示字符(显示为空白)。
![](https://img.haomeiwen.com/i2463290/1fee81cd2b7edff5.png)
![](https://img.haomeiwen.com/i2463290/dd26fbcdf3ed62f7.png)
比如,在ASCII编码方案中,“61H”表示小写字母“a”,“62H”表示小写字母“b”。
一种编码规则,需要人们共同遵守才有意义。在文本编辑的过程中,就包含着按照 ASCII 编码规则进行的编码和解码。
比如,我们在文本编辑时,在键盘上按下"a"键,就会在屏幕上看到"a",这是怎样的一个过程呢?
按下键盘"a"键,键盘芯片根据ASCII编码规则把"a"转化成数字"61H"并将其送往到内存中去,文本编辑器程序再通过CPU从内存中读取"61H",然后再根据ASCII编码规则把"61H"转化成小写字母"a",最后显示在文本程序的屏幕上。
在汇编程序中使用“字符”
在汇编程序中,我们使用 单引号 的方式来指明数据是以“字符”的形式给出的,汇编编译器会根据ASCII编码规则把这些“字符”数据转化成相应数值数据。示例代码如下:
assume cs:code, ds:data
data segment
db 'unIX'
db 'foRK'
data ends
code segment
start: mov al, 'a'
mov bl, 'b'
mov ax, 4c00H
int 21H
code ends
end start
源码解读如下:
1)" db 'unIX' " 被编译后得到 “ db 75H, 6eH, 49H, 58H ”(即,字母"u"、"n"、"I"、"X"所对应的ASCII码分别是"75H"、"6eH"、"49H"、"58H")。
2)" db 'foRK' " 被编译后得到“ db 66H, 6fH, 52H, 4bH”。
3)" mov al, 'a' "被编译后得到“ mov al, 61H ”。
4)" mov bl, 'b' "被编译后得到 “ mov bl, 62H ”。
这种转化机制,是由编译器根据 ASCII 编码规则来完成的。ASCII编码规则,正是这样的一种介于人类语言和机器语言之间的翻译官,一方面用于把人类语言或符号转化机器能理解的二进制数据,另一方面把机器所生成的二进制数据转化成我们可读的人类语言或符号。
ASCII编码规则是8位的,除了ASCII编码规则还有很多其它编码规则,比如16位的 Unicode 编码规则。Unicode编码规则是一种全球通用的支持多国语言的编码方案。
基于ASCII编码规则的大小字母之间的转换
我们知道,同一个字母有大小之分,且它们对应的 ASCII 码不同的。因此,要转换一个字母的大小写,本质上就是改变大小写字母所对应的ASCII码即可。
![](https://img.haomeiwen.com/i2463290/acef4d9ab5a69564.png)
通过对上图 ASCII 码对照表观察发现:同一个字母的大小写字母所对应的16进制的ASCII码之间相差 20H。于是,我们可以得到第1种大小写转换方案:对同一个字母来讲,把小写字母对应的ASCII码减去 20H,即得到相应大写字母的ASCII码;把大写字母对应的ASCII码加上 20H,即得到相应小写字母的ASCII码。
继续观察,我们又发现,同一个字母的大小写字母所对应的二进制的ASCII码之间,仅仅是第5位数值是相反的,其它位都一致。于是,我们可以得到第2种大小写转换方案:对同一个字母来讲,对其进行“位”运算以实现大小写转换,使用 and 运算可以把小写转换成大写,使用 or 运算可以把大写转换成小写。示例如下:
把小写 a 转换成 大写 A:
01100001B and 11011111B
结果:01000001B,即大写字母“A”的ASCII码
把大写 A 转换成 小写 a:
01000001B or 00100000B
结果:01100001B,即小写字母“a”的ASCII码
[bx + idata] 方式访问内存单元
在前面我们使用 [bx] 来指明内存单元的偏移地址。我们还可以用一种更为灵活的方式来指明内存单元的物理地址,即 [bx + idata]。
[bx + idata] 表示一个内存单元,它的偏移地址为 (bx) + idata,即 bx 寄存器中的数值加上 idata。示例如下:
mov ax, [bx + 200]
mov ax, [200 + bx]
mov ax, 200[bx]
mov ax, [bx].200
代码解释:上述四行代码的功能是等价的。即把段地址为 ds、偏移地址为 (bx)+200的存储空间中的“字”型数据送入到 ax寄存器中。
数学化的描述为:(ax) = ((ds)*16 + (bx) + 200)
用 [bx + idata] 的方式处理数组
[bx + idata] 的方式,为高级语言的数组实现和处理提供了便利的实现机制。如下代码,用 [bx + idata] 的方式处理数组:
assume cs:code, ds:data
data segment
db 'BaSiC'
db 'MinIX'
data ends
code segment
start: mov ax, data
mov ds, ax ; 设置数据段的段地址
mov bx, 0 ; 初始化数据段的偏移地址
mov cx, 5 ; 循环次数
s: mov al, [bx + 0]
and al, 11011111B ; 小写转大写
mov [bx + 0], al
mov al, [bx + 5]
or al, 00100000B ; 大写转小写
mov [bx + 5], al
inc bx
loop s
mov ax, 4c00H
int 21H
code ends
end start
源码解读如下:
1)"db 'BaSiC'"定义了一段字符数据,可将其看成是 ds[0] ~ ds[5] 的数组,"db 'MinIX'"同理看成 ds[5] ~ ds[9] 的数组。
2)在循环中,[bx + i] 如果是第一个数组的第 i 个值,则 [bx + 5 + i] 就是第二个数组的第 i 个值。根据这种关系,我们只使用一次 loop 循环即可同时遍历这两个数组。
3)在遍历过程中,使用 and 与运算把小写字母转换成大写,使用 or 与运算把大写字母转换成小写,这是基于 ASCII 编码规则来实现的。
SI 和 DI 寄存器(使用 [si]、[di] 访问内存单元)
SI 和 DI 是8086CPU中和 BX 功能相近的两个通用寄存器,但 SI 和 DI 不能像 BX 那样被分成两个 8 位的寄存器来使用。 SI 和 DI 只能被当成是 16位的寄存器来使用,其功能和 bx 寄存器相似,值得注意的是:我们通常使用 ds:[si] 指向数据源所在的内存位置,使用 ds:[di] 指向数据将要抵达的目标内存位置。如下代码示例:
mov bx, 0
mov ax, [bx]
mov si, 0
mov ax, [si]
mov di, 0
mov ax, [di]
以上三段代码的功能是一样的,其作用都是把段地址为 (ds)、偏移地址为 0 的内存单元中的数据送入到 ax 寄存器中。
不仅 bx 支持 [bx+idata]的内存访问方式,事实上 si、di 寄存器也支持 [si+idata]、[di+idata]的内存访问方式。如下代码示例:
mov bx, 0
mov ax, [bx + 123]
mov si, 0
mov ax, [si + 123]
mov di, 0
mov ax, [di + 123]
以上三段代码的功能也是一样的,使用了 [ bx + idata] 方式,把段地址为 (ds)、偏移地址为 (0+123)的内存单元中的数据送入到 ax 寄存器中。
【例1】使用 SI 和 DI 寄存器把字符串“Welcome to masm!”复制到其所在数据段之后的内存段中去。题目图示如下:
![](https://img.haomeiwen.com/i2463290/fbd59b3a979c0e15.png)
assume cs:code, ds:data
data segment
db 'Welcome to masm!' ; 数据源
db '................' ; 占位空间,16个字符长度
data ends
code segment
start: mov ax, data
mov ds, ax ; 设置 ds 段地址
mov ax, 0
mov si, ax ; 设置数据源的偏移地址
mov ax, 16
mov di, ax ; 目标空间的偏移地址
mov cx, 8 ; 设置循环次数,每次复制两个字节
s: mov ax, [si] ; 每次复制两个字节
mov [di], ax
add si, 2
add di, 2
loop s
mov ax, 4c00H
int 21H
code ends
end start
源码解读如下:
1)编写程序的过程,大都是对数据进行处理,而数据在内存中存放着。所以我们在处理数据之前首先要搞清楚数据存储在什么地方,也就是说数据所在内存地址。在本例中,db 指令所定义的数据源所在的内存地址为 ds:[0] ~ ds[16]。
2)使用 ds:[si] 指向数据源所在内存地址,使用 ds:[di] 指向目标内存地址。
3)"Welcome to masm!"字符串的长度是16个字节,在这次复制任务过程中,每次复制两个字节,共8次循环即可实现需求。
4)对源码进行编译、连接,在 debug.exe 中进行跟踪调试如下图示:
![](https://img.haomeiwen.com/i2463290/863c59b6a01e39f8.png)
注意:SI 和 DI 也是通用寄存器,所以"mov si, di"是合法的指令。代码示例如下:
mov si, di
mov di, si
[bx + si] 和 [bx + di] 方式访问内存单元
在前面内容中,我们使用了 [bx]、 [si]、[di]、[bx+idata]、[si+idata]、[di+idata]的方式访问内存单元,事实上我们还可以使用 [bx+si]、[bx+di]的方式来访问内存单元。[bx+si]、[bx+di] 的使用方式相近,其功能含义也相近。在这一系列的内存访问方式中,我们可以把 idata 看成是常量,所有的寄存器看成是变量。示例代码如下:
mov ax, [bx + si]
mov ax, [bx][si]
上述两行代码是等价的,用描述符号表示为:(ax) = ((ds)*16 + (si) + (bx))。DI寄存器的使用类似,不再赘述。
[bx + si + idata] 和 [bx + di + idata] 方式访问内存单元
[bx + si + idata] 和 [bx + di + idata] 的用法相似,我们以 [bx + si + idata] 为例进行学习。[bx + si + idata] 即代表着一内存单元的偏移地址:(bx) + (si) + idata。示例如下:
mov ax, [bx + si + idata]
mov ax, [bx + idata + si]
mov ax, [idata + bx + si]
mov ax, idata[bx][si]
mov ax, [bx].idata[si]
mov ax, [bx][si].idata
以上6组代码是等价的,都表示把段地址为 ds、偏移地址为 (bx)+(si)+idata 的内存单元中的数据送入到 ax 寄存器中去。
多种寻址方式的灵活应用
首先我们先回顾一个前面所讲到的几种内存寻址方式:
1)[idata]方式,用一个常量来表示内存单元的物理地址,如 ds:[3]。
2)[bx]方式,用一个寄存器变量来表示内存单元的物理地址,这是一种间接定位内存单元的方式,如 ds:[bx]。
3)[bx+idata]方式,用一个常量和寄存器变量来表示内存单元的物理地址,即在一个起始地址的基础上再用一个变量间接地定位一个内存单元,如 ds:[bx+idata]。
4)[bx+si]、[bx+di]方式,用两个寄存器变量来表示内存单元的物理地址,即 ds:[bx+si]、ds:[bx+di]。
5)[bx+si+idata]、[bx+di+idata]方式,用两个寄存器变量和一个常量来表示内存单元的偏移地址,即 ds:[bx+si+idata]、ds:[bx+di+idata]。
从[idata]到[bx+si+idata],我们把内存单元的偏移地址拆分成了多个部分,从而形成了结构化的内存访问方式,每个部分都可以独立地代表着一个特定的含义,这就是灵活性寻址方式的根本成因。下面,我们通过几个例题来体会这种“灵活性”的好处。
【例2】把如下数据段中的每个单词的首字母改成大写,数据源如下图示:
![](https://img.haomeiwen.com/i2463290/f4eb077c6990b14d.png)
assume cs:code, ds:data
data segment
db '1. file ' ; 用空格来构造16字节的字符串
db '2. edit '
db '3. search '
db '4. view '
db '5. options '
db '6. help '
data ends
code segment
start: mov ax, data
mov ds, ax ; 数据源的段地址
mov bx, 0 ; 设置偏移地址
mov cx, 6 ; 循环次数
s: mov al, [bx+3]
and al, 11011111B ; 把小写字母转换成大写
mov [bx+3], al
add bx, 16
loop s
mov ax, 4c00H
int 21H
code ends
end start
源码解读如下:
1)在 db 指令中,使用空格把每一串字符串都构造成16字节,这将使得程序中的内存访问更加有规律。
2)通过观察,我们需要6次循环来实现需求,用一个寄存器变量 bx 来定位行,用一个idata常量 3 来定位列。每循环一次让 bx 加16,即能跳转至下一行。idata 常量为3,我们要修改每个16字节字符串中的第4个字节,即这些单词的首字母。
4)使用 and 与运算把小写字母转换成大写字母。
5)对源码进行编译、连接,在 debug.exe 中进行跟踪调试,结果如下图所示:
![](https://img.haomeiwen.com/i2463290/3fe1817bbed54f9a.png)
【例3】把内存段中的四个字符串中所有字母都变换成大写。源数据如下图示:
![](https://img.haomeiwen.com/i2463290/4cfef091b0be214e.png)
assume cs:code, ds:data
data segment
db 'ibm ' ; 用空格来构造16字节的字符串
db 'dec '
db 'dos '
db 'vax '
data ends
code segment
start: mov ax, data
mov ds, ax ; 数据源的段地址
mov bx, 0 ; 控制行循环
mov cx, 4 ; 外层循环的次数
outer: mov dx, cx ; 缓存外层循环的次数
mov si, 0 ; 控制列循环
mov cx, 3 ; 内层循环的次数
inner: mov al, [bx+si]
and al, 11011111B ; 把小写字母变成大写字母
mov [bx+si], al
inc si ; 下一列
loop inner ; 执行内层循环
add bx, 16 ; 下一行
mov cx, dx ; 还原外层循环的次数
loop outer ; 执行外层循环
mov ax, 4c00H
int 21H
code ends
end start
源码解读如下:
1)数据段中有4个16位的字符串,每个字符串又有3个字符。因此本例中,我们可以使用“嵌套循环”来遍历这些字符并将其变换成大写字母。
2)我们已经知道,loop 循环指令 和 CX 寄存器息息相关,CX中的值决定着 loop 循环的次数。本例中有两层循环,所以需要设置两个 CX 的值,但只有一个 CX 寄存器该如何是好?基于这样的问题,我们可以在每次即将进入内层循环之前先把外层循环的 CX 缓存(暂存)下来,当内层循环结束后再次返回至外层循环时再把缓存(暂存)下来的 CX 进行还原。一般来说,当需要缓存(暂存)数据的时候,我们应该使用栈来管理。但在本例中,我们用 dx 来缓存(暂存)外层循环次数 CX 。
3)使用嵌套循环遍历二元表格,[bx]变量控制着outer“行”循环,[si]变量控制着inner“列”循环。
4)使用 and 与运算,把小写字母转换成大写字母,这是基于ASCII编码规则的标准。
5)对源码进行编译、连接,在 debug.exe 中进行跟踪调试,显示结果如下图所示:
![](https://img.haomeiwen.com/i2463290/3442d1890c2eade5.png)
当程序中经常有数据需要暂存、但CPU寄存器又不够用时怎么办?
这些需要被暂存的数据可能是内存中的数据,也可能是寄存器中的数据。我们使用空闲的寄存器来暂存数据可以解决普通问题,但这本质上并不是一个一般化的解决方案,因为寄存器的数量总是有限的,每个程序中可使用的寄存器都不一样。
我们希望找到一种通用的解决方案,来解决这种在编程中经常会出现的暂存问题。鉴于寄存器的数量是有限的,因此我们可以考虑用内存空间来缓存(暂存)数据:即当需要暂存数据时,我们就从内存单元中开辟一段内存空间来暂存数据,这段内存空间通常使用“栈”。
基于这样的分析,我们对上述【例3】程序进行优化,优化结果如下:
assume ds:data, ss:stack, cs:code
data segment
db 'ibm ' ; 定义数据源
db 'dec '
db 'dos '
db 'vax '
data ends
stack segment
dw 0,0,0,0,0,0,0,0 ; 用于暂存数据的栈段
stack ends
code segment
start: mov ax, data
mov ds, ax ; 设置数据段的段地址
mov ax, stack
mov ss, ax ; 设置栈段的段地址
mov sp, 10H ; 设置栈顶位置
mov cx, 4 ; 设置外层循环次数
mov bx, 0 ; 控制外层循环
s1: push cx ; 入栈
mov cx, 3 ; 内层循环次数
mov si, 0 ; 控制内层循环
s2: mov al, [bx+si]
and al, 11011111B ; 小写转大写
mov [bx+si], al
inc si
loop s2
pop cx
add bx, 16
loop s1
mov ax, 4c00H
int 21H
code ends
end start
源码解读如下:
1)用 dw 开辟一段空间,并让 ss:sp 指向栈段底部。
2)当即将进入内层循环时,push cx 指令即把外层循环指针暂存入栈,当从内层循环再次返回至外层循环时,pop ax 指令即把栈中暂存的外层循环指针重新送入到 cx 中。如此,即可做到两层 loop 循环共享一个 cx 中的循环指针。
3)对源码进行编译、连接,在 debug.exe 中跟踪调试,结果如下图示:
![](https://img.haomeiwen.com/i2463290/87339c3025b3b20b.png)
用“栈空间”来缓存程序中的数据
至于为什么应该使用“栈段”、而不建议使用寄存器来暂存程序中的数据?相关原因上述已有说明。本示例中,我们直接进入主题,用“栈段”来暂存程序中需要缓存的数据,这便是汇编编程的最佳实践技巧之一。
【例4】题目图示如下,要求把数据源中每行数据的前4个小写字母变换成大写。
![](https://img.haomeiwen.com/i2463290/d281471de967370a.png)
assume ds:data, ss:stack, cs:code
data segment
db '1. display ' ; 定义16字节的数据源
db '2. brows '
db '3. replace '
db '4. modify '
data ends
stack segment
dw 0,0,0,0,0,0,0,0 ; 栈,用于暂存数据
stack ends
code segment
start: mov ax, data
mov ds, ax
mov bx, 0 ; 初始化 ds:[bx]
mov ax, stack
mov ss, ax
mov sp, 10H ; 初始化栈顶 ss:[sp]
mov cx, 4 ; 行循环 4 次
s1: push cx ; 暂存外层循环指针
mov cx, 4 ; 列循环 4 次
mov si, 3 ; 列指针
s2: mov al, [bx+si]
and al, 11011111B ; 位运算,大小写转换
mov [bx+si], al
inc si
loop s2
pop cx ; 恢复行循环指针
add bx, 16
loop s1
mov ax, 4c00H
int 21H
code ends
end start
源码分析如下:
1)通过对题目进行分析,我们发现本例需要使用 4 * 4 的双层循环来完成任务。外层循环 ds:[bx] 用来控制行的循环,内层循环 ds:[bx+si] 用来控制列的循环。
2)基于汇编编程的最佳实践之一 “用栈空间来暂存程序中需要暂存的数据”,我们初始化了一段栈空间,并用 ss:[sp] 指向栈位置。
3)从行循环进入列循环之前,push ax 暂存行循环指针;当从列循环结束进入行循环之前,pop ax 恢复行循环指针。通过对程序代码的分析,使用栈空间来暂存程序数据是一种效率极高的编程技巧,“栈”是CPU架构中相当重要的一种数据处理方案。
4)对上述源码进行编译、连接,在debug.exe 中进行跟踪调度,结果如下图示:
![](https://img.haomeiwen.com/i2463290/23fd47611a81fbf5.png)
阶段小结
本阶段主要学习了以下几个方面的知识点:
1)and、or 指令的位运算。
2)基于 ASCII 编码规则的大小写字母之间的转换。
3)嵌套循环的应用。
4)使用“栈空间”来管理程序中需要暂存的数据。
5)多种内存寻址方式及其灵活的组合应用。寻址方式的适当使用,使我们可以以更合理的结构来看待我们所要处理的数据。
6)把看似杂乱无章的数据设计成一种清晰的数据结构,是程序设计过程中最为关键的问题。
我们已经知道,计算机是进行数据处理、运算的机器,那么我们不得不关注两个最基本的问题:(1)待处理的数据存放在什么地方?(2)待处理的数据有多长?这两个最基本的问题,在机器指令中必须给以明确或隐含的说明,否则计算机就无法工作。本章我们就针对 8086CPU 对这两个基础问题进行讨论。
自定义两个描述性符号:reg 和 sreg
为了描述上的简洁,在之后的课程中, 我们使用两个描述性符号:用 reg 表示一个寄存器,用 sreg 表示一个段寄存器。
reg 的集合有:ax, bx, cx, dx, ah, al, bh, bl, ch, cl, dh, dl, sp, bp, si, di。
sreg 的集合有:ds, ss, cs, es。
bx、si、di、bp 寄存器
1)在 8086CPU中,只有这四个寄存器可以用在 "[ bx / si / di / bp ]" 中括号中,用于表示内存单元的偏移地址,其它寄存器都不能用在中括号中。
2)在 "[ ]" 表示偏移地址时,bx 能和 si、di 组合,不能和 bp 组合;bp 能和 si、di 组合,不能和 bx 组合。即 [bx+si+idata]、[bx+di+idata]、[bp+si+idata]、[bp+di+idata] 这四种组合。(简单说,bx 和 bp 不能组合,si 和 di 不能组合)
3)当 "[ ... ]" 中使用了 bp 寄存器,而指令中又没有显性地给出段地址,则段地址就默认从 ss 中取。可见,bp 寄存器就是用来扩展 sp 的,共同为栈空间的访问提供了灵活的方式。示例如下:
mov ax, [bp]
mov ax, [bp+idata]
mov ax, [bp+si]
mov ax, [bp+si+idata]
上述四行代码中对内存单元的访问,段地址默认都是 (ss)。
机器指令所操作的数据存放在什么地方?
绝大部分的机器指令都是用于进行数据处理的指令,数据处理大致可以分为三类:读取、写入、运算。
在机器指令的层面上讲,它并不关心数据的值是多少,而是关心指令执行前一刻它将要处理的数据所在的位置。事实上,机器指令在执行前,它所要处理的数据可能在三个地方:CPU内部、内存中、端口中。示图如下:
![](https://img.haomeiwen.com/i2463290/3a29c1d2bbc441d2.png)
在汇编语言中如何表达数据所在的位置?
在汇编语言中,用如下三个概念来表达数据所在的位置:
1)立即数(idata)。对于直接包含在机器指令中的数据,执行前这个数据存放在CPU指令缓冲器中,这种数据被称为“立即数 idata”。示例如下
mov ax, 1
// 数据 1 ,就是立即数,指令执行前它存放在CPU指令缓冲器中
2)寄存器。指令中所要处理的数据存放在寄存器中时,在汇编指令中给出相应的寄存器名称即可。示例如下
mov ax, bx
// 所要处理的数据存放在 bx 寄存器中
3)内存单元地址(段地址(SA)和偏移地址(EA))。当指令将要处理的数据在内存中时,在汇编指令中可以用 [EA] 或者 SA:[EA] 指出数据所在的内存单元地址。存放数据的段地址 EA 可以是默认的,也可以显性地给出。示例如下
mov ax, [0]
mov ax, [bx]
mov ax, [bx+si+8] // 以上三个数据默认的段地址在 DS 寄存器中
mov ax, [bp+si+8] // 该数据默认的段地址在 SS 寄存器中
mov ax, ds:[bp]
mov ax, es:[bx]
mov ax, ss:[bx+si]
mov ax, cs:[bx+si+9] // 这四个数据,显性地强制性地给出了段地址
8086CPU 寻址方式 小结
当数据存放在内存中时,我们可以用多种灵活的方式来指定这个内存单元的物理地址,这种定位内存单元的方法就叫做“寻址方式”。多种灵活的寻址方式(EA是偏移地址,SA是段地址),总结如下图表:
![](https://img.haomeiwen.com/i2463290/02a2c57282a38f16.png)
指令所处理的数据有多长?
8086CPU的指令,可以处理两种尺寸的数据,byte字节数据 和 word字型数据。所以在机器指令中,要指明指令正在执行的是“字节”操作还是“字”操作。汇编语言通过以下几种方式来确定数据的长度:
1)通过寄存器名指明所处理的数据的长度。如下示例,通过寄存器名指定数据长度为 16 位,即字操作。
mov ax, 1
mov bx, ds:[0]
mov ds, ax
mov ds:[0], ax
inc ax
如下示例,通过寄存名指定数据长度为 8位,即字节操作。
mov al, 2
mov bh, [0]
inc al
add bl, 100
2)在没有寄存器名存在的情况下,用操作符“X ptr”指明内存单元的长度,X 的值在汇编指令中可以为 'word' 或 'byte'。如下示例,通过“word ptr”指明数据长度是 16位,即字操作。
mov word ptr ds:[0], 1
inc word ptr [bx]
inc word ptr ds:[0]
add word ptr [bx], 2
如下示例,通过“byte ptr”指明数据长度是 8位,即字节操作。
mov byte ptr ds:[0], 1
inc byte ptr [bx]
inc byte ptr ds:[0]
add byte ptr [bx], 2
在没有寄存器参与的内存单元访问指令中,用 'word ptr'或'byte ptr'显性地指明数据长度是很有必要的,否则CPU将无法得知所要访问的内存单元是字单元,还是字节单元。
3)有些指令默认了访问数据是字单元还是字节单元。比如栈指令 push/pop 就不用指明数据长度,因为栈指令永远都是字操作(sp 每次的变化量是 2,即两个字节)。
push ax // 入栈
pop dx // 出栈
寻址方式 的综合应用(从结构化的角度去看待所要处理的数据)
下面我们通过一个例题来进一步讨论各种寻址方式的作用。题目如下:已知DEC公司在1982年时的公司名称是“DEC”,总裁姓名是“Ken Olsen”,公司排名是“137”,收入总额是“40”亿美元,核心产品是“PDP”。到了1992年时,该公司的公司排名变成了“38”,收入总额增加了“70”亿美元,核心产品变成了“VAX”。现在,我们的编程任务是在内存中更新该公司的这些信息(源数据在内存中的存储情况如下图示)。
![](https://img.haomeiwen.com/i2463290/fd8c31fa4a90d305.png)
通过题目分析,数据源所在的段地址为 seg,我们将要修改的三处数据分别在 [60 + 000C]、[60 + 000E]、[60 + 0010]三处,汇编源码实现如下:
assume cs:code, ds:data
data segment
db ' ' ; 定义占位空间
db ' ' ; 使数据源从 ds:[0060H] 开始
db 'DEC'
db 'Ken Oslen' ; 数据源
dw 0089H
dw 0028H
db 'PDP'
data ends
code segment
main: mov ax, data
mov ds, ax
mov bx, 0060H
mov si, 0
mov word ptr [bx+000CH], 0026H ; 公司排名升至 38名
add word ptr [bx+000EH], 0046H ; 营业额增加 70亿美元
mov byte ptr [bx+0010H+si], 'V' ; 变更核心产品 “VAX”
inc si
mov byte ptr [bx+0010H+si], 'A'
inc si
mov byte ptr [bx+0010H+si], 'X'
mov ax, 4c00H
int 21H
code ends
end main
上述汇编程序,我们用高级语言 C 来重写,代码实现如下:
struct company { // 定义一个公司记录的结构体
char cn[3]; // 公司名称
char hn[9]; // 总裁姓名
int pm; // 公司排名
int st; // 公司总营业额,单元“亿美元”
char cp[3]; // 公司核心产品
}
struct company dec = {"DEC", "Ken Olsen", 137, 40, "PDP"}; // 初始化
main(){
int i;
dec.pm = 38; // 修改公司排名
dec.sr = dec.sr + 70; // 修改营业额
i = 0;
dec.cp[0] = 'V';
i++
dec.cp[1] = 'A';
i++
dec.cp[2] = 'X';
return 0;
}
源码解析如下:
1)在汇编程序中,我们使用 "word ptr" 和 "byte ptr" 来显示地指定所要操作的数据长度。并且使用了多种灵活的寻址方式来定位内存单元。
2)8086CPU所提供的如 [bx+si+idata]的寻址方式,为结构化数据的处理提供了方便。使得我们可以在编程时,从结构化的角度去看待所要处理的数据。从与 C程序的对比可以看出,一个结构化的数据可以包含多个数据项,每个数据项的数据类型又可以不同,有的是字型数据,有的是字节数据,有的是数组(字符串)。当我们在汇编中使用 [bx+idata+si] 的方式来访问结构体中的数据时,通常使用 bx 来定位整个结构化数据,使用 idata 来定位结构体中的某一个数据项,使用 si 来定位数组数据项中的每一个元素。事实上,汇编语言已经为我们提供了更加贴切的书写方式,如 [bx+idata] 等价于 [bx].idata; [bx+idata+si] 等价于 [bx].idata[si] 。在 C 程序中,dec 变量就是这个结构体数据,dec.cp 对应着汇编中 [bx].idata,dec.cp[i] 对应着汇编中的 [bx].idata[si]。这样经过解析后,会不会发现汇编编程和高级语言编程之间的相似之处?
![](https://img.haomeiwen.com/i2463290/e8269dfa5a70c6ae.png)
汇编中,[bx] 即 ds:[bx],对应着 C 程序中的 dec 这个结构化的数据变量(JSON数据)。
汇编中,[bx+idata] 等价于 [bx].idata,对应着 C程序中的 "dec.pm"、"dec.sr"数据项。
汇编中,[bx+idata+si] 等价于 [bx].idata[si],对应着 C程序中的 "dec.cp[i]"数组数据项中的一个元素。
可以看出,汇编中的寻址方式定位内存单元,与高级语言中的结构化数据访问原理是一致的。
3)对上述汇编程序进行编译、连接,并在 debug.exe 中调试,结果如下图
![](https://img.haomeiwen.com/i2463290/a7538824e2c876b0.png)
div 指令,除法运算
div 是除法指令,即 division。作为除法运算,除数可以是 8位的数据,也可以是 16位的数据,除数是存放在寄存器或者内存单元中的。
1)当除数是8位时,被除数就是16位,此时被除数默认存放在 AX 寄存器中。执行除法运算后,“商”存放在 AL中,“余数”存放在 AH中。
2)当除数是16位时,被除数就是 32位,此时被除数默认存放在 DX+AX寄存器中(高16位存放在 DX中,低16位存放在 AX中)。执行除法运算后,“商”存放在 AX中,“余数”存放在 DX中。
div 指令的基本语法:"div reg"、"div 内存单元"。
div byte ptr ds:[0]
含义是,(al) = (ax) / ((ds)16+0)的商,(ah) = (ax) / ((ds)16+0)的余数。
div word ptr es:[0]
含义是,(ax) = [(dx)10000H + (ax)] / ((ds)16+0)的商, (dx) = [(dx)10000H + (ax)] / ((ds)16+0)的余数。
div byte ptr [bx+si+8]
含义是,(al) = (ax) / ((ds)16+(bx)+(si)+8)的商,(ah) = (ax) / ((ds)16+(bx)+(si)+8)的余数。
div word ptr [bx+si+8]
含义是,(ax) = [(dx)10000H + (ax)] / ((ds)16+(bx)+(si)+8)的商,(dx) = [(dx)10000H + (ax)] / ((ds)16+(bx)+(si)+8)的余数。
【例1】使用 div 指令,计算十进制数 1001 / 100 的结果。汇编源码如下:
assume cs:code
code segment
start: mov ax, 03E9H ; 16位的被除数
mov bl, 64H ; 8位的除数
div bl ; 执行除法
mov ax, 4c00H
int 21H
code ends
end start
源码解析:1)十进制数 1001 小于 65536(2的16次方),因此用 16 位的寄存器即可存储这个被除数,默认使用 AX 寄存器来存储被除数。2)十进制的除数 100,小于 256(2的8次方),因此用 8 位的 bl 寄存器来存储除数。3)执行“div bl”除法运算后, 除法运算的结果,“商”存储在 AL 中,“余数”存储在 AH 中。经过编译、连接、调试,运行后的结果如下图所示:
![](https://img.haomeiwen.com/i2463290/6372b245ca6e5635.png)
从 debug.exe 中的运行结果看,十进制数 1001 / 100 相除的运算结果为 0AH(10),余数为 1。
【例2】使用 div 指令,计算十进制数 100001 / 100 的结果。汇编源码如下:
assume cs:code
code segment
start: mov dx, 0001H
mov ax, 86A1H ; 32位的被除数
mov bx, 64H ; 16位的除数
div bx ; 执行除法
mov ax, 4c00H
int 21H
code ends
end start
源码解析:1)十进制的 100001 大于 65536(2的16次方),因此这个被除数必须用 32 位(4字节)才够用。十进制的 100001 转换成 十六进制为 000186A1H,高16位 0001H 存放至 DX寄存器中,低16位 86A1H存放至 AX寄存器中。2)因为被除数是32位,所以除数就是16位,于是我们可以把十进制的除数 100 (0064H)存放至 BX寄存器中。3)使用 "div bx" 执行除法,除法运算的结果,“商”存放在 ax 中,“余数”存放在 dx 寄存器中。经过编译、连接、调试的运行结果如下图所示:
![](https://img.haomeiwen.com/i2463290/f14be0e6c38138c4.png)
从 debug.exe 中的运行结果看,十进制数 100001 / 100 相除的运算结果为 03E8H(1000),余数为 1。
db、dw、dd 伪指令
这个三个伪指令,都是由汇编编译器来解释执行的。本质上讲,它们并不是汇编指令。
1)db,用于定义一个字节数据,8位的数据。
2)dw,用于定义一个字型数据,16位的数据。
3)dd,用于定义一个双字型数据,即 double word,32位的数据。
data segment
db 64 ; 8位字节
dw 100 ; 16位字型
dd 100001 ; 32位双字型
data ends
如何才能让电脑充分发挥64位的数据处理能力呢?有三个条件缺一不可,分别是:CPU必须是64位的,操作系统必须是64位的,系统中运行的软件也必须是64位的。
dup 伪指令
dup 伪指令和 db / dw / dd 等伪指令一样,都是由汇编编译器来解释执行的,它的作用是用来指定重复的数据。示例如下:
db 3 dup (0)
它定义了3个值相等的字节,每个字节的值都是0。等价于 "db 0,0,0"。
db 4 dup (0,1,2)
它定义了12个字节。其等价于 "db 0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,1,2"。
db 2 dup ('abc','ABC')
它定义了12个字节。其等价于 "db 'abcABCabcABC'"。
通过上述示例,可见使用 dup 伪指令能够方便地定义大量的有规律的重复数据。其基本语法有如下三种:
1)db 重复次数N dup (要被重复N次的字节数据)
2)dw 重复次数N dup (要被重复N次的字型数据)
3)dd 重复次数N dup (要被重复N次的双字型数据)
比如我们要定义一个容量为200个字节的栈段,如果不使用 dup 伪指令,那么我们必须像如下代码这样去实现:
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends
如果我们使用 dup 伪指令,那么上述代码可以大大地简化如下:
stack segment
db 200 dup (0) ; 定义了 200 个值为 00H 的字节数据
stack ends
由此可见,dup 伪指令,结合 db、dw、dd 一起组合使用,是非常有意义的,能为重复的数据定义省不少事儿。
实验七:寻址方式在结构化数据访问中的应用
题目:已知 Power Idea公司,从1975年至1995年(共21年),年收入依次为 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514,345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000;员工人数依次为 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226,11542,14430,15257,17800。现需求是,按年把“年份、年收入、员工数、人均收入”的数据录入到内存中去,每年的信息占用连续内存空间,如下表格示样:
![](https://img.haomeiwen.com/i2463290/76dd324a6b57d491.png)
assume ds:data, cs:code
data segment
db '1975','1976','1977','1978','1979','1980','1981','1982','1983','1984','1985','1986','1987','1988','1989','1990','1991','1992','1993','1994','1995' ; 21个“年份”
dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514,345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000 ; 21个“总收入”
dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226,11542,14430,15257,17800 ; 21个“员工人数”
data ends
table segment
db 21 dup('YYYY summ nu ?? ') ; 二元表
table ends
code segment
start: mov ax, data
mov ds, ax ; 数据源的段地址
mov bx, 0
mov ax, table
mov ss, ax ; 目标区段的段地址
mov bp, 0
mov si, 0
mov di, 0
mov cx, 21
s: push cx
mov ax, [bx+si]
mov ss:[bp+0], ax
mov ax, [bx+si+2]
mov ss:[bp+2], ax ; 复制4个字节的“年份”
mov ax, [bx+84+si]
mov dx, [bx+84+si+2]
mov ss:[bp+5], ax
mov ss:[bp+7], dx ; 复制4个字节的“总收入”
mov cx, [bx+168+di]
mov ss:[bp+10], cx ; 复制2个字节的“员工人数”
div word ptr [bx+168+di]
mov ss:[bp+13], ax ; 复制2个字节的“人均收入”
add bp, 16
add si, 4
add di, 2
pop cx
loop s
mov ax, 4c00H
int 21H
code ends
end start
本章完!!
网友评论