LEA指令的目的是什么?

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

806292 次浏览

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

lea ebx, [ebx+eax*8]

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

来自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状态。

也许只是关于LEA指令的另一件事。您还可以使用LEA快速将寄存器乘以3、5或9。

LEA EAX, [EAX * 2 + EAX]   ;EAX = EAX * 3LEA EAX, [EAX * 4 + EAX]   ;EAX = EAX * 5LEA EAX, [EAX * 8 + EAX]   ;EAX = EAX * 9

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

struct Point{int xcoord;int ycoord;};

现在想象一下这样的语句:

int y = points[i].ycoord;

其中points[]Point的数组。假设数组的基数已经在EBX中,变量iEAX中,xcoordycoord各为32位(因此ycoord在结构中偏移4个字节),此语句可以编译为:

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

这将使y进入EDX。8的比例因子是因为每个Point的大小为8个字节。现在考虑与“地址”运算符&使用的相同表达式:

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

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

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

这将加载ESI中的地址。

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

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

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

8086有一个庞大的指令家族,它们接受一个寄存器操作数和一个有效地址,执行一些计算来计算该有效地址的偏移部分,并执行一些涉及寄存器和计算地址引用的内存的操作。让该家族中的一条指令表现得像上面一样非常简单,除了跳过实际的内存操作。因此,指令:

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

在内部实现几乎相同。区别是跳过的步骤。两个指令的工作方式如下:

temp = fetched immediate operand (5)temp += bxtemp += siaddress_out = temp  (skipped for LEA)trigger 16-bit read  (skipped for LEA)temp = data_in  (skipped for LEA)ax = temp

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

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

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

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

比以下更方便:

mov ax,0 ; Data to storemov cx,16 ; Number of wordsmov di,bpadd 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。哎呀。

LEA指令的另一个重要特性是它不会更改条件码,例如CFZF,而通过算术指令(例如ADDMUL)计算地址。此特性降低了指令之间的依赖级别,从而为编译器或硬件调度器进一步优化腾出空间。

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

这里有一个例子。

// compute parity of permutation from lexicographic indexint 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 instructionk += 2;r ^= p;}return r & 1;}

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

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

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

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

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

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

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

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

MOV EAX, [ESP+4]

这将把ESP+4指向的内容移动到EAX

LEA EAX, [EBX*8]

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

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

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

MOV EAX, <MEM-OPERAND>

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

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

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

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

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

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

$ asLEA 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标志没有影响。)

与从地址零加载值相比:

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

这是一个非常相似的编码,看到了吗?只是LEA8d更改为8b

当然,这个LEA编码比将直接零移动到EAX要长:

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

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

看起来很多答案已经完成了,我想再添加一个示例代码来展示lea和移动指令在具有相同表达式格式时如何以不同的方式工作。

长话短说,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_tregister_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 operandsasm("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),%edxprintf("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: 14inst at 0x4007b5 tries to access memory at 14, but failed

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

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

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

底线是

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

LEA, AX'| [BX]

AX有相同的效果,但对状态标志没有。(这是ciasdis符号。)

如果有人已经提到了,请原谅我,但如果有人想知道x86过去的糟糕日子,当时内存分割仍然相关:你总是会从这两个指令中得到相同的结果:

LEA AX, DS:[0x1234]

LEA AX, CS:[0x1234]

“有效地址”只是seg: off逻辑地址的偏移部分。在这种情况下,0x1234。

LEA没有添加段基。这将破坏最初的用例之一,用于进行地址数学以获得实际上可以取消引用的指针(偏移量)。例如lea bx, [array + si]。如果添加DS基以给出线性地址,稍后的mov ax, [bx]将添加DS基再次
此外,20位结果通常不适合16位寄存器。

参见https://www.stevemorse.org/8086/index.html-8086的架构师写了一本关于指令集的书,现在在他的网站上免费提供。关于LEA的部分提到了他的一些设计意图。

LEA vs MOV(回复原问题)

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

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

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

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

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

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

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

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

在64位模式下,这些指令没有实现。它们的操作码给出访问违反中断(异常)。自从Intel实现了VEX——“向量扩展——(AVX),Intel拿走了它们的操作码LDSLES,并开始将它们用于VEX前缀。正如Peter Cordes所指出的,这就是为什么只有x/ymm0…7可以在32位模式下访问(引用):“VEX前缀经过精心设计,只与32位模式下LDS和LES的无效编码重叠,其中R X B都是1。这就是为什么一些位在VEX前缀中反转的原因”。