如何避免在没有 POP 的情况下使用 PUSH?
How to avoid using PUSH without POP?
我目前正在手工编写 x86 程序集 (FASM),我经常犯的一个典型错误是 push
堆栈上的一个参数,但是 return 在 pop
之前被执行。
这会导致调用者的堆栈偏移发生变化,从而导致程序崩溃。
这是一个演示它的粗略示例:
proc MyFunction
; A loop:
mov ecx, 100
.loop:
push ecx
; ==== loop content
...
; Somewhere, the decision is made to return, not just to exit the loop
jmp .ret
...
; ==== loop content
pop ecx
loop .loop
.ret:
ret
endp
现在,显而易见的答案是在发出 ret
之前从堆栈中弹出适当数量的元素。然而,在 1000 多行手工组装中很容易忽略一些东西。
我也在考虑使用 pushad
/ popad
always,但我不确定那个约定是什么。
问题:有什么模式可以避免这个问题吗?
通常不要在循环内使用push
/pop
;像编译器一样使用 mov
,这样您就不会不必要地移动 ESP
。 (这可能会导致额外的 if/when 你为其他本地人明确引用 ESP
。)
或者在这种情况下,只需为您的两个不同循环选择一个不同的寄存器,或者在保留一些 space 之后将外循环计数器完全保留在内存中。 (sub dword [esp], 1
/ jnz .outer_loop
。或者 [ebp-4]
如果您将 EBP
设置为帧指针而不是仅将其用作另一个调用保留寄存器。)
Spilling/reloading 围绕循环内某物的寄存器效率低下。释放寄存器的第一步应该是将只读内容保留在内存中,如果它们不是经常需要的话。例如像 inc edx
/ cmp edx, [esp+12]
/ jbe .outer_loop
这样的外循环计数器避免了 store/reload。仅当您 运行 超出寄存器时才将可变内容保留在内存中,然后当然更喜欢不经常更改的内容。
在编译器生成的代码中,您通常只会在序言中看到推送,并沿着通向 ret
的路径弹出。这使得它们很容易匹配。如果您需要保存另一个 call-preserved register 供函数内部使用,或者为本地人保留更多堆栈 space,您可以更改函数顶部的推送顺序,然后更改 return 路径。
(你可以有不止一种方法退出一个函数,特别是如果不需要太多清理,那么尾部复制比 jmp
到尾声的另一个副本更好。)
你不必像编译器那样严于律己(或脑残),毕竟你是在 asm 中手工编写以获得更好的性能。 (对吗?否则,只需让编译器为您进行微优化,生成“数千行”的 asm!中到大量代码是编译器在快速分析数据流和编写相当不错的代码方面真正发挥作用的地方。 )
因此,例如,您可以将 asm 堆栈用作堆栈数据结构;你无法说服编译器去做的事情。 (尽管 是一种不安全的尝试。)与 push
和 pop
一样,通过指针比较进行“空”检测。在那种情况下,如果您对堆栈内存有任何其他需求,您会希望使用 EBP
作为帧指针。
我目前正在手工编写 x86 程序集 (FASM),我经常犯的一个典型错误是 push
堆栈上的一个参数,但是 return 在 pop
之前被执行。
这会导致调用者的堆栈偏移发生变化,从而导致程序崩溃。
这是一个演示它的粗略示例:
proc MyFunction
; A loop:
mov ecx, 100
.loop:
push ecx
; ==== loop content
...
; Somewhere, the decision is made to return, not just to exit the loop
jmp .ret
...
; ==== loop content
pop ecx
loop .loop
.ret:
ret
endp
现在,显而易见的答案是在发出 ret
之前从堆栈中弹出适当数量的元素。然而,在 1000 多行手工组装中很容易忽略一些东西。
我也在考虑使用 pushad
/ popad
always,但我不确定那个约定是什么。
问题:有什么模式可以避免这个问题吗?
通常不要在循环内使用push
/pop
;像编译器一样使用 mov
,这样您就不会不必要地移动 ESP
。 (这可能会导致额外的 ESP
。)
或者在这种情况下,只需为您的两个不同循环选择一个不同的寄存器,或者在保留一些 space 之后将外循环计数器完全保留在内存中。 (sub dword [esp], 1
/ jnz .outer_loop
。或者 [ebp-4]
如果您将 EBP
设置为帧指针而不是仅将其用作另一个调用保留寄存器。)
Spilling/reloading 围绕循环内某物的寄存器效率低下。释放寄存器的第一步应该是将只读内容保留在内存中,如果它们不是经常需要的话。例如像 inc edx
/ cmp edx, [esp+12]
/ jbe .outer_loop
这样的外循环计数器避免了 store/reload。仅当您 运行 超出寄存器时才将可变内容保留在内存中,然后当然更喜欢不经常更改的内容。
在编译器生成的代码中,您通常只会在序言中看到推送,并沿着通向 ret
的路径弹出。这使得它们很容易匹配。如果您需要保存另一个 call-preserved register 供函数内部使用,或者为本地人保留更多堆栈 space,您可以更改函数顶部的推送顺序,然后更改 return 路径。
(你可以有不止一种方法退出一个函数,特别是如果不需要太多清理,那么尾部复制比 jmp
到尾声的另一个副本更好。)
你不必像编译器那样严于律己(或脑残),毕竟你是在 asm 中手工编写以获得更好的性能。 (对吗?否则,只需让编译器为您进行微优化,生成“数千行”的 asm!中到大量代码是编译器在快速分析数据流和编写相当不错的代码方面真正发挥作用的地方。 )
因此,例如,您可以将 asm 堆栈用作堆栈数据结构;你无法说服编译器去做的事情。 (尽管 push
和 pop
一样,通过指针比较进行“空”检测。在那种情况下,如果您对堆栈内存有任何其他需求,您会希望使用 EBP
作为帧指针。