文章目录

前言

一、子程序1 显示字符串

1.实验任务

2.分析

(1)如何在指定位置显示

(2)如何显示指定颜色

(3)保存子程序中用到的寄存器

3.代码

二、子程序2 解决除法溢出的问题

1.实验任务

2.代码

三、子程序3 数值显示

1.实验任务

2.显示一个word型数据的代码

总结


前言

本文是王爽老师《汇编语言》(第四版) 第十章 实验10 的分析及代码。

本实验的内容是:编写三个子程序(显示字符串、解决除法溢出的问题、数值显示)。

一、子程序1 显示字符串

1.实验任务

编写一个子程序,向外提供可以调用的接口,能够在指定的行号、列号,用指定的颜色,显示一个用0结束的字符串。

assume cs:code
data segment
  db 'Welcome to masm!',0
data ends

code segment
start:
    mov dh,8    ;指定行号
    mov dl,3    ;指定列号
    mov cl,2    ;指定颜色

    mov ax,data    ;设置ds
    mov ds,ax
    mov si,0    ;设置ds:[si]指向当前要显示的字符

    call show_str    ;调用子程序,显示字符串

    mov ax,4c00H    ;程序返回
    int 21H
 
show_str:        
    

code ends
end start

2.分析

(1)如何在指定位置显示

第一屏的内容所对应的内存地址空间为B8000H~B8F9FH,因而可以令这一段的段地址为B800H。

显示器一屏可以显示25行,每行80个字符(每个字符占2个字节)。因此第一个字符应该在的偏移地址应该是 ( dh*80*2 + dl*2 )。注意,任务中给出的列号是指显示屏上的第几个字符,而不是指该行的第几个字节,显示区每个字符占两个字节!因此,指定的行号参数取值范围是00-24,列号取值范围是00-79 (十进制)。

(2)如何显示指定颜色

属性字节中,最低三位可以用来设置颜色。这个属性字节的内容应当是寄存器cl的值。

(3)保存子程序中用到的寄存器

子程序中会使用到dh  dl  si  等寄存器,因而在子程序的一开始要把这些寄存器的值压入栈,在子程序返回前再pop出来。由此可知需要一段栈空间。

3.代码

assume cs:code,ds:data,ss:stack
data segment
    db 'Welcome to masm!',0     ;要显示的字符串
data ends
 
stack segment
    dw 8 dup(0)        ;用于在调用子程序时存放寄存器值
stack ends
 
code segment
start:
    mov dh,8    ;指定行号(从0开始计数)
    mov dl,3    ;指定列号(从0开始计数)
    mov cl,2    ;指定颜色
 
    mov ax,data    ;设置ds
    mov ds,ax
    mov si,0    ;设置ds:[si]指向data段中当前要显示的字符
    mov ax,stack    ;设置栈顶
    mov ss,ax   
    mov sp,10H 
 
    call show_str    ;调用子程序,显示字符串
 
    mov ax,4c00H    ;程序返回
    int 21H
 
show_str:      
                ;功能:将data段中首地址为ds:si的字符,以指定颜色显示在屏幕指定位置
                ;参数:dh 行号(从0开始计数), dl 列号(从0开始计数) ,cl 颜色
                ;返回:无
               
    push dx        ;将子程序用到的寄存器压入栈
    push si
    push ax
    push bx
    push es
    
    mov ax,0B800H    ;设置es为显示区段地址
    mov es,ax
    
    mov ax,00A0H    ;设置首字符显示的地址
    mul dh
    mov dh,0
    add ax,dx 
    add ax,dx
    mov bx,ax    ;bx是显示区的偏移地址
        
    mov al,cl    ;用al存储属性字节
    mov ch,0
    mov si,0
    
    s:                ;循环读取字符并显示
    mov cl,ds:[si]
    jcxz ok            ;若读到0,就退出循环
    mov es:[bx],cl
    inc bx
    mov es:[bx],al
    inc bx
    inc si
    jmp short s
 
    ok:        ;将寄存器的值pop出来
    pop es
    pop bx
    pop ax
    pop si
    pop dx
    
    ret    ;返回
 
