Suggerimenti per giocare a golf nel codice macchina x86 / x64


27

Ho notato che non esiste una domanda del genere, quindi eccola qui:

Hai suggerimenti generali per giocare a golf nel codice della macchina? Se il suggerimento si applica solo a un determinato ambiente o convenzione di chiamata, specificarlo nella risposta.

Per favore, solo un suggerimento per risposta (vedi qui ).

Risposte:


11

mov-immediato è costoso per le costanti

Questo potrebbe essere ovvio, ma lo metterò ancora qui. In generale, è utile pensare alla rappresentazione a livello di bit di un numero quando è necessario inizializzare un valore.

Inizializzazione eaxcon 0:

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

dovrebbe essere abbreviato ( per prestazioni e dimensioni del codice ) a

31 c0                   xor    %eax,%eax

Inizializzazione eaxcon -1:

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

può essere abbreviato in

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

o

83 c8 ff                or     $-1,%eax

O più in generale, è possibile creare qualsiasi valore con estensione del segno a 8 bit in 3 byte con push -12(2 byte) / pop %eax(1 byte). Funziona anche con i registri a 64 bit senza prefisso REX aggiuntivo; push/ popdefault operando-size = 64.

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

Oppure, data una costante nota in un registro, è possibile creare un'altra costante vicina usando lea 123(%eax), %ecx(3 byte). Questo è utile se hai bisogno di un registro azzerato e una costante; xor-zero (2 byte) + lea-disp8(3 byte).

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

Vedi anche Impostare tutti i bit nel registro CPU su 1 in modo efficiente


Inoltre, per inizializzare un registro con un valore piccolo (8 bit) diverso da 0: utilizzare ad es. push 200; pop edx- 3 byte per l'inizializzazione.
Anatolyg,

2
BTW per inizializzare un registro su -1, utilizzare dec, ad esempioxor eax, eax; dec eax
anatolyg

@anatolyg: 200 è un cattivo esempio, non si adatta a un segno-esteso-imm8. Ma sì, push imm8/ pop regè 3 byte ed è fantastico per le costanti a 64 bit su x86-64, dove dec/ incè 2 byte. E push r64/ pop 64(2 byte) può persino sostituire un 3 byte mov r64, r64(3 byte con REX). Vedi anche Impostare tutti i bit nel registro CPU su 1 in modo efficiente per cose come lea eax, [rcx-1]dato un valore noto in eax(ad esempio se è necessario un registro azzerato e un'altra costante, basta usare LEA invece di push / pop
Peter Cordes

10

In molti casi, le istruzioni basate sull'accumulatore (ovvero quelle che considerano (R|E)AXl'operando di destinazione) sono più brevi di 1 byte rispetto alle istruzioni del caso generale; vedi questa domanda su StackOverflow.


Normalmente la maggior quelli utili sono i al, imm8casi particolari, come or al, 0x20/ sub al, 'a'/ cmp al, 'z'-'a'/ ja .non_alphabeticessendo 2 byte ciascuno, invece di 3. Utilizzo aldi dati di carattere consente anche lodsbe / o stosb. Oppure usa alper testare qualcosa sul byte basso di EAX, come lodsd/ test al, 1/ setnz clrende cl = 1 o 0 per pari / dispari. Ma nel raro caso in cui hai bisogno di un immediato a 32 bit, quindi sicuro op eax, imm32, come nella mia risposta di chroma-key
Peter Cordes,

8

Scegli la tua convenzione di chiamata per mettere args dove li vuoi.

La lingua della tua risposta è asm (in realtà il codice macchina), quindi trattala come parte di un programma scritto in asm, non C-compilato-per-x86. La tua funzione non deve essere facilmente richiamabile da C con nessuna convenzione di chiamata standard. Questo è un bel bonus se non ti costa byte extra, però.

In un programma asm puro, è normale che alcune funzioni di supporto utilizzino una convenzione di chiamata che è conveniente per loro e per il loro chiamante. Tali funzioni documentano la loro convenzione di chiamata (input / output / clobbers) con commenti.

Nella vita reale, anche i programmi asm (penso) tendono ad usare convenzioni di chiamata coerenti per la maggior parte delle funzioni (specialmente attraverso diversi file sorgente), ma ogni data funzione importante potrebbe fare qualcosa di speciale. Nel code-golf, stai ottimizzando la schifezza di una singola funzione, quindi ovviamente è importante / speciale.


Per testare la tua funzione da un programma C, puoi scrivere un wrapper che metta args nei posti giusti, salva / ripristina tutti i registri extra che blocchi e inserisce il valore di ritorno e/raxse non era già lì.


