在阅读有关汇编程序的文章时,我经常遇到人们在写文件时他们推送处理器的某个寄存器并稍后再次弹出它以恢复它之前的状态。
怎么能推一个寄存器? 它在哪里推? 为什么需要这个?
这可归结为单处理器指令还是更复杂?
警告:所有当前答案都以Intels汇编语法给出; 例如,AT&T语法中的push-pop使用像b,w,l或q这样的后置修复来表示被操作的内存的大小。 例如:pushl %eax和popl %eax
@hawken在大多数能够吞下AT&T语法(特别是气体)的汇编程序中,如果可以从操作数大小推导出操作数大小,则可以省略后缀大小。 这是您给出的示例的情况,因为%eax总是32位大小。
推送值(不一定存储在寄存器中)意味着将其写入堆栈。
弹出意味着将堆栈顶部的任何内容恢复到寄存器中。这些是基本的指示:
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
push和pop的显式操作数是rm,而不仅仅是register,因此你可以push dword [esi]。甚至pop dword [esp]加载然后将相同的值存储回同一地址。 (github.com/HJLebbink/asm-dude/wiki/POP)。我只提到这个因为你说"不一定是寄存器"。
您还可以pop进入内存区域:pop [0xdeadbeef]
这是你如何推送一个寄存器。我假设我们正在谈论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寄存器减小到推送值的大小。
1.和2.应该重新排列
它在哪里推?
esp - 4。更确切地说:
esp减去4
该值被推送到esp
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.
*/
上面的GitHub上有runnable断言。
为什么需要这个?
确实,这些指令可以通过mov,add和sub轻松实现。
他们之所以存在,那些指令组合是如此频繁,以至于英特尔决定为我们提供它们。
这些组合如此频繁的原因是它们可以很容易地将寄存器的值保存并暂时恢复到存储器中,这样它们就不会被覆盖。
要了解问题,请尝试手动编译一些C代码。
一个主要的困难是决定每个变量的存储位置。
理想情况下,所有变量都适合寄存器,这是访问速度最快的存储器(目前比RAM快约100倍)。
但是,当然,我们可以轻松地拥有比寄存器更多的变量,特别是嵌套函数的参数,因此唯一的解决方案是写入内存。
我们可以写入任何内存地址,但由于函数调用和返回的局部变量和参数适合一个很好的堆栈模式,这可以防止内存碎片,这是处理它的最佳方法。将其与编写堆分配器的精神错误进行比较。
然后我们让编译器为我们优化寄存器分配,因为这是NP完成的,也是编写编译器最困难的部分之一。这个问题称为寄存器分配,它与图着色同构。
当编译器的分配器被迫将内容存储在内存而不仅仅是寄存器时,这就是所谓的溢出。
这可归结为单处理器指令还是更复杂?
我们所知道的只是英特尔记录了一条push和一条pop指令,因此它们就是这方面的一条指令。
在内部,它可以扩展到多个微码,一个用于修改esp,一个用于执行内存IO,并且需要多个周期。
但是,单个push也可能比其他指令的等效组合更快,因为它更具体。
这主要是未记录的:
Peter Cordes提到http://agner.org/optimize/microarchitecture.pdf中描述的技术表明push和pop只进行一次微操作。
Johan提到由于Pentium M Intel使用"堆栈引擎",它存储预先计算的esp + regsize和esp-regsize值,允许push和pop在单个uop中执行。另请参阅:https://en.wikipedia.org/wiki/Stack_register
什么是Intel微码?
https://security.stackexchange.com/questions/29730/processor-microcode-manipulation-to-change-opcodes
每个汇编指令需要多少个CPU周期?
@Downvoters请解释,以便我可以学习和改进。
您不需要猜测push / pop如何解码为uops。由于性能计数器,可以进行实验测试,Agner Fog已经完成并发布了指令表。由于堆栈引擎,Pentium-M和更高版本的CPU具有单uop push / pop(参见Agners microarch pdf)。由于英特尔/ AMD专利共享协议,这包括最近的AMD CPU。
@PeterCordes太棒了!那么英特尔是否记录了性能计数器来计算微操作?
此外,如果实际使用了其中任何一个,则从regs溢出的局部变量通常在L1缓存中仍然很热。但是从寄存器读取实际上是免费的,零延迟。因此它的速度比L1缓存快得多,具体取决于您希望如何定义术语。对于溢出到堆栈的只读本地,主要成本只是额外的负载uops(有时是内存操作数,有时带有单独的mov负载)。对于溢出的非常量变量,存储转发往返是很多额外延迟(额外约5c与直接转发,并且存储指令不便宜)。
是的,在几个不同的流水线阶段(发布/执行/退出)有总计uop的计数器,因此您可以计算融合域或未融合域。例如,请参阅此答案。如果我现在正在重写该答案,我会使用ocperf.py包装器脚本为计数器获取简单的符号名称。
推送和弹出寄存器在幕后等效于此:
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, [rsp8]而不是add / sub来更好地模拟push / pop对标志的影响。
几乎所有CPU都使用堆栈。程序堆栈是LIFO技术,支持硬件管理。
堆栈是通常在CPU内存堆顶部分配的程序(RAM)内存量,并且在相反方向上增长(在PUSH指令处堆栈指针减少)。插入堆栈的标准术语是PUSH,从堆栈中删除是POP。
堆栈通过堆栈预期的CPU寄存器管理,也称为堆栈指针,因此当CPU执行POP或PUSH时,堆栈指针将加载/存储寄存器或常量到堆栈内存中,堆栈指针将自动减少xor根据推送的字数增加或插入(从)堆栈。
通过汇编程序指令我们可以存储到堆栈:
CPU寄存器和常量。
返回函数或的地址
程序
功能/程序输入/输出
变量
功能/程序本地
变量。