8086汇编-简单应用程序的设计
9unk Lv5

字符串处理

字符串就是一组连续的字符数据。对字符串的操作处理包括复制、检索、插入、删除和替换等。为了方便对字符串将进行有效的处理,8086/8088提供了专门用于处理字符串的指令,这些指令称为字符串操作指令,简称为串操作指令。本节先介绍串操作指令与串操作指令密切相关的重复前缀,并举例说明如何利用它们进行字符串处理。

字符串操作指令

简单说明

8086/8088 共有五种基本的串操作指令。每种基本的串操作指令包含两条指令,一条适用于以字节为单元的字符串,另一条是适用于以字为单元的字符串。
在字符串操作指令中,由 SI 指向源操作数(串),由 DI 指向目的操作数(串)。规定源串存放在当前 ds(数据段)中,目的串存放在当前 es(附加段)中,即 DS:SI 指向源串,ES:DI指向目的串。
串操作指令执行时会自动调整 SI 和 DI 的值。此外,字符串操作的方向是由 DF(方向) 标志位控制。DF 默认为0,按递增的方式调整 SI 或 DI 值;当DF置为1时,按递减当方式调整 SI 和 DI 的值。

字符串装入指令(LOAD String)

字符串装入指令如下:

1
2
LODSB   ;装入字节(Byte)
LODSW ;装入字(Word)

字符串装入指令只是把字符串中的一个字符装入到累加器中。

  • LODSB 把 SI 所指向的一个字节装入 AL 中,然后根据方向标志位 DF 使 SI 的值递增1或递减1。
  • LODSW 把 SI 所指向的一个字节装入 AX 中,然后根据方向标志位 DF 使 SI 的值递增2或递减2。

字符串装入指令的源操作数是存储操作数,所以引用数据段寄存器DS,该指令不影响标志位。

例1:修改 T3-12.asm ,使用 LODSB 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
;程序名:T6-1.asm
;功能:把一个字符串中的所有大写字母改写成小写,使用 LODSB 指令

assume ds:data,cs:code

data segment
char db 'HOW are yoU !',0
data ends

code segment
start:
mov ax,data
mov ds,ax
;
CLD ;清除方向标志位
char_A:
LODSB ;读字节,同时si+1
cmp al,0
jz stop
;
cmp al,41h
jb output
cmp al,5Ah
ja output
add al,20h
;
output:
mov dl,al
mov ah,2
int 21h
jmp char_A
;
stop:
mov ax,4c00h
int 21h
code ends
end start

在汇编语言中,两条字符串装入指令的格式可统一写成如下格式:
LODS OPRD

汇编程序根据操作数的类型,决定使用字节装入指令还是字装入指令。

案例:演示LODS指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
;程序名:T6-1-1.asm
;功能:演示 LODS 指令

assume ds:data,cs:code

data segment
char db 'HOW are yoU !',0
data ends

code segment
start:
mov ax,data
mov ds,ax
;
CLD ;清除方向标志位
char_A:
LODS char ;读字节,同时si+1
cmp al,0
jz stop
;
cmp al,41h
jb output
cmp al,5Ah
ja output
add al,20h
;
output:
mov dl,al
mov ah,2
int 21h
jmp char_A
;
stop:
mov ax,4c00h
int 21h
code ends
end start

字符串存储指令(Store String)

字符串存储指令格式如下:

1
2
STOSB
STOSW

字符串存储指令是把累加器的值存储到字符串中,即替换字符串中的一个字符。

  • 字节存储指令 STOSB:把累加器 AL 的内容送到寄存器 DI 所指向的存储单元中,然后根据方向标志位 DF 的值增1或减1
  • 字存储指令 STOSW:把累加器 AX 的内容送到寄存器 DI 所指向的存储单元中,然后根据方向标志位 DF 的值增2或减2

字符串操作指令引用当前附加段寄存器 ES,该指令不影响标志位。

在汇编语言中,两条字符串存储指令的格式可统一写成如下格式:
STOS OPRD

汇编程序根据操作数的类型,决定使用字节装入指令还是字装入指令。

案例:把当前数据段中偏移 1000H 开始的 100 个字节的数据传送到从偏移2000H 开始的单元中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;程序名:T6-2.asm
;功能:把当前数据段中偏移 1000H 开始的 100 个字节的数据传送到从偏移2000H 开始的单元中。

assume cs:code

code segment
start:
mov ax,data
mov ds,ax
mov si,1000H
mov di,2000H
mov cx,100
;
CLD ;清除方向标志位
NEXT:
LODSB
STOSB
loop NEXT
;
mov ax,4c00h
int 21h
code ends
end start

字符串传送指令(Move String)

字符串传送指令格式如下:

1
2
MOVSB   ;字节传送
MOVSW ;字传送
  • MOVSB 把寄存器SI所指向的一个字节数据传送到由寄存器 DI 所指向的存储单元中,然后根据方向标志位 DF 的值增1或减1
  • MOVSW 把寄存器SI所指向的一个字数据传送到由寄存器 DI 所指向的存储单元中,然后根据方向标志位 DF 的值增2或减2

    DS:SI为源操作数地址,ES:DI为目的操作数地址。字符串传送指令不影响标志位。

在汇编语言中,两条字符串存储指令的格式可统一写成如下格式:

1
MOVS OPRD1,OPRD2

案例:修改T6-2.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
;程序名:T6-3.asm
;功能:演示字符串传送指令

assume ds:data,cs:code

data segment
char db 'HOW are yoU !',0
data ends

code segment
start:
mov ax,data
mov ds,ax
mov si,1000H
mov di,2000H
;mov cx,100
mov cx,100/2
;
CLD ;清除方向标志位
NEXT:
;MOVSB
MOVSW
loop NEXT
;
mov ax,4c00h
int 21h
code ends
end start

字符串扫描指令(Scan String)

字符串扫描指令如下:

1
2
SCASB   ;串字节扫描
SCASW ;串字扫描
  • SCASB 把 AL 的内容与由 DI 所指向的一个字节数据采用相减的方式比较,相减结果反映到有关标志位(AF,CF,OF,PF,SF和ZF),但不影响两个操作数,然后根据方向标志位 DF 的值增1或减1。
  • SCASW 把 AX 的内容与由 DI 所指向的一个字节数据采用相减的方式比较,相减结果反映到有关标志位(AF,CF,OF,PF,SF和ZF),但不影响两个操作数,然后根据方向标志位 DF 的值增2或减2。

案例:判断字符串中的字符是否有特殊字符’#’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
;程序名:T6-4.asm
;功能:判断字符串中的字符是否有特殊字符'#'

assume ds:data,cs:code

data segment
char db '012345#ABCD',0
char_len = $ - char
mess db "NOT FOUND CHAR #",'$'
mess1 db "FOUND CHAR #",'$'
data ends

code segment
start:
mov ax,data
mov ds,ax
mov es,ax
xor di,di
mov cx,char_len
mov al,'#'
;
CLD ;清除方向标志位
NEXT:
SCASB
loopnz NEXT
jnz NOT_FOUND
lea dx,mess1
mov ah,9
int 21h
jmp stop
NOT_FOUND:
lea dx,mess
mov ah,9
int 21h
stop:
mov ax,4c00h
int 21h
code ends
end start

在汇编语言中,两条字符串扫描指令的格式可统一写成如下格式:
SCAS OPRD

字符串比较指令

字符串比较指令格式如下:

1
2
CMPSB   ;串字节比较
CMPSW ;串字比较
  • CMPSB 把 SI 所指向的一个字节数据与 DI 所指向的一个字节数据采用相减方式比较,相减结果反映到各相关标志位(AF、CF、OF、PF、SF、ZF),但不影响两个操作数,然后根据方向标志位 DF 的值增1或减1。

  • CMPSW 把 SI 所指向的一个字数据与 DI 所指向的一个字数据采用相减方式比较,相减结果反映到各相关标志位(AF、CF、OF、PF、SF、ZF),但不影响两个操作数,然后根据方向标志位 DF 的值增2或减2。

在汇编语言中,两条字符串比较指令的格式可统一写成如下格式:

1
CMPS OPRD1,OPRD2

重复前缀

