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 xcoorde ycoordsã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 yno EDX. O fator de escala 8 é porque cada um Pointtem 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 LEAentra (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.
movinstruções e deixar os parênteses? MOV EDX, EBX + 8*EAX + 4
MOVcom 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.LEAaceita 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
ADDnã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 LEAnã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 + ECXsem 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, EFLAGSmas o primeiro não; isso preserva o CMPestado.
LEA EAX, [ EAX + EBX + 1234567 ]calcula a soma de EAX, EBXe 1234567(são três operandos). LEA EAX, [ EBX + ECX ]calcula EBX + ECX sem substituir o resultado. A terceira coisa a LEAser 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 ]( Npode 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, EFLAGSmas o primeiro não; isso preserva o CMPestado
LEApodem 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 LEAinstrução é que ela não altera os códigos de condição como CFe ZF, enquanto calcula o endereço por instruções aritméticas como ADDou MULfaz. 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 leanã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).
LEAnas 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" ...
LEAfornece 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]?
shlinstruçã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 mulinstruçõ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 ebxponteiro 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 LEAsobre 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 LEAcomo um, MOVmas na verdade não está desreferenciando a memória. Em outras palavras:
MOV EAX, [ESP+4]
Isso moverá o conteúdo do que ESP+4aponta para EAX.
LEA EAX, [EBX*8]
Isso moverá o endereço efetivo EBX * 8para 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.
LEAfaz.
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 fnordfoi 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, LEAtem 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 LEAe outro endereço de referência de memória), isso significa que a operação aritmética LEAe 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, MemoryAddressque 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, LEApode ser usado para converter esses não PIC em PIC.
leaem 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 addou a submaioria das outras operações aritméticas básicas em quatro ALUs diferentes , mas pode executar apenas leaem uma (complexa lea) ou duas (simples lea). Mais importante ainda, essas duas leaALUs 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 esivalor ecxmultiplicado 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 MOVpor 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.
LEAnã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 LEAapenas 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 LEAsão supérfluos estão seriamente enganadas; os colchetes não são LEAsintaxe, 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, LEAnã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 8dde LEAmudou para 8b.
Obviamente, essa LEAcodificaçã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 LEAexcluir 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
=ddizer 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, movpois 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 = 0xff00000001ou algo assim. Pois leavocê 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 GLOBALVARvez disso. Você pode usar o LEA, mas é um tamanho de código um pouco maior que mov r32, imm32e é 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, 1nã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, 0apenas 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+1em 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 raxtecnicamente "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ê