汇编语言 自己动手实现Debug的D命令(第2版)

发布于:2022-11-09 ⋅ 阅读:(10) ⋅ 点赞:(0) ⋅ 评论:(0)

文章目录

前言

一、思路分析

1.程序框架

2.编程方法小结

(1)从底层功能开始

(2)执行时跳过数据部分

(3)查表法

(4)在主程序中设置栈顶

(5)增强程序的容错性

二、最终成果

1.完整代码

2.效果图

总结


前言

之前我写过一篇博文 《汇编语言 自己动手实现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命令,因为有了上一次的经验,这次熟练很多,一些子程序的功能实现逻辑也进一步优化,练习使用了“查表法”这种编程技巧,并且总结了近一段时间学习到的编程方法。最终的显示效果也比上次更好。总之很有收获!