由于串操作指令每次只能对字符串中的一个字符进行处理,所以使用了一个循环,以便完成对整个字符串的处理。为了进一步提高效率,8086/8088还提供了重复指令前缀。重复前缀 可加在串操作指令之前,达到重复执行其后的串操作指令的目的。

重复前缀 REP

REP 作为一个串操作指令的前缀,它重复其后的串操作指令动作。每次重复都会先判断CX是否为0,如为0就结束重复,否则CX的值减1

在重复过程中的 CX减1操作,不影响各标志位。
重复前缀 REP 主要用在串传送指令 MOVS 和串存储指令 STOS 之前。值得指出的是,一般不在 LODSB 或 LODSW 指令之前使用任何重复前缀。

案例:设计一个子程序,用指定字符填充缓冲区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
;程序名:T6-5.asm
;功能:设计一个子程序,用指定字符填充缓冲区。

assume ds:data,cs:code

data segment
buffer db 0,0,0,0,0
buffer_len = $ - buffer
data ends

code segment
start:
mov ax,data
mov ds,ax
mov es,ax
xor di,di
;
mov cx,buffer_len
mov al,0CCh
call FILLB
stop:
mov ax,4c00h
int 21h

;-------------------------------------------------
FILLB PROC
;子程序名:FILLB
;功能:用指定字符填充缓冲区
;入口参数:ES:DI=缓冲区首地址
; CX=缓冲区长度,AL=填充字符
;出口参数:无
push ax
cld
shr cx,1
mov ah,al ;mov 不影响标志位
rep stosw ;stosw 不影响标志位
jnc FILLB1
stosb ;有溢出,再传送1个字节
FILLB1:
pop ax
ret
FILLB ENDP
code ends
end start

重复前缀 REPZ/REPE

REPZ 与 REPE 是一个前缀的两个助记符。
REPZ 用作为一个串操作指令的前缀,它重复其后的串操作指令动作。每重复一次,CX的值减1,直到 cx=0 或 ZF=0 时止。

再重复过程中的 CX 值减 1 操作,不影响标志。

重复前缀 REPZ 主要用在字符串比较指令 CMPS 和 字符串扫描指令 SCAS 之前。由于传送指令 MOVS 和串存储指令 STOS 都不影响标志位,所以在这些操作指令前使用 REP 和前缀 REPZ 的效果一样。

案例:设计一个子程序,使用 REPZ 与 CMPSB 配合,比较两个字符串是否相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
;程序名:T6-6.asm
;功能:设计一个子程序,使用 REPZ 与 CMPSB 配合,比较两个字符串是否相同。

assume ds:data,cs:code

data segment
string db 'hello',0
string1 db 'hello',0

mess db 'Two strings are the same','$'
data ends

code segment
start:
mov ax,data
mov ds,ax
mov es,ax
;
lea si,string
lea di,string1
call STRCMP
cmp ax,0
jnz stop
mov ah,9
lea dx,mess
int 21h
stop:
mov ax,4c00h
int 21h

;-------------------------------------------------
STRCMP PROC
;子程序名:STRCMP
;功能:比较两个字符串是否相同
;入口参数:DS:SI=字符串1首地址
; ES:DI=字符串2首地址
;出口参数:AX=0(两个字符串相同)
push di
cld ;DF位置0
xor al,al
mov cx,0ffffh
NEXT:
SCASB ;0是字符串结束标志位,指针会多减1
loopnz NEXT
inc cx
not cx ;计算字符串(ES:DI)的长度
;
pop di ;重置di
REPZ CMPSB
;比较两个字符串的结束标志
mov al,[si]
mov bl,es:[di]
xor ah,ah
mov bh,ah
sub ax,bx
ret
STRCMP ENDP
code ends
end start

重复前缀 REPNZ/REPNE

REPNZ 与 REPNE 是一个前缀的两个助记符。
REPNZ 用作为一个串操作指令的前缀。与REPZ类似,直到 CX=0 或 ZF=1 时止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
;程序名:T6-7.asm
;功能:设计一个子程序,使用 REPNZ 与 CMPSB 配合,比较两个字符串是否相同。

assume ds:data,cs:code

data segment
string db 'hello','$'
string1 db 'hello',0

mess db 'Two strings are the same','$'
data ends

code segment
start:
mov ax,data
mov ds,ax
mov es,ax
;
lea si,string
lea di,string1
call STRCMP
cmp ax,0
jnz stop
mov ah,9
lea dx,mess
int 21h
stop:
mov ax,4c00h
int 21h

;-------------------------------------------------
STRCMP PROC
;子程序名:STRCMP
;功能:比较两个字符串是否相同
;入口参数:DS:SI=字符串1首地址
; ES:DI=字符串2首地址
;出口参数:AX=0(两个字符串相同)
push di
cld ;DF位置0
xor al,al
mov cx,0ffffh
;
REPNZ SCASB ;0是字符串结束标志位,指针会多减1
not cx ;计算字符串(ES:DI)的长度,cx多加1
;
pop di ;重置di
REPZ CMPSB ;正好比较完字符串所有值,包括结束字符
ret
STRCMP ENDP
code ends
end start

说明

重复字符串处理操作过程可被中断。CPU在处理字符串的下一个字符之前识别中断。如果发生中断,那么在中断处理返回以后,重读过程再从中断点继续执行下去。但应注意,如指令前还有其他前缀的话,中断返回时其他的前缀就不再生效。因为 CPU 在中断时,只能 “记住” 一个前缀,即字符串操作指令前的重复前缀。如字符操作指令必须使用一个以上的前缀,则可在之前禁止中断。

字符串操作指令

例1:写一个判别字符是否在字符串中出现的子程序。设字符串以 0 结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
;程序名:T6-8.asm
;功能:写一个判别字符是否在字符串中出现的子程序。设字符串以 0 结尾。
assume ds:data,cs:code

data segment
char db 'l'
string1 db 'hello',0

mess db 'The string contains characters: ','$'
data ends

code segment
start:
mov ax,data
mov ds,ax
;
mov al,char
lea si,string1
call STRCHR
jc stop
mov ah,9
lea dx,mess
int 21h
mov dl,char
mov ah,2
int 21h
stop:
mov ax,4c00h
int 21h

;-------------------------------------------------
STRCHR PROC
;子程序名:STRCHR
;功能:判别字符是否在字符串中出现
;入口参数:AL=字符
; ES:SI=字符串首地址
;出口参数:CF=0 表示字符在字符串中,AX=字符首地址出现处的偏移
; CF=1 表示字符不在字符串中
push bx
push si
cld ;DF位置0
mov bl,al
test si,1
jz STRCHAR1
LODSB
cmp al,bl
jz STRCHAR3
and al,al
jz STRCHAR2
;
STRCHAR1:
LODSW
cmp al,bl
jz STRCHAR4
and al,al
jz STRCHAR2
cmp ah,bl
jz STRCHAR3
and ah,ah
jnz STRCHAR1
STRCHAR2:
stc
jmp SHORT STRCHAR5
STRCHAR3:
inc SI
STRCHAR4:
lea ax,[si-2]
STRCHAR5:
pop si
pop bx
ret
STRCHR ENDP
code ends
end start

例2:写一个在字符串1后追加字符串2的子程序。设字符串均以0结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
;程序名:T6-9.asm
;功能:写一个在字符串1后追加字符串2的子程序。设字符串均以0结尾。
assume ds:data,cs:code

data segment
string1 db 'hello ',0
string2 db 'word!',0
data ends

code segment
start:
mov ax,data
mov ds,ax
mov es,ax
;
lea si,string1
lea di,string2
call STRCAT
jc stop
mov ah,9
lea dx,string1
int 21h
stop:
mov ax,4c00h
int 21h