code ends
end start

二、子程序2 解决除法溢出的问题

1.实验任务

(1)写一个子程序,功能是进行不会产生溢出的除法运算。被除数为dword型,除数为word型,结果为dword型。

参数:ax = dword型数据的低16位 ;dx = dword型数据的高16位 ;cx = 余数

返回: dx = 结果的高16位 ;ax = 结果的低16位 ; cx = 余数

(2)可以参考的公式如下。

X 为被除数,H 为X的高16位,L 为X的低16位 , N 除数。

int()表示取商,rem()表示取余数。

则  X/N = int( H/N ) * 65536 + [rem( H/N) * 65536 + L]/N   。

(通过实际写代码会对这个公式的巧妙性有更为深入的理解。)

2.代码

assume cs:code,ds:data,ss:stack
 
data segment  ;定义数据段
  db 0
data ends
 
stack segment ;定义栈段
  db 0
stack ends
 
code segment  ;定义代码段
start:
 
  mov ax,stack    ;设置栈顶
  mov ss,ax
  mov sp,10H

  mov ax,4240H    ;被除数的低16位
  mov dx,000FH    ;被除数的高16位
  mov cx,0AH    ;除数
  call divdw    ;调用子程序

  mov ax,4c00H          ;程序返回
  int 21H

divdw:    ;功能:改进后的不会溢出的div指令
          ;参数:  ax=被除数低16位,dx=被除数高16位,cx = 除数
          ;返回:  ax=商的低16位,dx=商的高16位,cx = 余数

  ;计算公式: X/N = int( H/N ) * 65536 + [rem( H/N) * 65536 + L]/N  
  ;其中X为被除数,N为除数,H为被除数的高16位,L为被除数的低16位,
  ;int()表示结果的商,rem()表示结果的余数。
  
  push bx    ;bx是额外用到的寄存器,要压入栈

  mov bx,ax    ;bx=L
  mov ax,dx    ;ax=H
  mov dx,0    ;dx=0
  div cx        ;计算H/N,结果的商即int(H/N)保存在ax,余数即rem(H/N)保存在dx

                  ;接下来要计算int(H/N)*65536,思考一下,65536就是0001 0000 H,
                  ;因此计算结果就是,高16位=int(H/N),低16位为0000H。

  push ax        ;将int(H/N)*65536结果的高16位,即int(H/N),压入栈
  mov ax,0
  push ax        ;将int(H/N)*65536结果的低16位,即0000H,压入栈

                  ;接下来要计算 rem(H/N)*65536 ,同理可得,
                  ;计算结果为 高16位=  rem(H/N)*65536 ,即此时dx的值,
                  ;低16位为 0000H。

  mov ax,bx    ;ax = bx = L ,即 [rem(H/N)*65536 + L]的低16位
                ;此时dx=rem(H/N),可以看做 [rem(H/N)*65536 + L] 的高16位
  div cx        ;计算 [rem( H/N) * 65536 + L]/N ,结果的商保存在ax,余数保存在dx

                  ;接下来要将两项求和。  左边项的高、低16位都在栈中,
                  ;其中高16位就是最终结果的高16位,低16位是0000H。
                  ;右边项的商为16位,在ax中,也就是最终结果的低16位,
                  ;余数在dx中,也就是最终结果的余数。

  mov cx,dx    ;cx = 最终结果的余数
  pop bx        ;cx = int(H/N)*65536结果的低16位,即0000H。
  pop dx        ;bx = int(H/N)*65536结果的高16位,即最终结果的高16位

  pop bx    ;还原bx的值

  ret
  
code ends
end start

三、子程序3 数值显示

1.实验任务

