Qual è lo scopo dell'istruzione LEA?


676

Per me, sembra solo un MOV funky. Qual è il suo scopo e quando dovrei usarlo?


2
Vedi anche Usare LEA su valori che non sono indirizzi / puntatori? : LEA è solo un'istruzione shift-and-add. Probabilmente è stato aggiunto a 8086 perché l'hardware è già lì per decodificare e calcolare le modalità di indirizzamento, non perché è "progettato" solo per l'uso con gli indirizzi. Ricorda che i puntatori sono solo numeri interi nell'assembly.
Peter Cordes,

Risposte:


798

Come altri hanno sottolineato, il LEA (indirizzo efficace per il caricamento) viene spesso utilizzato come "trucco" per eseguire determinati calcoli, ma questo non è il suo scopo principale. Il set di istruzioni x86 è stato progettato per supportare linguaggi di alto livello come Pascal e C, dove le matrici - in particolare matrici di ints o piccole strutture - sono comuni. Considera, ad esempio, una struttura che rappresenta le coordinate (x, y):

struct Point
{
     int xcoord;
     int ycoord;
};

Ora immagina un'affermazione come:

int y = points[i].ycoord;

dove points[]è un array di Point. Supponendo che la base dell'array è già EBX, e variabile iè in EAX, e xcoorde ycoordsono ogni 32 bit (così ycoordviene a compensare 4 byte nel struct), questa affermazione può essere compilato a:

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

, che si terra yin EDX. Il fattore di scala di 8 è perché ognuno ha Pointuna dimensione di 8 byte. Consideriamo ora la stessa espressione utilizzata con l'operatore "address of" &:

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

In questo caso, non vuoi il valore di ycoord, ma il suo indirizzo. È qui che LEAentra in gioco (indirizzo effettivo di caricamento). Invece di a MOV, il compilatore può generare

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

che caricherà l'indirizzo ESI.


112
Non sarebbe stato più pulito estendere le movistruzioni e lasciare le parentesi? MOV EDX, EBX + 8*EAX + 4
Natan Yellin,

14
@imacake Sostituendo LEA con un MOV specializzato, mantieni pulita la sintassi: [] le parentesi sono sempre equivalenti a dereferenziare un puntatore in C. Senza parentesi, gestisci sempre il puntatore stesso.
Natan Yellin,

139
Fare matematica in un'istruzione MOV (EBX + 8 * EAX + 4) non è valido. LEA ESI, [EBX + 8 * EAX + 4] è valido perché si tratta di una modalità di indirizzamento supportata da x86. en.wikipedia.org/wiki/X86#Addressing_modes
Erik

30
@JonathanDickinson LEA è come un MOVcon una fonte indiretta, tranne che fa solo il riferimento indiretto e non il MOV. In realtà non legge dall'indirizzo calcolato, ma solo lo calcola.
Hobbs,

24
Erik, il commento del tour non è preciso. MOV eax, [ebx + 8 * ecx + 4] è valido. Tuttavia MOV restituisce il contenuto della prima posizione di memoria mentre LEA restituisce l'indirizzo
Olorin,

562

Dallo "Zen of Assembly" di Abrash:

LEA, l'unica istruzione che esegue calcoli di indirizzamento della memoria ma in realtà non si occupa della memoria. LEAaccetta un operando di indirizzamento di memoria standard, ma non fa altro che memorizzare l'offset di memoria calcolato nel registro specificato, che può essere qualsiasi registro per scopi generici.

Cosa ci dà questo? Due cose che ADDnon forniscono:

  1. la capacità di eseguire addizioni con due o tre operandi e
  2. la capacità di memorizzare il risultato in qualsiasi registro; non solo uno degli operandi di origine.

E LEAnon altera le bandiere.

Esempi

  • LEA EAX, [ EAX + EBX + 1234567 ]calcola EAX + EBX + 1234567(sono tre operandi)
  • LEA EAX, [ EBX + ECX ]calcola EBX + ECXsenza ignorare neanche con il risultato.
  • moltiplicazione per costante (per due, tre, cinque o nove), se la usi come LEA EAX, [ EBX + N * EBX ](N può essere 1,2,4,8).

