Was ist der Zweck der LEA-Anweisung?

676

Für mich scheint es nur ein funky MOV zu sein. Was ist ihr Zweck und wann sollte ich es verwenden?

user200557
quelle
2
Siehe auch Verwenden von LEA für Werte, die keine Adressen / Zeiger sind? : LEA ist nur eine Shift-and-Add-Anweisung. Es wurde wahrscheinlich zu 8086 hinzugefügt, weil die Hardware bereits zum Dekodieren und Berechnen von Adressierungsmodi vorhanden ist, nicht weil sie nur für die Verwendung mit Adressen "vorgesehen" ist. Denken Sie daran, dass Zeiger in Assembly nur Ganzzahlen sind.
Peter Cordes

Antworten:

797

Wie andere bereits betont haben, wird LEA (Load Effective Address) häufig als "Trick" für bestimmte Berechnungen verwendet, aber das ist nicht der Hauptzweck. Der x86-Befehlssatz wurde entwickelt, um Hochsprachen wie Pascal und C zu unterstützen, bei denen Arrays - insbesondere Arrays von Ints oder kleinen Strukturen - häufig vorkommen. Stellen Sie sich zum Beispiel eine Struktur vor, die (x, y) -Koordinaten darstellt:

struct Point
{
     int xcoord;
     int ycoord;
};

Stellen Sie sich nun eine Aussage vor wie:

int y = points[i].ycoord;

wo points[]ist ein Array von Point. Angenommen , die Basis der Anordnung ist bereits in EBXund variabel iist EAX, und xcoordund ycoordsind jeweils 32 Bit (so ycoordwird bei Offset 4 Bytes in der struct) Diese Anweisung kann zu erstellen:

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

die landen yin EDX. Der Skalierungsfaktor von 8 liegt daran, dass jedes Point8 Byte groß ist. Betrachten Sie nun denselben Ausdruck, der mit dem Operator "Adresse von" & verwendet wird:

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

In diesem Fall möchten Sie nicht den Wert von ycoord, sondern dessen Adresse. Hier kommt die LEA(effektive Adresse laden) ins Spiel. Anstelle von a MOVkann der Compiler generieren

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

Dadurch wird die Adresse geladen ESI.

IJ Kennedy
quelle
112
Wäre es nicht sauberer gewesen, die movAnweisung zu erweitern und die Klammern wegzulassen? MOV EDX, EBX + 8*EAX + 4
Natan Yellin
14
@imacake Wenn Sie LEA durch ein spezielles MOV ersetzen, halten Sie die Syntax sauber: [] Klammern entsprechen immer der Dereferenzierung eines Zeigers in C. Ohne Klammern behandeln Sie immer den Zeiger selbst.
Natan Yellin
139
Das Rechnen in einer MOV-Anweisung (EBX + 8 * EAX + 4) ist ungültig. LEA ESI, [EBX + 8 * EAX + 4] ist gültig, da dies ein von x86 unterstützter Adressierungsmodus ist. en.wikipedia.org/wiki/X86#Addressing_modes
Erik
29
@ JonathanDickinson LEA ist wie eine MOVmit einer indirekten Quelle, außer dass es nur die Indirektion und nicht die tut MOV. Es liest nicht wirklich von der berechneten Adresse, sondern berechnet sie nur.
Hobbs
24
Erik, Tourkommentar ist nicht korrekt. MOV eax, [ebx + 8 * ecx + 4] ist gültig. MOV gibt jedoch den Inhalt des Speicherorts zurück, während LEA die Adresse
zurückgibt
562

Aus dem "Zen der Versammlung" von Abrash:

LEA, der einzige Befehl, der Speicheradressierungsberechnungen durchführt, den Speicher jedoch nicht adressiert. LEAakzeptiert einen Standardspeicheradressierungsoperanden, speichert jedoch lediglich den berechneten Speicherversatz in dem angegebenen Register, das ein beliebiges Allzweckregister sein kann.

Was gibt uns das? Zwei Dinge, ADDdie nicht bieten:

  1. die Fähigkeit, eine Addition mit zwei oder drei Operanden durchzuführen, und
  2. die Fähigkeit, das Ergebnis in einem beliebigen Register zu speichern ; nicht nur einer der Quelloperanden.

Und LEAändert die Flaggen nicht.

Beispiele

  • LEA EAX, [ EAX + EBX + 1234567 ]berechnet EAX + EBX + 1234567(das sind drei Operanden)
  • LEA EAX, [ EBX + ECX ]berechnet, EBX + ECXohne das Ergebnis zu überschreiben.
  • Multiplikation mit Konstante (mit zwei, drei, fünf oder neun), wenn Sie es wie verwenden LEA EAX, [ EBX + N * EBX ](N kann 1,2,4,8 sein).

