在阅读有关汇编程序的信息时,我经常遇到人们写信,他们压入处理器的某个寄存器并稍后再次弹出以恢复其先前的状态。
- 你怎么能推寄存器?它在哪里推?为什么需要这个?
- 这会归结为单个处理器指令还是更复杂?
Answers:
压入一个值(不一定存储在寄存器中)意味着将其写入堆栈。
弹出意味着将堆栈顶部的所有内容恢复到寄存器中。这些是基本说明:
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]
加载后将相同的值存储回相同的地址。(github.com/HJLebbink/asm-dude/wiki/POP)。我之所以仅提及这一点,是因为您说的“不一定是寄存器”。
pop
进入一个内存区域:pop [0xdeadbeef]
这是您推送寄存器的方式。我假设我们正在谈论x86。
push ebx
push eax
它被推入堆栈。ESP
随着堆栈在x86系统中向下增长,寄存器的值将减小为推入值的大小。
需要保留这些值。一般用法是
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
Apush
是x86中的一条指令,它在内部执行两项操作。
ESP
寄存器减入值的大小。ESP
寄存器的当前地址。它在哪里推?
esp - 4
。更确切地说:
esp
减去4esp
pop
扭转这一点。
rsp
当程序开始运行时,System V ABI告诉Linux指向一个合理的堆栈位置:程序启动时(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上带有可运行的断言。
为什么需要这个?
这是事实,这些指令可以通过轻松实现mov
,add
并sub
。
它们之所以存在,是因为这些指令组合是如此频繁,以至于英特尔决定为我们提供这些指令。
这些组合之所以如此频繁,是因为它们使得可以轻松地将寄存器的值临时保存和恢复到内存中,从而不会被覆盖。
要理解该问题,请尝试手动编译一些C代码。
一个主要的困难是决定每个变量的存储位置。
理想情况下,所有变量都适合放入寄存器,这是访问速度最快的内存(目前比RAM快100倍)。
但是,当然,我们可以轻松拥有比寄存器更多的变量,特别是对于嵌套函数的参数而言,因此唯一的解决方案是写入内存。
我们可以写入任何内存地址,但是由于函数调用和返回的局部变量和参数适合一个好的堆栈模式,因此可以防止内存碎片,这是处理内存的最佳方法。将其与编写堆分配器的精神错乱相提并论。
然后,让编译器为我们优化寄存器分配,因为这已完成NP,并且是编写编译器中最难的部分之一。这个问题称为寄存器分配,图形着色是同构的。
当强制编译器的分配器将内容存储在内存中而不是仅将寄存器存储在内存中时,这称为溢出。
这会归结为单个处理器指令还是更复杂?
我们唯一可以确定的是,英特尔记录了push
和pop
指令,因此从某种意义上来说它们是一条指令。
在内部,它可以扩展为多个微码,一个可以修改esp
,一个可以执行内存IO,并需要多个周期。
但是,单个push
指令比其他指令的等效组合更快,因为它更具体。
这大部分是未记录的:
push
并pop
进行一次微操作。 push
/pop
解码为uops。借助性能计数器,可以进行实验测试,而Agner Fog做到了这一点并发布了说明表。奔腾M和更新的CPU具有单UOP push
/pop
感谢堆栈引擎(SEE昂纳的microarch PDF)。由于采用了Intel / AMD专利共享协议,其中包括最近的AMD CPU。
mov
负载)。对于溢出的非const变量,存储转发往返有很多额外的延迟(与直接转发相比,额外的〜5c且存储指令并不便宜)。
推入和弹出寄存器在后台等效于此:
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]
代替add
/sub
来更好地模拟push
/pop
对标志的影响。
几乎所有的CPU都使用堆栈。程序堆栈是具有硬件支持管理的LIFO技术。
堆栈是通常在CPU内存堆顶部分配并在相反方向增长(按PUSH指令,堆栈指针减少)的程序(RAM)内存量。插入堆栈的标准术语是PUSH,从堆栈中移除的标准术语是POP。
堆栈是通过堆栈专用的CPU寄存器(也称为堆栈指针)来管理的,因此,当CPU执行POP或PUSH时,堆栈指针会将寄存器或常量加载/存储到堆栈存储器中,并且堆栈指针将根据被压入的字数自动减少或增加或从堆栈弹出。
通过汇编程序指令,我们可以存储到堆栈:
b
,w
,l
,或q
以表示存储器的被操纵的大小。例如:pushl %eax
和popl %eax