Tipps zum Golfen im x86 / x64-Maschinencode


27

Mir ist aufgefallen, dass es keine solche Frage gibt, also hier ist es:

Haben Sie allgemeine Tipps zum Golfen im Maschinencode? Wenn der Tipp nur für eine bestimmte Umgebung oder Anrufkonvention gilt, geben Sie dies bitte in Ihrer Antwort an.

Bitte nur einen Tipp pro Antwort (siehe hier ).

Antworten:


11

mov-immediate ist teuer für Konstanten

Das mag offensichtlich sein, aber ich werde es trotzdem hier platzieren. Im Allgemeinen lohnt es sich, über die Darstellung einer Zahl auf Bitebene nachzudenken, wenn Sie einen Wert initialisieren müssen.

Initialisierung eaxmit 0:

b8 00 00 00 00          mov    $0x0,%eax

sollte gekürzt werden ( aus Gründen der Leistung sowie der Codegröße ) auf

31 c0                   xor    %eax,%eax

Initialisierung eaxmit -1:

b8 ff ff ff ff          mov    $-1,%eax

kann auf gekürzt werden

31 c0                   xor    %eax,%eax
48                      dec    %eax

oder

83 c8 ff                or     $-1,%eax

Im Allgemeinen kann jeder vorzeichenerweiterte 8-Bit-Wert in 3 Bytes mit push -12(2 Bytes) / pop %eax(1 Byte) erstellt werden. Dies funktioniert sogar für 64-Bit-Register ohne zusätzliches REX-Präfix. push/ popdefault Operandengröße = 64.

6a f3                   pushq  $0xfffffffffffffff3
5d                      pop    %rbp

Oder wenn Sie eine bekannte Konstante in einem Register haben, können Sie mit lea 123(%eax), %ecx(3 Byte) eine weitere Konstante in der Nähe erstellen . Dies ist praktisch, wenn Sie ein Nullregister und eine Konstante benötigen . xor-zero (2 Bytes) + lea-disp8(3 Bytes).

31 c0                   xor    %eax,%eax
8d 48 0c                lea    0xc(%eax),%ecx

Siehe auch Alle Bits im CPU-Register effizient auf 1 setzen


Um ein Register mit einem kleinen Wert (8 Bit) ungleich 0 push 200; pop edxzu initialisieren, verwenden Sie zB - 3 Byte für die Initialisierung.
Anatolyg

2
Übrigens, um ein Register auf -1 zu initialisieren, verwenden Sie deczBxor eax, eax; dec eax
anatolyg