Ein anderer Anwendungsfall ist in Schleifen praktisch: Der Unterschied zwischen LEA EAX, [ EAX + 1 ]und INC EAXbesteht darin, dass sich der letztere ändert EFLAGS, der erstere jedoch nicht; Dies bewahrt den CMPZustand.

Frank Krueger
quelle
42
@AbidRahmanK einige Beispiele: LEA EAX, [ EAX + EBX + 1234567 ]Berechnet die Summe von EAX, EBXund 1234567(das sind drei Operanden). LEA EAX, [ EBX + ECX ]berechnet, EBX + ECX ohne das Ergebnis zu überschreiben. Das dritte, wofür LEA(nicht von Frank aufgeführt) verwendet wird, ist die Multiplikation mit der Konstanten (mit zwei, drei, fünf oder neun), wenn Sie es wie verwenden LEA EAX, [ EBX + N * EBX ]( Nkann 1,2,4,8 sein). Ein anderer Anwendungsfall ist in Schleifen praktisch: Der Unterschied zwischen LEA EAX, [ EAX + 1 ]und INC EAXbesteht darin, dass sich der letztere ändert EFLAGS, der erstere jedoch nicht; das bewahrt den CMPZustand
FrankH.
@FrankH. Ich verstehe immer noch nicht, also lädt es einen Zeiger auf irgendwo anders?
6
@ ripDaddy69 ja, irgendwie - wenn mit "Laden" gemeint ist "führt die Adressberechnung / Zeigerarithmetik durch". Es greift nicht auf den Speicher zu (dh es wird der Zeiger nicht "dereferenziert", wie er in C-Programmierbegriffen genannt wird).
FrankH.
2
+1: Dies macht deutlich, für welche Arten von 'Tricks' LEAverwendet werden kann ... (siehe "LEA (Load Effective Address) wird häufig als" Trick "verwendet, um bestimmte Berechnungen durchzuführen" in IJ Kennedys populärer Antwort oben)
Assad Ebrahim
3
Es gibt einen großen Unterschied zwischen 2 Operanden LEA, die schnell sind, und 3 Operanden LEA, die langsam sind. Das Intel Optimization-Handbuch besagt, dass LEA mit schnellem Pfad ein einzelner Zyklus ist und LEA mit langsamem Pfad drei Zyklen dauert. Darüber hinaus gibt es auf Skylake zwei Funktionseinheiten für schnelle Pfade (Ports 1 und 5) und nur eine Funktionseinheit für langsame Pfade (Port 1). Assembly / Compiler-Codierung Regel 33 im Handbuch warnt sogar vor der Verwendung von 3 Operanden LEA.
Olsonist
110

Ein weiteres wichtiges Merkmal des LEABefehls ist, dass er die Bedingungscodes wie CFund nicht ändert ZF, während die Adresse durch arithmetische Befehle wie ADDoder berechnet MULwird. Diese Funktion verringert die Abhängigkeit zwischen Anweisungen und bietet somit Raum für weitere Optimierungen durch den Compiler oder den Hardware-Scheduler.

Angus Lee
quelle
1
Ja, leamanchmal ist es für den Compiler (oder den menschlichen Codierer) nützlich, zu rechnen, ohne ein Flag-Ergebnis zu beeinträchtigen. Ist leaaber nicht schneller als add. Die meisten x86-Anweisungen schreiben Flags. Hochleistungs-x86-Implementierungen müssen EFLAGS umbenennen oder auf andere Weise die Gefahr des Schreibens nach dem Schreiben vermeiden , damit normaler Code schnell ausgeführt werden kann. Daher sind Anweisungen, die das Schreiben von Flags vermeiden, aus diesem Grund nicht besser. ( Teilweise Flaggenmaterial kann Probleme verursachen, siehe INC-Anweisung gegen ADD 1: Ist das wichtig? )
Peter Cordes
2
@PeterCordes: Ich hasse es, das hier anzusprechen, aber - bin ich allein, wenn ich denke, dass dieses neue [x86-lea] -Tag überflüssig und unnötig ist?
Michael Petch
2
@ MichaelPetch: Ja, ich denke es ist zu spezifisch. Es scheint Anfänger zu verwirren, die die Maschinensprache nicht verstehen und dass alles (einschließlich Zeiger) nur Bits / Bytes / Ganzzahlen sind, daher gibt es viele Fragen mit einer großen Anzahl von Stimmen. Ein Tag dafür bedeutet jedoch, dass Platz für eine unbegrenzte Anzahl zukünftiger Fragen vorhanden ist, obwohl es tatsächlich etwa 2 oder 3 insgesamt gibt, die nicht nur Duplikate sind. (Was ist das? Wie wird es zum Multiplizieren von ganzen Zahlen verwendet? Und wie läuft es intern auf AGUs im Vergleich zu ALUs und mit welcher Latenz / welchem ​​Durchsatz. Und vielleicht ist es ein "beabsichtigter" Zweck)
Peter Cordes
@PeterCordes: Ich stimme zu, und wenn überhaupt, sind all diese Beiträge, die bearbeitet werden, so ziemlich ein Duplikat einiger der spannenden LEA-bezogenen Fragen. Anstelle eines Tags sollten alle Duplikate identifiziert und imho markiert werden.
Michael Petch
1
@EvanCarroll: Hängen Sie daran, alle LEA-Fragen zu markieren, falls Sie noch nicht fertig sind. Wie oben erläutert, halten wir x86-lea für zu spezifisch für ein Tag, und es gibt nicht viel Spielraum für zukünftige nicht doppelte Fragen. Ich denke, es wäre eine Menge Arbeit, tatsächlich ein "bestes" Q & A als Dup-Ziel für die meisten von ihnen auszuwählen oder tatsächlich zu entscheiden, welche Mods zusammengeführt werden sollen.
Peter Cordes
93

