LEA指令的目的是什么?

时间:2009-11-01 20:57:27

标签: assembly x86 x86-64 x86-16

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

17 个答案:

答案 0 :(得分:725)

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

struct Point
{
     int xcoord;
     int ycoord;
};

现在想象一下如下声明:

int y = points[i].ycoord;

其中points[]Point的数组。假设数组的基数已经在EBX中,变量iEAX中,而xcoordycoord都是32位(所以{{ 1}}在结构中偏移4个字节),该语句可以编译为:

ycoord

MOV EDX, [EBX + 8*EAX + 4] ; right side is "effective address" 放入y。比例因子为8是因为每个EDX的大小为8个字节。现在考虑与“地址”运算符&:

使用相同的表达式
Point

在这种情况下,您不需要int *p = &points[i].ycoord; 的值,而是它的地址。这就是ycoord(加载有效地址)的来源。而不是LEA,编译器可以生成

MOV

将加载LEA ESI, [EBX + 8*EAX + 4] 中的地址。

答案 1 :(得分:511)

来自Abrash的“大会禅”

  

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

     

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

     
      
  1. 使用两个或三个操作数执行添加的能力,
  2.   
  3. 将结果存储在任何寄存器中的能力;不只是其中一个源操作数。
  4.   

并且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州。

答案 2 :(得分:94)

LEA指令的另一个重要特性是,它不会改变条件代码,例如CFZF,而是通过算术指令计算地址,例如ADDMUL。此功能降低了指令之间的依赖性,从而为编译器或硬件调度程序提供了进一步优化的空间。

答案 3 :(得分:78)

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

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

只是它的名字对于shift + add操作极其愚蠢。其原因已在最高评级的答案中解释(即它旨在直接映射高级别的内存参考)。

答案 4 :(得分:69)

也许只是关于LEA指令的另一件事。 您还可以使用LEA将快速乘法寄存器设置为3,5或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

答案 5 :(得分:55)

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

lea ebx, [ebx+eax*8]

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

答案 6 :(得分:22)

使用LEA超过MOV的最大原因是,您需要对用于计算地址的寄存器执行算术运算。实际上,对于" free,你可以有效地对几个寄存器中的指针算术执行相应的操作。"

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

MOV EAX, [ESP+4]

这会将ESP+4指向的内容移至EAX

LEA EAX, [EBX*8]

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

答案 7 :(得分:18)

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

请注意,忘记世界“偏移”会导致位置[BP + 8]的内容而不是值8添加到DI。糟糕。

答案 8 :(得分:10)

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

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

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

答案 9 :(得分:7)

LEA指令可用于避免CPU对有效地址进行耗时的计算。如果重复使用地址,则将其存储在寄存器中更有效,而不是每次使用时都计算有效地址。

答案 10 :(得分:6)

这是一个例子。

// 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(optimize)作为编译器选项,gcc将找到指定代码行的lea指令。

答案 11 :(得分:6)

LEA(加载有效地址)指令是一种获取任何Intel处理器存储器寻址模式所产生地址的方法。

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

var thisIsAudioSrc = $('#jp_audio_0').attr('src');

将指定存储单元的内容移动到目标寄存器中。

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

<MEM-OPERAND>不是特定的算术指令;它是一种拦截处理器的任何一种存储器寻址模式产生的有效地址的方法。

例如,我们可以在简单的直接地址上使用LEA。根本没有算术:

LEA

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

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

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

这就是为什么认为$ 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 中的括号多余的人严重错误的原因;括号不是LEA语法,而是寻址模式的一部分。

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

与加载地址零的值对比:

LEA

这是一个非常相似的编码,请参阅?只有$ as movl 0, %eax $ objdump -d a.out | grep mov 0: 8b 04 25 00 00 00 00 mov 0x0,%eax 的{​​{1}}已更改为8d

当然,此LEA编码比将{0}立即调整为8b要长:

LEA

EAX没有理由排除这种可能性,只是因为有一个较短的选择;它只是以正交方式与可用的寻址模式相结合。

答案 12 :(得分:4)

LEA:只是一个&#34;算术&#34;指令..

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

答案 13 :(得分:1)

似乎许多答案已经完成,我想再添加一个示例代码,以显示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

答案 14 :(得分:0)

请原谅我,如果有人已经提到过,但是在x86时代内存分段仍然很重要的时候,您可能无法从这两条指令获得相同的结果:

LEA AX, DS:[0x1234]

LEA AX, CS:[0x1234]

答案 15 :(得分:0)

所有常规的“计算”指令(例如加乘法,异或)或设置状态标志(例如零,符号)。如果使用复杂的地址,则AX xor:= mem[0x333 +BX + 8*CX]的标志将根据异或运算进行设置。

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

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

底线

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

LEA, AX'| [BX]

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

答案 16 :(得分:-6)

它因为你编写了代码

mov dx,offset something

你可以简单地写

lea dx,something