引用内存位置的内容。 (x86 寻址模式)

Referencing the contents of a memory location. (x86 addressing modes)

我有一个内存位置,其中包含一个我想与另一个字符进行比较的字符(它不在堆栈的顶部,所以我不能 pop 它)。如何引用内存位置的内容以便进行比较?

基本上我是如何在语法上做到这一点的。

有关寻址模式(16/32/64 位)的更详细讨论,请参阅 Agner Fog's "Optimizing Assembly" guide,第 3.3 节。该指南比符号和/或 32 位 position-independent 代码重定位的答案更详细。

当然,Intel 和 AMD 的手册中有完整的章节介绍 ModRM 的编码细节(以及可选的 SIB 和 disp8/disp32 字节),这清楚地说明了什么是可编码的以及为什么存在限制。

另请参阅:table of AT&T(GNU) syntax vs. NASM syntax for different addressing modes,包括间接跳转/调用。另请参阅此答案底部的 link 集合。


x86(32位和64位)有多种寻址方式可供选择。它们都是以下形式:

[base_reg + index_reg*scale + displacement]      ; or a subset of this
[RIP + displacement]     ; or RIP-relative: 64bit only.  No index reg is allowed

(其中比例为 1、2、4 或 8,位移为带符号的 32 位常量)。 所有其他形式(RIP-relative 除外)都是此形式的子集,其中省略了一个或多个组件。这意味着您不需要清零 index_reg 来访问 [rsi] 例如。

在 asm 源代码 中,编写顺序无关紧要:[5 + rax + rsp + 15*4 + MY_ASSEMBLER_MACRO*2] 工作正常。 (所有关于常数的数学计算都发生在 assemble 时间,导致一个常数位移。)

所有寄存器的大小必须相同。并且大小与您所处的模式相同,除非 , requiring an extra prefix byte. Narrow pointers are rarely useful outside of the x32 ABI (ILP32 in long mode) 您可能希望忽略寄存器的前 32 位,例如而不是使用 movsxd 到 sign-extend 寄存器中的 32 位 possibly-negative 偏移量到 64 位指针宽度。

如果你想 ,你需要将它归零或 sign-extend 到指针宽度。 (有时可以在乱用字节寄存器之前将 rax 的高位清零,这是实现此目的的好方法。)


这些限制反映了汇编语言通常在 machine-code、 中可编码的内容。比例因子是 2 位移位计数。 ModRM(和可选的 SIB)字节最多可以编码 2 个寄存器,但不能更多,并且没有任何减去寄存器的模式,只能添加。任何寄存器都可以作为基数。除了 ESP/RSP 之外的任何寄存器都可以是索引。有关编码详细信息,请参阅 ,例如为什么 [rsp] 总是需要一个 SIB 字节。

一般情况的每个可能子集都是可编码的,除了使用 e/rsp*scale 的子集(在 "normal" 代码中显然没用,它总是在 esp 中保留指向堆栈内存的指针)。

通常,编码的code-size是:

  • 1B 用于 one-register 模式 (mod/rm (Mode / Register-or-memory))
  • 2B 用于 two-register 模式(mod/rm + SIB(比例索引基数)字节)
  • 位移可以是 0、1 或 4 个字节(sign-extended 为 32 或 64,具体取决于 address-size)。因此 [-128 to +127] 的位移可以使用更紧凑的 disp8 编码,与 disp32.
  • 相比节省 3 个字节

