首页 > 解决方案 > 将相同的值压入堆栈并且 ret 的行为不同

问题描述

在 x-86 中,如果您从寄存器中压入一个值(例如,%eax),然后返回,则程序将控制权转移到与 %eax 处的值对应的地址,据我所知。

在程序的另一次运行中,如果您编辑了代码,以便通过不同的方式压入堆栈(例如,取消引用寄存器,将该值移动到另一个寄存器,然后压入该寄存器),然后返回,程序也应该将控制权转移到与推送的值对应的地址吗?

在程序的两次不同运行中,如果推送的值是相等的(即使它们的推送方式不同),那么程序在每次运行中的行为方式是否相同?

我无法展示我的代码,但我想确保我的概念性思维是正确的,因为我在一种情况下会抛出错误,但在另一种情况下不会。谢谢!

例 1:

func1:
/* should push address onto stack when calling */
call func2
...
...

func2:
/* should pop address and transfer control back to func1 */
ret

例 2:

func1:
...
...

func2:
/* %eax contains value equivalent to address from the first example */
pushl %eax

/* should pop %eax and transfer control to address contained in %eax */
ret

func2 也应该返回 func1 吧?但第二个例子不起作用

标签: assemblyx86

解决方案


是的,您可以不匹配 call/ret 并手动执行它们(在大型性能代码中),但是如果您不想破坏任何东西,则必须正确模拟它们(包括它们对堆栈指针所做的事情)。

但第二个例子不起作用

push/ret相当于jmp一个绝对地址。(性能除外:它总是会导致分支错误预测并与调用/调用预测器不匹配)。

你跳到了正确的地方(大概),但你忘了从堆栈中弹出返回地址。所以你“返回”,ESP指向错误的地方。这很容易导致大多数呼叫者崩溃;他们自己ret可能会弹出错误的返回地址。

(调用者使用 EBP 作为帧指针并且之前没有访问与 ESP 相关的任何内容leave/ret可能不会注意到。例如,如果调用者的 asm 是由 C 编译器在调试模式下生成的。即使这样,现代 Linux 也需要 16- a 之前的字节堆栈对齐call,以及在调用您的中断后进行的函数调用func2将不再使用 16 字节对齐的堆栈进行。当您以这种方式违反 ABI 时,某些 libc 函数可能会崩溃。)


通常你会把事情描述为:

  • call target= push $retaddr; jmp target;retaddr:
  • ret= pop %tmp; jmp *%tmp. 这是一个间接分支。
    (认为​​是ret一种写作方式pop %eip

但是是的,如果一个函数只有一个调用者,您可以硬编码返回地址并使用add $esp, 4/jmp after_call而不是ret. ret(再次通过不使用从 a 返回来打破对未来 rets 的分支预测call。)

或者用 , 替换调用者的calljmp然后jmp返回。这有效地形成func2了一个块,它是func1某些看待它的方式的一部分。它不能call从其他任何地方编辑,因为它不需要返回地址。

获取返回地址(即在跳回之前将其从堆栈中删除)但忽略它并总是跳转到硬编码的位置func1似乎没有用。


推荐阅读