实验10 编写子程序

实验十:编写子程序

一. 子程序:显示字符串

      实验要求:在屏幕的8行3列,用绿色显示data段中的字符串。

      名称:show_str

      功能:在指定的位置,用指定的颜色,显示一个用0结束的字符串。

      参数:(dh)=行号(0-24取值范围);(dl)=列号(0-79取值范围);(cl)=颜色(是一个二进制排列组合的值);ds:si指向字符串的首地址。

      实验目的:

      1.熟练掌握在dos屏幕上输出字符的基本操作。掌握显示缓冲区范围。

      2.为什么定义字符串用0来结尾?

      3.熟练掌握从内存中读取字节单元内容和字单元内容;并将该内容写入我们期望的内存中。

      4.掌握8位乘法和16位乘法的操作。

      程序分析:

      在实验九中我们可以知道一些基本信息:

      命令提示符窗口或dos窗口,我们可以显示80X25的字符(我的机器行数多,命令提示符窗口,跟设置有关)。每行80个字符,一共是25行。它们在内存中是在一个内存段中存储的,这个内存区域叫做显示缓冲区。从物理地址B8000H~BFFFH这个32K的内存区域就是显示缓冲区。

      在显示缓冲区中,偶数字节单元表示的是字符,奇数字节单元表示的是字符的属性(颜色、闪烁等)。也就是说在显示缓冲区中,每2个字节负责屏幕上一个字符的显示(包括显示的属性)。

       在显示缓冲区内写入的字符,立即就显示在屏幕上。

      因为每行要显示80个字符,故从0000H~009FH是显示的第一行(共160个字节)。每行可以类推。也就是说行的偏移量是160个字节。

      为什么在定义字符串时候,结尾有个0?

      讲解:因为在汇编和其它语言中,字符串存储在内存中,长度不一致,我们统一规定在每个字符串的结尾有个数字0,就代表了这个字符串结束了。规定(也是便于管理)

      在写入显示缓冲区中时,我们为什么使用[bx+di+idata]的方式?

      在子程序中,我们通过计算得出了在特定行和特定列(由主程序中的dl和dh参数传入)的基于b800:0000的偏移地址是(bx);(di)代表了从这个偏移地址开始,每个字符的偏移地址。(idata)代表了每个字符的二个字节(一个是字符本身,一个是字符的颜色属性)。

 

代码如下:

assume cs:code

data segment

   db 'Welcome to masm!', 0        ;内存data段中定义一个字符串

data ends

code segment

main:  ;字符串参数

       mov dh, 8           ;屏幕的行数

       mov dl, 3           ;所在行的列数

       mov ch, 0           ;ch清零,防止高8位不为零。

       mov cl, 2           ;颜色属性(此处应是二进制数00000010)

      

       mov ax, data

       mov ds, ax

       mov si, 0           ;将ds:si指向字符串

       call show_str

      

       mov ax, 4c00H

       int 21H

   ;show_str功能 :按行和列及字符属性显示字符串 

    ;入口参数:dh-行数、dl-列数、cl-字符属性

    ;返回值:无

show_str:  push dx

           push cx

           push si             ;将子程序用到的寄存器入栈

          

           mov ax, 0b800H

           mov es, ax          ;设置显示缓冲区内存段

          

           mov ax, 0           ;(ax)= 0,防止高位不为零 

           mov al, 160         ;0a0H-   160字节/行

           mul dh              ;相对于0b800:0000第dh行偏移量

           mov bx, ax          ;将第(dh)行的偏移地址送入bx,bx代表行偏移

           mov ax, 0

           mov al, 2           ;列的标准偏移量是2个字节

           mul dl              ;同一行列的偏移量,尽量使用乘法,(al)=列偏移

           add bx, ax          ;最终获得偏移地址(bx)=506H

           mov di,0            ;将di作为每个字符的偏移量

           mov al, cl          ;将字符属性写入al中

           mov ch, 0           ;将cx高8位设置为0

          

   show:   mov cl, ds:[si]     ;将字符串单个字符读入cl中

           jcxz ok             ;判断字符串是否为零。

           mov es:[bx+di+0], cl    ;在显示缓冲区中写入字符

           mov es:[bx+di+1], al    ;在显示缓冲区中写入字符属性

           add di, 2

           inc si

           jmp short show

  

       ok: pop si              ;字符串字符为0,结尾

           pop dx

           pop cx              ;恢复寄存器

           ret

  

