retcall都是转移指令,都是修改IP的值,或同时修改CS和IP。它们经常被一起用来实现子程序的设计。

ret和retf

ret指令用栈中的数据修改IP,实现的是近转移;
retf指令用栈中的数据修改CS和IP的值,实现远转移。
格式:直接用 ret

ret指令执行时,cpu进行2步操作:

  1. 1. ip = ss * 16 + sp
  2. 2. sp = sp + 2

retf指令执行时,cpu进行4步操作:

  1. 1. ip = ss * 16 + sp
  2. 2. sp = sp + 2
  3. 3. cs = ss * 16 + sp
  4. 4. sp = sp + 2

可以看出,ret相当于汇编指令的

  1. pop IP

retf相当于

  1. pop IP
  2. pop CS

call指令

call指令也是一个转移指令,执行格式:call 目标(具体使用接下来说明),call的执行步骤:

  1. 将当前的IP或CS和IP入栈
  2. 转移

call不能实现短转移,但它实现转移的原理和jmp相同。

根据位移转移:call 标号,近转移,16位转移范围,也是使用相对的转移地址。

执行步骤:

  1. (SP)=(SP)-2
  2. ((SS)*16+(SP))=(IP)
  3. (IP)=(IP)+16

所以执行这条命令相当于执行

  1. push ip
  2. jmp near ptr 标号

直接使用地址进行(远)转移:call far ptr 标号,执行步骤:

  1. (SP)=(SP)-2
  2. ((SS)*16+(SP))=(CS)
  3. (SP)=(SP)-2
  4. ((SS)*16+(SP))=(IP)
  5. (CS)=标号所在的段的段地址
  6. (IP)=标号的偏移地址

所以执行call far ptr 标号相当于执行

  1. push cs
  2. push ip
  3. jmp far ptr 标号

使用寄存器的值作为call的跳转地址:call 16位reg

  1. (SP)=(SP)-2
  2. ((SS)*16+(SP))=(IP)
  3. (IP)=(16为reg)

相当于执行

  1. push ip
  2. jmp 16reg

使用内存中的值作为call的跳转地址:call word ptr 内存单元地址,当然还有call dword ptr 内存单元地址,这样进行的就是远转移。

联合使用ret和call

联合使用ret和call实现子程序的框架:

  1. assume cs:code
  2. code segment
  3. main:
  4. ···
  5. call sub1
  6. ···
  7. mov ax,4c00h
  8. int 21h
  9. sub1:
  10. ···
  11. call sub2
  12. ···
  13. ret
  14. sub2:
  15. ···
  16. ret
  17. code ends
  18. end main

mul指令

mul是乘法指令,使用时应注意,两个相乘的数,要么都是8位,要么都是16位,如果是8位,那么其中一个默认放在al中,另一个在一个8位reg或字节内存单元中;若是16位,则一个默认在ax中,另一个在16位reg或字内存单元中。如果是8位乘法, 则结果放在ax中,结果是16位;若是16位乘法,结果默认在ax和dx中,dx高位,ax低位,共32位。

格式:mul regmul 内存单元,支持内存单元的各种寻址方式。

mul word ptr [bx+si+8]代表:

  1. (ax)=(ax)*((ds)*16+(bx)+(si)+8)低16
  2. (dx)=(ax)*((ds)*16+(bx)+(si)+8)高16

例:计算100*10

  1. mov al,100
  2. mov bl,10
  3. mul bl

参数的传递和模块化编程

看下面一段程序:计算data中第一行的数的立方存在第二行

  1. assume cs:code
  2. data segment
  3. dw 1,2,3,4,5,6,7,8
  4. dd 0,0,0,0,0,0,0,0
  5. data ends
  6. code segment
  7. start:mov ax,data
  8. mov ds,ax
  9. mov si,0
  10. mov di,16
  11. mov cs,8
  12. s:mov bx,[si]
  13. call cube
  14. mov [di],ax
  15. mov [di].2,dx
  16. add si,2
  17. add di,4
  18. loop s
  19. mov ax,4c00h
  20. int 21h
  21. cube:mov ax,bx
  22. mul bx
  23. mul bx
  24. ret
  25. code ends
  26. end start

寄存器冲突

观察下面将data中的数据全转化为大写的代码:

  1. assume cs:code
  2. data segment
  3. db 'word',0
  4. db 'unix',0
  5. db 'wind',0
  6. db 'good',0
  7. data ends
  8. code segment
  9. start:mov ax,data
  10. mov ds,ax
  11. mov bx,0
  12. mov cx,4
  13. s:mov si,bx
  14. call capital
  15. add bx,5
  16. loop s
  17. mov ax,4c00h
  18. int 21h
  19. capital:mov cl,[si]
  20. mov ch,0
  21. jcxz ok
  22. and byte ptr [si],11011111b
  23. inc si
  24. jmp short capital
  25. ok:ret
  26. code ends
  27. end start

这段代码有一个问题出在,主函数部分使用cx设置循环次数4次,在循环中调用了子函数,而子函数中有一个判断语句jcxz也是用了cx,并且在之前修改了cx的值,造成逻辑错误。虽然修改的方法有很多,但我们应遵循以下的标准:

  • 编写调用子程序的程序不必关心子程序使用了什么寄存器
  • 编写子程序不用关心调用子程序的程序使用了什么寄存器
  • 不会发生寄存器冲突

针对这三点,我们可以如下修改代码:

  1. ···
  2. capital:push cx
  3. push si
  4. change:mov cl,[si]
  5. mov ch,0
  6. jcxz ok
  7. and byte ptr [si],11011111b
  8. inc si
  9. jmp short change
  10. ok:pop si
  11. pop cx
  12. ret
  13. ···

虽然和上面的程序中没有冲突的是si,但我们保险起见,在子程序开始时将子程序用到的所有的寄存器的内容存入栈中,在返回之前在出栈回到相应寄存器中。这样无论调用子程序的程序使用了什么寄存器,都不会产生寄存器冲突。