《汇编语言(第四版)》—王爽 第十章call和ret指令
10.1、ret和retf
call和ret指令都是转移指令,它们都是修改IP,或者同时修改CS和IP
ret指令用栈中的数据,修改IP的内容,从而实现近转移
CPU执行ret指令是执行以下操作:
- (ip) = ((ss) * 16 + (sp)) ;指向栈顶
- (sp) = (sp) + 2
retf指令用于修改cs和ip的内容,从而实现远转移
CPU在执行retf指令时,进行下面的操作:
- (ip) = ((ss) * 16 + (sp))
- (sp) = (sp) + 2
- (cs) = ((ss) * 16 + (sp))
- (sp) = (sp) + 2
可以看出我们用汇编语言来表示ret和retf的时候,就相当于是
ret:pop ip
retf:pop ip pop cs
10.2、call指令
call指令经常和ret指令配合使用,因此CPU之心call指令,进行两部操作
- 将ip或者cs ip压入栈中
- 转移(jmp)
call指令不能实现短转移,除此之外,call指令实现转移的方式和jmp指令的原理相同
10.3、依据位移进行转移的call指令
call 标号(将IP压入栈中,然后执行跳转)
- (sp) = (sp) -2
- ((ss) * 16 + (sp)) = (ip)
- (ip) = (ip) + 16位位移
call 标号
16位位移 = “标号”处的地址 - call指令后的第一个字节的地址、
范围为:-32768~32767
16位的位移由编译程序在 编译时算出
10.4、转移的目的地址在指令中的call指令
前面讲解的call指令,其对应的机器码指令中并没有转移的目的地址,而是相当于当前IP的转移位移
指令call far ptr 标号实现的是段间的转移
call far ptr 标号 CPU在执行的时候的操纵:
- (sp) = (sp) - 2
- ((ss) * 16 + (sp)) = (cs)
- (sp) = (sp) - 2
- ((ss) * 16 + (sp)) = (IP)
- (cs) = 标号所在的段地址
- (ip) = 标号所在的偏移地址
((ss) * 16 + (sp)) = (cs)在执行的时候相当于是进行了:
push CS
push IP
jmp far ptr 标号
10.5、转移地址在寄存器中的call指令
指令格式:call 16位寄存器
功能:
- (sp) = (sp) - 2
- ((ss) * 16 + (sp)) = (IP)
- ip = (16位寄存器)
call 16位寄存器的执行就相当于是
push IP
jmp 16位寄存器
10.6、转移地址在内存单元中的call指令有两种格式
call word ptr 内存单元地址
在8086CPU中实现的是段内的短转移
- push ip
- jmp word ptr 内存单元地址
mov sp,10H
mov ax,0123H
mov ds:[0],ax
call word ptr ds:[0]
执行之后(IP) = 0123H,(sp) = 0Eh
call dword ptr 内存单元地址
在8086CPU中实现的是段间转移
- push cs
- push ip
- jmp dword ptr 内存单元地址
mov sp,10H
mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[2],0
call dword ptr ds:[0]
执行之后(cs) = 0;(ip) =0123H ;(sp) =
默认段地址放在高位,偏移地址放在低位
10.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指令,从栈中他拿出一个值即之前压入栈中的IP的数字,将这个数值送入到IP中,然后继续执行指令mov bx,ax
- CPU从mov bx,ax开始执行,直到指令结束为止
从上面的讨论中我们发现,可以写一个具有一定功能的程序段,我们称其为子程序,在需要的时候,用call 指令转去执行。可是执行完子程序后,如何让CPU 接着call 指令向下执行? call 指令转去执行子程序之前,call 指令后面的指令的地址将存储在栈中,所以可在子程序的后面使用ret 指令,用栈中的数据设置IP的值,从而转到 call 指令后面的代码处继续执行。
10.8、mul指令
mul是乘法指令使用mul做乘法的时候
- 相乘的两个数,要么都是八位要么都是十六位
- 8位:AL中和8位寄存器或者内存单元
- 16位:AX中和16位寄存器或者内存单元中
- 结果:
- 8位:AX中
- 16位:DX(高位)和AX(低位)中
- 如果是用8位乘以16位
- 就是将8位强制转化为16位,高位补0即可
- 格式如下:
- 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))结果的低8位
- (dx) = (ax) * ((ds) * 16 + (bx) + (si) + (8))结果的高8位
mul使用举例:
-
计算100 * 10
-
分析:两个数据都没有超过255,因此使用的是8位的乘法
-
mov al,100 mov bl,10 mul bl
-
-
计算100 * 10000
-
分析:有一个数据超过了255,则此时需要使用16位的乘法,所以需要将100这个数字转化为16位
-
mov ax,100 mov bx,10000 mov bx
-
10.9、模块化程序设计
在之前的学习中,我们看到了,call与ret指令共同支持了汇编语言编程中的模块化设计,在实际的编程中,程序的模块化设计是必不可少的
因为现实的问题比较复杂,对现实问题进行分析时,把它转化成为相互联系、不同层次的子问题,是必须的解决方法。
而 call 与ret 指令对这种分析方法提供了程序实现上的支持。利用call 和 ret 指令,我们可以用简捷的方法,实现多个相互联系、功能独立的子程序来解决一个复杂的问题。
10.10、参数和结果传递的问题
子程序一般都是根据提供的参数处理一定的事务,处理之后,将结果提供给调用者
其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。
我们设计一个子程序,可以根据提供的N,来计算N的3次方:
存在的问题:
- 我们将参数N存储在什么地方
- 计算得到的数值,我们存储在什么地方
注意,我们在编程的时候要注意形成良好的风格,对于程序应有详细的注释。子程序的注释信息应该包含对子程序的功能、参数和结果的说明。
10.11、批量数据的传递
编程将data段中的字符全部由变为大写字母
assume cs:code
data segment
db 'conversation'
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
mov cx,12
call capital
mov ax,4C00H
int 21H
capital:
and byte ptr [si],11011111b
inc si
loop capital
ret
code ends
end start
除了寄存器、内存传递参数外,还有一种通用的方法使用栈来传递参数。
10.12、寄存器冲突问题
设计一个子程序:将一个全是字母的以0为结尾的字符串,转化为大写
分析:这个子程序,字符串的内容后面是一个0,标记着字符串的结束,子程序可以读取每个字符进行,如果不是0则进行转换,如果是0则结束处理
由于可以通过检测0而知道是否已经处理完整的字符串,所以子程序可以不需要字符串的长度作为参数,我们可以直接用JCXZ来检测0