MOV和LEA有何区别?

184

我想知道这些指令之间的区别:

MOV AX, [TABLE-ADDR]
并且
LEA AX, [TABLE-ADDR]

5
LEA指令(load effective address)在x86汇编中用于计算内存地址,但它并不会真正地加载任何数据到寄存器。相反,它将某个内存地址的有效地址(即偏移量)加上一个可选的常量或变量,并将结果保存在目标寄存器中。这使LEA成为一种非常有用的指令,可以用于执行各种数学和逻辑运算,而无需使用专门的算术或逻辑操作码。 - Nick Dandoulakis
14
首先,通过查看那个链接,我没有找到这个问题的答案。我在寻找特定信息,而你提供的链接中的讨论比较普遍。 - naveen
3
我很久之前点了@Nick的重复问题的赞同,但刚刚投了反对票。仔细想了想,我太匆忙了,现在我同意naveen的看法,即a)另一个问题没有回答“有什么区别”,b)这是一个有用的问题。对于我的错误,向naveen道歉 - 如果我能撤销我的反对票就好了... - Ruben Bartelink
1
LEA与ADD:https://dev59.com/questions/YG015IYBdhLWcg3w6QA0 - Ciro Santilli OurBigBook.com
相关:在不是地址/指针的值上使用LEA?讨论了LEA的其他用途,用于任意数学计算。 - Peter Cordes
这个回答解决了你的问题吗?LEA指令的目的是什么? - Cole Tobin
12个回答

226
  • LEA 的意思是“加载有效地址”
  • MOV 的意思是“加载值”

简单来说,LEA 加载的是指向所寻址项的指针,而 MOV 则是加载该地址处的实际值。

LEA 的作用是允许进行非平凡的地址计算并存储结果[以备后续使用]。

LEA ax, [BP+SI+5] ; Compute address of value

MOV ax, [BP+SI+5] ; Load value at that address

当只涉及常量时,MOV(通过汇编器的常量计算)有时会与LEA的最简单用法重叠。如果你有多个基地址的复杂计算,那么它是很有用的。