ModRM 始终存在,并且其位指示 SIB 是否也存在。 disp8/disp32 类似。 Code-size 异常:

  • [reg*scale] 本身只能用 32 位位移(当然可以为零)进行编码。 Smart assemblers 通过将 lea eax, [rdx*2] 编码为 lea eax, [rdx + rdx] 来解决这个问题,但该技巧仅适用于按 2 缩放。除了 ModRM 之外,任何一种方式都需要 SIB 字节。

  • 不可能将e/rbpr13编码为没有位移字节的基址寄存器,所以[ebp]被编码为[ebp + byte 0]。以 ebp 作为基址寄存器的 no-displacement 编码意味着没有 没有 基址寄存器(例如 [disp + reg*scale])。

  • [e/rsp] 即使没有索引寄存器也需要一个 SIB 字节。 (无论是否有位移)。指定 [rsp] 的 mod/rm 编码意味着有一个 SIB 字节。

有关特殊情况的详细信息,请参阅英特尔参考手册中的 Table 2-5 和周围部分。 (它们在 32 位和 64 位模式下是相同的。添加 RIP-relative 编码不会与任何其他编码冲突,即使没有 REX 前缀。)

为了性能,通常不值得花费额外的指令来获得更小的 x86 机器代码。在具有 uop 高速缓存的 Intel CPU 上,它比 L1 I$ 小,而且是更宝贵的资源。最小化 fused-domain 微指令通常更为重要。


如何使用它们

(这个问题被标记为 MASM,但是这个答案中的一些内容谈到了 NASM 的 Intel 语法版本,特别是它们对于 x86-64 RIP-relative 寻址不同的地方。AT&T 语法不是涵盖,但请记住,这只是同一机器代码的另一种语法,因此限制是相同的。)

这 table 与可能的寻址模式的硬件编码不完全匹配,因为我要区分使用标签(例如全局或静态数据)与使用小的恒定位移。所以我将介绍硬件寻址模式 + linker 对符号的支持。

(注意:当源是一个字节时,通常你会想要 movzx eax, byte [esi]movsx,但是 mov al, byte_src 确实 assemble 并且在旧代码中很常见,合并到EAX/RAX的低字节。参见 and How to isolate byte and word array elements in a 64-bit register)

