文章目录
前言
之前我写过一篇博文 《汇编语言 自己动手实现debug的D命令》 ,记录了如何从零开始手动实现Debug的D命令(当然是基础版本)。现在学完王爽老师《汇编语言》(第四版)第16章,了解了列表法的编程技巧,我意识到之前的程序还可以再优化。于是本文在之前的思路基础上,再次手动实现Debug的D命令。
一、思路分析
这次采用由宏观到微观的思路,先写出整体的程序框架,然后再细化实现每一个子程序。
1.程序框架
以下写好了每个子程序的功能、参数、返回,基本的程序框架已经明了。
assume cs:code ;实现debug的D命令(第2版)
data segment
db 20H,22H,11H,09H ;假定要显示的数据
data ends
stack segment
dw 32 dup(0) ;预留足够的栈空间
stack ends
code segment
start:
mov ax,stack ;设置栈顶
mov ss,ax
mov sp,40H
mov ax,data ;设置ds:si指向要显示的内容首地址
mov ds,ax
mov si,0
mov cx,20H ;指定要显示几个字节的数据
call debug_D2 ;调用D命令,显示指定地址、指定长度的内容
mov ax,4c00H
int 21H
debug_D2: ;功能:显示指定地址、指定长度的内容
;参数:ds:si指向要显示的内容首地址
;返回:无
;指定默认的显示参数
;调用接收自定义显示参数的子程序
call debug_D2_arrs
ret
debug_D2_arrs: ;功能:在屏幕指定位置,显示指定地址、指定长度的内容
;参数:ds:si指向要显示的内容首地址
; cx指定要显示几个字节的数据
; dx指定屏幕显示的起始行号、列号
; bl指定字符的显示属性
; bh指定每行显示几个字节内容的对应字符
;返回:无
;根据cx与bh计算共需要显示几行
s_debug_D2:
call showLine ;显示当前行
add si,dh ;首地址指向下一行
loop s_debug_D2
ret
showLine: ;功能:显示一行内容
;参数:ds:si指向要显示的内容的首地址
; dx指定当前行显示的行号、起始列号
; bl指定字符的显示属性
; bh指定每行显示几个字节内容的对应字符
;返回:无
;显示行首的段地址:偏移地址
call showAddr
;显示bh个字节的数据内容
mov cl,bh
s_showLine:
call showByte ;显示两个16进制位对应的字符
call showChar ;显示一个空格作为分隔符
;下一行
loop s_showLine
;若bh>8,就在第8个字节的数据后边加一个“-”,方便查看
call showMid
ret
showAddr: ;功能:在屏幕指定位置显示当前行的段地址:偏移地址
;参数:ds:si指向要显示的内容的首地址
; dx指定当前行显示的行号、起始列号
; bl指定字符的显示属性
;返回:无
;显示ds指向的段地址
call showByte
;显示一个冒号
call showChar
;显示si指向的偏移地址
call showByte
;显示一个空格
showChar
ret
showByte: ;功能:在屏幕指定位置显示一个字节的字符形式
;参数:ds:si指向当前要显示的内容的地址
; dx指向当前行号、列号
; bl指定字符的显示属性
;返回:无
;查表得当前数据的ASCII码
call showChar ;在屏幕上显示出来
ret
showChar: ;功能:在屏幕指定位置显示一个ASCII码字符
;参数:al指定要显示的ASCII码
; dx指向当前行号、列号
; bl指定字符的显示属性
;返回:无
ret
code ends
end start
2.编程方法小结
其中运用到的一些值得整理的编程方法,这里单独记录一下。
(1)从底层功能开始
在实现功能过程中,个人的习惯是从最底层的子程序开始写。每写完一个子程序,就写一个对应的test程序进行测试,测试通过了再向上封装。这样可以避免在写完一个庞大的项目后发现有bug,这时要找到它就要花一番功夫了。
(2)执行时跳过数据部分
有一些子程序中会在开头定义一段数据,而在执行时,这段数据并不希望被当做指令执行,那么在子程序开头一定要记得写上“jmp short s”指令,否则程序会出现奇怪的错误。
比如下面这个子程序。
showByte: ;功能:在屏幕指定位置显示一个字节的字符形式
;参数:ds:si指向当前要显示的内容的地址
; dx指向当前行号、列号
; bl指定字符的显示属性
;返回:无
jmp short showByte_start ;*******这句不要忘记!
charTable db '0123456789ABCDEF' ;十六进制字符表
showByte_start:
;执行代码
ret
(3)查表法
这一章重点学习了查表法的编程技巧,这次编程也用上了。这里举一个例子,就是这个功能为“显示一个字节的十六进制字符形式”的子程序,其中就要将内存中一个字节的数据转为2个0-F对应的字符。在前一篇博文中我采用的是判断给定数据是否大于10,然后分类处理的方法。这次使用查表法,可以建立统一的映射,(虽然占用了一点点内存空间)使得代码清晰明了,也不容易出错。
下面是这个子程序的代码。
showByte: ;功能:在屏幕指定位置显示一个字节的字符形式
;参数:ds:si指向当前要显示的内容的地址
; dx指向当前行号、列号
; bl指定字符的显示属性
;返回:无
jmp short showByte_start
charTable db '0123456789ABCDEF' ;十六进制字符表
showByte_start:
push ax
push cx
push dx
;查表得当前数据的ASCII码
mov al,ds:[si] ;取当前字节数据
mov ah,al
mov cl,4 ;取高4位存放到ah中
shr ah,cl
push bx ;查表得高4位的ASCII码
mov bh,0
mov bl,ah
mov ah,charTable[bx]
and al,00001111B ;取低4位存放到al中
mov bl,al ;查表得低4位的ASCII码
mov al,charTable[bx]
pop bx
;显示高4位、低4位的字符
mov cl,al
mov al,ah
call showChar ;在屏幕上显示高4位
mov al,cl
add dl,1
call showChar ;在屏幕上显示低4位
pop dx
pop cx
pop ax
ret
(4)在主程序中设置栈顶
因为栈空间是为整个程序服务的,所以要在主程序中设置栈顶。如果在子程序中设置,容易出错。
栈空间的设置,注意在stack段定义的空间大小要与sp的初始值相一致。
(5)增强程序的容错性
这也是这一章老师讲过的。在这个程序中,可以指定一行显示内存中多少个字节的数据。debug的D命令中默认显示16个字节的数据,如果多于16个,那么查看起来会不方便。因此这里就单独判断一下接收的参数中,指定的字节数是否超出16个,如果超出了,那么就改为16个。这样,就增强了程序的容错性。
二、最终成果
1.完整代码
下面给出这个程序的完整代码。
assume cs:code ;实现debug的D命令(第2版)
data segment
db 20H,22H,11H,09H,'A' ;假定要显示的数据
data ends
stack segment
dw 32 dup(0) ;预留足够的栈空间
stack ends
code segment
start:
mov ax,stack ;设置栈顶
mov ss,ax
mov sp,40H
mov ax,data ;设置ds:si指向要显示的内容首地址
mov ds,ax
mov si,0
call debug_D2 ;调用D命令,显示指定地址、指定长度的内容
;call test1
mov ax,4c00H
int 21H
debug_D2: ;功能:显示指定地址、指定长度的内容
;参数:ds:si指向要显示的内容首地址
;返回:无
;说明:这是子程序相当于debug_D2_arrs的简洁版本,
; 因为其中内置了一些默认的参数,这样方便调用者使用
push cx
push dx
push bx
;指定默认的显示参数
mov cx,40H ;cx指定要显示几个字节的数据
mov dx,0502H ;dx指定屏幕显示的起始行号、列号
mov bl,07H ;bl指定字符的显示属性
mov bh,10H ;bh指定每行显示几个字节内容的对应字符,<16
;调用接收自定义显示参数的子程序
call debug_D2_arrs
pop bx
pop dx
pop cx
ret
debug_D2_arrs: ;功能:在屏幕指定位置,显示指定地址、指定长度的内容
;参数:ds:si指向要显示的内容首地址
; cx指定要显示几个字节的数据
; dx指定屏幕显示的起始行号、列号
; bl指定字符的显示属性
; bh指定每行显示几个字节内容的对应字符,<16
;返回:无
push ax
push cx
push si
push dx
push bx
;根据cx与bh计算共需要显示几行
cmp bh,16 ;如果指定的一行显示字节数>16,则改为16
jna debug_D2_arrs_div
mov bh,16
debug_D2_arrs_div:
mov ax,cx
div bh ;商在al中,余数在ah中
;显示满行的数据
cmp al,0
je debug_D2_arrs_yu
mov ch,0
mov cl,al
s_debug_D2:
call showLine ;显示当前行
push bx ;首地址指向下一行
mov bl,bh
mov bh,0
add si,bx
pop bx
inc dh ;显示位置指向下一行
loop s_debug_D2
;显示最后不满一行的数据
debug_D2_arrs_yu:
cmp ah,0
je debug_D2_arrs_ret ;如果余数为0,就不显示了
mov bh,ah
call showLine
debug_D2_arrs_ret:
pop bx
pop dx
pop si
pop cx
pop ax
ret
showLine: ;功能:显示一行内容
;参数:ds:si指向要显示的内容的首地址
; dx指定当前行显示的行号、起始列号
; bl指定字符的显示属性
; bh指定该行显示几个字节内容的对应字符
;返回:无
push cx
push si
push ax
push dx
;显示行首的段地址:偏移地址
call showAddr
add dl,0BH ;行首地址占11个字符位置
;显示bh个字节的数据内容
mov ch,0
mov cl,bh
s_showLine:
call showByte ;显示两个16进制位对应的字符
add dl,2
mov al,0 ;显示一个空格作为分隔符
call showChar
add dl,1
inc si ;下一个数据
loop s_showLine
;若bh>8,就在第8个字节的数据后边加一个“-”,方便查看
cmp bh,8
jna showLineRet
pop dx ;将本行起始列号取出来赋给dx
push dx
add dl,34
mov al,2DH ;2DH是-的ASCII码
call showChar
showLineRet:
pop dx
pop ax
pop si
pop cx
ret
showAddr: ;功能:在屏幕指定位置显示当前行的段地址:偏移地址
;参数:ds:si指向要显示的内容的首地址
; dx指定当前行显示的行号、起始列号
; bl指定字符的显示属性
;返回:无
jmp short showAddr_start
addrTable dw 0,0 ;存放当前ds si的内容
showAddr_start:
push ds
push si
push ax
push dx
;将ds si存到表中
mov ax,ds
mov addrTable,ax
mov ax,si
mov addrTable[2],ax
;显示ds指向的段地址
mov ax,seg addrTable
mov ds,ax
mov si,offset addrTable ;这里注意,是将addrTable的偏移地址赋给si,不是将addrTable地址单元的内容赋给si
inc si ;显示段地址的高字节
call showByte
add dl,2
dec si ;显示段地址的低字节
call showByte
add dl,2
;显示一个冒号
mov al,3AH ;冒号的ASCII码为3AH
call showChar
add dl,1
;显示si指向的偏移地址
add si,3 ;显示偏移地址的高字节
call showByte
add dl,2
dec si ;显示偏移地址的低字节
call showByte
add dl,2
;显示2个空格
mov al,0 ;空格的ASCII码为0H
call showChar
add dl,1
call showChar
add dl,1
pop dx
pop ax
pop si
pop ds
ret
showByte: ;功能:在屏幕指定位置显示一个字节的字符形式
;参数:ds:si指向当前要显示的内容的地址
; dx指向当前行号、列号
; bl指定字符的显示属性
;返回:无
jmp short showByte_start
charTable db '0123456789ABCDEF' ;十六进制字符表
showByte_start:
push ax
push cx
push dx
;查表得当前数据的ASCII码
mov al,ds:[si] ;取当前字节数据
mov ah,al
mov cl,4 ;取高4位存放到ah中
shr ah,cl
push bx ;查表得高4位的ASCII码
mov bh,0
mov bl,ah
mov ah,charTable[bx]
and al,00001111B ;取低4位存放到al中
mov bl,al ;查表得低4位的ASCII码
mov al,charTable[bx]
pop bx
;显示高4位、低4位的字符
mov cl,al
mov al,ah
call showChar ;在屏幕上显示高4位
mov al,cl
add dl,1
call showChar ;在屏幕上显示低4位
pop dx
pop cx
pop ax
ret
showChar: ;功能:在屏幕指定位置显示一个ASCII码字符
;参数:al指定要显示的ASCII码
; dx指向当前行号、列号
; bl指定字符的显示属性
;返回:无
push cx
push es
push di
push ax
push dx
;es:di指向屏幕显示首地址
mov cx,0B800H
mov es,cx
mov cl,al ;用cl暂存al
mov ax,160
mul dh
mov dh,0
add ax,dx
add ax,dx
mov di,ax
;显示指定字符
mov es:[di],cl
mov es:[di+1],bl ;显示字符属性
pop dx
pop ax
pop di
pop es
pop cx
ret
code ends
end start
2.效果图
总结
本文再次从零开始手动实现了debug的D命令,因为有了上一次的经验,这次熟练很多,一些子程序的功能实现逻辑也进一步优化,练习使用了“查表法”这种编程技巧,并且总结了近一段时间学习到的编程方法。最终的显示效果也比上次更好。总之很有收获!