loop 指令
可用于实现循环。
- 格式:
loop 标号
执行loop
指令时,要进行两步操作:
(cx) -= 1
- 判断 cx 的值,如果不为0,就转移至标号处执行程序,如果为0就向下执行。
使用 loop
指令可以实现循环功能,cx中存放循环的次数
以下是一个计算 2^12 的程序
assume cs:code
code segment
mov ax, 2
mov cx, 11
s: add ax, ax
loop s
mov ax, 4c00h
int 21h
code ends
end
- 标号s:在汇编语言中,标号标识了一个地址,而这个地址处有一条指令
add ax, ax
与内存的交互
描述性符号(),用于表示一个寄存器或者内存单元中的内容。
[bx]
[bx]
表示一个内存单元的偏移地址,而偏移地址的值就是寄存器BX中的数据。
例如:
mov ax, [bx]
就是把DS:BX
指向的长度为2的内存单元的内容存入ax
mov al, [bx]
就是把DS:BX
指向的长度为1的内存单元的内容存入ax
约定符号idata
表示常量:如:mov ax, [idata]
就表示mov ax, [1]
、mov ax, [2]
等
masm 会将
mov ax, [idata]
解释为mov ax, idata
但是mov ax, [bx]
仍是mov ax, [bx]
。
此外,mov ax, ds:[idata]
可以用来表示把ds:[idata]
处的数据存入 ax。
也可以使用mov bx, idata; mov ax, [bx]
类C语言的表示法
可以用诸如这样的指令:
mov ax, [bx + 200]
来访问内存空间。
其数学化的描述为:(ax) = [(ds) * 16 + (bx) + 200]
此外,类似的有四种不同写法,以下四条指令都是等价的:
mov ax, [bx + 200]
mov ax, [200 + bx]
mov ax, 200[bx]
mov ax, [bx].200
其中后两种形式分别类似于数组和结构体。
可以看到,寄存器 bx 一般都会用在这样的情形下,被置于中括号中用于索引数据。此外,si 和 di 寄存器也可以当作 bx 使用。
以下指令等价:
mov ax, [bx + 200 + si]
mov ax, [200 + bx + si]
mov ax, 200[bx][si]
mov ax, [bx].200[si]
mov ax, [bx][si].200
segment
程序需要一段内存空间来存储数据,且需要保证这段空间是安全的,不会影响其他程序正在使用的内存。0:200 ~ 0:300这段空间就是相对安全的空间。
在操作系统的环境中,程序可以从操作系统申请一段合法空间来使用。
要在一个程序被加载的时候取得所需空间,就必须在源程序中做出说明。
示例代码:
assume cs:codesg, ds:datasg, ss:stacksg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends
stacksg segment
dw 0, 0, 0, 0, 0, 0, 0, 0
stacksg ends
codesg segment
start:
mov ax, datasg
mov ds, ax
mov ax, stacksg
mov ss, ax
mov sp, 16
mov bx, 0
mov cx, 4
s0:
push cx
mov si, 0
mov cx, 3
s1:
mov al, [bx][si]
and al, 11011111B
mov [bx][si], al
inc si
loop s1
add bx, 16
pop cx
loop s0
mov ax, 4c00H
int 21H
codesg ends
end start
其中,伪指令 start
标志了程序入口的位置,注意 end 后面也有 start。如果不这样写,程序会在写入了 'BaSiC'
的那段内存开始执行代码,由于其未必是合法的机器码,可能会造成异常。
伪指令 assume 并不会在程序运行时将 ds, ss
寄存器的值设定为段 datasg, stacksg
的地址,因此要在程序开始时把对应的段地址传入对应的段寄存器。
伪指令 db, dw 表示在对应内存位置申请内存,并填入预设的数据。
这个程序的作用是把在 datasg
内存储的字符串从小写转为大写,可以看到这里处理转换大小写的技巧。效果如图所示:
CPU的栈机制
8086提供了相关指令,可以用栈的方式来访问内存空间。
两个最基础的指令:
- push:
push ax
表示将 ax 中的数据入栈 - pop:
pop ax
表示把栈中的数据弹入 ax 中
8086CPU的入栈和出栈操作都是以字为单位进行的。
8086CPU中的两个寄存器 SS 和 SP 用来记录栈顶地址。栈顶的段地址存放在SS中,偏移地址存放在SP中,任意时刻,SS:SP指向栈顶元素。
在现代CPU中,往往使用ESP或者RSP(32位和64位),其主要作用是维护函数调用栈,当然,函数调用栈的概念主要是由C语言编译器所维护的。由于这些CPU没有使用段寄存器的必要,所以没有相应的SS。
push 和 pop
push ax
的执行过程:
SP -= 2
,也就是令SS : SP
指向当前栈顶前面的一个字单元。- 将ax的内容送入
SS : SP
指向的内存单元处,此时SS : SP
就是新的栈顶。
pop ax
的执行过程:
- 将
SS : SP
指向的内容送入ax SP += 2
,指向当前栈顶下面的单元,作为新的栈顶。
8086不会因为栈内存已满就拒绝push指令的执行,程序员需要自己担心栈溢出的问题。
栈段
我们可以在编程的时候做这样的安排,也就是把一段内存当作栈段
注意:假如我们用X ~ Y作为一个栈段(X < Y)
则栈为空时
S
P
=
(
Y
−
S
S
×
16
)
+
1
\mathrm{SP} = (Y - \mathrm{SS} \times 16)+1
SP=(Y−SS×16)+1
对于,8086CPU,栈段最大为64KB
数据处理
要考虑两个基本问题:
- 处理的数据在什么地方?
- 要处理的数据有多长?
寻址方法
下面,SA 表示段地址,EA 表示偏移地址
指令要处理的数据在内存中,在汇编指令中可以用 [x] 的格式给出EA,而SA存在于某个段寄存器中。
寻址方式 | 含义 | 名称 | 常用格式举例 |
---|---|---|---|
[idata] | EA = idata; SA = (ds) | 直接寻址 | [idata] |
[bx] [si] [di] [bp] | EA = (bx); SA = (ds) EA = (si); SA = (ds) EA = (di); SA = (ds) EA = (bp); SA = (ss) | 寄存器间接寻址 | [bx] |
[bx + idata] [si + idata] [di + idata] [bp + idata] | EA = (bx) + idata; SA = (ds) EA = (si) + idata; SA = (ds) EA = (di) + idata; SA = (ds) EA = (bp) + idata; SA = (ss) | 寄存器相对寻址 | 用于结构体: [bx].idata 用于数组: idata[si] 用于二维数组: [bx][idata] |
[bx + si] [bx + di] [bp + si] [bp + di] | EA = (bx) + (si); SA = (ds) EA = (bx) + (di); SA = (ds) EA = (bp) + (si); SA = (ss) EA = (bp) + (di); SA = (ss) | 基址变址寻址 | 用于二维数组: [bx][si] |
[bx + si + idata] [bx + di + idata] [bp + si + idata] [bp + di + idata] | EA = (bx) + (si) + idata; SA = (ds) EA = (bx) + (di) + idata; SA = (ds) EA = (bp) + (si) + idata; SA = (ss) EA = (bp) + (di) + idata; SA = (ss) | 相对基址变址寻址 | 用于结构体中的数组项: [bx].idata[si] 用于二维数组: idata[bx][si] |
数据长度控制
8086CPU的指令可以处理两种尺寸的数据:byte
和word
,因此在机器指令中要指明指令要进行字操作还是字节操作。
汇编语言有以下处理方法:
通过寄存器名指明要处理的数据的尺寸
例如以下指令,就指明了要进行字操作:
mov ax, 1
mov bx, ds:[0]
mov ds, ax
mov ds:[0], ax
inc ax
add ax, 1000
例如以下指令,就指明了要进行字节操作:
mov al, 1
mov al, bl
mov al, ds:[0]
mov ds:[0], al
inc al
add al, 100
用操作符X ptr指明内存单元的长度。
X在汇编指令中可以是 qword, dword, word, byte,分别是8字节、4字节、2字节、1字节
然而,对于8086CPU,仅支持 word 和 byte
例如以下指令就指明了是字操作:
mov word ptr ds:[0], 1
inc word ptr [bx]
inc word ptr ds:[0]
add word ptr [bx], 2
例如以下指令就指明了是字节操作:
mov byte ptr ds:[0], 1
inc byte ptr [bx]
inc byte ptr ds:[0]
add byte ptr [bx], 2
而push只进行字操作
跳转指令
可以修改ip
或者同时修改cs
和ip
的指令统称为转移指令。
转移指令就是可以控制cpU执行内存中某处代码的指令。
8086CPU只有两类转移行为:
- 段内转移:只修改
ip
,例如jmp ax
- 段间转移:同时修改
cs
和ip
,例如:jmp 1000:0
通过转移指令对ip
的修改范围不同,段内转移又分为: - 短转移,
ip
的修改范围是-128 ~ 127 - 近转移,
ip
的修改范围是-32768 ~ 32767
8086CPU的转移指令分为以下几类: - 无条件转移指令(例如
jmp
) - 条件转移指令
- 循环指令(如
loop
) - 过程
- 中断
这些转移指令的前提条件未必相同,但其原理基本相同。
offset
操作符
操作符offset在汇编语言中会由编译器处理,能够取得标号的偏移地址。
例如:
assume cs:codesg
codesg segment
start:
mov ax, offset start ;相当于mov ax, 0
s:
mov ax, offset s ;相当于mov ax, 3
codesg ends
end start
例:代码复制
assume cs:codesg
codesg segment
# 把start后的第一个指令复制到s0处去。
start:
mov ax, bx
mov si, offset start
mov di, offset s0
mov ax, cs:[si]
mov cs:[di], ax
s0:
nop # nop的机器码占一个字节。
nop
codesg ends
end start
jmp
指令
jmp
是无条件转移的指令,可以只修改ip
,也可以同时修改cs
和ip
jmp
指令要给出两种信息:
- 转移的目的地址
- 转移的距离(段间转移、段内短转移、段内近转移)
1. 依据位移进行转移的jmp
指令
jmp short 标号
,转到标号处执行指令。这个实现的是段内短转移,修改范围是-128 ~ 127
注意:这种形式的jmp
指令,其对应的机器码中的idata
部分,是一个-128 ~ 127的数字,表示相对的偏移位置。
此外还有jmp near ptr 标号
其功能与jmp short 标号
类似,但是标号的范围更长。
机器代码中,jmp
指令后面对应的位移的数字,是要转移到的命令的首地址,减去jmp
指令的下一条指令的首地址。
2. 转移目的地址在指令中的jmp
指令
jmp far ptr 标号
实现的是段间转移,又称远转移。
这里的标号可以写成这样:
s:
mov ax, 1
db 256 dup (0)
jmp far ptr s
当然,标号也可以写成 段地址:偏移地址 的形式
3. 转移地址在寄存器中的jmp
指令
jmp 16位寄存器
直接修改了ip
的内容。
4. 转移地址在内存中的jmp
指令
有两种格式:
jmp word ptr 内存单元地址
(段内转移)- 这里内存单元地址指向的内容就是新的
ip
的值,也就是偏移地址。
- 这里内存单元地址指向的内容就是新的
jmp dword ptr 内存单元地址
(段间转移)- 内存单元地址处开始存放的两个字,高地址的字是目的段地址,低地址的字是目的偏移地址
- (
cs
) = (内存单元地址 + 2) - (ip) = (内存单元地址)
由于
jmp
指令在某些情况下是执行从这条指令所在的位置进行偏移
因此如果将jmp
直接复制到其他位置可能会产生不符合伪代码的期望的效果。
call
, ret
, retf
ret
指令用栈中的数据,修改ip
的内容,从而实现近转移。
retf
指令用栈中的数据,修改cs
和ip
的内容,从而实现远转移。
ret
相当于:
pop ip
retf
相当于:
pop ip
pop cs
call far ptr 标号
指令call far ptr 标号
实现的是段间转移。
CPU执行此种格式的call
指令时,会进行如下操作:
push cs
push ip
jmp far ptr 标号
代码示例:
assume cs:codesg, ss:data
data segment
dw 16 dup (0)
data ends
codesg segment
start:
mov ax, data
mov ss, ax
mov sp, 16
mov ax, 0
call far ptr s
inc ax
s:
pop ax
add ax, ax
pop bx
add ax, bx
mov ax, 4c00H
int 21H
codesg ends
end start
其在 debug
中显示的指令列表如下:
执行到 call
指令后,内存情况如下:
076A:0000所占的一行是栈段,显然,076C是执行call
时的cs
。此时栈顶是0010,结合指令表一看,被压入栈中的是call指令的下一条指令的首地址
call 16位寄存器
相当于
push ip
jmp 16位寄存器
call word ptr 内存单元地址
相当于
push ip
jmp word ptr 内存单元地址
call dword ptr 内存单元地址
相当于
push cs
push ip
jmp dword ptr 内存单元地址
jcxz
指令
jcxz
指令是有条件转移指令。所有的有条件转移指令都是短转移,对ip
的修改范围都是-128 ~ 127
指令格式:jcxz + 标号
作用:执行到这条指令的时候,若cx
中的内容是0,则跳转到标号所示的位置。
模块化汇编程序
通过对 call, ret, retf, 可以实现模块化程序设计。
一、call
和ret
的配合使用
call
和ret
配合使用,可以实现子程序的机制
类似于函数的机制。
其中一种代码框架如下:
assume cs:code
code segment
main:
;
;
call sub1
;
;
mov ax, 4c00H
int 21H
sub1:
;
;
call sub2
;
;
ret
sub2:
;
;
;
ret
code ends
end main
二、参数与结果传递的问题
子程序一般都要根据提供的参数处理一定的事务,然后将结果(返回值)提供给调用者。
讨论参数与返回值的传递问题,实际上就是在讨论如何存储子程序需要的参数和产生的返回值。
;说明:计算N的3次方
;参数:(bx) = N
;结果:(dx:ax) = N^3
cube:
mov ax, bx
mul bx
mul bx
ret
子程序最好像这样添加注释,保证其良好的复用性。
3. 批量数据的传递
设计一个将全是字母的字符串转成大写的函数
assume cs:code
data segment
db 'conversation'
data ends
code segment
start:
mov ax, data
mvo ds, ax
mov si, 0
mov cx, 12
call capital
mov ax, 4c00H
mov 21H
capital:
and byte ptr [si], 11011111B
inc si
loop capital
ret
code ends
end start
这里的
capital
实际上涉及到了两个参数:字符串的首地址和长度。
其中,首地址的段地址存放在ds中,偏移地址存放在si中,长度存放在cx中。
除了用寄存器传递参数,还有一种通用的方法是用栈来传递参数
4. 寄存器冲突的问题
譬如:
assume cs:code
data segment
db 'word', 0
db 'unix', 0
db 'wind', 0
db 'good', 0
data ends
code segment
start:
mov ax, data
mov ds, ax
mov bx, 0
mov cx, 4
s:
mov si, bx
call capital
add bx, 5
loop s
mov ax, 4c00H
int 21H
capital:
mov cl, [si]
mov ch, 0
jcxz ok
and byte ptr [si], 11011111B
inc si
jmp short capital
ok:
ret
code ends
end start
以上程序由于主程序与子程序之间发生了寄存器的冲突,可能无法正确运行。其中一种解决方案是:
- 子程序所使用的寄存器入栈
- 执行子程序
- 子程序使用的内容出栈
因此可以把capital
函数改成如下:
capital:
push cx
push si
change:
mov cl, [si]
mov ch, 0
jcxz ok
and byte ptr [si], 11011111B
inc si
jmp short change
ok:
pop si
pop cx
ret