x86汇编寄存器中使用的push / pop指令的function是什么?
在阅读汇编程序时,我经常遇到一些人,他们会推送一个处理器的某个寄存器,稍后再popup来恢复它以前的状态。
- 你怎么能推注册? 它在哪里推? 为什么这需要?
- 这是归结为一个单一的处理器指令还是更复杂?
推送一个值(不一定存储在寄存器中)意味着将其写入堆栈。
popup意味着将堆栈顶部的任何内容恢复到registry中。 这些是基本的说明:
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
这是你如何推注册。 我假设我们正在谈论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 - 4
。 更确切地说:
-
esp
被减去4 - 该值被推到
esp
pop
反转这一点。
System V ABI告诉Linux,当程序开始运行时, rsp
指向一个合理的堆栈位置: https : //stackoverflow.com/a/32967009/895245这是你应该经常使用的。
你怎么能推注册?
最小的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
轻松实现。
他们之所以存在,是因为这些指令组合太频繁了,英特尔决定为我们提供这些指令。
这些组合之所以如此频繁,是因为它们可以很容易地将寄存器的值暂时保存并恢复到内存中,从而不会被覆盖。
要理解这个问题,请手动编译一些C代码。
一个主要困难是决定每个variables的存储位置。
理想情况下,所有的variables都适合寄存器,这是访问速度最快的内存(目前大约比RAM 快100倍 )。
但是,当然,我们可以比寄存器更容易拥有更多的variables,特别是嵌套函数的参数,所以唯一的解决scheme是写入内存。
我们可以写任何内存地址,但是由于函数调用和返回的局部variables和参数适合一个很好的堆栈模式,这可以防止内存碎片 ,这是处理它的最好方法。 比较这与写一个堆分配器的疯狂。
然后我们让编译器为我们优化寄存器分配,因为这是NP完成的,也是编写编译器最困难的部分之一。 这个问题叫做寄存器分配 ,与图着色是同构的。
当编译器的分配器被迫将内容存储在内存中而不是寄存器时,这被称为溢出 。
这是归结为一个单一的处理器指令还是更复杂?
我们所能确定的是,英特尔提供了一个push
和pop
指令,所以它们是这个意义上的一个指令。
在内部,它可以扩展到多个微码,一个修改esp
,一个修改内存IO,并且需要多个周期。
但是,单次push
也可能比其他指令的等效组合更快,因为它更具体。
这大部分是非文件logging的:
- 彼得·科尔德(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
- 什么是英特尔微代码?
- https://security.stackexchange.com/questions/29730/processor-microcode-manipulation-to-change-opcodes
- 每个汇编指令需要多less个CPU周期?
几乎所有的CPU都使用堆栈。 程序栈是硬件支持pipe理的LIFO技术。
堆栈是通常在CPU内存堆顶部分配的程序(RAM)内存的数量,在相反的方向上增长(在PUSH指令处,堆栈指针减less)。 插入堆栈的标准术语是PUSH ,从堆栈移除是POP 。
堆栈通过堆栈预期的CPU寄存器进行pipe理,也称为堆栈指针,所以当CPU执行POP或PUSH时 ,堆栈指针会将一个寄存器或常量加载/存储到堆栈内存中,堆栈指针将自动减less。或者从(堆叠)中取出。
通过汇编指令,我们可以存储堆栈:
- CPU寄存器和常量。
- 返回函数或过程的地址
- 函数/程序input/输出variables
- 函数/程序局部variables。
压入和popup寄存器在幕后等同于:
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语法。
作为一对使用,这可以让您保存在堆栈上的寄存器,并在以后恢复。 还有其他用途。