code ends

end main

程序体会:

1).参数的传递.。此程序有3个参数,它们是:dh)=行号(0-24取值范围);(dl)=列号(0-79取值范围);(cl)=颜色。参数的传递方式是传递值。

      通过修改这3个参数,我们可以方便的将data段中定义的字符串显示在我们需要的位置。

2).子程序的调用,我们可以多次调用该子程序,用于显示特定的字符串。只要知道字符串的地址,我们不必关心子程序内部是怎样运算的。也就是说只要我们指定ds:si的指向就可以了。那么我们使用实验10的代码,改写实验9的程序就轻松了。只需要在主程序中添加参数和再次call下就行了。

3).只要给出相关的参数值,调用子程序,我们就可以达到我们预期的目的(子程序设计的目的)

4).还是要熟悉从内存中读入一个字符,我们采用的是ds:[si]     方式;写入到显示缓冲区中(同样是内存,它们没有任何的区别,CPU把所有的设备都内存化了),我们采用了

es:[bx+di+idata]的方式。由于ds寄存器被data段占用了,目前只有es寄存器可用,只好把es当做了显存的段寄存器,bx代表了行偏移、di代表了列偏移、idata(值是0和1)代表了列的2个字节(2个字节代表一个字符的显示)的偏移。

5)这个程序是固定了行和列,我们也可以通过程序来提示行和列,做到人机交互,这个本章没有涉及。呵呵。

6)关于接口的问题,我们查找其他资料了解。

 

二. 解决除法溢出的问题

问题提出:

      考虑下面代码1:

      mov bh, 1

      mov ax, 1000

      div bh

 程序分析:由于除数是(bh),故div是执行的8位除法,(ax)/(bh)=1000(结果);结果的商(1000)应该存放在al中,结果的余数(0)存放在ah中;从代码我们得知,它的结果的商是1000,al是8位寄存器,保存的数值(0~255如果按照无符号数运算)超出了存储范围。

      考虑下面代码2:

      mov ax, 1000H

      mov dx, 1

      mov bx, 1

      div bx

      程序分析:由于除数是(bx),故div是执行的16位除法,被除数应该是:(dx)高16位,与(ax)低16位组合在一起=11000H,故11000H/(bx)=11000H(结果)。结果的商(11000H)存入ax中,结果的余数(0)存入到dx中;由于ax寄存器不能存储11000H数值,导致溢出。

      除法溢出:除法操作时,由于运算结果商的值过大,超出ax寄存器的存储范围,导致ax寄存器不能存储该值。CPU将引发一个内部错误:除法溢出。

      展示下如何导致除法的溢出。(在debug中直接演示)

解决方法:编程一个子程序。

名称:divdw

      功能:进行不会产生溢出的除法运算,被除数为dword型,除数为word型,结果为dword型。

      参数:(ax)=dword型被除数的低16位

            (dx)=dword型被除数的高16位

            (cx)=除数            

     返回值:(ax)=dword型结果的低16位

               (dx)=dword型结果的高16位

               (cx)=除数

例子:计算1000000/10(F4240H/0AH)

      mov ax, 4240H

      mov dx, 000FH

      mov cx, 0AH

      call divdw

