Gov*_*mar 13 x86 assembly return
假设我在x86程序集中编写例程,比如"add",它添加了两个作为参数传递的数字.
在大多数情况下,这是一个非常简单的方法:
push ebp
mov ebp, esp
mov eax, [ebp+8]
add eax, [ebp+12]
mov esp, ebp
pop ebp
ret
Run Code Online (Sandbox Code Playgroud)
但是,有没有什么方法可以重写这个方法来避免使用"ret"指令并仍然产生完全相同的结果?
Ira*_*ter 21
当然.
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
Run Code Online (Sandbox Code Playgroud)
这假设您有一个免费注册(例如,ecx).编写一个使用"无寄存器"的等价物是可能的(毕竟x86 是图灵机),但很可能包含大量复杂的寄存器和堆栈混洗.
大多数当前操作系统提供可由其中一个段寄存器访问的特定于线程的存储.然后,您可以安全地以这种方式模拟"ret":
pop gs:preallocated_tls_slot ; pick one
jmp gs:preallocated_tls_slot
Run Code Online (Sandbox Code Playgroud)
这不需要任何免费寄存器来模拟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
尚未测试过,但您可以在不使用这样的GPR的情况下进行修复:
add esp,4
jmp dword ptr [esp-4]
Run Code Online (Sandbox Code Playgroud)
其他一些答案提出了完全避免寄存器的想法。这比较慢,通常不需要。
(如果你没有低于 ESP/RSP 的红区,你可以使用,比如 x86-64 System V ABI 保证用户空间。但没有其他 x86/x86-64 ABI 保证红区,因此print some_func(123),在断点处评估一段时间的调试器可能会破坏 ESP 或 Unix 信号处理程序下方的空间。有关在 ESP 下方写入数据的更多信息,尤其是在 Windows 上,请参阅在 ESP 下方写入是否有效?)
在典型的 32 位调用约定中,EAX、ECX 和 EDX 都被调用破坏了。 (i386 System V,以及所有 Windows cdecl、stdcall、fastcall 等)
Irvine32 调用约定没有调用破坏寄存器,这是我所知道的一种情况,这不起作用。
因此,除非您使用在 ECX 中返回某些内容的自定义调用约定,否则您可以安全地替换ret为pop ecx/jmp ecx并仍然产生“完全相同的结果”并完全遵守调用约定。(64 位整数在 EDX:EAX 中返回,因此在某些函数中您不能破坏 EDX)。
add:
mov eax, [esp+4]
add eax, [esp+8]
;;ret
pop ecx
jmp ecx ; bad performance: misaligns the return address predictor stack
Run Code Online (Sandbox Code Playgroud)
我还删除了堆栈帧开销/噪音以提高可读性。
ret基本上是您pop eip在 x86 中编写(或 IP / RIP)的方式,因此弹出架构寄存器并使用寄存器间接跳转在架构上是等效的。(但由于call/ret对分支预测的特殊处理,在微体系结构上要糟糕得多。)
为了避免寄存器,在具有堆栈参数的函数中,我们可以覆盖 args 之一。在标准调用约定中,函数拥有它们的传入 args,并且可以将那些 arg-passing 槽用作暂存空间,即使它们被声明为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
Run Code Online (Sandbox Code Playgroud)
这不适用于没有 args或只有寄存器 args的函数。(除了在 Windows x64 中,您可以将 retaddr 复制到返回地址上方的 32 字节阴影空间中。)
尽管英特尔 ISA 手册 ( https://www.felixcloutier.com/x86/pop )的操作部分中的伪代码显示DEST ? SS:ESP;之前发生过ESP += 4,但描述部分说“如果 ESP 寄存器用作寻址目标操作数的基址寄存器在内存中,POP 指令在增加 ESP 寄存器后计算操作数的有效地址。” 此外,“POP ESP 在旧堆栈顶部的数据写入目标之前增加堆栈指针(ESP)。” 所以它真的tmp = pop; dst = 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]一旦你弄清楚它的作用。
(除非您的来电者也避免了call,请手动推送返回地址。)
不匹配的 call/ret 会导致未来ret指令返回父函数中的调用堆栈时性能不佳。
请参阅微基准测试返回地址分支预测,以及 Agner Fog 的微架构和优化指南。特别是返回地址预测堆栈缓冲区与堆栈存储的返回地址中引用和讨论的部分?
(有趣的事实:大多数 CPU 的特殊情况call +0,因为代码使用call next_instruction/pop ebx作为位置无关的 32 位代码的一部分来解决缺少 RIP 相对寻址的问题并不罕见。请参阅 stuffedcow.net 博客文章。)
请注意,尾调用jmp add而不是call add/ret很好:这不会导致不匹配,因为第一个ret返回到最近的call(在以尾调用结尾的函数的父级中)。你可以把它看成使第二功能的机构,做的尾调用函数“的一部分”,尽可能call/ret而言。
| 归档时间: |
|
| 查看次数: |
22784 次 |
| 最近记录: |