什么是x86“ret”指令相当于?

时间:2013-11-21 18:30:14

标签: 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”指令并仍然产生完全相同的结果?

5 个答案:

答案 0 :(得分:20)

不确定

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

答案 1 :(得分:6)

这不需要任何空闲寄存器来模拟ret,但它需要4个字节的内存(双字)。使用间接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

仅替换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

答案 2 :(得分:5)

尚未测试,但您可以在不使用这样的GPR的情况下进行修复:

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

答案 3 :(得分:3)

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

(如果您在ESP / RSP下没有红色区域,可以使用的速度要慢得多,例如x86-64 System V ABI对用户空间的保证。但是没有其他x86 / x86-64 ABI可以保证红色-zone,因此调试器在断点处停止时评估print some_func(123)可能会破坏ESP或Unix信号处理程序下方的空间。有关ESP以下数据的安全性,请参阅Is it valid to write below ESP?,尤其是在Windows上。)


在典型的32位调用约定中,EAX,ECX和EDX都是呼叫密集的。(i386 System V,以及所有Windows cdecl,stdcall,fastcall等)< / p>

Irvine32调用约定没有调用密集的寄存器,这是我所知的这种情况不起作用的一种情况。

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

add:
    mov   eax, [esp+4]
    add   eax, [esp+8]
    ;;ret
    pop   ecx
    jmp   ecx           ; bad performance: misaligns the return address predictor stack

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

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


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

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

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

尽管Intel ISA手册(https://www.felixcloutier.com/x86/pop)中“操作”部分的伪代码显示DEST ← SS:ESP;发生在ESP += 4之前,但“描述”部分显示“如果ESP寄存器用作基址寄存器,为了对内存中的目标操作数进行寻址,POP指令在增加ESP寄存器后会计算该操作数的有效地址。”另外,“ POP ESP在将栈顶旧数据写入目标之前,会递增栈指针(ESP)”。所以实际上是tmp = popdst = tmp。 AMD根本没有提到任何一个极端情况。

如果我将EBP留在旧式堆栈框架中,可以避免[ESP]目标弹出,在恢复之前使用EBP作为临时文件。 mov ebp, [ebp+4] / mov [esp+8], ebp / pop ebp / add esp,4 / jmp [esp],但这很难理解或遵循。 (保存的EBP值在寄信人地址下方,并且您也无法安全地将ESP移到它的上方。)这会在一系列指向保存的EBP的EBP之后暂时中断旧的回溯。

或者您可以保存/恢复另一个寄存器,以用作通过arg复制返回地址的临时寄存器。但是,只要您确切地知道它的作用,与pop [esp]相比就显得毫无意义。


避免使用RET对于性能而言很糟糕

(除非您的呼叫方避免使用call,而是手动推送回信地址。)

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

请参见Microbenchmarking Return Address Branch Prediction,以及Agner Fog的微体系结构和优化指南。特别是在Return address prediction stack buffer vs stack-stored return address?

中引用和讨论的部分

(有趣的事实:大多数CPU都是特殊情况call +0,因为使用call next_instruction / pop ebx作为与位置无关的32位代码来工作的一部分并不罕见缺少相对于RIP的寻址。请参阅stuffedcow.net博客文章。)

请注意,像jmp add而不是call add / ret的尾调用很好:这不会引起不匹配,因为第一个ret返回最新的call(在以tailcall结尾的函数的父级中)。就call / ret而言,您可以将其视为使第二个函数的主体成为执行尾调用的函数的“一部分”。

答案 4 :(得分:0)

这可能使for club in all_clubs: total_app = np.sum([[player['season_apps'], player['season_sub_apps'], player['season_goals']] for player in players_apps if player['player_club'] == club], axis=1) club_app_goal.append({'club' : club, 'total_app' : total_app[0], 'total_sub_app' : total_app[1], 'total_goals' : total_app[2]}) 成为return_address的数组,并使每个线程以唯一索引通过唯一标识符的一对一注入函数计算出的唯一索引访问dword

此更改使nrz可接受的答案也适用于多线程代码!