学习参考:汇编语言(第2版)王爽
- ret 和 retf
- call 指令
- 依据位移进行转移的 call 指令
- 转移的目的地址在指令中的 call 指令
- 转移地址在内存中的 call 指令
- call 和 ret 的配合使用
- mul 指令
- 参数和结果传递问题
- 批量数据的传递
- 寄存器冲突问题
call 和 ret 指令都是转移指令,它们都是修改 IP,或同时修改 CS 和 IP。它们经常被共同用来实现子程序设计
ret 和 retf
ret 指令用栈中的数据,修改 IP 的内容,从而实现近转移
retf 指令用栈中的数据,修改 CS 和 IP 的内容,从而实现远转移。
CPU 执行 ret 指令时,进行下面两步操作
- (IP)=((ss)*16+(sp))
- (sp)=(sp)+2
相当于
1 | pop IP |
CPU 执行 retf 指令时,进行下面 4 步操作
- (IP)=((ss)*16+(sp))
- (sp)=(sp)+2
- (CS)=((ss)*16+(sp))
- (sp)=(sp)+2
相当于
1 | pop IP |
call 指令
CPU 执行 call 指令时,进行两步操作:
- 将当前的 IP 或 CS 和 IP 压入栈中
- 转移
call 指令不能实现短转移,除此之外,call 指令实现转移的方法和 jmp 指令的原理相同
依据位移进行转移的 call 指令
call 标号(将当前的IP压栈后,转到标号处执行指令)
CPU 执行此种格式的 call 指令时,进行如下的操作:
- (sp)=(sp)-2 ; ((ss)*16+(sp))=(IP)
- (IP)=(IP)+16位位移。
16 位位移 = 标号处的地址 - call 指令后的第一个字节的地址;
16 位位移的范围为 -32768-32767,用补码表示;
16 位位移由编译程序在编译时算出。
从上面的描述中,可以看出,如果我们用汇编语法来解释此种格式的call 指令,则:
CPU执行 call 标号
时,相当于进行:
1 | push IP |
转移的目的地址在指令中的 call 指令
call far ptr 标号
实现的是段间转移
(sp)=(sp)-2
((ss)*16+(sp))=(CS)
(sp)-(sp)-2
((ss)*16+(sp))=(IP)(CS)=标号所在段的段地址
(IP)=标号在段中的偏移地址
从上面的描述中可以看出,如果我们用汇编语法来解释此种格式的 call 指令,则 CPU执行 call far ptr 标号
时,相当于进行:
1 | push CS |
转移地址在内存中的 call 指令
转移地址在内存中的 call 指令有两种格式
call word ptr 内存单元地址
CPU 执行此种格式的 call 指令,相当于进行:1
2push IP
jmp word ptr 内存单元地址call dword ptr 内存单元地址
CPU 执行此种格式的 call 指令,相当于进行:1
2
3push CS
push IP
jmp dword ptr 内存单元地址
call 和 ret 的配合使用
可以写一个具有一定功能的程序段,称其为子程序,在需要的时候,用 call 指令转去执行。call 指令转去执行子程序之前,call 指令后面的指令的地址将存储在栈中,所以可在子程序的后面使用 ret 指令,用栈中的数据设置 IP 的值,从而转到 call 指令后面的代码处继续执行。
用我个人的理解,不恰当的说,我感觉这一功能可以看作是调用一个函数,标号就相当于函数名,call 标号
就相当于调用了标号所在的子程序,当子程序的功能执行完过后,就使用 ret
指令回到调用的位置,ret
就相当于 return 一样。
这样我们可以利用 call 和 ret 来实现子程序的机制。子程序的框架如下:
1 | 标号: |
具有子程序的源程序的框架如下:
1 | assume cs:codesg |
mul 指令
mul 是乘法指令,使用 mul 做乘法的时候,注意一下两点
- 两个相乘的数:两个相乘的数,要么都是 8 位,要么都是 16 位。如果是 8 位,一个默认在 AL 中,另一个放在 8 位 reg 或内存字节单元中;如果是 16 位,一个默认在 AX 中,另一个放在 16 位 reg 或内存字单元中
- 结果:如果是 8 位乘法,结果默认放在 AX 中;如果是 16 位乘法,结果高位默认在 DX 中存放,低位在 AX 中放
格式如下
1 | mul reg |
内存单元可以用不同的寻址方式给出
1 | mul byte ptr ds:[0] |
举例如下
- 计算 100 * 10
1
2
3
4
5
6
7; 100 和 10 小于 255,做 8 位i乘法
mov al,100
mov bl,10
mul bl
; 结果(ax)=1000(03E8H) - 计算 100 * 10000
1
2
3
4
5
6
7; 100 小于 255, 可 10000 大于 255,所以必须做 16 位乘法
mov ax,100
mov bx,10000
mul bx
; 结果(ax)=4240H,(dx)=000FH (F4240H=1000000)
参数和结果传递问题
这个问题实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值
比如,设计一个子程序,可以根据提供的N,来计算N的3次方。
这里面就有两个问题:
(1) 将参数N存储在什么地方?
(2)计算得到的数值,存储在什么地方?
很显然,可以用寄存器来存储,可以将参数放到 bx 中; 因为子程序中要计算
NNN,可以使用多个mul指令。为了方便。可将结果放到d和ax中。子程序如下。
1 |
|
批量数据的传递
有时候我们需要传多个参数,那么寄存器的数量又是有限的,那么这个时候,我们将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可用同样的方法。
下面看一个例子,设计一个子程序,功能: 将一个全是字母的字符串转化为大写。
这个子程序需要知道两件事,字符串的内容和字符串的长度。因为字符串中的字母可能很多,所以不便将整个字符串中的所有字母都直接传递给子程序。但是,可以将字符串在内存中的首地址放在寄存器中传递给子程序。因为子程序中要用到循环,我们可以用 loop指令,而循环的次数恰恰就是字符串的长度。出于方便的考虑,可以将字符串的长度放到 cx 中。
1 | capital: and byte ptr [si]:11011111b ; 将 ds:si 所指单元中的字母转化为大写 |
寄存器冲突问题
设计一个子程序,功能:将一个全是字母,以 0 结尾的字符串,转化为大写
程序要处理的字符串以 0 作为结尾符,这个字符串可以如下定义:
1 | db 'conversation',0 |
应用这个子程序,字符串的内容后面一定要有一个0,标记字符串的结束。子程序可以依次读取每个字符进行检测,如果不是0,就进行大写的转化;如果是0,就结束处理。由于可通过检测 0 而知道是否已经处理完整个字符串,所以子程序可以不需要字符串的长度作为参数。可以用jcxz 来检测 0 。
1 | ;说明: 将一个全是字母,以0结尾的字符串。转化为大写 |
上面的代码没有问题,但是我们要考虑一个问题,我们这个使用 cx 寄存器,那么带着其他地方就没法使用该寄存器,可是在 CPU 中寄存器的数量是有限的,那么如何解决这个寄存器冲突问题?
解决这个问题的简捷方法是,在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复。可以用栈来保存寄存器中的内容。
以后,我们编写子程序的标准框架如下:
子程序开始:子程序中使用的寄存器入栈
子程序内容
子程序中使用的寄存器出栈
返回(ret.retf)
我们改进一下子程序capital 的设计:
1 | capital: push cx |
要注意寄存器入栈和出栈的顺序。