吾爱汇编

 找回密码
 立即注册

QQ登录

绑定QQ避免忘记帐号

查看: 1080|回复: 4

[汇编] 8086汇编-简单应用程序的设计

[复制链接]
啥都不会 发表于 2022-11-10 17:59 | 显示全部楼层 |阅读模式

本帖最后由 啥都不会 于 2022-11-10 21:29 编辑

前言:
这篇笔记主要学习 "杨季文《80x86汇编语言程序设计》第六章" --- 简单应用程序的设计。

第一部分:学习与字符串处理相关指令,使用这些指令及其机制来简化代码量;
第二部分:学习十进制数算数运算调整指令,使用这些指令及其机制来简化代码量;
第三部分:学习程序段前缀(PSP),并对DOS程序进行细节上的处理;
第四部分:学习如何将程序驻留在内存中,并且可以通过快捷键调用程序。


编程语言:
8086汇编

以下为主题内容:

字符串处理

字符串就是一组连续的字符数据。对字符串的操作处理包括复制、检索、插入、删除和替换等。为了方便对字符串将进行有效的处理,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)
字符串装入指令如下:
[Asm] 纯文本查看 复制代码
LODSB   ;装入字节(Byte)
LODSW   ;装入字(Word)

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

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


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

[Asm] 纯文本查看 复制代码
;程序名: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


在汇编语言中,两条字符串装入指令的格式可统一写成如下格式:
[Asm] 纯文本查看 复制代码
LODS OPRD

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

案例:演示LODS指令
[Asm] 纯文本查看 复制代码
;程序名: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)

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

[Asm] 纯文本查看 复制代码
STOSB
STOSW

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

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


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

[Asm] 纯文本查看 复制代码
STOS OPRD

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


案例:把当前数据段中偏移 1000H 开始的 100 个字节的数据传送到从偏移2000H 开始的单元中。
[Asm] 纯文本查看 复制代码
;程序名: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)

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

[Asm] 纯文本查看 复制代码
MOVSB   ;字节传送
MOVSW   ;字传送

  • MOVSB 把寄存器SI所指向的一个字节数据传送到由寄存器 DI 所指向的存储单元中,然后根据方向标志位 DF 的值增1或减1
  • MOVSW 把寄存器SI所指向的一个字数据传送到由寄存器 DI 所指向的存储单元中,然后根据方向标志位 DF 的值增2或减2
DS:SI为源操作数地址,ES:DI为目的操作数地址。字符串传送指令不影响标志位。


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

[Asm] 纯文本查看 复制代码
MOVS OPRD1,OPRD2


案例:修改T6-2.asm
[Asm] 纯文本查看 复制代码
;程序名: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)
字符串扫描指令如下:

[Asm] 纯文本查看 复制代码
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。


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

[Asm] 纯文本查看 复制代码
;程序名: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


在汇编语言中,两条字符串扫描指令的格式可统一写成如下格式:
[Asm] 纯文本查看 复制代码
SCAS OPRD


字符串比较指令

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

[Asm] 纯文本查看 复制代码
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。

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

[Asm] 纯文本查看 复制代码
CMPS OPRD1,OPRD2


重复前缀

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

REP 作为一个串操作指令的前缀,它重复其后的串操作指令动作。每次重复都会先判断CX是否为0,如为0就结束重复,否则CX的值减1
在重复过程中的 CX减1操作,不影响各标志位。重复前缀 REP 主要用在串传送指令 MOVS 和串存储指令 STOS 之前。值得指出的是,一般不在 LODSB 或 LODSW 指令之前使用任何重复前缀。


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

[Asm] 纯文本查看 复制代码
;程序名: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 配合,比较两个字符串是否相同。

[Asm] 纯文本查看 复制代码
;程序名: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 时止。

[Asm] 纯文本查看 复制代码
;程序名: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 结尾。
[Asm] 纯文本查看 复制代码
;程序名: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结尾。
[Asm] 纯文本查看 复制代码
;程序名: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:写一个程序,它先接收一个字符串,然后抽去其中的空格,最后按相反的顺序显示它。
[Asm] 纯文本查看 复制代码
;程序名: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


[Asm] 纯文本查看 复制代码
;程序名: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为结束符。

[Asm] 纯文本查看 复制代码
;程序名: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码的算数运算调整指令

DAA(Decimal Adjust for Addition)指令
[Asm] 纯文本查看 复制代码
DAA

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


案例:演示 DAA 指令
[Asm] 纯文本查看 复制代码
;程序名: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


DAS(Decimal Adjust for Subtraction)指令
组合的 BCD 码减法调整指令的格式如下:

[Asm] 纯文本查看 复制代码
DAS

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


案例:演示 DAS 指令
[Asm] 纯文本查看 复制代码
;程序名: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


AAA(ASCII Adjust for Addition)指令
未组合的 BCD 码加法调整指令格式如下:

[Asm] 纯文本查看 复制代码
AAA

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

案例:演示 AAA 指令
[Asm] 纯文本查看 复制代码
;程序名: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


AAS(ASCII Adjust for Subtraction)指令
未组合的 BCD 码减法调整指令格式如下:
[Asm] 纯文本查看 复制代码
AAS

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


案例:演示 AAS 指令
[Asm] 纯文本查看 复制代码
;程序名: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


AAM(ASCII Adjust for Multiplication)指令
未组合的 BCD 码乘法调整指令的格式如下:

[Asm] 纯文本查看 复制代码
AAM

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