让我感到困惑的是,lea在名称中有“load”,人们说它会将计算出的地址“load”到寄存器中,因为计算内存位置的所有输入都是立即值或寄存器。据我所知,lea只执行计算,它不会加载任何东西,其中加载意味着触及内存? - Joseph Garvin
3
如果我没记错的话,Fetch这个术语可以应用于那个方面;Load只是将寄存器中的值替换为从头开始的某些内容。例如,LAHF是:将FLAGS加载到AH寄存器中。在CLR的CIL中(它是一种更高级别的基于堆栈的抽象机器),术语“load”指将一个值放入概念堆栈中,并且通常是l...,而s...等效操作则执行相反的操作)。 这些笔记( https://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Mips/load.html)表明确实存在你所说的区别适用的架构。 - Ruben Bartelink
1
这让我想起了 https://www.slideshare.net/pirhilton/how-to-name-things-the-hardest-problem-in-programming ;) - Ruben Bartelink
2
@JosephGarvin:在计算机体系结构术语中,如果你讨论它的内部工作原理,“load”这个词可以用于任何写入寄存器的操作。值得注意的是,在RISC ISA中,load-immediate(仅将机器代码中的常量放入寄存器)如MIPS lui $t0, 1(Load Upper Immediate),它将$a0设置为1<<16,尽管在这种情况下,该值作为机器代码的一部分存储在内存中。(或者在像AArch64这样的现代ISA中,以某种方式编码,而不是字面上存在)。是的,当代码有“将1存储在EDI中”的注释时,我感到很烦。 - Peter Cordes
1
正如Ruben所指出的那样,x86也使用LAHF助记符,它会写入一个GP寄存器。(但我总是不得不查一下它是将AH加载到标志中还是从FLAGS中加载,因为FLAGS也是一个寄存器。还有一个SAHF,它可以读取通用寄存器AH。) - Peter Cordes

59
在NASM语法中:
mov eax, var       == lea eax, [var]   ; i.e. mov r32, imm32
lea eax, [var+16]  == mov eax, var+16
lea eax, [eax*4]   == shl eax, 2        ; but without setting flags
在MASM语法中,使用OFFSET var来获取mov-immediate而不是load。

4
只使用NASM语法。在MASM语法中,“mov eax,var”是一个装载操作,与“mov eax,[var]”相同,您必须使用“mov eax,OFFSET var”才能将标签用作立即常量。 - Peter Cordes
1
清晰简洁,展示了我想确认的内容。谢谢。 - jamesblacklock
2
请注意,在所有这些示例中,除了在64位模式下进行RIP相对寻址之外,lea都是更糟糕的选择。 mov r32,imm32可以在更多端口上运行。 lea eax,[edx * 4]是一种复制和移位操作,否则无法在一条指令中完成,但在同一个寄存器中,LEA编码需要更多的字节,因为[eax * 4]需要一个disp32 = 0。(尽管它运行在不同的端口上)。请参阅http://agner.org/optimize/和https://stackoverflow.com/tags/x86/info。 - Peter Cordes

36

指令MOV reg, addr表示将存储在地址addr中的变量读入寄存器reg。指令LEA reg, addr表示将地址(而不是存储在地址中的变量)读入寄存器reg。

MOV指令的另一种形式是MOV reg,immdata,它表示将立即数据(即常量)immdata读入寄存器reg。请注意,如果LEA reg,addr中的addr只是一个常量(即固定偏移量),那么该LEA指令本质上就相当于等效的MOV reg,immdata指令,其将相同的常量作为立即数据进行加载。


34

之前的回答并没有完全解决我的困惑,因此我想加上我的看法。

我错过的是 lea 操作对括号的使用与 mov 不同。

想象一下 C 语言。假设我有一个称为 arraylong 数组。现在表达式 array[i] 执行了一次解引用,从地址 array + i * sizeof(long) [1] 处加载值。

另一方面,考虑表达式 &array[i]。这仍然包含子表达式 array[i],但不会执行解引用!array[i] 的含义已经改变。它不再表示执行解引用,而是充当一种 规范,告诉 & 我们要查找哪个内存地址。如果你愿意,你可以把 & 看作是 "取消" 解引用的操作。

由于这两种用例在许多方面都很相似,它们共享语法 array[i],但是 & 的存在或缺失会改变该语法的解释方式。没有 &,它是一次解引用并从数组中读取。有了 &,它不是。值 array + i * sizeof(long) 仍然会被计算,但不会解引用。

movlea 的情况非常相似。使用 mov 时,会发生一次解引用,而使用 lea 则不会。这尽管两者都使用了括号。例如:movq (%r8), %r9leaq (%r8), %r9。对于 mov,这些括号表示 "解引用";对于 lea,则不是。这类似于只有在没有 & 时,array[i] 才表示 "解引用"。

需要举个例子。

考虑以下代码:

movq (%rdi, %rsi, 8), %rbp

这段代码将内存地址%rdi + %rsi * 8处的值加载到寄存器%rbp中。也就是说,获取寄存器%rdi和寄存器%rsi中的值。将后者乘以8,然后加上前者。找到此位置的值并将其放入寄存器%rbp中。
这段代码对应于C语言中的x = array[i];,其中array变成了%rdii变成了%rsix变成了%rbp8是数组中包含的数据类型的长度。
现在考虑使用lea的类似代码:
leaq (%rdi, %rsi, 8), %rbp

正如使用 movq 对应解引用一样,这里使用的 leaq 对应于不解引用。这行汇编对应于 C 代码 x = &array[i];。请记住,& 改变了 array[i] 的含义,从解引用变为仅指定位置。同样,使用 leaq(%rdi, %rsi, 8) 的含义从解引用更改为指定位置。

此代码行的语义如下:获取寄存器 %rdi 中的值和寄存器 %rsi 中的值。将后者乘以 8,然后加到前者上。将此值放入寄存器 %rbp 中。没有涉及内存加载,只有算术运算 [2]。

请注意,我对 leaqmovq 的描述之间唯一的区别在于 movq 进行了解引用,而 leaq 没有。实际上,为了编写 leaq 描述,我基本上是复制 + 粘贴了 movq 的描述,然后删除了“找到此位置的值”。

总结一下: movqleaq 的区别很棘手,因为它们不同地处理括号的使用,例如 (%rsi)(%rdi, %rsi, 8)。在 movq(和除 lea 之外的所有指令)中,这些括号表示真正的解引用,而在 leaq 中则不是,它们只是方便的语法。


[1] 我已经说过,当 array 是一个 long 数组时,表达式 array[i] 会加载地址 array + i * sizeof(long) 上的值。这是正确的,但应该注意一个细节。如果我写下 C 代码

long x = array[5];

这与打字不相同

long x = *(array + 5 * sizeof(long));

看起来应该基于我的先前陈述,但实际上并不是这样。

问题在于C指针加法有一个技巧。假设我有一个指向类型为T的值的指针p。表达式p + i并不意味着“p位置加上i字节”。相反,表达式p + i实际上意味着“p位置加上i * sizeof(T)字节”。

这种便利之处在于,要获取“下一个值”,我们只需要编写p + 1而不是p + 1 * sizeof(T)

这意味着C代码long x = array[5];实际上等同于

long x = *(array + 5)

因为C语言会自动将5乘以sizeof(long)

所以在这个StackOverflow问题的上下文中,这一切是如何相关的呢?这意味着当我说“地址array + i * sizeof(long)”时,我并不是指"array + i * sizeof(long)"被解释为C表达式。我自己通过sizeof(long)进行乘法运算,以使我的回答更加明确,但要理解由于这一点,此表达式不应被读作C语言。就像使用C语法的常规数学一样。

[2] 顺便说一句:因为所有lea操作只是算术运算,它的参数实际上不必引用有效地址。因此,它经常用于对可能不打算被取消引用的值执行纯算术运算。例如,带有-O2优化的cc编译器。

long f(long x) {
  return x * 5;
}

将以下无关的行删除后,翻译成如下内容:

f:
  leaq (%rdi, %rdi, 4), %rax  # set %rax to %rdi + %rdi * 4
  ret

5
不错的解释,比其他答案更详细,而且是的C的&运算符是一个好的类比。也许值得指出的是LEA是特殊情况,而MOV就像可以使用存储器或寄存器操作数的其他指令一样。例如,add (%rdi),%eax只是使用寻址模式来寻址内存,与MOV相同。另外相关的在不是地址/指针的值上使用LEA?进一步解释:LEA是如何利用CPU对地址计算的硬件支持来进行任意计算的。 - Peter Cordes
@PeterCordes 谢谢!我已经在答案中加入了这是一个特殊情况的观点。 - Quelklef
1
@ecm 很好的观点;我没有注意到。现在我已经改了,谢谢! :) - Quelklef
@PeterCordes 我在考虑是否应该写 array + i 还是 array + i * sizeof(long)。我决定采用后者,因为我们处于汇编的上下文中,并且我从未在 C 语句中使用过表达式 array + i。但是,正如你所指出的那样,它仍然是有效的 C 语法。我会添加一个脚注。 - Quelklef
1
最后一个技巧真的很棒。编译器确实在使exe高效方面做得非常出色。 - Sourav Kannantha B
显示剩余5条评论

