787

对我来说,它就像一个时髦的 MOV。它的目的是什么,我应该什么时候使用它?

4

17 回答 17

918

正如其他人指出的那样,LEA(加载有效地址)通常用作进行某些计算的“技巧”,但这不是其主要目的。x86 指令集旨在支持 Pascal 和 C 等高级语言,其中数组(尤其是整数数组或小型结构)很常见。例如,考虑一个表示 (x, y) 坐标的结构:

struct Point
{
     int xcoord;
     int ycoord;
};

现在想象这样一个语句:

int y = points[i].ycoord;

哪里points[]是一个数组Point。假设数组的基数已经在 中EBX,变量i在 中EAX,并且xcoord每个ycoord都是 32 位(ycoord结构体中的偏移量 4 字节也是如此),该语句可以编译为:

MOV EDX, [EBX + 8*EAX + 4]    ; right side is "effective address"

这将降落yEDX. 比例因子 8 是因为每个Point大小为 8 个字节。现在考虑与“地址”运算符 & 一起使用的相同表达式:

int *p = &points[i].ycoord;

在这种情况下,您不需要 的值ycoord,而是它的地址。这就是LEA(加载有效地址) 的用武之地。MOV编译器可以生成

LEA ESI, [EBX + 8*EAX + 4]

这将加载地址ESI

于 2009-11-03T06:25:00.017 回答
610

来自 Abrash 的“集会禅”

LEA,唯一执行内存寻址计算但实际上并不寻址内存的指令。LEA接受标准内存寻址操作数,但仅将计算出的内存偏移量存储在指定寄存器中,该寄存器可以是任何通用寄存器。

这给了我们什么?ADD没有提供的两件事:

  1. 使用两个或三个操作数执行加法的能力,以及
  2. 将结果存储在任何寄存器中的能力;不仅仅是源操作数之一。

并且LEA不会改变标志。

例子

  • LEA EAX, [ EAX + EBX + 1234567 ]计算EAX + EBX + 1234567(即三个操作数)
  • LEA EAX, [ EBX + ECX ]计算EBX + ECX而不用结果覆盖任何一个。
  • 乘以常数(乘以二、三、五或九),如果您像这样使用它LEA EAX, [ EBX + N * EBX ](N 可以是 1、2、4、8)。

其他用例在循环中很方便: 和 之间的区别在于LEA EAX, [ EAX + 1 ]后者INC EAX改变EFLAGS但前者没有;这保留了CMP状态。

于 2009-11-01T21:03:38.647 回答
126

Another important feature of the LEA instruction is that it does not alter the condition codes such as CF and ZF, while computing the address by arithmetic instructions like ADD or MUL does. This feature decreases the level of dependency among instructions and thus makes room for further optimization by the compiler or hardware scheduler.

于 2012-10-09T17:35:41.383 回答
108

尽管有所有的解释,LEA 是一个算术运算:

LEA Rt, [Rs1+a*Rs2+b] =>  Rt = Rs1 + a*Rs2 + b

只是它的名字对于 shift+add 操作来说是非常愚蠢的。其原因已在评分最高的答案中进行了解释(即,它旨在直接映射高级内存引用)。

于 2011-08-15T21:43:29.480 回答
88

Maybe just another thing about LEA instruction. You can also use LEA for fast multiplying registers by 3, 5 or 9.

LEA EAX, [EAX * 2 + EAX]   ;EAX = EAX * 3
LEA EAX, [EAX * 4 + EAX]   ;EAX = EAX * 5
LEA EAX, [EAX * 8 + EAX]   ;EAX = EAX * 9
于 2009-11-02T10:16:28.683 回答
70

lea是“加载有效地址”的缩写。它将源操作数的位置引用的地址加载到目标操作数。例如,您可以使用它来:

lea ebx, [ebx+eax*8]

用一条指令进一步移动ebx指针项(在 64 位/元素数组中)。eax基本上,您可以从 x86 架构支持的复杂寻址模式中受益,从而有效地操作指针。

于 2009-11-01T21:00:24.813 回答
30

The biggest reason that you use LEA over a MOV is if you need to perform arithmetic on the registers that you are using to calculate the address. Effectively, you can perform what amounts to pointer arithmetic on several of the registers in combination effectively for "free."

