【问题标题】:Referencing the contents of a memory location. (x86 addressing modes)引用内存位置的内容。 (x86 寻址模式)
【发布时间】:2016-03-07 14:25:23
【问题描述】:

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

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

【问题讨论】:

    标签: assembly x86 masm addressing-mode


    【解决方案1】:

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

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

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


    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] 工作正常。 (所有关于常量的数学运算都发生在汇编时,导致单个常量位移。)

    寄存器的大小必须相同。并且大小与您所处的模式相同,除非you use an alternate address-size,需要额外的前缀字节。窄指针在 x32 ABI (ILP32 in long mode) 之外很少有用,您可能希望忽略寄存器的前 32 位,例如而不是使用movsxd 将寄存器中的 32 位可能为负的偏移量符号扩展为 64 位指针宽度。

    如果您想use al as an array index, for example,您需要将其零或符号扩展为指针宽度。 (有时可以在处理字节寄存器之前将rax 的高位清零,这是实现此目的的好方法。)


    这些限制反映了机器代码中可编码的内容,就像汇编语言一样。比例因子是 2 位移位计数。 ModRM(和可选的 SIB)字节最多可以编码 2 个寄存器,但不能更多,并且没有任何减去寄存器的模式,只有加法。任何寄存器都可以作为基础。除 ESP/RSP 以外的任何寄存器都可以作为索引。编码细节见rbp not allowed as SIB base?,比如为什么[rsp]总是需要一个SIB字节。

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

    通常,编码的代码大小是:

    • 1B 用于单寄存器模式(mod/rm(模式/寄存器或内存))
    • 2B 用于两个寄存器模式(mod/rm + SIB(标度索引基)字节)
    • 位移可以是 0、1 或 4 个字节(符号扩展为 32 或 64,具体取决于地址大小)。所以来自[-128 to +127] 的位移可以使用更紧凑的disp8 编码,比disp32 节省3 个字节。

    ModRM 总是存在的,它的位表示 SIB 是否也存在。 disp8/disp32 类似。代码大小异常:

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

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

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

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

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


    如何使用

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

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

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

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

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

    • 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 相对地址。建议使用 NASM DEFAULT REL,默认情况下生成 RIP 相对地址,而不必总是使用 [rel global_array + 10]。我认为 MASM 默认情况下会这样做。无法直接使用具有 RIP 相对地址的索引寄存器。正常的方法是lea rax, [global_array]mov al, [rax + rcx*8 + 10]或类似的。

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

    • mov al, [global_array + ecx + edx*2 + 10]global_array[ecx + edx*2 + 10] 加载显然你可以用一个寄存器来索引一个静态/全局数组。甚至使用两个独立寄存器的二维阵列也是可能的。 (对于 2、4 或 8 以外的比例因子,使用额外指令预缩放 1)。请注意,global_array + 10 数学是在链接时完成的。目标文件(汇编器输出,链接器输入)通知链接器将 +10 添加到最终绝对地址,以将正确的 4 字节位移放入可执行文件(链接器输出)。这就是为什么you can't use arbitrary expressions on link-time constants that aren't assemble-time constants(例如符号地址)。

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

    • mov al, 0ABh 根本不是负载,而是存储在指令中的立即常数。 (请注意,您需要为 0 添加前缀,以便汇编器知道它是一个常量,而不是符号。一些汇编器也将接受 0xAB,而其中一些将不接受 0ABhsee more)。

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

      • NASM:mov esi, global_array 组装成 mov esi, imm32,将地址放入 esi。
      • MASM:需要mov esi, OFFSET global_array 来做同样的事情。
      • MASM:mov esi, global_array 组装成负载:mov esi, dword [global_array]

      在 64 位模式下,将符号地址放入寄存器的标准方法是 RIP 相对 LEA。语法因汇编程序而异。 MASM 默认执行此操作。 NASM 需要default rel 指令或[rel global_array]。 GAS 在每种寻址模式中都明确需要它。 How to load address of function or label into register in GNU Assemblermov r64, imm64 通常也支持 64 位绝对寻址,但通常是最慢的选项(代码大小会造成前端瓶颈)。 mov rdi, format_string / call printf 通常适用于 NASM,但效率不高。

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

      请注意,OS X 在低 32 位之外的地址加载所有代码,因此 32 位绝对寻址不可用。位置无关代码对于可执行文件不是必需,但您也可以这样做,因为 64 位绝对寻址不如 RIP 相对有效。 The macho64 object file format doesn't support relocations for 32-bit absolute addresses Linux ELF 的做法。确保不要在任何地方使用标签名称作为编译时 32 位常量。像[global_array + constant] 这样的有效地址很好,因为它可以组合成相对于 RIP 的寻址模式。但是[global_array + rcx]是不允许的,因为RIP不能与任何其他寄存器一起使用,所以它必须与global_array的绝对地址组装成32位位移(which will be sign-extended to 64b)。


    任何和所有这些寻址模式都可以是used with LEA to do integer math with a bonus of not affecting flags,无论它是否是有效地址。 Using LEA on values that aren't addresses / pointers?

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


    您可以指定段覆盖,例如 mov al, fs:[esi](NASM 语法)。段覆盖只是在通常的编码前面添加一个前缀字节。其他一切都保持不变,语法相同。

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


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

    mov       dword [rsi + 10], 123   ; NASM
    mov   dword ptr [rsi + 10], 123   ; MASM and GNU .intex_syntax noprefix
    
    movl      $123, 10(%rsi)         # GNU(AT&T): operand size from mnemonic suffix
    

    请参阅the yasm docs for NASM-syntax effective addresses 和/或wikipedia x86 entry's section on addressing modes

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


    16 位寻址模式

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

    请注意,当使用地址大小前缀时,16 位限制适用于 32 位代码,因此 16 位 LEA 数学具有高度限制性。但是,您可以解决这个问题:lea eax, [edx + ecx*2] 设置 ax = dx + cx*2because garbage in the upper bits of the source registers has no effect

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


    相关主题:

    上面也有很多链接,但不是全部。

    【讨论】:

    • 16 位代码仍然存在。并且用户确实将其标记为 DOS,因此对于遇到此问题和答案的任何人来说,对 16 位限制的解释可能是合理的。我见过的最好的经验法则是相当容易理解和记住的,可以在document 的第 1.2.7 节 记住 8086 内存寻址模式的简单方法 中找到。我发现它比您链接到的 Wiki 文章更好
    • 不,那不是 Wiki 文章。 wiki 文章没有提供很多关于如何混合和匹配这些行和列的解释。去年我在这个网站上帮助了一个人,他们不理解 Wiki 版本,但用另一个人提供了线索。
    • 可能值得一提的是 [OSX 不允许 global_array + [10]](stackoverflow.com/questions/26927278/…)。
    • @BeeOnRope:对,我假设的是默认地址大小。添加关于寄存器大小与地址大小相同的那一行,我猜也打开了讨论这个问题的大门。我想如果您编写一个函数,该函数将要用作静态表的索引的 32 位整数作为索引,如果您只使用 32 位地址大小,则可以将指令保存为零或符号扩展它。它非常晦涩,因为除了 x32 ABI(长模式下的 ILP32)之外,您不能将它与任意指针一起使用。
    • @BeeOnRope:不,我的意思是使用 LEA 及其默认操作数大小(32 位)来写入零扩展为 64 的 32 位结果。例如(坏)lea rax, [ecx + ebx*4] 总是给出与(好)lea eax, [rcx + rbx*4] 相同的结果,但需要两个额外的前缀字节。 LEA 的 32 位地址大小永远不会有用,因为没有它您总是可以得到相同的结果。输入寄存器的高位不会影响加法或左移结果的低位。
    【解决方案2】:

    这是一个快速备忘单,取自 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 |
    +------------------------+----------------------------+-----------------------------+
    

    在您的特定情况下,如果项目位于堆栈基 EBPoffset 4,您将使用 Reg + Off 表示法:

    MOV EAX, [ EBP - 4 ]
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-03-20
      • 1970-01-01
      • 1970-01-01
      • 2017-01-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多