对我来说,这就像一部时髦的MOV。它的目的是什么?何时使用?
对我来说,这就像一部时髦的MOV。它的目的是什么?何时使用?
Answers:
正如其他人指出的那样,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
在在struct偏移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
编译器可以生成而不是,
LEA ESI, [EBX + 8*EAX + 4]
这将在中加载地址ESI
。
mov
说明并放在方括号之外会更干净吗?MOV EDX, EBX + 8*EAX + 4
MOV
带有间接源的,不同之处在于它仅执行间接操作而不执行MOV
。它实际上并不从计算出的地址中读取,而只是对其进行计算。
摘自Abrash 的“汇编禅”:
LEA
,这是唯一执行内存寻址计算但实际上并未寻址内存的指令。LEA
接受标准内存寻址操作数,但是只不过将计算出的内存偏移量存储在指定的寄存器中即可,该寄存器可以是任何通用寄存器。这给了我们什么?
ADD
没有提供的两件事:
- 使用两个或三个操作数执行加法的能力,以及
- 将结果存储在任何寄存器中的能力;不只是源操作数之一。
并且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),则可以乘以常数(乘以2、3、5 或9 )。其他的用例是在循环中派上用场:之间的区别LEA EAX, [ EAX + 1 ]
,并INC EAX
是后者的变化EFLAGS
,但前者没有; 这样可以保留CMP
状态。
LEA EAX, [ EAX + EBX + 1234567 ]
计算的总和EAX
,EBX
和1234567
(这是三个操作数)。LEA EAX, [ EBX + ECX ]
计算时EBX + ECX
不会覆盖任何结果。第三个LEA
用于(未在Frank列出)的是乘以常数(乘以 2、3、5 或9),如果您使用它的话LEA EAX, [ EBX + N * EBX ]
(N
可以是1,2,4,8)。其他的用例是在循环中派上用场:之间的区别LEA EAX, [ EAX + 1 ]
,并INC EAX
是后者的变化EFLAGS
,但前者没有; 这保留了CMP
状态
LEA
可以用于...(请参阅上面IJ Kennedy的流行答案中的“ LEA(有效负载地址)经常被用作进行某些计算的“技巧”))
该LEA
指令的另一个重要特征是,在通过像或那样的算术指令计算地址时,它不会更改条件代码(例如CF
和)。此功能降低了指令之间的依赖性,从而为编译器或硬件调度程序的进一步优化留出了空间。ZF
ADD
MUL
lea
有时对于编译器(或人工编码器)进行数学运算而不会破坏标志结果很有用。但是lea
并没有比快add
。大多数x86指令都写标志。高性能x86实现必须重命名EFLAGS,否则将避免正常代码快速运行后的写后写入危险,因此避免标志写入的指令因此并不是最好的选择。(部分标记的内容可能会造成问题,请参阅INC指令与ADD 1:有关系吗?)
尽管有所有解释,LEA是一种算术运算:
LEA Rt, [Rs1+a*Rs2+b] => Rt = Rs1 + a*Rs2 + b
只是它的名称对于shift + add操作极其愚蠢。评分最高的答案中已经说明了其原因(即,它旨在直接映射高级内存引用)。
LEA
AGU上执行,而是在普通整数ALU上执行。这些天,人们必须非常仔细地阅读CPU规格,以找出“运行的地方” ...
LEA
为您提供由任何与存储器相关的寻址模式产生的地址。这不是移位和加法运算。
也许关于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
LEA EAX, [EAX*3]
呢?
shl
指令一样使用左移,将寄存器乘以2,4,8,16 ...它更快,更短。但是对于乘以2的幂次方不同的数字,我们通常使用mul
更自命不凡和较慢的指令。
lea eax,[eax*3]
将转换为的等价物lea eax,[eax+eax*2]
。
lea
是“加载有效地址”的缩写。它将源操作数将位置引用的地址加载到目标操作数。例如,您可以使用它来:
lea ebx, [ebx+eax*8]
使用单个指令进一步移动ebx
指针eax
项(在64位/元素数组中)。基本上,您将从x86架构支持的复杂寻址模式中受益,可以有效地操作指针。
LEA
在a MOV
上使用的最大原因是是否需要对要用来计算地址的寄存器执行算术运算。有效地,您可以有效地对多个寄存器组合使用相当于“免费”的指针算术运算。
真正令人困惑的是,您通常写的LEA
像a,MOV
但是实际上并没有取消引用内存。换一种说法:
MOV EAX, [ESP+4]
这将把ESP+4
指向的内容移到EAX
。
LEA EAX, [EBX*8]
这会将有效地址EBX * 8
移入EAX,而不是该位置中找到的地址。如您所见,也可以乘以2的因子(缩放比例),而a MOV
仅限于加/减。
LEA
。
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。哎呀。
如现有答案所述,它LEA
具有以下优点:无需访问存储器即可执行存储器寻址算术,将算术结果保存到其他寄存器中,而不是简单的添加指令形式。真正的潜在性能优势是,现代处理器具有单独的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。
lea
在执行其他算术指令的一个或多个相同的ALU上执行(但通常少于其他算术指令)。例如,提到的Haswell CPU可以在四个不同的 ALU 上执行add
或sub
大多数其他基本的算术运算,但只能在一个(复杂)或两个(简单)上执行。更重要的是,具有这两个功能的ALU只是可以执行其他指令的四个中的两个,因此没有所要求的并行性优势。lea
lea
lea
lea
LEA指令可用于避免CPU耗时的有效地址计算。如果地址被重复使用,则将其存储在寄存器中会比每次使用该地址都更有效,而不是计算有效地址。
[esi]
很少比说便宜,[esi + 4200]
而且很少比说便宜[esi + ecx*8 + 4200]
。
[esi]
并不比便宜[esi + ecx*8 + 4200]
。但是为什么要比较呢?它们不相等。如果希望前者指定与后者相同的内存位置,则需要其他说明:您必须将乘以8 esi
的值加起来ecx
。哦,哦,乘法将破坏您的CPU标志!然后,您必须添加4200。这些其他指令会增加代码大小(占用指令高速缓存中的空间,以及获取周期)。
[esi + 4200]
在一系列指令中重复使用类似的东西,那么最好先将有效地址加载到寄存器中并使用它。例如,add eax, [esi + 4200]; add ebx, [esi + 4200]; add ecx, [esi + 4200]
您最好选择,而不是写作lea edi, [esi + 4200]; add eax, [edi]; add ebx, [edi]; add ecx, [edi]
,它通常不会更快。至少那是对这个答案的简单解释。
[esi]
和[esi + 4200]
(或[esi + ecx*8 + 4200]
为这是OP提议简化(据我所知):具有相同复杂的地址N个指令转化为简单的(一个REG)寻址,加上一个N个指令lea
,因为复杂的寻址是“耗时的”,实际上,即使在现代的x86上,它也较慢,但仅在延迟方面,这似乎对于具有相同地址的连续指令似乎不太重要
lea
因此在这种情况下会增加压力。一般而言,储存中间体是引起压力升高的原因,而不是解决方案的压力-但我认为在大多数情况下,这是一种洗礼。@Kaz
LEA(加载有效地址)指令是一种获取地址的方法,该地址源自任何英特尔处理器的内存寻址模式。
也就是说,如果我们有这样的数据移动:
MOV EAX, <MEM-OPERAND>
它将指定存储单元的内容移至目标寄存器。
如果我们将MOV
by 替换为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
这是非常相似的编码,看到了吗?正义8d
的LEA
已更改为8b
。
当然,这种LEA
编码比将立即数零移动到的时间更长EAX
。
$ as
movl $0, %eax
$ objdump -d a.out | grep mov
0: b8 00 00 00 00 mov $0x0,%eax
没有理由LEA
排除这种可能性,尽管仅仅是因为有一个更短的选择。它只是以正交方式与可用的寻址模式结合在一起。
似乎许多答案已经完成,我想再添加一个示例代码,以显示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
=d
用来告诉编译器结果在EDX中,并保存一个mov
。您还忽略了输出的早期提示。这确实演示了您要演示的内容,但这也是一个误导性的内联汇编错误示例,如果在其他情况下使用该内联汇编,它将损坏。对于堆栈溢出答案,这是一件坏事。
%%
在扩展asm中写所有这些寄存器名称,请使用输入约束。喜欢asm("lea 4(%%ebx, %%eax, 8), %%edx" : "=d"(result_of_lea) : "a"(1), "b"(2));
。让编译器初始化寄存器意味着您也不必声明Clobbers。在mov-immediate覆盖整个寄存器之前,您还需要通过异或归零来使事情复杂化。
mov 4(%ebx, %eax, 8), %edx
是无效的?无论如何,是的,因为mov
写"a"(1ULL)
它告诉编译器您有一个64位的值是有意义的,因此需要确保扩展它以填充整个寄存器。实际上,它仍然会使用mov $1, %eax
,因为将EAX零扩展写入RAX中,除非您对周围的代码感到奇怪的情况,其中编译器知道RAX = 0xff00000001
或其他含义。对于lea
,您仍在使用32位操作数大小,因此输入寄存器中的任何杂散高位都不会影响32位结果。
LEA:只是“算术”指令。
MOV在操作数之间传输数据,但是lea只是在计算
mov eax, offset GLOBALVAR
代替。您可以使用LEA,但它的代码大小略大一些,mov r32, imm32
并且可以在更少的端口上运行,因为它仍然需要经过地址计算过程。 lea reg, symbol
仅当需要PIC和/或低32位以下的地址时,才在64位中用于相对RIP的LEA。在32位或16位代码中,优势为零。LEA是一种算术指令,可显示CPU解码/计算寻址模式的能力。
imul eax, edx, 1
不是计算结果:它只是将edx复制到eax。但实际上,它通过乘法器以3个周期的延迟运行数据。或rorx eax, edx, 0
只是复制(旋转零)。
所有正常的“计算”指令(如加法,异或)或设置状态标志(如零,符号)。如果使用复杂的地址,AX xor:= mem[0x333 +BX + 8*CX]
则根据异或运算设置标志。
现在,您可能需要多次使用该地址。从未将这样的地址加载到寄存器中是为了设置状态标志,幸运的是,没有这样做。短语“加载有效地址”使程序员意识到这一点。那就是怪异表情的来源。
显然,一旦处理器能够使用复杂的地址来处理其内容,就可以将其用于其他目的。实际上,它可以用于x <- 3*x+1
在一条指令中执行转换。这是汇编编程中的一条一般规则:使用说明,但会摇晃您的船。
唯一重要的是指令所体现的特定转换是否对您有用。
底线
MOV, X| T| AX'| R| BX|
和
LEA, AX'| [BX]
对AX具有相同的作用,但对状态标志没有影响。(这是ciasdis符号。)
call lbl
lbl: pop rax
技术上讲“正在工作”之类的方法来获取的价值,我不会亲自提供建议rip
,但是您会使分支预测非常不愉快。使用所需的说明,但是如果您做一些棘手的事情也不要感到惊讶,它会带来您没有预料到的后果