10

我猜,除了在GNU汇编器中关于.bss段的标签外,这种情况并不成立?如果你有像.bss .lcomm LabelFromBssSegment, 4这样的东西时,你就不能真正地使用leal TextLabel, LabelFromBssSegment,是吗?你必须要使用movl $TextLabel, LabelFromBssSegment,对吧? - JSmyth
@JSmyth:这仅因为lea需要一个寄存器目标,但mov可以有一个imm32源和一个内存目标。这个限制当然不是特定于GNU汇编器的。 - Peter Cordes
2
此外,这个答案基本上是错误的,因为问题是关于 MOV AX, [TABLE-ADDR] 的,它是一种加载操作。因此有一个重大的区别。相应的指令是 mov ax, OFFSET table_addr - Peter Cordes
1
链接已失效。 - Ryan1729

9

如其他答案所述:

  • MOV将获取方括号内地址处的数据,并将该数据放入目标操作数中。
  • LEA将执行方括号内地址的计算,并将计算出的地址放入目标操作数中。这发生在实际上不出现在内存中并获取数据的情况下。 LEA完成的工作是“有效地址”的计算。

由于内存可以用几种不同的方式寻址(见下面的示例),因此有时使用LEA来添加或相乘寄存器,而无需使用显式的ADDMUL指令(或等效指令)。

由于每个人都在显示Intel语法的示例,因此这里提供一些AT&T语法的示例:

MOVL 16(%ebp), %eax       /* put long  at  ebp+16  into eax */
LEAL 16(%ebp), %eax       /* add 16 to ebp and store in eax */

MOVQ (%rdx,%rcx,8), %rax  /* put qword at  rcx*8 + rdx  into rax */
LEAQ (%rdx,%rcx,8), %rax  /* put value of "rcx*8 + rdx" into rax */

MOVW 5(%bp,%si), %ax      /* put word  at  si + bp + 5  into ax */
LEAW 5(%bp,%si), %ax      /* put value of "si + bp + 5" into ax */

MOVQ 16(%rip), %rax       /* put qword at rip + 16 into rax                 */
LEAQ 16(%rip), %rax       /* add 16 to instruction pointer and store in rax */