@anatolyg: 200 ist ein schlechtes Beispiel, es passt nicht in eine sign-extended-imm8. Aber ja, push imm8/ pop regist 3 Bytes und ist fantastisch für 64-Bit-Konstanten auf x86-64, wobei dec/ inc2 Bytes ist. Und push r64/ pop 64(2 Bytes) kann sogar 3 Bytes ersetzen mov r64, r64(3 Bytes mit REX). Siehe auch Setzen Sie alle Bits in CPU - Register auf 1 effizient für Sachen wie lea eax, [rcx-1]angegeben einen bekannten Wert in eax(zB bei Bedarf ein Null gesetzten Register und eine weitere Konstante, LEA nur Gebrauch statt Push / Pop
Peter Cordes

10

In vielen Fällen sind akkumulatorbasierte Befehle (dh Befehle, die (R|E)AXals Zieloperanden dienen) 1 Byte kürzer als allgemeine Befehle. Siehe diese Frage auf StackOverflow.


Normalerweise sind die al, imm8Sonderfälle am nützlichsten , zB or al, 0x20/ sub al, 'a'/ cmp al, 'z'-'a'/ ja .non_alphabeticmit jeweils 2 Bytes anstelle von 3. Die Verwendung alfür Zeichendaten ermöglicht auch lodsbund / oder stosb. Oder Gebrauch alzu testen , etwas über den Low - Byte von EAX, wie lodsd/ test al, 1/ setnz clcl = 1 oder 0 für ungerade macht / sogar. Aber in dem seltenen Fall, in dem Sie eine 32-Bit-Sofortversion benötigen, dann sicher op eax, imm32, wie in meiner Chroma-Key-Antwort
Peter Cordes

8

Wählen Sie Ihre Anrufkonvention, um die Argumente an die von Ihnen gewünschte Stelle zu setzen.

Die Sprache Ihrer Antwort ist asm (tatsächlich Maschinencode). Behandeln Sie sie daher als Teil eines in asm geschriebenen Programms, nicht in C-compiled-for-x86. Ihre Funktion muss mit keiner Standard-Aufrufkonvention von C aus leicht aufrufbar sein. Das ist aber ein schöner Bonus, wenn es Sie keine zusätzlichen Bytes kostet.

In einem reinen asm-Programm ist es normal, dass einige Hilfsfunktionen eine für sie und ihren Aufrufer geeignete Aufrufkonvention verwenden. Solche Funktionen dokumentieren ihre Aufrufkonvention (Ein- / Ausgänge / Clobber) mit Kommentaren.

Im wirklichen Leben tendieren sogar ASM-Programme (glaube ich) dazu, konsistente Aufrufkonventionen für die meisten Funktionen zu verwenden (insbesondere für verschiedene Quelldateien), aber jede wichtige Funktion könnte etwas Besonderes bewirken. Beim Code-Golf optimieren Sie den Mist aus einer einzigen Funktion heraus, es ist also offensichtlich wichtig / besonders.


Um Ihre Funktion in einem C-Programm zu testen, können Sie einen Wrapper schreiben , der args an die richtigen Stellen setzt, alle zusätzlichen Register speichert / wiederherstellt, die Sie überladen, und den Rückgabewert dort ablegen, e/raxwo er noch nicht vorhanden war.


Die Grenzen des Zumutbaren: Alles, was dem Anrufer keine unzumutbare Last auferlegt:

  • ESP / RSP muss aufrufsicher sein. andere Ganzzahlregs sind Freiwild. (RBP und RBX werden in der Regel in normalen Konventionen für Anrufe beibehalten , aber Sie können beide blockieren.)
  • Jedes Argument in einem beliebigen Register (außer RSP) ist sinnvoll, jedoch nicht die Aufforderung an den Aufrufer, dasselbe Argument in mehrere Register zu kopieren.
  • Es ist normal, dass DF (Zeichenfolgenrichtungsflag für lods/ stos/ usw.) beim Aufrufen / Zurückrufen (nach oben) frei sein muss. Es wäre in Ordnung, wenn es beim Anrufen / Zurückrufen undefiniert wäre. Es wäre seltsam, wenn es beim Betreten gelöscht oder eingestellt werden müsste, aber bei der Rückkehr geändert werden müsste.

  • Die Rückgabe von FP-Werten in x87 st0ist sinnvoll, die Rückgabe st3mit Garbage in einem anderen x87-Register jedoch nicht. Der Aufrufer müsste den x87-Stack aufräumen. Auch die Rückkehr st0mit nicht leeren höheren Stack-Registern wäre fraglich (es sei denn, Sie geben mehrere Werte zurück).

  • Ihre Funktion wird mit aufgerufen call, ebenso [rsp]Ihre Absenderadresse. Sie könnencall / retauf x86 vermeiden, indem Sie Linkregister wie lea rbx, [ret_addr]/ verwenden jmp functionund mit zurückkehren jmp rbx, aber das ist nicht "vernünftig". Das ist nicht so effizient wie call / ret, es ist also nichts, was man in echtem Code plausibel finden würde.
  • Unbegrenzten Speicher über RSP zu blockieren ist nicht sinnvoll, aber das Blockieren Ihrer Funktionsargumente auf dem Stack ist in normalen Aufrufkonventionen zulässig. x64 Windows benötigt 32 Byte Schattenspeicherplatz über der Rücksprungadresse, während x86-64 System V eine 128 Byte große rote Zone unter RSP anzeigt. (Oder sogar eine viel größere rote Zone, insbesondere in einem eigenständigen Programm, anstatt zu funktionieren.)

Grenzfälle: Schreiben Sie eine Funktion, die eine Sequenz in einem Array erzeugt, wenn die ersten beiden Elemente als Funktionsargumente angegeben werden . Ich habe mich dafür entschieden , dass der Aufrufer den Beginn der Sequenz im Array speichert und nur einen Zeiger auf das Array übergibt. Dies ist definitiv eine Biegung der Anforderungen der Frage. Ich betrachtete die args Einnahme verpackt in xmm0für movlps [rdi], xmm0, die auch eine seltsame Aufrufkonvention wäre.


Rückgabe eines Booleschen Wertes in FLAGS (Bedingungscodes)

OS X-Systemaufrufe führen dies aus ( CF=0bedeutet, dass kein Fehler vorliegt): Wird die Verwendung des Flags-Registers als boolescher Rückgabewert als schlechte Praxis angesehen? .

Jede Bedingung, die mit einem JCC überprüft werden kann, ist völlig zumutbar, insbesondere wenn Sie eine Bedingung auswählen können, die für das Problem semantisch relevant ist. (Zum Beispiel kann eine Vergleichsfunktion Flags setzen, jnedie verwendet werden, wenn sie nicht gleich sind.)


Es ist erforderlich, dass schmale Args (wie a char) ein Vorzeichen oder eine Null sind, die auf 32 oder 64 Bit erweitert ist.

Das ist nicht unvernünftig. Die Verwendung von movzxoder movsx zur Vermeidung von Teilregister-Verlangsamungen ist in modernen x86-Umgebungen normal. Tatsächlich erstellt clang / LLVM bereits Code, der von einer undokumentierten Erweiterung der x86-64-System-V-Aufrufkonvention abhängt: Argumente, die schmaler als 32 Bit sind, werden vom Aufrufer mit Vorzeichen oder Null auf 32 Bit erweitert .

Sie können die Erweiterung auf 64 Bit schriftlich uint64_toder int64_tin Ihrem Prototyp dokumentieren / beschreiben, wenn Sie möchten. Sie können also einen loopBefehl verwenden, der die gesamten 64 Bits von RCX verwendet, es sei denn, Sie verwenden ein Adressgrößenpräfix, um die Größe auf 32-Bit-ECX herabzusetzen (ja, tatsächlich, Adressgröße nicht Operandengröße).

Beachten Sie, dass longes sich beim Windows 64-Bit-ABI und beim Linux x32-ABI nur um einen 32-Bit-Typ handelt . uint64_tist eindeutig und kürzer als unsigned long long.


Bestehende Anrufkonventionen:

  • Windows 32-Bit __fastcall, bereits von einer anderen Antwort vorgeschlagen : Integer-Argumente in ecxund edx.

  • x86-64-System V : Übergibt viele Argumente in Registern und verfügt über viele Call-Clobbered-Register, die Sie ohne REX-Präfixe verwenden können. Noch wichtiger ist, dass Compiler memcpyso rep movsbeinfach inline oder memset arbeiten können: Die ersten 6 Integer / Pointer-Args werden in RDI, RSI, RDX, RCX, R8, R9 übergeben.

    Wenn Ihre Funktion lodsd/ stosdin einer Schleife verwendet, die rcx(mit der loopAnweisung) Zeiten ausführt, können Sie sagen, dass "von C wie int foo(int *rdi, const int *rsi, int dummy, uint64_t len)mit der x86-64-System V-Aufrufkonvention aufrufbar " ist. Beispiel: Chromakey .

  • 32-Bit-GCC regparm: Ganzzahlige Argumente in EAX , ECX, EDX, Rückgabe in EAX (oder EDX: EAX). Das erste Argument im selben Register wie der Rückgabewert zu haben, ermöglicht einige Optimierungen, wie in diesem Fall mit einem Beispielaufrufer und einem Prototyp mit einem Funktionsattribut . Und natürlich ist AL / EAX speziell für einige Anweisungen.

  • Das Linux x32-ABI verwendet im Langmodus 32-Bit-Zeiger, sodass Sie beim Ändern eines Zeigers ein REX-Präfix speichern können ( Beispielanwendungsfall ). Sie können weiterhin die 64-Bit-Adressgröße verwenden, es sei denn, Sie haben eine negative 32-Bit-Ganzzahl mit der Erweiterung Null in einem Register (dies wäre in diesem Fall ein großer vorzeichenloser Wert [rdi + rdx]).

    Beachten Sie, dass push rsp/ pop rax2 Bytes entspricht mov rax,rsp, sodass Sie weiterhin vollständige 64-Bit-Register in 2 Bytes kopieren können .


Halten Sie die Rückgabe auf dem Stack für sinnvoll, wenn Sie aufgefordert werden, ein Array zurückzugeben? Ich denke, das ist, was Compiler tun, wenn sie eine Struktur nach Wert zurückgeben.
Qwr

@qwr: nein, die Mainstream-Aufrufkonventionen übergeben einen versteckten Zeiger auf den Rückgabewert. (Einige Konventionen übergeben / geben kleine Strukturen in Registern zurück). C / C ++ gibt Struktur nach Wert unter der Haube zurück und siehe das Ende von Wie funktionieren Objekte in x86 auf Assembly-Ebene? . Beachten Sie, dass durch das Übergeben von Arrays (innerhalb von Strukturen) diese in den Stapel für x86-64-SysV kopiert werden: Was für ein C11-Datentyp ist ein Array gemäß AMD64-ABI , aber Windows x64 übergibt einen Nicht-Konstanten-Zeiger.
Peter Cordes

Also, was denkst du über vernünftig oder nicht? Zählen Sie x86 unter dieser Regel codegolf.meta.stackexchange.com/a/8507/17360
qwr

1
@qwr: x86 ist keine "stapelbasierte Sprache". x86 ist eine Registermaschine mit RAM , keine Stapelmaschine . Eine Stapelmaschine ist wie eine umgekehrte polnische Notation, wie x87-Register. fld / fld / faddp. Der Call-Stack von x86 passt nicht zu diesem Modell: Alle normalen Aufrufkonventionen lassen RSP unverändert oder lassen die Argumente aufflammen ret 16. Sie geben die Absenderadresse nicht an, verschieben ein Array und dann push rcx/ ret. Der Aufrufer müsste die Array-Größe kennen oder RSP irgendwo außerhalb des Stapels gespeichert haben, um sich selbst zu finden.
Peter Cordes

Aufruf drücke die Adresse des Befehls nach dem Aufruf im Stack jmp um die Funktion aufzurufen; ret Pop die Adresse aus dem Stapel und jmp zu dieser Adresse
RosLuP

7

Verwenden Sie spezielle Kurzformkodierungen für AL / AX / EAX sowie andere Kurzformen und Einzelbyte-Anweisungen

Bei den Beispielen wird der 32/64-Bit-Modus angenommen, bei dem die Standardoperandengröße 32 Bit beträgt. Ein Präfix mit Operandengröße ändert den Befehl in AX anstelle von EAX (oder umgekehrt im 16-Bit-Modus).

  • inc/decein Register (außer 8-Bit): inc eax/ dec ebp. (Nicht x86-64: Die 0x4xOpcode-Bytes wurden als REX-Präfixe verwendet. Dies inc r/m32ist die einzige Codierung.)

    8-Bit - inc bl2 Byte, unter Verwendung des inc r/m8opcode + ModR / M - Operanden kodieren . So verwenden inc ebxzu erhöhen bl, wenn es sicher ist. (zB wenn Sie das ZF-Ergebnis nicht benötigen, wenn die oberen Bytes möglicherweise nicht Null sind).

  • scasd: e/rdi+=4, erfordert, dass das Register auf einen lesbaren Speicher zeigt. Manchmal nützlich, auch wenn Sie sich nicht für das FLAGS-Ergebnis interessieren (wie cmp eax,[rdi]/ rdi+=4). Und im 64-Bit-Modus scasbkann als 1-Byte arbeiteninc rdi , wenn lodsb oder stosb nicht nützlich sind.

  • xchg eax, r32: Hier wird von 0x90 NOP kam: xchg eax,eax. Beispiel: 3 Register mit zwei xchgBefehlen in einer cdq/ idiv-Schleife für GCD in 8 Bytes neu anordnen, wobei die meisten Befehle Einzelbytes sind, einschließlich eines Missbrauchs von inc ecx/ loopanstelle von test ecx,ecx/jnz

  • cdq: Vorzeichenerweiterung von EAX in EDX: EAX, dh Kopieren des hohen EAX-Bits in alle EDX-Bits. Um eine Null mit bekannten nicht-negativen Werten zu erstellen, oder um eine 0 / -1 zu erhalten, mit der / sub oder maskiert wird. x86-Geschichtsstunde: cltqvs.movslq , und auch AT & T vs. Intel-Mnemonics für diese und die verwandten cdqe.

  • lodsb / d : like mov eax, [rsi]/ rsi += 4without clobbering flags. (Angenommen, DF ist klar, welche Standardaufrufkonventionen für die Funktionseingabe erforderlich sind.) Außerdem stosb / d, manchmal scas und seltener movs / cmps.

  • push/ pop reg. ZB im 64-Bit-Modus ist push rsp/ pop rdi2 Byte, mov rdi, rspbenötigt aber ein REX-Präfix und ist 3 Byte.

xlatbexistiert, ist aber selten nützlich. Eine große Nachschlagetabelle sollte vermieden werden. Ich habe auch noch nie eine Verwendung für AAA / DAA oder andere gepackte BCD- oder 2-ASCII-Ziffern-Anweisungen gefunden.

1 Byte lahf/ sahfsind selten nützlich. Sie könnten lahf / and ah, 1als Alternative zu setc ah, aber es ist in der Regel nicht nützlich.

Und speziell für CF gibt sbb eax,eaxes eine 0 / -1 oder sogar eine nicht dokumentierte, aber universell unterstützte 1-Byte-Größe salc(setze AL von Carry), die effektiv keine sbb al,alAuswirkung auf Flags hat. (In x86-64 entfernt). Ich habe SALC in der User Appreciation Challenge # 1 verwendet: Dennis ♦ .

1-Byte cmc/ clc/ stc(Flip ("Komplement"), Clear oder Set CF) sind selten nützlich, obwohl ich eine Verwendung für einecmc Addition mit erweiterter Genauigkeit mit Basis 10 ^ 9-Chunks gefunden habe. Um CF bedingungslos zu setzen / löschen, lassen Sie dies normalerweise als Teil eines anderen Befehls geschehen, z. B. xor eax,eaxCF und EAX löschen. Es gibt keine entsprechenden Anweisungen für andere Bedingungsflags, nur DF (Zeichenfolgenrichtung) und IF (Interrupts). Das Carry Flag ist speziell für viele Anweisungen. Shifts setzen es, adc al, 0können es in 2 Byte zu AL hinzufügen, und ich erwähnte zuvor die undokumentierte SALC.

std/ cldScheinen selten wert . Insbesondere im 32-Bit-Code ist es besser, nur deceinen Zeiger und einen movoder einen Speicherquellenoperanden für einen ALU-Befehl zu verwenden, anstatt DF so zu setzen lodsb/ stosbnach unten statt nach oben zu gehen. Normalerweise , wenn Sie nach unten überhaupt brauchen, haben Sie noch einen anderen Zeiger geht nach oben, so dass Sie mehr brauchen würden als eine stdund cldin der gesamten Funktion Verwendung lods/ stosfür beide. Verwenden Sie stattdessen einfach die Zeichenfolgenanweisungen für die Aufwärtsrichtung. (Die Standardaufrufkonventionen garantieren DF = 0 bei der Funktionseingabe, sodass Sie davon ausgehen können, dass dies ohne Verwendung von kostenlos ist cld.)


8086 history: Warum gibt es diese Kodierungen?

Im Original 8086 war AX ganz Besonderes: Anweisungen wie lodsb/ stosb, cbw, mul/ divund andere implizit verwenden. Das ist natürlich immer noch der Fall; Der aktuelle x86 hat keinen der 8086-Opcodes gelöscht (zumindest keinen der offiziell dokumentierten). Spätere CPUs fügten neue Anweisungen hinzu, die bessere / effizientere Möglichkeiten boten, Dinge zu erledigen, ohne sie zuerst in AX zu kopieren oder zu tauschen. (Oder zu EAX im 32-Bit-Modus.)

Zum Beispiel fehlten bei 8086 spätere Zusätze wie movsx/ movzxzum Laden oder Verschieben + Vorzeichen-Erweitern oder 2- und 3-Operanden imul cx, bx, 1234, die kein High-Half-Ergebnis liefern und keine impliziten Operanden haben.

Auch 8086 Haupt Engpass war Befehl holen, so die Optimierung für die Code-Größe wichtig war für die Leistung damals . Der ISA-Designer von 8086 (Stephen Morse) hat viel Opcode -Code für Sonderfälle für AX / AL ausgegeben, einschließlich spezieller (E) AX / AL-Ziel-Opcodes für alle grundlegenden ALU-Anweisungen von src , nur opcode + instant ohne ModR / M-Byte. 2 Byte add/sub/and/or/xor/cmp/test/... AL,imm8oder AX,imm16oder (im 32-Bit-Modus) EAX,imm32.

Es gibt jedoch keinen Sonderfall für EAX,imm8, sodass die reguläre ModR / M-Codierung add eax,4kürzer ist.

Es wird davon ausgegangen, dass Sie einige Daten in AX / AL bearbeiten möchten. Daher sollten Sie ein Register mit AX tauschen, vielleicht sogar öfter, als ein Register mit AX zu kopierenmov .

Alles, was mit der 8086-Befehlskodierung zu tun hat, unterstützt dieses Paradigma, angefangen von Befehlen lodsb/wüber alle Sonderfallkodierungen für Direktbefehle mit EAX bis hin zur impliziten Verwendung auch für Multiplikationen / Divisionen.


Lass dich nicht mitreißen; Es ist nicht automatisch ein Gewinn, alles zu EAX zu tauschen, besonders wenn Sie Sofort mit 32-Bit-Registern anstelle von 8-Bit verwenden müssen. Oder wenn Sie Operationen mit mehreren Variablen in Registern gleichzeitig verschachteln müssen. Oder wenn Sie Anweisungen mit 2 Registern verwenden, nicht sofort.

Aber denken Sie immer daran: Tue ich irgendetwas, das in EAX / AL kürzer wäre? Kann ich neu anordnen, damit ich dies in AL habe, oder nutze ich derzeit AL besser mit dem, wofür ich es bereits benutze?

Mischen Sie 8-Bit- und 32-Bit-Operationen frei, um die Vorteile zu nutzen, wann immer dies sicher ist (Sie müssen nicht in das vollständige Register oder was auch immer übertragen).


cdqist nützlich für divdie Bedürfnisse edxin vielen Fällen auf Null gesetzt .
Qwr

1
@qwr: Richtig, Sie können Missbrauch betreiben, cdqbevor Sie unsigniert sind, divwenn Sie wissen, dass Ihre Dividende unter 2 ^ 31 liegt (dh nicht negativ, wenn Sie als signiert behandelt werden), oder wenn Sie sie verwenden, bevor Sie eaxeinen potenziell großen Wert festlegen. Normalerweise (außerhalb von Code-Golf) würden Sie verwenden cdqals Setup für idivund xor edx,edxvordiv
Peter Cordes

5

Verwenden Sie fastcallKonventionen

Die x86-Plattform kennt viele Aufrufkonventionen . Sie sollten diejenigen verwenden, die Parameter in Registern übergeben. Auf x86_64 werden die ersten Parameter ohnehin in Registern übergeben, also kein Problem. Auf 32-Bit-Plattformen cdeclübergibt die Standardaufrufkonvention ( ) Parameter im Stapel, was für das Golfen ungeeignet ist - für den Zugriff auf Parameter im Stapel sind lange Anweisungen erforderlich.

Bei Verwendung fastcallauf 32-Bit-Plattformen werden in der Regel 2 erste Parameter in ecxund übergeben edx. Wenn Ihre Funktion 3 Parameter hat, können Sie sie auf einer 64-Bit-Plattform implementieren.

C-Funktionsprototypen für fastcallKonventionen (entnommen aus dieser Beispielantwort ):

extern int __fastcall SwapParity(int value);                 // MSVC
extern int __attribute__((fastcall)) SwapParity(int value);  // GNU   

Oder verwenden Sie eine vollständig benutzerdefinierte Aufrufkonvention , da Sie in asm schreiben und nicht unbedingt Code schreiben, der von C aus aufgerufen werden soll. Die Rückgabe von Booleschen Werten in FLAGS ist häufig praktisch.
Peter Cordes

5

Subtrahiere -128 anstatt 128 zu addieren

0100 81C38000      ADD     BX,0080
0104 83EB80        SUB     BX,-80

Addiere -128 anstatt 128 zu subtrahieren


1
Dies funktioniert auch in die andere Richtung, natürlich: add -128 statt Sub 128 Fun Tatsache: Compiler diese Optimierung wissen und tun auch eine damit verbundene Optimierung des Drehens < 128in <= 127der Größe einer unmittelbaren Operanden zu reduzieren cmp, oder gcc immer bevorzugt Neuanordnung vergleicht, um die Größe zu verringern, selbst wenn es nicht -129 gegen -128 ist.
Peter Cordes

4

Erstelle 3 Nullen mit mul(dann inc/ decum +1 / -1 sowie Null zu bekommen)

Sie können eax und edx auf Null setzen, indem Sie in einem dritten Register mit Null multiplizieren.

xor   ebx, ebx      ; 2B  ebx = 0
mul   ebx           ; 2B  eax=edx = 0

inc   ebx           ; 1B  ebx=1

Dies führt dazu, dass EAX, EDX und EBX in nur vier Bytes Null sind. Sie können EAX und EDX in drei Bytes auf Null setzen:

xor eax, eax
cdq

Von diesem Ausgangspunkt aus können Sie jedoch kein Register mit der dritten Null in einem weiteren Byte oder ein Register mit +1 oder -1 in weiteren 2 Bytes erhalten. Verwenden Sie stattdessen die Mul-Technik.

Anwendungsbeispiel: Verketten der Fibonacci-Zahlen in Binärform .

Beachten Sie, dass LOOPECX nach Beendigung einer Schleife Null ist und zum Nullen von EDX und EAX verwendet werden kann. Sie müssen nicht immer die erste Null mit erstellen xor.


1
Das ist etwas verwirrend. Könnten Sie erweitern?
NoOneIsHere

@NoOneIsHere Ich glaube, er möchte drei Register auf 0 setzen, einschließlich EAX und EDX.
NieDzejkob

4

CPU-Register und Flags befinden sich in bekannten Startzuständen

Wir können davon ausgehen, dass sich die CPU in einem bekannten und dokumentierten Standardzustand befindet, der auf der Plattform und dem Betriebssystem basiert.

Beispielsweise:

DOS http://www.fysnet.net/yourhelp.htm

Linux x86 ELF http://asm.sourceforge.net/articles/startup.html


1
Code Golf Regeln besagen, dass Ihr Code an mindestens einer Implementierung arbeiten muss. Linux setzt alle Regs (mit Ausnahme von RSP) und Stacks auf Null, bevor ein neuer User-Space-Prozess gestartet wird, obwohl die ABI-Dokumente für i386 und x86-64 System V beim Zugriff auf "undefiniert" sagen _start. Also ja, es ist fair, dies auszunutzen, wenn Sie ein Programm anstelle einer Funktion schreiben. Ich habe das in Extreme Fibonacci gemacht . (In einer dynamisch verknüpften ausführbaren Datei wird ld.so ausgeführt, bevor zu Ihrer Datei gesprungen wird _start, und es verbleibt kein Speicherplatz in den Registern, aber statisch ist nur Ihr Code.)
Peter Cordes,

3

Verwenden Sie zum Addieren oder Subtrahieren von 1 das eine Byte incoder die decAnweisungen, die kleiner sind als die Anweisungen zum Addieren und Subtrahieren von Mehrbytes.


Beachten Sie, dass der 32-Bit-Modus 1 Byte enthält, inc/dec r32wobei die Registernummer im Opcode codiert ist. Ist inc ebxalso 1 Byte, ist aber inc bl2. Noch kleiner als add bl, 1natürlich für andere Register als al. Beachten Sie auch, dass inc/ decCF unverändert bleibt, aber aktualisieren Sie die anderen Flags.
Peter Cordes

1
2 für +2 & -2 in x86
l4m2

3

lea für Mathe

Dies ist wahrscheinlich eines der ersten Dinge, die man über x86 erfährt, aber ich lasse es hier als Erinnerung. leakann verwendet werden, um eine Multiplikation mit 2, 3, 4, 5, 8 oder 9 durchzuführen und einen Versatz hinzuzufügen.

So berechnen Sie beispielsweise ebx = 9*eax + 3in einem Befehl (im 32-Bit-Modus):

8d 5c c0 03             lea    0x3(%eax,%eax,8),%ebx

Hier ist es ohne Versatz:

8d 1c c0                lea    (%eax,%eax,8),%ebx

Wow! leaKann natürlich auch verwendet werden, um mathematische Aufgaben ebx = edx + 8*eax + 3zur Berechnung der Array-Indizierung auszuführen.


1
Vielleicht ist erwähnenswert, dass dies lea eax, [rcx + 13]die Version ohne zusätzliche Präfixe für den 64-Bit-Modus ist. 32-Bit-Operandengröße (für das Ergebnis) und 64-Bit-Adressgröße (für die Eingaben).
Peter Cordes

3

Die Schleifen- und Zeichenkettenbefehle sind kleiner als alternative Befehlssequenzen. Am nützlichsten ist, loop <label>welche kleiner als die beiden Befehlssequenzen dec ECXund jnz <label>und lodsbkleiner als mov al,[esi]und ist inc si.


2

mov small wird bei Bedarf sofort in die unteren Register verschoben

Wenn Sie bereits wissen, dass die oberen Bits eines Registers 0 sind, können Sie einen kürzeren Befehl verwenden, um ein unmittelbares Bit in die unteren Register zu verschieben.

b8 0a 00 00 00          mov    $0xa,%eax

gegen

b0 0a                   mov    $0xa,%al

Verwenden Sie push/ popfür imm8, um die oberen Bits auf Null zu setzen

Dank an Peter Cordes. xor/ movist 4 Bytes, aber push/ popist nur 3!

6a 0a                   push   $0xa
58                      pop    %eax

mov al, 0xaist gut, wenn Sie es nicht null-erweitert auf die volle Ausrichtung brauchen. Wenn Sie dies jedoch tun, ist xor / mov 4 Byte im Vergleich zu 3 Byte für push imm8 / pop oder leavon einer anderen bekannten Konstante. Dies kann in Kombination mit mulder Nullstellung von 3 Registern in 4 Bytes nützlich sein , oder cdqwenn Sie viele Konstanten benötigen.
Peter Cordes

Der andere Anwendungsfall wäre für Konstanten aus [0x80..0xFF], die nicht als vorzeichenerweitertes imm8 darstellbar sind. Oder wenn Sie die oberen Bytes bereits kennen, z. B. mov cl, 0x10nach einer loopAnweisung, weil der einzige Weg loop, nicht zu springen, der ist, wenn es gemacht wird rcx=0. (Ich denke, Sie haben das gesagt , aber Ihr Beispiel verwendet ein xor). Sie können sogar das Low-Byte eines Registers für etwas anderes verwenden, sofern es durch etwas anderes auf Null (oder was auch immer) zurückgesetzt wird, wenn Sie fertig sind. zB mein Fibonacci-Programm bleibt -1024in ebx und benutzt bl.
Peter Cordes

@ PeterCordes Ich habe Ihre Push / Pop-Technik hinzugefügt
qwr

Sollte wohl in die bestehende Antwort über Konstanten gehen, wo anatolyg es schon in einem Kommentar angedeutet hat . Ich werde diese Antwort bearbeiten. IMO sollten Sie diese eine überarbeiten , um weitere Sachen vorschlagen , mit 8-Bit - Operanden-Größe (außer xchg eax, r32) zB mov bl, 10/ dec bl/ jnzso Ihr Code nicht über das hohe Bytes RBX schert.
Peter Cordes

@PeterCordes hmm. Ich bin mir immer noch nicht sicher, wann ich 8-Bit-Operanden verwenden soll, also weiß ich nicht, was ich in diese Antwort schreiben soll.
Qwr

2

Die FLAGS werden nach vielen Anweisungen gesetzt

Nach vielen arithmetischen Anweisungen werden das Carry Flag (ohne Vorzeichen) und das Overflow Flag (mit Vorzeichen) automatisch gesetzt ( weitere Informationen ). Das Vorzeichen-Flag und das Null-Flag werden nach vielen arithmetischen und logischen Operationen gesetzt. Dies kann zur bedingten Verzweigung verwendet werden.

Beispiel:

d1 f8                   sar    %eax

ZF wird durch diese Anweisung gesetzt, sodass wir es zur bedingten Verzweigung verwenden können.


Wann haben Sie jemals die Paritätsflagge verwendet? Sie wissen, es ist das horizontale xor der niedrigen 8 Bits des Ergebnisses, nicht wahr? (Unabhängig von der Operandengröße wird PF nur von den niedrigen 8 Bits gesetzt ; siehe auch ). Nicht gerade Zahl / ungerade Zahl; für den check ZF danach test al,1; das bekommst du normalerweise nicht umsonst. (Oder and al,1eine ganze Zahl 0/1 in Abhängigkeit von ungeraden / geraden zu erstellen.)
Peter Cordes

Wie auch immer, wenn in dieser Antwort stand "benutze Flags, die bereits durch andere Anweisungen gesetzt wurden, um test/ zu vermeiden cmp", dann wäre das ein ziemlich einfacher x86-Anfänger, aber dennoch eine Aufwertung wert.
Peter Cordes

@PeterCordes Huh, ich schien die Paritätsflagge falsch verstanden zu haben. Ich arbeite noch an meiner anderen Antwort. Ich werde die Antwort bearbeiten. Und wie Sie wahrscheinlich sagen können, bin ich ein Anfänger, also helfen grundlegende Tipps.
Qwr

2

Verwenden Sie do-while-Schleifen anstelle von while-Schleifen

Dies ist nicht x86-spezifisch, aber ein allgemein verwendbarer Einsteigertipp. Wenn Sie wissen, dass eine while-Schleife mindestens einmal ausgeführt wird, speichert das Umschreiben der Schleife als do-while-Schleife mit der Prüfung der Schleifenbedingung am Ende häufig einen 2-Byte-Sprungbefehl. In besonderen Fällen können Sie sogar verwenden loop.


2
Verwandte: Warum werden Schleifen immer so kompiliert? erklärt, warum do{}while()die natürliche Loop-Sprache bei der Montage verwendet wird (insbesondere aus Gründen der Effizienz). Beachten Sie auch, dass 2-Byte jecxz/ jrcxzvor einer Schleife sehr gut funktioniert loop, um den Fall "muss null Mal ausgeführt werden" "effizient" zu behandeln (auf den seltenen CPUs, bei denen loopes nicht langsam ist). jecxzist auch verwendbar innerhalb der Schleife einen zu implementierenwhile(ecx){} , mit jmpdem Boden bei.
Peter Cordes

@PeterCordes das ist eine sehr gut geschriebene Antwort. Ich würde gerne eine Verwendung finden, um in einem Code-Golf-Programm in die Mitte einer Schleife zu springen.
Qwr

Benutze goto jmp und indentation ... Schleife folgen
RosLuP

2

Verwenden Sie die gewünschten Aufrufkonventionen

System V x86 verwendet den Stack und System V x86-64 Anwendungen rdi, rsi, rdx, rcxusw. für Eingabeparameter und raxals Rückgabewert, aber es ist durchaus sinnvoll Ihre eigene Aufrufkonvention zu verwenden. __fastcall verwendet ecxund edxals Eingabeparameter, und andere Compiler / Betriebssysteme verwenden ihre eigenen Konventionen . Verwenden Sie den Stapel und alle Register als Ein- / Ausgabe, wenn Sie dies möchten.

Beispiel: Der repetitive Bytezähler unter Verwendung einer cleveren Aufrufkonvention für eine 1-Byte-Lösung.

Meta: Schreiben Eingang zu den Registern , Schreiben Ausgang Register

Weitere Quellen: Anmerkungen von Agner Fog zu Anrufkonventionen


1
Endlich kam ich dazu , meine eigene Antwort auf diese Frage zu verfassen, wie man Konventionen aufstellt und was vernünftig oder unvernünftig ist.
Peter Cordes

@ PeterCordes unabhängig, was ist der beste Weg, um in x86 zu drucken? Bisher habe ich Herausforderungen vermieden, die das Drucken erfordern. DOS scheint nützliche Interrupts für I / O zu haben, aber ich plane nur, 32/64-Bit-Antworten zu schreiben. Der einzige Weg, den ich kenne, ist int 0x80der, der ein paar Einstellungen erfordert.
qwr

Ja, int 0x80in 32-Bit-Code oder syscallin 64-Bit-Code aufzurufen sys_write, ist der einzige gute Weg. Das habe ich für Extreme Fibonacci verwendet . In 64-Bit-Code __NR_write = 1 = STDOUT_FILENO, so können Sie mov eax, edi. Oder wenn die oberen Bytes von EAX Null sind, mov al, 4im 32-Bit-Code. Du könntest auch call printfoder puts, denke ich, eine "x86 asm for Linux + glibc" Antwort schreiben. Ich halte es für vernünftig, den PLT- oder GOT-Eintragsbereich oder den Bibliothekscode selbst nicht zu zählen.
Peter Cordes

1
Ich wäre eher geneigt, wenn der Anrufer eine weitergibt char*bufund die Zeichenfolge mit manueller Formatierung erzeugt. zB so (umständlich auf Geschwindigkeit optimiert) wie FizzBuzz , wo ich String-Daten ins Register bekam und sie dann mitspeichertemov , weil die Strings kurz und von fester Länge waren.
Peter Cordes

1

Verwende bedingte Züge CMOVccund MengenSETcc

Dies ist eher eine Erinnerung an mich selbst, aber auf den Prozessoren P6 (Pentium Pro) oder neuer gibt es Anweisungen für bedingte Sätze und Anweisungen für bedingte Verschiebungen. Es gibt viele Anweisungen, die auf einem oder mehreren in EFLAGS gesetzten Flags basieren.


1
Ich habe festgestellt, dass die Verzweigung normalerweise kleiner ist. Es gibt einige Fälle, in denen es sich um eine natürliche Anpassung handelt, die jedoch cmoveinen 2-Byte-Opcode ( 0F 4x +ModR/M) enthält, also mindestens 3 Byte. Die Quelle ist jedoch r / m32, sodass Sie bedingt 3 Bytes laden können. Anders als Verzweigung setccist in mehr Fällen als nützlich cmovcc. Betrachten Sie dennoch den gesamten Befehlssatz und nicht nur die Basisanweisungen. (Obwohl SSE2- und BMI / BMI2-Befehle so umfangreich sind, dass sie selten nützlich sind. Sie rorx eax, ecx, 32sind 6 Byte lang und länger als mov + ror. Gut für die Leistung, nicht für Golf, es sei denn, POPCNT oder PDEP speichern viele isns.)
Peter Cordes

@PeterCordes danke, ich habe hinzugefügt setcc.
Qwr

1

Sparen Sie jmpBytes, indem Sie in if / then statt if / then / else anordnen

Dies ist sicherlich sehr einfach, dachte nur, ich würde dies als etwas zu denken, wenn Sie Golf spielen. Betrachten Sie als Beispiel den folgenden einfachen Code zum Dekodieren eines hexadezimalen Ziffernzeichens:

    cmp $'A', %al
    jae .Lletter
    sub $'0', %al
    jmp .Lprocess
.Lletter:
    sub $('A'-10), %al
.Lprocess:
    movzbl %al, %eax
    ...

Dies kann um zwei Bytes verkürzt werden, indem ein "then" -Fall in einen "else" -Fall umgewandelt wird:

    cmp $'A', %al
    jb .digit
    sub $('A'-'0'-10), %eax
.digit:
    sub $'0', %eax
    movzbl %al, %eax
    ...

Dies wird normalerweise häufig bei der Optimierung der Leistung durchgeführt, insbesondere wenn die zusätzliche subLatenz auf dem kritischen Pfad für einen Fall nicht Teil einer schleifenbasierten Abhängigkeitskette ist (wie hier, wo jede Eingabeziffer unabhängig ist, bis 4-Bit-Blöcke zusammengeführt werden ). Aber ich denke trotzdem +1. Übrigens hat Ihr Beispiel eine separate Fehloptimierung: Wenn Sie movzxam Ende ohnehin eine benötigen, verwenden Sie sub $imm, %alnicht EAX, um die 2-Byte-Codierung von no-modrm zu nutzen op $imm, %al.
Peter Cordes

Sie können das auch beseitigen, cmpindem Sie tun sub $'A'-10, %al; jae .was_alpha; add $('A'-10)-'0'. (Ich glaube, ich habe die Logik richtig verstanden). Beachten Sie, 'A'-10 > '9'dass es keine Mehrdeutigkeiten gibt. Wenn Sie die Korrektur für einen Buchstaben subtrahieren, wird eine Dezimalstelle umgebrochen. Das ist also sicher, wenn wir davon ausgehen, dass unsere Eingabe ein gültiges Hex ist, genau wie Ihre.
Peter Cordes

0

Sie können sequentielle Objekte aus dem Stapel abrufen, indem Sie esi auf esp setzen und eine Sequenz von lodsd / xchg reg, eax ausführen.


Warum ist das besser als pop eax/ pop edx/ ...? Wenn Sie sie auf dem Stapel belassen müssen, können Sie pushsie alle zurücksetzen, um ESP wiederherzustellen, und zwar immer noch 2 Bytes pro Objekt, ohne dass dies erforderlich ist mov esi,esp. Oder meinten Sie für 4-Byte-Objekte im 64-Bit-Code, wo pop8 Bytes erhalten würden? Übrigens können Sie sogar popeine Schleife über einen Puffer mit einer besseren Leistung als lodsdz. B. für eine Addition mit erweiterter Genauigkeit in Extreme Fibonacci
Peter Cordes,

Es ist richtiger nach einem "lea esi, [esp + size of ret address]", was die Verwendung von pop ausschließen würde, wenn Sie kein Ersatzregister haben.
Peter Ferrie

Oh, für Funktionsargumente? Ziemlich selten möchten Sie, dass mehr Argumente als Register vorhanden sind, oder dass der Aufrufer eines im Speicher belässt, anstatt sie alle in Registern zu übergeben. (Ich habe eine halbfertige Antwort zur Verwendung von benutzerdefinierten Anrufkonventionen, falls eine der Standardkonventionen für Registeraufrufe nicht perfekt passt.)
Peter Cordes

cdecl anstelle von fastcall belässt die Parameter auf dem Stack und es ist einfach, viele Parameter zu haben. Siehe zum Beispiel github.com/peterferrie/tinycrypt.
Peter Ferrie

0

Für Codegolf und ASM: Verwenden Sie Anweisungen, verwenden Sie nur Register, drücken Sie Pop, minimieren Sie den Registerspeicher oder speichern Sie sofort


0

Verwenden Sie zum Kopieren eines 64-Bit-Registers push rcx; pop rdxanstelle eines 3-Byte mov.
Die Standardoperandengröße für Push / Pop ist 64-Bit, ohne dass ein REX-Präfix erforderlich ist.

  51                      push   rcx
  5a                      pop    rdx
                vs.
  48 89 ca                mov    rdx,rcx

(Ein Präfix mit Operandengröße kann die Push / Pop-Größe auf 16-Bit überschreiben, aber die 32-Bit-Push / Pop-Operandengröße kann im 64-Bit-Modus auch mit REX.W = 0 nicht codiert werden .)

Wenn eines oder beide Register r8.. sind r15, verwenden Sie, movda Push und / oder Pop ein REX-Präfix benötigen. Schlimmstenfalls verliert dies tatsächlich, wenn beide REX-Präfixe benötigen. Offensichtlich sollten Sie im Codegolf normalerweise ohnehin r8..r15 meiden.


Sie können Ihre Quelle während der Entwicklung mit diesem NASM-Makro besser lesbar halten . Denken Sie daran, dass die 8 Bytes unterhalb von RSP angezeigt werden. (In der roten Zone in x86-64 System V). Aber unter normalen Bedingungen ist es ein Ersatz für 64-Bit mov r64,r64odermov r64, -128..127

    ; mov  %1, %2       ; use this macro to copy 64-bit registers in 2 bytes (no REX prefix)
%macro MOVE 2
    push  %2
    pop   %1
%endmacro

Beispiele:

   MOVE  rax, rsi            ; 2 bytes  (push + pop)
   MOVE  rbp, rdx            ; 2 bytes  (push + pop)
   mov   ecx, edi            ; 2 bytes.  32-bit operand size doesn't need REX prefixes

   MOVE  r8, r10             ; 4 bytes, don't use
   mov   r8, r10             ; 3 bytes, REX prefix has W=1 and the bits for reg and r/m being high

   xchg  eax, edi            ; 1 byte  (special xchg-with-accumulator opcodes)
   xchg  rax, rdi            ; 2 bytes (REX.W + that)

   xchg  ecx, edx            ; 2 bytes (normal xchg + modrm)
   xchg  rcx, rdx            ; 3 bytes (normal REX + xchg + modrm)

Der xchgTeil des Beispiels ist, weil Sie manchmal einen Wert in EAX oder RAX erhalten müssen und es nicht wichtig ist, die alte Kopie beizubehalten. push / pop hilft dir aber nicht beim tauschen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.