对我来说,这就像一部时髦的电影。它的用途是什么?我应该什么时候使用它?


当前回答

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

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

MOV EAX, <MEM-OPERAND>

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

如果我们用LEA替换MOV,那么内存位置的地址将通过<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标志也没有影响。)

与从地址0加载值相比:

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

这是一个非常相似的编码,看到了吗?只有LEA的8d已更改为8b。

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

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

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

其他回答

尽管有各种解释,LEA是一种算术运算:

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

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

正如其他人所指出的,LEA(负载有效地址)经常被用作进行某些计算的“技巧”,但这并不是它的主要目的。x86指令集是为支持Pascal和C等高级语言而设计的,在这些语言中,数组特别是int数组或小型结构是常见的。例如,考虑表示(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"

其将在EDX中降落y。比例因子为8是因为每个点的大小为8字节。现在考虑与“address of”运算符使用的相同表达式&:

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

在这种情况下,您不需要ycoord的值,而是需要它的地址。这就是LEA(加载有效地址)的作用

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

这将在ESI中加载地址。

似乎很多答案都已经完成了,我想再添加一个示例代码,说明当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

在MOV上使用LEA的最大原因是,如果需要对用于计算地址的寄存器执行算术运算。实际上,您可以在几个寄存器上组合有效地“免费”执行相当于指针运算的操作

真正令人困惑的是,您通常会像MOV一样编写LEA,但实际上并没有取消对内存的引用。换句话说:

移动EAX,[ESP+4]

这将把ESP+4点的内容移动到EAX中。

LEA-EAX,[EBX*8]

这将把有效地址EBX*8移动到EAX,而不是在该位置找到的地址。正如您所看到的,当MOV仅限于加法/减法时,也可以乘以2的因子(缩放)。

LEA vs MOV(回复原始问题)

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

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

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

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

与LEA相反,有指令LDS和LES,相反,它们将值从内存加载到一对寄存器:一个段寄存器(DS或ES)和一个通用寄存器。其他寄存器也有版本:分别用于FS、GS和SS段寄存器的LFS、LGS和LSS(80386中介绍)。

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

这些是16位模式的方便说明,无论是16位实际模式还是16位保护模式。

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

在64位模式下,不执行这些指令。它们的操作码会导致访问违规中断(异常)。自从Intel实施VEX“矢量扩展”(AVX)以来,Intel采用了LDS和LES的操作码,并开始将它们用于VEX前缀。正如Peter Cordes所指出的,这就是为什么在32位模式下只能访问x/ymm0..7的原因(引用):“VEX前缀经过精心设计,仅与32位模式中LDS和LES的无效编码重叠,其中RÜxÜBÜ均为1。这就是为什么某些位在VEX前缀中被反转的原因”。