过程调用的概念
在编写程序时,我们会遇到同样的一段代码的运行效果在很多地方都要使用的情况,而如果每个地方都写一遍难免会造成资源的浪费。为此汇编语言的创作这门就创造了过程调用的概念
汇编语言的过程调用和高级语言的调用函数、和调用方法类似。它的主要作用就是,将一段代码的使用效果引用到此处,以省去重新编写代码的时间。
过程调用的实现
过程调用有一个独立的指令call
指令,如call <标识符>
。这个指令从字面意思来看,好像是将代码段转移到调用的地方运行,但真实情况却是跳转到调用的代码段。而跳转回来就需要在调用的代码段最后设置ret
指令,和高级语言的return相似。
call指令的使用
call的使用主要有四种格式:相对近调用、间接绝对近调用、直接绝对远调用、间接绝对远调用。
- 相对近调用,这里的近指的是,call指令和其调用目标处在同一个段下。
例如:
section test
call_add:
call near proc_1
proc_1:
....
注意: 这里的near可以省略,call proc_1
,编译器一般默认添加near。
当使用相对进调用时,call指令会被编译为0xE8
,这是一个3字节的指令,也就是后面的操作数可以有2字节大小,当代码被编译后,标识符会被替换成:(proc_1 - call_add) - 3,也就是call指令的结束到目标指令的开始的长度。
call指令的操作数是一个有符号数也就是它可以调用到距离当前 -32768 ~ 32767的位置。
为负如下,(编译后的计算过程同上)
section test:
proc_2:
..... ;代码没有进行分段
call_add:
call proc_2
- 间接绝对近调用,近的意义同上,不同的是代码格式
call 0x7431
call [0x7373]
call cx
call [cx]
call [cx+bi+si+0x02]
和相对近调用唯一的不同就是,帮助编译器省掉了计算过程。
以上两中call指令执行时,会首先将ip寄存器中值与操作数相加,然后将ip的值压入栈中,随后,将计算好的值移动到IP中。
- 直接绝对远调用,同理,其实就是call指令和调用目标不在同一个段内。代码格式唯一的不同,就是多了一个段地址。
call 0x2000:0x0030
这里的call会被编译成 0x9A,运行时会直接调用到目标地址所指定的位置,而不另外计算。
4. 间接绝对远调用。
代码格式
call far [0x2000]
call far [proc_1]
call far [bx]
call far [bx+si]
注意: 和上面3中格式都不同,这里的far是不能省略的。而且目标地址必须是连续的两个16位数,如proc_1 dd 0x0102, 0x2000
,编译器需要通过far关键字来判断,是否读取两个16位数。这里编译器会把低16位作为偏移地址,高16位作为段地址,如proc_1所指向的内容,就会在运行时被解释为0x2000:0x0102。
以上两种call指令执行时,会把原来的cs寄存器的内容压入栈中,再把指针寄存器ip的内容压入栈中,然后分别把给出的代码段地址和编译地址移入cs和ip。
返回指令
返回指令ret/retf,和call/call far的使用是一一对应的,一个call必须对应一个ret(写在目标代码的最后位置),同样call far也会对应一个retf。
当然,它们的作用是和call/call far命令相反的,他们的主要作用就是将从栈中弹出的第一个内容作为ip偏移量移动到IP寄存器中,而将第二个内容移动到CS寄存器中。
注意: 这里会有一个问题,如果再目标代码的执行过程中存在有压栈的情况,一定要在最后进行弹栈,否则程序会直接把压入栈中的内容当作返回时代码的地址移入CS:IP中,导致程序出错。
其实再这里也可以看出一条规律,8086处理器总是将,地址高位作为段基址,低位作为偏移地址,就和段基址和偏移地址的相对位置一样。
补充
使用间接寻址和直接寻址相比更加灵活,我们可以在程序运行通过代码修改目标地址的值,以改变程序跳转的位置。这在我们加载用户程序时非常有用