What's really confusing about it is that you typically write an LEA just like a MOV but you aren't actually dereferencing the memory. In other words:

MOV EAX, [ESP+4]

This will move the content of what ESP+4 points to into EAX.

LEA EAX, [EBX*8]

This will move the effective address EBX * 8 into EAX, not what is found in that location. As you can see, also, it is possible to multiply by factors of two (scaling) while a MOV is limited to adding/subtracting.

于 2015-05-05T22:22:04.190 回答
26

8086 有一大类指令,它们接受寄存器操作数和有效地址,执行一些计算以计算该有效地址的偏移部分,并执行一些涉及寄存器和计算地址所引用的内存的操作。除了跳过实际的内存操作之外,让该系列中的一条指令的行为与上述相同是相当简单的。因此,说明:

mov ax,[bx+si+5]
lea ax,[bx+si+5]

在内部实现几乎相同。不同之处在于跳过了一步。两条指令的工作方式如下:

temp = fetched immediate operand (5)
temp += bx
temp += si
address_out = temp  (skipped for LEA)
trigger 16-bit read  (skipped for LEA)
temp = data_in  (skipped for LEA)
ax = temp

至于为什么英特尔认为这条指令值得包括在内,我不太确定,但实施起来很便宜这一事实将是一个重要因素。另一个因素是英特尔的汇编器允许相对于BP寄存器定义符号。如果fnord被定义为BP相对符号(例如BP+8),可以说:

mov ax,fnord  ; Equivalent to "mov ax,[BP+8]"

如果有人想使用诸如stosw将数据存储到 BP 相对地址之类的东西,可以说

mov ax,0 ; Data to store
mov cx,16 ; Number of words
lea di,fnord
rep movs fnord  ; Address is ignored EXCEPT to note that it's an SS-relative word ptr

比:

mov ax,0 ; Data to store
mov cx,16 ; Number of words
mov di,bp
add di,offset fnord (i.e. 8)
rep movs fnord  ; Address is ignored EXCEPT to note that it's an SS-relative word ptr

请注意,忘记世界“偏移量”会导致 location 的内容[BP+8],而不是值 8 被添加到DI. 哎呀。

于 2012-04-29T19:46:51.630 回答
14

LEA(加载有效地址)指令是一种获取来自任何英特尔处理器内存寻址模式的地址的方法。

也就是说,如果我们有这样的数据移动:

MOV EAX, <MEM-OPERAND>

它将指定内存位置的内容移动到目标寄存器中。

如果我们替换MOVby ,那么内存位置的地址将通过寻址表达式LEA以完全相同的方式计算。<MEM-OPERAND>但是,我们将位置本身放入目的地,而不是内存位置的内容。

LEA不是特定的算术指令;它是一种拦截由处理器的任何一种内存寻址模式产生的有效地址的方法。

例如,我们可以LEA只使用一个简单的直接地址。根本不涉及算术:

MOV EAX, GLOBALVAR   ; fetch the value of GLOBALVAR into EAX
LEA EAX, GLOBALVAR   ; fetch the address of GLOBALVAR into EAX.

这是有效的;我们可以在 Linux 提示符下测试它:

$ as
LEA 0, %eax
$ objdump -d a.out

a.out:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <.text>:
   0:   8d 04 25 00 00 00 00    lea    0x0,%eax

在这里,没有添加缩放值,也没有偏移。零被移入 EAX。我们也可以使用带有立即操作数的 MOV 来做到这一点。

这就是为什么认为括号中的括号LEA是多余的人严重错误的原因;括号不是LEA语法,而是寻址模式的一部分。

LEA 在硬件层面是真实的。生成的指令对实际寻址模式进行编码,处理器将其执行到计算地址的点。然后它将该地址移动到目的地,而不是生成内存引用。(由于任何其他指令中寻址模式的地址计算对 CPU 标志LEA没有影响,因此对 CPU 标志没有影响。)

与从地址零加载值相反:

$ as
movl 0, %eax
$ objdump -d a.out | grep mov
   0:   8b 04 25 00 00 00 00    mov    0x0,%eax

这是一个非常相似的编码,明白吗?只是 的8dLEA更改为8b

当然,这种LEA编码比将立即零移动到 中要长EAX

$ as
movl $0, %eax
$ objdump -d a.out | grep mov
   0:   b8 00 00 00 00          mov    $0x0,%eax

