对不是地址/指针的值使用 LEA?

Posted

技术标签:

【中文标题】对不是地址/指针的值使用 LEA?【英文标题】:Using LEA on values that aren't addresses / pointers? 【发布时间】:2021-12-07 09:10:39 【问题描述】:

我试图了解地址计算指令的工作原理,尤其是使用leaq 命令。然后,当我看到使用leaq 进行算术计算的示例时,我会感到困惑。比如下面的C代码,

long m12(long x) 
return x*12;

在汇编中,

leaq (%rdi, %rdi, 2), %rax
salq $2, $rax

如果我的理解是正确的,leaq 应该将任何地址(%rdi, %rdi, 2),应该是2*%rdi+%rdi,评估为%rax。我感到困惑的是,由于值 x 存储在 %rdi 中,这只是内存地址,为什么乘以 %rdi 3 然后将这个 内存地址 左移 2 等于 x 乘以 12?不是说当我们将%rdi 乘以 3 时,我们会跳转到另一个不包含值 x 的内存地址吗?

【问题讨论】:

@Johan,我关闭了 ***.com/questions/13517083/… 作为这个的副本,因为它有更详细的答案,以消除新手对使用非指针 LEA 的困惑。 相关:What's the purpose of the LEA instruction? 主要是在询问 leamov,这是从相反的方向接近相同的问题。那里的答案都在谈论将它用于地址/指针,或者只是说“这是移位和加法指令的愚蠢名称”,两者都只能说明一半。 【参考方案1】:

