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

12
我有一个包含字符的内存位置,我想将其与另一个字符进行比较(它不在堆栈顶部,因此我不能只是使用pop命令)。 我如何引用内存位置的内容,以便可以进行比较?
基本上,语法上我该如何操作。
2个回答

39

如需更深入地讨论寻址模式(16/32/64位),请参见Agner Fog的"优化汇编"指南第3.3节。该指南对符号重定位和32位位置无关代码等方面有更多详细信息。

当然,英特尔和AMD的手册都有关于ModRM编码(以及可选的SIB和disp8/disp32字节)的细节部分,这使得可编码的内容和为什么存在限制非常清晰。

另请参阅:AT&T(GNU)语法与NASM语法在不同寻址模式下的表格,包括间接跳转/调用。此外,请参阅本答案底部的链接收藏。


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相对地址)都是这个的子集,省略一个或多个组件。这意味着您不需要将索引寄存器清零即可访问例如[rsi]
在asm源代码中,写的顺序无所谓:[5 + rax + rsp + 15 * 4 + MY_ASSEMBLER_MACRO * 2]可以正常工作。(所有常量上的数学运算在汇编时发生,结果为单个常量位移。)
所有寄存器的大小都必须相同,并且与您所处的模式的大小相同,除非您使用替代地址大小,需要额外的前缀字节。狭窄指针在x32 ABI (long mode中的ILP32)之外很少有用,其中您可能希望忽略寄存器的高32位,例如,而不是使用movsxd将32位可能为负的偏移量扩展为64位指针宽度。
如果您想要{{link3:例如使用al作为数组索引},则需要将其零扩展或符号扩展到指针宽度。(在操作字节寄存器之前已经将rax的上位比特清零有时是可能的,并且是实现这一目标的好方法。)
这些限制反映了机器码的可编码性,如同汇编语言一般。比例因子是2位移位计数器。ModRM(和可选的SIB)字节可以编码多达2个寄存器,但不能超过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到+127]的位移可以使用更紧凑的disp8编码,与disp32相比节省3个字节。

ModRM始终存在,其位表示SIB是否也存在。 disp8 / disp32同理。 代码大小异常:

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

  • 如果没有位移字节,就不可能将 e/rbpr13 编码为基础寄存器,因此 [ebp] 被编码为 [ebp + byte 0]。没有位移的编码中,ebp 作为基础寄存器的意思是根本没有基础寄存器(例如对于 [disp + reg*scale])。

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

请参阅英特尔参考手册中的表2-5以及周围部分,了解特殊情况的详细信息。(在32位和64位模式下相同。即使没有REX前缀,添加RIP相对编码也不会与任何其他编码冲突。)为了性能,通常不值得再增加一条指令来获得更小的x86机器码。在具有uop缓存的英特尔CPU上,它比L1 I$小,是一种更珍贵的资源。通常更重要的是最小化融合域uops。

如何使用它们

(此问题标记为MASM,但部分答案涉及NASM的Intel语法版本,特别是在x86-64 RIP相对寻址方面的区别。未涵盖AT&T语法,但请记住这只是同一机器码的另一种语法,因此限制是相同的。)

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

