Para mim, parece um MOV descolado. Qual é o seu propósito e quando devo usá-lo?
Para mim, parece um MOV descolado. Qual é o seu propósito e quando devo usá-lo?
Respostas:
Como outros já apontaram, o LEA (carregamento efetivo do endereço) é frequentemente usado como um "truque" para realizar certos cálculos, mas esse não é seu objetivo 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 entradas 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
. Assumindo que a base da matriz já está em EBX
, e variável i
é em EAX
, e xcoord
e ycoord
são cada 32 bits (por isso ycoord
é no deslocamento 4 bytes na estrutura), esta afirmação pode ser compilado para:
MOV EDX, [EBX + 8*EAX + 4] ; right side is "effective address"
que vai pousar y
no EDX
. O fator de escala 8 é porque cada um Point
tem 8 bytes de tamanho. Agora considere a mesma expressão usada com o operador "address of" &:
int *p = &points[i].ycoord;
Nesse caso, você não deseja o valor de ycoord
, mas o endereço. É aí que LEA
entra (o endereço efetivo da carga). Em vez de a MOV
, o compilador pode gerar
LEA ESI, [EBX + 8*EAX + 4]
que carregará o endereço ESI
.
mov
instruções e deixar os parênteses? MOV EDX, EBX + 8*EAX + 4
MOV
com uma fonte indireta, exceto que apenas faz o indireto e não o MOV
. Na verdade, ele não lê o endereço calculado, apenas calcula.
Do "Zen da Assembléia" de Abrash:
LEA
, a única instrução que executa cálculos de endereçamento de memória, mas na verdade não endereça a memória.LEA
aceita um operando de endereçamento de memória padrão, mas não faz nada além de armazenar o deslocamento de memória calculado no registro especificado, que pode ser qualquer registro de uso geral.O que isso nos dá? Duas coisas que
ADD
não fornecem:
- a capacidade de realizar adição com dois ou três operandos, e
- a capacidade de armazenar o resultado em qualquer registro; não apenas um dos operandos de origem.
E 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 substituir o resultado.LEA EAX, [ EBX + N * EBX ]
(N pode ser 1,2,4,8).Outro caso de uso é útil em loops: a diferença entre LEA EAX, [ EAX + 1 ]
e INC EAX
é que o último muda, EFLAGS
mas o primeiro não; isso preserva o CMP
estado.
LEA EAX, [ EAX + EBX + 1234567 ]
calcula a soma de EAX
, EBX
e 1234567
(são três operandos). LEA EAX, [ EBX + ECX ]
calcula EBX + ECX
sem substituir o resultado. A terceira coisa a LEA
ser usada (não listada por Frank) é a multiplicação por constante (por duas, três, cinco ou nove), se você usá-la como LEA EAX, [ EBX + N * EBX ]
( N
pode ser 1,2,4,8). Outro caso de uso é útil em loops: a diferença entre LEA EAX, [ EAX + 1 ]
e INC EAX
é que o último muda, EFLAGS
mas o primeiro não; isso preserva o CMP
estado
LEA
podem ser usados ... (consulte "LEA (endereço efetivo de carga) é frequentemente usado como um" truque "para realizar certos cálculos" na resposta popular de IJ Kennedy acima)
Outra característica importante da LEA
instrução é que ela não altera os códigos de condição como CF
e ZF
, enquanto calcula o endereço por instruções aritméticas como ADD
ou MUL
faz. 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 planejador de hardware.
lea
às vezes é útil para o compilador (ou codificador humano) fazer matemática sem prejudicar um resultado de flag. Mas lea
não é mais rápido que add
. A maioria das instruções x86 grava sinalizadores. As implementações de alto desempenho x86 precisam renomear EFLAGS ou evitar o risco de gravação após gravação para que o código normal seja executado rapidamente; portanto, as instruções que evitam gravações de sinalizadores não são melhores por causa disso. ( Parcial coisas bandeira pode criar problemas, consulte a instrução INC vs ADD 1: Será que isso importa? )
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 o nome é extremamente estúpido para uma operação shift + add. A razão para isso já foi explicada nas respostas com melhor classificação (ou seja, foi projetada para mapear diretamente as referências de memória de alto nível).
LEA
nas AGUs, mas nas ALUs inteiras comuns. É preciso ler as especificações da CPU muito de perto hoje em dia para descobrir "onde as coisas correm" ...
LEA
fornece o endereço que surge de qualquer modo de endereçamento relacionado à memória. Não é uma operação de troca e adição.
Talvez apenas outra coisa sobre a instrução LEA. Você também pode usar o LEA para registros de multiplicação rápida 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 EAX, [EAX*3]
?
shl
instrução shift left left para multiplicar registros por 2,4,8,16 ... é mais rápido e mais curto. Mas, para multiplicar com números diferentes de potência 2, normalmente usamos mul
instruções mais pretensiosas e lentas.
lea eax,[eax*3]
seria traduzido para equivalente a lea eax,[eax+eax*2]
.
lea
é uma abreviação de "carregar endereço efetivo". Carrega o endereço da referência de localização pelo operando de origem no operando de destino. Por exemplo, você pode usá-lo para:
lea ebx, [ebx+eax*8]
para mover ainda mais os itens do ebx
ponteiro eax
(em uma matriz de 64 bits / elemento) com uma única instrução. Basicamente, você se beneficia dos modos complexos de endereçamento suportados pela arquitetura x86 para manipular os ponteiros com eficiência.
O maior motivo que você usa LEA
sobre a MOV
é se você precisa executar aritmética nos registros que está usando para calcular o endereço. Efetivamente, você pode executar o que equivale a aritmética de ponteiro em vários registros em combinação de forma eficaz para "grátis".
O que é realmente confuso é que você normalmente escreve um exemplo LEA
como um, MOV
mas na verdade não está desreferenciando a memória. Em outras palavras:
MOV EAX, [ESP+4]
Isso moverá o conteúdo do que ESP+4
aponta para EAX
.
LEA EAX, [EBX*8]
Isso moverá o endereço efetivo EBX * 8
para o EAX, não o que é encontrado nesse local. Como você pode ver, também é possível multiplicar por fatores de dois (dimensionamento) enquanto a MOV
é limitado a adicionar / subtrair.
LEA
faz.
O 8086 possui uma grande família de instruções que aceitam um operando de registro e um endereço efetivo, realizam alguns cálculos para calcular a parte deslocada desse endereço efetivo e realizam algumas operações envolvendo o registro e a memória referida pelo endereço calculado. Era bastante simples ter uma das instruções dessa família como acima, exceto para pular essa operação de memória real. Isto, as instruções:
mov ax,[bx+si+5]
lea ax,[bx+si+5]
foram implementados quase de forma idêntica internamente. A diferença é uma etapa ignorada. Ambas as instruções funcionam algo 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 à razão pela qual a Intel pensou que essa instrução valia a pena incluir, não tenho muita certeza, mas o fato de ser barato de implementar teria sido um grande fator. Outro fator teria sido o fato de o montador da Intel permitir a definição de símbolos em relação ao registro BP. Se fnord
foi definido como um símbolo relativo à BP (por exemplo, BP + 8), pode-se dizer:
mov ax,fnord ; Equivalent to "mov ax,[BP+8]"
Se alguém quiser usar algo como stosw para armazenar dados em um endereço relativo à BP, poderá 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 (i.e. 8)
rep movs fnord ; Address is ignored EXCEPT to note that it's an SS-relative word ptr
Observe que esquecer o mundo "offset" faria com que o conteúdo da localização [BP + 8], em vez do valor 8, fosse adicionado ao DI. Opa
Como as respostas existentes mencionadas, LEA
tem as vantagens de executar aritmética de endereçamento de memória sem acessar a memória, salvando o resultado aritmético em um registro diferente em vez da forma simples de instrução add. O benefício real de desempenho subjacente é que o processador moderno possui uma unidade e porta LEA ALU separadas para geração eficaz de endereços (incluindo LEA
e outro endereço de referência de memória), isso significa que a operação aritmética LEA
e outra operação aritmética normal na ALU podem ser feitas em paralelo em um testemunho.
Consulte este artigo da arquitetura Haswell para obter mais detalhes sobre a unidade LEA: http://www.realworldtech.com/haswell-cpu/4/
Outro ponto importante que não é mencionado em outras respostas é a LEA REG, [MemoryAddress]
instrução é o PIC (código independente de posição) que codifica o endereço relativo do PC nesta instrução para referência MemoryAddress
. É diferente do MOV REG, MemoryAddress
que codifica o endereço virtual relativo e requer a realocação / aplicação de patches nos sistemas operacionais modernos (como o ASLR é um recurso comum). Portanto, LEA
pode ser usado para converter esses não PIC em PIC.
lea
em uma ou mais das mesmas ALUs que executam outras instruções aritméticas (mas geralmente menos do que outras aritméticas). Por exemplo, a CPU Haswell mencionada pode executar add
ou a sub
maioria das outras operações aritméticas básicas em quatro ALUs diferentes , mas pode executar apenas lea
em uma (complexa lea
) ou duas (simples lea
). Mais importante ainda, essas duas lea
ALUs com capacidade são simplesmente duas das quatro que podem executar outras instruções, portanto, não há benefício de paralelismo conforme reivindicado.
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 eficaz armazená-lo em um registro em vez de calcular o endereço efetivo toda vez que for usado.
[esi]
raramente é mais barato do que dizer [esi + 4200]
e raramente é mais barato do que [esi + ecx*8 + 4200]
.
[esi]
não é mais barato que [esi + ecx*8 + 4200]
. Mas por que se preocupar em comparar? Eles não são equivalentes. Se você deseja que o primeiro designe o mesmo local de memória que o último, você precisa de instruções adicionais: você deve adicionar ao esi
valor ecx
multiplicado por 8. Uh, a multiplicação vai derrubar os sinalizadores da CPU! É necessário adicionar o 4200. Essas instruções adicionais aumentam o tamanho do código (ocupando espaço no cache de instruções, ciclos para buscar).
[esi + 4200]
repetidamente em uma sequência de instruções, é melhor primeiro carregar o endereço efetivo em um registro e usá-lo. Por exemplo, em vez de escrever add eax, [esi + 4200]; add ebx, [esi + 4200]; add ecx, [esi + 4200]
, você deve preferir lea edi, [esi + 4200]; add eax, [edi]; add ebx, [edi]; add ecx, [edi]
, o que raramente é mais rápido. Pelo menos essa é a interpretação clara dessa resposta.
[esi]
e [esi + 4200]
(ou [esi + ecx*8 + 4200]
é que essa é a simplificação que o OP está propondo (como eu a entendo)): que N instruções com o mesmo endereço complexo são transformadas em N instruções com endereçamento simples (um registro), mais um lea
, desde endereçamento complexo é "demorado" na verdade, é mais lento, mesmo em x86 moderna, mas apenas latência-wise que parece improvável que importa para obter instruções consecutivos com o mesmo endereço..
lea
, aumentando a pressão nesse caso. Em geral, o armazenamento de intermediários é uma causa da pressão do registro, não uma solução para isso - mas acho que na maioria das situações é uma lavagem. @Kaz
A instrução LEA (Load Effective Address) é uma maneira de obter o endereço que surge em qualquer um dos modos de endereçamento de memória do processador Intel.
Ou seja, se tivermos dados movidos assim:
MOV EAX, <MEM-OPERAND>
move o conteúdo da localização da memória designada para o registro de destino.
Se substituirmos MOV
por LEA
, o endereço da localização da memória será calculado exatamente da mesma maneira pela <MEM-OPERAND>
expressão de endereçamento. Mas, em vez do conteúdo da localização da memória, obtemos a própria localização no destino.
LEA
não é uma instrução aritmética específica; é uma maneira de interceptar o endereço efetivo resultante de qualquer um dos modos de endereçamento de memória do processador.
Por exemplo, podemos usar LEA
apenas um endereço direto simples. Nenhuma aritmética está envolvida:
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 nem deslocamento. Zero é movido para o EAX. Poderíamos fazer isso usando MOV com um operando imediato também.
Essa é a razão pela qual as pessoas que pensam que os colchetes LEA
são supérfluos estão seriamente enganadas; os colchetes não são LEA
sintaxe, 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 executa até o ponto de calcular o endereço. Em seguida, move esse endereço para o destino em vez de gerar uma referência de memória. (Como o cálculo do endereço de um modo de endereçamento em qualquer outra instrução não afeta os sinalizadores da CPU, LEA
não afeta os sinalizadores da CPU).
Contraste 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, entende? Apenas o 8d
de LEA
mudou para 8b
.
Obviamente, essa LEA
codificação é mais longa do que mover um zero imediato para 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 LEA
excluir essa possibilidade, apenas porque existe uma alternativa mais curta; está apenas combinando de forma ortogonal com os modos de endereçamento disponíveis.
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.
Parece que muitas respostas já estão completas. Gostaria de adicionar mais um código de exemplo para mostrar como as instruções lea e move funcionam de maneira diferente quando elas têm o mesmo formato de expressão.
Para encurtar a história, as instruções lea e as instruções mov podem ser usadas com os parênteses que encerram o operando src das instruções. Quando eles são colocados com o () , a expressão no () é calculada da mesma maneira; no entanto, duas instruções interpretarão o valor calculado no operando src de uma maneira diferente.
Se a expressão é usada com lea ou mov, o valor src é calculado como abaixo.
D (Rb, Ri, S) => (Reg [Rb] + S * Reg [Ri] + D)
No entanto, quando é usada com a instrução mov, ela tenta acessar o valor apontado pelo endereço gerado pela expressão acima e armazená-lo no destino.
Por outro lado, quando a instrução lea é executada com a expressão acima, ela carrega o valor gerado como está no destino.
O código abaixo executa a instrução lea e a instrução mov com o mesmo parâmetro. No entanto, para entender a diferença, adicionei um manipulador de sinal no nível do usuário para detectar a falha de segmentação causada pelo acesso a um endereço errado como resultado da instrução mov.
Código de exemplo
#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
);
}
Resultado de execução
Result of lea instruction: 14
inst at 0x4007b5 tries to access memory at 14, but failed
=d
dizer ao compilador que o resultado está no EDX, salvando um mov
. Você também deixou de fora uma declaração antecipada na saída. Isso demonstra o que você está tentando demonstrar, mas também é um péssimo exemplo enganoso de asm inline que será interrompido se usado em outros contextos. Isso é uma coisa ruim para uma resposta de estouro de pilha.
%%
sobre todos esses nomes de registro no Extended asm, use restrições de entrada. gosto asm("lea 4(%%ebx, %%eax, 8), %%edx" : "=d"(result_of_lea) : "a"(1), "b"(2));
. Permitir que o init do compilador seja registrado significa que você também não precisa declarar clobbers. Você está supercomplicando as coisas com xor-zerando antes que mov-imediato substitua o registro inteiro também.
mov 4(%ebx, %eax, 8), %edx
é inválido? De qualquer forma, sim, mov
pois faria sentido escrever "a"(1ULL)
para informar ao compilador que você tem um valor de 64 bits e, portanto, ele precisa garantir que ele seja estendido para preencher todo o registro. Na prática, ele ainda será usado mov $1, %eax
, porque escrever EAX zero se estende ao RAX, a menos que você tenha uma situação estranha de código circundante em que o compilador sabia que RAX = 0xff00000001
ou algo assim. Pois lea
você ainda está usando o tamanho de operando de 32 bits, para que quaisquer bits altos e dispersos nos registros de entrada não tenham efeito no resultado de 32 bits.
LEA: apenas uma instrução "aritmética" ..
MOV transfere dados entre operandos, mas lea está apenas calculando
mov eax, offset GLOBALVAR
vez disso. Você pode usar o LEA, mas é um tamanho de código um pouco maior que mov r32, imm32
e é executado em menos portas, porque ainda passa pelo processo de cálculo de endereço . lea reg, symbol
é útil apenas em 64 bits para um LEA relativo ao RIP, quando você precisa de PIC e / ou endereços fora dos 32 bits baixos. No código de 32 ou 16 bits, não há vantagem nenhuma. LEA é uma instrução aritmética que expõe a capacidade da CPU de decodificar / calcular os modos de endereçamento.
imul eax, edx, 1
não calcula: apenas copia edx para eax. Mas, na verdade, ele executa seus dados através do multiplicador com latência de 3 ciclos. Ou rorx eax, edx, 0
apenas copia (gire em zero).
Todas as instruções normais de "cálculo", como adição de multiplicação, exclusividade ou definição dos sinalizadores de status como zero, sinal. Se você usar um endereço complicado, AX xor:= mem[0x333 +BX + 8*CX]
os sinalizadores serão definidos de acordo com a operação xor.
Agora você pode querer usar o endereço várias vezes. O carregamento desses endereços em um registro nunca se destina a definir sinalizadores de status e, felizmente, não. A frase "carregar endereço efetivo" informa o programador sobre isso. É daí que vem a expressão estranha.
É claro que, uma vez que o processador é capaz de usar o endereço complicado para processar seu conteúdo, ele é capaz de calculá-lo para outros fins. De fato, pode ser usado para realizar uma transformação x <- 3*x+1
em uma instrução. Esta é uma regra geral na programação de montagem: use as instruções, porém isso agita o seu barco.
A única coisa que conta é se a transformação específica incorporada pela instrução é útil para você.
Bottom line
MOV, X| T| AX'| R| BX|
e
LEA, AX'| [BX]
têm o mesmo efeito no AX, mas não nos sinalizadores de status. (Esta é uma notação de ciasdis .)
call lbl
lbl: pop rax
tecnicamente "trabalhar" como uma maneira de obter o valor rip
, mas você tornará a previsão do ramo muito infeliz. Use as instruções que quiser, mas não se surpreenda se você fizer algo complicado e tem consequências que você fez não prevê