程序分析:

      1)首先判断这个无符号数值的除法运算;1000000==F4240H这个数ax寄存器肯定存储不下,1000000/10==F4240H/0AH=186A0H(结果),结果ax也存储不下,结果的余数(0)dx倒是可以存储。这种情况CPU会发生内部错误:除法溢出。

      2)我们将被除数(一个双字单元,4个字节)的高16位(000FH)存储在dx中,将低16位(4240H)存储在低16位中;除数(0AH)存储在cx中。

      3)调用子程序divdw,子程序的返回值,高16位存储在dx中,将低16位存储在低16位中;除数依然不变存储在cx中。

      4)考虑将被除数和除数定义在内存data段中,通过内存读入到寄存器中,这样符合设计思想。在此例子中,似乎王老师希望直接在寄存器赋值。

      5)我们不必纠结这个公式的推算,这是数据结构中算法负责研究的事情,我们只管负责把这个推算的公式汇编语言代码化。

      6)分析这个公式:

      X是被除数:(范围[0~FFFFFFFFH]),也就是0F4240H

      N是除数:(范围[0~FFFFH]),也就是0AH

      H:高16位(范围[0~FFFFH]),对于被除数来说就是000FH

      L:低16位(范围[0~FFFFH]),对于被除数来说就是4240H

      int():取商,int(H/N)也就是求H/N结果的商。

      rem():取余,rem(H/N)也就是求H/N结果的余数。

      公式:X/N=int(H/N)*65536+[rem(H/N)*65536+L]/N

      噢!MY GOD!你还能不能再把这个烂公式简单点???这个有点弯弯绕。大部分人看不懂这个公式。

      解读这个公式(希望我们不要把时间花费在这个上面,这是数据结构的问题):

      明确65536是什么东东?10000H;等价于左移16位,也就是说此例中代表了高位寄存器。

      X/N代表,你懂的!!!   X(被除数)代表一个dw类型的双字单元,N(除数)代表一个字单元。

      int(H/N)*65536代表了X/N结果高16位

      [rem(H/N)*65536+L]/N代表了X/N结果的低16位

      +代表了将它们的组合一起,代表了一个dw类型的双字单元。

      程序分析:

      首先:我们先求H/N,这个有结果了,公式的二个部分都有结果了,商=int(H/N);

      余数:rem(H/N)。

       要使用div指令,div cx,最初:(ax) =4240H(低16位);(dx)=000FH(高16位);(cx)=0AH(除数)。

      由于ax中目前有值,先将其压栈保存。

      然后(ax)=(dx)(高16位的数值),(dx)=0000H,(cx)不变,也就是说0000 000FH/000AH。结果的商为0001H,存储在了ax中,此时(ax)=0001H,结果的余数存储在dx中了,此时(dx)=0005H。

      然后我们求出int(H/N)*65536,结果的高16位:

      H/N的结果有了,商是0001H(等价于int(H/N)),保存在了ax中。这个数我们另存储在一个寄存器中(由于后面需要ax参与运算)。此例中我们把这个值先保存在了bx中;

      为毛*65536?表明了它是一个数据(2个字单元)的高16位。也就是说0001H代表了最终结果的高16位的数值。

      其次我们求出[rem(H/N)*65536+L]/N       ,运算结果商代表最终结果的低16位,运算结果的余数,代表最终结果的余数:

      理解公式的后部分:同理:[rem(H/N)*65536+L]也代表一个数值:高16位是rem(H/N)*65536(使用这个表示),其实存储在高16位寄存器变量中的数值是rem(H/N);为什么*65536,表示它左移了16位,代表高16位。低16位是L,就是最初存储在ax中的那个数值。将它们二者组合后,形成一个新的数值,这个新的数值与N(始终保持不变的cx变量)做除法运算,运算结果的商=(ax),余数=(dx)。也就是说此时的(ax)就是低16位的数值。

      通俗的讲就是将H/N的余数(等价于rem(H/N))作为高16位;将L作为低16位;

将它们组合成一个数值,与cx做除法运算。此时我们的H/N运算结果的余数存储在dx中了,是0005H,正好,就是我们需要的数值;L代表了低16位的值,这个值在栈中呢,弹栈到ax(把它恢复就可以了pop ax),那么(dx)=0005H,(ax)=4240H,(cx)=0AH,将它们组合后形成一个dw型的双字数值:0005H+4240H=00054240H。也就是00054240H/0AH;结果的商存储在ax中(此时ax=86A0H),余数是0存储在dx中。

      最终结果的高16位的值是:0001H(我们把它存储在了bx中),把它送入dx中去;低16位就是ax值,除数cx值不变。也就是说最终结果是186A0H。

汇编代码如下:

assume cs:code

code segment

start:

       mov ax, 4240H       ;被除数,低16位

       mov dx, 000FH       ;被除数,高16位

       mov cx, 0AH         ;除数

      

       call divdw          ;调用divdw子程序,做不溢出的除法运算。

 

       mov ax, 4c00H

       int 21H

 

