汇编语言(第3版) 第10章相关内容及实验10和课程设计1

第十章 CALL和RET指令

章节内容

概述

主要介绍了CALL和RET两个转移指令,它们通过修改IP,或CS:IP使程序跳转。

二者常被共同使用以实现子程序的设计。

同时介绍了mul乘法指令,以及子程序模块设计相关问题和内容。

ret/retf

ret指令取栈中数据修改IP

  1. (IP)=((ss)*16+(sp))
  2. (sp)=(sp)+2

相当于:pop IP

retf指令取栈中的数据同时修改CS和IP

  1. (IP)=((ss)*16+(sp))
  2. (sp)=(sp)+2
  3. (CS)=((ss)*16+(sp))
  4. (sp)=(sp)+2

相当于:
pop IP
pop CS

call

call xxx(xxx为位置)

call指令执行时,进行两步操作

  1. 将当前 IP 或 CS和IP 入栈
  2. 转移到某位置

转移的地址可以是某标号的位置,可以是某寄存器中存的地址,也可以是某内存中存的位置。

  • 为某标号时,call 标号,修改IP近转移;call far ptr 标号,修改CS和IP段间转移。
  • 为寄存器时,call 16位reg,修改CS和IP,转移到相应位置。
  • 为内存时,call word ptr 内存地址,修改IP转移;call dword ptr 内存地址,修改CS和IP转移。
ret和call配合

用于设计子程序。

例如:


​xxxxxx                    ;主程序代码
​xxx 
​xxxxx
​call zichengxu       ;当前IP或CS和IP入栈,转移到zichengxu子程序处
​xxxxxx
​………………
​………………
​zichengxu :            ;子程序代码
​		xxxxx
​		xxx
​		xxxxx
ret                           ;读取栈中IP,转移回主程序
​………………
mul

乘法指令

格式: mul reg

​ mul 内存单元

两乘数需同为8位或同位16位。

8位一个存在al中,一个放在其他8位reg或者内存单元中。

16位一个存在ax中,一个放在其他16位reg或者内存单元中。

结果:若是8位相乘,积存放在AX中;若是16位相乘,积高位存在DX,低位存在AX。

子程序设计时的问题
  1. 参数和返回值
  2. 寄存器冲突

单个参数一般用某寄存器存,若有多个参数,一般存在某段内存中,然后记录首地址传入子程序。

返回值一般也用寄存器存,注意分配和使用。

同时注意寄存器冲突的问题,如果子程序中用到的寄存器主程序也使用了,会产生冲突和不可预料的错误。

因为寄存器数量有限,一般在子程序开始时将子程序会用到的寄存器的值入栈,结束时再将其逆序出栈,这样不影响主程序中寄存器的使用。

实验10 编写子程序

编写下列三个子程序

  • 显示字符串
  • 解决除法溢出
  • 数值显示
显示字符串

把字符串显示在屏幕相应位置

名称:show_str

功能:指定行号列号和颜色,在屏幕相应位置输出相应以0结尾的字符串。

参数:(dh)=行号(0~24);(dl)=列号(0~79);(cl)=颜色信息;ds:si指向字符串首地址。

返回:无

不是很难,首先计算相应行列在显存中的位置:b8000h+dh*160+dl*2

然后逐字符复制内容和颜色就行了。

注意寄存器占用冲突问题,用栈和不用的寄存器处理就好。

代码:

assume cs:code
data segment
        db 'Welcome to masm!',0
data ends
stack segment
        db 16 dup (0)
stack ends
code segment
start:
		;行号列号颜色字符串首地址
        mov dh,8
        mov dl,3
        mov cl,2
        mov ax,data
        mov ds,ax
        mov si,0
		;初始化栈
        mov ax,stack
        mov ss,ax
        mov sp,16
		;执行
        call show_str
        mov ax,4c00h
        int 21h
        
