目录
前言
最近学了王爽教授写的《汇编语言》,整理一下学习笔记。
5.1 [BX]
[bx]和内存单元的描述
通过前面的学习我们知道。[···]表示内存单元,比如[2],它的偏移地址是2。[bx]同样表示一个内存单元,只不过它的
偏移地址在bx中,段地址在ds中
。
要完整的描述一个内存单元,需要两种信息:
(1)内存单元的地址;
(2)内存单元的长度;
以[bx]为例:
mov ax, [bx]表示将一个内存单元的内容送入ax,这个
内存单元的长度为2字节
;
mov al, [bx]表示将一个内存单元的内容送入al,这个
内存单元的长度为1字节
。
我们定义“()”来表示一个寄存器或一个内存单元中的内容。如:
(ax)表示ax中的内容,(bl)表示bl中的内容。
要注意的是,“()”中的元素可以有三种类型:
(1)寄存器名
(2)段寄存器名
(3)内存单元的物理地址(一个20位数据)
比如:(20000H)是正确的用法,但(2000: 0)是错误的用法。
idata常量
idata表示常量
,比如:
mov ax, [idata]就可以代表 mov ax, [1]、mov ax, [2]等;
mov bx, idata就可以代表mov bx, 1、mov bx, 2等。
inc指令
inc
,
算数运算指令
,表示
加1
,比如:
mov bx, 1
inc bx
执行后 bx的值为2。
类似于高级语言中的“
++运算符
”
5.2 loop指令
loop
,
循环控制指令
,Loop指令需要和cx寄存器配合使用, 用于循环操作, 类似于高级语言中的for, while等。
循环次数存储在cx寄存器中
,循环标号要自己定义,使用格式是:
mov cx, 循环次数
标号: (表明后面就是需要循环的循环体) 循环执行的程序代码
loop 标号
CPU在执行loop指令时,进行两步操作:
(1) (cx)=(cx)-1;
(2) 判断cx中的值,不为0则转至标号处执行程序,如果为0则向下执行。
比如:计算2^12,用add ax, ax指令的话要重复写11次,但是用loop指令就可以大大化简程序:
现在来分析一下上面的程序:
(1)标号
汇编语言中,标号通常代表地址。上面程序中有一个标号
s
,它
标识了一个地址
,此地址处有一条指令:
add ax,ax
。
(2)loop s
CPU执行loop s时,要进行两步操作:
<1> (cx)=(cx)-1;
<2> 判断(cx)中的值,不为0则转至标号s处所标识的地址,如果为0,则执行下一条指令
(即mov ax, 4c00h)。
5.3 在Debug中跟踪用loop指令实现的循环程序
现在有一个问题:
计算 ffff:0006单元中的数乘以3,结果存储在dx中
。
让我们来分析一下这个问题:
(1)运算后的结果是否会超出dx的存储范围?
ffff:0006
中的数是一个字节型的数据,范围在
0~255
之间,则它和3相乘的结果不会大于
65535
,可以在
dx
中放下。
(2)用循环累加来实现乘法,用哪个寄存器进行累加?
我们可以将
ffff:0006
单元中的数赋值给
ax
,用dx进行累加。先设
(dx)=0
,然后做3次
(dx)=(dx)+(ax)
。
(3)
ffff:0006
单元是一个字节,
ax
十一个
16
位寄存器,数据长度不一样,如何赋值?
很显然,我们需要让
ax
的高八位为
0
,把
ffff:0006
赋值给
ax
的低八位,这样就可以解决这个问题了。
下面开始写代码:
注意:
程序中第一条指令,
mov ax, 0ffffh
。我们知道,大于9FFFh的十六进制数据如
A000H、A001H
等等,在书写的时候都是以字母开头的。而在汇编源程序中,
数据不能以字母开头
,所以要在前面加
0
。
下面我们对程序的执行过程进行跟踪。首先将它编辑成源文件,文件名为
p3.asm
;编译连接之后生成
p3.exe
;然后用
Debug
对
p3.exe
中的程序进行跟踪。
从图中我们可以看到,
(ds)=075AH
,所以程序在
076A: 0000
处;
CS: IP
指向程序的第一条指令。
我们再用
U命令
看一下被
Debug
加载入内存的程序:
我们可以看到,从
076A: 0000~076A: 001A
是我们的程序,
076A: 0014
处是源程序中的指令
loop s
,只是此时标号s已经变成了一个地址
0012h
。在执行循环指令时,如果
(cx)-1
后不为
0
,那么此时 “
loop 0012
” 就会把
IP
设置成
0012h
,从而使
CS: IP
指向
076A: 0012
处的
add dx, ax
,从而实现跳转。
g命令
由于循环程序从
CS: 0012
开始,在这前面的指令,我们不想再一步步地跟踪,想一次性执行完,然后从
CS: 0012
开始跟踪。可以用
g命令
来实现这样的操作:
Debug
执行 “
g 0012
” 后,我们可以通过各个相关寄存器的值,看出执行结果。
p命令
但是现在我又嫌一次次循环跟踪太麻烦,我希望它一次性就可以将循环执行完。可以用
p命令
来达到我们的目的。以后当我们遇到
loop
指令时,就可以用p命令来
自动重复执行循环中的指令
,直到
(cx)=0
为止。
执行完循环,当前指令为
CS: 0016
处的 “
mov ax,4c00
”。当然,我们也可以直接用
g
命令,“
g 0016
”,来达到同样的目的。
5.4 Debug和汇编编译器masm对指令的不同处理
举个例子:
在
Debug
中:
mov ax, [0]
表示将
ds: 0
处的数据送入
ax
中,这里[
0
]表示一个内存单元;
但是在汇编程序中,
masm
会将这句话解释为
mov ax, 0!!!
也就是说,对于[
idata
],Debug将它解释为
偏移地址为idata的内存单元
;而编译器将它解释为
常数idata
。
目前的
解决方法
是,可以将偏移地址送进一个寄存器
bx
,用[
bx
]的方式来访问内存单元就不会出错了,段地址默认在
ds
中。
5.5 loop和[bx]的联合应用
想一下这个问题:
计算 ffff: 0~ffff: b 单元中的数据和,结果存储在dx中
。
先来分析一下这个问题:
(1)运算后的结果是否会超过
dx
所能存储的范围?
ffff: 0~ffff: b
内存单元中的数据是字节型,范围在
0 ~ 225
之间,
12
个这样的数据相加,结果不会大于
65535
,可以在
dx
中存放下。
(2)可不可以直接将
ffff: 0~ffff: b
中的数据累加到
dx
中,或者
dl
中?
ffff: 0~ffff: b
中的数据是
8
位的,不能直接加到
16
位寄存器中。而且向
dl
中累加
12
个
8
位数据,很有可能造成进位丢失
(3)如何将
ffff: 0~ffff: b 中
的
8
位数据,累加到
16
位寄存器
dx
中?
没办法,只能找一个
16
位寄存器当中介。将内存中的8位数据赋值到一个
16
位寄存器
ax
中,再将
ax
中的数据加到
bx
上。
话不多说,上代码:
5.6 段前缀
用于显示地指明内存单元的段地址的 “
ds:
”、“
cs:
”、“
ss:
”、“
es:
”,在汇编语言中称为段前缀。
比如 “
mov ax, ds: [bx]
”,然后来看一个问题:
将内存ffff: 0~ffff: b单元中的数据复制到0: 200 ~ 0: 20b单元中
。
浅浅分析一下:
(1)复制过程应用
循环
来实现,这样会方便很多,简单描述一下:
初始化:
X=0
循环12次:
将ffff: X单元中的数据送入0020: X (需要用一个寄存器中转)、
X++;
(2)在循环中,原始单元
ffff: X
和目标单元
0020: X
的偏移地址X是变量。我们用
bx
来存放。
(3)将
0: 200 ~ 0: 20b
用
0020: 0 ~ 0020: b
描述,使原始单元和目标单元的偏移地址都从同一数值0开始。
话不多说过,上代码:
程序使用es访问目标空间
0020: 0 ~ 0020: b
的段地址,用ds存放原始空间
ffff: 0 ~ ffff: b
的段地址。在访问内存单元的指令 “
mov es: [bx], al
” 中,用段前缀 “
es:
” 给出单元的段地址,这样就不必在循环中重复设置ds。
5.7 一段安全的空间
在
8086模式
中,随意向一段内存空间中写入内容是很危险的,因为这段内存空间中可能存放着
重要的系统数据或代码
。比如,在
Windows 7
的
DOS方式
中,在
Debug
里运行 “
mov [0026], ax
”这条指令时,就会出错:
将
ax
赋值为
1
,然后运行指令,结果发现:
产生这种结果的原因就是
0: 0026
处存放着
重要的系统数据
,而“mov [0026],ax ” 将其改写了。
可见,我们要向内存空间写入数据的话,要
使用操作系统给我们分配的空间
,而不应该直接用地址任意指定内存单元,向里面写入。
DOS方式下,一般情况,
0:200 ~ 0:2ff
空间中没有系统或其他程序的数据或代码,我们可以使用这段空间来向内存写入内容。
总结
以上为本人学习汇编语言时的摘录总结,主要内容来源于
汇编语言(第四版) 王爽 著
,大家若是感兴趣可以看看原书,很值得推荐,以上内容如果有什么错误的话,还请大家指正!