I limiti di ciò che è ragionevole: tutto ciò che non impone un onere irragionevole al chiamante:

  • ESP / RSP devono essere conservati nella chiamata; altri registri interi sono un gioco equo. (RBP e RBX sono generalmente conservati nelle normali convenzioni, ma è possibile ostruire entrambi.)
  • Qualsiasi argomento in qualsiasi registro (tranne RSP) è ragionevole, ma non lo è chiedere al chiamante di copiare lo stesso argomento in più registri.
  • Richiede DF (flag di direzione stringa per lods/ stos/ ecc.) Per essere chiaro (verso l'alto) su chiamata / ret è normale. Lasciarlo indefinito in chiamata / ret andrebbe bene. Richiedere che sia cancellato o impostato all'entrata, ma poi lasciarlo modificato al tuo ritorno sarebbe strano.

  • Restituire i valori FP in x87 st0è ragionevole, ma tornare st3con immondizia in altri registri x87 non lo è. Il chiamante dovrebbe ripulire lo stack x87. Anche tornare st0con registri dello stack superiore non vuoti sarebbe anche discutibile (a meno che non si restituiscano più valori).

  • La tua funzione verrà chiamata con call, così [rsp]come il tuo indirizzo di ritorno. È possibile evitare di call/ retsu x86 che utilizza il registro link come lea rbx, [ret_addr]/ jmp functione ritorno con jmp rbx, ma non è "ragionevole". Non è efficiente come call / ret, quindi non è qualcosa che potresti plausibilmente trovare nel codice reale.
  • Il clobbering della memoria illimitata sopra RSP non è ragionevole, ma il clobbering della propria funzione sostiene che lo stack sia consentito nelle normali convenzioni di chiamata. x64 Windows richiede 32 byte di spazio ombra sopra l'indirizzo di ritorno, mentre x86-64 System V offre una zona rossa di 128 byte al di sotto di RSP, quindi uno di questi è ragionevole. (O anche una zona rossa molto più ampia, specialmente in un programma autonomo piuttosto che in una funzione.)

Casi limite: scrivere una funzione che produce una sequenza in un array, dati i primi 2 elementi come args di funzione . Ho scelto di fare in modo che il chiamante memorizzasse l'inizio della sequenza nell'array e passasse un puntatore all'array. Questo sta sicuramente piegando i requisiti della domanda. Ho pensato di prendere le args confezionati in xmm0per movlps [rdi], xmm0, che sarebbe anche una convenzione di chiamata strano.


Restituisce un valore booleano in FLAG (codici delle condizioni)

Le chiamate di sistema di OS X fanno questo ( CF=0significa nessun errore): è considerato una cattiva pratica usare il registro dei flag come valore di ritorno booleano? .

Qualsiasi condizione che può essere verificata con un JCC è perfettamente ragionevole, specialmente se puoi sceglierne una che abbia una rilevanza semantica per il problema. (ad es. una funzione di confronto potrebbe impostare flag, quindi jneverranno prese se non fossero uguali).


Richiede che gli arg ristretti (come a char) siano firmati o zero estesi a 32 o 64 bit.

Questo non è irragionevole; l'uso movzxo movsx per evitare rallentamenti del registro parziale è normale nei moderni x86 asm. In effetti clang / LLVM crea già codice che dipende da un'estensione non documentata della convenzione di chiamata System V x86-64: args più stretti di 32 bit sono segno o zero estesi a 32 bit dal chiamante .

Puoi documentare / descrivere l'estensione a 64 bit scrivendo uint64_to int64_tnel tuo prototipo, se lo desideri. ad esempio, è possibile utilizzare loopun'istruzione, che utilizza tutti i 64 bit di RCX a meno che non si utilizzi un prefisso di dimensione dell'indirizzo per sovrascrivere la dimensione fino a ECX a 32 bit (sì, in realtà, la dimensione dell'indirizzo non è la dimensione dell'operando).

Si noti che longè solo un tipo a 32 bit nell'ABI di Windows a 64 bit e nell'ABI di Linux x32 ; uint64_tè inequivocabile e più breve da digitare di unsigned long long.


Convenzioni di chiamata esistenti:

  • Windows a 32 bit __fastcall, già suggerito da un'altra risposta : arg integer in ecxe edx.

  • x86-64 Sistema V : passa molti arg nei registri e ha molti registri con blocco delle chiamate che puoi usare senza i prefissi REX. Ancora più importante, è stato effettivamente scelto per consentire ai compilatori di inline memcpyo memset con la stessa rep movsbfacilità: i primi 6 argomenti integer / pointer vengono passati in RDI, RSI, RDX, RCX, R8, R9.

    Se la tua funzione utilizza lodsd/ stosdall'interno di un ciclo che esegue i rcxtempi (con l' loopistruzione), puoi dire "richiamabile da C come int foo(int *rdi, const int *rsi, int dummy, uint64_t len)con la convenzione di chiamata System V x86-64". esempio: chromakey .

  • GCC a 32 bit regparm: Argomenti interi in EAX , ECX, EDX, ritorno in EAX (o EDX: EAX). Avere il primo arg nello stesso registro del valore restituito consente alcune ottimizzazioni, come in questo caso con un chiamante di esempio e un prototipo con un attributo di funzione . E ovviamente AL / EAX è speciale per alcune istruzioni.

  • L'ABI Linux x32 utilizza puntatori a 32 bit in modalità lunga, quindi è possibile salvare un prefisso REX quando si modifica un puntatore ( esempio caso d'uso ). Puoi comunque utilizzare la dimensione dell'indirizzo a 64 bit, a meno che tu non abbia un intero negativo a 32 bit con estensione zero in un registro (quindi, se lo facessi, sarebbe un grande valore senza segno [rdi + rdx]).

    Si noti che push rsp/ pop raxè 2 byte ed equivale a mov rax,rsp, quindi è ancora possibile copiare registri a 64 bit completi in 2 byte.