show_str:
        ;入栈
        push ax
        push bx
        push cx
        push dx
        push si
        push es
        ;子程序主体
        ;起始地址为:b8000h+dh*160+dl*2
        mov ax,0b800h
        mov es,ax
        mov al,160
        mul dh
        mov bl,dl
        mov bh,0
        add bx,bx
        add bx,ax
        mov dl,cl
        ;复制字符串及颜色信息
        start_copy:
                mov cl,[si]
                mov ch,0
                jcxz copy_ok
                mov es:[bx],cl
                mov es:[bx+1],dl
                inc si
                add bx,2
                jmp start_copy
        copy_ok:
        ;出栈
        pop es
        pop si
        pop dx
        pop cx
        pop bx
        pop ax

        ret
code ends
end start

解决除法溢出

在做类似:11000H/1

的除法时,若使用div命令,会产生除法溢出(结果大于16位,ax寄存器存不下)。

所以设计子程序divdw,用于处理这类除法。

名称:divdw

功能:做一个不会溢出的除法运算,被除数dword型,除数word型,结果为dword型

参数:(ax)=dword型数据的低16位;(dx)=dword型数据的高16位;(cx)=除数。

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

关键思路是将会溢出的除法转换成多个不会溢出的除法,程序代码不难,理解这个转换比较关键。

给出一个公式:

X:被除数,范围:[0,,FFFFFFFF]

N:除数,范围:[0,FFFF]

H:X的高16位,范围:[0,FFFF]

L :X的低16位,范围:[0,FFFF]

int():描述性运算符,取商,比如,int(38/10)=3

rem():描述性运算符,取余数,比如,rem(38/10)=8

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

理解这个公式就好做了。

代码:

assume cs:code
stack segment
        db 16 dup (0)
stack ends
code segment
start:
		;初始化栈
        mov ax,stack
        mov ss,ax
        mov sp,16
        ;除数和被除数
        mov ax,4240h
        mov dx,000fh
        mov cx,0ah
        ;执行
        call divdw

        mov ax,4c00h
        int 21h

divdw:
		;这里除了bx,其他ax,cx,dx,值都是要改变的,所以入栈bx
		push bx
        ;高16位除除数,商在ax,余数在dx
        mov bx,ax
        mov ax,dx
        mov dx,0
        div cx
        ;高16位除的余数加上低16位除除数,把之前的商转存到bx中,新的商在ax,余数在dx
        push ax
        mov ax,bx
        pop bx
        div cx
        ;结果的高16位在dx,低16位在ax,余数在cx
        mov cx,dx
        mov dx,bx
		
		pop bx
        ret
code ends
end start

结果:

在这里插入图片描述

数值显示

将2进制(16进制)数据转换成十进制,并以字符串的形式显示出来。(调用show_str子程序)

名称:dtoc

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

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

返回:无

首先要设计将数据转换成十进制字符串的子程序,然后调用show_str将其显示在屏幕。

转换成十进制,通过将原数不断除10,得到余数,所有余数逆序之后,即为十进制每一位。

然后每一位数+30H即为对应ASCII码字符串

对于一个数要除多少次,事先并不确定,可以通过jcxz处理循环结束。

代码:

assume cs:code
data segment
        db 10 dup (0)
data ends
stack segment    ;考虑到要用栈存寄存器的值和字符串的值,这里栈给了稍微大一点
        db 128 dup (0)
stack ends
code segment
start:
		;初始化栈
        mov ax,stack
        mov ss,ax
        mov sp,128
		;把ax的值转换成字符串存到data中
        mov ax,12666
        mov bx,data
        mov si,0
        call dtoc
		;调用show_str把data中数据显示在相应位置
        mov dh,8
        mov dl,3
        mov cl,2
        call show_str

        mov ax,4c00h
        int 21h
show_str:
        ;此处略去,具体见上文