案例:演示 AAM 指令
[Asm] 纯文本查看 复制代码
;程序名: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


AAD(ASCII Adjust for Division)指令
未组合的 BCD 码除法调整指令的格式如下:
[Asm] 纯文本查看 复制代码
AAD

该指令和其他调整指令的使用次序上不同,其他调整指令均安排在有关算数运算指令后,而这条指令应该安排在除法运算指令之前。它的功能是:把存放在寄存器 AH(高位十进制数)及存放在寄存器 AL 中的两位非组合 BCD 码,调整为一个二进制数,存放在寄存器 AL 中。调整的方法如下:
[Asm] 纯文本查看 复制代码
AL<-AH*10+(AL)
AH<-0

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


案例:演示 AAD 指令

[Asm] 纯文本查看 复制代码
;程序名: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 中。

[Asm] 纯文本查看 复制代码
;程序名: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。

[Asm] 纯文本查看 复制代码
;程序名: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


[Asm] 纯文本查看 复制代码
;程序名: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位。
[Asm] 纯文本查看 复制代码
;程序名: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 字节的信息区。

196043681-bd6e0774-d752-45b8-af21-224da23218fa.png


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


PSP 含有多个可用信息,其中常用信息的安排如下表:
196041545-00024247-9872-4d97-af59-ad97ad52b183.png
根据上图中的偏移,我们可以在 DOS 程序段前缀找到相应的数据。如:偏移 0 处,的十六进制是 "CD 20",这里表示的就是指令 “INT 20H”


196044208-94f26842-b436-456b-9976-315160023672.png
如上图所示,这里演示了命令行参数的偏移。


当 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 处,就能实现程序的终止。
[Asm] 纯文本查看 复制代码
;程序名: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:写一个显示命令行参数的程序
[Asm] 纯文本查看 复制代码
;程序名: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:写一个显示文本文件内容的程序。文件名作为命令行参数给出。
[Asm] 纯文本查看 复制代码
;程序名: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 键为止。
[Asm] 纯文本查看 复制代码
;程序名: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 功能。所以在DOSBOX中无法对该程序进行验证。


在 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,即不做任何处理。
[Asm] 纯文本查看 复制代码
;程序名: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\*\*”,然后就返回。


[Asm] 纯文本查看 复制代码
;程序名: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 号中断。

[Asm] 纯文本查看 复制代码
;程序名: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程序员装入内存时,初次运行的是初始化部分。初始化程序的主要功能是,对驻留部分完成必要的初始化工作;使驻留部分保留在内存中。


[Asm] 纯文本查看 复制代码
;程序名: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 键,就在屏幕的固定位置显示一字符串。

[Asm] 纯文本查看 复制代码
;程序名: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


196384295-495e6db6-3f87-40f3-9510-49d511ae26ea.png

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

MSDOS8.0原版镜像 V8.0 完全安装版:https://www.xitongzhijia.net/soft/243505.html
利用Vmware workstation安装MS-DOS:http://t.zoukankan.com/yanhua-tj-p-13996585.html

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

评分

参与人数 8威望 +1 HB +24 THX +6 收起 理由
消逝的过去 + 1
yexing + 1
sjtkxy + 1 + 1
今日下雨 + 1
acaidipan + 1 + 1 [吾爱汇编论坛52HB.COM]-学破解防破解,知进攻懂防守!
WolfKing + 1
Shark恒 + 1 + 20 + 1 [吾爱汇编论坛52HB.COM]-感谢楼主热心分享,小小评分不成敬意!
zxjzzh + 1 [吾爱汇编论坛52HB.COM]-学破解防破解,知进攻懂防守!

查看全部评分

吾爱汇编论坛-学破解,防破解!知进攻,懂防守!逆向分析,软件安全!52HB.COM
WolfKing 发表于 2022-11-11 11:41 | 显示全部楼层
吾爱汇编论坛-学破解,防破解!知进攻,懂防守!逆向分析,软件安全!52HB.COM
acaidipan 发表于 2022-11-11 22:52 | 显示全部楼层

楼主优秀!
吾爱汇编论坛-学破解,防破解!知进攻,懂防守!逆向分析,软件安全!52HB.COM
头像被屏蔽
sjtkxy 发表于 2022-12-1 05:14 | 显示全部楼层
吾爱汇编论坛-学破解,防破解!知进攻,懂防守!逆向分析,软件安全!52HB.COM
zhaocming 发表于 2023-1-9 20:39 | 显示全部楼层

感谢分享 拿走学习
吾爱汇编论坛-学破解,防破解!知进攻,懂防守!逆向分析,软件安全!52HB.COM
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

警告:本站严惩灌水回复,尊重自己从尊重他人开始!

1层
2层
3层
4层
5层

免责声明

吾爱汇编(www.52hb.com)所讨论的技术及相关工具仅限用于研究学习,皆在提高软件产品的安全性,严禁用于不良动机。任何个人、团体、组织不得将其用于非法目的,否则,一切后果自行承担。吾爱汇编不承担任何因为技术滥用所产生的连带责任。吾爱汇编内容源于网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除。如有侵权请邮件或微信与我们联系处理。

站长邮箱:SharkHeng@sina.com
站长QQ:1140549900


QQ|RSS|手机版|小黑屋|帮助|吾爱汇编 ( 京公网安备11011502005403号 , 京ICP备20003498号-6 )|网站地图

Powered by Discuz!

吾爱汇编 www.52hb.com

快速回复 返回顶部 返回列表