MOVL label(,1), %eax      /* put long at label into eax            */
LEAL label(,1), %eax      /* put the address of the label into eax */

永远不要使用lea label, %eax来进行绝对[disp32]寻址模式。请改用mov $label, %eax。虽然它可以工作,但效率较低(机器码更大,运行在更少的执行单元上)。由于您提到了AT&T,因此在非地址/指针值上使用LEA?使用了AT&T,并且我的答案中有一些其他AT&T示例。 - Peter Cordes
那么 leaq (%rax,%rax), %rdx 的意思是 rdx = rax + 1 * rax?我的clang反汇编器应该这样写,而不是这样写:leaq (%rax,%rax,), %rdx。请注意多余的逗号。因为这个括号部分是一个三元表达式,而不是二元表达式。 - BitTickler

7

这取决于所使用的汇编器,因为

mov ax,table_addr

在 MASM 中的作用是

mov ax,word ptr[table_addr]

因此,它加载的是来自table_addr的第一个字节,而不是table_addr的偏移量。您应该使用以下内容:

mov ax,offset table_addr

或者

lea ax,table_addr

它们的功能是相同的。

lea 版本也可以正常工作,如果 table_addr 是一个局部变量,例如:

some_procedure proc

local table_addr[64]:word

lea ax,table_addr

非常感谢,只是我不能标记多个答案 :( - naveen
7
x86指令MOV和LEA的区别绝对不取决于汇编器。 - I. J. Kennedy
我在汇编语言编程方面的鼎盛时期是在1984年使用6502处理器时期。人们会认为,在这40年中,对于更复杂的架构(如x86_64),汇编语法应该得到改进,变得不那么晦涩难懂和容易出错... - BitTickler
@BitTickler:有比MASM更好的x86汇编器,例如NASM,其中事情尽可能简单明了。例如,[]始终表示内存操作数,缺少[]则绝对不是内存操作数。(当然,LEA指令需要其源操作数为内存操作数,但需要取地址。这只是ISA的一部分,除非您想发明全新的LEA语法,尽管它使用正常的寻址模式编码。像lea rax,[rdi + rsi * 4]这样的东西很有用,或者在64位模式下进行RIP相对LEA。) - Peter Cordes
2
@Bartosz Wójcik - 你为什么撤销了编辑,使你的回答的格式/风格变得更糟?将“coz”(“because”的俚语拼写)单独放在一行上只会减慢和分散读者的注意力。 - Peter Cordes
2
@BartoszWójcik 这些编辑很好。请不要再进行回滚操作。请参阅https://stackoverflow.com/help/editing - Zoe stands with Ukraine

4

基本上...在计算之后“移动到REG …” 看起来对其他用途也很好 :)

如果你忘记了该值是指针 你可以将其用于代码优化/最小化...不管怎样...

MOV EBX , 1
MOV ECX , 2

;//with 1 instruction you got result of 2 registers in 3rd one ...
LEA EAX , [EBX+ECX+5]

EAX = 8

原本的意思是:

MOV EAX, EBX
ADD EAX, ECX
ADD EAX, 5

是的,lea 是一种移位加法指令,它使用内存操作数机器编码和语法,因为硬件已经知道如何解码 ModR/M + SIB + disp0/8/32。 - Peter Cordes

2
让我们通过一个例子来理解这个问题。
mov eax,[ebx] and lea eax,[ebx]
假设ebx中的值为0x400000。然后,mov指令将转到地址0x400000,并将那里存在的4个字节数据复制到eax寄存器中。而lea指令将把地址0x400000复制到eax中。因此,在每种情况下执行完指令后,eax的值将是(假设在内存0x400000处包含30)。
eax = 30(在mov的情况下) eax = 0x400000(在lea的情况下)
对于定义,mov将数据从rm32复制到目的地(mov dest rm32),而lea(加载有效地址)将地址复制到目标(mov dest rm32)。

1

MOV指令可以像LEA [label]一样完成相同的操作,但MOV指令在指令本身中包含有效地址作为立即数常量(由汇编程序提前计算)。LEA使用PC相对地址在执行指令期间计算有效地址。


1
这仅适用于64位模式(其中PC相对寻址是新的); 在其他模式中,与更紧凑的“mov”相比,“lea [label]”是浪费字节的无意义行为,因此您应指定您所说的条件。此外,对于某些汇编程序,[label]不是RIP相对寻址模式的正确语法。但是,是的,这是准确的。如何在GNU Assembler中将函数或标签的地址加载到寄存器中更详细地解释了这一点。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接