Trotz aller Erklärungen ist LEA eine arithmetische Operation:

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

Es ist nur so, dass sein Name für eine Shift + Add-Operation extrem dumm ist. Der Grund dafür wurde bereits in den am besten bewerteten Antworten erläutert (dh es wurde entwickelt, um Speicherreferenzen auf hoher Ebene direkt abzubilden).

hdante
quelle
8
Und dass die Arithmetik von der Adressberechnungshardware ausgeführt wird.
Ben Voigt
30
@ BenVoigt Ich habe das immer gesagt, weil ich ein alter Kerl bin :-) Traditionell haben x86-CPUs die Adressierungseinheiten dafür verwendet, stimmte zu. Aber die "Trennung" ist heutzutage sehr verschwommen. Einige CPUs haben überhaupt keine dedizierten AGUs mehr, andere haben sich dafür entschieden, nicht LEAauf den AGUs, sondern auf den normalen Ganzzahl-ALUs auszuführen . Man muss die CPU-Spezifikationen heutzutage sehr genau lesen, um herauszufinden, "wo Sachen laufen" ...
FrankH.
2
@FrankH.: CPUs außerhalb der Reihenfolge führen LEA normalerweise auf ALUs aus, während einige CPUs in der richtigen Reihenfolge (wie Atom) es manchmal auf AGUs ausführen (weil sie nicht mit der Verarbeitung eines Speicherzugriffs beschäftigt sein können).
Peter Cordes
3
Nein, der Name ist nicht dumm. LEAgibt Ihnen die Adresse an, die sich aus einem speicherbezogenen Adressierungsmodus ergibt. Es ist keine Shift- und Add-Operation.
Kaz
3
FWIW gibt es nur sehr wenige (wenn überhaupt) aktuelle x86-CPUs, die die Operation auf der AGU ausführen. Die meisten oder alle verwenden einfach eine ALU wie jede andere arithmetische Operation.
BeeOnRope
77

Vielleicht nur eine andere Sache über LEA-Unterricht. Sie können LEA auch zum schnellen Multiplizieren von Registern mit 3, 5 oder 9 verwenden.

LEA EAX, [EAX * 2 + EAX]   ;EAX = EAX * 3
LEA EAX, [EAX * 4 + EAX]   ;EAX = EAX * 5
LEA EAX, [EAX * 8 + EAX]   ;EAX = EAX * 9
GJ.
quelle
13
+1 für den Trick. Aber ich möchte eine Frage stellen (mag dumm sein), warum nicht direkt mit drei davon multiplizieren LEA EAX, [EAX*3]?
Abid Rahman K
13
@Abid Rahman K: Es gibt keinen Befehl unter x86-CPU-Befehlssatz.
GJ.
50
@AbidRahmanK Trotz der Intel ASM-Syntax sieht es wie eine Multiplikation aus. Der Befehl lea kann nur Verschiebungsoperationen codieren. Der Opcode hat 2 Bits, um die Verschiebung zu beschreiben, daher können Sie nur mit 1,2,4 oder 8 multiplizieren.
ithkuil
6
@Koray Tugay: Sie können die shlAnweisung zum Verschieben nach links verwenden , um Register mit 2,4,8,16 zu multiplizieren ... es ist schneller und kürzer. Aber zum Multiplizieren mit Zahlen unterschiedlicher Potenz von 2 verwenden wir normalerweise mulAnweisungen, die anspruchsvoller und langsamer sind.
GJ.
7
@ GJ. Obwohl es keine solche Codierung gibt, akzeptieren einige Assembler dies als Abkürzung, z. B. fasm. Also zB lea eax,[eax*3]würde das gleichbedeutend sein mit lea eax,[eax+eax*2].
Ruslan
59

