【问题标题】:In x86 assembly, when should I use global variables instead of local variables?在 x86 程序集中,什么时候应该使用全局变量而不是局部变量?
【发布时间】:2019-08-16 19:08:52
【问题描述】:

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

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

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

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

提前致谢。

【问题讨论】:

  • 不需要使用全局变量,但设置起来不那么复杂,使教程更容易理解。如果可以的话,坚持使用局部变量。

标签: assembly x86 global-variables local-variables i386


【解决方案1】:

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

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

在 asm 中,指令较少的代码通常更容易理解。删除存储/重新加载mov 指令以仅将数据保存在寄存器中通常更容易遵循。用 asm 编写的乐趣在于找到用更少(和/或更便宜)指令完成相同工作的方法,而无用的存储/重新加载到内存则与之相反。它让你的代码很难看,IMO。


Globals 之所以使用 asm,是因为它们使用高级语言(函数读取/写入它们时数据流不明确),以及您在高级语言中可能不会考虑的其他考虑因素:每条指令使用像 [my_var] 这样的静态地址有一个 4 字节 disp32 作为寻址模式的一部分,而 [esp+8] 只需要 2 个额外字节(SIB 因为 ESP 作为基础,而 disp8 因为 +8 适合符号-扩展的 8 位整数)。或者,如果您使用 EBP 创建堆栈帧,则将 SIB 字节保存为寻址模式。

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

在 asm 示例/教程中使用大量全局变量可能是 6502 或 8051 等没有堆栈指针相对寻址模式的旧 ISA 遗留下来的风格习惯,因此使用局部变量调用堆栈是一件坏事。 (见Why do C to Z80 compilers produce poor code?

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

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

   mov  eax, [ebp + foo]
   leave
   ret

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

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

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

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


Memory-destination sub dword [loop_counter], 1 在典型的现代 x86 ISA 上具有 6 个周期的延迟(5 个周期的存储转发 + 1 个周期的 ALU)。如果将其用作循环的一部分,它将以 best 每 6 个循环运行一次迭代。这就是禁用优化的 C 编译器生成如此慢代码的部分原因。用手做这件事基本上是在踢自己的脚。

dec ecx / jnz 只有 1 个周期延迟,因此作为循环携带依赖项的一部分,没有任何存储/重新加载的循环可以以每个时钟周期 1 次迭代的速度运行。 (对于当前 Intel CPU 上最多 4 个 uop 的循环;如果底部的 dec/jnz 或 cmp/jcc 宏融合成一个 uop,则最多 5 条指令。否则你会遇到前端瓶颈。说到这,内存-destination read-modify-write 操作总是至少 2 uops。)


何时使用全局变量

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

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

静态常数数据很有用

最常见的用例可能是section .rodata(或Windows 上的section .rdata)中的字符串。

section .rodata                     ; linked as part of the TEXT segment
msg: db "Hello World", 10
msglen equ $ - msg                  ; assemble-time constant

您通常需要内存中的字符串通过引用像write 这样的系统调用或像putsprintf 这样的函数(例如作为格式字符串)传递。将它放在只读内存中并实现指针比将字符串从立即数存储到内存要容易得多,例如push `rld\n`

mov dword [esp], "Hell"
mov dword [esp+4], "o Wo"
...
mov ecx, esp              ; pointer to the string

【讨论】:

  • 我不明白反对意见。我有没有冒犯全局变量的粉丝? How do I print an integer in Assembly Level Programming without printf from the c library? 是一个非常好的例子,它使用寄存器作为局部变量,以及堆栈上的一个小缓冲区。 Extreme Fibonacci 是一个较长的程序,代码量很大。源代码有点乱,但确实有一些在 cmets 中记录寄存器使用情况的示例。
  • 这几乎是对我的总结:“由于所有原因,全球人都在学习 asm,因为他们在使用高级语言”。顺便说一句,我赞成并关注您的回答,继续努力!
猜你喜欢
  • 1970-01-01
  • 2013-08-14
  • 1970-01-01
  • 2021-06-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-15
  • 1970-01-01
相关资源
最近更新 更多