;-------------------------------------------------
STRCAT PROC
;子程序名:STRCAT
;功能:在字符串1末追加字符串2
;入口参数:DS:SI=字符串1起始地址的段值:偏移
; ES:DI=字符串2起始地址的段值:偏移
;出口参数:无
pushf
push bx
push dx
push si
push di
push es
push ds
;
cld ;DF位置0
;保存字符串2的偏移
push di
;计算字符串2的长度
mov cx,0ffffh
xor al,al
REPNZ SCASB
not cx
;恢复di,并调换 DS:SI 和 ES:DI
pop di
xchg si,di
mov bx,ds
mov dx,es
mov es,bx
mov ds,dx
;调整字符串1的偏移
push cx
mov cx,0ffffh
REPNZ SCASB
dec di
;恢复原字符串2的长度
pop cx
;判断使用 movsb 还是 movsw
shr cx,1
jnc STRCAT1
movsb
STRCAT1:
rep movsw
;为方便输出字符串
mov byte ptr es:[di-1],'$'
pop ds
pop es
pop di
pop si
pop dx
pop bx
popf
ret
STRCAT ENDP
code ends
end start

书中用 “PUSH DS;POP ES” 来使得DS=ES,而我写的例子使用 mov 指令来调换 DS 和 ES 的值。

例3:写一个程序,它先接收一个字符串,然后抽去其中的空格,最后按相反的顺序显示它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
;程序名:T6-10.asm
;功能:写一个程序,它先接收一个字符串,然后抽去其中的空格,最后按相反的顺序显示它。
assume ds:data,cs:code

data segment
buffer db 128 dup(0)
data ends

code segment
start:
mov ax,data
mov ds,ax
mov es,ax
;
lea di,buffer
call STRINV
stop:
mov ax,4c00h
int 21h

;-------------------------------------------------
STRINV PROC
;子程序名:STRINV
;功能:它先接收一个字符串,然后抽去其中的空格,最后按相反的顺序显示它。
;入口参数:ES:DI=字符串2起始地址的段值:偏移
;
;出口参数:无
pushf
push bx
push dx
push si
push di
push es
push ds
;让 DS:SI 和 ES:DI 都指向缓冲区 BUFFER
push ds
pop es
;接收字符串
mov dx,di
call INPUT
push di
;计算字符串长度
xor al,al
mov cx,0ffffh
REPNZ SCASB
not cx
;还原di的值
pop di
STRINV1:
;删除空格
call DELSP
call NEWLINE
;反向输出
mov cx,ax
;定位字符串末尾的偏移
add di,ax
lea si,[di-1]
std
STRINV2:
LODSB
mov dl,al
mov ah,2
int 21h
loop STRINV2
;
pop ds
pop es
pop di
pop si
pop dx
pop bx
popf
ret
STRINV ENDP

;--------------------------------------------------------
INPUT PROC
; 功能:接收输入,并存储到 128 字节大小的 buffer 缓冲区中
; 入口参数:dx=buffer缓冲区首地址
; 出口参数:dx=buffer缓冲区首地址
; 说 明:通过显示回车符形成回车,通过显示换行符形成换行
;---------------------------------------------------------
pushf
push ax
push dx
push bx
mov bx,dx
INPUT1:
mov ah,1
int 21h
cmp al,0dh
jz INPUT2
mov [bx],al
inc bx
jmp INPUT1
INPUT2:
pop bx
pop dx
pop ax
popf
;
ret
INPUT endp
;-------------------------------------
;功能:删除字符串中的空格符
;入口参数:DS:SI=ES:DI=字符串缓冲区首地址
;出口参数:AX=字符串去除空格后的长度
DELSP PROC
pushf
push bx
push dx
push si
push di
push cx

cld ;DF位置0
;dx保存字符串真实长度
mov dx,cx
mov al,20h
jmp DELSP2
DELSP1:
pop di
pop cx
;
DELSP2:
REPNZ SCASB
;计数器,每出现一个空格dx减1
dec dx
;bl判断有没有处理完字符串,bl为0,说明字符串已经处理结束
mov bl,[di]
cmp bl,0
jz DELSP3
;保存 cx 和 di 的值
push cx
push di
;移动字符串DI、SI、CX的值都会改变
mov si,di
dec di
REPZ MOVSB
jmp DELSP1
DELSP3:
;返回值
mov ax,dx
;
pop cx
pop di
pop si
pop dx
pop bx
popf
ret
DELSP ENDP

;--------------------------------------------------------
NEWLINE PROC
; 功能:形成回车和换行(光标移到写一行首)
; 入口参数:无
; 出口参数:无
; 说 明:通过显示回车符形成回车,通过显示换行符形成换行
;---------------------------------------------------------
push ax
push dx
mov dl,0dh
mov ah,2
int 21h
mov dl,0ah
mov ah,2
int 21h
pop dx
pop ax
ret
NEWLINE endp
code ends
end start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
;程序名:T6-10-1.asm
;功能:接收一个字符串,去掉其中的空格后,最后按相反的顺序显示它。
;注:书中代码有些问题,CX=字符串长度+1
assume ds:data,cs:code

MAXLEN = 64
SPACE = ' '
CR = 0DH
LF = 0AH

data segment
buffer DB MAXLEN+1,0,MAXLEN+1 DUP(0)
STRING DB MAXLEN+3 DUP(0)
data ends

code segment
start:
mov ax,data
mov ds,ax
mov es,ax
;
MOV DX,OFFSET BUFFER
MOV AH,10
INT 21h ;0AH号功能:DS:DX=缓冲区最大字符数;DS:DX+1=实际输入的字符数
XOR CH,CH
MOV CL,BUFFER+1 ;CX=字符串长度
INC CL ;循环次数=字符串长度+1
JCXZ OK
;
CLD
MOV SI,OFFSET BUFFER+2 ;输入字符串偏移
MOV DI,OFFSET STRING ;目的字符串
XOR AL,AL
STOSB ;改变 DI 的偏移
MOV AL,SPACE
PP1:
XCHG SI,DI ;调换 SI 和 DI 的偏移
REPZ SCASB ;比较字符串是否有空格,每比较一个字符 DI+1
XCHG SI,DI ;还原 DI 和 SI
JCXZ PP3 ;CX=0,比较结束
DEC SI
INC CX
PP2:
CMP BYTE PTR [SI],SPACE ;删除空格,按顺序把不是空格的字符,放到目的字符串中
JZ PP1 ;双循环比较,移动字符
MOVSB
LOOP PP2
;
PP3:
MOV AL,CR
STOSB
MOV AL,LF
MOV [DI],AL
STD
MOV SI,DI
PP4:
LODSB
OR AL,AL
JZ OK
MOV DL,AL
MOV AH,2
INT 21H
JMP PP4
;
OK:
MOV AH,4CH
INT 21H
code ends
end start

书中代码有些问题,CX=字符串长度+1

例4:写一个判断字符串2是否为字符串1的子程序。具体要求如下:(1)子程序是一个远过程;(2)指向字符串的指针是远指针(即包括段值);(3)通过堆栈传递两个分别指向字符串1和字符串2的远指针;(4)由DX:AX返回指向字符串2在字符串1中首次出现的指针,如字符串2不是字符串1的子串,则返回空指针;(5)字符串均以0为结束符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
;程序名:T6-11.asm
;功能:写一个判断字符串2是否为字符串1的子程序。
assume ds:data,cs:code

data segment
string1 db 'hello word!',0
string2 db 'word',0
data ends

code segment
start:
mov ax,data
mov ds,ax
;
;mov ax,SEG string1
push ds
lea si,string1
push si
mov ax,SEG string2
push ax
lea di,string2
push di
call FAR PTR STRSTR
;
MOV AH,4CH
INT 21H

;--------------------------------------------
STRSTR PROC FAR
;子程序名:STRSTR
;功能:判断字符串2是否为字符串1的子串
;入口参数:指向字符串的远指针,DS:SI=字符串1地址;ES:DI=字符串2地址
;出口参数:DX:AX返回指向字符串2在字符串1中首次出现的处的指针
;--------------------------------------------
push bp
mov bp,sp
push bx
push cx
push di
push si
;
xor bx,bx
mov ax,[bp+12]
mov ds,ax
mov si,[bp+10]
mov ax,[bp+8]
mov es,ax
mov di,[bp+6]
;计算字符串2长度,不包括结束字符
mov cx,0ffffh
mov al,0
repnz scasb
mov di,[bp+6] ;还原di
not cx
dec cx ;字符串2的长度
jmp STRSTR2
;
STRSTR1:
pop cx ;每次循环还原si
mov di,[bp+6] ;每次循环还原di
mov si,[bp+10] ;还原si
STRSTR2:
inc bx
add si,bx ;si+1
push cx
repnz cmpsb ;si=7,di=6找到第一个字符
mov dx,ds ;
mov ax,si ;保存"段值:偏移",直至循环判断到最后一次
dec ax ;指针指向下一个字符的偏移,还原当前偏移
;
repz cmpsb ;如果后面的字符匹配出错,就继续循环
jnz STRSTR1 ;找不到相等的字符继续循环
;
jcxz STRSTR4
xor dx,dx ;返回空指针
xor ax,ax
STRSTR4:
;
pop cx ;把之前的 cx 提取出来,主要用于还原堆栈
pop si
pop di
pop cx
pop bx
mov sp,bp
pop bp
ret
STRSTR ENDP
code ends
end start