Un altro caso d'uso è utile nei loop: la differenza tra LEA EAX, [ EAX + 1 ]ed INC EAXè che quest'ultimo cambia EFLAGSma il primo no; questo preserva lo CMPstato.


42
@AbidRahmanK alcuni esempi: LEA EAX, [ EAX + EBX + 1234567 ]calcola la somma di EAX, EBXe 1234567(cioè tre operandi). LEA EAX, [ EBX + ECX ]calcola EBX + ECX senza ignorare neanche con il risultato. La terza cosa LEAusata (non elencata da Frank) è la moltiplicazione per costante (per due, tre, cinque o nove), se la usi come LEA EAX, [ EBX + N * EBX ]( Npuò essere 1,2,4,8). Un altro caso d'uso è utile nei loop: la differenza tra LEA EAX, [ EAX + 1 ]ed INC EAXè che quest'ultimo cambia EFLAGSma il primo no; questo preserva lo CMPstato
FrankH.

@FrankH. Ancora non capisco, quindi carica un puntatore da qualche altra parte?

6
@ ripDaddy69 sì, sorta di - se per "caricamento" intendi "esegue il calcolo dell'indirizzo / aritmetica del puntatore". Non accede alla memoria (cioè non "dereference" il puntatore come verrebbe chiamato in termini di programmazione in C).
FrankH.

2
+1: Questo rende esplicito che tipo di 'trucchi' LEApossono essere usati per ... (vedi "LEA (indirizzo di caricamento efficace) è spesso usato come un" trucco "per fare determinati calcoli" nella risposta popolare di IJ Kennedy sopra)
Assad Ebrahim,

3
C'è una grande differenza tra 2 operando LEA che è veloce e 3 operando LEA che è lento. Il manuale di ottimizzazione Intel afferma che il LEA a percorso rapido è a ciclo singolo e che il LEA a percorso lento richiede tre cicli. Inoltre, su Skylake ci sono due unità funzionali di percorso rapido (porte 1 e 5) e c'è solo un'unità funzionale di percorso lento (porta 1). La codifica 33 di assemblaggio / compilatore nel manuale avverte persino di non usare 3 LEA operando.
Olsonist

110

Un'altra caratteristica importante LEAdell'istruzione è che non altera i codici di condizione come CFe ZF, mentre calcola l'indirizzo con istruzioni aritmetiche come ADDo MULfa. Questa funzione riduce il livello di dipendenza tra le istruzioni e fa quindi spazio a ulteriori ottimizzazioni da parte del compilatore o del programmatore hardware.


1
Sì, a leavolte è utile per il compilatore (o codificatore umano) fare matematica senza ostruire un risultato di bandiera. Ma leanon è più veloce di add. La maggior parte delle istruzioni x86 scrive flag. Le implementazioni x86 ad alte prestazioni devono rinominare EFLAGS o altrimenti evitare il rischio di scrittura dopo scrittura per l'esecuzione rapida del codice normale, quindi le istruzioni che evitano la scrittura di flag non sono migliori a causa di ciò. (I contenuti di flag parziali possono creare problemi, vedere le istruzioni INC vs ADD 1: importa? )
Peter Cordes,

2
@PeterCordes: Odio portarlo qui, ma - sono solo a pensare che questo nuovo tag [x86-lea] sia ridondante e non necessario?
Michael Petch,

2
@MichaelPetch: Sì, penso che sia troppo specifico. Sembra confondere il principiante che non capisce il linguaggio macchina e che tutto (compresi i puntatori) sono solo bit / byte / numeri interi, quindi ci sono molte domande a riguardo con un numero enorme di voti. Ma avere un tag significa che c'è spazio per un numero aperto di domande future, quando in realtà ci sono circa 2 o 3 totali che non sono solo duplicati. (che cos'è? Come si usa per moltiplicare numeri interi? e come funziona internamente su AGU contro ALU e con quale latenza / throughput. E forse è lo scopo "previsto")
Peter Cordes

@PeterCordes: sono d'accordo, e semmai tutti questi post in fase di modifica sono praticamente un duplicato di alcune delle domande relative al LEA in uscita. Piuttosto che un tag, tutti i duplicati dovrebbero essere identificati e contrassegnati come imho.
Michael Petch,

