用于寻址的 MASM 偏移量与标签

MASM Offset vs. Label for addressing

我目前正在阅读 Irvine x86 Assembly 一书,我正在学习第四章。

他们引入了 OFFSET 指令,但我对为什么要使用它感到困惑。为什么我不直接使用标签(它已经是该数据的地址)?似乎 OFFSET 只是增加了额外的噪音。

我有这个小程序来说明我的观点。我有一个名为 array 的数据标签,我可以将数组的元素移动到 al 中。但是书上说的是使用OFFSET指令获取array的地址,并将其移动到esi。但这对我来说似乎没有必要,因为我可以使用标签。

我有两段代码在下面做同样的事情。一个是我使用标签访问数组的元素,另一个是我使用 OFFSET 将地址移动到 esi 然后访问数组的元素。

.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode: DWORD

.data
    array   BYTE 10h, 20h, 30h, 40h, 50h

.code
main PROC
    xor eax, eax        ; set eax to 0

    ; Using Labels
    mov al, array      
    mov al, [array + 1]
    mov al, [array + 2]
    mov al, [array + 3]
    mov al, [array + 4]

    ; Using Offset
    mov esi, OFFSET array
    mov al, [esi]
    mov al, [esi + 1]
    mov al, [esi + 2]
    mov al, [esi + 3]
    mov al, [esi + 4]

    INVOKE ExitProcess, 0
main ENDP
END main

它们真的只是实现同一件事的两种方法吗?

本书后面讲到指针时,有这个例子:

.data
arrayB byte 10h, 20h, 30h, 40h
ptrB dword arrayB

这对我来说很有意义。 ptrB 保存 arrayB 的地址。但后来他们说,"Optionally, you can delcare ptrB with the OFFSET operator to make the relationship clearer:"

ptrB dword OFFSET arrayB

这并没有让我更清楚。我已经知道 arrayB 是一个地址。看起来 OFFSET 只是被扔在那里,并没有真正做任何事情。从最后一行中删除 OFFSET 确实可以达到同样的效果。如果我仍然可以使用标签来获取地址,OFFSET 到底有什么用?

Are they really just two ways to achieve the same thing?

是的,程序集有许多 种方法。

C 等价物是
char *p = array; 然后使用 p[0]p[1] 等与使用 array[0]array[1]

将指针放在寄存器中的好处是在重复使用时可以节省一些代码量; 2 字节 mov 指令,仅包含操作码 + ModRM,而不是针对 [disp32] 寻址模式将绝对地址分别编码到每条指令中。

另一个优点是你可以增加指针inc esi。在您没有完全展开循环的其他情况下,您需要寄存器中的指针或索引。

普通指针通常优于 [array + ecx],尤其优于 [array + ecx*4],因为索引寻址模式有一些缺点。 ([array + ecx] 在技术上没有索引;它是 [base + disp32] 并且不需要 SIB 字节,并且不算作 Micro fusion and addressing modes 的索引)。

不过,您可以使用字节偏移量(例如 add ecx, TYPE array),以允许 [base + disp32] 寻址模式进入 int 的静态数组而不是 [disp32 + idx*scale]

每次都使用[disp32]可以避免需要额外的指令将地址放入寄存器。 mov reg, imm32 只是一个 5 字节的单 uop 指令,但在几个静态数组访问之前,它可能仍然不值得为了性能而这样做。这可能取决于您的代码在 uop 缓存中已经很热的频率与它必须 fetch/decode 的频率。 (节省代码大小提高了 L1 I$ 命中率,或者至少意味着更多指令适合一个缓存行,因此如果它在不在最热内循环中的东西中节省代码大小,那么使用更多指令/更多 uops 是值得的.)

在循环之前(未完全展开),您通常需要一条指令将循环计数器/索引归零,例如 xor ecx, ecx。使用 mov reg, imm32 仅长 3 个字节,并且没有额外的微指令。如果您每次使用指针而不是索引寻址模式时都节省 4 或 5 个字节,那么您已经从每次迭代中仅使用一个数组引用中脱颖而出。并且没有额外的微指令。 (忽略执行异或归零指令与 mov-immediate 指令的循环外成本之间的任何细微差异。)

请注意,对于 x86-64,您通常会将静态地址放入具有 7 字节 RIP 相对 LEA 的寄存器中。对于你的代码根本就是 LargeAddressAware,你不能使用 [array + rcx] 因为它只适用于 [disp32 + reg] 寻址模式,而不是 [RIP + rel32].


顺便说一句,为了保持一致性,我推荐这个超过 mov al, array

    mov al, [array + 0]
    mov al, [array + 1]
    ...

您问题下的第一条评论来自您对 mov al, array 然后 mov al, [array + 1] 对相似地址使用 2 种不同语法感到困惑的人;我想 Jester 认为你 打算 mov al, OFFSET array 这样的事情。顺便说一句,你可以这样写(我认为)

mov al, array
mov al, array + 1

但为了清楚起见,我总是建议在内存操作数周围使用方括号。 尤其是当您查看 NASM 语法时, 总是 是必需的,但即使您只使用 MASM,也有人建议采用该约定。 (但请注意,在某些情况下,当没有寄存器时,MASM 会 忽略 括号:Confusing brackets in MASM32 所以不要认为在 MASM 中使用括号会使它像 NASM.)


顺便说一句,加载单个字节的性能高效方法是将其零扩展到完整寄存器中,而不是合并到完整寄存器的低字节中。 movzx eax, byte ptr [esi]


顺便说一句,是的,mov esi, OFFSET array(5 字节)是将静态地址放入寄存器(代码大小和性能)的最有效方法。 lea esi, array是6个字节(操作码+modrm+[disp32]寻址方式),可以运行在更少的执行端口上;切勿在 32 位模式下使用没有寄存器的 LEA。

在 64 位模式下,您需要 lea rsi, array,因为 MASM 会自动使用 RIP 相对寻址,这正是您想要的。否则,对于非 LargeAddressAware 的代码仍然使用 mov esi, OFFSET array(是 ESI,不是 RSI)并且仍然可以利用使用 32 位绝对地址的紧凑代码。