十进制数算数运算调整指令及应用

8086/8088 的十进制算数运算调整指令所认可的十进制数是以8421BCD码表示的,它分为未组合和组合的两种。组合的BCD码是指一字节含两位 BCD 码;未组合的BCD码是指一字节含一位BCD码,字节的高四位无意义。

组合的BCD码的算数运算调整指令

组合的BCD码加法调整指令DAA(Decimal Adjust for Addition)

组合的 BCD 码加法调整指令格式如下:
DAA

这条指令对在 AL 中的和(由两个组合的 BCD 码相加后的结果)进行调整,产生一个组合的 BCD 码。
(1)如 AL 中的低 4 位在 A~F 之间,或 A~F 为 1,则 AL<-(AL)+6,且 AF 位置 1
(2)如 AL 中的高4位在 A~F 之间,或 CF 为 1,则 AL<-(AL)+60H,且 CF 位置 1
该指令影响标志位 AF,CF,PF,SF和ZF,但不影响标志位 OF。

案例:演示 DAA 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;程序名:T6-12.asm
;功能:演示 DAA 指令
assume cs:code

code segment
start:
xor ax,ax
mov al,34h
add al,47h
DAA ;34h+47h=7b,7b+6=81,十进制数 34+47=81
adc al,87h ;81h+87h=108h,但是 al只能存储1字节,所以结果是08h
DAA ;因为有溢出 CF=1,08h+60h=68h
adc al,79h ;AL=E2h
DAA ;高4位在 A~F 之间,E2h+60h=42h;低4位+6,42+6=48h
;
mov ax,4c00h
int 21h
code ends
end start

组合的 BCD 码减法调整指令 DAS(Decimal Adjust for Subtraction)

组合的 BCD 码减法调整指令的格式如下:
DAS
这条指令对在 AL 中的差(由两个组合 BCD 码相减后的结果)进行调整,产生一个组合的 BCD 码。调整方法如下:
(1)如 AL 中的低 4 位在 A~F 之间,或 AF 为 1,则 AL<-(AL)-6,且 AF 位置 1。
(2)如 AL 中的高 4 位在 A~F 之间,或 CF 为 1,则 AL<-(AL)-60h,且 CF 位置 1。
该指令影响标志 AF,CF,PF,SF和ZF,但不影响标志 OF。

案例:演示 DAS 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;程序名:T6-13.asm
;功能:演示 DAS 指令
assume cs:code

code segment
start:
xor ax,ax
mov al,45h
sub al,27h ;AL=1EH,AF=1,CF=0
DAS ;因为 AF=1,AL=1EH-6=18H
sbb al,49h ;AL=CFH,AF=1,CF=1
DAS ;AL=CFH-66H=69H
;
mov ax,4c00h
int 21h
code ends
end start

未组合的 BCD 码加法调整指令AAA(ASCII Adjust for Addition)

未组合的 BCD 码加法调整指令格式如下:
AAA
这条指令对在 AL 中的和(由两个未组合的 BCD 码相加后的结果)进行调整,产生一个未组合的 BCD 码。调整方法如下:
(1)如 AL 中的低 4 位在 0~9 之间,且 AF为0,则跳到(3)进行处理
(2)如 AL 中的低 4 位在 A~F 之间,或AF为1,则 AL<-(AL)+6,AH<-(AH)+1,且 AF 位置1
(3)清除 AL 的高 4 位。
(4)AF 位的值送 CF 位。
该指令影响标志位 AF 和 CF,对其他标志均无定义

案例:演示 AAA 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;程序名:T6-14.asm
;功能:演示 AAA 指令
assume cs:code

code segment
start:
xor ax,ax
mov ax,7
add al,6 ;AL=0DH
AAA ;AL=0DH+6=13H;AL高4位清0 AL=03;AH=1;AF=1,CF=1
adc al,5 ;AL=09H
AAA ;AH=1
add al,39h ;AL=42H,AF=1
AAA ;AL=48J,AH=2,AL=08H,AF=1,CF=1
;
mov ax,4c00h
int 21h
code ends
end start

未组合的 BCD 码减法调整指令AAS(ASCII Adjust for Subtraction)

未组合的 BCD 码减法调整指令格式如下:
AAS
这条指令在对 AL 中的差(由两个未组合的BCD码相减后的结果)进行调整,产生一个未组合的 BCD 码。调整方法如下:
(1)如 AL 中的低 4 位在 0~9 之间,且 AF 为 0,则跳转到(3)进行处理;
(2)如 AL 中的低 4 位在 A~F 之间,或 AF 为 1,则 AL<-(AL)-6,AH<-(AH)-1,且 AF 位置1;
(3)清除 AL 的高 4 位;
(4)AF 位送至 CF 位。
该指令影响标志位 AF 和 CF,对其他标志均无定义。

案例:演示 AAS 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;程序名:T6-15.asm
;功能:演示 AAS 指令
assume cs:code

code segment
start:
xor ax,ax
mov al,34h
sub al,09h ;AL=2BH,AF=1,CF=0
AAS ;AL=05,AH=0FFH,AF=1,CF=1
;
mov ax,4c00h
int 21h
code ends
end start

未组合的 BCD 乘法调整指令AAM(ASCII Adjust for Multiplication)

未组合的 BCD 码乘法调整指令的格式如下:
AAM
这条指令对在 AL 中的积(由两个组合的 BCD 码相乘的结果)进行调整,产生两个未组合的 BCD 码。调整方法如下:
(1)把 AL 中值除以10,商放到 AH 中,余数放到 AL 中。
该指令影响标志SF,ZF和PF,对其他标志无影响。

案例:演示 AAM 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;程序名:T6-16.asm
;功能:演示 AAM 指令
assume cs:code

code segment
start:
xor ax,ax
mov al,3
mov bl,4
mul bl ;AL=0CH,AH=0
AAM ;AL=2,AH=1
mov ax,4c00h
int 21h
code ends
end start

未组合的 BCD 码除法调整指令AAD(ASCII Adjust for Division)

未组合的 BCD 码除法调整指令的格式如下:
AAD
该指令和其他调整指令的使用次序上不同,其他调整指令均安排在有关算数运算指令后,而这条指令应该安排在除法运算指令之前。它的功能是:把存放在寄存器 AH(高位十进制数)及存放在寄存器 AL 中的两位非组合 BCD 码,调整为一个二进制数,存放在寄存器 AL 中。调整的方法如下:

1
2
AL<-AH*10+(AL)
AH<-0

改指令影响标志 SF,ZF和PF,对其他标志无影响。

案例:演示 AAD 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;程序名:T6-17.asm
;功能:演示 AAD 指令
assume cs:code

code segment
start:
xor ax,ax
mov ah,4
mov al,3
mov bl,8
AAD ;AL=4*10+3=43=2BH,AH=0
div bl ;AL=5,AH=3
mov ax,4c00h
int 21h
code ends
end start

应用举例

例1:设在缓冲区 DATA 中存放着12个组合的BCD码,求它们的和,把结果存放到缓冲区 SUM 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
;程序名:T6-18.asm
;功能:设在缓冲区 DATA 中存放着12个组合的BCD码,求它们的和,把结果存放到缓冲区 SUM 中。
assume ds:data,cs:code

data segment
NUM1 DB 23H,45H,67H,89H,32H,93H,36H,12H,66H,78H,43H,99H
RESULT DB 2 DUP(0)
data ends