1
@EvanCarroll: aspetta di taggare tutte le domande LEA, se non hai già finito. Come discusso in precedenza, riteniamo che x86-lea sia troppo specifico per un tag e non c'è molto spazio per future domande non duplicate. Penso che sarebbe un sacco di lavoro scegliere effettivamente una "migliore" Domande e risposte come obiettivo duplice per la maggior parte di loro, comunque, o decidere effettivamente quali ottenere le mod da unire.
Peter Cordes,

93

Nonostante tutte le spiegazioni, LEA è un'operazione aritmetica:

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

È solo che il suo nome è estremamente stupido per un turno + aggiungi operazione. Il motivo era già stato spiegato nelle risposte più votate (ovvero è stato progettato per mappare direttamente i riferimenti di memoria di alto livello).


8
E che l'aritmetica viene eseguita dall'hardware di calcolo dell'indirizzo.
Ben Voigt,

30
@BenVoigt Lo dicevo, perché sono un vecchio ragazzo :-) Tradizionalmente, le CPU x86 utilizzavano le unità di indirizzamento per questo, d'accordo. Ma la "separazione" è diventata molto sfocata in questi giorni. Alcune CPU non hanno più AGU dedicate , altre hanno scelto di non eseguire LEAsulle AGU ma sulle ALU intere ordinarie. Bisogna leggere molto attentamente le specifiche della CPU in questi giorni per scoprire "dove vanno le cose" ...
FrankH.

2
@FrankH .: le CPU fuori servizio in genere eseguono LEA su ALU, mentre alcune CPU in ordine (come Atom) a volte lo eseguono su un AGU (perché non possono essere occupate a gestire un accesso alla memoria).
Peter Cordes,

3
No, il nome non è stupido. LEAti dà l'indirizzo che deriva da qualsiasi modalità di indirizzamento legata alla memoria. Non è un turno e aggiungi un'operazione.
Kaz,

3
FWIW ci sono pochissime (se presenti) attuali CPU x86 che eseguono l'operazione sull'AGU. Quasi tutti usano una ALU come qualsiasi altra operazione aritmetica.
BeeOnRope,

77

Forse solo un'altra cosa sull'istruzione LEA. Puoi anche utilizzare LEA per i registri a moltiplicazione rapida per 3, 5 o 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

13
+1 per il trucco. Ma vorrei fare una domanda (può essere stupido), perché non moltiplicare direttamente con tre come questo LEA EAX, [EAX*3]?
Abid Rahman K

13
@Abid Rahman K: Non esistono istruzioni come unde set di istruzioni CPU x86.
GJ.

50
@AbidRahmanK nonostante la sintassi Intel asm la faccia sembrare una moltiplicazione, l'istruzione lea può codificare solo le operazioni di spostamento. Il codice operativo ha 2 bit per descrivere lo spostamento, quindi puoi moltiplicare solo per 1,2,4 o 8.
ithkuil

6
@Koray Tugay: puoi usare shift left come shlistruzione per moltiplicare i registri per 2,4,8,16 ... è più veloce e più breve. Ma per moltiplicare con numeri diversi di potenza di 2 usiamo normalmente mulistruzioni che sono più pretenziose e più lente.
GJ.

8
@GJ. sebbene non esista tale codifica, alcuni assemblatori lo accettano come scorciatoia, ad es. fasm. Quindi, ad esempio, lea eax,[eax*3]si tradurrebbe in equivalente di lea eax,[eax+eax*2].
Ruslan,

59

leaè un'abbreviazione di "indirizzo di caricamento efficace". Carica l'indirizzo del riferimento di posizione dall'operando di origine nell'operando di destinazione. Ad esempio, puoi usarlo per:

lea ebx, [ebx+eax*8]

per spostare ulteriormente gli elementi del ebxpuntatore eax(in un array a 64 bit / elemento) con una singola istruzione. Fondamentalmente, beneficiate di modalità di indirizzamento complesse supportate dall'architettura x86 per manipolare i puntatori in modo efficiente.


23

