I. *_*edy 756
正如其他人所指出的那样,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是在偏移量在struct 4个字节),这语句可以被编译成:
MOV EDX, [EBX + 8*EAX + 4]    ; right side is "effective address"
这将降落y在EDX.比例因子为8是因为每个Point大小为8个字节.现在考虑与"address of"运算符一起使用的相同表达式:
int *p = &points[i].ycoord;
在这种情况下,您不需要其值ycoord,而是其地址.这就是LEA(加载有效地址)的来源.而不是a MOV,编译器可以生成
LEA ESI, [EBX + 8*EAX + 4]
这将加载地址ESI.
Fra*_*ger 534
从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).其他的用例是在循环中派上用场:之间的区别LEA EAX, [ EAX + 1 ],并INC EAX是后者的变化EFLAGS,但前者没有; 这保留了CMP国家.
小智 105
该LEA指令的另一个重要特征是它不会改变条件代码,例如CF和ZF,在通过算术指令计算地址时,ADD或者MUL是.此功能降低了指令之间的依赖性级别,从而为编译器或硬件调度程序进一步优化留出了空间.
hda*_*nte 88
尽管有各种解释,LEA是一个算术运算:
LEA Rt, [Rs1+a*Rs2+b] =>  Rt = Rs1 + a*Rs2 + b
只是它的名字对于shift + add操作极其愚蠢.原因已在最高评级的答案中解释(即它被设计为直接映射高级内存引用).
GJ.*_*GJ. 74
也许只是关于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
Meh*_*ari 57
lea是"加载有效地址"的缩写.它将源操作数的位置引用的地址加载到目标操作数.例如,您可以使用它:
lea ebx, [ebx+eax*8]
使用单个指令进一步移动ebx指针eax项(在64位/元素数组中).基本上,您可以从x86架构支持的复杂寻址模式中受益,从而有效地操作指针.
Dav*_*zer 22
使用LEAa 的最大原因MOV是,如果需要对用于计算地址的寄存器执行算术运算.实际上,您可以有效地对几个寄存器中的指针算术进行有效的"免费"操作.
令人困惑的是,你通常会写一个LEA像a一样MOV但你实际上并没有取消引用内存.换一种说法:
MOV  EAX, [ESP+4]
这将移动ESP+4指向的内容EAX.
LEA EAX, [EBX*8]
这会将有效地址移动EBX * 8到EAX中,而不是在该位置找到的地址.正如您所看到的,也可以将因子乘以2(缩放),而a MOV则限制为加/减.
sup*_*cat 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.哎呀.
Tho*_*son 11
如所提到的现有答案,LEA具有在不访问存储器的情况下执行存储器寻址算法的优点,将算术结果保存到不同的寄存器而不是简单形式的添加指令.真正的基本性能优势是现代处理器具有单独的LEA ALU单元和端口,用于有效地生成地址(包括LEA和其他存储器参考地址),这意味着LEAALU中的算术运算和其他正常算术运算可以并行完成核心.
有关LEA单元的一些详细信息,请查看Haswell架构的这篇文章:http: //www.realworldtech.com/haswell-cpu/4/
另一个重要的一点是在其他答案中没有提到的是LEA REG, [MemoryAddress]指令是PIC(位置无关代码),它在该指令中编码PC相对地址以供参考MemoryAddress.这与MOV REG, MemoryAddress编码相对虚拟地址不同,需要在现代操作系统中重定位/修补(如ASLR是常见功能).因此LEA可以用于将这种非PIC转换为PIC.
似乎很多答案已经完成,我想再添加一个示例代码,以显示 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
小智 7
LEA指令可用于避免CPU对有效地址进行耗时的计算.如果重复使用地址,则将其存储在寄存器中更有效,而不是每次使用时计算有效地址.
LEA(加载有效地址)指令是一种获取由任何Intel处理器的存储器寻址模式产生的地址的方法.
也就是说,如果我们有这样的数据移动:
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
这是一个非常相似的编码,看到了吗?正义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不是时髦的MOV。当您使用 时MOV,它会计算地址并访问内存。LEA只是计算地址,它并不实际访问内存。这就是区别。
在 8086 及更高版本中,LEA只需将最多两个源寄存器和一个立即值设置到目标寄存器。例如,将plus加 3 的lea bp, [bx+si+3]和设置到 bp 寄存器。使用 无法实现将结果保存到寄存器的计算。bxsiMOV
80386处理器引入了一系列缩放模式,其中可以将索引寄存器值乘以有效的缩放因子以获得位移。有效的比例因子为 1、2、4 和 8。因此,您可以使用类似 的指令lea ebp, [ebx+esi*8+3]。
与 相比LEA,有指令LDS和LES,相反,它们将值从内存加载到一对寄存器:一个段寄存器(DS或ES)和一个通用寄存器。还有其他寄存器的版本:分别为LFS、LGS和LSS、FS和段寄存器(在 80386 中引入)。GSSS
因此,这些指令加载“远”指针 - 一个由 16 位段选择器和 16 位(或 32 位,取决于模式)偏移量组成的指针,因此总远指针大小为 32 位16位模式下为48位,32位模式下为48位。
\n这些是 16 位模式的方便指令,无论是 16 位实模式还是 16 位保护模式。
\n在32位模式下,不需要这些指令,因为操作系统将所有段基址设置为零(平坦内存模型),因此不需要加载段寄存器。我们只使用 32 位指针,而不是 48 位。
\n在 64 位模式下,这些指令不被执行。它们的操作码会产生访问冲突中断(异常)。自从英特尔实施 VEX -“矢量扩展 - (AVX) 以来,英特尔采用了 和 的操作码LDS并LES开始将它们用于 VEX 前缀。正如 Peter Cordes 指出的那样,这就是为什么只有 x/ymm0..7 在 32 位中可以访问模式(引用):“VEX 前缀经过精心设计,仅与 32 位模式下的 LDS 和 LES 的无效编码重叠,其中 R\xcc\x85 X\xcc\x85 B\xcc\x85 均为 1。这就是为什么VEX 前缀中的一些位被反转”。
小智 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指令.
| 归档时间: | 
 | 
| 查看次数: | 542630 次 | 
| 最近记录: |