code segment
start:
mov ax,data
mov ds,ax
mov bx,offset NUM1
mov cx,10
xor ax,ax
next:
add al,[bx]
DAA
ADC ah,0
xchg ah,al
DAA
xchg ah,al
inc bx
loop next
mov ax,4c00h
int 21h
code ends
end start

例2:使用 DAA 指令改写把一位十六进制数转换为对应的 ASCII 码符的子程序 HTOASC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
;程序名:T6-19.asm
;功能:使用 DAA 指令改写把一位十六进制数转换为对应的 ASCII 码符的子程序 HTOASC。
assume cs:code

code segment
start:
mov al,0ch
call HTOASC
mov ax,4c00h
int 21h

HTOASC PROC
;-------------------------------------------------------
;子程序名:HTOASC
;功能:把一个十六进制数转换为对应的 ASCII
;入口参数:AL 的低4位为要转换的十六进制数
;出口参数:AL含对应的 ASCII 码
;-------------------------------------------------------
and al,0fh
add al,90h ;主要作用是把al的高4位清零,如:9Ch+6=A2h
DAA ;AL=60h+A2h=102h,al=02h,此时出现溢出(这两行代码,巧妙的使 al 高 4 位清零,同时又出现溢出)
adc al,40h ;AL=42h+1=43h
DAA
ret
HTOASC ENDP
code ends
end start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
;程序名:T6-19-1.asm
;功能:使用 DAA 指令改写把一位十六进制数转换为对应的 ASCII 码符的子程序 HTOASC。
assume cs:code

code segment
start:
mov al,0Dh
call HTOASC
mov ax,4c00h
int 21h

HTOASC PROC
;-------------------------------------------------------
;子程序名:HTOASC
;功能:把一个十六进制数转换为对应的 ASCII
;入口参数:AL 的低4位为要转换的十六进制数
;出口参数:AL含对应的 ASCII 码
;-------------------------------------------------------
and al,0fh
DAA
add al,31h
ret
HTOASC ENDP
code ends
end start

书中的例子固然巧妙,但在这个例子中,可以人为确认的规律,就没必要多写无关紧要的代码。

例3:写一个能实现两个十进制数的加法运算处理的程序。设每个十进制数最多10位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
;程序名:T6-20.asm
;功能:写一个能实现两个十进制数的加法运算处理的程序。设每个十进制数最多10位。
assume ds:data,cs:code

;常数定义
MAXLEN = 10
BUFFLEN = MAXLEN+1

data segment
BUFF1 DB BUFFLEN,0,BUFFLEN DUP(?)
NUM1 EQU BUFF1+2
BUFF2 DB BUFFLEN,0,BUFFLEN DUP(?)
NUM2 EQU BUFF2+2
RESULT DB BUFFLEN DUP(?),24H
DIGITL DB '0123456789'
DIGITLEN EQU $-DIGITL
MESS DB 'Invalid number!',0DH,0AH,24H
data ends

code segment
start:
MOV AX,data
MOV DS,AX
MOV ES,AX ;置DS和ES
MOV DX,OFFSET BUFF1
CALL GETNUM ;接收被加数
JC OVER
MOV DX,OFFSET BUFF2
CALL GETNUM
JC OVER
MOV SI,OFFSET NUM1
MOV DI,OFFSET NUM2
MOV BX,OFFSET RESULT
MOV CX,MAXLEN
CALL ADDITION
MOV DX,OFFSET RESULT
CALL DISPNUM
JMP SHORT OK
OVER:
MOV DX,OFFSET MESS
MOV AH,9
INT 21h
OK:
MOV AX,4c00h
INT 21h
GETNUM PROC
;-------------------------------------------------------
;子程序名:GETNUM
;功能:接收一个十进制数字串,且扩展成10位
;入口参数:DX=缓冲区偏移
;出口参数:CF=0,表示成功;CF=1,表示不成功
;-------------------------------------------------------
MOV AH,10
INT 21h
CALL NEWLINE
CALL ISDNUM
JC GETNUM2
MOV SI,DX
INC SI
MOV CL,[SI]
XOR CH,CH
MOV AX,MAXLEN
STD
MOV DI,SI
ADD DI,AX
ADD SI,CX
SUB AX,CX
REP MOVSB
MOV CX,AX
JCXZ GETNUM1
XOR AL,AL
REP STOSB
GETNUM1:
CLD
CLC
GETNUM2:
RET
GETNUM ENDP

ADDITION PROC
;-------------------------------------------------------
;子程序名:ADDITION
;功能:多位非组合 bcd 码数相加
;入口参数:SI=代表被加数的非组合 BCD 码串开始地址偏移
; DI=代表加数的非组合 BCD 码串开始地址偏移
; CX=BCD码串长度(字节数)
; BX=存放结果的缓冲区开始地址偏移
;出口参数:结果缓冲区结果
;说 明:在非组合的 BCD 码中,十进制数的高位在低地址
;-------------------------------------------------------
STD
ADD BX,CX
ADD SI,CX
ADD DI,CX
DEC SI
DEC DI
XCHG DI,BX
INC BX
CLC
ADDP1:
DEC BX
LODSB
ADC AL,[BX]
AAA
STOSB
LOOP ADDP1
MOV AL,0
ADC AL,0
STOSB
CLD
RET
ADDITION ENDP

DISPNUM PROC
;-------------------------------------------------------
;子程序名:DISPNUM
;功能:显示结果
;入口参数:DX=结果缓冲区开始地址偏移
;出口参数:无
;-------------------------------------------------------
;OR CX,1 ;把标志位 zf 置 0
MOV DI,DX
MOV AL,0
MOV CX,MAXLEN
REPZ SCASB
;计算有效字符长度
DEC DI
mov bx,di
sub bx,dx
lea cx,[bx-MAXLEN-1]
not cx
;
MOV DX,DI
MOV SI,DI
;
INC CX
DISPNU2:
LODSB
ADD AL,30H
STOSB
LOOP DISPNU2
MOV AH,9
INT 21h
RET
DISPNUM ENDP

ISDNUM PROC
;-------------------------------------------------------
;子程序名:ISDNUM
;功能:判断一个利用 DOS 的 0AH 号功能调用输入的字符串是否为数字字符串
;入口参数:DX=缓冲区开始地址偏移
;出口参数:CF=0,表示是;CF=1,表示否
;-------------------------------------------------------
MOV SI,DX
LODSB
LODSB
MOV CL,AL
XOR CH,CH
JCXZ ISDNUM2
ISDNUM1:
LODSB
CALL ISDECM
JNZ ISDNUM2
LOOP ISDNUM1
RET
ISDNUM2:
STC
RET
ISDNUM ENDP

ISDECM PROC
;-------------------------------------------------------
;子程序名:ISDECM
;功能:判断一个字符是否为十进制字符
;入口参数:AL=字符
;出口参数:ZF=1,表示是;zf=0,表示否
;-------------------------------------------------------
PUSH CX
MOV DI,OFFSET DIGITL
MOV CX,DIGITLEN
REPNZ SCASB
POP CX
RET
ISDECM ENDP

NEWLINE PROC
;-------------------------------------------------------
;子程序名:NEWLINE
;功能:打印换行
;入口参数:无
;出口参数:无
;-------------------------------------------------------
push ax
push dx
mov dl,0dh
mov ah,2
int 21h
mov dl,0ah
mov ah,2
int 21h
pop dx
pop ax
ret
NEWLINE ENDP
code ends
end start

书中的例子中 DISPNUM 子程序,并没有计算有效字符长度,导致输出结果少一位。

DOS 程序段前缀和特殊情况处理程序

DOS程序段前缀 PSP

程序段前缀(简称:PSP)是 DOS 加载一个外部命令或应用程序(EXE或COM类型)时,在程序段之前设置一个具有 256 字节的信息区。
2

DOS 系统刚加载程序,但未开始执行程序时,此时红框中的 256 个字节就是 DOS 程序段前缀。注意:这些数据是属于 DOS 操作系统的,并不属于我们调试的 EXE 程序。

PSP 含有多个可用信息,其中常用信息的安排如下表:
1