divdw:                      ;子程序开始

       push ax             ;将被除数低16位先压栈保存。

       mov ax, dx          ;(ax)=(dx)

       mov dx, 0000H       ;

       div cx              ;此时(dx)=0000H,(ax)=000FH,组合成0000000FH。

       mov bx, ax          ;将H/N结果的商先保存在bx中,(bx)=0001H

      

       pop ax              ;将L值弹栈到ax

       div cx              ;此时(dx)=0005H,(ax)=4240H,组合成54240H

       mov cx, dx          ;返回值(cx)等于最终结果的余数

       mov dx, bx          ;最终结果高16位值=(bx)

code ends

end start 

 

在debug中运行结果是:

AX=86A0 BX=0001  CX=0000  DX=0001 SP=FFFE  BP=0000  SI=0000 DI=0000

DS=0B56 ES=0B56  SS=0B66  CS=0B66 IP=0022   NV UP EI PL NZ NA PO NC

结果分析:与F4240H/0AH=186A0H(商)结果一样,余数:(cx)=0000H

实验目的:

     1)考察对于一个较大的数值的存储,无论是在寄存器中还是在内存中。

      2)熟悉div除法指令的内涵,它的操作数存储的寄存器是那些?运算的结果又存储在那些寄存器中。

      3)合理例如栈空间保存一些临时的数值。

改进程序:这个程序看着有点累,如果将这个被除数存储在内存中,代码就显得好理解些,有兴趣的自行修改。

 

三。数值显示

问题提出:

      编程,将data段中定义的数据以十进制的方式显示出来(在计算机屏幕上)

      data segment

              dw 123, 12366, 1, 8 , 3, 38

      data ends

编程分析:

      1)确定123, 12366, 1, 8 , 3, 38这些数在内存中是以二进制形式存储的(二进制补码),2个字节存储一个数字。12366=(0011 0001 0111 1010B)=(317AH)

      2)在计算机屏幕上显示的数字、字符、其他符号,一律按照字符方式显示,都是按照ASCII码来处理的。也就是说在屏幕上显示的1它不代表数值1,而是字符1。我们遇到的问题变成了怎样把二进制代码(补码方式存储的)变成ASCII码;

      提示:0~9字符在ASCII码中是30H~39H,是否有规律?0=30H、1=30 H +1、……9=30H+9。

3)在底层显卡显示方面,由于我们知道了CPU只要在显卡的显存中写入期望的数据,它就显示在屏幕上,那么我们就可以利用实验10第一个子程序了。

4)怎样将一个十进制的数值转变为表示十进制数的字符串,并且字符串以0为结尾符号。例如:将数值12666转变成一个字符串“12666”,也就是说得到1,2,6,6, 6的ASCII码,他们分别是31H、32H、36H、36H,36H。怎样得到十进制的各个数字呢?我们可以使用将12666除以10,然后取余数,将余数倒序后,就得到了12666的各个位的数字了,对于12666搞个循环,5次,就将它们搞定了。这里注意余数的顺序。

怎样转换成ASCII码?字符0(ASCII码30H),同理,字符3就是30H+3,总结:30H+余数就是对应的ASCII码。

但是对于我们不知道的一个十进制数字,怎么判断各位的值求出来呢?只要保证结果的商是0,那么这个数除以10肯定结束了。这样我们可以使用jcxz指令判断(CX)是否为0(将结果的商每次送入到cx中),作为结束循环条件。

5)由于(ax)/10的求商(求各个位的数字)的顺序是倒序的(原理看书吧!)。怎样把它的顺序给倒过来?我们可以采用栈的结构,利用栈的先进后出的原理,弹栈时将栈顶的值(也就是数字的最高位的值)先写入内存data段中。这样就解决了字符顺序的问题。

也可以判断该数字一共有几个数字组成,然后在写入内存时,si的值是从大到小递减也可以。

还是利用系统给你的栈结构吧。那个是免费了,不用费事了!

6)编写子程序dtoc,功能是将ax中的存储十进制数值(传入参数(ax))转换成对应的ASCII码,并将这些字符按顺序写入到内存data段中。

7)调用子程序show_str,显示该data段的字符串。完成在计算机屏幕中显示12666这个字符串的功能。

汇编代码如下:

assume cs:code

data segment

   db 10 dup (0)           ;初始化10个字节,置零

data ends

 

code segment

