在x86汇编中,什么时候应该使用全局变量而不是局部变量?

tha*_*ate 2 x86 assembly global-variables local-variables i386

我正在使用 x86 汇编创建一些小程序,这是我第一次使用低级语言,所以我不习惯。

在高级语言中我很少使用全局变量,但是我看过很多在汇编中使用全局变量的教程,所以我不确定何时使用全局变量而不是局部变量。

全局变量是指在 .bss 和 .data 段中创建的数据,局部变量是指使用堆栈指针在当前过程的堆栈上分配的数据。

现在,我使用局部变量和参数比全局变量多得多。

提前致谢。

Pet*_*des 6

是的,更喜欢将本地变量保存在寄存器中,或者如果需要的话保存在堆栈中。

“变量”是一个高级概念,在 asm 中并不真正存在。因此,问题只是将正在处理的数据保存在哪里。但是当然,如​​果您考虑的是未优化的C,其中每个变量确实具有,那么本地变量与全局变量是讨论静态存储 (.data/.bss/.rodata) 与堆栈内存的好方法。内存中的一个地址。

在 asm 中,指令较少的代码通常更容易理解。删除存储/重新加载mov指令而只将数据保存在寄存器中通常会更容易理解。用汇编编写的乐趣在于找到用更少(和/或更便宜)的指令完成相同工作的方法,而无用地存储/重新加载到内存则相反。在我看来,这会让你的代码变得丑陋。


全局变量之所以会陷入汇编,其原因与它们在高级语言中的所有原因一样(函数读/写它们时不清楚的数据流),以及您在高级语言中可能没有考虑到的其他注意事项:每条使用静态地址的指令,例如[my_var]有 4 个字节disp32作为寻址模式的一部分,而[esp+8]只需要 2 个额外字节(SIB 因为 ESP 作为基础,disp8 因为+8适合符号扩展的 8 位整数)。或者,如果您使用 EBP 创建堆栈帧,则可以在寻址模式中保存 SIB 字节。

如果您不关心效率并且宁愿使用标签和dd//而不只是堆栈帧中的偏移量来定义内存布局dw,那么在基本上只是一个函数的玩具程序中,全局变量可能是合理的。db但在这种情况下,通常您可以将所有内容保存在寄存器中。(特别是在 x86-64 上,除了堆栈指针之外还有 15 个 GP 寄存器,而 IA-32 只有 7 个或 6 个(如果您将 EBP 专用为帧指针)。

在 asm 示例/教程中使用大量全局变量可能是像 6502 或 8051 这样没有堆栈指针相对寻址模式的旧 ISA 遗留下来的风格习惯,因此调用堆栈上的局部变量是一件坏事。(请参阅为什么 C 到 Z80 编译器会生成糟糕的代码?

它也可能是/而不是作为一种简单的方式来命名变量以进行示例,但在 asm 中这就是注释的用途。没有编译器可以将您的自文档代码转换为高效代码。或者,您可以执行 MSVC 的 asm 输出的操作,并为每个本地相对于堆栈帧的偏移量定义汇编时常量。例如

foo equ -12
func:
   push  ebp
   mov   ebp, esp
   sub   esp, 24
   ...

   mov  eax, [ebp + foo]
   leave
   ret
Run Code Online (Sandbox Code Playgroud)

更好的是:将局部变量保存在寄存器中

对于大多数变量,通常不需要将它们溢出到内存中的任何地方。使用注释来跟踪哪个变量或表达式在哪里。

如果不影响效率,通常可以在寄存器和设计算法时考虑的高级变量之间建立 1:1 的对应关系。例如,可能x会保留在edi整个函数中,包括分支后的所有块中。(其他一些寄存器主要用作计算和从内存加载的内容的暂存空间。)

在这种情况下,您将在函数顶部有一块注释来记录这一点。如果某些寄存器设置在函数顶部附近,那么这些源代码行可能是进行此类注释的好地方。


在典型的现代 x86 ISA 上,内存目标sub dword [loop_counter], 1有 6 个周期延迟(5 个周期存储转发 + 1 个周期 ALU)。如果将其用作循环的一部分,则它最多6 个周期运行一次迭代。这就是禁用优化的 C 编译器生成如此缓慢的代码的部分原因。手动执行此操作基本上是搬起石头砸自己的脚。

dec ecx/jnz仅具有 1 个周期延迟,因此作为循环携带依赖项的一部分而没有任何存储/重新加载的循环可以以每个时钟周期 1 次迭代的速度运行。(对于当前 Intel CPU 上最多 4 个微指令的循环;如果 dec/jnz 或cmp/jcc底部宏融合到单个微指令中,则最多 5 个指令。否则您会遇到前端瓶颈。说到这里,内存目标读取-修改写入操作始终至少为 2 uops。)


何时使用全局变量

在 BSS 中分配一个大数组很容易测试东西。mov edi, array然后,您可以使用 NASM 语法或mov edi, OFFSET arrayMASM 语法将地址存入寄存器。因此,您可以使用它来测试为将指向数组的指针作为输入而编写的代码。

(在某些 Linux 内核版本上,匿名大页对于 BSS 与 mmapped 区域的工作方式可能有所不同,但我认为它们确实适用于现代内核中的 BSS 区域。不过,不适用于文件支持.data/.rodata区域,除非您弄脏了私有.data页面,本质上他们是匿名的。)

静态常量数据很有用

最常见的用例可能是 Windows 中的section .rodata 字符串section .rdata

section .rodata                     ; linked as part of the TEXT segment
msg: db "Hello World", 10
msglen equ $ - msg                  ; assemble-time constant
Run Code Online (Sandbox Code Playgroud)

通常,您需要内存中的字符串通过引用传递给系统调用或类似或的write函数(例如作为格式字符串)。将其放在只读内存中并具体化指针比将字符串从立即数存储到内存要容易得多,例如使用或putsprintfpush `rld\n`

mov dword [esp], "Hell"
mov dword [esp+4], "o Wo"
...
mov ecx, esp              ; pointer to the string
Run Code Online (Sandbox Code Playgroud)