dtoc:
		;寄存器入栈
        push di
        push ax
        push bx
        push cx
        push dx
        push ds
        push si
		;di记录转换后字符串长度(不包括末尾0)
        mov di,0
        ;开始转换
        dtoc_start:
                mov dx,0
                mov bx,10
                div bx
                ;除完先存
                inc di
                add dx,30H
                push dx
                ;再判断商为零就结束
                mov cx,ax
                jcxz dtoc_ok
                jmp short dtoc_start
        dtoc_ok:
        		;利用栈后入先出的特点,存的时候是逆序,取的时候就变回正序了
                mov cx,di
                s:
                pop ax
                mov [si],ax
                inc si
                loop s
                ;添加末尾结束符0
                mov [si],0
		;寄存器出栈
        pop si
        pop ds
        pop dx
        pop cx
        pop bx
        pop ax
        pop di

        ret
code ends
end start

结果:
在这里插入图片描述

程序的不足:

虽然程序可以正常运行,也能正常显示,但存在一些问题。

若要显示的数据大于65535,则需要用ax和dx两个寄存器存参数。

同时大数据也存在除法溢出的可能,所以应当调用之前的divdw子程序解决。

课程设计1

任务

将实验7中的Power idea公司的数据按照图所示的格式在屏幕上显示。

在这里插入图片描述

实验7:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

给出我在实验7中的代码

assume cs:codesg
data segment
		;表示21年的21个字符串
        db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
        db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
        db '1993','1994','1995'
		;表示21年公司收入的21个dword型数据
        dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
        dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
		;表示21年公司雇员人数的21个word型数据
        dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
        dw 11542,14430,15257,17800
data ends

table segment
		;表示以'year summ ne ?? '格式存入数据
        db 21 dup ('year summ ne ?? ')
table ends

stack segment
        db 16 dup (0)
stack ends

codesg segment
        start:
        ;初始化栈
        mov ax,stack
        mov ss,ax
        mov sp,16
		;初始化data段
        mov ax,data
        mov es,ax
        mov bp,0
        mov di,168
		;初始化table段
        mov ax,table
        mov ds,ax
        mov bx,0
        ;设置循环次数,21年共21次
        mov cx,21

        s:
        		;存cx的值
                push cx
				;年份
                mov word ptr ax,es:[bp+0]
                mov word ptr [bx+0],ax
                mov word ptr ax,es:[bp+2]
                mov word ptr [bx+2],ax
				;空格
                mov byte ptr [bx+4],' '
                ;收入      
                mov word ptr ax,es:[bp+84]
                mov word ptr [bx+5],ax
                mov word ptr ax,es:[bp+86]
                mov word ptr [bx+7],ax
				;空格
                mov byte ptr [bx+9],' '
				;雇员数
                mov word ptr ax,es:[di]
                mov word ptr [bx+10],ax
				;空格
                mov byte ptr [bx+12],' '
				;人均收入
                mov word ptr ax,[bx+5]
                mov word ptr dx,[bx+7]
                mov word ptr cx,[bx+10]
                div word ptr cx
                mov word ptr [bx+13],ax
				;空格
                mov byte ptr [bx+15],' '
				;取cx的值,并且继续循环
                pop cx
                add bx,10h
                add bp,4
                add di,2
        loop s

        mov ax,4c00h
        int 21h

        codesg ends
end start



分析

梳理一下,现在我们应该在 假定table段中已经按照格式存入了所有数据 的基础上,将它们显示在屏幕上。

也就是说,我们可以将实验七的程序改写成子程序,先执行该子程序,使得table段充满数据。

再利用我们之前写的三个子程序将其显示在屏幕上。

这里需要将dtoc子程序改写一下使得可以处理dword类型的数据到字符串的转化,其中用上divdw处理除法溢出。

因为我们已经将data段中的数据存到了table段中,所以我们可以利用data段作为暂时存放转换后字符串的地方。

最后用show_str将其按照格式逐行逐列显示在屏幕上。

这里新的dtoc

名称:dtoc

功能:将dword型数转变为表示十进制数的字符串,并以0为结束符。

参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,ds:si指向字符串的首地址。

返回:无

代码

最后程序如下(注意,dtoc子程序与之前的不同之处):