Quando le sfide chiedono di restituire un array, pensi che il ritorno in pila sia ragionevole? Penso che sia ciò che i compilatori faranno quando restituiranno una struttura in base al valore.
qwr,

@qwr: no, le convenzioni di chiamata tradizionali passano un puntatore nascosto al valore restituito. (Alcune convenzioni passano / restituiscono piccole strutture nei registri). C / C ++ restituisce una struttura in base al valore sotto il cofano e vedi la fine di Come funzionano gli oggetti in x86 a livello di assieme? . Si noti che le matrici passanti (all'interno di strutture) le copiano nello stack per SysV x86-64: che tipo di tipo di dati C11 è un array secondo l'ABI AMD64 , ma Windows x64 passa un puntatore non const.
Peter Cordes,

quindi cosa ne pensi di ragionevole o no? Conti
qwr

1
@qwr: x86 non è un "linguaggio basato su stack". x86 è un registratore con RAM , non uno stack . Una macchina stack è come una notazione polacca inversa, come i registri x87. fld / fld / faddp. lo stack di chiamate di x86 non si adatta a quel modello: tutte le convenzioni di chiamata normali lasciano RSP non modificato, o pop con gli arg ret 16; non visualizzano l'indirizzo di ritorno, non inviano un array, quindi push rcx/ ret. Il chiamante dovrebbe conoscere la dimensione dell'array o aver salvato RSP da qualche parte fuori dallo stack per trovarsi.
Peter Cordes,

Call spinge l'indirizzo dell'istruzione dopo la chiamata nello stack jmp per la funzione chiamata; ret pop l'indirizzo dallo stack e jmp a quell'indirizzo
RosLuP

7

Utilizzare codifiche in formato breve per casi speciali per AL / AX / EAX e altri formati brevi e istruzioni a byte singolo

Gli esempi presuppongono la modalità 32/64 bit, in cui la dimensione dell'operando predefinita è 32 bit. Un prefisso delle dimensioni di un operando modifica le istruzioni in AX anziché EAX (o viceversa in modalità 16 bit).

  • inc/decun registro (diverso da 8 bit): inc eax/ dec ebp. (Non x86-64: i 0x4xbyte del codice operativo sono stati riutilizzati come prefissi REX, quindi inc r/m32è l'unica codifica.)

    8-bit inc blè 2 byte, utilizzando il inc r/m8codice operativo + MODR / M operando codifica . Quindi usa inc ebxper incrementare bl, se è sicuro. (ad esempio se non è necessario il risultato ZF nei casi in cui i byte superiori potrebbero essere diversi da zero).

  • scasd: e/rdi+=4, richiede che il registro punti alla memoria leggibile. A volte utile anche se non ti interessa il risultato FLAGS (come cmp eax,[rdi]/ rdi+=4). E in modalità 64 bit, scasbpuò funzionare come 1 byteinc rdi , se lodsb o stosb non sono utili.

  • xchg eax, r32: Questo è dove 0x90 NOP è venuto da: xchg eax,eax. Esempio: riorganizzare 3 registri con due xchgistruzioni in un cdq/ idivloop per GCD in 8 byte in cui la maggior parte delle istruzioni sono a byte singolo, incluso un abuso di inc ecx/ loopanziché test ecx,ecx/jnz

  • cdq: estendi il segno EAX in EDX: EAX, ovvero copiando il bit alto di EAX su tutti i bit di EDX. Per creare uno zero con noto non negativo o ottenere uno 0 / -1 da aggiungere / sub o maschera con. lezione di storia x86: cltqvs.movslq e anche AT&T vs. Intel mnemonics per questo e per i relativi cdqe.

  • lodsb / d : mi piace mov eax, [rsi]/ rsi += 4senza flag di clobbering. (Supponendo che DF sia chiaro, quali convenzioni di chiamata standard richiedono l'inserimento della funzione.) Anche stosb / d, a volte scas, e più raramente mov / cmps.

  • push/ pop reg. ad es. in modalità 64 bit, push rsp/ pop rdiè di 2 byte, ma mov rdi, rspnecessita di un prefisso REX ed è di 3 byte.

xlatbesiste, ma è raramente utile. Una grande tabella di ricerca è qualcosa da evitare. Inoltre non ho mai trovato un uso per le istruzioni AAA / DAA o altre istruzioni BCD confezionate o a 2 cifre ASCII.

1 byte lahf/ sahfsono raramente utili. Si potrebbe lahf / and ah, 1in alternativa a setc ah, ma non è in genere utile.

E per CF in particolare, è sbb eax,eaxnecessario ottenere uno 0 / -1, o anche non documentato ma universalmente supportato a 1 byte salc(impostare AL da Carry) che funziona efficacemente sbb al,alsenza influire sui flag. (Rimosso in x86-64). Ho usato SALC in User Appreciation Challenge # 1: Dennis ♦ .

1 byte cmc/ clc/ stc(capovolgi ("complemento"), cancella o imposta CF) sono raramente utili, anche se ho trovato un usocmc nell'aggiunta di precisione estesa con pezzi di base 10 ^ 9. Per impostare / cancellare incondizionatamente CF, di solito provvedere affinché ciò accada come parte di un'altra istruzione, ad esempio xor eax,eaxcancella CF e EAX. Non ci sono istruzioni equivalenti per altri flag di condizione, solo DF (direzione della stringa) e IF (interruzioni). La bandiera carry è speciale per molte istruzioni; i turni lo impostano, adc al, 0possono aggiungerlo ad AL in 2 byte, e ho già menzionato il SALC non documentato.

std/ cldraramente sembra valerne la pena . Soprattutto nel codice a 32 bit, è meglio utilizzare solo decun puntatore e un movoperando di origine memoria o un'istruzione ALU invece di impostare DF così lodsb/ stosbandare verso il basso anziché verso l'alto. Di solito, se hai bisogno del tutto verso il basso, hai ancora un altro puntatore che sale, quindi avresti bisogno di più di uno stde cldnell'intera funzione per usare lods/ stosper entrambi. Invece, basta usare le istruzioni di stringa per la direzione verso l'alto. (Le convenzioni di chiamata standard garantiscono DF = 0 all'ingresso della funzione, quindi puoi supporre che gratuitamente senza usare cld.)


8086 storia: perché esistono queste codifiche

In originale 8086, AX era molto speciale: istruzioni piace lodsb/ stosb, cbw, mul/ dive altri usano implicitamente. Questo è ancora il caso ovviamente; l'attuale x86 non ha eliminato nessuno dei codici operativi dell'8086 (almeno non uno di quelli ufficialmente documentati). Ma successivamente le CPU hanno aggiunto nuove istruzioni che hanno fornito modi migliori / più efficienti per fare le cose senza prima copiarle o scambiarle su AX. (O a EAX in modalità a 32 bit.)

ad esempio 8086 mancava aggiunte successive come movsx/ movzxcaricare o spostare + segno-estensione o 2 e 3-operando imul cx, bx, 1234che non producono un risultato di metà alto e non hanno operandi impliciti.

Inoltre, il principale collo di bottiglia dell'8086 era il recupero delle istruzioni, quindi l'ottimizzazione per la dimensione del codice era importante per le prestazioni di allora . Il designer ISA dell'8086 (Stephen Morse) ha speso molto spazio per la codifica di opcode in casi speciali per AX / AL, inclusi gli speciali codici di destinazione (E) AX / AL per tutte le istruzioni ALU di base immediate-src , solo opcode + immediate senza byte ModR / M. 2 byte add/sub/and/or/xor/cmp/test/... AL,imm8o AX,imm16o (in modalità 32 bit) EAX,imm32.

Ma non c'è un caso speciale per EAX,imm8, quindi la normale codifica ModR / M di add eax,4è più breve.

Il presupposto è che se lavorerai su alcuni dati, li vorrai in AX / AL, quindi scambiare un registro con AX era qualcosa che potresti voler fare, forse anche più spesso che copiare un registro in AX con mov.

Tutto ciò che riguarda la codifica delle istruzioni 8086 supporta questo paradigma, dalle istruzioni come lodsb/wa tutte le codifiche di casi speciali per gli immediati con EAX al suo uso implicito anche per moltiplicare / dividere.


Non lasciarti trasportare; non è automaticamente una vittoria scambiare tutto con EAX, specialmente se è necessario utilizzare gli immediati con i registri a 32 bit anziché a 8 bit. O se è necessario interlacciare operazioni su più variabili contemporaneamente nei registri. O se stai usando le istruzioni con 2 registri, non immediatamente.

Ma tieni sempre a mente: sto facendo qualcosa che sarebbe più breve in EAX / AL? Posso riorganizzare così ho questo in AL, o sto attualmente sfruttando meglio AL con quello per cui lo sto già usando.

Mescola liberamente le operazioni a 8 e 32 bit per trarne vantaggio ogni volta che è sicuro farlo (non è necessario eseguirlo nel registro completo o altro).


cdqè utile per divcui è necessario azzerare edxin molti casi.
qwr

1
@qwr: giusto, puoi abusare cdqprima di unsigned divse sai che il tuo dividendo è inferiore a 2 ^ 31 (ovvero non negativo se trattato come firmato), o se lo usi prima di impostare eaxun valore potenzialmente elevato. Normalmente (fuori da code-golf) che ci si usa cdqcome setup per idiv, e xor edx,edxprima didiv
Peter Cordes

5

Usa le fastcallconvenzioni

La piattaforma x86 ha molte convenzioni di chiamata . Dovresti usare quelli che passano i parametri nei registri. Su x86_64, i primi parametri vengono comunque passati nei registri, quindi nessun problema lì. Su piattaforme a 32 bit, la convenzione di chiamata predefinita ( cdecl) passa i parametri nello stack, il che non va bene per il golf - l'accesso ai parametri sullo stack richiede istruzioni lunghe.

Quando si utilizza fastcallsu piattaforme a 32 bit, in genere vengono passati 2 primi parametri ecxe edx. Se la tua funzione ha 3 parametri, potresti considerare di implementarla su una piattaforma a 64 bit.

Prototipi di funzione C per fastcallconvenzione (presi da questa risposta di esempio ):

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

Oppure usa una convenzione di chiamata completamente personalizzata , perché stai scrivendo in puro asm, non necessariamente scrivere codice da chiamare da C. Restituire booleani in FLAGS è spesso conveniente.
Peter Cordes,

5

Sottrai -128 invece di aggiungere 128

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

Samely, aggiungi -128 invece di sottrarre 128


1
Questo funziona anche nell'altra direzione, ovviamente: aggiungi -128 anziché sottotitoli 128. Curiosità: i compilatori conoscono questa ottimizzazione e fanno anche un'ottimizzazione correlata della trasformazione < 128in <= 127per ridurre la grandezza di un operando immediato per cmp, o gcc preferisce sempre riorganizzare confronta per ridurre la grandezza anche se non è -129 contro -128.
Peter Cordes,

4

Crea 3 zero con mul(quindi inc/ decper ottenere +1 / -1 e zero)

Puoi azzerare eax ed edx moltiplicando per zero in un terzo registro.

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

inc   ebx           ; 1B  ebx=1

comporterà che EAX, EDX ed EBX saranno tutti zero in soli quattro byte. È possibile azzerare EAX ed EDX in tre byte:

xor eax, eax
cdq

Ma da quel punto di partenza non è possibile ottenere un terzo registro azzerato in un altro byte o un registro +1 o -1 in altri 2 byte. Invece, usa la tecnica mul.

Esempio d'uso: concatenare i numeri di Fibonacci in binario .

Si noti che al termine di un LOOPloop, ECX sarà zero e può essere utilizzato per azzerare EDX ed EAX; non devi sempre creare il primo zero con xor.


1
Questo è un po 'confuso. Potresti espandere?
NoOneIsHere

@NoOneIsHere Credo che voglia impostare tre registri su 0, inclusi EAX ed EDX.
NieDzejkob,

4

I registri e i flag della CPU sono in stati di avvio noti

Possiamo supporre che la CPU sia in uno stato predefinito noto e documentato basato sulla piattaforma e sul sistema operativo.

Per esempio:

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

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


1
Le regole di Code Golf dicono che il tuo codice deve funzionare su almeno un'implementazione. Linux sceglie di azzerare tutti i reg (tranne RSP) e impilare prima di entrare in un nuovo processo di spazio utente, anche se i documenti ABI System V i386 e x86-64 dicono che sono "non definiti" al momento dell'accesso _start. Quindi sì, è giusto sfruttarlo se stai scrivendo un programma anziché una funzione. L'ho fatto in Extreme Fibonacci . (In un file eseguibile in modo dinamico-linked, ld.so corre prima di saltare al vostro _start, e lo fa congedo spazzatura nei registri, ma statiche è solo il codice.)
Peter Cordes

3

Per aggiungere o sottrarre 1, utilizzare un byte inco le decistruzioni che sono più piccole delle istruzioni di aggiunta e sottomissione multibyte.


Si noti che la modalità a 32 bit ha 1 byte inc/dec r32con il numero di registro codificato nel codice operativo. Quindi inc ebxè 1 byte, ma inc blè 2. Ancora più piccolo add bl, 1di ovviamente, per i registri diversi da al. Si noti inoltre che inc/ declasciare CF non modificato, ma aggiornare gli altri flag.
Peter Cordes,

1
2 per +2 e -2 in x86
l4m2

3

lea per la matematica

Questa è probabilmente una delle prime cose che si imparano su x86, ma lascio qui come promemoria. leapuò essere usato per fare moltiplicazioni per 2, 3, 4, 5, 8 o 9, e aggiungendo un offset.

Ad esempio, per calcolare ebx = 9*eax + 3in un'istruzione (in modalità 32 bit):

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

Qui è senza offset:

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

Wow! Naturalmente, leapuò essere utilizzato anche per fare matematica come ebx = edx + 8*eax + 3per il calcolo dell'indicizzazione dell'array.


1
Forse vale la pena ricordare che lea eax, [rcx + 13]è la versione senza prefissi extra per la modalità a 64 bit. Dimensione dell'operando a 32 bit (per il risultato) e dimensione dell'indirizzo a 64 bit (per gli ingressi).
Peter Cordes,

3

Le istruzioni loop e stringa sono più piccole delle sequenze di istruzioni alternative. Il più utile è loop <label>quale è più piccolo della sequenza di due istruzioni dec ECXe jnz <label>, ed lodsbè più piccolo di mov al,[esi]e inc si.


2

mov piccolo entra immediatamente nei registri inferiori, ove applicabile

Se sai già che i bit superiori di un registro sono 0, puoi usare un'istruzione più breve per spostare un immediato nei registri inferiori.

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

contro

b0 0a                   mov    $0xa,%al

Utilizzare push/ popper imm8 per zero bit superiori

Ringraziamo Peter Cordes. xor/ movè 4 byte, ma push/ popè solo 3!

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

mov al, 0xaè buono se non è necessario che sia esteso a zero al registro completo. Ma se lo fai, xor / mov è 4 byte contro 3 per push imm8 / pop o leada un'altra costante nota. Questo potrebbe essere utile in combinazione con mulazzerare 3 registri in 4 byte o cdq, se hai bisogno di molte costanti, comunque.
Peter Cordes,

L'altro caso d'uso sarebbe per le costanti da [0x80..0xFF], che non sono rappresentabili come imm8 con segno esteso. O se conosci già i byte superiori, ad esempio mov cl, 0x10dopo loopun'istruzione, perché l'unico modo per loopnon saltare è quando è stato creato rcx=0. (Immagino tu l'abbia detto , ma il tuo esempio usa un xor). Puoi anche usare il byte basso di un registro per qualcos'altro, purché qualcos'altro lo riporti a zero (o qualsiasi altra cosa) quando hai finito. ad es. il mio programma Fibonacci mantiene -1024in ebx e usa bl.
Peter Cordes,

@PeterCordes Ho aggiunto la tua tecnica push / pop
qwr

Dovrebbe probabilmente andare nella risposta esistente sulle costanti, dove l' anatolito l'ha già suggerito in un commento . Modificherò quella risposta. IMO, dovresti rielaborare questo per suggerire di usare una dimensione dell'operando a 8 bit per altre cose (tranne xchg eax, r32), ad es. mov bl, 10/ dec bl/ jnzQuindi il tuo codice non si preoccupa degli alti byte di RBX.
Peter Cordes,

@PeterCordes hmm. Non sono ancora sicuro di quando utilizzare gli operandi a 8 bit, quindi non sono sicuro di cosa inserire la risposta.
qwr

2

I FLAGS vengono impostati dopo molte istruzioni

Dopo molte istruzioni aritmetiche, la bandiera di trasporto (non firmata) e la bandiera di tracimazione (firmata) vengono impostate automaticamente ( ulteriori informazioni ). La bandiera del segno e la bandiera zero sono impostate dopo molte operazioni aritmetiche e logiche. Questo può essere usato per la ramificazione condizionale.

Esempio:

d1 f8                   sar    %eax

ZF è impostato da questa istruzione, quindi possiamo usarlo per la diramazione condizionale.


Quando hai mai usato la bandiera di parità? Sai che è lo xor orizzontale degli 8 bit bassi del risultato, giusto? (Indipendentemente dalla dimensione dell'operando, PF è impostato solo dagli 8 bit bassi ; vedere anche ). Non numero pari / numero dispari; per quel controllo ZF dopo test al,1; di solito non lo ricevi gratuitamente. (O and al,1per creare un numero intero 0/1 a seconda del pari / dispari.)
Peter Cordes,

Ad ogni modo, se questa risposta dicesse "usa flag già impostati da altre istruzioni per evitare test/ cmp", allora sarebbe un principiante abbastanza x86 per principianti, ma comunque merita un voto.
Peter Cordes,

@PeterCordes Huh, mi è sembrato di aver frainteso la bandiera della parità. Sto ancora lavorando sull'altra mia risposta. Modificherò la risposta. E come probabilmente puoi dire, sono un principiante, quindi i consigli di base aiutano.
qwr

2

Usa i cicli do-while anziché i cicli while

Questo non è specifico per x86 ma è un suggerimento per principianti ampiamente applicabile. Se sai che un ciclo while verrà eseguito almeno una volta, riscrivendolo come un ciclo do-while, con il controllo delle condizioni del ciclo alla fine, spesso viene salvata un'istruzione di salto a 2 byte. In un caso speciale potresti persino essere in grado di utilizzare loop.


2
Correlati: Perché i loop sono sempre compilati in questo modo? spiega perché do{}while()il linguaggio naturale del looping nell'assemblaggio (specialmente per efficienza). Si noti inoltre che 2 byte jecxz/ jrcxzprima di un ciclo funzionano molto bene con la loopgestione "efficiente" del caso "necessita di eseguire zero volte" (sulle rare CPU dove loopnon è lento). jecxzè anche utilizzabile all'interno del loop per implementare unwhile(ecx){} , con jmpin fondo.
Peter Cordes,

@PeterCordes è una risposta molto ben scritta. Mi piacerebbe trovare un modo per saltare nel mezzo di un ciclo in un programma di golf di codice.
qwr

Usa goto jmp e rientro ... Loop follow
RosLuP

2

Usa le convenzioni di chiamata più convenienti

System V 86 utilizza la pila e System V x86-64 usi rdi, rsi, rdx, rcx, ecc per i parametri di input, ed raxil valore di ritorno, ma è perfettamente ragionevole utilizzare il proprio convenzione di chiamata. __fastcall utilizza ecxe edxcome parametri di input e altri compilatori / sistemi operativi utilizzano le proprie convenzioni . Utilizzare lo stack e qualsiasi registro come input / output quando è conveniente.

Esempio: il contatore di byte ripetitivi , utilizzando una convenzione di chiamata intelligente per una soluzione a 1 byte.

Meta: scrivere input nei registri , scrivere output nei registri

Altre risorse: note di Agner Fog sulle convenzioni di chiamata


1
Finalmente sono riuscito a pubblicare la mia risposta su questa domanda sul trucco delle convenzioni di chiamata e su cosa sia ragionevole o irragionevole.
Peter Cordes,

@PeterCordes non correlato, qual è il modo migliore per stampare in x86? Finora ho evitato le sfide che richiedono la stampa. DOS sembra avere degli utili interrupt per l'I / O, ma sto solo pensando di scrivere risposte a 32/64 bit. L'unico modo che conosco è int 0x80che richiede un sacco di installazione.
qwr

Sì, int 0x80nel codice a 32 bit o syscallnel codice a 64 bit, invocare sys_write, è l'unico modo valido. È quello che ho usato per Extreme Fibonacci . Nel codice a 64 bit __NR_write = 1 = STDOUT_FILENO, quindi puoi farlo mov eax, edi. O se i byte superiori di EAX sono zero, mov al, 4nel codice a 32 bit. Potresti anche call printfo puts, immagino, scrivere una risposta "x86 asm per Linux + glibc". Penso che sia ragionevole non contare lo spazio di immissione PLT o GOT o il codice della libreria stesso.
Peter Cordes,

1
Sarei più propenso a fare in modo che il chiamante passi ae char*bufproduca la stringa, con formattazione manuale. ad esempio come questo (ottimamente goffamente per la velocità) come FizzBuzz , dove ho registrato i dati delle stringhe e poi li ho archiviati mov, perché le stringhe erano corte e di lunghezza fissa.
Peter Cordes,

1

Usa mosse CMOVcce set condizionaliSETcc

Questo è più un promemoria per me stesso, ma esistono istruzioni condizionali impostate ed esistono istruzioni di spostamento condizionali sui processori P6 (Pentium Pro) o più recenti. Esistono molte istruzioni basate su uno o più flag impostati in EFLAGS.


1
Ho scoperto che la ramificazione è di solito più piccola. Ci sono alcuni casi in cui si adatta in modo naturale, ma cmovha un opcode a 2 byte ( 0F 4x +ModR/M) quindi è minimo 3 byte. Ma l'origine è r / m32, quindi è possibile caricare in modo condizionale in 3 byte. Oltre alla ramificazione, setccè utile in più casi di cmovcc. Tuttavia, considera l'intero set di istruzioni, non solo le istruzioni di base 386. (Sebbene le istruzioni SSE2 e BMI / BMI2 siano così grandi che raramente sono utili. rorx eax, ecx, 32È di 6 byte, più lunghe di mov + ror. Piacevole per le prestazioni, non per il golf a meno che POPCNT o PDEP non salvi molti isn)
Peter Cordes

@PeterCordes grazie, ho aggiunto setcc.
qwr

1

Risparmia sui jmpbyte organizzando if / then anziché if / then / else

Questo è certamente molto semplice, ho pensato di pubblicarlo come qualcosa a cui pensare quando giocavo a golf. Ad esempio, considerare il seguente codice semplice per decodificare un carattere di cifra esadecimale:

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

Questo può essere abbreviato di due byte lasciando cadere un caso "then" in un caso "else":

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

Lo faresti spesso normalmente quando ottimizzi per le prestazioni, specialmente quando la sublatenza extra sul percorso critico per un caso non fa parte di una catena di dipendenze trasportata da loop (come qui dove ogni cifra di input è indipendente fino a unire blocchi di 4 bit ). Ma suppongo che +1 comunque. A proposito, il tuo esempio ha un'ottimizzazione mancante separata: se hai bisogno di un movzxalla fine comunque, usa sub $imm, %alnon EAX per sfruttare la codifica a 2 byte no-modrm di op $imm, %al.
Peter Cordes,

Inoltre, puoi eliminare il cmpfacendo sub $'A'-10, %al; jae .was_alpha; add $('A'-10)-'0'. (Penso di aver capito bene la logica). Si noti che 'A'-10 > '9'quindi non c'è ambiguità. Sottraendo la correzione per una lettera verrà inserita una cifra decimale. Quindi questo è sicuro se assumiamo che il nostro input sia esadecimale valido, proprio come il tuo.
Peter Cordes,

0

È possibile recuperare oggetti sequenziali dallo stack impostando esi su esp ed eseguendo una sequenza di lodsd / xchg reg, eax.


Perché è meglio di pop eax/ pop edx/ ...? Se è necessario lasciarli nello stack, è possibile pushripristinarli tutti dopo per ripristinare ESP, ancora 2 byte per oggetto senza necessità mov esi,esp. O intendevi per oggetti a 4 byte nel codice a 64 bit dove popavresti ottenuto 8 byte? A proposito, puoi persino usare un poploop su un buffer con prestazioni migliori rispetto lodsd, ad esempio, per un'aggiunta di precisione estesa in Extreme Fibonacci
Peter Cordes,

è più correttamente utile dopo un "lea esi, [esp + size of ret address]", che precluderebbe l'uso di pop a meno che tu non abbia un registro di riserva.
peter ferrie,

Oh, per args funzione? Abbastanza raro vuoi più argomenti di quanti ce ne siano nei registri, o che vorresti che il chiamante ne lasciasse uno in memoria invece di passarli tutti nei registri. (Ho una risposta a metà sull'uso delle convenzioni di chiamata personalizzate, nel caso in cui una delle convenzioni di chiamata di registro standard non si adatta perfettamente.)
Peter Cordes

cdecl invece di fastcall lascerà i parametri nello stack ed è facile avere molti parametri. Vedi github.com/peterferrie/tinycrypt, per esempio.
peter ferrie

0

Per codegolf e ASM: utilizzare le istruzioni utilizzare solo registri, push pop, minimizzare la memoria dei registri o la memoria immediata


0

Per copiare un registro a 64 bit, utilizzare push rcx; pop rdxinvece di un 3 byte mov.
La dimensione dell'operando predefinita di push / pop è 64-bit senza bisogno di un prefisso REX.

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

(Un prefisso della dimensione dell'operando può sovrascrivere la dimensione push / pop a 16 bit, ma la dimensione dell'operando push / pop a 32 bit non è codificabile in modalità 64 bit anche con REX.W = 0.)

Se uno o entrambi i registri sono r8... r15, usare movperché push e / o pop avranno bisogno di un prefisso REX. Nel peggiore dei casi questo in realtà perde se entrambi hanno bisogno di prefissi REX. Ovviamente dovresti evitare comunque r8..r15 nel codice golf.


Puoi mantenere la tua fonte più leggibile durante lo sviluppo con questa macro NASM . Basta ricordare che passa sugli 8 byte sotto RSP. (Nella zona rossa nel sistema V x86-64). Ma in condizioni normali è una sostituzione drop-in per 64-bit mov r64,r64omov 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

Esempi:

   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)

La xchgparte dell'esempio è perché a volte è necessario ottenere un valore in EAX o RAX e non preoccuparsi di conservare la vecchia copia. push / pop non ti aiuta in realtà a scambiare, però.

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.