(1)将data段中的数据以十进制的形式显示出来。应该编写一个通用的子程序,将一个给定的word型数据转为以十进制显示的字符串。而字符的显示功能,可以调用上边写的子程序1来完成。

子程序描述:

名称:dtoc

功能:将word型数据转变为表示十进制数的字符串,字符串以0为结尾符。

参数:ax = word型数据 ; ds:si 指向字符串的首地址

返回:无

(2)代码

assume cs:code,ds:data
data segment
    db 10 dup(0)
data ends

code segment
start:
    mov bx,data    ;设置ds段地址
    mov ds,bx

    mov ax,12666    ;要显示的数据
    mov si,0    ;ds:si指向字符串首地址
    call dtoc    ;将数据转为十进制字符

    mov dh,8    ;在屏幕第几行开始显示
    mov dl,3    ;在屏幕第几列开始显示
    mov cl,2    ;显示的字符的颜色
    call show_str

    mov ax,4c00H    ;程序返回
    int 21H

dtoc:


show_str:        


code ends
end start

2.显示一个word型数据的代码

这里要把十六进制(或者说二进制)的数据转换为十进制,就要用到除法,也就是div指令,有可能出现除法溢出的问题,可以调用子程序2(解决除法溢出问题)来解决!而转换成十进制的字符数字之后,可以调用子程序1(显示字符串)来显示。

assume cs:code,ds:data,ss:stack
data segment
    db 10 dup(0)
data ends
 
stack segment
    dw 16 dup(0)    ;32字节
stack ends
 
code segment
start:
    mov bx,data    ;设置ds段地址
    mov ds,bx
    mov bx,stack    ;设置栈顶
    mov ss,bx
    mov sp,20H
 
    mov ax,12666    ;要显示的数据
    mov si,0    ;ds:si指向字符串首地址
    call dtoc    ;将数据转为十进制字符
    
    mov dh,8    ;在屏幕第几行开始显示
    mov dl,3    ;在屏幕第几列开始显示
    mov cl,2    ;显示的字符的颜色
    call show_str
 
    mov ax,4c00H    ;程序返回
    int 21H
 
 
dtoc:    ;功能:将给定的word型数据转为十进制字符形式,存入data段,首地址ds:si
            ;参数:ax 指定的word数据 
            ;返回:ds:si指向data段字符串首地址
    
    push bx;    ;将子程序用到的寄存器压入栈
    push cx;    
    push dx;
    push si;
 
    mov bx,000aH    ;bl = 除数,bh = 一共除了几次
    mov dx,0    ;即将进行除法,dx是高16位,低16位在ax中
    pushyushu:  ;每次循环都进行一次除法,将所得余数压入栈,直到某次除法后商为0
    mov cx,0
    mov cl,bl    ;cx = 除数
    call divdw    ;调用不会溢出的除法函数,结果的商的高16位,在dx中,
                    ;低16位在ax中,余数在cx中,此例中余数一定<10
    push cx        ;cx=余数,这个余数在显示的时候要倒序显示,因此先压入栈
    inc bh        ;记录将余数压入栈的次数
    mov cx,dx    ;cx = dx = 结果的商的高16位,这一步是为了判断dx是否为0
    jcxz compareax  ;若cx=0,则继续判断ax是否为0
    jmp short pushyushu   ;若cx!=0,则商不为零,返回循环体开头进行下一次除法
    compareax:      ;继续判断ax(除法的商的低16位)是否为0
    mov cx,ax       ;cx = ax = 结果的商的低16位,这一步是为了判断ax是否为0
    jcxz popyushu        ;若cx=0,则说明除法计算已经完毕,跳转下一步执行
    jmp short pushyushu   ;若cx!=0,则商不为零,返回循环体开头进行下一次除法
 
    popyushu:     ;商为0,除法结束
    mov ch,0
    mov cl,bh    ;ch=0,所以cx = 将余数压入栈的次数,也就是接下来的循环次数
    s1:
    pop ax;        ;从栈中pop出一个余数
    add ax,30H    ;从数字转为对应的数字字符
    mov ds:[si],al    ;用al就够了
    inc si
    loop s1       
    
    pop si    ;子程序结束,将寄存器的值pop出来
    pop dx;    
    pop cx;
    pop bx;
    
    ret
 