(注意:通常情况下,当源为字节时,您会想要使用movzx eax,byte [esi]movsx,但mov al,byte_src可以组装,并且在旧代码中很常见,合并到EAX / RAX的低字节中。请参见为什么GCC不使用部分寄存器?如何分离64位寄存器中的字节和字数组元素

如果你有一个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更大的比例因子)。请注意,global_array + 10的计算是在链接时完成的。对象文件(汇编器输出,链接器输入)通知链接器要添加到最终绝对地址中的+10,以将正确的4字节位移放入可执行文件(链接器输出)。这就是you can't use arbitrary expressions on link-time constants that aren't assemble-time constants(例如符号地址)的原因。

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

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

    您可以使用符号作为立即常量,将地址放入寄存器:

    • NASM:mov esi, global_array汇编成将地址放入esi的mov esi, imm32
    • MASM:需要mov esi, OFFSET global_array来做同样的事情。
    • MASM:mov
      任何一种寻址模式都可以与LEA一起使用,进行整数运算而不影响标志位,无论它是否是一个有效的地址。 在非地址/指针上使用LEA?[esi*4 + 10]通常只有在LEA中才有用(除非位移是符号,而不是小常量)。在机器代码中,没有单独编码缩放寄存器,因此[esi*4]必须汇编为[esi*4 + 0],用于32位位移的4个字节零。尽管如此,通常复制+移位只需一条指令,而不是更短的mov + shl,因为通常uop吞吐量更受限于代码大小,特别是在具有解码的uop缓存的CPU上。
      你可以像这样指定段覆盖:mov al, fs:[esi](NASM语法)。 段覆盖只是在通常的编码前面添加一个前缀字节。 其他所有内容都保持不变,具有相同的语法。
      您甚至可以在RIP相对寻址中使用段覆盖。 32位绝对寻址需要比RIP相对寻址多一个字节进行编码,因此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
      

      请参阅yasm文档,有关NASM-syntax有效地址的部分,和/或wikipedia x86条目中有关寻址模式的部分。维基页面说明了16位模式下允许的内容。这里是另一份32位寻址模式的“作弊表”

      16位寻址模式

      16位地址大小不能使用SIB字节,因此所有单寄存器和双寄存器寻址模式都被编码到单个mod/rm字节中。 reg1 可以是BX或BP,reg2 可以是SI或DI(或者您可以单独使用这4个寄存器中的任何一个)。缩放不可用。由于许多原因,包括这个原因,16位代码已经过时,如果您不需要学习,就不值得去学。

      请注意,在使用地址大小前缀时,16位限制适用于32位代码,因此16位LEA数学非常受限制。但是,您可以解决这个问题:lea eax,[edx + ecx * 2] 设置 ax = dx + cx * 2因为源寄存器的高位垃圾不会产生影响

      还有一个更详细的16位寻址模式指南。 16位有限的寻址模式(仅几个寄存器有效,没有比例因子),但您可能想阅读它以了解有关x86 CPU如何使用地址的一些基本原理,因为其中一些内容在32位模式下没有改变。


      相关主题:

      其中许多主题已在上文中链接,但并非全部。

      请参阅SO x86 tag wiki页面,其中包含文档和参考手册的链接,包括英特尔的手册。 Intel-syntaxAT&T syntax 标签维基介绍了它们之间的区别,以及(对于英特尔)不同版本的英特尔语法。 Micro fusion and addressing modes 索引寻址模式在Sandybridge系列上的性能影响:除少数情况外,未解开。 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) How to load address of function or label into register in GNU Assembler 如何高效地将符号地址放入寄存器中,而不是直接在寻址模式中使用它们。 Why is the address of static variables relative to the Instruction Pointer? and Why does this MOVSS instruction use RIP-relative addressing? - RIP相对寻址是从静态数据中加载/存储的标准有效方法,即使数据与代码不在同一部分(由于链接器/程序装载器的工作方式,相对偏移量保持不变,即使整个程序/库都是位置无关的)。

4
16位代码仍然存在。考虑到用户将其标记为DOS,因此对于任何意外发现这个问题和答案的人来说,解释16位限制可能是合理的。我看过的最好的经验法则可以在这个文件的第1.2.7节“一个记忆8086内存寻址模式的简单方法”中找到,这种方法相对容易理解和记忆。我认为它比您提供的维基文章更为详细。 - Michael Petch
1
不,那不是维基百科文章。维基百科文章并没有提供如何混合匹配这些行和列的详细解释。去年我在这个网站上帮助了一个人,他们不理解维基版本,但通过其他版本得以理解。 - Michael Petch
1
值得一提的是,OSX不允许global_array+[10] - Z boson
1
@BeeOnRope:对,我假设了默认的地址大小。添加一行关于寄存器与地址大小相同的说明,也为讨论这个问题打开了大门。如果你编写一个函数,它接受一个32位整数作为索引来使用静态表格,那么如果你只使用32位地址大小,你可以节省一条指令来将其零扩展或符号扩展。这非常难以理解,因为除了在x32 ABI(长模式下的ILP32)中之外,你不能将其用于任意指针。 - Peter Cordes
1
@ZhaniBaramidze:是的。[fs: 0]可以使用一个32位的绝对模式编码,其中disp32 = 0,或者(比较短的1个字节)使用RIP + rel32,使得rel32被选择为RIP+rel32=0。仅在位置相关代码中,使用相对寻址来生成绝对地址才有效,因为代码地址必须是已知常量(在链接时已知)。而且必须与所需绝对地址相差不超过2GiB。 - Peter Cordes
显示剩余25条评论

1
这是一个快速备忘单,从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 中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接