Conseils pour jouer au golf en code machine x86 / x64

27

J'ai remarqué qu'il n'y a pas une telle question, alors voici:

Avez-vous des conseils généraux pour jouer au golf en code machine? Si le conseil ne s'applique qu'à un certain environnement ou à une convention d'appel, veuillez le préciser dans votre réponse.

Veuillez ne donner qu'un seul conseil par réponse (voir ici ).

ბიმო
la source

Réponses:

11

mov-immédiat est cher pour les constantes

Cela peut être évident, mais je le mettrai toujours ici. En général, il vaut la peine de penser à la représentation au niveau du bit d'un nombre lorsque vous devez initialiser une valeur.

Initialisation eaxavec 0:

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

devrait être raccourci ( pour les performances ainsi que la taille du code ) à

31 c0                   xor    %eax,%eax

Initialisation eaxavec -1:

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

peut être raccourci en

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

ou

83 c8 ff                or     $-1,%eax

Ou plus généralement, toute valeur étendue de signe 8 bits peut être créée en 3 octets avec push -12(2 octets) / pop %eax(1 octet). Cela fonctionne même pour les registres 64 bits sans préfixe REX supplémentaire; push/ poptaille d'opérande par défaut = 64.

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

Ou étant donné une constante connue dans un registre, vous pouvez créer une autre constante proche en utilisant lea 123(%eax), %ecx(3 octets). C'est pratique si vous avez besoin d'un registre mis à zéro et d' une constante; xor-zero (2 octets) + lea-disp8(3 octets).

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

Voir aussi Définir efficacement tous les bits du registre CPU sur 1

