对我来说,这就像一部时髦的电影。它的用途是什么?我应该什么时候使用它?
当前回答
似乎很多答案都已经完成了,我想再添加一个示例代码,说明当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
其他回答
尽管有各种解释,LEA是一种算术运算:
LEA Rt, [Rs1+a*Rs2+b] => Rt = Rs1 + a*Rs2 + b
只是它的名字对于shift+add操作来说非常愚蠢。其原因已经在最高评级的答案中解释过(即,它是为了直接映射高级内存引用而设计的)。
来自Abrash的“装配禅”:
LEA,唯一执行内存寻址计算但实际上不寻址内存的指令。LEA接受标准内存寻址操作数,但只会将计算出的内存偏移量存储在指定寄存器中,该寄存器可以是任何通用寄存器。这给了我们什么?ADD没有提供的两件事:使用两个或三个操作数执行加法的能力,以及将结果存储在任何寄存器中的能力;而不仅仅是源操作数之一。
执法机关不改变旗帜。
示例
LEA EAX,[EAX+EBX+1234567]计算EAX+EBX+134567(即三个操作数)LEA EAX,[EBX+ECX]计算EBX+ECX,而不使用结果覆盖两者。乘以常数(乘以2、3、5或9),如果你像LEA EAX那样使用,[EBX+N*EBX](N可以是1,2,4,8)。
其他用例在循环中很方便:LEA EAX、[EAX+1]和INC EAX之间的区别在于后者更改EFLAGS,但前者不更改;这保持了CMP状态。
LEA指令可用于避免CPU对有效地址进行耗时的计算。如果地址被重复使用,则将其存储在寄存器中而不是每次使用时计算有效地址更有效。
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
至于英特尔为什么认为这条指令值得包括在内,我并不完全确定,但它的实现成本低是一个重要因素。另一个因素是Intel的汇编器允许相对于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中。哎呀。
这里有一个例子。
// 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指令。
推荐文章
- Trap和中断的区别是什么?
- 为什么程序不是经常用汇编编写的?
- 保护可执行文件不受逆向工程的影响?
- “每个程序员应该知道的关于内存的事情”有多少仍然是有效的?
- 在GDB中显示当前的组装指令
- 为什么引入无用的MOV指令会加速x86_64汇编中的紧循环?
- ARM架构与x86有何不同?
- 为什么GCC在实现整数除法时使用奇数乘法?
- 基指针和堆栈指针到底是什么?他们指的是什么?
- 汇编代码vs机器代码vs目标代码?
- 什么是回跳线?它是如何工作的?
- 提交到App Store的问题:不支持架构x86
- “switch”比“if”快吗?
- 为什么Java在连续整数上的切换与添加的情况下运行得更快?
- 多核汇编语言是什么样子的?