start: mov ax, 12666       ;将显示的数字赋值给ax

       mov bx, data      

       mov ds, bx        

       mov si, 0           ;将ds:si指向data内存段

       call dtoc           ;调用dtoc子程序

       ;为调用show_str做准备 

       mov dh, 8           ;屏幕的行数

       mov dl, 3           ;所在行的列数

       mov ch, 0           ;ch清零,防止高8位不为零。

       mov cl, 2           ;颜色属性(此处应是二进制数00000010)

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

      

       mov ax, 4c00H

       int 21H

;-----

;dtoc功能:将一个数字转换成字符串,并写入data段中。

;入口参数:ax, ds

;返回值:无

;-----    

dtoc:      ;保护寄存器变量值,因为下面的变量子程序都用到。

           push ax

           push cx

           push bx

           push si           

          

           mov si, 0       ;偏移地址置零

           mov bx, 10      ;除数=10

   change: mov dx, 0       ;涉及到16位除法,先将存储余数的变量置零

           div bx          ;将(ax)/(bx)   

                      

           mov cx, ax      ;将除法运算结果的商赋值给cx,用于条件判断                     jcxz last       ;判断cx是否为0?或商为零?

           add dx, 30H     ;将每个位的数字转换成ASCII码值

           push dx         ;将ASCII码值压栈保存

          

           inc si        

           jmp short change

  

   last:   ;最后一次除法,商为0,(dx)=余数时,没有转换并压栈。故。。。。。。

           add dx, 30H     ;将数字转换成ASCII码

           push dx         ;将字符值压栈

            inc si          ;最后一次也要转换并压栈

          

    ;将栈中数据倒序写入内存data段中

           mov cx, si      ;(si)=字符串共几个字符,设置循环计数器cx。

           mov si, 0

       s:  pop ds:[si]     ;弹栈,并写入data内存段。

           inc si

           loop s

      

   exit:   ;恢复寄存器,并返回主调程序。

           pop si

           pop bx

           pop cx

           pop ax

           ret

;------   

;show_str功能 :按行和列及字符属性显示字符串 

    ;入口参数:dh-行数、dl-列数、cl-字符属性

    ;返回值:?

;------

show_str:  push dx

           push cx

           push si             ;将子程序用到的寄存器入栈

          

           mov ax, 0b800H

           mov es, ax          ;设置显示缓冲区内存段

          

           mov ax, 0           ;(ax)= 0,防止高位不为零 

           mov al, 160         ;0a0H-   160字节/行

           mul dh              ;相对于0b800:0000第dh行偏移量

           mov bx, ax          ;将第(dh)行的偏移地址送入bx,bx代表行偏移

           mov ax, 0

           mov al, 2           ;列的标准偏移量是2个字节

           mul dl              ;同一行列的偏移量,尽量使用乘法,(al)=列偏移

           add bx, ax          ;最终获得偏移地址(bx)=506H

           mov di,0            ;将di作为每个字符的偏移量

           mov al, cl          ;将字符属性写入al中

           mov ch, 0           ;将cx高8位设置为0

          

   show:   mov cl, ds:[si]     ;将字符串单个字符读入cl中

           jcxz ok             ;判断字符串是否为零。

           mov es:[bx+di+0], cl    ;在显示缓冲区中写入字符

           mov es:[bx+di+1], al    ;在显示缓冲区中写入字符属性

           add di, 2

           inc si

           jmp short show

  

       ok: pop si              ;字符串字符为0,结尾

           pop dx

           pop cx              ;恢复寄存器

           ret

code ends

end start

程序理解:

1)子程序show_str的代码没有任何改变,拿来直接用就可以了。注意在汇编语言编程中代码段的框架结构。

2)如果遇到的数字数值过大?可以考虑实验第二个子程序:divdw来解决问题。此时也应该考虑这个数字位数多,在data段中多初始化内存空间;

3)合理利用系统提供的栈结构,或程序员创建的栈结构,提高临时存储数据的效率。

4)有时间看看ASCII的有关资料。帮助你理解字符及字符串。

5)我们在写入data内存段时,结尾没有0,这个不必纠结,在我们初始化data时,都置零了,也就是说“12666”后面有零。字符串后面有0,为什么?我们以前介绍了。

C语言随想:看来我们还是怀念C,为了在屏幕上显示字符串,费劲太大了。C语言一个语句就搞定了。但在C中你看不到底层是怎么操作的,其实跟这个类似。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值