没有理由LEA排除这种可能性,尽管只是因为有一个更短的替代方案;它只是以正交方式与可用的寻址模式相结合。

于 2017-04-26T19:25:03.210 回答
13

正如提到的现有答案,LEA具有在不访问内存的情况下执行内存寻址算术的优点,将算术结果保存到不同的寄存器而不是简单的加法指令形式。真正的潜在性能优势在于现代处理器具有单独的 LEA ALU 单元和端口,用于有效地址生成(包括LEA和其他内存引用地址),这意味着 ALU 中的算术运算LEA和其他正常算术运算可以在一个并行完成核。

有关 LEA 单元的一些详细信息,请查看 Haswell 架构的这篇文章: http ://www.realworldtech.com/haswell-cpu/4/

其他答案中未提及的另一个重要点是LEA REG, [MemoryAddress]指令是 PIC(位置无关代码),它对该指令中的 PC 相对地址进行编码以供参考MemoryAddress。这与MOV REG, MemoryAddress编码相对虚拟地址不同,并且需要在现代操作系统中重新定位/修补(如 ASLR 是常见功能)。所以LEA可以用来将这种非PIC转换为PIC。

于 2014-07-31T03:41:52.163 回答
9

LEA 指令可用于避免 CPU 对有效地址的耗时计算。如果一个地址被重复使用,将它存储在寄存器中比每次使用时计算有效地址更有效。

于 2013-06-04T20:32:59.460 回答
9

似乎很多答案已经完成,我想再添加一个示例代码来展示 lea 和 move 指令在具有相同表达式格式时如何不同地工作。

长话短说,lea 指令和 mov 指令都可以与包含指令的 src 操作数的括号一起使用。当它们被 () 括起来时,()的表达式以同样的方式计算;但是,两条指令将以不同的方式解释 src 操作数中的计算值。

无论表达式与 lea 还是 mov 一起使用,src 值的计算如下。

D ( Rb, Ri, S ) => (Reg[Rb]+S*Reg[Ri]+ D)

但是,当它与 mov 指令一起使用时,它会尝试访问由上述表达式生成的地址所指向的值并将其存储到目的地。

相反,当使用上述表达式执行 lea 指令时,它将生成的值原样加载到目的地。

下面的代码使用相同的参数执行 lea 指令和 mov 指令。但是,为了捕捉差异,我添加了一个用户级信号处理程序来捕捉由于 mov 指令访问错误地址而导致的分段错误。

示例代码

#define _GNU_SOURCE 1  /* To pick up REG_RIP */
#include <stdio.h> 
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <signal.h>


uint32_t
register_handler (uint32_t event, void (*handler)(int, siginfo_t*, void*))
{
        uint32_t ret = 0;
        struct sigaction act;

        memset(&act, 0, sizeof(act));
        act.sa_sigaction = handler;
        act.sa_flags = SA_SIGINFO;
        ret = sigaction(event, &act, NULL);
        return ret;
}

void
segfault_handler (int signum, siginfo_t *info, void *priv)
{
        ucontext_t *context = (ucontext_t *)(priv);
        uint64_t rip = (uint64_t)(context->uc_mcontext.gregs[REG_RIP]);
        uint64_t faulty_addr = (uint64_t)(info->si_addr);

        printf("inst at 0x%lx tries to access memory at %ld, but failed\n",
                rip,faulty_addr);
        exit(1);
}

int
main(void)
{
        int result_of_lea = 0;

        register_handler(SIGSEGV, segfault_handler);

        //initialize registers %eax = 1, %ebx = 2

        // the compiler will emit something like
           // mov $1, %eax
           // mov $2, %ebx
        // because of the input operands
        asm("lea 4(%%rbx, %%rax, 8), %%edx \t\n"
            :"=d" (result_of_lea)   // output in EDX
            : "a"(1), "b"(2)        // inputs in EAX and EBX
            : // no clobbers
         );

        //lea 4(rbx, rax, 8),%edx == lea (rbx + 8*rax + 4),%edx == lea(14),%edx
        printf("Result of lea instruction: %d\n", result_of_lea);

        asm volatile ("mov 4(%%rbx, %%rax, 8), %%edx"
                       :
                       : "a"(1), "b"(2)
                       : "edx"  // if it didn't segfault, it would write EDX
          );
}