leaist eine Abkürzung für "effektive Adresse laden". Es lädt die Adresse der Ortsreferenz durch den Quelloperanden in den Zieloperanden. Zum Beispiel könnten Sie es verwenden, um:

lea ebx, [ebx+eax*8]

bewegen ebxZeiger eaxArtikel weiter (in einer 64-bit / Element - Array) mit einer einzigen Anweisung. Grundsätzlich profitieren Sie von komplexen Adressierungsmodi, die von der x86-Architektur unterstützt werden, um Zeiger effizient zu bearbeiten.

Mehrdad Afshari
quelle
23

Der Hauptgrund, den Sie LEAüber a verwenden, MOVbesteht darin, dass Sie für die Register, die Sie zur Berechnung der Adresse verwenden, eine Arithmetik durchführen müssen. Effektiv können Sie effektiv "kostenlos" eine Zeigerarithmetik für mehrere Register in Kombination durchführen.

Was wirklich verwirrend ist, ist, dass Sie normalerweise ein LEAgenau wie ein schreiben, MOVaber den Speicher nicht wirklich dereferenzieren. Mit anderen Worten:

MOV EAX, [ESP+4]

Dadurch wird der Inhalt dessen verschoben, worauf ESP+4verwiesen wird EAX.

LEA EAX, [EBX*8]

Dadurch wird die effektive Adresse EBX * 8in EAX verschoben , nicht in das, was sich an diesem Speicherort befindet. Wie Sie sehen können, ist es auch möglich, mit zwei Faktoren zu multiplizieren (Skalierung), während a MOVauf das Addieren / Subtrahieren beschränkt ist.

David Hoelzer
quelle
Entschuldigung an alle. @ big.heart hat mich getäuscht, indem er vor drei Stunden eine Antwort darauf gegeben hat, damit sie in meiner Versammlungsfrage als "neu" angezeigt wird.
David Hoelzer
1
Warum verwendet die Syntax Klammern, wenn keine Speicheradressierung erfolgt?
Golopot
3
@ q4w56 Dies ist eines der Dinge, bei denen die Antwort lautet: "So machst du es einfach." Ich glaube, das ist einer der Gründe, warum es den Menschen so schwer fällt, herauszufinden, was sie LEAtun.
David Hoelzer
2
@ q4w56: Es handelt sich um eine Anweisung Shift + Add, die die Syntax des Speicheroperanden und die Codierung des Maschinencodes verwendet. Auf einigen CPUs wird möglicherweise sogar die AGU-Hardware verwendet, dies ist jedoch ein historisches Detail. Die immer noch relevante Tatsache ist, dass die Decoderhardware bereits zum Decodieren dieser Art von Shift + Add vorhanden ist und LEA es uns ermöglicht, sie für die Arithmetik anstelle der Speicheradressierung zu verwenden. (Oder für Adressberechnungen, wenn eine Eingabe tatsächlich ein Zeiger ist).
Peter Cordes
20

Der 8086 verfügt über eine große Familie von Befehlen, die einen Registeroperanden und eine effektive Adresse akzeptieren, einige Berechnungen durchführen, um den Versatzteil dieser effektiven Adresse zu berechnen, und einige Operationen ausführen, die das Register und den Speicher betreffen, auf die sich die berechnete Adresse bezieht. Es war ziemlich einfach, eine der Anweisungen in dieser Familie wie oben zu verhalten, außer dass diese eigentliche Speicheroperation übersprungen wurde. Dies sind die Anweisungen:

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

wurden intern fast identisch implementiert. Der Unterschied ist ein übersprungener Schritt. Beide Anweisungen funktionieren ungefähr so:

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

Ich bin mir nicht ganz sicher, warum Intel diese Anweisung für wertvoll hielt, aber die Tatsache, dass die Implementierung billig war, wäre ein großer Faktor gewesen. Ein weiterer Faktor wäre die Tatsache gewesen, dass Intels Assembler die Definition von Symbolen relativ zum BP-Register ermöglichte. Wenn fnordals BP-relatives Symbol definiert wurde (z. B. BP + 8), könnte man sagen:

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

Wenn man so etwas wie stosw verwenden wollte, um Daten an einer BP-relativen Adresse zu speichern, kann man sagen

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

war bequemer als:

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

Beachten Sie, dass das Vergessen des Welt- "Offsets" dazu führen würde, dass der Inhalt von Position [BP + 8] anstelle des Werts 8 zu DI hinzugefügt wird. Hoppla.

Superkatze
quelle
12