根据上图中的偏移,我们可以在 DOS 程序段前缀找到相应的数据。如:偏移 0 处,的十六进制是 “CD 20”,这里表示的就是指令 “INT 20H”

3

如上图所示,这里演示了命令行参数的偏移。

当 DOS 把控制权转给外部命令或应用程序时,数据段寄存器 DS 和附加段寄存器 ES 均指向其 PSP,即均含有 PSP 的段值,并不指向程序的数据段和附加段。这样应用程序可方便地使用到 PSP 中地有关信息。

终止程序的另一途径

利用 DOS 的 4CH 号系统功能调用能终止程序,把控制权转交给DOS,这是我们现在常用的方法。但早先常利用 DOS 提供的 20H 号中断处理程序来终止程序。
通过 20H 号中断处理程序终止程序有一个条件,即进入 20H 号中断处理程序之前,代码段寄存器 CS 必须含有 PSP 的段值。由于对 EXE 类型的应用程序而言,其代码段与 PSP 不是同一个段,所以不能简单地直接利用指令 “INT 20H”来终止程序。DOS 注意到了这一点,在 PSP 的偏移 0 处,安排了一条 “INT 21H” 来终止程序。 于是,应用程序只要设法转到 PSP 的偏移 0 处,就能实现程序的终止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
;程序名:T6-21.asm
;功能:另一种终止程序的方式,利用 INT 20H。
assume ds:data,cs:code,ss:stack

stack segment
DW 256 DUP(?)
stack ends

data segment
MESS db 'HELLO',0DH,0AH,'$'
data ends

code segment
MAIN PROC FAR
START:
PUSH DS ;把 PSP 的段值压入堆栈
XOR AX,AX ;偏移地址置0
PUSH AX ;压入偏移地址
;
MOV AX,data
MOV DS,AX
MOV DX,OFFSET MESS
MOV AH,9
INT 21h
RET
MAIN ENDP
code ends
end start
  1. 在标号 start 开始处的三条指令把 PSP 的段值和偏移地址 0 压入堆栈;
  2. ret 从堆栈中弹出程序开始时压入堆栈的 PSP 段值和偏移 0 到 CS 和 IP 中;
  3. 执行位于 PSP 首的指令 “INT 20H”,程序终止。

这里多了一个名为 MAIN 的远过程子程序,这样做的目的是告诉汇编程序,把为了终止程序返回 DOS 而设的 RET 指令汇编成远返回指令。

应用程序取得命令行参数

DOS 加载一个外部命令或应用程序时,允许在被加载的程序名之后,输入多达 127 个字符(包括最后的回车符)的参数,并把这些参数送到 PSP 的非格式化参数区,即 PSP 中从偏移 80H 开始的区域。注意,命令行中的重定向符和管道符及有关信息不作为命令行参数送到 PSP。

例1:写一个显示命令行参数的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
;程序名:T6-22.asm
;功能:显示命令行参数。。
assume ds:data,cs:code


data segment
parameter db 127 dup(0)
data ends

code segment
START:
mov ax,ds
mov es,ax
mov di,81h ;要打印的参数从 81 开始
;
mov ax,data
mov ds,ax
lea si,parameter
push di
;
mov al,0dh
mov cx,0ffffh
repnz scasb
not cx
;
pop di
xchg si,di
mov ax,es
mov bx,ds
mov es,bx
mov ds,ax
rep movsb
;
mov byte ptr es:[di-1],24h
MOV AX,data
MOV DS,AX
MOV DX,OFFSET parameter
MOV AH,9
INT 21h
;
mov ax,4c00h
int 21h
code ends
end start

例2:写一个显示文本文件内容的程序。文件名作为命令行参数给出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
;程序名:T6-23.asm
;功能:写一个显示文本内容的程序文件名作为命令行参数给出。
assume ds:data,cs:code

EOF = 1AH

data segment
filename db 128 dup(0)
handle dw 0
buffer db 128 dup(0)
data ends

code segment
START:
mov ax,data
mov ds,ax
lea dx,filename
call MOVPAR
;打开文件
mov ah,3dh
mov al,0
int 21h
jc stop
mov bx,ax
mov handle,ax
cont:
call READCH ;从文件中读一个字符
jc stop
cmp al,EOF
jz stop
call putch
jmp cont
;关闭文件
mov ah,3EH
int 21h
stop:
mov ax,4c00h
int 21h

;----------------------------------------------
;子程序名:MOVPAR
;功能:传递-f参数到缓冲区
;入口参数:dx=存放“-f”参数的缓冲区
;出口参数:无
;注:该子程序必须要放在代码段最前面
MOVPAR PROC
pushf
push ds
push es
push ax
push bx
push cx
push dx
push si
push di
;
mov di,80h
mov cx,0ffffh
mov al,'-'
repnz scasb
mov ax,'f-'
cmp ax,es:[di-1]
jnz stop
next:
inc di
cmp byte ptr es:[di],20h
jz next ;如果第一个字符是空格就剔除掉
;
push di
mov cx,0ffffh
mov al,0dh
repnz scasb
not cx
dec cx
;
pop di
xchg si,di
push ds
push es
pop ds
pop es
mov di,dx
rep movsb
;
pop di
pop si
pop dx
pop cx
pop bx
pop ax
pop es
pop ds
popf
ret
MOVPAR ENDP

;-----------------------------------------------------
READCH PROC
; 功能:每次从文件中按顺序读一个字符。
; 传参方式:寄存器传参
; 入口参数:AL=十六进制
; 出口参数:AX=高4位,低4位
; 说 明:子程序通过进位标志CF来反映是否正确读到字符,
;如果读时发生错误,则CF置位,否则CF清零。考虑到万一文本
;文件没有文件结束符的情况,所以该子程序还判断是否的却已
;读到文件尾(如果实际读到的字符为0就意味着文件结束),
;当这种情况发生时,就返回一个文件结束符。
;------------------------------------------------------
mov cx,1
mov dx,offset buffer
mov ah,3FH
int 21h
jc READCH2
cmp ax,cx
mov al,EOF
jb READCH1
mov AL,BUFFER
READCH1:
CLC
READCH2:
ret
READCH endp

;-----------------------------------------------------
PUTCH PROC
;
; 功能:使用 int 21h的 2 号功能输出字符。
; 传参方式:寄存器传参
; 入口参数:al
; 出口参数:无
;-----------------------------------------------------
push dx
mov dl,al
mov ah,2
int 21h
pop dx
ret
PUTCH endp
code ends
end start

对 CTRL+C 键和 CTRL+BREAK 键的处理

CTRL + C 键的处理程序

先看如下程序,它的功能是在屏幕上显示用户所按字符,直到用户按 ESC 键为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
;程序名:T6-24.asm
;功能:在屏幕上显示用户所按字符,直到用户按 ESC 键为止。
assume cs:code

CR = 0dh
LF = 0ah
ESCAPE = 1Bh

code segment
START:
push cs
pop ds
COUNT:
mov ah,8
int 21h
cmp al,ESCAPE
jz short XIT
mov dl,al
mov ah,2
int 21h
cmp dl,CR
jnz COUNT
mov ah,2
int 21h
mov dl,LF
mov ah,2
int 21h
jmp COUNT
XIT:
mov ax,4c00h
int 21h
code ends
end start

注:DOSBOX 装的是简化版的操作系统,该系统不支持 CTRL+C 功能。所以暂时无法对该程序进行验证。

在 DOS 下的 exe 程序是可以通过快捷键 CTRL+C 终止程序的。当应用程序利用 DOS 系统功能调用进行字符输入输出时,DOS 通常要检测 CTRL+C 键。如果检测到,就先显示符号 “^C” ,并产生中断 “int 23h”。缺省的 23H 号中断处理程序是终止程序运行。DOS 提供的这一功能是为了方便用户随机终止一个执行错误或不必执行的程序。
DOS 为应用程序改变这种处理方法做了准备。应用程序只要改变 23H 号中断处理程序,就可基本控制住 CTRL+C 键的处理。为了改变 23H 号中断处理程序,应用程序得提供一个新的 23H 号中断处理程序,然后修改 23H 号中断向量,使其指向新的 23H 号中断处理程序。由于 DOS 在设置 PSP 时,已把当时的 23H 号中断向量保存到 PSP 中,且在程序终止时再自动从 PSP 中取出并恢复。所以,应用程序在修改 23H 号中断向量后,可不必恢复它。

