Assembly 为什么调用函数后,该代码会两次跳入同一寄存器?

Assembly 为什么调用函数后,该代码会两次跳入同一寄存器?,assembly,x86,Assembly,X86,为什么我们需要做两次流行EBX?EBX每次会得到什么值?基本规则是,无论你推什么,你都必须弹出。否则,会使堆栈失衡,导致代码崩溃或更糟。该规则的意思是,您需要弹出与您所推送的值相同大小(以字节为单位)的值 因此,在本例中,在调用malloc之前,将8个字节压入堆栈: push EAX push 8 call malloc pop EBX pop EBX mov [EAX], 0 mov [EAX+4], EBX 要在函数调用后清理堆栈(malloc需要cdecl,因为它使用调用方清理的cdec

为什么我们需要做两次流行EBX?EBX每次会得到什么值?

基本规则是,无论你推什么,你都必须弹出。否则,会使堆栈失衡,导致代码崩溃或更糟。该规则的意思是,您需要弹出与您所推送的值相同大小(以字节为单位)的值

因此,在本例中,在调用malloc之前,将8个字节压入堆栈:

push EAX
push 8
call malloc
pop EBX
pop EBX
mov [EAX], 0
mov [EAX+4], EBX
要在函数调用后清理堆栈(malloc需要
cdecl
,因为它使用调用方清理的
cdecl
调用约定),需要弹出8个字节。恰好一种方便的方法是将寄存器大小的值弹出两次:

push EAX    ; push a DWORD-sized register (4 bytes)
push 8      ; push a DWORD immediate      (4 bytes)
这一堆是。 第一次弹出将
8
放入
EBX
(因为这是您最后一次按下的按钮),而您并不关心。下一个弹出窗口将
EAX
的原始值放回
EBX
(您按下的第一个按钮),然后稍后继续使用

如果您不关心保留从堆栈中弹出的任何值,只需使用ADD指令,向堆栈指针添加8个字节:

pop EBX   ; pop 4 bytes
pop EBX   ; pop 4 bytes
这可能比两个POP稍快,但实际上稍大(3个字节而不是2个字节),有时优化代码大小与优化代码速度同样重要,因为当代码较小时,缓存中可以容纳更多的代码。但在本例中,我怀疑更重要的问题是获得第一个被推送的值。由于malloc只接受一个参数,第一次推送的唯一原因是保留
EAX
的原始值,因为它被函数调用(函数在
EAX
中返回其结果)所破坏。因此,编写代码的另一种方法是:

add esp, 8

它通过移除推送的两个东西来平衡堆栈。它不必是ebx,也不必是pop。通常你会看到
添加esp,8
,但这是3个字节,而这只是2个字节,所以可能是在优化大小。你在代码块中的一些注释说,每次推/弹出都有8个字节。@peter Oh,它们实际上都有。我想我只是喜欢打8。现在修复。额外的推/弹出更改堆栈对齐。根据ABI,
esp
可能需要在调用
之前对齐16字节(例如i386 SysV)。在带有堆栈引擎(奔腾M及更高版本)的英特尔CPU上,直接使用esp需要堆栈引擎插入额外的uop,以将无序内核中的值与其偏移量同步。因此,如果加载/存储端口吞吐量不是问题,那么push/pop实际上是一种更便宜的对齐堆栈的方法。即使在
-O3
(不仅仅是
-Os
)时,clang也会这样做,但会弹出到一个虚拟寄存器中。这种使用push/pop进行实际保存/恢复的方式增加了EBX的延迟。还需要注意的是,
cdecl
是几种可能的所谓“调用约定”之一,所有这些约定都与堆栈的管理方式(以及由谁管理)以及参数如何从函数传递到函数有关。您必须知道被调用的库正在使用什么约定,并且必须严格遵守它。当然,只要您正确地告诉编译器约定是什么,编译器就会为您做一些肮脏的工作。
; Save EAX by moving it into a caller-save register
; (that will not get clobbered by the malloc function).
mov EBX, EAX

; Call the malloc function by pushing a 4-byte parameter and then rebalancing the stack.
push 8 
call malloc
add  esp, 4

; EAX contains malloc's return value, and EBX contains the original value of EAX
; that we saved before calling malloc.
mov [EAX],   0
mov [EAX+4], EBX