Consulte também Usando LEA em valores que não são endereços / ponteiros? : LEA é apenas uma instrução shift-and-add. Provavelmente foi adicionado ao 8086 porque o hardware já está lá para decodificar e calcular os modos de endereçamento, não porque é "destinado" apenas para uso com endereços. Lembre-se de que ponteiros são apenas números inteiros na montagem.
Peter Cordes
Respostas:
798
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
Não teria sido mais limpo estender as movinstruções e deixar os parênteses? MOV EDX, EBX + 8*EAX + 4
Natan Yellin
14
@imacake Ao substituir o LEA por um MOV especializado, você mantém a sintaxe limpa: [] colchetes sempre equivalem a desreferenciar um ponteiro em C. Sem colchetes, você sempre lida com o ponteiro.
Natan Yellin
139
Fazer matemática em uma instrução MOV (EBX + 8 * EAX + 4) não é válido. LEA ESI, [EBX + 8 * EAX + 4] é válido porque este é um modo de endereçamento suportado pelo x86. en.wikipedia.org/wiki/X86#Addressing_modes
Erik
29
@JonathanDickinson LEA é como um 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.
Hbbs 28/08/2013
24
Erik, o comentário da turnê não é exato. MOV eax, [ebx + 8 * ecx + 4] é válido. No entanto MOV retorna o conteúdo de thst local de memória enquanto LEA retorna o endereço
Olorin
562
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.
multiplicação por constante (por dois, três, cinco ou nove), se você usá-lo 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, EFLAGSmas o primeiro não; isso preserva o CMPestado.
@AbidRahmanK alguns exemplos: LEA EAX, [ EAX + EBX + 1234567 ]calcula a soma de EAX, EBXe 1234567(são três operandos). LEA EAX, [ EBX + ECX ]calcula EBX + ECXsem 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
FrankH.
@FrankH. Eu ainda não entendo, então carrega um ponteiro em outro lugar?
6
@ ripDaddy69 sim, mais ou menos - se por "carga" você quer dizer "executa a aritmética de cálculo de endereço / ponteiro". Ele não acessa a memória (ou seja, não desrefere o ponteiro como seria chamado em termos de programação C).
FrankH.
2
+1: Isso torna explícito para que tipos de 'truques' 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)
Assad Ebrahim
3
Há uma grande diferença entre o 2 operando LEA, que é rápido e 3 operando, que é lento. O manual Intel Optimization diz que o LEA de caminho rápido é de ciclo único e o LEA de caminho lento leva três ciclos. Além disso, no Skylake, existem duas unidades funcionais de caminho rápido (portas 1 e 5) e há apenas uma unidade funcional de caminho lento (porta 1). A Regra 33 de Codificação de Montagem / Compilador no manual até adverte contra o uso de 3 operandos LEA.
Olsonist
110
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.
Sim, 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? )
Peter Cordes
2
@ PeterCordes: Odeio trazer isso aqui, mas - estou sozinho pensando que essa nova tag [x86-lea] é redundante e desnecessária?
precisa saber é o seguinte
2
@ MichaelPetch: Sim, acho que é muito específico. Parece confundir iniciantes que não entendem a linguagem de máquina e que tudo (incluindo ponteiros) são apenas bits / bytes / números inteiros; portanto, há muitas perguntas sobre isso com um grande número de votos. Mas ter uma etiqueta implica que há espaço para um número ilimitado de perguntas futuras, quando na verdade existem cerca de 2 ou 3 no total que não são apenas duplicatas. (o que é? Como usá-lo para multiplicar números inteiros e como ele é executado internamente no UAG vs. ALUs e com que latência / débito e talvez seja "destinada" propósito?.)
Peter Cordes
@ PeterCordes: eu concordo, e se alguma coisa todas essas postagens sendo editadas forem praticamente uma duplicata de algumas das questões relacionadas à LEA que estão saindo. Em vez de uma tag, todas as duplicatas devem ser identificadas e marcadas como imho.
22818 Michael Petch
1
@EvanCarroll: aguarde a marcação de todas as perguntas da LEA, se você ainda não tiver concluído. Como discutido acima, consideramos o x86-lea muito específico para uma tag e não há muito espaço para futuras perguntas não duplicadas. Eu acho que seria muito trabalho realmente escolher uma "melhor" sessão de perguntas e respostas como alvo de dup para a maioria deles, ou decidir realmente quais os mods a serem mesclados.
Peter Cordes
93
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).
E que a aritmética é realizada pelo hardware de cálculo de endereço.
Ben Voigt
30
@BenVoigt Eu costumava dizer isso, porque eu sou um cara velho :-) Tradicionalmente, as CPUs x86 usavam as unidades de endereçamento para isso, concordou. Mas a "separação" ficou muito embaçada nos dias de hoje. Algumas CPUs não têm mais AGUs dedicadas , outras optaram por não executar 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" ...
FrankH.
2
@FrankH .: CPUs fora de ordem normalmente executam LEA em ALUs, enquanto algumas CPUs em ordem (como Atom) às vezes o executam em AGUs (porque não podem estar ocupadas lidando com um acesso à memória).
27568 Peter Panes
3
Não, o nome não é estúpido. 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.
Kaz
3
FWIW, existem muito poucas (se houver) CPUs x86 atuais que executam a operação na AGU. A maioria ou todos usam apenas uma ALU como qualquer outra operação aritmética.
BeeOnRope
77
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.
+1 para o truque. Mas eu gostaria de fazer uma pergunta (pode ser estúpido), por que não multiplicar diretamente com três assim LEA EAX, [EAX*3]?
Abid Rahman K
13
@Abid Rahman K: Não há instruções como o conjunto de instruções da CPU x86.
GJ.
50
@AbidRahmanK, apesar da sintaxe intel asm parecer uma multiplicação, a instrução lea pode codificar apenas operações de turno. O opcode tem 2 bits para descrever a mudança, portanto, você pode multiplicar apenas por 1,2,4 ou 8. #
970
6
@Koray Tugay: Você pode usar a 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.
GJ.
7
@GJ. embora não exista essa codificação, alguns montadores aceitam isso como um atalho, por exemplo, fasm. Então, por exemplo, lea eax,[eax*3]seria traduzido para equivalente a lea eax,[eax+eax*2].
Ruslan
59
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.
Desculpe todo mundo. O @ big.heart me enganou ao dar uma resposta a isso há três horas, fazendo com que ele aparecesse como "novo" na minha pergunta da Assembléia.
David Hoelzer 6/15
1
Por que a sintaxe usa colchetes quando não faz endereçamento de memória?
golopot
3
@ q4w56 Essa é uma das coisas em que a resposta é: "É assim que você faz". Eu acredito que é uma das razões pelas quais as pessoas têm dificuldade em descobrir o que LEAfaz.
David Hoelzer
2
@ q4w56: é uma instrução shift + add que usa sintaxe de operando de memória e codificação de código de máquina. Em algumas CPUs, pode até usar o hardware AGU, mas esse é um detalhe histórico. O fato ainda relevante é que o hardware do decodificador já existe para decodificar esse tipo de shift + add, e o LEA nos permite usá-lo para aritmética em vez de endereçamento de memória. (Ou para cálculos de endereço, se uma entrada é realmente um ponteiro).
Peter Cordes
20
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.
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.
A parte "LEA ALU" separada é basicamente falsa. As CPUs modernas são executadas 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.
BeeOnRope
O artigo que você vinculou (corretamente) mostra que o LEA está na mesma porta que uma ALU inteira (add / sub / boolean) e a unidade MUL inteira em Haswell. (E ALUs vetoriais, incluindo FP ADD / MUL / FMA). A unidade LEA simples está na porta 5, que também executa ADD / SUB / qualquer que seja, embaralha vector e outras coisas. A única razão pela qual não estou com voto negativo é que você aponta o uso do LEA relativo ao RIP (apenas para x86-64).
Peter Cordes
8
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.
Não necessariamente no x86 moderno. A maioria dos modos de endereçamento tem o mesmo custo, com algumas ressalvas. Portanto, [esi]raramente é mais barato do que dizer [esi + 4200]e raramente é mais barato do que [esi + ecx*8 + 4200].
BeeOnRope 26/06
@BeeOnRope [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).
Kaz
2
@ Kaz - Eu acho que você estava perdendo o meu ponto (ou então eu perdi o ponto do OP). Meu entendimento é que o OP está dizendo que, se você usar algo como [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.
BeeOnRope
Portanto, o motivo pelo qual eu estava comparando [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..
BeeOnRope
1
Talvez você alivie alguma pressão do registro, sim - mas o oposto pode ser o caso: se os registros com os quais você gerou o endereço efetivo estiverem ativos, você precisará de outro registro para salvar o resultado 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
BeeOnRope
7
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:
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.
// 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
Dividir seu asm inline em declarações separadas não é seguro, e suas listas de rejeitos estão incompletas. O bloco basic-asm informa que o compilador não possui clobbers, mas na verdade modifica vários registros. Além disso, você pode =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.
Peter Cordes
Se você não quiser escrever %%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.
Peter Cordes
@PeterCordes Obrigado, Peter, você quer que eu exclua esta resposta ou modifique-a após seus comentários?
Jaehyuk Lee
1
Se você corrigir o asm inline, ele não fará nenhum mal e talvez seja um bom exemplo concreto para iniciantes que não entenderam as outras respostas. Não há necessidade de excluir, e é uma solução fácil, como mostrei no meu último comentário. Eu acho que valeria a pena uma votação se o mau exemplo de inline asm fosse corrigido em um exemplo "bom". (Eu não downvote)
Peter Cordes
1
Onde alguém diz que isso 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.
Peter Cordes
4
LEA: apenas uma instrução "aritmética" ..
MOV transfere dados entre operandos, mas lea está apenas calculando
O LEA obviamente move os dados; tem um operando de destino. O LEA nem sempre calcula; calcula se o endereço efetivo expresso no operando de origem calcula. LEA EAX, GLOBALVAR não calcula; apenas move o endereço de GLOBALVAR para EAX.
Kaz
@Kaz obrigado pelo seu feedback. minha fonte era "LEA (endereço efetivo de carga) é essencialmente uma instrução aritmética - ele não realiza nenhum acesso real à memória, mas é comumente usado para calcular endereços (embora você possa calcular inteiros de uso geral com ele)". formulário Eldad-Eilam livro página 149
o contador
@Kaz: É por isso que o LEA é redundante quando o endereço já é uma constante no tempo do link; use em 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.
Peter Cordes
@ Kaz: pelo mesmo argumento, você poderia dizer que 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).
Peter Cordes
@PeterCordes Meu argumento é que tanto o LEA EAX, o GLOBALVAL quanto o MOV EAX, o GLOBALVAR simplesmente pegam o endereço de um operando imediato. Não há multiplicador de 1 ou deslocamento de 0 sendo aplicado; pode ser assim no nível do hardware, mas não é visto na linguagem assembly ou no conjunto de instruções.
Kaz
1
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 .)
"Esta é uma regra geral na programação de montagem: use as instruções, porém isso agita o seu barco." Eu não entregaria pessoalmente esse conselho, por coisas como call lbllbl: 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ê
The6P4C
@ The6P4C Essa é uma ressalva útil. No entanto, se não houver alternativa para tornar a previsão de ramificação infeliz, é preciso fazer isso. Há outra regra geral na programação de montagem. Pode haver maneiras alternativas de fazer algo e você deve escolher sabiamente as alternativas. Existem centenas de maneiras de inserir o conteúdo do registro BL no registro AL. Se o restante do RAX não precisar ser preservado, o LEA pode ser uma opção. Não afetar os sinalizadores pode ser uma boa idéia em alguns dos milhares de tipos de processadores x86. Groetjes Albert
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:
Agora imagine uma declaração como:
Onde
points[]
é uma matriz dePoint
. Assumindo que a base da matriz já está emEBX
, e variáveli
é emEAX
, excoord
eycoord
são cada 32 bits (por issoycoord
é no deslocamento 4 bytes na estrutura), esta afirmação pode ser compilado para:que vai pousar
y
noEDX
. O fator de escala 8 é porque cada umPoint
tem 8 bytes de tamanho. Agora considere a mesma expressão usada com o operador "address of" &:Nesse caso, você não deseja o valor de
ycoord
, mas o endereço. É aí queLEA
entra (o endereço efetivo da carga). Em vez de aMOV
, o compilador pode gerarque carregará o endereço
ESI
.fonte
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 oMOV
. Na verdade, ele não lê o endereço calculado, apenas calcula.Do "Zen da Assembléia" de Abrash:
E
LEA
não altera as bandeiras.Exemplos
LEA EAX, [ EAX + EBX + 1234567 ]
calculaEAX + EBX + 1234567
(são três operandos)LEA EAX, [ EBX + ECX ]
calculaEBX + 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 ]
eINC EAX
é que o último muda,EFLAGS
mas o primeiro não; isso preserva oCMP
estado.fonte
LEA EAX, [ EAX + EBX + 1234567 ]
calcula a soma deEAX
,EBX
e1234567
(são três operandos).LEA EAX, [ EBX + ECX ]
calculaEBX + ECX
sem substituir o resultado. A terceira coisa aLEA
ser usada (não listada por Frank) é a multiplicação por constante (por duas, três, cinco ou nove), se você usá-la comoLEA EAX, [ EBX + N * EBX ]
(N
pode ser 1,2,4,8). Outro caso de uso é útil em loops: a diferença entreLEA EAX, [ EAX + 1 ]
eINC EAX
é que o último muda,EFLAGS
mas o primeiro não; isso preserva oCMP
estadoLEA
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 comoCF
eZF
, enquanto calcula o endereço por instruções aritméticas comoADD
ouMUL
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.fonte
lea
às vezes é útil para o compilador (ou codificador humano) fazer matemática sem prejudicar um resultado de flag. Maslea
não é mais rápido queadd
. 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:
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).
fonte
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.
fonte
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 usamosmul
instruções mais pretensiosas e lentas.lea eax,[eax*3]
seria traduzido para equivalente alea 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:para mover ainda mais os itens do
ebx
ponteiroeax
(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.fonte
O maior motivo que você usa
LEA
sobre aMOV
é 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 paraEAX
.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 aMOV
é limitado a adicionar / subtrair.fonte
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:
foram implementados quase de forma idêntica internamente. A diferença é uma etapa ignorada. Ambas as instruções funcionam algo como:
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:Se alguém quiser usar algo como stosw para armazenar dados em um endereço relativo à BP, poderá dizer
foi mais conveniente do que:
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
fonte
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 (incluindoLEA
e outro endereço de referência de memória), isso significa que a operação aritméticaLEA
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ênciaMemoryAddress
. É diferente doMOV 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.fonte
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 executaradd
ou asub
maioria das outras operações aritméticas básicas em quatro ALUs diferentes , mas pode executar apenaslea
em uma (complexalea
) ou duas (simpleslea
). Mais importante ainda, essas duaslea
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.
fonte
[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 aoesi
valorecx
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 escreveradd eax, [esi + 4200]; add ebx, [esi + 4200]; add ecx, [esi + 4200]
, você deve preferirlea 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 umlea
, 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. @KazA 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:
move o conteúdo da localização da memória designada para o registro de destino.
Se substituirmos
MOV
porLEA
, 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:Isso é válido; podemos testá-lo no prompt do Linux:
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ãoLEA
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:
É uma codificação muito semelhante, entende? Apenas o
8d
deLEA
mudou para8b
.Obviamente, essa
LEA
codificação é mais longa do que mover um zero imediato paraEAX
: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.fonte
Aqui está um exemplo.
Com -O (otimizar) como opção de compilador, o gcc encontrará a instrução lea para a linha de código indicada.
fonte
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.
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
Resultado de execução
fonte
=d
dizer ao compilador que o resultado está no EDX, salvando ummov
. 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. gostoasm("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á usadomov $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. Poislea
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
fonte
mov eax, offset GLOBALVAR
vez disso. Você pode usar o LEA, mas é um tamanho de código um pouco maior quemov 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. Ourorx 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
e
têm o mesmo efeito no AX, mas não nos sinalizadores de status. (Esta é uma notação de ciasdis .)
fonte
call lbl
lbl: pop rax
tecnicamente "trabalhar" como uma maneira de obter o valorrip
, 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ê