Assembly 什么是x86;ret";指令等同于?

Assembly 什么是x86;ret";指令等同于?,assembly,x86,return,Assembly,X86,Return,假设我正在x86汇编中编写一个例程,比如“add”,它将两个作为参数传递的数字相加 在大多数情况下,这是一种非常简单的方法: push ebp mov ebp, esp mov eax, [ebp+8] add eax, [ebp+12] mov esp, ebp pop ebp ret 但是,有没有办法重写此方法以避免使用“ret”指令,并且仍然让它产生完全相同的结果?当然 push ebp mov ebp, esp mov eax, [ebp+8] add eax, [ebp+12] mo

假设我正在x86汇编中编写一个例程,比如“add”,它将两个作为参数传递的数字相加

在大多数情况下,这是一种非常简单的方法:

push ebp
mov ebp, esp
mov eax, [ebp+8]
add eax, [ebp+12]
mov esp, ebp
pop ebp
ret
但是,有没有办法重写此方法以避免使用“ret”指令,并且仍然让它产生完全相同的结果?

当然

push ebp
mov ebp, esp
mov eax, [ebp+8]
add eax, [ebp+12]
mov esp, ebp
pop ebp

pop ecx  ; these two instructions simulate "ret"
jmp ecx
这假设您有一个空闲寄存器(例如ecx)。编写一个使用“无寄存器”的等价物是可能的(毕竟x86是一台图灵机),但可能包含大量复杂的寄存器和堆栈洗牌

大多数当前操作系统都提供可由其中一个段寄存器访问的特定于线程的存储。然后,您可以这样安全地模拟“ret”:

 pop   gs:preallocated_tls_slot  ; pick one
 jmp   gs:preallocated_tls_slot

尚未测试,但您可以在不使用GPR的情况下执行ret,如下所示:

add esp,4
jmp dword ptr [esp-4]

这不需要任何空闲寄存器来模拟
ret
,但它需要4字节的内存(一个dword)。使用间接
jmp
编辑:正如Ira Baxter所指出的,此代码不可重入。在单线程代码中工作良好。如果在多线程代码中使用,将崩溃

push ebp mov ebp, esp mov eax, [ebp+8] add eax, [ebp+12] mov ebp, [ebp+4] mov [return_address], ebp pop ebp add esp,4 jmp [return_address] .data return_address dd 0
这可以使
return\u address
成为一个
dword
s数组,并允许每个线程访问
return\u address
的唯一索引,该索引由其唯一标识符的一对一内射函数计算


这一变化使得nrz的公认答案也适用于多线程代码

其他一些答案提出了完全避免注册的想法。这是缓慢的,通常不需要

(如果在ESP/RSP下没有红色区域,则速度会慢得多,比如x86-64 System V ABI保证用户空间。但没有其他x86/x86-64 ABI保证红色区域,因此调试器评估
时会打印一些函数(123)
在断点处停止时可能会阻塞ESP或Unix信号处理程序下方的空间。有关ESP下方数据安全性的更多信息,请参阅,尤其是在Windows上。)


在典型的32位呼叫约定中,EAX、ECX和EDX都是呼叫阻塞。(i386 System V,以及所有Windows cdecl、stdcall、fastcall等)

Irvine32调用约定没有调用阻塞寄存器,这是我所知道的一种情况,这在哪里不起作用

因此,除非您使用的是在ECX中返回内容的自定义调用约定,您可以安全地将
ret
替换为
pop ECX
/
jmp ECX
,并且仍然产生“完全相同的结果”,并完全遵守调用约定。(64位整数在EDX:EAX中返回,因此在某些函数中不能对EDX进行加密)

为了可读性,我还删除了堆栈帧开销/噪声

ret
基本上就是在x86中编写
pop-eip
(或IP/RIP)的方式,因此跳入架构寄存器并使用寄存器间接跳转在架构上是等效的。(但更糟糕的是微体系结构,因为
call
/
ret
对分支预测的特殊处理。)


为了避免寄存器,在具有堆栈arg的函数中,我们可以覆盖其中一个arg。在标准调用约定中,函数拥有其传入的arg,并且可以使用这些arg传递槽作为暂存空间,即使它们被声明为
foo(const int a,const int b)

这不适用于没有参数或只有寄存器参数的函数。(除了在Windows x64中,您可以将retaddr复制到返回地址上方的32字节阴影空间中。)

尽管Intel的ISA手册()的操作部分中有伪代码显示
DEST← SS:ESP
发生在
ESP+=4
之前,描述部分说“如果ESP寄存器用作对内存中目标操作数寻址的基址寄存器,POP指令在递增ESP寄存器后计算操作数的有效地址。”还有“POP ESP递增堆栈指针(ESP)在旧堆栈顶部的数据写入目标之前。”所以它实际上是
tmp=pop
<代码>dst=tmp。AMD根本没有提到任何一个角落的案例

如果我把EBP放在遗留的堆栈帧垃圾中,我本来可以避免目标pop,在恢复之前将EBP用作临时文件
mov ebp、[ebp+4]
/
mov[esp+8]、ebp
/
pop ebp
/
添加esp、4
/
jmp[esp]
,但这并不是更好或更容易遵循的方法。(保存的EBP值低于返回地址,您也无法安全地将ESP向上移动超过它。)这会暂时中断指向保存的EBP的EBP链之后的旧回溯

或者,您可以保存/恢复另一个寄存器,用作通过arg复制返回地址的临时寄存器。但是,一旦你弄清楚了它到底是做什么的,那么与流行音乐相比,这似乎毫无意义


避免RET对性能很糟糕 (除非您的来电者也避免呼叫,否则请手动输入回音地址。)

调用/ret不匹配会导致将来的
ret
指令返回父函数中的调用堆栈时性能不佳。

请参阅,以及Agner Fog的微阵列和优化指南。特别是在中引用和讨论的部分

(有趣的事实:大多数CPU的特殊情况是
调用+0
,因为代码使用
调用下一条指令
/
pop ebx
作为位置独立的32位代码的一部分来解决RIP相对寻址不足的问题并不罕见。请参阅stuffedcow.net博客文章。)

请注意,类似于
jmp add
而不是
call add
/
ret
的尾调用是可以的:这不会导致不匹配,因为第一个
ret
将返回到最近的
调用(在以尾调用结束的函数的父级中)。你可以看看我
push ebp
mov  ebp, esp
mov  ebp, [ebp+4]
mov  [return_address], ebp
pop  ebp

add  esp,4
jmp  [return_address]

.data
return_address dd 0
add:
    mov   eax, [esp+4]
    add   eax, [esp+8]
    ;;ret
    pop   ecx
    jmp   ecx           ; bad performance: misaligns the return address predictor stack
add:
    mov   eax, [esp+4]    ; arg1
    add   eax, [esp+8]    ; arg2
    ;;ret
    pop   [esp]           ; copy return address to arg1, and do ESP+=4
    jmp   [esp]           ; ESP is pointing to arg1