Il motivo principale che si utilizza LEAsu un MOVè se è necessario eseguire l'aritmetica sui registri che si sta utilizzando per calcolare l'indirizzo. In effetti, è possibile eseguire ciò che equivale all'aritmetica del puntatore su molti dei registri in combinazione in modo efficace "gratis".

La cosa davvero confusa al riguardo è che in genere scrivi un LEAtipo come un MOVma in realtà non stai dereferenziando la memoria. In altre parole:

MOV EAX, [ESP+4]

Ciò sposterà il contenuto di ciò a cui ESP+4punta EAX.

LEA EAX, [EBX*8]

Questo sposterà l'indirizzo effettivo EBX * 8in EAX, non quello che si trova in quella posizione. Come puoi vedere, inoltre, è possibile moltiplicare per due fattori (ridimensionamento) mentre a MOVè limitato all'aggiunta / sottrazione.


Scusate tutti @ big.heart mi ha ingannato dando una risposta a questo tre ore fa, facendolo apparire come "nuovo" nella mia domanda di interrogatorio dell'Assemblea.
David Hoelzer,

1
Perché la sintassi usa le parentesi quando non esegue l'indirizzamento della memoria?
Golopot

3
@ q4w56 Questa è una di quelle cose in cui la risposta è "È proprio così che lo fai." Credo che sia uno dei motivi per cui le persone hanno difficoltà a capire cosa LEAfa.
David Hoelzer,

2
@ q4w56: è un'istruzione shift + add che utilizza la sintassi dell'operando di memoria e la codifica del codice macchina. Su alcune CPU può persino usare l'hardware AGU, ma questo è un dettaglio storico. Il fatto ancora rilevante è che l'hardware del decodificatore esiste già per decodificare questo tipo di shift + add e LEA ci consente di usarlo per l'aritmetica anziché per l'indirizzamento della memoria. (O per i calcoli degli indirizzi se un input è effettivamente un puntatore).
Peter Cordes,

20

L'8086 ha una vasta famiglia di istruzioni che accetta un operando di registro e un indirizzo effettivo, esegue alcuni calcoli per calcolare la parte offset di quell'indirizzo effettivo ed esegue alcune operazioni che coinvolgono il registro e la memoria a cui fa riferimento l'indirizzo calcolato. Era abbastanza semplice che una delle istruzioni di quella famiglia si comportasse come sopra, tranne che per saltare quell'effettiva operazione di memoria. Questo, le istruzioni:

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

sono stati implementati quasi identicamente internamente. La differenza è un passo saltato. Entrambe le istruzioni funzionano in questo modo:

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

Per quanto riguarda il motivo per cui Intel pensava che valesse la pena includere queste istruzioni, non ne sono esattamente sicuro, ma il fatto che fosse economico da implementare sarebbe stato un grande fattore. Un altro fattore sarebbe stato il fatto che l'assemblatore Intel consentiva di definire simboli relativi al registro BP. Se fnordfosse definito come un simbolo relativo alla BP (es. BP + 8), si potrebbe dire:

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

Se uno volesse usare qualcosa come stosw per memorizzare i dati in un indirizzo relativo alla BP, potendo dire

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

era più conveniente di:

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

Si noti che dimenticare l'offset del mondo causerebbe l'aggiunta a DI del contenuto della posizione [BP + 8], anziché del valore 8. Ops.


12

Come indicato nelle risposte esistenti, LEApresenta i vantaggi di eseguire l'aritmetica di indirizzamento della memoria senza accedere alla memoria, salvando il risultato aritmetico in un registro diverso anziché nella semplice forma di istruzione add. Il vero vantaggio prestazionale sottostante è che il moderno processore ha un'unità LEA ALU separata e una porta per un'efficace generazione di indirizzi (compresi LEAe altri indirizzi di riferimento di memoria), ciò significa che l'operazione aritmetica in LEAe altre normali operazioni aritmetiche in ALU potrebbero essere eseguite in parallelo in una nucleo.

Consulta questo articolo sull'architettura Haswell per alcuni dettagli sull'unità LEA: http://www.realworldtech.com/haswell-cpu/4/

