在 x86 汇编中,什么时候应该使用全局变量而不是局部变量?
In x86 assembly, when should I use global variables instead of local variables?
我正在用 x86 汇编编写一些小程序,这是我第一次使用低级语言,所以我不习惯。
在高级语言中我很少使用全局变量,但是我看到很多教程在汇编中使用全局变量,所以我不确定什么时候使用全局变量而不是局部变量。
全局变量是指在 .bss 和 .data 段中创建的数据,局部变量是指使用堆栈指针在当前过程的堆栈上分配的数据。
现在,我使用的是局部变量,参数比全局变量多很多。
提前致谢。
是的,更喜欢您保存在寄存器中或在需要时保存在堆栈中的局部变量。
“变量”是一个高级概念,在 asm 中并不真正存在。所以这只是一个问题,你在哪里保存你正在处理的数据。但是可以肯定的是,如果您考虑 未优化 C,其中每个变量确实在内存中有一个地址。
在 asm 中,指令越少的代码通常越容易理解。删除 store/reload mov
指令以支持仅将数据保存在寄存器中通常更容易理解。用 asm 编写的乐趣在于找到用更少(and/or 更便宜)指令完成相同工作的方法,而无用的 storing/reloading 内存恰恰相反。 IMO,它让你的代码变得丑陋。
Globals 吸收 asm 的所有原因是它们吸收高级语言(当函数 read/write 它们时数据流不清晰),以及您可能不会在高级语言中考虑的其他考虑因素:每个使用像 [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 中使用大量全局变量 examples/tutorials 可能是 6502 或 8051 等没有堆栈指针相对寻址模式的旧 ISA 遗留下来的风格习惯,因此调用堆栈上的局部变量是一件坏事。 (参见 Why do C to Z80 compilers produce poor code?)
它也可能/而不是作为一种简单的方法来命名变量,以便举例,但在 asm 中,这就是注释的作用。没有编译器可以将您的自文档化代码转换为高效代码。或者您可以执行 MSVC 的 asm 输出所做的操作,并为每个局部变量相对于堆栈帧的偏移量定义 assemble-time 常数。例如
foo equ -12
func:
push ebp
mov ebp, esp
sub esp, 24
...
mov eax, [ebp + foo]
leave
ret
更好:将局部变量保存在寄存器中
对于大多数变量,通常不需要将它们溢出到内存中的任何地方。使用注释来跟踪哪个变量或表达式在哪里。
如果不影响效率,您通常可以在寄存器和设计算法时考虑的高级变量之间建立 1:1 对应关系。例如也许 x
在整个函数中保留在 edi
中,包括在分支后的所有块中。 (还有一些其他寄存器主要用作计算和从内存加载内容的临时 space。)
在那种情况下,您将在记录此功能的函数顶部有一个注释块。如果某些寄存器设置在函数顶部附近,那么这些源代码行可能是此类注释的好地方。
内存目标 sub dword [loop_counter], 1
在典型的现代 x86 ISA 上有 6 个周期的延迟(5 个周期的存储转发 + 1 个周期的 ALU)。如果您将其用作循环的一部分,它将 运行 at best 每 6 个循环迭代一次。这就是为什么禁用优化的 C 编译器会生成如此慢的代码的部分原因。手动做这件事基本上是搬起石头砸自己的脚。
dec ecx
/ jnz
只有 1 个周期延迟,因此没有任何 store/reload 作为循环携带依赖的一部分的循环可以 运行 快到 1每个时钟周期的迭代。 (对于当前 Intel CPU 上最多 4 微指令的循环;如果底部的 dec/jnz 或 cmp/jcc
宏融合为单个微指令,则最多 5 条指令。否则你会遇到前端瓶颈。说到其中,内存目标读-修改-写操作总是至少 2 微指令。)
何时使用全局变量
在 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
或函数,如 puts
或 printf
(例如,作为格式字符串)。将它放在只读内存中并具体化指针比将字符串从立即数存储到内存要容易得多,例如 push `rld\n`
或
mov dword [esp], "Hell"
mov dword [esp+4], "o Wo"
...
mov ecx, esp ; pointer to the string
我正在用 x86 汇编编写一些小程序,这是我第一次使用低级语言,所以我不习惯。
在高级语言中我很少使用全局变量,但是我看到很多教程在汇编中使用全局变量,所以我不确定什么时候使用全局变量而不是局部变量。
全局变量是指在 .bss 和 .data 段中创建的数据,局部变量是指使用堆栈指针在当前过程的堆栈上分配的数据。
现在,我使用的是局部变量,参数比全局变量多很多。
提前致谢。
是的,更喜欢您保存在寄存器中或在需要时保存在堆栈中的局部变量。
“变量”是一个高级概念,在 asm 中并不真正存在。所以这只是一个问题,你在哪里保存你正在处理的数据。但是可以肯定的是,如果您考虑 未优化 C,其中每个变量确实在内存中有一个地址。
在 asm 中,指令越少的代码通常越容易理解。删除 store/reload mov
指令以支持仅将数据保存在寄存器中通常更容易理解。用 asm 编写的乐趣在于找到用更少(and/or 更便宜)指令完成相同工作的方法,而无用的 storing/reloading 内存恰恰相反。 IMO,它让你的代码变得丑陋。
Globals 吸收 asm 的所有原因是它们吸收高级语言(当函数 read/write 它们时数据流不清晰),以及您可能不会在高级语言中考虑的其他考虑因素:每个使用像 [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 中使用大量全局变量 examples/tutorials 可能是 6502 或 8051 等没有堆栈指针相对寻址模式的旧 ISA 遗留下来的风格习惯,因此调用堆栈上的局部变量是一件坏事。 (参见 Why do C to Z80 compilers produce poor code?)
它也可能/而不是作为一种简单的方法来命名变量,以便举例,但在 asm 中,这就是注释的作用。没有编译器可以将您的自文档化代码转换为高效代码。或者您可以执行 MSVC 的 asm 输出所做的操作,并为每个局部变量相对于堆栈帧的偏移量定义 assemble-time 常数。例如
foo equ -12
func:
push ebp
mov ebp, esp
sub esp, 24
...
mov eax, [ebp + foo]
leave
ret
更好:将局部变量保存在寄存器中
对于大多数变量,通常不需要将它们溢出到内存中的任何地方。使用注释来跟踪哪个变量或表达式在哪里。
如果不影响效率,您通常可以在寄存器和设计算法时考虑的高级变量之间建立 1:1 对应关系。例如也许 x
在整个函数中保留在 edi
中,包括在分支后的所有块中。 (还有一些其他寄存器主要用作计算和从内存加载内容的临时 space。)
在那种情况下,您将在记录此功能的函数顶部有一个注释块。如果某些寄存器设置在函数顶部附近,那么这些源代码行可能是此类注释的好地方。
内存目标 sub dword [loop_counter], 1
在典型的现代 x86 ISA 上有 6 个周期的延迟(5 个周期的存储转发 + 1 个周期的 ALU)。如果您将其用作循环的一部分,它将 运行 at best 每 6 个循环迭代一次。这就是为什么禁用优化的 C 编译器会生成如此慢的代码的部分原因。手动做这件事基本上是搬起石头砸自己的脚。
dec ecx
/ jnz
只有 1 个周期延迟,因此没有任何 store/reload 作为循环携带依赖的一部分的循环可以 运行 快到 1每个时钟周期的迭代。 (对于当前 Intel CPU 上最多 4 微指令的循环;如果底部的 dec/jnz 或 cmp/jcc
宏融合为单个微指令,则最多 5 条指令。否则你会遇到前端瓶颈。说到其中,内存目标读-修改-写操作总是至少 2 微指令。)
何时使用全局变量
在 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
或函数,如 puts
或 printf
(例如,作为格式字符串)。将它放在只读内存中并具体化指针比将字符串从立即数存储到内存要容易得多,例如 push `rld\n`
或
mov dword [esp], "Hell"
mov dword [esp+4], "o Wo"
...
mov ecx, esp ; pointer to the string