call 和 ret 都是转移指令,他们可以共同使用实现汇编中的中的子程序设计
call 指令
call 近转移
使用格式:call label
当执行call指令时,CPU可以认为执行2个操作:
- push ip + 3 (将call后的下一条指令的ip压入栈中)
- jmp near ptr label (跳转到label所在的ip)
call 远转移
当要进行段间转移时,使用 call far ptr 指令(在emu8086中无效,可以用’转移地址在内存中的call’代替),该指令会将当前的cs与ip先后压入栈中,并进行等效于 jmp far ptr 的跳转
转移地址在寄存器中的call指令
格式:call reg,在把当前IP压入栈中后跳转到16reg中的地址
转移地址在内存中的call指令
分为两类,近转移与远转移
call word ptr [Address]
call dword ptr [Address]
mov word ptr [0], func ;偏移地址 mov word ptr [2], code ;段地址 call dword ptr [0]
ret 与 retf 指令
与call指令对应的,ret与retf指令从栈中获得跳转地址进行跳转。ret指令对应call的近转移,从栈中弹出一个地址给ip。retf对应远转移,从栈中弹出两个元素分别修改ip与cs
ret 指令还可以与立即数一起使用,如 ret N 可以认为是:
- pop ip
- add sp, N
这样可以实现用栈来转递参数
汇编的模块化编程
使用call与ret可以实现对子程序的调用与返回
如以下子程序实现了计算ax的立方:
; 描述:计算N的立方根
; 参数:(ax) = N
; 结果:(bx ax) = N^3
cube:
push cx ; 保存cx中原来的元素
mov cx, ax ; 用cx保存ax
mul cx ; 计算N^2
mul cx ; 计算N^3
pop cx ; 恢复cx
ret ; 返回
这里使用了寄存器来转递参数,更常用的作法是使用栈来传递参数,如同样的程序可以改写为:
; 描述:计算N的立方根
; 参数: N = 栈中除IP外的第二个元素
; 返回:(bax ax) = N^3
cube2:
push bp ; 保存bp
mov bp, sp ; 将bp指向栈顶
mov ax, ss:[bp+4] ; 从栈中获得参数
mov bp, ax ; 计算N^3
mul bp
mul bp
pop bp ; 恢复bp
ret 2 ; 将栈顶指向参数前,并返回
调用时则:
push 2 call cube2
还可以用寄存器/栈转递地址,子程序从内存中获得数据(常用于批量数据的传递),以下程序使用这种方法在屏幕的指定位置打印内存中的数据:
; 描述:在屏幕的指定位置打印字符串
; 参数:起始位置 <- 栈中第5个元素
; 长度 <- 栈中第4个元素
; Y位置 <- 栈中第3个元素
; X位置 <- 栈中第2个元素
; 结果:无
print:
push ax ;中转
push bx ;字符串寻址
push cx ;控制循环
push si ;输出偏移地址
push es ;输出段地址
mov bx, sp
mov ax, ss:[bx+14] ;获得输出的偏移位置
mov cl, 158
mul cl
mov si, ax
mov ax, ss:[bx+12]
add si, ax
add si, ax
mov cx, ss:[bx+16] ;获得长度
mov bx, ss:[bx+18] ;获得字符串地址
mov ax, 0B800H
mov es, ax ;获得输出的段地址
print_loop:
mov al, [bx] ;获得字符
mov es:[si], al ;输出
add bx, 1
add si, 2
loop print_loop
pop es
pop si
pop cx
pop bx
pop ax
ret 8
还有要注意设计子程序时,要注意寄存器冲突(子程序修改了主程序要用的寄存器),解决方案一个时避免使用冲突的寄存器,另一个则是提前将寄存器的值压入栈中,子程序返回前再从栈中弹出
啦啦啦
评论都要审核?
原来不用审核呀