字符串处理 字符串就是一组连续的字符数据。对字符串的操作处理包括复制、检索、插入、删除和替换等。为了方便对字符串将进行有效的处理,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) 字符串存储指令格式如下:
字符串存储指令是把累加器的值存储到字符串中,即替换字符串中的一个字符。
字节存储指令 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) 字符串传送指令格式如下:
MOVSB 把寄存器SI所指向的一个字节数据传送到由寄存器 DI 所指向的存储单元中,然后根据方向标志位 DF 的值增1或减1
MOVSW 把寄存器SI所指向的一个字数据传送到由寄存器 DI 所指向的存储单元中,然后根据方向标志位 DF 的值增2或减2
DS:SI为源操作数地址,ES:DI为目的操作数地址。字符串传送指令不影响标志位。
在汇编语言中,两条字符串存储指令的格式可统一写成如下格式:
案例:修改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。
在汇编语言中,两条字符串比较指令的格式可统一写成如下格式:
重复前缀 由于串操作指令每次只能对字符串中的一个字符进行处理,所以使用了一个循环,以便完成对整个字符串的处理。为了进一步提高效率,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 中。调整的方法如下:
改指令影响标志 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 字节的信息区。
DOS 系统刚加载程序,但未开始执行程序时,此时红框中的 256 个字节就是 DOS 程序段前缀。注意:这些数据是属于 DOS 操作系统的,并不属于我们调试的 EXE 程序。
PSP 含有多个可用信息,其中常用信息的安排如下表:
根据上图中的偏移,我们可以在 DOS 程序段前缀找到相应的数据。如:偏移 0 处,的十六进制是 “CD 20”,这里表示的就是指令 “INT 20H”
如上图所示,这里演示了命令行参数的偏移。
当 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
在标号 start 开始处的三条指令把 PSP 的段值和偏移地址 0 压入堆栈;
ret 从堆栈中弹出程序开始时压入堆栈的 PSP 段值和偏移 0 到 CS 和 IP 中;
执行位于 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
DOSBOX 不能使用热键,有兴趣的可以安装 MS-DOS 进行实验。
MSDOS8.0原版镜像 V8.0 完全安装版 利用Vmware workstation安装MS-DOS
上面程序的初始化部分先保存了 9号中断向量,然后设置新的 9号中断向量,使其指向新的键盘中断处理程序,最后驻留结束。这样每当按键,就会运行新的键盘中断处理程序。新的键盘中断处理沉痼先调用老的键盘中断处理程序完成按键工作,然后通过检查键盘缓冲区,判断是否按下照约定的热键 CTRL+F8,如果按了就显示提示信息。