Un altro punto importante che non è menzionato in altre risposte è l' LEA REG, [MemoryAddress]istruzione PIC (codice indipendente dalla posizione) che codifica l'indirizzo relativo del PC in questa istruzione come riferimento MemoryAddress. Questo è diverso dal MOV REG, MemoryAddressquale codifica l'indirizzo virtuale relativo e richiede il trasferimento / patch nei moderni sistemi operativi (come ASLR è una caratteristica comune). Quindi LEApuò essere utilizzato per convertire tali non PIC in PIC.


2
La parte "separata LEA ALU" è per lo più falsa. Le CPU moderne vengono eseguite leasu una o più delle stesse ALU che eseguono altre istruzioni aritmetiche (ma generalmente meno di quelle di altre aritmetiche). Ad esempio, la CPU Haswell menzionata può eseguire addo la submaggior parte delle altre operazioni aritmetiche di base su quattro ALU diverse , ma può essere eseguita solo leasu una (complessa lea) o due (semplice lea). Ancora più importante, questi leaALU a due capacità sono semplicemente due dei quattro che possono eseguire altre istruzioni, quindi non vi è alcun vantaggio di parallelismo come affermato.
BeeOnRope,

L'articolo che hai collegato (correttamente) mostra che LEA si trova sulla stessa porta di un intero ALU (add / sub / boolean) e dell'unità MUL intera in Haswell. (E ALU vettoriali tra cui FP ADD / MUL / FMA). L'unità LEA solo-semplice si trova sulla porta 5, che esegue anche ADD / SUB / qualunque cosa, e vettori shuffle, e altro. L'unico motivo per cui non sto effettuando il downvoting è che fai notare l'uso del LEA relativo al RIP (solo per x86-64).
Peter Cordes,

8

L'istruzione LEA può essere utilizzata per evitare calcoli che richiedono tempo per indirizzi efficaci da parte della CPU. Se un indirizzo viene utilizzato ripetutamente, è più efficace memorizzarlo in un registro invece di calcolare l'indirizzo effettivo ogni volta che viene utilizzato.


Non necessariamente sul moderno x86. La maggior parte delle modalità di indirizzamento hanno lo stesso costo, con alcuni avvertimenti. Quindi [esi]raramente è più economico di quanto si pensi [esi + 4200]ed è solo raramente più economico di [esi + ecx*8 + 4200].
BeeOnRope,

@BeeOnRope [esi]non è più economico di [esi + ecx*8 + 4200]. Ma perché preoccuparsi del confronto? Non sono equivalenti. Se vuoi che il primo designi la stessa posizione di memoria del secondo, hai bisogno di ulteriori istruzioni: devi aggiungere esiil valore di ecxmoltiplicato per 8. Uh oh, la moltiplicazione sta per bloccare le bandiere della tua CPU! Quindi devi aggiungere il 4200. Queste istruzioni aggiuntive aggiungono alla dimensione del codice (occupando spazio nella cache delle istruzioni, cicli per il recupero).
Kaz,

2
@Kaz - Penso che ti stia perdendo il punto (altrimenti ho perso il punto del PO). La mia comprensione è che l'OP sta dicendo che se hai intenzione di usare qualcosa come [esi + 4200]ripetutamente in una sequenza di istruzioni, allora è meglio prima caricare l'indirizzo effettivo in un registro e usarlo. Ad esempio, piuttosto che scrivere add eax, [esi + 4200]; add ebx, [esi + 4200]; add ecx, [esi + 4200], dovresti preferire lea edi, [esi + 4200]; add eax, [edi]; add ebx, [edi]; add ecx, [edi], che raramente è più veloce. Almeno questa è la semplice interpretazione di questa risposta.
BeeOnRope,

