CALL子程序地址相当于
PUSH下一条指令地址 + JMP子程序地址.
同时,推送地址几乎相当于
SUB xSP,指针大小 + MOV [xSP],地址.
SUB xSP,指针大小可以替换为PUSH.
RET几乎相当于
JMP [xSP]其次是ADD xSP,指针地址位于 JMP 领先的位置。
And ADD xSP,指针地址可以替换为POP.
所以,你可以看到编译器有什么样的基本自由。哦,顺便说一句,它可以优化您的代码,使您的函数完全内联,并且既不会调用它,也不会从它返回。
虽然有些反常,但使用针对平台(CPU 和操作系统)高度特定的指令和技术来设计更奇怪的控制传输并非不可能。
您可以使用IRET代替CALL and RET对于控制转移,前提是您将适当的内容放入指令的堆栈中。
您可以使用WindowsStructured Exception Handling
导致 CPU 异常(例如除以 0、页面错误等)的指令将执行转移到异常处理程序,并且控制权可以从那里转移回同一指令或下一个或下一个异常处理程序或任何位置。并且大多数x86指令都会导致CPU异常。
我确信还有其他不寻常的方式可以在子例程/函数之间进行控制传输、从子例程/函数内部传输以及在子例程/函数内进行控制传输。
类似这样的代码也并不罕见:
...
CALL A
A: JMP B
db "some data", 0
B: CALL C ; effectively call C with a pointer to "some data" as a parameter.
...
C:
; extracts the location of "some data" from the stack and uses it.
...
RET
在这里,第一次调用不是子例程,它只是将代码中间的数据地址放入堆栈的一种方法。
这可能是程序员会写的,而不是编译器会写的。但我可能错了。
我想说的是,你不应该期望拥有CALL
and RET
作为进入和离开子例程的唯一方法,您不应期望它们仅用于此目的并相互平衡。