Wie die vorhandenen Antworten erwähnt haben, LEAhat dies den Vorteil, dass eine Speicheradressierungsarithmetik ohne Zugriff auf den Speicher durchgeführt wird und das arithmetische Ergebnis in einem anderen Register anstelle der einfachen Form der Additionsanweisung gespeichert wird. Der eigentliche Leistungsvorteil besteht darin, dass der moderne Prozessor über eine separate LEA-ALU-Einheit und einen separaten Port für eine effektive Adressgenerierung (einschließlich LEAund anderer Speicherreferenzadressen) verfügt. Dies bedeutet, dass die arithmetische Operation in LEAund andere normale arithmetische Operationen in ALU parallel in einer ausgeführt werden können Ader.

In diesem Artikel der Haswell-Architektur finden Sie einige Details zur LEA-Einheit: http://www.realworldtech.com/haswell-cpu/4/

Ein weiterer wichtiger Punkt, der in anderen Antworten nicht erwähnt wird, ist der LEA REG, [MemoryAddress]Befehl PIC (positionsunabhängiger Code), der die relative PC-Adresse in diesem Befehl als Referenz codiert MemoryAddress. Dies unterscheidet sich von MOV REG, MemoryAddressder Codierung der relativen virtuellen Adresse und erfordert das Verschieben / Patchen in modernen Betriebssystemen (wie ASLR ist eine gemeinsame Funktion). So LEAkann verwendet werden, nicht PIC zu PIC zu konvertieren.

Thomson
quelle
2
Der "separate LEA ALU" Teil ist größtenteils falsch. Moderne CPUs werden leaauf einer oder mehreren derselben ALUs ausgeführt, die andere arithmetische Befehle ausführen (im Allgemeinen jedoch weniger als andere arithmetische). Zum Beispiel erwähnte der Haswell - CPU ausführen kann addoder suboder die meisten anderen Grundrechenarten auf vier verschiedene ALUs, kann aber nur ausführen leaauf einem (Komplex lea) oder zwei (einfach lea). Noch wichtiger ist, dass diese zwei leafähigen ALUs einfach zwei der vier sind, die andere Befehle ausführen können, so dass es keinen behaupteten Parallelitätsvorteil gibt.
BeeOnRope
Der Artikel, den Sie (korrekt) verlinkt haben, zeigt, dass sich LEA am selben Port befindet wie eine Ganzzahl-ALU (add / sub / boolean) und die Ganzzahl-MUL-Einheit in Haswell. (Und Vektor-ALUs einschließlich FP ADD / MUL / FMA). Die einfache LEA-Einheit befindet sich an Port 5, auf dem auch ADD / SUB / Whatever sowie Vektor-Shuffles und andere Dinge ausgeführt werden. Der einzige Grund, warum ich nicht abstimme, ist, dass Sie auf die Verwendung von RIP-relativem LEA hinweisen (nur für x86-64).
Peter Cordes
8

Der LEA-Befehl kann verwendet werden, um zeitaufwändige Berechnungen effektiver Adressen durch die CPU zu vermeiden. Wenn eine Adresse wiederholt verwendet wird, ist es effektiver, sie in einem Register zu speichern, als die effektive Adresse jedes Mal zu berechnen, wenn sie verwendet wird.