Quindi il motivo per cui stavo confrontando [esi]e [esi + 4200](o [esi + ecx*8 + 4200]è che questa è la semplificazione che l'OP sta proponendo (come lo capisco): che N istruzioni con lo stesso indirizzo complesso vengono trasformate in N istruzioni con indirizzamento semplice (un registro), più uno lea, poiché l'indirizzamento complesso è "dispendioso in termini di tempo". In effetti, è più lento anche sul moderno x86, ma solo in termini di latenza che sembra improbabile per istruzioni consecutive con lo stesso indirizzo.
BeeOnRope

1
Forse allevi un po 'di pressione del registro, sì - ma potrebbe essere il contrario: se i registri con cui hai generato l'indirizzo effettivo sono attivi, hai bisogno di un altro registro per salvare il risultato e leaquindi aumentare la pressione in quel caso. In generale, la memorizzazione di intermedi è una causa della pressione del registro, non una soluzione ad esso - ma penso che nella maggior parte dei casi si tratti di un lavaggio. @Kaz
BeeOnRope,

7

L'istruzione LEA (Load Effective Address) è un modo per ottenere l'indirizzo che deriva da una qualsiasi delle modalità di indirizzamento della memoria del processore Intel.

Vale a dire, se abbiamo uno spostamento dei dati in questo modo:

MOV EAX, <MEM-OPERAND>

sposta il contenuto della posizione di memoria designata nel registro di destinazione.

Se sostituiamo il simbolo MOVby LEA, l'indirizzo della posizione di memoria viene calcolato esattamente allo stesso modo dall'espressione di <MEM-OPERAND>indirizzamento. Ma invece del contenuto della posizione della memoria, otteniamo la posizione stessa nella destinazione.

LEAnon è un'istruzione aritmetica specifica; è un modo per intercettare l'indirizzo effettivo derivante da una qualsiasi delle modalità di indirizzamento della memoria del processore.

Ad esempio, possiamo usare LEAsolo un semplice indirizzo diretto. Nessuna aritmetica è coinvolta affatto:

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

Questo è valido; possiamo provarlo al prompt di 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

Qui, non c'è aggiunta di un valore in scala e nessun offset. Zero viene spostato in EAX. Potremmo farlo usando anche MOV con un operando immediato.

Questo è il motivo per cui le persone che pensano che le parentesi LEAsiano superflue si sbagliano gravemente; le parentesi non sono LEAsintassi ma fanno parte della modalità di indirizzamento.

LEA è reale a livello hardware. L'istruzione generata codifica la modalità di indirizzamento effettiva e il processore la esegue fino al punto di calcolo dell'indirizzo. Quindi sposta l'indirizzo verso la destinazione invece di generare un riferimento di memoria. (Poiché il calcolo dell'indirizzo di una modalità di indirizzamento in qualsiasi altra istruzione non ha alcun effetto sui flag della CPU, LEAnon ha alcun effetto sui flag della CPU.)

Contrasto con il caricamento del valore dall'indirizzo zero:

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

È una codifica molto simile, vedi? Solo il 8dof LEAè cambiato in 8b.

Naturalmente, questa LEAcodifica è più lunga rispetto allo spostamento di uno zero immediato in EAX:

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

Non vi è alcun motivo per LEAescludere questa possibilità solo perché esiste un'alternativa più breve; si sta semplicemente combinando in modo ortogonale con le modalità di indirizzamento disponibili.


6

Ecco un esempio

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

Con -O (ottimizza) come opzione del compilatore, gcc troverà l'istruzione lea per la riga di codice indicata.


6

Sembra che molte risposte siano già complete, vorrei aggiungere un altro codice di esempio per mostrare come l'istruzione lea e move funzioni in modo diverso quando hanno lo stesso formato di espressione.

Per farla breve, sia le istruzioni lea che le istruzioni mov possono essere usate con le parentesi che racchiudono l'operando src delle istruzioni. Quando sono racchiusi tra () , l'espressione in () viene calcolata allo stesso modo; tuttavia, due istruzioni interpreteranno il valore calcolato nell'operando src in modo diverso.

Se l'espressione viene utilizzata con lea o mov, il valore src viene calcolato come di seguito.

D (Rb, Ri, S) => (Reg [Rb] + S * Reg [Ri] + D)

Tuttavia, quando viene utilizzato con l'istruzione mov, tenta di accedere al valore indicato dall'indirizzo generato dall'espressione precedente e di memorizzarlo nella destinazione.

Al contrario, quando l'istruzione lea viene eseguita con l'espressione precedente, carica il valore generato così com'è alla destinazione.

