第十章 CALL和RET指令
文章目录
章节内容
概述
主要介绍了CALL和RET两个转移指令,它们通过修改IP,或CS:IP使程序跳转。
二者常被共同使用以实现子程序的设计。
同时介绍了mul乘法指令,以及子程序模块设计相关问题和内容。
ret/retf
ret指令取栈中数据修改IP
- (IP)=((ss)*16+(sp))
- (sp)=(sp)+2
相当于:pop IP
retf指令取栈中的数据同时修改CS和IP
- (IP)=((ss)*16+(sp))
- (sp)=(sp)+2
- (CS)=((ss)*16+(sp))
- (sp)=(sp)+2
相当于:
pop IP
pop CS
call
call xxx(xxx为位置)
call指令执行时,进行两步操作
- 将当前 IP 或 CS和IP 入栈
- 转移到某位置
转移的地址可以是某标号的位置,可以是某寄存器中存的地址,也可以是某内存中存的位置。
- 为某标号时,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。
子程序设计时的问题
- 参数和返回值
- 寄存器冲突
单个参数一般用某寄存器存,若有多个参数,一般存在某段内存中,然后记录首地址传入子程序。
返回值一般也用寄存器存,注意分配和使用。
同时注意寄存器冲突的问题,如果子程序中用到的寄存器主程序也使用了,会产生冲突和不可预料的错误。
因为寄存器数量有限,一般在子程序开始时将子程序会用到的寄存器的值入栈,结束时再将其逆序出栈,这样不影响主程序中寄存器的使用。
实验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。确实这样就不会那么麻烦了,学到了学到了。