lea (see Intel's instruction-set manual entry) 是使用内存操作数语法和机器编码的移位加法指令。这解释了名称,但它并不是唯一有用的东西。它实际上从不访问内存,所以它就像在 C 中使用 &

例如见How to multiply a register by 37 using only 2 consecutive leal instructions in x86?

在 C 中,就像uintptr_t foo = (uintptr_t) &arr[idx]。注意& 给你arr + idx(缩放arr 的对象大小,因为这是C 而不是asm)。在 C 中,这将是对语言语法和类型的滥用,但 在 x86 汇编中指针和整数是一回事。 一切都只是字节,这取决于程序将指令以正确的顺序放置以获得有用的结果。

Effective address 是 x86 中的一个技术术语:它表示 seg:off 逻辑地址的“偏移”部分,尤其是在需要计算 base_reg + index*scale + displacement 时。例如rax + (rcx<<2) 中的 %gs:(%rax,%rcx,4) addressing mode。 (但 EA 仍然适用于 %rdistosb,或绝对位移 movabs 加载/存储,或其他没有 ModRM addr 模式的情况)。它在这种情况下的使用并不意味着它必须是一个有效/有用的内存地址,它告诉你计算doesn't involve the segment base 所以它不是计算一个线性地址。 (添加 seg 基数会使其无法用于非平面内存模型中的实际地址数学运算。)


8086 指令集 (Stephen Morse) 的原始设计者/架构师可能会或可能不会将指针数学作为主要用例,但 现代编译器认为它只是进行算术的另一种选择关于指针/整数,人类也应该如此。

(请注意,16 位寻址模式不包括移位,仅包含 [BP|BX] + [SI|DI] + disp8/disp16,因此 LEA 对 386 之前的非指针数学没有那样有用。有关更多信息,请参阅 this Q&A关于 32/64 位寻址模式,尽管该答案使用 Intel 语法,如 [rax + rdi*4],而不是此问题中使用的 AT&T 语法。x86 机器代码是相同的,无论您使用什么语法创建它。)

也许 8086 架构师只是想公开地址计算硬件以供任意使用,因为他们可以在不使用大量额外晶体管的情况下做到这一点。解码器必须能够解码寻址模式,而 CPU 的其他部分必须能够进行地址计算。将结果放入寄存器而不是将其与段寄存器值一起用于内存访问并不需要很多额外的晶体管。 Ross Ridge confirms 原 8086 上的 LEA 重用了 CPU 的有效地址解码和计算硬件。


请注意,大多数现代 CPU 在与普通加法和移位指令相同的 ALU 上运行 LEA。它们具有专用的 AGU(地址生成单元),但仅将它们用于实际的内存操作数。有序原子是一个例外。 LEA 比 ALU 在管道中运行得更早:输入必须更早准备好,但输出也必须更早准备好。乱序执行 CPU(所有现代 x86)不希望 LEA 干扰实际的加载/存储,因此它们在 ALU 上运行它。

lea 具有良好的延迟和吞吐量,但在大多数 CPU 上不如addmov r32, imm32 的吞吐量好,因此仅当您可以使用lea 保存指令时才使用lea 而不是add。 (见Agner Fog's x86 microarch guide and asm optimization manual和https://uops.info/) Ice Lake 对 Intel 进行了改进,现在可以在所有四个 ALU 端口上运行 LEA。

对于哪种 LEA 是“复杂的”、在较少的端口上运行可以处理它,规则因微架构而异。例如3 分量(两个 + 操作)是 SnB 系列上较慢的情况,具有缩放索引是 Ice Lake 上吞吐量较低的情况。 Alder Lake E-cores (Gracemont) 为 4 个/时钟,但在有索引时为 1 个/时钟,当有索引和位移时为 2 个周期延迟(无论是否有基本寄存器)。当有一个缩放索引或 3 个组件时,Zen 会更慢。 (2c 延迟和 2/clock 从 1c 和 4/clock)。


内部实现无关紧要,但可以肯定的是,将操作数解码为 LEA 与任何其他指令的解码寻址模式共享晶体管。 (因此,即使在现代 CPU 上也存在硬件重用/共享,这些 CPU 不会在 AGU 上执行 lea。)任何其他公开多输入移位和加法指令的方式都会采取操作数的特殊编码。

因此,当 386 扩展寻址模式以包括缩放索引时,它获得了一个“免费”的移位加 ALU 指令,并且能够在寻址模式下使用任何寄存器使得 LEA 更容易用于非指针也是。

x86-64 通过 LEA “免费”获得了对程序计数器 (instead of needing to read what call pushed) 的廉价访问,因为它添加了 RIP 相对寻址模式,使得在 x86-64 位置无关代码中访问静态数据的成本明显低于在 32 位 PIC 中。 (RIP-relative 确实需要在处理 LEA 的 ALU 以及处理实际加载/存储地址的单独 AGU 中提供特殊支持。但不需要新指令。)


它对于任意算术和对于指针一样好,所以认为它现在是为指针设计的是错误的。将它用于非指针不是“滥用”或“技巧”,因为在汇编语言中一切都是整数。它的吞吐量低于add,但它足够便宜,几乎可以在节省一条指令时使用。但它最多可以保存三个指令:

;; Intel syntax.
lea  eax, [rdi + rsi*4 - 8]   ; 3 cycle latency on Intel SnB-family
                              ; 2-component LEA is only 1c latency

 ;;; without LEA:
mov  eax, esi             ; maybe 0 cycle latency, otherwise 1
shl  eax, 2               ; 1 cycle latency
add  eax, edi             ; 1 cycle latency
sub  eax, 8               ; 1 cycle latency

在某些 AMD CPU 上,即使是复杂的 LEA 也只有 2 个周期的延迟,但 4 条指令序列从 esi 准备好到最终 eax 准备好有 4 个周期的延迟。无论哪种方式,这都为前端的解码和发布节省了 3 微秒,并且一直占用重新排序缓冲区中的空间,直到退休。

lea 有几个主要优点,尤其是在寻址模式可以使用任何寄存器并且可以移位的 32/64 位代码中:

非破坏性:在不是输入之一的寄存器中输出。有时它只是像lea 1(%rdi), %eaxlea (%rdx, %rbp), %ecx 这样的复制和添加很有用。 可以在一条指令中执行 3 或 4 个操作(见上文)。 无需修改 EFLAGS 的数学运算,在 cmovcc 之前的测试之后可以很方便。或者可能在带有部分标志停顿的 CPU 上的加法循环中。 x86-64:位置无关代码可以使用 RIP 相对 LEA 来获取指向静态数据的指针。

7 字节 lea foo(%rip), %rdimov $foo, %edi(5 字节)稍大和慢,因此在符号位于虚拟地址空间的低 32 位的操作系统(如 Linux)上,在位置相关代码中更喜欢 mov r32, imm32 .您可能需要disable the default PIE setting in gcc 才能使用它。

在 32 位代码中,mov edi, OFFSET symbol 同样比lea edi, [symbol] 更短、更快。 (在 NASM 语法中省略 OFFSET。)RIP-relative 不可用,并且地址适合 32 位立即数,因此如果您需要获取静态符号,没有理由考虑 lea 而不是 mov r32, imm32地址到寄存器中。

除了 x86-64 模式下的 RIP 相对 LEA 之外,所有这些都同样适用于计算指针与计算非指针整数加法/移位。

另请参阅x86

【参考方案2】:

leaq 没有 对内存地址进行操作,它计算一个地址,它实际上并不读取结果,所以在mov 或类似的人尝试使用它之前,这只是添加一个数字的一​​种深奥方法,再加上另一个数字的 1、2、4 或 8 倍(或在这种情况下为相同的数字)。如您所见,出于数学目的,它经常被“滥用”2*%rdi+%rdi 就是 3 * %rdi,所以它在计算 x * 3 时不涉及 CPU 上的乘法器单元。

同样,对于整数,左移,由于二进制数的工作方式(与十进制数相同,在右侧添加零乘以 10 )。

所以这是滥用 leaq 指令来完成乘以 3,然后将结果移位以实现进一步乘以 4,以获得乘以 12 的最终结果,而无需实际使用乘法指令(它可能认为会运行得更慢,据我所知,这可能是正确的;事后猜测编译器通常是一场失败的游戏)。

:需要明确的是,这不是误用意义上的滥用,只是以与您的隐含目的不明确一致的方式使用它' d期望它的名字。以这种方式使用它是 100% 没问题的。

【讨论】:

那么,如果我们传入 x 为 1。假设寄存器是 4 位,%rdi 将是 0001 还是 0x1 ? (如果我们忽略 long 类型) 我认为这不是 LEA 的滥用,复制和添加是通过 lea 指令公开 CPU 的地址生成能力的预期目的之一。看我的回答。 @ZhiyuanRuan 是的,像int/short/long/... 这样的类型是常见的x86-64 ABIs 传值,当以ABI 一致的方式调用某些函数时,值本身在寄存器中。编译器的原始程序集不涉及内存地址。 @PeterCordes:“滥用”主要与用于描述指令的术语(加载有效地址)有关;它是为地址生成而设计的,但寄存器是寄存器,无论哪种方式,数学都是一样的。我并不是说使用lea不好,只是说明的名称不会让你相信它的目的。 那是我不同意的地方。我认为它旨在公开硬件的地址生成功能以用于任意目的。这就是编译器的想法,人类也应该如此。命名只是与它使用寻址模式语法和机器编码这一事实有关,而不是“预期”目的。 (我真的不知道英特尔的想法,正如我在回答中所说,但我认为以这种方式向初学者解释它使使用 LEA 听起来很正常,因为它正常的。那是为什么我不喜欢“滥用”这个词,但这是使用它的合理理由。)【参考方案3】:

LEA is for calculating the address。它不会取消引用内存地址

Intel syntax 应该更易读

m12(long):
  lea rax, [rdi+rdi*2]
  sal rax, 2
  ret

所以第一行相当于rax = rdi*3 然后左移就是将rax乘以4,得到rdi*3*4 = rdi*12

【讨论】:

以上是关于对不是地址/指针的值使用 LEA?的主要内容,如果未能解决你的问题,请参考以下文章

在 C/C++ 中,是不是可以通过使用指针更改“重要”内存地址的值来创建简单的恶意软件?

GO 一文搞懂指针和地址值的区别

c语言如何输出指针所指向的值

LEA rep stos 指令学习

golang-指针,函数,map

C语言中的二维数组名是一个二重指针吗?