阅读有关汇编语言的文章时,经常遇到人们写道他们将处理器的某个寄存器“push”一下,然后稍后再“pop”回来以恢复其先前的状态。
这是什么意思呢?如何将寄存器“push”起来?它被推到哪里去了?为什么需要这样做?
这是否简化为单个处理器指令,还是更加复杂?
推入一个值(不一定存储在寄存器中)意味着将其写入堆栈。
弹出意味着将位于栈顶的内容恢复到一个寄存器中。这些是基本指令:
push 0xdeadbeef ; push a value to the stack
pop eax ; eax is now 0xdeadbeef
; swap contents of registers
push eax
mov eax, ebx
pop ebx
r/m
,而不仅仅是寄存器,因此您可以执行push dword [esi]
。甚至使用pop dword [esp]
来加载,然后将相同的值存回到相同的地址。(https://github.com/HJLebbink/asm-dude/wiki/POP)。我之所以提到这一点,是因为您说“不一定是寄存器”。 - Peter Cordespop [0xdeadbeef]
。 - S.S. Annepushq
将一个qword(64位)推入堆栈,而push
必须从其操作数中推断大小。 - Sourav Kannantha Bpush 1
(2个字节)/ pop eax
(1个字节)总共3个字节,而不是mov eax,1
(总共5个字节,其中包括3个零字节的imm32,因此对于shellcode也是一个问题) [参见x86 / x64机器码高尔夫技巧]。此外,这种方式交换寄存器非常糟糕,可以使用xchg eax,ebx
(1个字节,在现代Intel CPU上需要3个微操作,但它们都不涉及内存访问。并且在现代AMD上仅需要2个微操作)。 - Peter Cordes以下是如何将寄存器压入栈中的方法,假设我们正在讨论x86。
push ebx
push eax
它被推入栈中。在x86系统中,由于栈向下增长,因此将ESP
寄存器的值减少到已经推送的值的大小。
需要保存这些值。通常的用法如下:
push eax ; preserve the value of eax
call some_method ; some method is called which will put return value in eax
mov edx, eax ; move the return value to edx
pop eax ; restore original eax
push
是 x86 指令中的一种,它在内部执行两个操作:
ESP
寄存器减去被推送值的大小。ESP
寄存器当前的地址上。ESP
寄存器减去推送值的大小”是什么意思?减少寄存器意味着什么?为什么push
命令要这样做? - coulombpop eax
(最后一条指令)并期望恢复原始值,这取决于 some_method
保证每个 push
操作都会有相应的 pop
操作的程度。我们是否应该总是期望满足这一点,这是否归结于阅读文档并确保? - First User它被推到哪里了?
esp - 4
。更准确地说:
esp
减去 4esp
上pop
反转这个过程。
System V ABI 告诉 Linux 在程序开始运行时使 rsp
指向一个合理的堆栈位置:程序启动时默认的寄存器状态是什么(asm,linux)? 这通常是你应该使用的。
如何推送寄存器?
最小的 GNU GAS 示例:
.data
/* .long takes 4 bytes each. */
val1:
/* Store bytes 0x 01 00 00 00 here. */
.long 1
val2:
/* 0x 02 00 00 00 */
.long 2
.text
/* Make esp point to the address of val2.
* Unusual, but totally possible. */
mov $val2, %esp
/* eax = 3 */
mov $3, %ea
push %eax
/*
Outcome:
- esp == val1
- val1 == 3
esp was changed to point to val1,
and then val1 was modified.
*/
pop %ebx
/*
Outcome:
- esp == &val2
- ebx == 3
Inverses push: ebx gets the value of val1 (first)
and then esp is increased back to point to val2.
*/
mov
、add
和sub
轻松实现这些指令,但它们存在的原因是这些指令组合非常频繁,因此英特尔决定为我们提供它们。当然,我们可以很容易地拥有比寄存器更多的变量,特别是对于嵌套函数的参数,所以唯一的解决方案是写入内存。
我们可以写入任何内存地址,但由于函数调用和返回的局部变量和参数适合一个漂亮的堆栈模式,这可以防止内存碎片化,所以这是处理它的最佳方法。与编写堆分配器的疯狂相比,可以进行比较。
然后我们让编译器为我们优化寄存器分配,因为那是NP完全的,并且是编写编译器最困难的部分之一。这个问题被称为寄存器分配,它与图形着色同构。
当编译器的分配器被迫将东西存储在内存中而不仅仅是寄存器时,这被称为溢出。
这是否归结为单个处理器指令,还是更复杂?
我们唯一确定的是,英特尔文档记录了push
和pop
指令,因此从某种意义上来说它们是一个指令。
内部而言,它可能会扩展为多个微代码,一个用于修改esp
,另一个用于执行内存IO,并需要多个周期。
但是,一个单独的push
指令可能比其他指令的等效组合更快,因为它更具体。
这大多数时间都没有得到记录:
push
和pop
只需要一个微操作。push
/pop
如何解码成uops。多亏了性能计数器,实验测试变得可能,并且Agner Fog已经完成并发布了指令表。由于堆栈引擎,Pentium-M及其后续版本的CPU具有单uop的push
/pop
操作(请参见Agner的微架构pdf)。这也包括最近的AMD CPU,这要归功于英特尔/ AMD的专利共享协议。 - Peter Cordesocperf.py
包装脚本来获取计数器的易于理解的符号名称。 - Peter Cordes推入和弹出寄存器在幕后相当于这样:
push reg <= same as => sub $8,%rsp # subtract 8 from rsp
mov reg,(%rsp) # store, using rsp as the address
pop reg <= same as=> mov (%rsp),reg # load, using rsp as the address
add $8,%rsp # add 8 to the rsp
注意,这是x86-64 At&t语法。
作为一对使用,它可以让你在堆栈上保存一个寄存器,并在以后恢复它。还有其他用途。
lea rsp,[rsp±8]
来模拟push
/pop
对标志位的影响,而不是使用add
/sub
。 - Ruslan
pushl %eax
和popl %eax
。 - Hawken%eax
始终是32位大小。 - Gunther Piez