assume cs:code
data segment
	;表示21年的21个字符串
        db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
        db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
        db '1993','1994','1995'
	;表示21年公司收入的21个dword型数据
        dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
        dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
	;表示21年公司雇员人数的21个word型数据
        dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
        dw 11542,14430,15257,17800
data ends

table segment
	;表示以'year summ ne ?? '格式存入数据
        db 21 dup ('year summ ne ?? ')
table ends


stack segment
        ;栈开大一点点
        db 128 dup (0)
stack ends

code segment
start:
	;初始化栈
        mov ax,stack
        mov ss,ax
        mov sp,128

        call set_table
        ;执行完后,table段应该已经按照'year summ ne ?? '的格式存入了21组数据


        mov cx,21
        mov ax,table
        mov ds,ax
        mov si,0
        mov dh,4
        
        s:
                push cx
                ;颜色
                mov cl,7
                mov ch,0
                ;在第0列显示年份
                mov dl,0
                ;年份,是字符串,直接显示
                call show_str
                ;在第10列显示收入
                add dl,10
                ;因为下面dtoc要用到dx,所以先用bx存起来(行号和列号在dh和dl里)
                mov bx,dx
                ;ax和dx接收数据
                mov ax,[si+5]
                mov dx,[si+7]
                ;虽然比较乱。。但是用栈寄存一下ds,si,bx的数据
                push ds
                push si
                push bx
                ;这里data段的数据已经不需要了,所以用来暂存要输出的字符串
                mov bx,data 
                mov ds,bx
                mov si,0
                ;dtoc
                call dtoc
                ;把行号列号从bx中取回
                pop bx
                mov dx,bx
                ;show_str
                call show_str
                ;把si和ds取回
                pop si
                pop ds
                ;在第20列显示雇员数
                add dl,10
                ;接下来就和收入那段差不多了,
                ;其实为了方便可以设计成子程序,
                ;此处还是用了较为麻烦的方法重复。
                ;存行号和列号
                mov bx,dx
                ;ax和dx接收数据
                mov ax,[si+10]
                mov dx,0
                ;寄存ds,si,bx的数据
                push ds
                push si
                push bx
                ;暂存要输出的字符串
                mov bx,data 
                mov ds,bx
                mov si,0
                ;dtoc
                call dtoc
                ;把行号列号从bx中取回
                pop bx
                mov dx,bx
                ;show_str
                call show_str
                ;把si和ds取回
                pop si
                pop ds
                 ;在第30列显示平均收入
                add dl,10
                ;同上重复
                ;存行号和列号
                mov bx,dx
                ;ax和dx接收数据
                mov ax,[si+13]
                mov dx,0
                ;寄存ds,si,bx的数据
                push ds
                push si
                push bx
                ;暂存要输出的字符串
                mov bx,data 
                mov ds,bx
                mov si,0
                ;dtoc
                call dtoc
                ;把行号列号从bx中取回
                pop bx
                mov dx,bx
                ;show_str
                call show_str
                ;把si和ds取回
                pop si
                pop ds
                ;行+1
                inc dh
                add si,16
                pop cx
        loop s

       ; 测试  
       ; mov ax,12666
       ; mov dx,0
       ; mov bx,data
       ; mov si,0
       ; call dtoc

       ; mov dh,8
       ; mov dl,3
       ; mov cl,2
       ; call show_str

        mov ax,4c00h
        int 21h

