Qual é o propósito da instrução LEA?

Para mim, parece apenas um MOV funky. Qual é o seu propósito e quando devo usá-lo?

Como outros apontaram, o LEA (endereço de carregamento efetivo) é frequentemente usado como um “truque” para fazer certos cálculos, mas esse não é seu objective principal. O conjunto de instruções x86 foi projetado para suportar linguagens de alto nível como Pascal e C, onde matrizes – especialmente matrizes de ints ou pequenas estruturas – são comuns. Considere, por exemplo, uma estrutura representando (x, y) coordenadas:

struct Point { int xcoord; int ycoord; }; 

Agora imagine uma declaração como:

 int y = points[i].ycoord; 

onde points[] é uma matriz de Point . Supondo que a base da matriz já esteja em EBX , e a variável i esteja em EAX e xcoord e ycoord tenham 32 bits cada (portanto, ycoord está em offset de 4 bytes na estrutura), essa instrução pode ser compilada para:

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

que vai pousar y em EDX . O fator de escala de 8 é porque cada Point tem 8 bytes de tamanho. Agora, considere a mesma expressão usada com o operador “endereço de” e:

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

Nesse caso, você não quer o valor de ycoord , mas seu endereço. É aí que entra o LEA (load effective address). Em vez de um MOV , o compilador pode gerar

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

que carregará o endereço no ESI .

Do “Zen of Assembly” de Abrash:

LEA , a única instrução que realiza cálculos de endereçamento de memory, mas que na verdade não endereça memory. LEA aceita um operando de endereçamento de memory padrão, mas nada mais faz do que armazenar o deslocamento de memory calculado no registro especificado, que pode ser qualquer registrador de propósito geral.

O que isso nos dá? Duas coisas que o ADD não oferece:

  1. a capacidade de realizar a adição com dois ou três operandos e
  2. a capacidade de armazenar o resultado em qualquer registro; não apenas um dos operandos de origem.

E o LEA não altera as bandeiras.

Exemplos

  • LEA EAX, [ EAX + EBX + 1234567 ] calcula EAX + EBX + 1234567 (são três operandos)
  • LEA EAX, [ EBX + ECX ] calcula EBX + ECX sem replace o resultado.
  • multiplicação por constante (por dois, três, cinco ou nove), se você usar como LEA EAX, [ EBX + N * EBX ] (N pode ser 1,2,4,8).

Outro uso é útil em loops: a diferença entre LEA EAX, [ EAX + 1 ] e INC EAX é que o último altera o EFLAGS mas o primeiro não; isso preserva o estado CMP .

Outra característica importante da instrução LEA é que ela não altera os códigos de condição, como CF e ZF , enquanto calcula o endereço por meio de instruções aritméticas, como ADD ou MUL . Esse recurso diminui o nível de dependência entre as instruções e, portanto, abre espaço para otimização adicional pelo compilador ou pelo agendador de hardware.

Apesar de todas as explicações, o LEA é uma operação aritmética:

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

É só que seu nome é extremamente estúpido para uma operação shift + add. A razão para isso já foi explicada nas respostas mais bem avaliadas (ou seja, foi projetada para mapear diretamente referências de memory de alto nível).

Talvez apenas mais uma coisa sobre a instrução LEA. Você também pode usar o LEA para multiplicar rapidamente os registros por 3, 5 ou 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 é uma abreviação de “load effective address”. Carrega o endereço da referência de localização pelo operando de origem para o operando de destino. Por exemplo, você poderia usá-lo para:

 lea ebx, [ebx+eax*8] 

para mover os itens eax ponteiro de ebx (em um array de 64 bits / elemento) com uma única instrução. Basicamente, você se beneficia de modos de endereçamento complexos suportados pela arquitetura x86 para manipular pointers de maneira eficiente.

O maior motivo pelo qual você usa o LEA em um MOV é se precisar executar a aritmética nos registros que você está usando para calcular o endereço. Efetivamente, você pode executar o que equivale a aritmética de ponteiro em vários dos registradores em combinação de forma eficaz para “livre”.

O que é realmente confuso sobre isso é que você normalmente escreve um LEA como um MOV mas na verdade não está desreferenciando a memory. Em outras palavras:

MOV EAX, [ESP+4]

Isso moverá o conteúdo do que o ESP+4 aponta para o EAX .

LEA EAX, [EBX*8]

Isso moverá o endereço efetivo EBX * 8 para o EAX, não o que for encontrado nesse local. Como você pode ver, também é possível multiplicar por fatores de dois (escala) enquanto um MOV é limitado a adição / subtração.

O 8086 possui uma grande família de instruções que aceitam um operando registrador e um endereço efetivo, executam alguns cálculos para calcular a parte do deslocamento desse endereço efetivo, e executam alguma operação envolvendo o registro e a memory referida pelo endereço computado. Era bastante simples ter uma das instruções dessa família se comportando como acima, exceto por ignorar a operação real da memory. Isto, as instruções:

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