如果您有 int*,如果您有元素索引而不是字节偏移量,通常您会使用比例因子按数组元素大小缩放索引。 (出于 code-size 的原因,首选字节偏移量或指针以避免索引寻址模式,以及在某些情况下的性能,尤其是在 Intel CPU 上,它可能会损害 micro-fusion)。但你也可以做其他事情。
如果你在esi中有一个指针char array*:

  • mov al, esi:无效,不会assemble。没有方括号,它根本不是负载。这是一个错误,因为寄存器大小不同。

  • mov al, [esi] 加载指向的字节,即 array[0]*array.

  • mov al, [esi + ecx] 加载 array[ecx].

  • mov al, [esi + 10] 加载 array[10].

  • mov al, [esi + ecx*8 + 200] 加载 array[ecx*8 + 200]

  • mov al, [global_array + 10]global_array[10] 加载。在 64 位模式下,这可以而且应该是 RIP-relative 地址。建议使用 NASM DEFAULT REL,以默认生成 RIP-relative 地址,而不必始终使用 [rel global_array + 10]。我认为 MASM 默认情况下会这样做。无法直接使用具有 RIP-relative 地址的变址寄存器。正常的方法是 lea rax, [global_array] mov al, [rax + rcx*8 + 10] 或类似的方法。

    请参阅 How do RIP-relative variable references like "[RIP + _a]" in x86-64 GAS Intel-syntax work? 了解更多详细信息,以及 GAS .intel_syntax、NASM 和 GAS AT&T 语法的语法。

  • mov al, [global_array + ecx + edx*2 + 10]global_array[ecx + edx*2 + 10] 加载 显然,您可以使用单个寄存器索引 static/global 数组。甚至使用两个独立寄存器的二维数组也是可能的。 (pre-scaling 有一个额外的指令,比例因子不是 2、4 或 8)。请注意,global_array + 10 数学运算是在 link 时完成的。目标文件(assembler 输出,linker 输入)通知 linker 添加到最终绝对地址的 +10,将正确的 4 字节位移放入executable(linker 输出)。这就是为什么 (例如符号地址)。

    在 64 位模式下,这仍然需要 global_array 作为 disp32 部分的 32 位 绝对 地址,它仅适用于a position-dependent Linux executable,或 largeaddressaware=no Windows.

  • mov al, 0ABh 根本不是负载,而是存储在指令中的 immediate-constant。 (请注意,您需要添加 0 前缀,以便 assembler 知道它是一个常量,而不是一个符号。一些 assemblers 也将接受 0xAB,其中一些不接受 0ABh: see more).

    可以使用符号作为立即数,将地址存入寄存器:

    • NASM: mov esi, global_array assembles 到 mov esi, imm32 中,将地址放入 esi.
    • MASM:mov esi, OFFSET global_array 需要做同样的事情。
    • MASM:mov esi, global_array assembles 加载:mov esi, dword [global_array].

    在 64 位模式下,将符号地址放入寄存器的标准方法是 RIP-relative LEA。语法因 assembler 而异。 MASM 默认执行此操作。 NASM 需要一个 default rel 指令,或者 [rel global_array]。 GAS 在每个寻址模式中都明确需要它。 。对于 64 位绝对寻址,通常也支持 mov r64, imm64,但通常是最慢的选项(代码大小会造成 front-end 瓶颈)。 mov rdi, format_string / call printf 通常适用于 NASM,但效率不高。

    当地址可以表示为 32 位 absolute(而不是相对于当前位置的 rel32 偏移量)时,mov reg, imm32 仍然是最优的就像在 32 位代码中一样。 (Linux non-PIE executable 或 Windows with LargeAddressAware=no)。但请注意,在 32 位模式下,lea eax, [array] 不是 效率:它浪费了 code-size 的一个字节(ModRM + absolute disp32)并且不能 运行 在与 mov eax, imm32 一样多的执行端口上。 32 位模式没有 RIP-relative 寻址。

    注意OSX加载低32位以外地址的所有代码,所以32位绝对寻址不可用。 Position-independent 代码对于 executable 不是 必需的,但您也可以这样做,因为 64 位绝对寻址的效率低于 RIP-relative。 The macho64 object file format doesn't support relocations for 32-bit absolute addresses the way Linux ELF does. Make sure not to use a label name as a compile-time 32-bit constant anywhere. An effective-address like [global_array + constant] is fine because that can be assembled to a RIP-relative addressing mode. But [global_array + rcx] is not allowed because RIP can't be used with any other registers, so it would have to be assembled with the absolute address of global_array hard-coded as the 32bit displacement (which will be sign-extended to 64b).


所有这些寻址模式都可以是used with LEA to do integer math with a bonus of not affecting flags, regardless of whether it's a valid address.

[esi*4 + 10] 通常只对 LEA 有用(除非位移是一个符号,而不是一个小常数)。在机器代码中,scaled-register 没有单独的编码,因此 [esi*4] 必须从 assemble 到 [esi*4 + 0],其中 4 个字节的零用于 32 位位移。在一条指令中复制+移位而不是更短的 mov + shl 通常仍然是值得的,因为通常 uop 吞吐量比代码大小更成为瓶颈,尤其是在具有 decoded-uop 缓存的 CPU 上。


您可以像 mov al, fs:[esi] 一样指定 segment-overrides(NASM 语法)。一个segment-override只是在通常的编码前加上一个prefix-byte。其他一切都保持不变,语法相同。

您甚至可以使用具有 RIP-relative 寻址的段覆盖。 32 位绝对寻址比 RIP-relative 多占用一个字节进行编码,因此 mov eax, fs:[0] 可以使用产生已知绝对地址的相对位移最有效地进行编码。即选择 rel32,因此 RIP+rel32 = 0。YASM 将使用 mov ecx, [fs: rel 0] 执行此操作,但 NASM 始终使用 disp32 绝对寻址,忽略 rel 说明符。我还没有测试过 MASM 或 gas。


