第十节:CALL和RET指令
retf和retf经常被用来实现子程序的设计
1.ret和retf:
ret指令用栈中的数据,修改IP的内容,从而实现近转移
CPU执行ret指令时,进行下面两步操作:
a.(IP)=((ss)*16+(sp))
b.(sp)=(sp)+2
汇编语言相当于进行 :pop IP
ret指令执行后,(IP)=0,CS:IP指向代码段的第一条指令
assume cs:code
stack segment
db 16 dup(0)
stack ends
code segment
mov ax,4c00h
int 21h
start:mov ax,stack
mov ss,ax
mov sp,16
mov ax,0
push ax
mov bx,0
ret
code ends
end start
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移
CPU执行retf指令时,进行下面四步操作:
a.(IP)=((ss)*16+(sp))
b.(sp)=(sp)+2
c.(CS)=((ss)*16+(sp))
d.(sp)=(sp)+2
汇编语言相当于进行:pop IP,pop cs
retf指令执行后,CS:IP指向代码段的第一条指令
assume cs:code
stack segment
db 16 dup(0)
stack ends
code segment
mov ax,4c00h
int 21h
start:mov ax,stack
mov ss,ax
mov sp,16
mov ax,0
push ax
mov bx,0
retf
code ends
end start
2.call指令:
CPU执行call指令时,进行两步操作:
a.将当前的IP或CS和IP压入栈中;
b.转移
call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同
3.依据位移进行转移的call指令:
call 标号(将当前的IP压栈后,转到标号处执行指令)
CPU执行此格式call指令时,进行如下操作:
a.(sp)=(sp)-2
b.((ss)*16+(sp))=(IP)
c.(IP)=(IP)+16位位移
16位位移=标号处的地址-call指令后的第一个字节的地址
16位位移的范围为-32768~32767,用补码表示
16位位移有编译程序在编译时算出
汇编语言相当于进行
push IP
jmp near ptr 标号
4.转移的目的地址在指令中call指令:
前面的call指令都是相对于当前IP的转移位移
call far ptr 标号 ;实现的段间转移
a.(sp)=(sp)-2
b.((ss)*16+(sp))=(CS)
c.(sp)=(sp)-2
d.((ss)*16+(sp))=(IP)
(CS)=标号所在段的段地址
(IP)=标号在段中的偏移地址
CPU执行call far ptr 标号 相当于进行:
push CS
push IP
jmp far ptr 标号
5.转移地址在寄存器中的call指令:
call 16位 reg
功能:
(sp)=(sp)-2
((ss)*16+(sp))=(IP)
(IP)=(16位reg)
CPU执行call 16位 reg时,相当于进行
push IP
jmp 16位 reg
6.转移地址在内存中的call指令:
a.call word ptr
内存单元地址
执行相当于:push IP /jmp word ptr
内存单元地址
b.call dword ptr 内存单元地址
执行相当于:push CS /push IP /jmp dword ptr
内存单元地址
7.call和ret的配合使用:
assume cs:code
code segment
start:mov ax,1
mov cx,3
call s
mov bx,ax
mov ax,4c00h
int 21h
s: add ax,ax
loop s
ret
code ends
end start
执行过程,cpu将call s指令的机器码读入,IP指向了call s 后的指令 mov bx,ax,然后cpu执行call s指令,将当前的IP值(指令 mov bx,ax的偏移地址)压栈,并将IP的值改变为标号S处的偏移地址,cpu从标号s处开始执行指令,loop循环完毕,(ax)=8;cpu将ret指令的机器码读入,IP指向了ret指令后的内存单元,然后cpu执行ret指令,从栈中弹出一个值(即call s 先前压入的mov bx,ax 指令的偏移地址)送入IP中,则CS:IP指向指令mov bx,ax;CPU从mov bx,ax开始执行指令,直至完成。
故 (bx)=8
从标号s到ret的程序段的作用是计算2的N次方。N的值由cx提供
从上面的讨论中可以发现,可以写一个具有一定功能的程序段,称其为子程序,在需要的时候,用call指令转去执行。
子程序的框架如下:
标号:
指令
ret
assume cs:code
code segment
main::
:
call sub1 ;调用子程序sub1
:
:
mov ax,4c00h
int 21h
sub1:: ;子程序sub1开始
:
call sub2 ;调用子程序sub2
:
:
ret ;子程序返回
sub2:: ;子程序sub2开始
:
:
ret ;子程序返回
code ends
end main
8.mul指令:
mul是乘法指令
a.两个相乘的数,要么都是8位,要么都是16位。
如果是8位,一个默认在AL中,另一个放在8位reg或内存字节单元中;
如果是16位,一个默认在AX中,另一个放在16位reg或内存字单元中;
b.结果:如果是8位乘法,结果默认放在AX中;
如果是16位乘法,结果高位默认在DX中存放,低位在AX中存放。
格式如下:
mul reg
mul 内存单元
内存单元可以用不同的寻址方式给出:比如:
mul byte ptr ds:[0] /含义 (ax)=(al)*((ds)*16+0)
mul word ptr [bx+si+8]
/含义:
(ax)=(ax)*((ds)*16+(bx)+(si)+8)结果的低16位
(dx)=(ax)*((ds)*16+(bx)+(si)+8)结果的高16位
eg:计算100*10000
100<255;可10000>255 必须进行16位乘法
mov ax,100
mov bx,10000
mul bx
结果(ax)=4240H,(dx)=000FH (F4240H=1000000)
9.模块化程序设计:
call和ret 指令共同支持了汇编语言编程中模块化设计,可以用简捷的方法,实现多个相互联系,功能独立的子程序来解决一个复杂的问题
10.参数和结果传递的问题:
子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者。其实,讨论参数和返回值传递的问题,实际上就是在探讨,如何存储子程序需要的参数和产生的返回值。
用寄存器来存储参数和结果是最常使用的方法,对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:调用者将参数送入参数寄存器,从结果寄存器中取得返回值;子程序从参数寄存器中取得参数,将返回值送入结果寄存器。
eg:计算data段中第一组数据的3次方,结果保存在后面一组dword单元中。
assume cs:code
data segment
dw 1,2,3,4,5,6,7,8
dd 0,0,0,0,0,0,0,0
data ends
code segment
start:mov ax,data
mov ds,ax
mov si,0 ;ds:si指向第一组word单元
mov di,16 ;ds:di指向第二组dword单元
mov cx,8
s : mov bx,[si]
call cube
mov [di],ax
mov [di].2,dx
add si,2 ;ds:si指向下一个word单元
add di,4 ;ds:di指向下一个dword单元
loop s
mov ax,4c00h
mov int 21h
cube:mov ax,bx
mul bx
mul bx
ret
code ends
end start
11.批量数据的传递:
上例cube只有一个参数,放在bx中,如果由多个参数,该如何存放?
这个将批量数据放在内存中,然后将它们所在的内存空间的首地址放在寄存器中,传递给需要的子程。
eg:将一个全是字母的字符串转化为大写。
assume cs:code
data segment
db 'conversation'
data ends
code segment
start:mov ax,data
mov ds,ax
mov si,0 ;ds:si指向字符串(批量数据)所在空间的首地址
mov cx,12 ;cx存放字符串的长度
call capital
mov ax,4c00h
int 21h
capital:and byte ptr [si],11011111b
inc si
loop capital
ret
code ends
end start
12.寄存器冲突的问题:
eg:将一个全是字母,以0结尾的字符串,转化为大写;
注意:在编写调用子程序的程序,注意看看子程序中有没有用到会产生冲突的寄存器。
在编写子程序的时候,不要使用会产生冲突的寄存器
*错误程序:
code segment
assume cs:code
data segment
db 'word',0
db 'unix',0
db 'wind',0
db 'good',0
data ends
code segment
start:mov ax,data
mov ds,ax
mov bx,0
mov cx,4
s:mov si,bx
call capital
add bx,5
loop s
mov ax,4c00h
int 21h
capital: mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short capital
ok:ret
code ends
end start
错误:问题在于cx的使用,主程序要使用cx记录循环次数,可是子程序中也使用了cx,在执行子程序的时候,cx中保存的循环计数值被改变,使得主程序的循环出错。
如何解决?
在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复。可以用栈保存寄存器中的内容。
code segment
assume cs:code
data segment
db 'word',0
db 'unix',0
db 'wind',0
db 'good',0
data ends
code segment
start:mov ax,data
mov ds,ax
mov bx,0
mov cx,4
s:mov si,bx
call capital
add bx,5
loop s
mov ax,4c00h
int 21h
capital: push cx
push si
change: mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change
ok: pop si ;注意寄存器入栈出栈顺序
pop cx
ret
code ends
end start
·