下面的程序增加了 23H 号中断处理程序,该处理程序及其简单,只有一条中断返回指令 IRET,即不做任何处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
;程序名:T6-24A.asm
;功能:在屏幕上显示用户所按字符,直到用户按 ESC 键为止。禁止使用 CTRL+C 结果程序。
assume cs:code

CR = 0dh
LF = 0ah
ESCAPE = 1Bh

code segment
new23h:
iret ;新的中断处理程序,不需要做任何处理,直接返回就好。
START:
push cs
pop ds
mov dx,offset new23h
mov ax,2523h ;指向新的 23h 号中断向量
int 21h
COUNT:
mov ah,8
int 21h
cmp al,ESCAPE
jz short XIT
mov dl,al
mov ah,2
int 21h
cmp dl,CR
jnz COUNT
mov ah,2
int 21h
mov dl,LF
mov ah,2
int 21h
jmp COUNT
XIT:
mov ax,4c00h
int 21h
code ends
end start

尽管按 CTRL+C 键不再能终止该程序的运行,但屏幕上却显示出符号 “^C”。如果应用程序不在乎由于按 CTRL+C 键带来的符号,那么上述处理就可接受。如果不愿显示由 CTRL+C 键带来的符号,那么如下几种处理方法也许可以满足需求:(1)应用程序使用不检测 CTRL+C 键的 DOS 功能调用进行字符输入输出;(2)应用程序不利用 DOS 功能调用进行字符输入输出。对一般程序而言这两种方法不完全有效。首先,应用程序不利用 DOS 功能调用进行字符输入输出,就要利用 BISO 进行字符输入输出或直接进行输入输出,有时这样操作是比较麻烦的。其次,在大多数 DOS 系统功能调用期间,DOS 要查看 CTRL+BREAK 键是否被按下,如发现 CTRL+BREAK 键被按,则也会显示符号 “^C” 和 产生 INT 23H 中断。

对 CTRL+BREAK 键的处理

键盘中断处理程序(9H 号中断处理程序)发现 CTRL+BREAK 键被按时,将产生 INT 1BH。在 DOS 自举时,由 DOS 提供的 1BH 号中断处理程序将在约定的内存单元中设置一个标志,然后结束。DOS 通过该标志检测 CTRL+BREAK 键是否被按下,如果发现被按下,则像 CTRL+C 那样显示符号 “^C” 和产生 INT 23H。
如果应用程序要自己处理 CTRL+BREAK 键,则可通过提供新的 1BH 号中断处理程序的方法来实现。所以,如果应用程序要使得 CTRL+BREAK 键不干扰程序的运行,只要使 1BH 号中断处理程序不设置与 DOS 约定的内存单元。但要注意,DOS并不自动保存和恢复 1BH 号中断向量,所以如果应用程序提供新的 1BH 号中断处理程序,那么在修改 1BH 号中断向量前,先要保存原 1BH 号中断向量,在程序结束前恢复它。
下面的程序提供了新的 1BH 号中断处理程序。作为例子,新的 1BH 号中断处理程序只显示信息“**BREAK**”,然后就返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
;程序名:T6-24B.asm
;功能:提供新的 1BH 号中断处理程序,且只显示信息“\*\*BREAK\*\*”,然后就返回
assume cs:code

CR = 0dh
LF = 0ah
ESCAPE = 1Bh

code segment
ODL1BH DD ?
VPAGE DB ?
MESS DB '**BREAK**',0
;
;新的 1BH 号中断处理程序
NEW1BH:
push ds ;保护现场
push ax
push bx
push si
push cs
pop ds
CLD
mov si,offset MESS
mov bh,VPAGE ;准备显示信息 **BREAK**
mov ah,0EH
BRKNEXT:
lodsb
or al,al
jz short BRKEXIT
int 10H
jmp BRKNEXT
BRKEXIT:
pop si
pop bx
pop ax
pop ds
iret
;新的 23H 号中断处理程序
NEW23H:
iret
;主程序
start:
push cs
pop ds
mov ah,0fh ;取状态信息
int 10H
mov VPAGE,bh ;保存当前显示页号
;
mov ax,351bh
int 21h ;取原 1bh 中断向量并保存
mov word ptr ODL1BH,bx
mov word ptr ODL1BH+2,es
;
mov dx,offset NEW23H ;置 23H 号中断向量
mov ax,2523H ;使其指向新的处理程序
int 21h
;
mov dx,offset NEW1BH ;置 1BH 号中断向量
mov ax,251bh ;使其指向新的处理程序
int 21h
COUNT:
mov ah,8
int 21h
cmp al,ESCAPE
jz short XIT
mov dl,al
mov ah,2
int 21h
cmp dl,CR
jnz COUNT
mov ah,2
int 21h
mov dl,LF
mov ah,2
int 21h
jmp COUNT
XIT:
lds dx,ODL1BH
mov ax,251bh ;恢复原 1BH 号中断向量
int 21h
mov ax,4c00h
int 21h
code ends
end start

一个能控制住 Ctrl+C 键和 CTRL+BREAK 键的例子

在上一个例子中,控制住了 CTRL+BREAK 键。在其运行时,按 CTRL+C 键也不终止程序的运行,但仍会出现符号 “^C”。现在修改键盘管理程序(16H号中断处理程序),使其不返回 CTRL+C 键(即 ASCII 码 03H)。
在下面的程序中,提供了新的 16H 号中断处理程序,它“吃掉”了 CTRL+C 键,同时也过滤掉了 Ctrl+2 键(它类似于 Ctrl+C,其扫描码为 03H)。这样,DOS 就不可能检测到 CTRL+C 键了。新的 1BH 号中断处理程序仅时一条中断返回指令。所以不再提供新的 23H 号中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
;程序名:T6-24C.asm
;功能:提供新的 1BH 号中断处理程序,且只显示信息“\*\*BREAK\*\*”,然后就返回
assume cs:code

CR = 0dh
LF = 0ah
ESCAPE = 1Bh

code segment
ODL1BH DD ?
ODL16H DD ?
;
;新的 16H 号中断处理程序
NEWKEY PROC FAR
NEW16H:
cmp ah,10H
jz PKEY
cmp ah,11h
jz PKEY2
or ah,ah
jz PKEY
jmp dword ptr cs:ODL16H ;转原16H号中断处理程序
;
PKEY:
push ax
PKEY1:
pop ax
push ax
pushf
call dword ptr cs:ODL16H ;调原16H号中中断处理程序
cmp al,3
jz PKEY1
add sp,2
iret
;
PKEY2:
push ax
PKEY3:
pop ax
push ax
pushf
call dword ptr cs:ODL16H
jz PKEY6
cmp al,3
jz PKEY4
cmp ax,0300h
jnz PKEY5
;
PKEY4:
xor ah,ah
pushf
call dword ptr cs:ODL16H
jmp PKEY3
;
PKEY5:
add sp,2
cmp ax,0300h
ret 2
;
PKEY6:
add sp,2
cmp ax,ax
ret 2
NEWKEY ENDP
NEW1BH:
iret
;---------------------------------
;主程序
start:
push cs
pop ds
mov ax,3516h
int 21h
mov word ptr ODL16H,bx
mov word ptr ODL16H+2,es
;
mov ax,351bh
int 21h
mov word ptr ODL1BH,bx
mov word ptr ODL1BH,es
;
mov dx,offset NEW16H
mov ax,2516h
int 21h
mov dx,offset NEW16H
mov ax,2516h
int 21h
mov dx,offset NEW1BH
mov ax,251bh
int 21h
COUNT:
mov ah,8
int 21h
cmp al,ESCAPE
jz short XIT
mov dl,al
mov ah,2
int 21h
cmp dl,CR
jnz COUNT
mov ah,2
int 21h
mov dl,LF
mov ah,2
int 21h
jmp COUNT
XIT:
lds dx,ODL1BH
mov ax,251bh
int 21h
lds dx,cs:ODL16H
mov ax,2516h
int 21h
mov ax,4c00h
int 21h
code ends
end start