如果 operand-size 不明确(例如,在带有立即数和内存操作数的指令中),请使用 byte / word /dword/qword指定:

mov       dword [rsi + 10], 123   ; NASM
mov   dword ptr [rsi + 10], 123   ; MASM and GNU .intex_syntax noprefix

movl      3, 10(%rsi)         # GNU(AT&T): operand size from mnemonic suffix

参见 the yasm docs for NASM-syntax effective addresses, and/or the wikipedia x86 entry's section on addressing modes

wiki 页面说明了 16 位模式下允许的内容。这里是 another "cheat sheet" for 32bit addressing modes.


16 位寻址模式

16bit地址大小不能用一个SIB字节,所以所有的一、二寄存器寻址方式都编码成单个mod/rm字节。 reg1可以是BX或BP,reg2可以是SI或DI(或者你可以自己使用这4个寄存器中的任何一个)。缩放不可用。 16 位代码已过时,原因有很多,包括这个,如果没有必要,不值得学习。

请注意,当使用 address-size 前缀时,16 位限制适用于 32 位代码,因此 16 位 LEA-math 具有高度限制性。但是,您可以解决这个问题:lea eax, [edx + ecx*2] 设置 ax = dx + cx*2.

还有一个more detailed guide to addressing modes, for 16bit。 16 位具有一组有限的寻址模式(只有少数寄存器有效,并且没有比例因子),但您可能想阅读它以了解有关 x86 CPU 如何使用地址的一些基础知识,因为其中一些没有改变32 位模式。


相关主题:

其中许多也在上面 linked,但不是全部。

  • 请参阅 SO x86 tag wiki 页面以获取 link 文档和参考手册,包括英特尔手册。
  • Intel-syntax and AT&T syntax 标签 wiki 涵盖了它们之间的差异,以及(对于英特尔)不同风格的英特尔语法。
  • Micro fusion and addressing modes 索引寻址模式对 Sandybridge-family 的性能影响:除少数情况外的非分层。
  • Mach-O 64-bit format does not support 32-bit absolute addresses. NASM Accessing Array MacOS 64 位寻址
  • 32-bit absolute addresses no longer allowed in x86-64 Linux? (Linux PIE vs. position-dependent executables)
  • How do RIP-relative variable references like "[RIP + _a]" in x86-64 GAS Intel-syntax work?(还包括 NASM 和 GAS AT&T)
  • 如何有效地将符号地址放入寄存器,而不是直接在寻址模式下使用它们。
  • Why is the address of static variables relative to the Instruction Pointer? - RIP-relative 是从静态数据 load/store 的标准有效方式,即使数据与代码位于不同的部分(因为 linkers / program-loaders 工作,相对偏移量保持不变,即使程序/库作为一个整体是 position-independent。)

这是从 this site 检索到的快速备忘单。它显示了可用于在 x86 汇编中寻址主内存的各种方法:

+------------------------+----------------------------+-----------------------------+
| Mode                   | Intel                      | AT&T                        |
+------------------------+----------------------------+-----------------------------+
| Absolute               | MOV EAX, [0100]            | movl           0x0100, %eax |
| Register               | MOV EAX, [ESI]             | movl           (%esi), %eax |
| Reg + Off              | MOV EAX, [EBP-8]           | movl         -8(%ebp), %eax |
| Reg*Scale + Off        | MOV EAX, [EBX*4 + 0100]    | movl   0x100(,%ebx,4), %eax |
| Base + Reg*Scale + Off | MOV EAX, [EDX + EBX*4 + 8] | movl 0x8(%edx,%ebx,4), %eax |
+------------------------+----------------------------+-----------------------------+

在您的具体情况下,如果该项目位于距堆栈基 EBP 4 偏移量 处,您将使用 Reg + Off 表示法:

MOV EAX, [ EBP - 4 ]

这会将项目复制到寄存器 EAX