rot-E
quelle
Nicht unbedingt auf modernem x86. Die meisten Adressierungsmodi haben mit einigen Einschränkungen die gleichen Kosten. Ist [esi]also selten billiger als sagen [esi + 4200]und ist nur selten billiger als [esi + ecx*8 + 4200].
BeeOnRope
@BeeOnRope [esi]ist nicht billiger als [esi + ecx*8 + 4200]. Aber warum sich die Mühe machen zu vergleichen? Sie sind nicht gleichwertig. Wenn Sie möchten, dass der erstere denselben Speicherort wie der letztere bezeichnet, benötigen Sie zusätzliche Anweisungen: Sie müssen esiden Wert von ecxmultipliziert mit 8 addieren . Oh, die Multiplikation wird Ihre CPU-Flags blockieren! Dann müssen Sie den 4200 hinzufügen. Diese zusätzlichen Anweisungen erhöhen die Codegröße (belegen Speicherplatz im Anweisungscache, Zyklen zum Abrufen).
Kaz
2
@Kaz - Ich denke, Sie haben meinen Punkt verfehlt (oder ich habe den Punkt des OP verpasst). Mein Verständnis ist, dass das OP sagt, wenn Sie so etwas wie [esi + 4200]wiederholt in einer Folge von Anweisungen verwenden wollen, ist es besser, zuerst die effektive Adresse in ein Register zu laden und diese zu verwenden. Zum Beispiel add eax, [esi + 4200]; add ebx, [esi + 4200]; add ecx, [esi + 4200]sollten Sie lieber schreiben als schreiben lea edi, [esi + 4200]; add eax, [edi]; add ebx, [edi]; add ecx, [edi], was selten schneller ist. Zumindest ist das die einfache Interpretation dieser Antwort.
BeeOnRope
Der Grund, warum ich verglichen habe [esi]und [esi + 4200](oder [esi + ecx*8 + 4200]ist, dass dies die Vereinfachung ist, die das OP vorschlägt (so wie ich es verstehe): dass N Befehle mit derselben komplexen Adresse in N Befehle mit einfacher (ein reg) Adressierung plus eins umgewandelt werden lea, da komplexe Adressierung "zeitaufwändig" ist. Tatsächlich ist sie sogar auf modernem x86 langsamer, aber nur in
Bezug auf die
1
Vielleicht entlasten Sie den Registerdruck, ja - aber das Gegenteil kann der Fall sein: Wenn die Register, mit denen Sie die effektive Adresse generiert haben, aktiv sind, benötigen Sie ein anderes Register, um das Ergebnis zu speichern, leasodass der Druck in diesem Fall erhöht wird. Im Allgemeinen ist die Lagerung von Zwischenprodukten eine Ursache für Registerdruck, keine Lösung dafür - aber ich denke, in den meisten Situationen ist es eine Wäsche. @ Kaz
BeeOnRope
7

Mit dem LEA-Befehl (Load Effective Address) kann die Adresse ermittelt werden, die sich aus einem der Speicheradressierungsmodi des Intel-Prozessors ergibt.

Das heißt, wenn wir eine Datenverschiebung wie diese haben:

MOV EAX, <MEM-OPERAND>

Es verschiebt den Inhalt des angegebenen Speicherorts in das Zielregister.

Wenn wir das MOVdurch ersetzen LEA, wird die Adresse des Speicherorts durch den <MEM-OPERAND>Adressierungsausdruck genauso berechnet . Anstelle des Inhalts des Speicherorts erhalten wir jedoch den Speicherort selbst in das Ziel.

LEAist keine spezifische arithmetische Anweisung; Dies ist eine Möglichkeit, die effektive Adresse abzufangen, die sich aus einem der Speicheradressierungsmodi des Prozessors ergibt.

Zum Beispiel können wir LEAnur eine einfache direkte Adresse verwenden. Es ist überhaupt keine Arithmetik beteiligt:

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

Dies ist gültig; Wir können es an der Linux-Eingabeaufforderung testen:

$ 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

Hier gibt es keine Addition eines skalierten Wertes und keinen Offset. Null wird in EAX verschoben. Wir könnten das auch mit MOV mit einem Sofortoperanden machen.

Dies ist der Grund, warum Leute, die denken, dass die Klammern LEAüberflüssig sind, sich schwer irren; Die Klammern sind keine LEASyntax, sondern Teil des Adressierungsmodus.

LEA ist auf Hardwareebene real. Der erzeugte Befehl codiert den tatsächlichen Adressierungsmodus und der Prozessor führt ihn bis zur Berechnung der Adresse aus. Dann wird diese Adresse an das Ziel verschoben, anstatt eine Speicherreferenz zu generieren. (Da die Adressberechnung eines Adressierungsmodus in einem anderen Befehl keine Auswirkung auf CPU-Flags hat, LEAhat dies keine Auswirkung auf CPU-Flags.)

Im Gegensatz zum Laden des Werts von Adresse Null:

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

Es ist eine sehr ähnliche Codierung, sehen Sie? Nur das 8dvon LEAhat sich geändert zu 8b.

Natürlich ist diese LEACodierung länger als das Verschieben einer unmittelbaren Null in EAX:

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

Es gibt keinen Grund LEA, diese Möglichkeit auszuschließen, nur weil es eine kürzere Alternative gibt. Es wird nur orthogonal mit den verfügbaren Adressierungsmodi kombiniert.

Kaz
quelle
6

Hier ist ein Beispiel.

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

Mit -O (optimieren) als Compileroption findet gcc die lea-Anweisung für die angegebene Codezeile.

user3634373
quelle
6

Es scheint, dass viele Antworten bereits vollständig sind. Ich möchte noch einen Beispielcode hinzufügen, um zu zeigen, wie die Anweisungen lea und move unterschiedlich funktionieren, wenn sie dasselbe Ausdrucksformat haben.

Um es kurz zu machen, können sowohl lea-Anweisungen als auch mov-Anweisungen mit den Klammern verwendet werden, die den src-Operanden der Anweisungen einschließen. Wenn sie mit () eingeschlossen sind , wird der Ausdruck in () auf die gleiche Weise berechnet. Zwei Anweisungen interpretieren den berechneten Wert im src-Operanden jedoch unterschiedlich.

Unabhängig davon, ob der Ausdruck mit lea oder mov verwendet wird, wird der src-Wert wie folgt berechnet.

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

Wenn es jedoch mit der Anweisung mov verwendet wird, versucht es, auf den Wert zuzugreifen, auf den die durch den obigen Ausdruck erzeugte Adresse zeigt, und ihn im Ziel zu speichern.

Im Gegensatz dazu lädt der lea-Befehl, wenn er mit dem obigen Ausdruck ausgeführt wird, den generierten Wert so wie er ist in das Ziel.

Der folgende Code führt den Befehl lea und mov mit demselben Parameter aus. Um den Unterschied zu erkennen, habe ich jedoch einen Signalhandler auf Benutzerebene hinzugefügt, um den Segmentierungsfehler zu erfassen, der durch den Zugriff auf eine falsche Adresse infolge eines mov-Befehls verursacht wird.

Beispielcode

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

Ausführungsergebnis

Result of lea instruction: 14
inst at 0x4007b5 tries to access memory at 14, but failed
Jaehyuk Lee
quelle
1
Das Aufteilen Ihres Inline-Asms in separate Anweisungen ist unsicher und Ihre Clobber-Listen sind unvollständig. Der Basic-Asm-Block teilt dem Compiler mit, dass er keine Clobber hat, ändert jedoch tatsächlich mehrere Register. Sie können =ddem Compiler auch mitteilen, dass das Ergebnis in EDX vorliegt, und a speichern mov. Sie haben auch eine Early-Clobber-Deklaration für die Ausgabe ausgelassen. Dies zeigt zwar, was Sie demonstrieren möchten, ist aber auch ein irreführendes Beispiel für Inline-Asm, das bei Verwendung in anderen Kontexten nicht funktioniert. Das ist eine schlechte Sache für eine Stapelüberlaufantwort.
Peter Cordes
Wenn Sie nicht %%auf alle diese Registernamen in Extended asm schreiben möchten , verwenden Sie Eingabeeinschränkungen. wie asm("lea 4(%%ebx, %%eax, 8), %%edx" : "=d"(result_of_lea) : "a"(1), "b"(2));. Wenn Sie die Compiler-Init-Register zulassen, müssen Sie auch keine Clobber deklarieren. Sie überkomplizieren die Dinge durch Xor-Zeroing, bevor mov-unmittelbar auch das gesamte Register überschreibt.
Peter Cordes
@PeterCordes Danke, Peter, soll ich diese Antwort löschen oder nach Ihren Kommentaren ändern?
Jaehyuk Lee
1
Wenn Sie den Inline-Asm korrigieren, schadet er nicht und ist möglicherweise ein gutes konkretes Beispiel für Anfänger, die die anderen Antworten nicht verstanden haben. Keine Notwendigkeit zu löschen, und es ist eine einfache Lösung, wie ich in meinem letzten Kommentar gezeigt habe. Ich denke, es wäre eine positive Bewertung wert, wenn das schlechte Beispiel für Inline-Asm in ein "gutes" Beispiel umgewandelt würde. (Ich habe nicht abgelehnt)
Peter Cordes
1
Wo sagt jemand, dass mov 4(%ebx, %eax, 8), %edxdas ungültig ist? Wie auch immer, ja, denn moves wäre sinnvoll zu schreiben "a"(1ULL), um dem Compiler mitzuteilen, dass Sie einen 64-Bit-Wert haben, und daher muss er sicherstellen, dass er erweitert ist, um das gesamte Register zu füllen. In der Praxis wird es weiterhin verwendet mov $1, %eax, da das Schreiben von EAX mit Null in RAX erweitert wird, es sei denn, Sie haben eine seltsame Situation mit umgebendem Code, in der der Compiler wusste, dass RAX = 0xff00000001oder so. Denn lea, sind Sie noch mit 32-Bit - Operanden-Größe, so dass die alle Streu High - Bits in Eingangsregister haben keinen Einfluss auf die 32-Bit - Ergebnis.
Peter Cordes
4

LEA: nur eine "arithmetische" Anweisung.

MOV überträgt Daten zwischen Operanden, aber lea berechnet nur

der Buchhalter
quelle
LEA verschiebt offensichtlich Daten; Es hat einen Zieloperanden. LEA berechnet nicht immer; Es wird berechnet, ob die im Quelloperanden ausgedrückte effektive Adresse berechnet wird. LEA EAX, GLOBALVAR berechnet nicht; Es wird nur die Adresse von GLOBALVAR in EAX verschoben.
Kaz
@ Kaz danke für dein Feedback. Meine Quelle war "LEA (Load Effective Address) ist im Wesentlichen eine arithmetische Anweisung - sie führt keinen tatsächlichen Speicherzugriff durch, wird jedoch häufig zur Berechnung von Adressen verwendet (obwohl Sie damit Ganzzahlen für allgemeine Zwecke berechnen können)." Formular Eldad-Eilam Buch Seite 149
der Buchhalter
@Kaz: Deshalb ist LEA redundant, wenn die Adresse bereits eine Verbindungszeitkonstante ist. Verwenden Sie mov eax, offset GLOBALVARstattdessen. Sie können LEA verwenden, es ist jedoch etwas größer als der Code mov r32, imm32und wird auf weniger Ports ausgeführt, da der Adressberechnungsprozess noch durchlaufen wird . lea reg, symbolist nur in 64-Bit für eine RIP-relative LEA nützlich, wenn Sie PIC und / oder Adressen außerhalb der niedrigen 32 Bit benötigen. In 32- oder 16-Bit-Code gibt es keinen Vorteil. LEA ist eine arithmetische Anweisung, die die Fähigkeit der CPU offenlegt, Adressierungsmodi zu decodieren / zu berechnen.
Peter Cordes
@Kaz: Mit dem gleichen Argument könnte man sagen, dass imul eax, edx, 1das nicht kalkuliert: Es kopiert nur edx nach eax. Tatsächlich werden Ihre Daten jedoch mit einer Latenz von 3 Zyklen durch den Multiplikator geleitet. Oder das rorx eax, edx, 0kopiert einfach (um Null drehen).
Peter Cordes
@PeterCordes Mein Punkt ist, dass sowohl LEA EAX, GLOBALVAL als auch MOV EAX, GLOBALVAR nur die Adresse von einem unmittelbaren Operanden abrufen. Es wird kein Multiplikator von 1 oder Offset von 0 angewendet. Dies könnte auf Hardwareebene so sein, wird jedoch in der Assemblersprache oder im Befehlssatz nicht angezeigt.
Kaz
1

Alle normalen "Berechnungs" -Anweisungen wie das Hinzufügen von Multiplikationen, Exklusiv- oder Setzen der Statusflags wie Null, Vorzeichen. Wenn Sie eine komplizierte Adresse verwenden, werden AX xor:= mem[0x333 +BX + 8*CX] die Flags gemäß der xor-Operation gesetzt.

Jetzt möchten Sie die Adresse möglicherweise mehrmals verwenden. Das Laden einer solchen Adresse in ein Register soll niemals Statusflags setzen und zum Glück nicht. Der Ausdruck "effektive Adresse laden" macht den Programmierer darauf aufmerksam. Von dort kommt der seltsame Ausdruck.

Es ist klar, dass der Prozessor, sobald er in der Lage ist, die komplizierte Adresse zur Verarbeitung seines Inhalts zu verwenden, diese für andere Zwecke berechnen kann. In der Tat kann es verwendet werden, um eine Transformation x <- 3*x+1in einem Befehl durchzuführen . Dies ist eine allgemeine Regel bei der Baugruppenprogrammierung: Verwenden Sie die Anweisungen, jedoch wird Ihr Boot dadurch erschüttert. Es kommt nur darauf an, ob die in der Anweisung enthaltene Transformation für Sie nützlich ist.

Endeffekt

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

und

LEA, AX'| [BX]

haben den gleichen Effekt auf AX, jedoch nicht auf die Statusflags. (Dies ist die Ciasdis- Notation.)

Albert van der Horst
quelle
"Dies ist eine allgemeine Regel bei der Baugruppenprogrammierung: Verwenden Sie die Anweisungen, aber es rockt Ihr Boot." Ich würde diesen Rat nicht persönlich aussprechen, weil Dinge wie call lbl lbl: pop rax"technisch" funktionieren, um den Wert zu ermitteln rip, aber Sie werden die Branchenvorhersage sehr unglücklich machen. Verwenden Sie die Anweisungen, wie Sie möchten, aber wundern Sie sich nicht, wenn Sie etwas
Kniffliges
@ The6P4C Das ist eine nützliche Einschränkung. Wenn es jedoch keine Alternative gibt, um die Verzweigungsvorhersage unglücklich zu machen, muss man sich dafür entscheiden. Es gibt eine andere allgemeine Regel bei der Baugruppenprogrammierung. Es kann alternative Möglichkeiten geben, etwas zu tun, und Sie müssen mit Bedacht aus Alternativen auswählen. Es gibt Hunderte von Möglichkeiten, den Inhalt des Registers BL in das Register AL zu übertragen. Wenn der Rest von RAX nicht erhalten bleiben muss, kann LEA eine Option sein. Bei einigen der Tausenden von x86-Prozessortypen ist es möglicherweise eine gute Idee, die Flags nicht zu beeinflussen. Groetjes Albert
Albert van der Horst