我有一个内存位置,其中包含一个我想与另一个字符进行比较的字符(它不在堆栈的顶部,所以我不能只是pop
它)。如何引用内存位置的内容以便进行比较?
基本上我如何在语法上做到这一点。
我有一个内存位置,其中包含一个我想与另一个字符进行比较的字符(它不在堆栈的顶部,所以我不能只是pop
它)。如何引用内存位置的内容以便进行比较?
基本上我如何在语法上做到这一点。
当然,英特尔和 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-relative 除外)都是其子集,省略了一个或多个组件。例如,这意味着您不需要归零index_reg
即可访问[rsi]
。
在 asm 源代码中,你写东西的顺序并不重要:[5 + rax + rsp + 15*4 + MY_ASSEMBLER_MACRO*2]
工作正常。(所有关于常数的数学运算都发生在汇编时,导致单个常数位移。)
所有寄存器都必须彼此大小相同。并且大小与您所处的模式相同,除非您使用备用 address-size,需要额外的前缀字节。窄指针在x32 ABI(长模式下的 ILP32)之外很少有用,您可能希望忽略寄存器的前 32 位,例如,而不是使用movsxd
符号扩展寄存器中的 32 位可能为负的偏移量以64 位指针宽度。
例如,如果要用作al
数组索引,则需要将其零或符号扩展为指针宽度。rax
(有时可以在处理字节寄存器之前将高位清零,这是实现此目的的好方法。)
这些限制反映了机器代码中可编码的内容,就像汇编语言一样。比例因子是 2 位移位计数。ModRM(和可选的 SIB)字节最多可以编码 2 个寄存器,但不能更多,并且没有任何减去寄存器的模式,只有加法。任何寄存器都可以作为基础。除 ESP/RSP 以外的任何寄存器都可以作为索引。看到rbp 不允许作为 SIB 基础?对于编码细节,比如为什么[rsp]
总是需要一个 SIB 字节。
一般情况的每个可能的子集都是可编码的,除了那些使用e/rsp*scale
(显然在“正常”代码中始终保持指向堆栈内存的指针的代码中无用esp
)。
通常,编码的代码大小为:
[-128 to +127]
可以使用更紧凑的disp8
编码,与disp32
.ModRM 总是存在的,它的位表示 SIB 是否也存在。disp8/disp32 类似。代码大小异常:
[reg*scale]
本身只能用 32 位位移(当然可以为零)进行编码。智能汇编器通过编码lea eax, [rdx*2]
来解决这个问题,lea eax, [rdx + rdx]
但该技巧仅适用于按 2 缩放。无论哪种方式,除了 ModRM 之外,还需要一个 SIB 字节。
没有位移字节就无法编码e/rbp
或r13
作为基址寄存器,因此[ebp]
编码为[ebp + byte 0]
. ebp
相反,作为基址寄存器的无位移编码意味着没有基址寄存器(例如 for [disp + reg*scale]
)。
[e/rsp]
即使没有索引寄存器也需要一个 SIB 字节。(是否有位移)。而指定的 mod/rm 编码[rsp]
意味着有一个 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 的低字节中。请参阅为什么 GCC 不使用部分寄存器?和如何隔离64 位寄存器中的字节和字数组元素)
如果你有一个int*
,如果你有一个元素索引而不是字节偏移量,你通常会使用比例因子来按数组元素大小缩放索引。(出于代码大小的原因,优先使用字节偏移或指针来避免索引寻址模式,以及在某些情况下的性能,尤其是在可能损害微融合的 Intel CPU 上)。但你也可以做其他事情。
如果你有一个char array*
指针esi
:
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]
或类似的。
请参阅x86-64 GAS Intel 语法中的 RIP 相对变量引用(如“[RIP + _a]”)如何工作?有关 GAS .intel_syntax
、NASM 和 GAS AT&T 语法的更多详细信息和语法。
mov al, [global_array + ecx + edx*2 + 10]
显然global_array[ecx + edx*2 + 10]
,您可以使用单个寄存器索引静态/全局数组。甚至使用两个独立寄存器的二维阵列也是可能的。(对于 2、4 或 8 以外的比例因子,使用额外指令预缩放 1)。请注意,global_array + 10
数学是在链接时完成的。目标文件(汇编器输出,链接器输入)通知链接器将 +10 添加到最终绝对地址,以将正确的 4 字节位移放入可执行文件(链接器输出)。这就是为什么您不能对不是汇编时常量(例如符号地址)的链接时常量使用任意表达式的原因。
在 64 位模式下,这仍然需要global_array
作为部件的 32 位绝对地址disp32
,这仅适用于位置相关的 Linux 可执行文件,或者 largeaddressaware=no Windows。
mov al, 0ABh
根本不是负载,而是存储在指令中的立即常数。(请注意,您需要为 a 添加前缀,0
以便汇编器知道它是一个常量,而不是符号。一些汇编器也将接受0xAB
,而其中一些将不接受0ABh
:查看更多)。
您可以使用符号作为直接常量,将地址放入寄存器:
mov esi, global_array
组装成mov esi, imm32
将地址放入esi的a。mov esi, OFFSET global_array
需要做同样的事情。mov esi, global_array
组装成负载: mov esi, dword [global_array]
.在 64 位模式下,将符号地址放入寄存器的标准方法是 RIP 相对 LEA。语法因汇编程序而异。MASM 默认执行此操作。NASM 需要一个default rel
指令,或者[rel global_array]
. GAS 在每种寻址模式中都明确需要它。 如何在 GNU Assembler 中将函数或标签的地址加载到寄存器中。 mov r64, imm64
通常也支持 64 位绝对寻址,但通常是最慢的选项(代码大小会造成前端瓶颈)。 mov rdi, format_string
/call printf
通常在 NASM 中工作,但效率不高。
当地址可以表示为 32 位绝对值(而不是从当前位置的 rel32 偏移量)时,作为一种优化,mov reg, imm32
它仍然是最佳的,就像在 32 位代码中一样。(Linux 非 PIE 可执行文件或带有 LargeAddressAware=no 的 Windows)。但请注意,在 32 位模式下,lea eax, [array]
效率不高:它浪费了一个字节的代码大小(ModRM + 绝对 disp32),并且无法在mov eax, imm32
. 32 位模式没有 RIP 相对寻址。
请注意,OS X 在低 32 位之外的地址加载所有代码,因此 32 位绝对寻址不可用。可执行文件不需要与位置无关的代码,但您也可以这样做,因为 64 位绝对寻址不如 RIP 相对高效。 macho64 目标文件格式不像Linux ELF 那样支持 32 位绝对地址的重定位。确保不要在任何地方使用标签名称作为编译时 32 位常量。有效地址之类[global_array + constant]
的很好,因为它可以组合成相对于 RIP 的寻址模式。但是[global_array + rcx]
不允许,因为 RIP 不能和任何其他寄存器一起使用,所以它必须用global_array
硬编码的绝对地址作为 32 位位移(这将被符号扩展为 64b)。
任何和所有这些寻址模式都可以用于LEA
进行整数数学运算,并且不会影响 flags,无论它是否是有效地址。 对不是地址/指针的值使用 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-relative 需要多一个字节来编码,因此mov eax, fs:[0]
使用产生已知绝对地址的相对位移可以最有效地进行编码。即选择 rel32 所以 RIP+rel32 = 0。YASM 将使用mov ecx, [fs: rel 0]
,但 NASM 总是使用 disp32 绝对寻址,忽略说明rel
符。我没有测试过 MASM 或气体。
如果操作数大小不明确(例如在具有立即数和内存操作数的指令中),请使用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 语法有效地址,和/或维基百科 x86 条目的寻址模式部分。
wiki 页面说明了 16 位模式下允许的内容。这是32 位寻址模式的另一个“备忘单”。
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*2
,因为源寄存器高位中的垃圾没有效果。
对于 16bit ,还有更详细的寻址模式指南。16 位具有一组有限的寻址模式(只有少数寄存器有效,并且没有比例因子),但您可能需要阅读它以了解有关 x86 CPU 如何使用地址的一些基础知识,因为其中一些并没有改变32 位模式。
其中许多也在上面链接,但不是全部。
这是从该站点检索到的快速备忘单。它显示了可用于在 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 |
+------------------------+----------------------------+-----------------------------+
在您的特定情况下,如果该项目位于与 stack base的偏移量处,您将使用以下符号:4
EBP
Reg + Off
MOV EAX, [ EBP - 4 ]
这会将项目复制到 register EAX
。