ბიმო
la source
Aussi, pour initialiser un registre avec une petite valeur (8 bits) autre que 0: utilisez par exemple push 200; pop edx- 3 octets pour l'initialisation.
anatolyg
2
BTW pour initialiser un registre à -1, utilisez dec, par exemplexor eax, eax; dec eax
anatolyg
@anatolyg: 200 est un mauvais exemple, il ne rentre pas dans un signe-extension-imm8. Mais oui, push imm8/pop reg est de 3 octets, et est fantastique pour les constantes 64 bits sur x86-64, où dec/ incest de 2 octets. Et push r64/ pop 64(2 octets) peut même remplacer un 3 octets mov r64, r64(3 octets avec REX). Voir aussi Réglez tous les bits du registre CPU à 1 efficacement pour des choses comme lea eax, [rcx-1]une valeur connue eax(par exemple, si vous avez besoin d'un registre mis à zéro et d' une autre constante, utilisez simplement LEA au lieu de push / pop
Peter Cordes
10

Dans de nombreux cas, les instructions basées sur des accumulateurs (c'est-à-dire celles qui prennent (R|E)AXcomme opérande de destination) sont 1 octet plus courtes que les instructions générales; voir cette question sur StackOverflow.

Govind Parmar
la source
Normalement, les plus utiles sont les al, imm8cas spéciaux, comme or al, 0x20/ sub al, 'a'/ cmp al, 'z'-'a'/ ja .non_alphabeticétant de 2 octets chacun, au lieu de 3. L'utilisation alpour les données de caractères permet également lodsbet / ou stosb. Ou utilisez alpour tester quelque chose sur l'octet de poids faible d'EAX, comme lodsd/ test al, 1/ setnz clfait cl = 1 ou 0 pour impair / pair. Mais dans les rares cas où vous avez besoin d'un immédiat 32 bits, alors bien sûr op eax, imm32, comme dans ma réponse chroma-key
Peter Cordes
8

Choisissez votre convention d'appel pour placer les arguments où vous le souhaitez.

Le langage de votre réponse est asm (en fait du code machine), alors traitez-le comme faisant partie d'un programme écrit en asm, pas en C-compiled-for-x86. Votre fonction ne doit pas être facilement appelable depuis C avec n'importe quelle convention d'appel standard. C'est un bon bonus si cela ne vous coûte pas d'octets supplémentaires.

Dans un programme asm pur, il est normal que certaines fonctions d'assistance utilisent une convention d'appel qui soit pratique pour elles et pour leur appelant. Ces fonctions documentent leur convention d'appel (entrées / sorties / clobbers) avec des commentaires.

Dans la vraie vie, même les programmes asm ont (je pense) tendance à utiliser des conventions d'appel cohérentes pour la plupart des fonctions (en particulier sur différents fichiers source), mais toute fonction importante donnée peut faire quelque chose de spécial. Dans le code-golf, vous optimisez la merde d'une seule fonction, donc c'est évidemment important / spécial.


Pour tester votre fonction à partir d'un programme C, vous pouvez écrire un wrapper qui place les arguments aux bons endroits, enregistre / restaure tous les registres supplémentaires que vous tapotez et place la valeur de retour dans le e/raxcas contraire.


Les limites de ce qui est raisonnable: tout ce qui n'impose pas un fardeau déraisonnable à l'appelant:

  • ESP / RSP doit être préservé des appels; les autres regs entiers sont équitables. (RBP et RBX sont généralement préservés par les appels dans les conventions normales, mais vous pouvez les assommer tous les deux.)
  • Tout argument dans n'importe quel registre (sauf RSP) est raisonnable, mais demander à l'appelant de copier le même argument dans plusieurs registres ne l'est pas.
  • DF (indicateur de direction de chaîne pour lodsstos est normal d'exiger que / / etc.) soit effacé (vers le haut) lors d'un appel / retrait. Le laisser indéfini lors d'un appel / retrait serait correct. Exiger qu'il soit effacé ou réglé à l'entrée, mais le laisser modifié à votre retour serait bizarre.

  • Le retour des valeurs FP en x87 st0est raisonnable, mais le retour enst3 avec des ordures dans un autre registre x87 ne l'est pas. L'appelant devrait nettoyer la pile x87. Même revenir st0avec des registres de pile supérieurs non vides serait également discutable (à moins que vous ne retourniez plusieurs valeurs).

  • Votre fonction sera appelée avec call, tout [rsp]comme votre adresse de retour. Vous pouvez éviter call/ retsur x86 en utilisant un registre de liens comme lea rbx, [ret_addr]/jmp function et revenir avec jmp rbx, mais ce n'est pas "raisonnable". Ce n'est pas aussi efficace que call / ret, donc ce n'est pas quelque chose que vous trouveriez vraisemblablement dans du vrai code.
  • Clobber la mémoire illimitée au-dessus de RSP n'est pas raisonnable, mais clobber vos arguments de fonction sur la pile est autorisé dans les conventions d'appel normales. Windows x64 nécessite 32 octets d'espace fantôme au-dessus de l'adresse de retour, tandis que x86-64 System V vous donne une zone rouge de 128 octets en dessous de RSP, donc l'un ou l'autre est raisonnable. (Ou même une zone rouge beaucoup plus grande, en particulier dans un programme autonome plutôt que de fonctionner.)

Cas limites: écrire une fonction qui produit une séquence dans un tableau, étant donné les 2 premiers éléments comme arguments de fonction . J'ai choisi que l'appelant stocke le début de la séquence dans le tableau et passe simplement un pointeur sur le tableau. C'est définitivement plier les exigences de la question. J'ai envisagé de prendre les arguments xmm0pourmovlps [rdi], xmm0 , qui serait également une convention d'appel bizarre.


Retourne un booléen en DRAPEAUX (codes de condition)

Les appels système OS X font cela ( CF=0signifie aucune erreur): est-il considéré comme une mauvaise pratique d'utiliser le registre des indicateurs comme valeur de retour booléenne? .

Toute condition qui peut être vérifiée avec un JCC est parfaitement raisonnable, surtout si vous pouvez en choisir une qui a une pertinence sémantique par rapport au problème. (par exemple, une fonction de comparaison peut définir des indicateurs afinjne sera donc prise si elles ne sont pas égales).


Exiger des arguments étroits (comme un char ) pour être signe ou zéro étendu à 32 ou 64 bits.

Ce n'est pas déraisonnable; utiliser movzxou movsx pour éviter les ralentissements de registres partiels est normal dans un asm x86 moderne. En fait, clang / LLVM fait déjà du code qui dépend d'une extension non documentée de la convention d'appel System V x86-64: les arguments plus étroits que 32 bits sont signe ou zéro étendu à 32 bits par l'appelant .

Vous pouvez documenter / décrire l'extension à 64 bits en écrivant uint64_touint64_t dans votre prototype si vous le souhaitez. Par exemple, vous pouvez utiliser une loopinstruction, qui utilise l'ensemble des 64 bits de RCX, sauf si vous utilisez un préfixe de taille d'adresse pour remplacer la taille jusqu'à 32 bits ECX (oui vraiment, la taille de l'adresse n'est pas la taille de l'opérande).

Notez qu'il longs'agit uniquement d'un type 32 bits dans l'ABI Windows 64 bits et l'ABI Linux x32 ; uint64_test sans ambiguïté et plus court à taper que unsigned long long.


Conventions d'appel existantes:

  • Windows 32 bits __fastcall, déjà suggéré par une autre réponse : arguments entiers dans ecxet edx.

  • x86-64 System V : transmet de nombreux arguments dans les registres et contient de nombreux registres clobés que vous pouvez utiliser sans préfixe REX. Plus important encore, il a été choisi pour permettre aux compilateurs de s'aligner memcpyou derep movsb facilement: les 6 premiers arguments entiers / pointeurs sont passés en RDI, RSI, RDX, RCX, R8, R9.

    Si votre fonction utilise lodsd/ à l' stosdintérieur d'une boucle qui s'exécute rcxfois (avec l' loopinstruction), vous pouvez dire "appelable depuis C comme int foo(int *rdi, const int *rsi, int dummy, uint64_t len)avec la convention d'appel x86-64 System V". exemple: chromakey .

  • GCC 32 bits regparm: arguments entiers dans EAX , ECX, EDX, retour dans EAX (ou EDX: EAX). Le fait d'avoir le premier argument dans le même registre que la valeur de retour permet certaines optimisations, comme dans ce cas avec un exemple d'appelant et un prototype avec un attribut de fonction . Et bien sûr, AL / EAX est spécial pour certaines instructions.

  • L'ABI Linux x32 utilise des pointeurs 32 bits en mode long, vous pouvez donc enregistrer un préfixe REX lors de la modification d'un pointeur ( exemple d'utilisation ). Vous pouvez toujours utiliser la taille d'adresse 64 bits, sauf si vous avez un entier négatif 32 bits étendu à zéro dans un registre (ce serait donc une grande valeur non signée si vous le faisiez).[rdi + rdx] ).

    Notez que push rsp/ pop raxest de 2 octets et équivaut à mov rax,rsp, de sorte que vous pouvez toujours copier des registres 64 bits complets sur 2 octets.

Peter Cordes
la source
Lorsque des défis demandent de retourner un tableau, pensez-vous que le retour sur la pile est raisonnable? Je pense que c'est ce que les compilateurs feront en retournant une structure par valeur.
qwr
@qwr: non, les conventions d'appel traditionnelles transmettent un pointeur caché à la valeur de retour. (Certaines conventions passent / renvoient de petites structures dans les registres). C / C ++ renvoyant la structure par valeur sous le capot , et voir la fin de Comment les objets fonctionnent-ils en x86 au niveau de l'assembly? . Notez que le passage de tableaux (à l'intérieur des structures) les copie sur la pile pour x86-64 SysV: quel type de type de données C11 est un tableau selon l'AMI AMD64 , mais Windows x64 transmet un pointeur non const.
Peter Cordes
alors que pensez-vous de raisonnable ou non? Comptez
qwr
1
@qwr: x86 n'est pas un "langage basé sur la pile". x86 est une machine d'enregistrement avec RAM , pas une machine de pile . Une machine de pile est comme la notation polonaise inverse, comme les registres x87. fld / fld / faddp. La pile d'appels de x86 ne correspond pas à ce modèle: toutes les conventions d'appel normales laissent RSP non modifié, ou pop les arguments avec ret 16; ils ne sautent pas l'adresse de retour, poussent un tableau, puis push rcx/ ret. L'appelant devrait connaître la taille du tableau ou avoir enregistré RSP quelque part en dehors de la pile pour se retrouver.
Peter Cordes
Appelez l'adresse de l'instruction après l'appel dans la pile jmp pour la fonction appelée; ret pop l'adresse de la pile et jmp à cette adresse
RosLuP
7

Utiliser des codages abrégés dans des cas spéciaux pour AL / AX / EAX et d'autres formes abrégées et instructions à un octet

Les exemples supposent un mode 32/64 bits, où la taille d'opérande par défaut est 32 bits. Un préfixe de taille opérande modifie l'instruction en AX au lieu de EAX (ou l'inverse en mode 16 bits).

  • inc/decun registre (autre que 8 bits): inc eax/ dec ebp. (Pas x86-64: les 0x4xoctets d'opcode ont été réutilisés en tant que préfixes REX, donc inc r/m32c'est le seul encodage.)

    8-bit inc blest de 2 octets, en utilisant le inc r/m8code d' opération + ModR / M opérande codant . Alors utilisezinc ebx pour incrémenter bl, si c'est sûr. (par exemple, si vous n'avez pas besoin du résultat ZF dans les cas où les octets supérieurs peuvent être différents de zéro).

  • scasd: e/rdi+=4, nécessite que le registre pointe vers une mémoire lisible. Parfois utile même si vous ne vous souciez pas du résultat FLAGS (comme cmp eax,[rdi]/ rdi+=4). Et en mode 64 bits, scasbpeut fonctionner comme un octetinc rdi , si lodsb ou stosb ne sont pas utiles.

  • xchg eax, r32: C'est là 0x90 NOP est provenu xchg eax,eax. Exemple: réorganiser 3 registres avec deux xchginstructions dans une boucle cdq/ pour GCD en 8 octets où la plupart des instructions sont à un octet, y compris un abus de / au lieu de /idivinc ecxlooptest ecx,ecxjnz

  • cdq: signe-étendre EAX dans EDX: EAX, c'est-à-dire copier le bit élevé d'EAX sur tous les bits d'EDX. Pour créer un zéro avec non négatif connu, ou pour obtenir un 0 / -1 à ajouter / sous ou masque avec. Leçon d'histoire x86: cltqvs.movslq , et aussi les mnémoniques AT&T vs Intel pour cela et les éléments connexes cdqe.

  • lodsb / d : comme mov eax, [rsi]/rsi += 4 sans drapeaux clobbering. (En supposant que DF est clair, quelles conventions d'appel standard exigent lors de l'entrée de fonction.) Aussi stosb / d, parfois scas, et plus rarement movs / cmps.

  • push/ pop reg. par exemple en mode 64 bits, push rsp/ pop rdiest de 2 octets, mais a mov rdi, rspbesoin d'un préfixe REX et est de 3 octets.

xlatbexiste, mais est rarement utile. Une grande table de recherche est à éviter. Je n'ai également jamais trouvé d'utilisation pour les instructions AAA / DAA ou d'autres instructions BCD ou à 2 chiffres ASCII.

1 octet lahf/ sahfsont rarement utiles. Tu pourrais lahf / and ah, 1comme alternative à setc ah, mais ce n'est généralement pas utile.

Et pour CF en particulier, il sbb eax,eaxdoit y avoir un octetsalc 0 / -1, ou même non documenté mais universellement pris en charge (définir AL à partir de Carry), ce qui fait effectivementsbb al,al sans affecter les indicateurs. (Supprimé dans x86-64). J'ai utilisé SALC dans le défi d'appréciation des utilisateurs n ° 1: Dennis ♦ .

1 octet cmc/ clc/ stc(flip ("complément"), clear ou set CF) sont rarement utiles, bien que j'aie trouvé une utilisation pour l'cmc addition en précision étendue avec des blocs de base 10 ^ 9. Pour régler / effacer inconditionnellement les FC, faites généralement en sorte que cela se fasse dans le cadre d'une autre instruction, par exemplexor eax,eax efface CF ainsi que EAX. Il n'y a pas d'instructions équivalentes pour les autres drapeaux de condition, juste DF (direction de chaîne) et IF (interruptions). Le drapeau de transport est spécial pour de nombreuses instructions; les décalages le définissent, adc al, 0peuvent l'ajouter à AL en 2 octets, et j'ai mentionné plus tôt le SALC non documenté.

std/ cldsemblent rarement en valoir la peine . Surtout dans le code 32 bits, il est préférable de simplement utiliser decsur un pointeur et un movopérande source de mémoire sur une instruction ALU au lieu de définir DF so lodsb/ stosbgo downward au lieu de up. Habituellement, si vous avez besoin de descendre, vous avez toujours un autre pointeur qui monte, vous en aurez donc besoin de plus d'un stdet clddans toute la fonction pour utiliser lods/ stospour les deux. À la place, utilisez simplement les instructions de chaîne pour la direction ascendante. (Les conventions d'appel standard garantissent DF = 0 à l'entrée de la fonction, vous pouvez donc supposer cela gratuitement sans utiliser cld.)


Historique 8086: pourquoi ces encodages existent

En 8086 d' origine, AX était très spécial: instructions aiment lodsb/ stosb, cbw, mul/ divet d' autres utilisent implicitement. C'est toujours le cas bien sûr; le x86 actuel n'a abandonné aucun des opcodes de 8086 (du moins aucun des officiellement documentés). Mais les CPU ultérieurs ont ajouté de nouvelles instructions qui ont donné des moyens meilleurs / plus efficaces de faire les choses sans les copier ou les échanger d'abord vers AX. (Ou vers EAX en mode 32 bits.)

Par exemple, 8086 manquait d'ajouts ultérieurs comme movsx/ movzxpour charger ou déplacer + extension de signe, ou 2 et 3 opérandes imul cx, bx, 1234qui ne produisent pas un résultat élevé et n'ont pas d'opérandes implicites.

En outre, le principal goulot d'étranglement du 8086 était la récupération d'instructions, il était donc important d'optimiser la taille du code pour les performances à l'époque . Le concepteur ISA de 8086 (Stephen Morse) a dépensé beaucoup d'espace de codage d'opcode sur des cas spéciaux pour AX / AL, y compris des opcodes de destination spéciaux (E) AX / AL pour toutes les instructions de base ALU immédiates-src , juste opcode + immediate sans octet ModR / M. 2 octets add/sub/and/or/xor/cmp/test/... AL,imm8ou AX,imm16ou (en mode 32 bits)EAX,imm32 .

Mais il n'y a pas de cas particulier pour EAX,imm8, donc l'encodage ModR / M normal de add eax,4est plus court.

L'hypothèse est que si vous allez travailler sur certaines données, vous en aurez besoin dans AX / AL, donc échanger un registre avec AX est quelque chose que vous voudrez peut-être faire, peut-être même plus souvent que de copier un registre vers AX avecmov .

Tout ce qui concerne le codage d'instructions 8086 prend en charge ce paradigme, des instructions comme lodsb/wà tous les codages de cas spéciaux pour les intermédiaires avec EAX à son utilisation implicite même pour la multiplication / division.


Ne vous laissez pas emporter; ce n'est pas automatiquement une victoire de tout échanger vers EAX, surtout si vous devez utiliser des intermédiaires avec des registres 32 bits au lieu de 8 bits. Ou si vous devez entrelacer des opérations sur plusieurs variables dans des registres à la fois. Ou si vous utilisez des instructions avec 2 registres, pas du tout immédiats.

Mais gardez toujours à l'esprit: est-ce que je fais quelque chose qui serait plus court dans EAX / AL? Puis-je réorganiser ce que j'ai en AL, ou suis-je actuellement en train de mieux tirer parti de AL avec ce que je l'utilise déjà.

Mélangez librement les opérations 8 bits et 32 ​​bits pour en profiter chaque fois que cela est sûr (vous n'avez pas besoin d'effectuer dans le registre complet ou autre).

Peter Cordes
la source
cdqest utile pour divlequel les besoins sont mis edxà zéro dans de nombreux cas.
qwr
1
@qwr: à droite, vous pouvez abuser cdqavant de ne pas signerdiv si vous savez que votre dividende est inférieur à 2 ^ 31 (c'est-à-dire non négatif lorsqu'il est traité comme signé), ou si vous l'utilisez avant de définir eaxune valeur potentiellement importante. Normalement (en dehors du code-golf), vous utiliseriez cdqcomme configuration pour idivet xor edx,edxavantdiv
Peter Cordes
5

Utiliser les fastcallconventions

La plate-forme x86 possède de nombreuses conventions d'appel . Vous devez utiliser ceux qui transmettent les paramètres dans les registres. Sur x86_64, les premiers paramètres sont de toute façon passés dans les registres, donc pas de problème. Sur les plates-formes 32 bits, la convention d'appel par défaut ( cdecl) transmet les paramètres dans la pile, ce qui n'est pas bon pour le golf - l'accès aux paramètres sur la pile nécessite de longues instructions.

Lors de l'utilisation fastcallsur des plates-formes 32 bits, 2 premiers paramètres sont généralement transmis dans ecxet edx. Si votre fonction a 3 paramètres, vous pourriez envisager de l'implémenter sur une plate-forme 64 bits.

Prototypes de fonctions C pour la fastcallconvention (tirés de cet exemple de réponse ):

extern int __fastcall SwapParity(int value);                 // MSVC
extern int __attribute__((fastcall)) SwapParity(int value);  // GNU   
anatolyg
la source
Ou utilisez une convention d'appel entièrement personnalisée , car vous écrivez en pure asm, pas nécessairement en écrivant du code à appeler depuis C. Le retour des booléens dans FLAGS est souvent pratique.
Peter Cordes
5

Soustrayez -128 au lieu d'ajouter 128

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

Samely, ajoutez -128 au lieu de soustraire 128

l4m2
la source
1
Cela fonctionne également dans l'autre sens, bien sûr: ajoutez -128 au lieu de sous 128. Fait amusant: les compilateurs connaissent cette optimisation et effectuent également une optimisation connexe de la transformation < 128en <= 127pour réduire l'amplitude d'un opérande immédiat pour cmp, ou gcc préfère toujours le réarrangement. compare pour réduire l'amplitude même si ce n'est pas -129 contre -128.
Peter Cordes
4

Créez 3 zéros avec mul(puis inc/ decpour obtenir +1 / -1 ainsi que zéro)

Vous pouvez mettre à zéro eax et edx en multipliant par zéro dans un troisième registre.

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

inc   ebx           ; 1B  ebx=1

aura pour résultat EAX, EDX et EBX étant tous à zéro en seulement quatre octets. Vous pouvez mettre à zéro EAX et EDX sur trois octets:

xor eax, eax
cdq

Mais à partir de ce point de départ, vous ne pouvez pas obtenir un troisième registre mis à zéro dans un octet de plus, ou un registre +1 ou -1 dans 2 autres octets. Utilisez plutôt la technique mul.

Exemple d'utilisation: concaténation des nombres de Fibonacci en binaire .

Notez qu'une fois la LOOPboucle terminée, ECX sera nul et peut être utilisé pour mettre à zéro EDX et EAX; vous n'avez pas toujours à créer le premier zéro avec xor.

peter ferrie
la source
1
C'est un peu déroutant. Pourriez-vous vous étendre?
NoOneIsHere
@NoOneIsHere Je pense qu'il veut mettre trois registres à 0, dont EAX et EDX.
NieDzejkob
4

Les registres et drapeaux du processeur sont dans des états de démarrage connus

Nous pouvons supposer que le CPU est dans un état par défaut connu et documenté basé sur la plate-forme et le système d'exploitation.

Par exemple:

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

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

640 Ko
la source
1
Les règles de Code Golf stipulent que votre code doit fonctionner sur au moins une implémentation. Linux choisit de mettre à zéro tous les regs (sauf RSP) et d'empiler avant d'entrer dans un nouveau processus d'espace utilisateur, même si les documents ABI i386 et x86-64 System V disent qu'ils ne sont pas définis à l'entrée _start. Alors oui, il est juste d'en profiter si vous écrivez un programme au lieu d'une fonction. Je l'ai fait dans Extreme Fibonacci . (Dans un exécutable lié dynamiquement, ld.so runs avant de sauter à votre _start, et fait ordures congé dans les registres, mais statique est juste votre code.)
Peter Cordes
3

Pour ajouter ou soustraire 1, utilisez le incou les decinstructions d' un octet qui sont plus petites que les instructions d'ajout et de sous-octets multi-octets.

user230118
la source
Notez que le mode 32 bits a 1 octet inc/dec r32avec le numéro de registre encodé dans l'opcode. inc ebxEst donc 1 octet, mais inc blest 2. Encore plus petit que add bl, 1bien sûr, pour les registres autres que al. Notez également que inc/ declaissez CF non modifié, mais mettez à jour les autres indicateurs.
Peter Cordes
1
2 pour +2 et -2 en x86
l4m2
3

lea pour les mathématiques

C'est probablement l'une des premières choses que l'on apprend sur x86, mais je le laisse ici pour rappel. leapeut être utilisé pour effectuer une multiplication par 2, 3, 4, 5, 8 ou 9 et ajouter un décalage.

Par exemple, pour calculer ebx = 9*eax + 3en une seule instruction (en mode 32 bits):

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

Ici, c'est sans décalage:

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

Hou la la! Bien sûr, leapeut également être utilisé pour faire des calculs comme ebx = edx + 8*eax + 3pour calculer l'indexation de tableaux.

qwr
la source
1
Il convient peut-être de mentionner qu'il lea eax, [rcx + 13]s'agit de la version sans préfixe supplémentaire pour le mode 64 bits. Taille d'opérande 32 bits (pour le résultat) et taille d'adresse 64 bits (pour les entrées).
Peter Cordes
3

Les instructions de boucle et de chaîne sont plus petites que les séquences d'instructions alternatives. Le plus utile est celui loop <label>qui est plus petit que la séquence de deux instructions dec ECXet jnz <label>, et lodsbest plus petit que mov al,[esi]et inc si.

user230118
la source
2

mov petits intermédiaires dans les registres inférieurs, le cas échéant

Si vous savez déjà que les bits supérieurs d'un registre sont à 0, vous pouvez utiliser une instruction plus courte pour déplacer un immédiat dans les registres inférieurs.

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

contre

b0 0a                   mov    $0xa,%al

Utilisation push / poppour imm8 pour remettre à zéro les bits supérieurs

Nous remercions Peter Cordes. xor/ movest de 4 octets, mais push/ popn'est que de 3!

6a 0a                   push   $0xa
58                      pop    %eax
qwr
la source
mov al, 0xaest bon si vous n'en avez pas besoin à zéro étendu jusqu'au reg complet. Mais si vous le faites, xor / mov est de 4 octets contre 3 pour push imm8 / pop ou lead'une autre constante connue. Cela peut être utile en combinaison avec mulzéro à 3 registres sur 4 octets , ou cdqsi vous avez besoin de beaucoup de constantes.
Peter Cordes
L'autre cas d'utilisation serait pour les constantes de [0x80..0xFF], qui ne sont pas représentables comme un imm8 étendu par signe. Ou si vous connaissez déjà les octets supérieurs, par exemple mov cl, 0x10après une loopinstruction, car la seule façon loopde ne pas sauter est quand elle a fait rcx=0. (Je suppose que vous avez dit cela, mais votre exemple utilise un xor). Vous pouvez même utiliser l'octet de poids faible d'un registre pour autre chose, tant que quelque chose d'autre le remet à zéro (ou autre) lorsque vous avez terminé. par exemple mon programme Fibonacci reste -1024en ebx et utilise bl.
Peter Cordes
@PeterCordes J'ai ajouté votre technique push / pop
qwr
Devrait probablement aller dans la réponse existante sur les constantes, où anatolyg l'a déjà suggéré dans un commentaire . Je vais modifier cette réponse. IMO, vous devriez retravailler celui-ci pour suggérer d'utiliser une taille d'opérande 8 bits pour plus de choses (sauf xchg eax, r32), par exemple mov bl, 10/ dec bl/ jnzafin que votre code ne se soucie pas des octets élevés de RBX.
Peter Cordes
@PeterCordes hmm. Je ne sais toujours pas quand utiliser des opérandes 8 bits, donc je ne sais pas quoi mettre dans cette réponse.
qwr
2

Les DRAPEAUX sont fixés après de nombreuses instructions

Après de nombreuses instructions arithmétiques, le drapeau de transport (non signé) et le drapeau de débordement (signé) sont définis automatiquement ( plus d'informations ). Le drapeau de signe et le drapeau zéro sont définis après de nombreuses opérations arithmétiques et logiques. Cela peut être utilisé pour la ramification conditionnelle.

Exemple:

d1 f8                   sar    %eax

ZF est défini par cette instruction, nous pouvons donc l'utiliser pour la ramification conditionnelle.

qwr
la source
Quand avez-vous déjà utilisé le drapeau de parité? Vous savez, c'est le xor horizontal des 8 bits bas du résultat, non? (Quelle que soit la taille de l'opérande, PF est défini uniquement à partir des 8 bits bas ; voir également ). Pas de nombre pair / impair; pour ce chèque ZF après test al,1; vous ne recevez généralement pas cela gratuitement. (Ou and al,1pour créer un entier 0/1 selon impair / pair.)
Peter Cordes
Quoi qu'il en soit, si cette réponse disait "utilisez des drapeaux déjà définis par d'autres instructions pour éviter test/ cmp", alors ce serait un débutant x86 assez basique, mais cela vaut quand même un vote positif.
Peter Cordes
@PeterCordes Huh, il me semblait avoir mal compris le drapeau de parité. Je travaille toujours sur mon autre réponse. Je vais modifier la réponse. Et comme vous pouvez probablement le constater, je suis un débutant, donc les conseils de base sont utiles.
qwr
2

Utilisez des boucles do-while au lieu de boucles while

Ce n'est pas spécifique à x86 mais c'est une astuce d'assemblage pour débutants largement applicable. Si vous savez qu'une boucle while s'exécutera au moins une fois, la réécriture en boucle do-while, avec vérification de l'état de la boucle à la fin, enregistre souvent une instruction de saut de 2 octets. Dans un cas particulier, vous pourriez même être en mesure d'utiliser loop.

qwr
la source
2
En relation: Pourquoi les boucles sont-elles toujours compilées comme ceci? explique pourquoi do{}while()l'idiome de bouclage naturel est dans l'assemblage (en particulier pour l'efficacité). Notez également que 2 octets jecxz/ jrcxzavant une boucle fonctionne très bien avec looppour gérer le cas "doit s'exécuter zéro fois" de manière "efficace" (sur les CPU rares où il loopn'est pas lent). jecxzest également utilisable à l' intérieur de la boucle pour implémenter unwhile(ecx){} , avecjmpen bas.
Peter Cordes
@PeterCordes qui est une réponse très bien écrite. Je voudrais trouver une utilisation pour sauter au milieu d'une boucle dans un programme de golf à code.
qwr
Utilisez goto jmp et indentation ... Loop follow
RosLuP
2

Utilisez les conventions d'appel qui vous conviennent

System V x86 utilise la pile et le système V x86-64 utilisations rdi, rsi, rdx, rcx, etc. pour les paramètres d'entrée, et raxque la valeur de retour, mais il est tout à fait raisonnable d'utiliser votre propre convention d'appel. __fastcall utilise ecxet edxcomme paramètres d'entrée, et d' autres compilateurs / OS utilisent leurs propres conventions . Utilisez la pile et tous les registres comme entrée / sortie lorsque cela vous convient.

Exemple: le compteur d'octets répétitifs , utilisant une convention d'appel intelligente pour une solution à 1 octet.

Méta: écriture d'entrée dans les registres , écriture de sortie dans les registres

Autres ressources: notes d'Agner Fog sur les conventions d'appel

qwr
la source
1
J'ai finalement réussi à publier ma propre réponse sur cette question concernant la création de conventions d'appel, et ce qui est raisonnable vs déraisonnable.
Peter Cordes
@PeterCordes sans rapport, quelle est la meilleure façon d'imprimer en x86? Jusqu'à présent, j'ai évité les défis qui nécessitent l'impression. DOS semble avoir des interruptions utiles pour les E / S, mais je ne prévois d'écrire que des réponses 32/64 bits. La seule façon que je connaisse est celle int 0x80qui nécessite un tas de configuration.
qwr
Ouais, int 0x80en code 32 bits, ou syscallen code 64 bits, pour invoquer sys_write, est le seul bon moyen. C'est ce que j'ai utilisé pour Extreme Fibonacci . En code 64 bits __NR_write = 1 = STDOUT_FILENO, vous pouvez donc mov eax, edi. Ou si les octets supérieurs de EAX sont nuls, mov al, 4en code 32 bits. Vous pourriez aussi call printfou puts, je suppose, et écrire une réponse "x86 asm pour Linux + glibc". Je pense qu'il est raisonnable de ne pas compter l'espace d'entrée PLT ou GOT, ou le code de bibliothèque lui-même.
Peter Cordes
1
Je serais plus enclin à demander à l'appelant de passer un char*bufet de produire la chaîne en cela, avec un formatage manuel. par exemple, comme ceci (maladroitement optimisé pour la vitesse) asm FizzBuzz , où j'ai obtenu des données de chaîne dans le registre, puis les ai stockées avec mov, parce que les chaînes étaient courtes et de longueur fixe.
Peter Cordes
1

Utiliser des mouvements CMOVccet des ensembles conditionnelsSETcc

C'est plus un rappel pour moi, mais des instructions de jeu conditionnel existent et des instructions de déplacement conditionnel existent sur les processeurs P6 (Pentium Pro) ou plus récents. Il existe de nombreuses instructions basées sur un ou plusieurs des indicateurs définis dans EFLAGS.

qwr
la source
1
J'ai trouvé que la ramification est généralement plus petite. Il y a des cas où c'est un ajustement naturel, mais qui cmova un opcode (2 octets 0F 4x +ModR/M), c'est donc 3 octets minimum. Mais la source est r / m32, vous pouvez donc charger conditionnellement en 3 octets. Autre que ramification, setccest utile dans plus de cas que cmovcc. Considérez tout de même l'ensemble des instructions, pas seulement les instructions de base 386. (Bien que les instructions SSE2 et BMI / BMI2 soient si grandes qu'elles soient rarement utiles. rorx eax, ecx, 32Est de 6 octets, plus long que mov + ror. Agréable pour les performances, pas pour le golf, sauf si POPCNT ou PDEP enregistre de nombreux isns)
Peter Cordes
@PeterCordes merci, j'ai ajouté setcc.
qwr
1

Économiser sur jmp octets en organisant dans if / then plutôt que if / then / else

C'est certainement très basique, je pensais simplement que je publierais cela comme quelque chose à penser lors du golf. Par exemple, considérez le code simple suivant pour décoder un caractère de chiffre hexadécimal:

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

Cela peut être raccourci de deux octets en laissant un cas "alors" tomber dans un cas "sinon":

    cmp $'A', %al
    jb .digit
    sub $('A'-'0'-10), %eax
.digit:
    sub $'0', %eax
    movzbl %al, %eax
    ...
Daniel Schepler
la source
Vous le feriez souvent normalement lors de l'optimisation des performances, en particulier lorsque la sublatence supplémentaire sur le chemin critique pour un cas ne fait pas partie d'une chaîne de dépendances en boucle (comme ici où chaque chiffre d'entrée est indépendant jusqu'à la fusion de blocs 4 bits ). Mais je suppose que +1 de toute façon. BTW, votre exemple a une optimisation manquée distincte: si vous avez besoin d'un movzxfin à la fin, alors n'utilisez sub $imm, %alpas EAX pour profiter de l'encodage sans modrm à 2 octets de op $imm, %al.
Peter Cordes
En outre, vous pouvez éliminer le cmpen faisant sub $'A'-10, %al; jae .was_alpha; add $('A'-10)-'0'. (Je pense que j'ai bien compris la logique). Notez qu'il 'A'-10 > '9'n'y a donc aucune ambiguïté. La soustraction de la correction d'une lettre encapsulera un chiffre décimal. Donc, c'est sûr si nous supposons que notre entrée est un hex valide, tout comme le vôtre.
Peter Cordes
0

Vous pouvez extraire des objets séquentiels de la pile en définissant esi sur esp et en exécutant une séquence de lodsd / xchg reg, eax.

peter ferrie
la source
Pourquoi est-ce mieux que pop eax/ pop edx/ ...? Si vous devez les laisser sur la pile, vous pouvez pushtous les récupérer après pour restaurer ESP, toujours 2 octets par objet sans avoir besoin de le faire mov esi,esp. Ou vouliez-vous dire pour les objets de 4 octets en code 64 bits où popobtiendrait 8 octets? BTW, vous pouvez même utiliser poppour boucler sur un tampon avec de meilleures performances que lodsd, par exemple, pour un ajout de précision étendue dans Extreme Fibonacci
Peter Cordes
il est plus correctement utile après un "lea esi, [esp + taille de l'adresse ret]", ce qui empêcherait d'utiliser pop sauf si vous avez un registre de rechange.
peter ferrie
Oh, pour la fonction args? Il est assez rare que vous souhaitiez plus d'arguments qu'il n'y ait de registres, ou que vous souhaitiez que l'appelant en laisse un en mémoire au lieu de les passer tous dans des registres. (J'ai une réponse à moitié terminée sur l'utilisation des conventions d'appel personnalisées, au cas où l'une des conventions standard d'enregistrement d'appel ne correspondrait pas parfaitement.)
Peter Cordes
cdecl au lieu de fastcall laissera les paramètres sur la pile, et il est facile d'avoir beaucoup de paramètres. Voir github.com/peterferrie/tinycrypt, par exemple.
peter ferrie
0

Pour codegolf et ASM: utilisez les instructions, utilisez uniquement des registres, appuyez sur pop, minimisez la mémoire de registre

RosLuP
la source
0

Pour copier un registre 64 bits, utilisez push rcx;pop rdxau lieu d'un octet mov.
La taille d'opérande par défaut de push / pop est 64 bits sans avoir besoin d'un préfixe REX.

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

(Un préfixe de taille d'opérande peut remplacer la taille push / pop par 16 bits, mais la taille d'opérande push / pop 32 bits n'est pas encodable en mode 64 bits même avec REX.W = 0.)

Si l'un ou les deux registres sont r8 .. r15, utilisez-les movcar push et / ou pop auront besoin d'un préfixe REX. Dans le pire des cas, cela perd en fait si les deux ont besoin de préfixes REX. Évidemment, vous devriez généralement éviter r8..r15 de toute façon dans le golf de code.


Vous pouvez garder votre source plus lisible tout en développant avec cette macro NASM . N'oubliez pas qu'il marche sur les 8 octets en dessous de RSP. (Dans la zone rouge dans x86-64 System V). Mais dans des conditions normales, c'est un remplacement direct pour 64 bits mov r64,r64oumov 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

Exemples:

   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 xchgpartie de l'exemple est parce que parfois vous devez obtenir une valeur dans EAX ou RAX et ne vous souciez pas de conserver l'ancienne copie. push / pop ne vous aide pas vraiment à échanger.

Peter Cordes
la source