执行结果

Result of lea instruction: 14
inst at 0x4007b5 tries to access memory at 14, but failed
于 2019-01-03T08:40:25.200 回答
7

这是一个例子。

// compute parity of permutation from lexicographic index
int parity (int p)
{
  assert (p >= 0);
  int r = p, k = 1, d = 2;
  while (p >= k) {
    p /= d;
    d += (k << 2) + 6; // only one lea instruction
    k += 2;
    r ^= p;
  }
  return r & 1;
}

使用 -O(优化)作为编译器选项,gcc 将找到指定代码行的 lea 指令。

于 2014-05-16T19:40:55.180 回答
5

LEA:只是一个“算术”指令..

MOV 在操作数之间传输数据,但 lea 只是在计算

于 2015-05-05T21:06:00.380 回答
4

所有正常的“计算”指令,如加乘、异或设置状态标志,如零、符号。如果您使用复杂的地址,AX xor:= mem[0x333 +BX + 8*CX] 则根据 xor 操作设置标志。

现在您可能想要多次使用该地址。将这样的地址加载到寄存器中绝不是为了设置状态标志,幸运的是它没有。短语“加载有效地址”使程序员意识到这一点。这就是奇怪的表达的来源。

很明显,一旦处理器能够使用复杂的地址来处理其内容,它就能够为其他目的计算它。实际上,它可以用于x <- 3*x+1在一条指令中执行转换。这是汇编编程中的一般规则:使用说明,但它会摇晃你的船。 唯一重要的是指令所体现的特定转换是否对您有用。

底线

MOV, X| T| AX'| R| BX|

LEA, AX'| [BX]

对AX具有相同的效果,但对状态标志没有效果。(这是ciasdis表示法。)

于 2019-09-04T22:06:13.367 回答
2

LEA vs MOV(回复原始问题)

LEA不是时髦的MOV。使用时MOV,它会计算地址并访问内存。LEA只是计算地址,它实际上并不访问内存。这就是区别。

在 8086 及更高版本中,LEA只需将最多两个源寄存器的总和和一个立即值设置为目标寄存器。例如,将加号加 3lea bp, [bx+si+3]的总和设置到 bp 寄存器。您无法实现此计算以将结果保存到带有 的寄存器中。bxsiMOV

80386处理器引入了一系列缩放模式,其中索引寄存器的值可以乘以一个有效的缩放因子来获得位移。有效的比例因子是 1、2、4 和 8。因此,您可以使用lea ebp, [ebx+esi*8+3].

LDS & LES(可选进一步阅读)

与 相比LEA,有指令LDSLES,相反,将值从内存加载到寄存器对:一个段寄存器(DSES)和一个通用寄存器。其他寄存器也有版本:LFSLGSLSS、和段寄存器FS,分别(在 80386 中引入)。GSSS

因此,这些指令加载“远”指针——一个由 16 位段选择器和 16 位(或 32 位,取决于模式)偏移量组成的指针,因此远指针的总大小为 32 位16 位模式和 32 位模式下的 48 位。

这些是 16 位模式的便捷指令,无论是 16 位实模式还是 16 位保护模式。

在 32 位模式下,不需要这些指令,因为操作系统将所有段基数设置为零(平面内存模型),因此不需要加载段寄存器。我们只使用 32 位指针,而不是 48 位。

在 64 位模式下,这些指令不会被执行。它们的操作码给出访问冲突中断(异常)。自从英特尔实现 VEX -“向量扩展 - (AVX)”以来,英特尔采用了他们的操作码LDSLES开始将它们用于 VEX 前缀。正如 Peter Cordes 指出的那样,这就是为什么只有 x/ymm0..7 可以在 32 位中访问模式(引用):“VEX 前缀经过精心设计,仅与 32 位模式下 LDS 和 LES 的无效编码重叠,其中 R̅ X̅ B̅ 都是 1。这就是为什么 VEX 前缀中的某些位被反转的原因”。

于 2021-08-17T04:46:30.383 回答
1

如果有人已经提到过,请原谅我,但在 x86 时代,内存分段仍然相关,您可能不会从这两个指令中得到相同的结果:

LEA AX, DS:[0x1234]

LEA AX, CS:[0x1234]
于 2020-06-07T03:20:07.520 回答