TSR 程序设计举例

TSR(Terminate and stay Resident)意为结束并驻留。TSR程序是一种特殊的 DOS 应用程序,不同于结束即退出的一般 DOS 应用程序。TSR程序装入内存并初次运行后,程序的大部分仍驻留在内存中,被某种条件激活后又投入运行。它能及时地处理许多驻留程序不能处理地时间,并可为单任务操作系统 DOS 增添一定地多任务处理能力。

驻留的时钟显示程序

通常 TSR 程序由驻留内存部分和初始化部分组成。把TSR程序员装入内存时,初次运行的是初始化部分。初始化程序的主要功能是,对驻留部分完成必要的初始化工作;使驻留部分保留在内存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
;程序名:T6-25.asm
;功能:在内存中驻留显示时钟的程序
assume cs:code,ds:code

code segment
count_val=18 ;间隔次数
dpage=0 ;显示页号
row=0 ;显示时钟行号
column=80-buff_len ;显示时钟的开始列号
color=07h ;显示时钟的属性

;代码
;1CH号中断处理程序使用的变量
count dw count_val ;计数
hhhh db ?,?,':' ;时
mmmm db ?,?,':' ;分
ssss db ?,? ;秒
buff_len=$-offset hhhh ;buff_len为显示信息长度
cursor dw ?
OLD1CH DD ? ;保存原中断向量变量
;1CH 号中断处理程序
NEW1CH:
cmp cs:count,0
jz next
;
next:
mov cs:count,count_val
sti
;再真正执行程序之前保护寄存器
push ds
push es
push ax
push bx
push cx
push dx
push si
push bp
;
push cs
pop ds
push ds
pop es ;置代码段寄存器

;输出时间
call far ptr GET_T
;取原光标位置
mov bh,dpage
mov ah,3
int 10h
;保存原光标位置
mov cursor,dx
mov bp,offset hhhh
mov bh,dpage
mov dh,row
mov dl,column
mov bl,color
mov cx,buff_len
mov al,0
mov ah,13h ;显示时钟
int 10h
mov bh,dpage
mov dx,cursor
mov ah,2
int 10h ;恢复原光标

pop bp
pop si
pop dx
pop cx
pop bx
pop ax
pop es
pop ds
;结束程序
jmp dword ptr cs:OLD1CH ;替换 iret

GET_T proc far
mov ah,2 ;取时间信息
int 1ah
mov al,ch ;把时数转换为可显形式
call TTASC
;call TECHO
xchg ah,al
mov word ptr hhhh,ax ;保存
mov al,cl ;把分转换为可显示形式
call TTASC
;call TECHO
xchg ah,al
mov word ptr mmmm,ax ;保存
mov al,dh ;把秒转换为可写形式
call TTASC
;call TECHO
xchg ah,al
mov word ptr ssss,ax ;保存
ret
GET_T endp

;将时间的压缩bcd码转换为ascii
;输入参数al
;输出参数ax
TTASC proc
mov ah,al
and al,0fh
shr ah,1
shr ah,1
shr ah,1
shr ah,1
add ax,3030h
ret
TTASC endp

start:
push cs
pop ds
mov ax,351ch
int 21h
mov word ptr OLD1CH,bx
mov word ptr OLD1CH+2,es ;保存原 1CH 号中断

mov dx,offset NEW1CH
mov ax,251ch
int 21h ;设置新的 1CH 号中断

;计算驻留字节数并驻留退出
mov dx,offset start ;欲驻留部分代码和数据的字节数
add dx,15 ;考虑字节数不是 16 倍数的情况
mov cl,4
shr dx,cl ;转换成字节数
add dx,10h ;加上 PSP 的长度
mov ah,31h
int 21h ;结束并驻留
code ends
end start

初始化部分包含了驻留退出的代码,把 1CH 号中断处理程序驻留在内存中,此外还把原 1CH 号中断向量的双字变量 OLD1CH 移到驻留区。
通过 DOS 的 31H 号功能调用进行驻留退出。该功能调用的主要入口参数是 DX=驻留节数(一个节表示 16 个字节),驻留的内容从程序段前缀(PSP)开始计算,所以在计算驻留节数时,除了计算要驻留的数据和代码的长度外,还需要加上 PSP 的 10H 节。
DOS 的 31H 号功能调用与 4CH 号功能调用相比,所不同的是它在交出控制权时没有全部交出占用的内存资源,而是根据要求(由入口参数规定)保留部分。

热键激活的 TSR 程序

有多种方式或方法激活驻留的程序,键盘激活是常见的一种方法。下面是一个简单的热激活 TSR 程序的例子。热键设定为 CTRL+F8,每按一次 CTRL+F8 键,就在屏幕的固定位置显示一字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
;程序名:T6-26.asm
;功能:简单的热键激活 TSR 程序
assume cs:code,ds:code
;常量说明
BUFF_HEAD = 1AH ;键盘缓冲区头指针保存单元偏移
BUFF_TAIL = 1Ch ;键盘缓冲区尾指针保存单元偏移
BUFF_START = 1EH ;键盘缓冲区开始偏移
BUFF_END = 3EH ;键盘缓冲区结束偏移
CTRL_F8 = 6500H ;激活键扫描码
ROW = 10 ;行号
COLUMN = 0 ;列号
PAGEN = 0 ;显示页号

code segment
OLD9H DD ? ;保存原中断向量变量
MESS DB 'Hello!'
MESSLEN equ $-MESS

;9H 号中断处理程序
NEW9H:
pushf
call cs:OLD9H ;调用原中断处理程序
sti ;开中断
push ds
push ax
push bx
;
mov ax,40h
mov ds,ax
mov bx,ds:[BUFF_HEAD]
cmp bx,ds:[BUFF_TAIL] ;判断键盘缓冲区是否为空
jz IOVER ;是,结束
mov ax,ds:[bx]
cmp ax,CTRL_F8 ;是否为激活键
jz YES
;
IOVER:
pop bx ;结束处理
pop ax ;恢复现场
pop ds
iret ;中断返回
;
YES:
inc bx
inc bx ;调整键盘缓冲区头指针(取走激活键)
cmp bx,BUFF_END ;指针是否到缓冲区尾
jnz YES1 ;否,转
mov bx,BUFF_START ;是,指向头
YES1:
mov ds:[BUFF_HEAD],bx ;保存
;
push cx
push dx
push bp
push es
mov ax,cs
mov es,ax
mov bp,offset MESS
mov cx,MESSLEN
mov dh,row
mov dl,COLUMN
mov bh,PAGEN
mov bl,07h
mov al,0 ;显示后不移动光标,串中不含属性
mov ah,13h
int 10h
;
pop es
pop bp
pop dx
pop cx
jmp IOVER
;----------------------
;初始化代码
INIT:
push cs
pop ds
mov ax,3509H
int 21h
mov word ptr OLD9H,bx
mov word ptr OLD9H+2,es ;保存原 9H 号中断向量
;
mov dx,offset NEW9H ;置新的 9H 号中断向量
mov ax,2509h
int 21h
;
mov dx,offset INIT+15
mov cl,4 ;计算驻留节数
shr dx,cl
add dx,10h ;加上 PSP 的节数
mov al,0
mov ah,31h ;驻留退出
int 21h
code ends
end INIT

4

DOSBOX 不能使用热键,有兴趣的可以安装 MS-DOS 进行实验。

MSDOS8.0原版镜像 V8.0 完全安装版
利用Vmware workstation安装MS-DOS

上面程序的初始化部分先保存了 9号中断向量,然后设置新的 9号中断向量,使其指向新的键盘中断处理程序,最后驻留结束。这样每当按键,就会运行新的键盘中断处理程序。新的键盘中断处理沉痼先调用老的键盘中断处理程序完成按键工作,然后通过检查键盘缓冲区,判断是否按下照约定的热键 CTRL+F8,如果按了就显示提示信息。

  • 本文标题:8086汇编-简单应用程序的设计
  • 本文作者:9unk
  • 创建时间:2022-10-09 21:53:00
  • 本文链接:https://9unkk.github.io/2022/10/09/8086-hui-bian-jian-dan-ying-yong-cheng-xu-de-she-ji/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!