Il codice seguente esegue l'istruzione lea e l'istruzione mov con lo stesso parametro. Tuttavia, per cogliere la differenza, ho aggiunto un gestore di segnale a livello di utente per rilevare l'errore di segmentazione causato dall'accesso a un indirizzo errato a seguito dell'istruzione mov.

Codice di esempio

#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
          );
}

Risultato dell'esecuzione

Result of lea instruction: 14
inst at 0x4007b5 tries to access memory at 14, but failed

1
Suddividere la tua richiesta inline in dichiarazioni separate non è sicuro e le tue liste di clobber sono incomplete. Il blocco basic-asm dice al compilatore che non ha clobber, ma in realtà modifica diversi registri. Inoltre, puoi usare =dper dire al compilatore che il risultato è in EDX, salvando a mov. Hai anche lasciato fuori una dichiarazione di clobber sull'output. Questo dimostra ciò che stai cercando di dimostrare, ma è anche un cattivo esempio fuorviante di asm inline che si romperà se utilizzato in altri contesti. Questa è una brutta cosa per una risposta di overflow dello stack.
Peter Cordes,

Se non si desidera scrivere %%su tutti quei nomi di registro in Extended asm, utilizzare i vincoli di input. come asm("lea 4(%%ebx, %%eax, 8), %%edx" : "=d"(result_of_lea) : "a"(1), "b"(2));. Consentire ai registri di inizializzazione del compilatore significa che non è necessario dichiarare i clobber. Stai complicando le cose con lo zero o l'azzeramento prima che mov-immediate sovrascriva anche l'intero registro.
Peter Cordes,

@PeterCordes Grazie, Peter, vuoi che elimini questa risposta o la modifichi dopo i tuoi commenti?
Jaehyuk Lee

1
Se risolvi l'asin inline, non sta facendo alcun male e forse fa un buon esempio concreto per i principianti che non hanno capito le altre risposte. Non è necessario eliminarlo ed è una soluzione semplice come ho mostrato nel mio ultimo commento. Penso che varrebbe la pena dare un voto positivo se il cattivo esempio di asline inline fosse fissato in un esempio "buono". (Non ho votato in negativo)
Peter Cordes,

1
Dove qualcuno dice che mov 4(%ebx, %eax, 8), %edxnon è valido? Comunque, sì, movperché avrebbe senso scrivere "a"(1ULL)per dire al compilatore che hai un valore a 64 bit, e quindi deve assicurarsi che sia esteso per riempire l'intero registro. In pratica lo userà ancora mov $1, %eax, perché scrivere EAX zero-si estende in RAX, a meno che tu non abbia una strana situazione di codice circostante in cui il compilatore sapeva che RAX = 0xff00000001o qualcosa del genere. Per lea, stai ancora usando una dimensione di operando a 32 bit, quindi i bit alti dispersi nei registri di input non hanno alcun effetto sul risultato a 32 bit.
Peter Cordes,

4

LEA: solo un'istruzione "aritmetica".

MOV trasferisce i dati tra gli operandi ma lea sta solo calcolando


LEA ovviamente sposta i dati; ha un operando di destinazione. LEA non calcola sempre; calcola se viene calcolato l'indirizzo effettivo espresso nell'operando di origine. LEA EAX, GLOBALVAR non calcola; sposta semplicemente l'indirizzo di GLOBALVAR in EAX.
Kaz,

@Kaz grazie per il tuo feedback. la mia fonte era "LEA (load efficace address) è essenzialmente un'istruzione aritmetica - non esegue alcun accesso effettivo alla memoria, ma è comunemente usata per calcolare gli indirizzi (anche se puoi calcolare numeri interi per scopi generali con esso)." da Eldad-Eilam libro pagina 149
il ragioniere

@Kaz: ecco perché LEA è ridondante quando l'indirizzo è già una costante tempo di collegamento; usa mov eax, offset GLOBALVARinvece. È possibile utilizzare LEA, ma è leggermente più grande di code-dimensioni rispetto mov r32, imm32e gira su un minor numero di porte, perché va ancora attraverso il processo di indirizzo-di calcolo . lea reg, symbolè utile solo a 64 bit per un LEA relativo al RIP, quando sono necessari PIC e / o indirizzi al di fuori dei 32 bit bassi. Nel codice a 32 o 16 bit, non vi è alcun vantaggio. LEA è un'istruzione aritmetica che espone la capacità della CPU di decodificare / calcolare le modalità di indirizzamento.
Peter Cordes,

