汇编语言(王爽第三版)实验10:编写子程序

实验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           ;颜色属性(此处应是二进制数0000 0010)

       

        mov ax, data

        mov ds, ax

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

        call show_str

       

        mov ax, 4c00H

        int 21H

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

    ;入口参数:dh-行数、dl-列数、cl-字符属性、ds:[si]指向字符串。

    ;返回值:无

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)

            ret

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           ;颜色属性(此处应是二进制数0000 0010)

        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中你看不到底层是怎么操作的,其实跟这个类似。

 

 

 

posted @ 2017-05-21 09:10  筑基2017  阅读(4520)  评论(2编辑  收藏  举报