文章目录
目录
前言
在学习汇编语言时,在调试程序的过程中,经常需要反复使用debug中的D命令,来查看程序运行是否正确,这样做有点麻烦。目前已经学完王爽老师《汇编语言》(第四版) 第11章,掌握了一点点汇编语言编程的语法,于是决定自己动手写一个子程序,模拟实现debug中的D命令。这样就可以让程序将指定的内存地址的数据显示在屏幕上,而无需调用debug的D命令。
效果图如下。
一、实现思路
1.程序整体框架
现在的目标是:手动模拟实现debug的D命令的功能。而debug中的D命令,实现的基本功能是查看指定地址中的数据。要执行这个命令,需要为程序指定要查看的段地址、偏移地址,以及要查看的数据长度。
如何实现呢?首先,可以用ds:si指向要查看的数据的首地址,之后用循环遍历所有要查看的字节。而对于其中的每一个字节数据,在dos系统中是用十六进制显示的,如2AH。要想将这个内存中的“2AH”数据,显示为屏幕上的字符“2A”,首先就要将十六进制数据转换为对应的字符,然后再送入显存中进行显示。有一点要注意:内存中的一个字节的数据,转换为字符的话,应当是两个字符,占2个字节的空间。
根据这个思路,可以写出程序的大体框架,如下。至于具体的子程序如何实现,这是接下来才考虑的事情,现在先不管它。
assume cs:code,ds:data,ss:stack
data segment
dw 2022H,0 ;假定要显示的数据
data ends
stack segment
dw 32 dup(0) ;预留足够的栈空间
stack ends
code segment
start:
mov ax,data ;设置ds指向data段
mov ds,ax
mov ax,stack ;设置ss指向stack段
mov ss,ax
mov sp,4H
mov si,0 ;ds:si指向要显示的首地址
mov cx,3 ;cx指定显示几个字节的数据
mov dx,0302H ;dx指定屏幕显示的起始行号、列号
mov bl,02H ;bl指定字符显示的属性
call debug_D ;将指定地址、指定长度的数据显示在屏幕指定位置上
mov ax,4c00H ;程序返回
int 21H
debug_D:
;功能:将指定地址、指定长度的数据显示在屏幕指定位置上
;参数:ds:si指向要显示的数据首地址;
; cx指定要显示几个字节;
; dh指定屏幕显示的行号;
; dl指定屏幕显示的列号;
; bl指定字符显示的属性;
;返回:无
push ax ;将寄存器的值压入栈暂存
push si
push dx
debug_D_s:
mov al,ds:[si] ;取当前字节数据
call byte_to_char ;将当前字节转为2个十六进制字符形式,存储在ax中
call show_char ;将ax中的2个十六进制字符,显示在屏幕指定位置
inc si
add dl,3 ;每一个字节的数据,用2个字符及一个空格
loop debug_D_s ;取下一个字节数据
pop dx
pop si
pop ax
ret
byte_to_char: ;功能:将al中数据转为2个十六进制字符形式,存储在ax中
ret
show_char: ;将ax中的2个十六进制字符,显示在屏幕指定位置
ret
code ends
end start
接下来,逐个实现需要用到的子程序。
2.子程序1:将一个字节的数据转为16进制字符形式
下面需要实现的是 byte_to_char 这个子程序,主要功能是将al中一个字节的数据,转为2个十六进制字符,存入ax中。
一个字节的数据,以十六进制表示,就有高、低两位。如“3CH”这个数据,高为是 3,低位是 C。而要分别获取这两个十六进制位也容易做到:将x/16取整,就得到高位数据,而x%16(取余),就得到低位数据。
之后,再将这两个数字分别转为对应的字符。字符0-9对应的ASCII码是30H-39H,而字符A-F对应的ASCII码是41H-46H。
代码如下。
byte_to_char: ;功能:将al中数据转为2个十六进制字符形式,存储在ax中
;参数:al中是待转换的一个byte数据
;返回:ax中是转换后的2个十六进制字符形式数据
push bx ;将寄存器的值压入栈暂存
mov ah,0 ;先用除法获取一个字节数据的高低2个16进制位的数字
mov bl,16
div bl ;byte数据除以16,商在al中,余数在ah中
mov bh,ah ;将低位数字ah送入bh暂存
mov bl,al ;将高位数据送入bl进行转字符的处理
call hex_to_char ;将bl中数据转为一个16进制字符,存在bl中
mov ah,bl ;将处理好的高位字符转入ah
mov bl,bh ;将低位数据送入bl进行转字符的处理
call hex_to_char ;将bl中数据转为一个16进制字符,存在bl中
mov al,bl ;将处理好的低位字符转入al
pop bx
ret
hex_to_char: ;功能:将bl中<16的数据转为一个16进制字符,存入bl中
;参数: bl中的待转为字符的数据,数据<16
;返回:bl中存放转换后的一个16进制字符
cmp bl,0AH ;比较商与10的大小,判断是否小于10
jb smallchar ;如果是<10的数字,就转为对应的0-9字符
add bl,37H ;如果不是,那就转为对应的A-F字符
jmp to_char_ok
smallchar: add bl,30H ;将数字转为对应的0-9字符
to_char_ok:
ret
3.子程序2:将2个16进制字符显示在屏幕指定位置
现在,内存中的一个字节的数据,已经转为了2个16进制字符形式,存放在ax中。接下来要实现的是 show_char 子程序,功能就是将ax中存放的2个十六进制字符,显示在屏幕的指定位置。
代码如下。
show_char: ;功能:将ax中的2个十六进制字符,显示在屏幕指定位置
;参数:ax中存放2个十六进制字符
; dh指定屏幕显示的起始行号;
; dl指定屏幕显示的起始列号
; bl指定字符显示的属性;
;返回:无
push ax ;将寄存器的值压入栈暂存
push es
push bx
push cx
push di
push bp
push dx
mov cx,ax ;用cx暂存要显示的字符数据ax
mov bp,bx ;用bp暂存bx中的字符显示属性
mov ax,0B800H ;设置es指向显存段
mov es,ax
mov bx,00A0H ;计算屏幕显示的首地址
mov ax,0
mov al,dh
mul bl
mov dh,0
add ax,dx
add ax,dx
mov di,ax ;di指向屏幕显示的首地址
mov bx,bp ;从bp中取出字符属性字节
mov ax,cx ;从cx中取出待显示的字符ax
mov es:[di],ah ;将高位字符送入显存
inc di
mov es:[di],bl ;将字符属性字节送入显存
inc di
mov es:[di],al ;将低位字符送入显存
inc di
mov es:[di],bl ;将字符属性字节送入显存
pop dx
pop bp
pop di
pop cx
pop bx
pop es
pop ax
ret
以上,就实现了在指定地址、屏幕指定位置,显示指定长度的内存数据 的功能。
二、功能优化
1.在行首显示内存地址
以上功能可以显示指定地址的数据,但是还可以进一步优化。
平时使用debug的D命令时,每行的行首会显示当前行的首地址,这样看起来比较方便。
于是就考虑写一个子程序,用于在显示内存数据前,先显示一下段地址和偏移地址。那么就需要将段地址和偏移地址,分别地转成字符,然后再送入显存进行显示。因为转字符、显示字符这两个子程序之前已经写好了,这里可以直接在程序中调用。
子程序代码如下。
show_addr: ;功能:将段地址:偏移地址显示在屏幕指定行的行首
;参数:ds是要显示的段地址
; si是要显示的偏移地址
; dh是指定的屏幕显示行号
; dl是指定的屏幕显示列号
; bl是指定的字符显示属性
;返回:dl是继续显示其它内容的起始列号
push ax ;将寄存器的值压入栈暂存
mov ax,ds ;取ds的高16位
mov al,ah
call byte_to_char ;将al字节转为2个十六进制字符形式,存储在ax中
call show_char ;将ax中的2个十六进制字符,显示在屏幕指定位置
add dl,2
mov ax,ds ;取ds的低16位
call byte_to_char ;将al字节转为2个十六进制字符形式,存储在ax中
call show_char ;将ax中的2个十六进制字符,显示在屏幕指定位置
add dl,2
mov ax,3A00H ;显示一个冒号,冒号的ASCII码是3AH
call show_char
add dl,2
mov ax,si ;取si的高16位
mov al,ah
call byte_to_char ;将al字节转为2个十六进制字符形式,存储在ax中
call show_char ;将ax中的2个十六进制字符,显示在屏幕指定位置
add dl,2
mov ax,si ;取si的低16位
call byte_to_char ;将al字节转为2个十六进制字符形式,存储在ax中
call show_char ;将ax中的2个十六进制字符,显示在屏幕指定位置
add dl,2
add dl,2 ;多显示两个空格
pop ax
ret
然后把显示一行数据(包括行首地址和后边的一行数据)的功能封装为一个函数。
代码如下。
show_line: ;功能:在屏幕指定位置,显示指定地址的一行内存数据
;参数:ds:si指向要显示的数据首地址;
; cx指定这一行要显示几个字节;
; dh指定屏幕显示的行号;
; dl指定屏幕显示的列号;
; bl指定字符显示的属性;
;返回:无
push ax ;将寄存器的值压入栈暂存
push si
push dx
call show_addr ;显示行首地址
debug_D_s: ;显示内存数据
mov al,ds:[si] ;取当前字节数据
call byte_to_char ;将当前字节转为2个十六进制字符形式,存储在ax中
call show_char ;将ax中的2个十六进制字符,显示在屏幕指定位置
inc si
add dl,3 ;每一个字节的数据,用2个字符及一个空格
loop debug_D_s ;取下一个字节数据
pop dx
pop si
pop ax
ret
2.显示多行数据
以上程序,只能将数据显示在一行中。而debug的D命令,是将数据分行显示的,每行只显示16个字节的数据,便于查看。于是考虑对程序进行优化,实现分行显示的功能。
代码如下。
debug_D:
;功能:将指定地址、指定长度的数据显示在屏幕指定位置上
;参数:ds:si指向要显示的数据首地址;
; cx指定要显示几个字节,<200;
; dh指定屏幕显示的行号;
; dl指定屏幕显示的列号;
; bl指定字符显示的属性;
; bh指定每行显示几个字节的数据
;返回:无
push cx ;将寄存器的值压入栈暂存
push ax
push si
push bx
mov ax,cx
div bh ;商在al中,余数在ah中
mov cx,0
mov cl,al
cmp al,0 ;判断商是否为0
je show_yushu ;如果为0,就去继续判断余数是否为0
s_show_line: ;如果商不为0,那么循环显示每一行的内容
push cx
mov cx,0
mov cl,bh ;每行显示几个内存数据
call show_line ;显示一行的内容
inc dh ;将行号对应增加
mov cx,0 ;将si内存偏移地址对应增加
mov cl,bh
add si,cx
pop cx
loop s_show_line ;下一行
show_yushu:
cmp ah,0 ;判断余数是否为0
je debug_D_ok ;如果为0,那么显示完毕,程序返回
mov cx,0 ;如果不为0,那么就再显示一行
mov cl,ah
call show_line
debug_D_ok:
pop bx
pop si
pop ax
pop cx
ret
3.封装一个默认函数
可以看到,上边这个程序,可以实现既定功能,但是需要指定的参数比较多,使用起来不方便。于是考虑参考java中函数的设计思路,写一个具有默认参数的子程序,把这个功能进一步封装起来。
代码如下。
debug_D_default: ;功能:将指定地址、指定长度的数据显示在屏幕上(参数默认)
;参数:ds:si指向要显示的数据首地址;
; cx指定显示几个字节的数据
;返回:无
push dx
push bx
mov dx,0405H ;dx指定屏幕显示的起始行号、列号
mov bl,02H ;bl指定字符显示的属性
mov bh,08H ;bh指定一行显示几个字节的数据
call debug_D ;将指定地址、指定长度的数据显示在屏幕指定位置上
pop bx
pop dx
ret
三、完整代码
assume cs:code,ds:data,ss:stack
data segment
db '1234567890abcdef' ;假定要显示的数据
data ends
stack segment
dw 32 dup(0) ;预留足够的栈空间
stack ends
code segment
start:
mov ax,data ;设置ds指向data段
mov ds,ax
mov ax,stack ;设置ss指向stack段
mov ss,ax
mov sp,4H
mov si,0 ;ds:si指向要显示的首地址
mov cx,20H ;cx指定显示几个字节的数据
call debug_D_default
mov ax,4c00H
int 21H
debug_D_default: ;功能:将指定地址、指定长度的数据显示在屏幕上(参数默认)
;参数:ds:si指向要显示的数据首地址;
; cx指定显示几个字节的数据
;返回:无
push dx
push bx
mov dx,0405H ;dx指定屏幕显示的起始行号、列号
mov bl,02H ;bl指定字符显示的属性
mov bh,08H ;bh指定一行显示几个字节的数据
call debug_D ;将指定地址、指定长度的数据显示在屏幕指定位置上
pop bx
pop dx
ret
debug_D:
;功能:将指定地址、指定长度的数据显示在屏幕指定位置上
;参数:ds:si指向要显示的数据首地址;
; cx指定要显示几个字节,<200;
; dh指定屏幕显示的行号;
; dl指定屏幕显示的列号;
; bl指定字符显示的属性;
; bh指定每行显示几个字节的数据
;返回:无
push cx ;将寄存器的值压入栈暂存
push ax
push si
push bx
mov ax,cx
div bh ;商在al中,余数在ah中
mov cx,0
mov cl,al
cmp al,0 ;判断商是否为0
je show_yushu ;如果为0,就去继续判断余数是否为0
s_show_line: ;如果商不为0,那么循环显示每一行的内容
push cx
mov cx,0
mov cl,bh ;每行显示几个内存数据
call show_line ;显示一行的内容
inc dh ;将行号对应增加
mov cx,0 ;将si内存偏移地址对应增加
mov cl,bh
add si,cx
pop cx
loop s_show_line ;下一行
show_yushu:
cmp ah,0 ;判断余数是否为0
je debug_D_ok ;如果为0,那么显示完毕,程序返回
mov cx,0 ;如果不为0,那么就再显示一行
mov cl,ah
call show_line
debug_D_ok:
pop bx
pop si
pop ax
pop cx
ret
show_line: ;功能:在屏幕指定位置,显示指定地址的一行内存数据
;参数:ds:si指向要显示的数据首地址;
; cx指定这一行要显示几个字节;
; dh指定屏幕显示的行号;
; dl指定屏幕显示的列号;
; bl指定字符显示的属性;
;返回:无
push ax ;将寄存器的值压入栈暂存
push si
push dx
call show_addr ;显示行首地址
debug_D_s: ;显示内存数据
mov al,ds:[si] ;取当前字节数据
call byte_to_char ;将当前字节转为2个十六进制字符形式,存储在ax中
call show_char ;将ax中的2个十六进制字符,显示在屏幕指定位置
inc si
add dl,3 ;每一个字节的数据,用2个字符及一个空格
loop debug_D_s ;取下一个字节数据
pop dx
pop si
pop ax
ret
show_addr: ;功能:将段地址:偏移地址显示在屏幕指定行的行首
;参数:ds是要显示的段地址
; si是要显示的偏移地址
; dh是指定的屏幕显示行号
; dl是指定的屏幕显示列号
; bl是指定的字符显示属性
;返回:dl是继续显示其它内容的起始列号
push ax ;将寄存器的值压入栈暂存
mov ax,ds ;取ds的高16位
mov al,ah
call byte_to_char ;将al字节转为2个十六进制字符形式,存储在ax中
call show_char ;将ax中的2个十六进制字符,显示在屏幕指定位置
add dl,2
mov ax,ds ;取ds的低16位
call byte_to_char ;将al字节转为2个十六进制字符形式,存储在ax中
call show_char ;将ax中的2个十六进制字符,显示在屏幕指定位置
add dl,2
mov ax,3A00H ;显示一个冒号,冒号的ASCII码是3AH
call show_char
add dl,2
mov ax,si ;取si的高16位
mov al,ah
call byte_to_char ;将al字节转为2个十六进制字符形式,存储在ax中
call show_char ;将ax中的2个十六进制字符,显示在屏幕指定位置
add dl,2
mov ax,si ;取si的低16位
call byte_to_char ;将al字节转为2个十六进制字符形式,存储在ax中
call show_char ;将ax中的2个十六进制字符,显示在屏幕指定位置
add dl,2
add dl,2 ;多显示两个空格
pop ax
ret
byte_to_char: ;功能:将al中数据转为2个十六进制字符形式,存储在ax中
;参数:al中是待转换的一个byte数据
;返回:ax中是转换后的2个十六进制字符形式数据
push bx ;将寄存器的值压入栈暂存
mov ah,0 ;先用除法获取一个字节数据的高低2个16进制位的数字
mov bl,16
div bl ;byte数据除以16,商在al中,余数在ah中
mov bh,ah ;将低位数字ah送入bh暂存
mov bl,al ;将高位数据送入bl进行转字符的处理
call hex_to_char ;将bl中数据转为一个16进制字符,存在bl中
mov ah,bl ;将处理好的高位字符转入ah
mov bl,bh ;将低位数据送入bl进行转字符的处理
call hex_to_char ;将bl中数据转为一个16进制字符,存在bl中
mov al,bl ;将处理好的低位字符转入al
pop bx
ret
hex_to_char: ;功能:将bl中<16的数据转为一个16进制字符,存入bl中
;参数: bl中的待转为字符的数据,数据<16
;返回:bl中存放转换后的一个16进制字符
cmp bl,0AH ;比较商与10的大小,判断是否小于10
jb smallchar ;如果是<10的数字,就转为对应的0-9字符
add bl,37H ;如果不是,那就转为对应的A-F字符
jmp to_char_ok
smallchar: add bl,30H ;将数字转为对应的0-9字符
to_char_ok:
ret
show_char: ;功能:将ax中的2个十六进制字符,显示在屏幕指定位置
;参数:ax中存放2个十六进制字符
; dh指定屏幕显示的起始行号;
; dl指定屏幕显示的起始列号
; bl指定字符显示的属性;
;返回:无
push ax ;将寄存器的值压入栈暂存
push es
push bx
push cx
push di
push bp
push dx
mov cx,ax ;用cx暂存要显示的字符数据ax
mov bp,bx ;用bp暂存bx中的字符显示属性
mov ax,0B800H ;设置es指向显存段
mov es,ax
mov bx,00A0H ;计算屏幕显示的首地址
mov ax,0
mov al,dh
mul bl
mov dh,0
add ax,dx
add ax,dx
mov di,ax ;di指向屏幕显示的首地址
mov bx,bp ;从bp中取出字符属性字节
mov ax,cx ;从cx中取出待显示的字符ax
mov es:[di],ah ;将高位字符送入显存
inc di
mov es:[di],bl ;将字符属性字节送入显存
inc di
mov es:[di],al ;将低位字符送入显存
inc di
mov es:[di],bl ;将字符属性字节送入显存
pop dx
pop bp
pop di
pop cx
pop bx
pop es
pop ax
ret
code ends
end start
总结
本文介绍了如何手动模拟debug中的D命令,将指定地址、指定长度、指定颜色的内存数据显示在屏幕的指定位置上。通过这个练习,进一步熟悉了汇编语言编程的方法,提高了编程能力。写好的子程序,以后可以随时调用,还可以自定义位置、长度、颜色等参数,很方便。