set_table:
        
        ;寄存器入栈
        push ax
        push es
        push bp
        push di
        push ds
        push bx
        push cx

	;初始化data段
        mov ax,data
        mov es,ax
        mov bp,0
        mov di,168
	;初始化table段
        mov ax,table
        mov ds,ax
        mov bx,0
        ;设置循环次数,21年共21次
        mov cx,21

        set_table_start:
        	;存cx的值
                push cx
		;年份
                mov word ptr ax,es:[bp+0]
                mov word ptr [bx+0],ax
                mov word ptr ax,es:[bp+2]
                mov word ptr [bx+2],ax
		;空格;这第一个空格改成0,方便直接显示字符串
                mov byte ptr [bx+4],0
                ;收入      
                mov word ptr ax,es:[bp+84]
                mov word ptr [bx+5],ax
                mov word ptr ax,es:[bp+86]
                mov word ptr [bx+7],ax
		;空格
                mov byte ptr [bx+9],' '
		;雇员数
                mov word ptr ax,es:[di]
                mov word ptr [bx+10],ax
		;空格
                mov byte ptr [bx+12],' '
		;人均收入
                mov word ptr ax,[bx+5]
                mov word ptr dx,[bx+7]
                mov word ptr cx,[bx+10]
                div word ptr cx
                mov word ptr [bx+13],ax
		;空格
                mov byte ptr [bx+15],' '
		;取cx的值,并且继续循环
                pop cx
                add bx,10h
                add bp,4
                add di,2
        loop set_table_start


        ;寄存器出栈
        pop cx
        pop bx
        pop ds
        pop di
        pop bp
        pop es
        pop ax
        ret

divdw:
        ;这里除了bx,其他ax,cx,dx,值都是要改变的,所以入栈bx
	push bx
        ;高16位除除数,商在ax,余数在dx
        mov bx,ax
        mov ax,dx
        mov dx,0
        div cx
        ;高16位除的余数加上低16位除除数,把之前的商转存到bx中,新的商在ax,余数在dx
        push ax
        mov ax,bx
        pop bx
        div cx
        ;结果的高16位在dx,低16位在ax,余数在cx
        mov cx,dx
        mov dx,bx

        pop bx
        ret
show_str:
        ;入栈
        push ax
        push bx
        push cx
        push dx
        push si
        push es
        ;子程序主体
        ;起始地址为:b8000h+dh*160+dl*2
        mov ax,0b800h
        mov es,ax
        mov al,160
        mul dh
        mov bl,dl
        mov bh,0
        add bx,bx
        add bx,ax
        mov dl,cl
        ;复制字符串及颜色信息
        start_copy:
                mov cl,[si]
                mov ch,0
                jcxz copy_ok
                mov es:[bx],cl
                mov es:[bx+1],dl
                inc si
                add bx,2
                jmp start_copy
        copy_ok:
        ;出栈
        pop es
        pop si
        pop dx
        pop cx
        pop bx
        pop ax

        ret

dtoc:
        push di
        push ax
        push bx
        push cx
        push dx
        push ds
        push si

        mov di,0
        dtoc_start:
                mov cx,10
                call divdw
                inc di
                add cx,30H
                push cx
                ;这里需要判断dx和ax是否均为0,均为0才表示除完了,利用or指令
                mov cx,dx
                or cx,ax
                jcxz dtoc_ok
                jmp short dtoc_start
        dtoc_ok:
                mov cx,di
                cun:
                pop ax
                mov [si],ax
                inc si
                loop cun
                mov [si],0

        pop si
        pop ds
        pop dx
        pop cx
        pop bx
        pop ax
        pop di

        ret

code ends
end start

结果

如图:

在这里插入图片描述

总结

原来的三个子程序,table段的子程序,修改的dtoc子程序都没有什么大问题,但是最后主程序代码过长,且有大量重复段。

其原因是本可以多写一个子程序或者用循环处理最后数据按格式在屏幕上的显示,因为偷懒嫌麻烦没有处理,而是直接复制粘贴相似段代码。

这个程序也可以不采用实验7的代码,不将数据按格式存到table段,直接存到显存相应位置。

从某些角度,那样可能会比现在的代码更清晰简洁,现在的反而绕了一些弯。

最后,写汇编代码一定要注意寄存器冲突的问题,处理好内存和寄存器,才不容易出错。


上课听了听老师的方法,直接将table储存格式修改,dtoc直接在table段存储转换后的字符串,直接整个字符串show_str。确实这样就不会那么麻烦了,学到了学到了。

猜你喜欢

转载自blog.csdn.net/weixin_42279809/article/details/83510829