foram implementados quase identicamente internamente. A diferença é um passo ignorado. Ambas as instruções funcionam como:

 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 

Quanto ao motivo pelo qual a Intel achava que essa instrução valeu a pena ser incluída, não tenho certeza, mas o fato de ser barata de implementar teria sido um grande fator. Outro fator seria o fato de que o montador da Intel permitia que símbolos fossem definidos em relação ao registrador BP. Se fnord foi definido como um símbolo relativo a BP (por exemplo, BP + 8), pode-se dizer:

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

Se alguém quisesse usar algo como stosw para armazenar dados em um endereço relativo a BP, ser capaz de dizer

 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 

foi mais conveniente do que:

 mov ax,0 ; Data to store mov cx,16 ; Number of words mov di,bp add di,offset fnord (ie 8) rep movs fnord ; Address is ignored EXCEPT to note that it's an SS-relative word ptr 

Note que esquecer o “offset” mundial faria com que o conteúdo da localização [BP + 8], ao invés do valor 8, fosse adicionado ao ID. Oops

Como as respostas existentes mencionadas, o LEA tem as vantagens de realizar a aritmética de endereçamento de memory sem acessar a memory, salvando o resultado aritmético em um registro diferente, em vez da forma simples de adicionar instrução. O benefício de desempenho subjacente real é que o processador moderno tem uma unidade e porta LEA ALU separada para geração efetiva de endereço (incluindo LEA e outro endereço de referência de memory), isso significa que a operação aritmética em LEA e outras operações aritméticas normais em ALU podem ser feitas em paralelo em um núcleo.

Confira este artigo da arquitetura Haswell para alguns detalhes sobre a unidade LEA: http://www.realworldtech.com/haswell-cpu/4/

Outro ponto importante que não é mencionado em outras respostas é o LEA REG, [MemoryAddress] instrução LEA REG, [MemoryAddress] é PIC (código independente de posição) que codifica o endereço relativo do PC nesta instrução para fazer referência ao MemoryAddress . Isso é diferente do MOV REG, MemoryAddress que codifica o endereço virtual relativo e requer realocação / correção em sistemas operacionais modernos (como o ASLR é um recurso comum). Assim, o LEA pode ser usado para converter esses não PIC em PIC.

A instrução LEA pode ser usada para evitar cálculos demorados de endereços efetivos pela CPU. Se um endereço for usado repetidamente, é mais eficiente armazená-lo em um registro, em vez de calcular o endereço efetivo sempre que for usado.

Aqui está um exemplo.

 // 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; } 

Com -O (otimizar) como opção de compilador, o gcc encontrará a instrução lea para a linha de código indicada.

A instrução LEA (Load Effective Address) é uma maneira de obter o endereço que surge de qualquer um dos modos de endereçamento de memory do processador Intel.

Ou seja, se temos um dado movido assim:

 MOV EAX,  

ele move o conteúdo do local de memory designado para o registrador de destino.

Se replacemos o MOV por LEA , o endereço do local da memory será calculado exatamente da mesma maneira pela expressão de endereçamento . Mas, em vez do conteúdo da localização da memory, obtemos a localização em si no destino.

LEA não é uma instrução aritmética específica; é uma maneira de interceptar o endereço efetivo que surge de qualquer um dos modos de endereçamento de memory do processador.

Por exemplo, podemos usar o LEA em apenas um endereço direto simples. Nenhuma aritmética está envolvida de todo:

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

Isso é válido; podemos testá-lo no prompt do 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 

Aqui, não há adição de um valor escalado e nenhum deslocamento. Zero é movido para o EAX. Poderíamos fazer isso usando MOV com um operando imediato também.

Esta é a razão pela qual as pessoas que pensam que os colchetes no LEA são supérfluos estão gravemente enganados; os colchetes não são syntax LEA mas fazem parte do modo de endereçamento.

O LEA é real no nível do hardware. A instrução gerada codifica o modo de endereçamento real e o processador o realiza até o ponto de calcular o endereço. Em seguida, ele move esse endereço para o destino em vez de gerar uma referência de memory. (Como o cálculo do endereço de um modo de endereçamento em qualquer outra instrução não tem efeito nos sinalizadores da CPU, o LEA não tem efeito nos sinalizadores da CPU.)

Compare com o carregamento do valor do endereço zero:

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

É uma codificação muito semelhante, veja? Apenas o 8d do LEA mudou para 8b .

Claro, essa codificação LEA é mais longa do que mover um zero imediato para o EAX :

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

Não há razão para a LEA excluir essa possibilidade, embora apenas porque há uma alternativa mais curta; está apenas combinando de maneira ortogonal com os modos de endereçamento disponíveis.

LEA: apenas uma instrução “aritmética”

MOV transfere dados entre operandos, mas lea está apenas calculando

porque em vez disso você escreve o código

 mov dx,offset something 

você pode simplesmente escrever

 lea dx,something