@Kaz: con lo stesso argomento, potresti dire che imul eax, edx, 1non calcola: copia solo edx in eax. Ma in realtà esegue i tuoi dati attraverso il moltiplicatore con latenza di 3 cicli. O che rorx eax, edx, 0semplicemente copia (ruota di zero).
Peter Cordes,

@PeterCordes Il mio punto è che sia LEA EAX, GLOBALVAL che MOV EAX, GLOBALVAR hanno appena preso l'indirizzo da un operando immediato. Non viene applicato un moltiplicatore di 1 o uno spostamento di 0; potrebbe essere così a livello hardware ma non si vede nel linguaggio assembly o nel set di istruzioni.
Kaz

1

Tutte le normali istruzioni di "calcolo" come l'aggiunta della moltiplicazione, l'esclusione o l'impostazione dei flag di stato come zero, firmano. Se si utilizza un indirizzo complicato, AX xor:= mem[0x333 +BX + 8*CX] i flag vengono impostati in base all'operazione xor.

Ora potresti voler utilizzare l'indirizzo più volte. Il caricamento di tali indirizzi in un registro non ha mai lo scopo di impostare flag di stato e per fortuna no. La frase "carica indirizzo effettivo" lo rende consapevole del programmatore. Ecco da dove viene la strana espressione.

È chiaro che una volta che il processore è in grado di utilizzare l'indirizzo complicato per elaborare il suo contenuto, è in grado di calcolarlo per altri scopi. In effetti può essere usato per eseguire una trasformazione x <- 3*x+1in un'istruzione. Questa è una regola generale nella programmazione degli assiemi: utilizzare le istruzioni comunque per far oscillare la barca. L'unica cosa che conta è se la particolare trasformazione incarnata dall'istruzione è utile per te.

Linea di fondo

MOV, X| T| AX'| R| BX|

e

LEA, AX'| [BX]

hanno lo stesso effetto su AX ma non sui flag di stato. (Questa è la notazione di ciasdis .)


"Questa è una regola generale nella programmazione degli assiemi: usa le istruzioni comunque scuoti la tua barca." Personalmente non darei questo consiglio, a causa di cose come call lbl lbl: pop rax"lavorare" tecnicamente come un modo per ottenere il valore di rip, ma renderai la previsione del ramo molto infelice. Usa le istruzioni come vuoi, ma non stupirti se fai qualcosa di complicato e ha conseguenze che non avevi previsto
The6P4C

@ The6P4C Questo è un avvertimento utile. Tuttavia, se non c'è alternativa a rendere infelice la previsione del ramo, bisogna provarci. C'è un'altra regola generale nella programmazione degli assiemi. Ci possono essere modi alternativi per fare qualcosa e devi scegliere saggiamente tra le alternative. Ci sono centinaia di modi per ottenere il contenuto del registro BL nel registro AL. Se non è necessario conservare il resto di RAX, LEA potrebbe essere un'opzione. Non influire sui flag potrebbe essere una buona idea su alcune delle migliaia di tipi di processori x86. Groetjes Albert
Albert van der Horst,

-1

Scusami se qualcuno lo ha già menzionato, ma nei giorni di x86 in cui la segmentazione della memoria era ancora rilevante, potresti non ottenere gli stessi risultati da queste due istruzioni:

LEA AX, DS:[0x1234]

e

LEA AX, CS:[0x1234]

1
L '"indirizzo effettivo" è solo la parte "offset" di una seg:offcoppia. LEA non è influenzato dalla base del segmento; entrambe queste istruzioni verranno (in modo inefficiente) inserite 0x1234in AX. x86 purtroppo non ha un modo semplice per calcolare un indirizzo lineare completo (efficace + base di segmenti) in un registro o in una coppia di registri.
Peter Cordes,

@PeterCordes Molto utile, grazie per avermi corretto.
Tzoz
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.