文章目录
前言
本文是王爽老师《汇编语言》(第四版) 第十七章第三节 “字符串的输入” 的分析及代码,主要任务是输入并显示一个字符串。书上已经给出了讲解,不过我认为需要自己梳理一遍思路。代码给出了详细注释,并且实现逻辑在细节处也与书上的略有不同。
一、实验任务
总体目标:读取用户输入的一个字符串,并实时显示在屏幕指定位置,以回车键结束。
二、思路分析
首先,将任务分解,这个程序要实现的逻辑如下。
(1)从16H端口不断读取用户的键盘输入
(2)有一块空间用来存放当前字符串,并且以栈的方式管理这块空间。
选择栈的原因是,当用户输入一个新字符,新字符应当加在当前字符串的末尾;当用户按退格键,应当从当前字符串的末尾删除一个字符;这是明显的先进后出原则,因此使用栈的方式进行管理。
注意,这里所说的管理,是要手动设置栈顶的变动,而不是用系统提供的SP寄存器。
(3)若用户输入一个新字符,则 ①在字符串栈的末尾添加这个字符;②在屏幕指定位置显示整个字符串。
(4)若用户按退格键,则 判断字符串栈是否为空。
A:若字符串栈空,则直接返回;
B:若字符串栈不空,则 ① 将字符串栈最末尾的一个字符删除;②在屏幕指定位置显示整个字符串。
三、程序框架
由上边的分析,可以列出基本的程序框架。字符串的插入、删除、显示功能,应分别封装为子程序。而这几个功能都是与字符串有关的,之前学过了,可以在一个子程序中设计多个功能,这里正好用上。于是,设计一个charstack子程序,提供字符入栈、字符出栈、显示字符串的功能。
以下是程序的框架。代码很少,主要是思路。
assume cs:code ;17.3 输入并显示一个字符串
stack segment
db 32 dup(0) ;通用的栈空间
stack ends
data segment
db 16 dup(0) ;用作字符栈的空间
data ends
code segment
start:
mov ax,stack ;设置栈顶
mov ss,ax
mov sp,20H
mov ax,data ;参数:ds:si指向字符栈首地址
mov ds,ax
mov si,0
mov dx,0402H ;参数:字符串的屏幕显示行号、列号
call getstr ;调用 “接收并显示一个字符串”子程序
mov ax,4c00H
int 21H
getstr: ;功能:接收一个字符串的输入,并显示在屏幕指定位置
;参数:dh 屏幕显示的行号; dl 屏幕显示的起始列号 ds:si指向字符串栈首地址
;返回:无
;读取用户的键盘输入
mov ah,0 ;16H中断例程,功能号0:从键盘缓冲区中读取一个键盘输入
int 16H ;返回:ah 扫描码;al ASCII码
;判断输入的内容
;若输入的是字符
ischar:
mov ah,0 ;功能号0:将新字符入栈
call charstack
mov ah,2 ;功能号2:在指定位置显示字符串
call charstack
jmp short getstr_start ;再次等待读取字符
;若输入的是退格键
backspace:
mov ah,1 ;功能号1:将末尾的一个字符删除
call charstack
mov ah,2 ;功能号2:在指定位置显示字符串
call charstack
jmp short getstr_start ;再次等待读取字符
;若输入的是回车键
enter:
mov ah,0 ;功能号0:将新字符入栈
mov al,0 ;参数:要入栈的新字符为0
call charstack
mov ah,2 ;功能号2:在指定位置显示字符串
call charstack
jmp short getstr_ret ;子程序返回
getstr_ret: ;子程序返回
pop ax
ret
charstack: ;功能:以栈的方式管理一个字符串空间,提供新增字符、删除字符、显示字符串功能。
;参数说明:ah 功能号
; 功能号0:字符入栈
; 参数: al入栈字符 ds:si指向字符串栈
; 返回:无
; 功能号1:字符出栈
; 参数:ds:si指向字符串栈
; 返回: al出栈字符
; 功能号2:显示字符串
; 参数:dh屏幕显示行号 dl屏幕显示起始列号 ds:si指向字符串栈
; 返回:无
jmp short charstack_start ;跳转到charstack子程序入口
table dw charpush,charpop,charshow ;子功能地址表
top dw 0 ;字符栈的栈顶
charstack_start: ;程序入口
;根据参数ah功能号,得到table中对应子功能的地址
charpush: ;功能号0:字符入栈
;参数:al入栈字符 ds:si指向字符串栈
;返回:无
;字符入栈代码
jmp charstack_ret ;子程序返回
charpop: ;功能号1:字符出栈
;参数:ds:si指向字符串栈
;返回:al出栈字符
;字符出栈代码
jmp charstack_ret ;子程序返回
charshow: ;功能号2:显示字符串
;参数:dh屏幕显示行号 dl屏幕显示起始列号 ds:si指向字符串栈
;返回:无
;显示字符串代码
jmp charstack_ret ;子程序返回
charstack_ret: ;子程序返回
ret
code ends
end start
四、一些细节
有一些细节处的实现逻辑,我与老师书上讲的略有不同,这里做个记录。
1.判断键盘输入的内容
书上的实现思路是:①判断是否是字符,若是则立即处理(入栈并显示字符串);②若不是字符,则跳转到非字符判断部分,再接着判断是否是退格键、是否是回车键。主要代码如下。
jmp getstrs ;接收字符串输入
mov ah,0 ;接收输入的一个字符
int 16H
;下面开始判断
cmp al,20H ;若ASCII码<20H,则不是字符
jb nochar
;接下来就处理“输入的是字符”这种情况
mov ah,0 ;功能号0:字符入栈
call charstack
mov ah,2 ;功能号2:显示字符串
call charstack
jmp getstrs ;再次等待输入
;这部分用于处理输入的是非字符的情况
nochar:
cmp ah,0EH
je backspace ;处理“输入的是退格键”的情况
cmp ah,1CH
je enter ;处理“输入的是回车键”的情况
jmp getstrs ;再次等待输入
backspace: ;处理“输入的是退格键”的情况
enter: ;处理“输入的是回车键”的情况
我的思路是:字符与非字符同等对待,以类似java中case语句的方式逐个判断。个人认为这样似乎结构更清晰一些。主要代码如下。
;读取用户的键盘输入
getstr_start:
mov ah,0
int 16H
;判断输入的内容
cmp al,20H
jnb ischar ;若ASCII码>=20H,则输入的是字符
cmp ah,0EH
je backspace ;若扫描码为0EH,则输入的是退格键
cmp ah,1CH
je enter ;若扫描码为1CH,则输入的是回车键
jmp short getstr_ret ;若以上都不是,则直接返回
ischar: ;处理 输入的是字符 的情况
jmp short getstr_start ;再次等待读取字符
backspace: ;处理 输入的是退格键 的情况
jmp short getstr_start ;再次等待读取字符
enter: ;处理 输入的是回车键 的情况
jmp short getstr_ret ;子程序返回
getstr_ret: ;子程序返回
ret
2.依据功能号找到子程序中对应子功能
老师在书上写的方法是,在程序中设置一个table表用于存放每个子功能的入口地址,然后用一段指令来根据功能号参数计算出相应的子功能入口地址,然后跳转。主要代码如下。
jmp short charstart ;跳转到程序入口处
table dw charpush,charpop,charshow ;子功能入口地址表
charstart: ;程序入口处
;下面计算出子功能的地址
cmp ah,2 ;提高程序容错性,若输入的功能号>2则忽略,直接返回
ja sret
mov bl,ah
mov bh,0
add bx,bx
jmp word ptr table[bx] ;跳转到对应的子功能入口处
老师在第16章讲过,这样的编程方法使得程序结构比较混乱,不利于功能的扩充。(我怀疑可能是老师故意在17章这样写,让读者自己进行优化,加深理解。)
更好的方法是根据功能号直接查找地址表。于是我将代码优化如下。
jmp short charstack_start ;跳转到charstack子程序入口
top dw 0 ;字符栈的栈顶
charstack_start: ;程序入口
;根据参数ah功能号,执行对应子功能
cmp ah,0
je do1 ;功能号0:字符入栈
cmp ah,1
je do2 ;功能号1:字符出栈
cmp ah,2
je do3 ;功能号2:显示字符串
jmp short charstack_ret ;若功能号输入有误,则子程序返回
do1: call charpush ;功能号0:字符入栈
jmp short charstack_ret
do2: call charpop ;功能号1:字符出栈
jmp short charstack_ret
do3: call charshow ;功能号2:显示字符串
jmp short charstack_ret
charpush: ;功能号0:字符入栈
ret ;子程序返回
charpop: ;功能号1:字符出栈
ret ;子程序返回
charshow: ;功能号2:显示字符串
ret ;子程序返回
charstack_ret: ;子程序返回
ret
3.入栈与出栈操作
汇编语言中的push与pop指令在第三章讲过。入栈push操作中,CPU会先修改栈顶SP的值,然后再复制数据。而这个程序中,要手动实现对字符栈的管理,其中入栈操作,书上给的代码是先复制数据,然后再修改栈顶。这让我读来略有不适。因此我在自己的程序中修改了语句的顺序,使之与push指令中的操作顺序一致。出栈操作同理。主要代码如下。
charpush: ;功能号0:字符入栈
;参数:al入栈字符 ds:si指向字符串栈 top为字符栈的栈顶位置
;返回:top为字符栈的栈顶位置
push bx
mov bx,top ;bx指向当前栈顶的偏移地址
inc top ;栈顶+1
mov ds:[si+bx],al ;将新字符加入到栈顶位置
pop bx
ret ;子程序返回
charpop: ;功能号1:字符出栈
;参数:ds:si指向字符串栈 top为字符栈的栈顶位置
;返回:al出栈字符 top为字符栈的栈顶位置
push bx
mov bx,top ;bx指向当前栈顶的偏移地址
mov al,ds:[si+bx-1] ;将栈顶字符赋给al作为返回值
dec top ;栈顶-1,即将最末尾一个字符出栈
pop bx
ret ;子程序返回
4.出栈时栈顶溢出的问题
书上之前讲过,在使用与栈有关的指令时需要自己考虑栈顶溢出的问题。这个程序中需要手动实现对字符栈的管理,因此更需要自己考虑这个问题。
先来看出栈操作。
出栈操作,可能出现的情况是栈已经空了。如果字符栈已经空了,而用户又输入了退格键,这时候应当直接忽略而读取下一个字符,绝不能继续修改栈顶(这会导致错误)。书上的代码中没有考虑这个问题,于是我进行了优化。因此,将出栈操作的代码修改如下。
charpop: ;功能号1:字符出栈
;参数:ds:si指向字符串栈 top为字符栈的栈顶位置
;返回:al出栈字符 top为字符栈的栈顶位置
;先判断栈是否为空,若为空,则直接返回
cmp top,0
jne charpop_do
ret
;若栈不为空,则进行出栈操作
charpop_do:
push bx
mov bx,top ;bx指向当前栈顶的偏移地址
mov al,ds:[si+bx-1] ;将栈顶字符赋给al作为返回值
dec top ;栈顶-1,即将最末尾一个字符出栈
pop bx
ret ;子程序返回
5.入栈时栈顶溢出的问题
再来看入栈操作。
入栈操作,可能出现的情况是栈空间已经满了。但是目前的情况是,字符栈的大小不是在子程序中定义的,而是在调用程序中定义的(比如这里就是在主程序中将data段定义为字符栈的空间)。因此,子程序无法知道这个字符栈的空间有多大(调用子程序时并没有传递关于栈空间大小的参数),因而也就无法处理入栈时发生栈满溢出的情况。
而作为一个实现字符栈出栈、入栈管理功能的子程序,是应当提供对于栈满这种情况的处理功能的。书上也没有这方面的处理,于是只能自己动手优化了。
优化的方法是,在调用子程序的时候,将字符栈的栈空间大小作为参数(cx)传递给子程序getstr。程序在入栈时先判断是否栈满,若栈已满,则不继续入栈,直接返回。
入栈的主要代码如下。
charpush:
;先判断栈是为已满,若已满,则直接返回
cmp top,cx
jne charpush_do
ret
;若栈未满,则进行入栈操作
charpush_do:
push bx
mov bx,top ;bx指向当前栈顶的偏移地址
inc top ;栈顶+1
mov ds:[si+bx],al ;将新字符加入到栈顶位置
pop bx
ret ;子程序返回
而主程序中在调用时也要记得用cx传递参数,指明字符栈的栈空间大小。
二、最终成果
1.完整代码
assume cs:code ;17.3 输入并显示一个字符串
stack segment
db 32 dup(0) ;通用的栈空间
stack ends
data segment
db 16 dup(0) ;用作字符栈的空间
data ends
code segment
start:
mov ax,stack ;设置栈顶
mov ss,ax
mov sp,20H
mov ax,data ;参数:ds:si指向字符栈首地址
mov ds,ax
mov si,0
mov dx,0503H ;参数:字符串的屏幕显示行号、列号
mov cx,16 ;参数:字符栈的栈空间大小
call getstr ;调用 “接收并显示一个字符串”子程序
mov ax,4c00H
int 21H
getstr: ;功能:接收一个字符串的输入,并显示在屏幕指定位置
;参数:dh 屏幕显示的行号; dl 屏幕显示的起始列号
; ds:si指向字符串栈首地址 cx 字符栈的空间大小
;返回:无
push ax
;读取用户的键盘输入
getstr_start:
mov ah,0 ;16H中断例程,功能号0:从键盘缓冲区中读取一个键盘输入
int 16H ;返回:ah 扫描码;al ASCII码
;判断输入的内容
cmp al,20H
jnb ischar ;若ASCII码>=20H,则输入的是字符
cmp ah,0EH
je backspace ;若扫描码为0EH,则输入的是退格键
cmp ah,1CH
je enter ;若扫描码为1CH,则输入的是回车键
jmp short getstr_ret ;若以上都不是,则直接返回
;若输入的是字符
ischar:
mov ah,0 ;功能号0:将新字符入栈
call charstack
mov ah,2 ;功能号2:在指定位置显示字符串
call charstack
jmp short getstr_start ;再次等待读取字符
;若输入的是退格键
backspace:
mov ah,1 ;功能号1:字符出栈
call charstack
mov ah,2 ;功能号2:在指定位置显示字符串
call charstack
jmp short getstr_start ;再次等待读取字符
;若输入的是回车键
enter:
mov ah,0 ;功能号0:将新字符入栈
mov al,0 ;参数:要入栈的新字符为0
call charstack
mov ah,2 ;功能号2:在指定位置显示字符串
call charstack
jmp short getstr_ret ;子程序返回
getstr_ret: ;子程序返回
pop ax
ret
charstack: ;功能:以栈的方式管理一个字符串空间,提供新增字符、删除字符、显示字符串功能。
;参数说明:ah 功能号
; 功能号0:字符入栈
; 参数: al入栈字符 ds:si指向字符串栈 cx 字符栈的栈空间大小
; 返回:无
; 功能号1:字符出栈
; 参数:ds:si指向字符串栈
; 返回: al出栈字符
; 功能号2:显示字符串
; 参数:dh屏幕显示行号 dl屏幕显示起始列号 ds:si指向字符串栈
; 返回:无
jmp short charstack_start ;跳转到charstack子程序入口
top dw 0 ;字符栈的栈顶
charstack_start: ;程序入口
;根据参数ah功能号,得到table中对应子功能的地址
cmp ah,0
je do1 ;功能号0:字符入栈
cmp ah,1
je do2 ;功能号1:字符出栈
cmp ah,2
je do3 ;功能号2:显示字符串
jmp short charstack_ret ;若功能号输入有误,则子程序返回
do1: call charpush ;功能号0:字符入栈
jmp short charstack_ret
do2: call charpop ;功能号1:字符出栈
jmp short charstack_ret
do3: call charshow ;功能号2:显示字符串
jmp short charstack_ret
charstack_ret: ;子程序返回
ret
;--------------------------------------
charpush: ;功能号0:字符入栈
;参数:al入栈字符 ds:si指向字符串栈
; cx 字符栈的栈空间大小 top为字符栈的栈顶位置
;返回:top为字符栈的栈顶位置
;先判断栈是为已满,若已满,则直接返回
cmp top,cx
jne charpush_do
ret
;若栈未满,则进行入栈操作
charpush_do:
push bx
mov bx,top ;bx指向当前栈顶的偏移地址
inc top ;栈顶+1
mov ds:[si+bx],al ;将新字符加入到栈顶位置
pop bx
ret ;子程序返回
;-------------------------------------------
charpop: ;功能号1:字符出栈
;参数:ds:si指向字符串栈 top为字符栈的栈顶位置
;返回:al出栈字符 top为字符栈的栈顶位置
;先判断栈是否为空,若为空,则直接返回
cmp top,0
jne charpop_do
ret
;若栈不为空,则进行出栈操作
charpop_do:
push bx
mov bx,top ;bx指向当前栈顶的偏移地址
mov al,ds:[si+bx-1] ;将栈顶字符赋给al作为返回值
dec top ;栈顶-1,即将最末尾一个字符出栈
pop bx
ret ;子程序返回
;-----------------------------------------
charshow: ;功能号2:显示字符串
;参数:dh屏幕显示行号 dl屏幕显示起始列号 ds:si指向字符串栈
;返回:无
push ax
push es
push di
push dx
push si
;计算,es:di指向显示的起始位置
mov ax,0B800H
mov es,ax
mov ax,160
mul dh
mov dh,0
add ax,dx
add ax,dx
mov di,ax
;判断字符栈是否为空,若为空,则跳转
cmp top,0
je charshow_ret ;跳转
;显示字符栈中的全部字符
mov ax,0
cli ;传送方向为正向传送
s_charshow:
cmp ax,top
je charshow_ret ;若已经显示完最后一个字符,则跳转
movsb ;传送一个字符进行显示
inc di ;di为显存区偏移地址
inc ax ;ax为字符栈的索引
jmp s_charshow
charshow_ret:
mov byte ptr es:[di],0 ;将屏幕上当前字符(字符串结尾的下一个字符)清空
pop si
pop dx
pop di
pop es
pop ax
ret ;子程序返回
code ends
end start
2.效果图
总结
本文是王爽老师《汇编语言》(第四版) 第十七章第三节 输入并显示一个字符串 的分析及代码。通过这个练习,初步熟悉了对int 16H中断例程的使用,提高了设计包含多个功能的子程序的能力。