divdw:    ;功能:计算word型被除数与byte型除数的除法
            ;参数:  ax=被除数低16位,dx=被除数高16位,cx = 除数
            ;返回:  ax=商的低16位,dx=商的高16位,cx = 余数
 
    ;计算公式: X/N = int( H/N ) * 65536 + [rem( H/N) * 65536 + L]/N  
    ;其中X为被除数,N为除数,H为被除数的高16位,L为被除数的低16位,
    ;int()表示结果的商,rem()表示结果的余数。
 
    push bx    ;bx是额外用到的寄存器,要压入栈
 
    mov bx,ax    ;bx=L
    mov ax,dx    ;ax=H
    mov dx,0    ;dx=0
    div cx        ;计算H/N,结果的商即int(H/N)保存在ax,余数即rem(H/N)保存在dx
 
                    ;接下来要计算int(H/N)*65536,思考一下,65536就是0001 0000 H,
                    ;因此计算结果就是,高16位=int(H/N),低16位为0000H。
 
    push ax        ;将int(H/N)*65536结果的高16位,即int(H/N),压入栈
    mov ax,0
    push ax        ;将int(H/N)*65536结果的低16位,即0000H,压入栈
 
                    ;接下来要计算 rem(H/N)*65536 ,同理可得,
                    ;计算结果为 高16位=  rem(H/N)*65536 ,即此时dx的值,
                    ;低16位为 0000H。
 
    mov ax,bx    ;ax = bx = L ,即 [rem(H/N)*65536 + L]的低16位
    div cx        ;计算 [rem( H/N) * 65536 + L]/N ,结果的商保存在ax,余数保存在dx
 
                    ;接下来要将两项求和。  左边项的高、低16位都在栈中,
                    ;其中高16位就是最终结果的高16位,低16位是0000H。
                    ;右边项的商为16位,在ax中,也就是最终结果的低16位,
                    ;余数在dx中,也就是最终结果的余数。
 
    mov cx,dx    ;cx = 最终结果的余数
    pop bx        ;cx = int(H/N)*65536结果的低16位,即0000H。
    pop dx        ;bx = int(H/N)*65536结果的高16位,即最终结果的高16位
 
    pop bx    ;还原bx的值
 
    ret
 
    
show_str:
                ;功能:将data段中首地址为ds:si的字符,以指定颜色显示在屏幕指定位置
                ;参数:dh 行号, dl 列号 ,cl 颜色
                ;返回:无
 
    push dx        ;将子程序用到的寄存器压入栈
    push si
    push ax
    push bx
    push es
 
    
    mov ax,0B800H    ;设置es为显示区段地址
    mov es,ax
    
    mov ax,00A0H    ;设置首字符显示的地址
    mul dh
    mov dh,0
    add ax,dx 
    add ax,dx
    mov bx,ax    ;bx是显示区的偏移地址
        
    mov al,cl    ;用al存储属性字节
    mov ch,0
    mov si,0
    
    s:                ;循环读取字符并显示
    mov cl,ds:[si]
    jcxz ok            ;若读到0,就退出循环
    mov es:[bx],cl
    inc bx
    mov es:[bx],al
    inc bx
    inc si
    jmp short s
 
    ok:        ;将寄存器的值pop出来
    pop es
    pop bx
    pop ax
    pop si
    pop dx
    
    ret    ;返回
 
 
code ends
end start
 


总结

本文是王爽老师《汇编语言》(第四版) 第十章 实验10 编写3个子程序 的分析及代码。这个实验任务要按顺序完成,因为后边的任务会用到前边写的子程序。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