Dicas para jogar golfe em código de máquina x86 / x64

27

Notei que não existe essa pergunta, então aqui está:

Você tem dicas gerais para jogar golfe em código de máquina? Se a dica se aplicar apenas a um determinado ambiente ou convenção de chamada, especifique isso na sua resposta.

Por favor, apenas uma dica por resposta (veja aqui ).

ბიმო
fonte

Respostas:

11

mov-imediato é caro para constantes

Isso pode ser óbvio, mas ainda vou colocá-lo aqui. Em geral, vale a pena pensar na representação no nível de bit de um número quando você precisa inicializar um valor.

Inicializando eaxcom 0:

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

deve ser reduzido ( para desempenho e tamanho do código ) para

31 c0                   xor    %eax,%eax

Inicializando eaxcom -1:

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

pode ser reduzido para

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

ou

83 c8 ff                or     $-1,%eax

Ou, geralmente, qualquer valor estendido de sinal de 8 bits pode ser criado em 3 bytes com push -12(2 bytes) / pop %eax(1 byte). Isso funciona mesmo para registros de 64 bits sem prefixo REX extra; push/ poptamanho padrão do operando = 64.

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

Ou, dada uma constante conhecida em um registro, você pode criar outra constante próxima usando lea 123(%eax), %ecx(3 bytes). Isso é útil se você precisar de um registro zerado e uma constante; xor-zero (2 bytes) + lea-disp8(3 bytes).

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

Consulte também Definir todos os bits no registro da CPU como 1 de forma eficiente

ბიმო
fonte
Além disso, para inicializar um registro com um valor pequeno (8 bits) diferente de 0: use, por exemplo, push 200; pop edx- 3 bytes para inicialização.
Anatolyg
2
BTW para inicializar um registro para -1, use dec, por exemploxor eax, eax; dec eax
anatolyg
@anatolyg: 200 é um exemplo ruim, não se encaixa em um sinal-estendido-imm8. Mas sim, push imm8/ pop regé de 3 bytes e é fantástico para constantes de 64 bits em x86-64, onde dec/ incé de 2 bytes. E push r64/ pop 64(2 bytes) pode até substituir 3 bytes mov r64, r64(3 bytes por REX). Ver também Definir todos os bits no registrador CPU de 1 de forma eficiente para coisas como lea eax, [rcx-1]dado um valor conhecido no eax(por exemplo, se for necessário um registo zerada e outra constante, basta usar LEA em vez de push / pop
Peter Cordes
10

Em muitos casos, as instruções baseadas no acumulador (ou seja, aquelas que tomam (R|E)AXcomo operando de destino) são 1 byte mais curto que as instruções de caso geral; veja esta pergunta no StackOverflow.

Govind Parmar
fonte
Normalmente, os mais úteis são os al, imm8casos especiais, como or al, 0x20/ sub al, 'a'/ cmp al, 'z'-'a'/ ja .non_alphabeticsendo 2 bytes cada, em vez de 3. O uso alde dados de caracteres também permite lodsbe / ou stosb. Ou use alpara testar algo sobre o byte baixo do EAX, como lodsd/ test al, 1/ setnz clmake cl = 1 ou 0 para ímpar / par. Mas, no caso raro em que você precisa de um 32-bit imediato, então tudo bem op eax, imm32, como em minha resposta chroma-key
Peter Cordes
8

Escolha sua convenção de chamada para colocar args onde desejar.

O idioma da sua resposta é asm (na verdade, código de máquina), portanto, trate-o como parte de um programa escrito em asm, não em C-compilado para x86. Sua função não precisa ser fácil de chamar de C com qualquer convenção de chamada padrão. Esse é um bônus interessante, se não lhe custar bytes extras.

Em um programa asm puro, é normal que algumas funções auxiliares usem uma convenção de chamada que seja conveniente para eles e para o responsável pela chamada. Tais funções documentam sua convenção de chamada (entradas / saídas / clobbers) com comentários.

Na vida real, mesmo os programas asm (acho) tendem a usar convenções de chamada consistentes para a maioria das funções (especialmente em arquivos de origem diferentes), mas qualquer função importante pode fazer algo especial. No code-golf, você está otimizando a porcaria de uma única função, então obviamente é importante / especial.


Para testar sua função a partir de um programa em C, é possível escrever um wrapper que coloque os argumentos no lugar certo, salve / restaure os registros extras que você bloqueia e insira o valor de retorno e/raxse já não estiver lá.


Os limites do que é razoável: qualquer coisa que não imponha um ônus irracional ao chamador:

  • O ESP / RSP deve ser preservado por chamada; outros números inteiros são um jogo justo. (RBP e RBX geralmente são preservados em convenções normais, mas você pode ignorar os dois.)
  • Qualquer argumento em qualquer registro (exceto o RSP) é razoável, mas solicitar que o chamador copie o mesmo argumento em vários registros não é.
  • É normal exigir que DF (sinalizador de direção de seqüência de caracteres para lods/ stos/ etc.) seja limpo (para cima) na chamada / ret. Deixar indefinido na chamada / ret seria bom. Exigir que ele seja limpo ou definido na entrada, mas deixá-lo modificado quando você voltar seria estranho.

  • Retornar valores de FP em x87 st0é razoável, mas retornar st3com lixo em outro registro x87 não é. O chamador teria que limpar a pilha x87. Mesmo retornando st0com registros de pilha superior não vazios também seria questionável (a menos que você retorne vários valores).

  • Sua função será chamada com call, assim [rsp]como o seu endereço de retorno. Você pode evitar call/ retno x86 usando o registro de link como lea rbx, [ret_addr]/ jmp functione retornar com jmp rbx, mas isso não é "razoável". Isso não é tão eficiente quanto chamar / reter; portanto, não é algo que você encontraria plausivelmente em código real.
  • Usar memória ilimitada acima do RSP não é razoável, mas usar argumentos de função na pilha é permitido em convenções de chamada normais. x64 O Windows requer 32 bytes de espaço de sombra acima do endereço de retorno, enquanto o x86-64 System V oferece uma zona vermelha de 128 bytes abaixo do RSP, portanto, qualquer um deles é razoável. (Ou até mesmo uma zona vermelha muito maior, especialmente em um programa independente, em vez de funcionar.)

Casos limítrofes: escreva uma função que produz uma sequência em uma matriz, considerando os 2 primeiros elementos como args de função . Eu escolhi fazer com que o chamador armazenasse o início da sequência no array e apenas passasse um ponteiro para o array. Isso definitivamente está dobrando os requisitos da pergunta. Eu considerei tomar os argumentos embalados em xmm0para movlps [rdi], xmm0, que também seria uma convenção de chamada estranho.


Retornar um booleano em FLAGS (códigos de condição)

As chamadas do sistema OS X fazem isso ( CF=0significa que não há erro): É uma prática recomendada usar o registro de sinalizadores como um valor de retorno booleano? .

Qualquer condição que possa ser verificada com um JCC é perfeitamente razoável, especialmente se você puder escolher um que tenha alguma relevância semântica para o problema. (por exemplo, uma função de comparação pode definir sinalizadores, então jneserá usada se não forem iguais).


Exija que args estreitos (como a char) sejam sinalizados ou estendidos a zero para 32 ou 64 bits.

Isso não é irracional; o uso movzxou movsx para evitar lentidão parcial no registro é normal no x86 asm moderno. De fato, clang / LLVM já cria código que depende de uma extensão não documentada da convenção de chamada do System V x86-64: args mais estreitos que 32 bits são sinal ou zero estendidos a 32 bits pelo chamador .

Você pode documentar / descrever a extensão para 64 bits escrevendo uint64_tou int64_tno seu protótipo, se desejar. por exemplo, para que você possa usar uma loopinstrução, que use os 64 bits inteiros do RCX, a menos que você use um prefixo de tamanho de endereço para substituir o tamanho de 32 bits para ECX (sim, tamanho de endereço e não operando).

Observe que longé apenas um tipo de 32 bits na ABI de 64 bits do Windows e na ABI do Linux x32 ; uint64_té inequívoco e mais curto para digitar que unsigned long long.


Convenções de chamada existentes:

  • Windows de 32 bits __fastcall, já sugerido por outra resposta : número inteiro args em ecxe edx.

  • x86-64 System V : passa muitos argumentos nos registros e possui muitos registros com excesso de chamadas que você pode usar sem prefixos REX. Mais importante, ele foi realmente escolhido para permitir que os compiladores incorporem memcpyou configurem o memset tão rep movsbfacilmente: os 6 primeiros argumentos de número inteiro / ponteiro são passados ​​em RDI, RSI, RDX, RCX, RCX, R8, R9.

    Se sua função usa lodsd/ stosddentro de um loop que executa rcxvezes (com a loopinstrução), você pode dizer "chamável a partir de C como int foo(int *rdi, const int *rsi, int dummy, uint64_t len)na convenção de chamada do System V x86-64". exemplo: chromakey .

  • GCC de 32 bits regparm: argumentos inteiros em EAX , ECX, EDX, retornam em EAX (ou EDX: EAX). Ter o primeiro argumento no mesmo registro que o valor de retorno permite algumas otimizações, como neste caso com um chamador de exemplo e um protótipo com um atributo de função . E, claro, o AL / EAX é especial para algumas instruções.

  • A ABI do Linux x32 usa ponteiros de 32 bits no modo longo, para que você possa salvar um prefixo REX ao modificar um ponteiro ( exemplo de caso de uso ). Você ainda pode usar o tamanho do endereço de 64 bits, a menos que tenha um número inteiro negativo de 32 bits estendido a zero em um registro (portanto, seria um valor grande sem sinal se você o fizesse [rdi + rdx]).

    Observe que push rsp/ pop raxé 2 bytes e equivalente a mov rax,rsp, portanto, você ainda pode copiar registros completos de 64 bits em 2 bytes.

Peter Cordes
fonte
Quando os desafios pedem para retornar uma matriz, você acha que retornar na pilha é razoável? Eu acho que é isso que os compiladores farão ao retornar uma estrutura por valor.
Qwr
@qwr: no, as convenções de chamadas convencionais transmitem um ponteiro oculto para o valor de retorno. (Algumas convenções passam / retornam pequenas estruturas nos registros). C / C ++ retornando struct por valor sob o capô e veja o final de Como os objetos funcionam em x86 no nível da montagem? . Observe que as matrizes passantes (dentro das estruturas) as copiam na pilha para x86-64 SysV: Que tipo de tipo de dado C11 é uma matriz de acordo com a ABI AMD64 , mas o Windows x64 passa um ponteiro não-const.
Peter Cordes
então o que você acha razoável ou não? Você conta x86 sob esta regra codegolf.meta.stackexchange.com/a/8507/17360
QWR
1
@qwr: x86 não é um "idioma baseado em pilha". x86 é uma máquina de registro com RAM , não uma máquina de empilhamento . Uma máquina de empilhamento é como notação de polimento reverso, como registros x87. fld / fld / faddp. A pilha de chamadas do x86 não se enquadra nesse modelo: todas as convenções de chamadas normais deixam o RSP inalterado ou alteram os argumentos ret 16; eles não exibem o endereço de retorno, pressionam uma matriz e depois push rcx/ ret. O chamador teria que saber o tamanho da matriz ou salvou o RSP em algum lugar fora da pilha para se encontrar.
Peter Cordes
A chamada pressiona o endereço da instrução após a chamada na pilha jmp para a função chamada; ret pop o endereço da pilha e jmp para esse endereço
RosLuP
7

Use codificações curtas de casos especiais para AL / AX / EAX e outras formas curtas e instruções de byte único

Os exemplos assumem o modo 32/64 bits, em que o tamanho padrão do operando é 32 bits. Um prefixo de tamanho de operando altera a instrução para AX em vez de EAX (ou o inverso no modo de 16 bits).

  • inc/decum registro (que não seja de 8 bits): inc eax/ dec ebp. (Não x86-64: os 0x4xbytes do código de operação foram redirecionados como prefixos REX, assim inc r/m32como a única codificação.)

    8 bits inc blsão 2 bytes, usando a inc r/m8codificação opcode + ModR / M operando . Então use inc ebxpara incrementar bl, se for seguro. (por exemplo, se você não precisar do resultado ZF nos casos em que os bytes superiores possam ser diferentes de zero).

  • scasd: e/rdi+=4, requer que o registro aponte para a memória legível. Às vezes, útil, mesmo que você não se importe com o resultado FLAGS (como cmp eax,[rdi]/ rdi+=4). E no modo de 64 bits, scasbpode funcionar como um byteinc rdi , se lodsb ou stosb não forem úteis.

  • xchg eax, r32: Este é o lugar onde 0x90 NOP vieram de: xchg eax,eax. Exemplo: reorganize 3 registros com duas xchginstruções em um loop cdq/ para o GCD em 8 bytes, onde a maioria das instruções é de byte único, incluindo um abuso de / em vez de /idivinc ecxlooptest ecx,ecxjnz

  • cdq: estenda o sinal EAX para o EDX: EAX, ou seja, copie o bit alto do EAX para todos os bits do EDX. Para criar um zero com conhecido não negativo ou obter um 0 / -1 para adicionar / submarcar ou mascarar. lição de história x86: cltqvs.movslq , e também AT & T vs. mnemônicos Intel para isso e os relacionados cdqe.

  • lodsb / d : como mov eax, [rsi]/ rsi += 4sem sinalizadores de clobber. (Supondo que o DF seja claro, quais convenções de chamada padrão requerem na entrada da função.) Também stosb / d, às vezes scas, e mais raramente mov / cmps.

  • push/ pop reg. por exemplo, no modo de 64 bits, push rsp/ pop rdié de 2 bytes, mas mov rdi, rspprecisa de um prefixo REX e de 3 bytes.

xlatbexiste, mas raramente é útil. Uma grande tabela de pesquisa é algo a evitar. Também nunca encontrei um uso para instruções AAA / DAA ou outras instruções de pacote BCD ou 2-ASCII.

1 byte lahf/ sahfraramente são úteis. Você pode lahf / and ah, 1como alternativa setc ah, mas normalmente não é útil.

E para o CF especificamente, é sbb eax,eaxnecessário obter um byte desalc 0 / -1 ou mesmo não documentado, mas com suporte universal (conjunto AL da Carry), o que efetivamente ocorre sbb al,alsem afetar os sinalizadores. (Removido em x86-64). Eu usei o SALC no Desafio de Apreciação do Usuário # 1: Dennis ♦ .

1 byte cmc/ clc/ stc(flip ("complemento"), clear ou set CF) raramente são úteis, embora eu tenha achado um usocmc na adição de precisão estendida com pedaços de base 10 ^ 9. Para definir / limpar incondicionalmente o CF, normalmente providencie para que isso aconteça como parte de outra instrução, por exemplo, xor eax,eaxlimpa o CF e o EAX. Não há instruções equivalentes para outros sinalizadores de condição, apenas DF (direção da corda) e IF (interrupções). A bandeira de transporte é especial para muitas instruções; turnos configurá-lo, adc al, 0pode adicioná-lo ao AL em 2 bytes, e mencionei anteriormente o SALC não documentado.

stdEu cldraramente pareço valer a pena . Especialmente no código de 32 bits, é melhor usar apenas decum ponteiro e um movoperando de origem de memória em uma instrução ALU, em vez de definir DF, então lodsb/ stosbvá para baixo em vez de para cima. Normalmente, se você precisar de um modo descendente, ainda terá outro ponteiro subindo; portanto, precisará de mais de um stde de cldtoda a função para usar lods/ stospara ambos. Em vez disso, basta usar as instruções da string para a direção ascendente. (As convenções de chamada padrão garantem DF = 0 na entrada da função, portanto, você pode assumir isso de graça, sem usar cld.)


História do 8086: por que essas codificações existem

No original 8086, AX foi muito especial: instruções gosto lodsb/ stosb, cbw, mul/ dive outros usá-lo implicitamente. Esse ainda é o caso, é claro; O x86 atual não eliminou nenhum dos opcodes do 8086 (pelo menos nenhum dos documentados oficialmente). Porém, as CPUs posteriores adicionaram novas instruções que forneceram maneiras melhores / mais eficientes de fazer as coisas sem copiá-las ou trocá-las primeiro pelo AX. (Ou para EAX no modo de 32 bits.)

por exemplo, o 8086 não possuía adições posteriores como movsx/ movzxpara carregar ou mover + extensão de sinal ou operando 2 e 3 imul cx, bx, 1234que não produzem um resultado de metade superior e não possuem operandos implícitos.

Além disso, o principal gargalo do 8086 era a busca de instruções, portanto a otimização do tamanho do código era importante para o desempenho naquela época . O designer de ISA do 8086 (Stephen Morse) gastou muito espaço de codificação de código de opcode em casos especiais para o AX / AL, incluindo opcodes especiais de destino (E) AX / AL para todas as instruções ALU-src imediatas básicas , apenas opcode + imediato sem byte ModR / M. 2 bytes add/sub/and/or/xor/cmp/test/... AL,imm8ou AX,imm16ou (no modo de 32 bits) EAX,imm32.

Mas não há um caso especial EAX,imm8, portanto, a codificação ModR / M regular add eax,4é mais curta.

A suposição é que, se você trabalhar com alguns dados, desejará no AX / AL; portanto, trocar um registro com o AX é algo que você pode querer fazer, talvez com mais frequência do que copiar um registro no AX com mov.

Tudo sobre a codificação de instruções 8086 suporta esse paradigma, desde instruções como lodsb/wtodas as codificações de casos especiais para imediatos com EAX até seu uso implícito, mesmo para multiplicar / dividir.


Não se empolgue; não é uma vitória automática trocar tudo para o EAX, especialmente se você precisar usar imediatos com registros de 32 bits em vez de 8 bits. Ou se você precisar intercalar operações em várias variáveis ​​em registros de uma só vez. Ou, se você estiver usando instruções com 2 registros, não é de todo imediato.

Mas lembre-se sempre: estou fazendo algo que seria mais curto no EAX / AL? Posso reorganizar para que eu tenha isso no AL ou atualmente estou aproveitando melhor o AL com o que já estou usando.

Misture operações de 8 e 32 bits livremente para tirar vantagem sempre que for seguro (não é necessário realizar o registro completo ou o que for).

Peter Cordes
fonte
cdqé útil para as divnecessidades zeradas edxem muitos casos.
Qd #
1
@qwr: certo, você pode abusar cdqantes de não assinar divse souber que seu dividendo está abaixo de 2 ^ 31 (ou seja, não negativo quando tratado como assinado) ou se você o usar antes de definir eaxum valor potencialmente grande. Normalmente (código-golfe fora) você usar cdqcomo configuração para idiv, e xor edx,edxantesdiv
Peter Cordes
5

Use fastcallconvenções

A plataforma x86 possui muitas convenções de chamada . Você deve usar aqueles que passam parâmetros nos registradores. No x86_64, os primeiros parâmetros são passados ​​nos registros de qualquer maneira, portanto não há problema nisso. Em plataformas de 32 bits, a convenção de chamada padrão ( cdecl) passa parâmetros na pilha, o que não é bom para jogar golfe - acessar parâmetros na pilha requer instruções longas.

Ao usar fastcallem plataformas de 32 bits, 2 primeiros parâmetros geralmente são passados ecxe edx. Se sua função tiver 3 parâmetros, considere implementá-la em uma plataforma de 64 bits.

Protótipos da função C para fastcallconvenção (extraídos desta resposta de exemplo ):

extern int __fastcall SwapParity(int value);                 // MSVC
extern int __attribute__((fastcall)) SwapParity(int value);  // GNU   
anatolyg
fonte
Ou use uma convenção de chamada totalmente personalizada , porque você está escrevendo em asm puro, não necessariamente escrevendo código a ser chamado a partir de C. Retornar booleanos no FLAGS geralmente é conveniente.
Peter Cordes
5

Subtraia -128 em vez de adicionar 128

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

Samely, adicione -128 em vez de subtrair 128

l4m2
fonte
1
Isso também funciona na outra direção, é claro: adicione -128 ao invés do sub 128. Curiosidade: os compiladores conhecem essa otimização e também fazem uma otimização relacionada de transformar < 128em <= 127para reduzir a magnitude de um operando imediato cmpou o gcc sempre prefere reorganizar se compara a reduzir a magnitude, mesmo que não seja -129 vs. -128.
Peter Cordes
4

Crie 3 zeros com mul(então inc/ decpara obter +1 / -1 e também zero)

Você pode zerar eax e edx multiplicando por zero em um terceiro registro.

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

inc   ebx           ; 1B  ebx=1

resultará em EAX, EDX e EBX sendo zero em apenas quatro bytes. Você pode zerar EAX e EDX em três bytes:

xor eax, eax
cdq

Mas a partir desse ponto inicial, você não pode obter um terceiro registro zerado em mais um byte ou um registro +1 ou -1 em outros 2 bytes. Em vez disso, use a técnica mul.

Exemplo de caso de uso: concatenando os números de Fibonacci em binário .

Observe que, após a conclusão de um LOOPloop, o ECX será zero e poderá ser usado para zerar EDX e EAX; você nem sempre precisa criar o primeiro zero com xor.

Peter Ferrie
fonte
1
Isso é um pouco confuso. Você poderia expandir?
NoOneIsHere
@NoOneIsHere Acredito que ele queira definir três registros para 0, incluindo EAX e EDX.
NieDzejkob
4

Registradores e sinalizadores de CPU estão em estados de inicialização conhecidos

Podemos assumir que a CPU está em um estado padrão conhecido e documentado com base na plataforma e no SO.

Por exemplo:

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

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

640KB
fonte
1
As regras do Code Golf dizem que seu código precisa trabalhar em pelo menos uma implementação. O Linux escolhe zerar todos os regs (exceto RSP) e empilhar antes de entrar em um novo processo de espaço para o usuário, mesmo que os documentos ABI do System V i386 e x86-64 digam que estão "indefinidos" na entrada do _start. Então, sim, é um jogo justo tirar proveito disso se você estiver escrevendo um programa em vez de uma função. Eu fiz isso em Extreme Fibonacci . (Em um executável dinamicamente vinculado, ld.so corridas antes de saltar para o seu _starte faz lixo licença em registros, mas estática é apenas o seu código.)
Peter Cordes
3

Para adicionar ou subtrair 1, use o byte incou as decinstruções menores que as instruções de adição e sub multibyte.

user230118
fonte
Observe que o modo de 32 bits possui 1 byte inc/dec r32com o número do registro codificado no código de operação. O mesmo inc ebxvale 1 byte, mas inc blé 2. Ainda menor do que é add bl, 1claro, para registros diferentes de al. Observe também que inc/ decdeixe o CF sem modificação, mas atualize os outros sinalizadores.
Peter Cordes
1
2 para +2 e -2 em x86
l4m2 31/03
3

lea para matemática

Essa é provavelmente uma das primeiras coisas que se aprende sobre o x86, mas deixo aqui como um lembrete. leapode ser usado para multiplicar por 2, 3, 4, 5, 8 ou 9 e adicionar um deslocamento.

Por exemplo, para calcular ebx = 9*eax + 3em uma instrução (no modo de 32 bits):

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

Aqui está sem um deslocamento:

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

Uau! Obviamente, também leapode ser usado para fazer contas, como ebx = edx + 8*eax + 3para calcular a indexação de array.

qwr
fonte
1
Talvez valha a pena mencionar que lea eax, [rcx + 13]é a versão sem prefixos extras para o modo de 64 bits. Tamanho de operando de 32 bits (para o resultado) e tamanho de endereço de 64 bits (para as entradas).
Peter Cordes
3

As instruções de loop e string são menores que as seqüências de instruções alternativas. O mais útil é o loop <label>que é menor que a sequência de duas instruções dec ECXe jnz <label>, e lodsbé menor que mov al,[esi]e inc si.

user230118
fonte
2

mov pequenos imediatos em registros mais baixos quando aplicável

Se você já sabe que os bits superiores de um registro são 0, pode usar uma instrução mais curta para mover um imediato para os registros inferiores.

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

versus

b0 0a                   mov    $0xa,%al

Use push/ poppara imm8 a zero bits superiores

Crédito para Peter Cordes. xor/ mové de 4 bytes, mas push/ popé de apenas 3!

6a 0a                   push   $0xa
58                      pop    %eax
qwr
fonte
mov al, 0xaé bom se você não precisar estender zero para o registro completo. Mas se o fizer, xor / mov é de 4 bytes vs. 3 para push imm8 / pop ou leade outra constante conhecida. Isso pode ser útil em combinação com mulzero 3 registros em 4 bytes ou cdq, se você precisar de muitas constantes.
Peter Cordes
O outro caso de uso seria para constantes de [0x80..0xFF], que não são representáveis ​​como um imm8 estendido por sinal8. Ou se você conhece os bytes superiores, por exemplo, mov cl, 0x10após uma loopinstrução, porque a única maneira de loopnão pular é quando ela é feita rcx=0. (Eu acho que você disse isso, mas seu exemplo usa um xor). Você pode até usar o byte baixo de um registro para outra coisa, desde que a outra coisa volte a zero (ou o que seja) quando terminar. por exemplo, meu programa Fibonacci fica -1024em ebx e usa bl.
Peter Cordes
@PeterCordes Adicionei sua técnica push / pop
qwr 29/03
Provavelmente deve entrar na resposta existente sobre constantes, onde o anatolyg já a sugeriu em um comentário . Eu vou editar essa resposta. Na IMO, você deve refazer este para sugerir o uso de tamanho de operando de 8 bits para mais coisas (exceto xchg eax, r32), por exemplo, mov bl, 10/ dec bl/ jnzpara que seu código não se importe com os altos bytes do RBX.
Peter Cordes
@PeterCordes hmm. Ainda não tenho certeza sobre quando usar operandos de 8 bits, por isso não sei o que colocar nessa resposta.
QWR
2

Os FLAGS são configurados após muitas instruções

Após muitas instruções aritméticas, o Sinalizador de transporte (não assinado) e Sinalizador de estouro (assinado) são definidos automaticamente ( mais informações ). O Sinalizador e o Sinalizador Zero são definidos após muitas operações aritméticas e lógicas. Isso pode ser usado para ramificação condicional.

Exemplo:

d1 f8                   sar    %eax

O ZF é definido por esta instrução, para que possamos usá-lo para ramificação condicional.

qwr
fonte
Quando você já usou a bandeira de paridade? Você sabe que é o xor horizontal dos baixos 8 bits do resultado, certo? (Independentemente do tamanho do operando, PF é definido apenas a partir dos 8 bits baixos ; veja também ). Não é um número par / número ímpar; para esse cheque ZF depois test al,1; você geralmente não recebe isso de graça. (Ou and al,1para criar um inteiro 0/1 dependendo par / ímpar.)
Peter Cordes
De qualquer forma, se essa resposta disser "use sinalizadores já definidos por outras instruções para evitar test/ cmp", isso seria um iniciante bastante básico x86, mas ainda vale a pena ser votado.
Peter Cordes
@ PeterCordes Huh, eu parecia ter entendido mal a bandeira de paridade. Ainda estou trabalhando na minha outra resposta. Vou editar a resposta. E, como você provavelmente pode perceber, sou iniciante, portanto, dicas básicas ajudam.
QWR
2

Use loops do-while em vez de loops while

Isso não é específico para x86, mas é uma dica de montagem para iniciantes amplamente aplicável. Se você souber que um loop while será executado pelo menos uma vez, reescrevendo o loop como um loop do while, com verificação da condição do loop no final, geralmente salva uma instrução de salto de 2 bytes. Em um caso especial, você pode até usar loop.

qwr
fonte
2
Relacionado: Por que os loops são sempre compilados assim? explica por que do{}while()o idioma natural de loop na montagem (especialmente para eficiência). Observe também que 2 bytes jecxz/jrcxz um loop de antes funciona muito bem looppara lidar com as "necessidades de executar zero vezes" case "eficientemente" (nas raras CPUs onde loopnão é lento). jecxztambém é utilizável dentro do loop para implementar awhile(ecx){} , com jmpna parte inferior.
Peter Cordes
@ PeterCordes, que é uma resposta muito bem escrita. Eu gostaria de encontrar um uso para pular no meio de um loop em um programa de código de golfe.
QWR
Use goto jmp e indentação ... Loop follow
RosLuP 12/03
2

Use as convenções de chamada que forem convenientes

System V x86 usa a pilha e System V x86-64 usos rdi, rsi, rdx, rcx, etc. para parâmetros de entrada, e raxcomo o valor de retorno, mas é perfeitamente razoável usar sua própria convenção de chamada. __fastcall usa ecxe edxcomo parâmetros de entrada, e outros compiladores / SOs usam suas próprias convenções . Use a pilha e quaisquer registros como entrada / saída, quando conveniente.

Exemplo: o contador de bytes repetitivos , usando uma convenção de chamada inteligente para uma solução de 1 byte.

Meta: Gravando entrada em registros , Gravando saída em registros

Outros recursos: notas de Agner Fog sobre convocar convenções

qwr
fonte
1
Finalmente, comecei a postar minha própria resposta sobre essa questão sobre inventar convenções de chamada e o que é razoável ou irracional.
Peter Cordes
@ PeterCordes independente, qual é a melhor maneira de imprimir em x86? Até agora, evitei desafios que exigem impressão. O DOS parece ter interrupções úteis para E / S, mas estou pensando apenas em escrever respostas de 32/64 bits. A única maneira que eu sei é int 0x80que requer um monte de configuração.
Qwr
Sim, int 0x80no código de 32 bits, ou syscallno código de 64 bits, para invocar sys_write, é a única maneira boa. É o que eu usei para o Extreme Fibonacci . No código de 64 bits __NR_write = 1 = STDOUT_FILENO, você pode mov eax, edi. Ou se os bytes superiores do EAX forem zero, mov al, 4no código de 32 bits. Você também pode , call printfou putsacho, e escrever uma resposta "x86 asm para Linux + glibc". Eu acho que é razoável não contar o espaço de entrada PLT ou GOT, ou o próprio código da biblioteca.
Peter Cordes
1
Eu estaria mais inclinado a fazer com que o chamador passasse um char*bufe produzisse a string com isso, com formatação manual. por exemplo, assim (otimizado desajeitadamente para velocidade) asm FizzBuzz , onde eu coloquei os dados das strings no registro e os armazenei mov, porque as strings eram curtas e de comprimento fixo.
Peter Cordes
1

Use movimentos CMOVcce conjuntos condicionaisSETcc

Isso é mais um lembrete para mim, mas existem instruções condicionais de conjunto e instruções de movimentação condicional nos processadores P6 (Pentium Pro) ou mais recente. Há muitas instruções baseadas em um ou mais dos sinalizadores definidos no EFLAGS.

qwr
fonte
1
Eu descobri que a ramificação geralmente é menor. Existem alguns casos em que é um ajuste natural, mas cmovpossui um código de operação de 2 bytes ( 0F 4x +ModR/M), portanto, é um mínimo de 3 bytes. Mas a fonte é r / m32, portanto, você pode carregar condicionalmente em 3 bytes. Além de ramificação, setccé útil em mais casos do que cmovcc. Ainda assim, considere todo o conjunto de instruções, não apenas as instruções da linha de base 386. (. Embora SSE2 e instrução IMC / BMI2 são tão grandes que eles são raramente útil rorx eax, ecx, 32é de 6 bytes, mais do que mov + ror agradável para o desempenho, não de golfe a menos POPCNT ou PDEP salva muitas iSNS.)
Peter Cordes
@ PeterCordes obrigado, eu adicionei setcc.
QWR
1

Economize em jmpbytes organizando se / então em vez de se / então / outra

Isso é certamente muito básico, apenas pensei em postar isso como algo para se pensar ao jogar golfe. Como exemplo, considere o seguinte código simples para decodificar um caractere de dígito hexadecimal:

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

Isso pode ser reduzido em dois bytes, deixando um caso "then" cair em um caso "else":

    cmp $'A', %al
    jb .digit
    sub $('A'-'0'-10), %eax
.digit:
    sub $'0', %eax
    movzbl %al, %eax
    ...
Daniel Schepler
fonte
Você costuma fazer isso normalmente ao otimizar o desempenho, especialmente quando a sublatência extra no caminho crítico para um caso não faz parte de uma cadeia de dependência transportada por loop (como aqui onde cada dígito de entrada é independente até mesclar blocos de 4 bits ) Mas acho que +1 de qualquer maneira. BTW, seu exemplo tem uma otimização perdida separada: se você precisar de uma movzxno final de qualquer maneira, sub $imm, %alnão use o EAX para aproveitar a codificação de 2 bytes no-modrm de op $imm, %al.
Peter Cordes
Além disso, você pode eliminar o cmpfazendo sub $'A'-10, %al; jae .was_alpha; add $('A'-10)-'0'. (Eu acho que entendi a lógica). Observe que, 'A'-10 > '9'portanto, não há ambiguidade. Subtrair a correção de uma letra quebra um dígito decimal. Portanto, isso é seguro se assumirmos que nossa entrada é hexadecimal válida, assim como a sua.
Peter Cordes
0

Você pode buscar objetos seqüenciais da pilha configurando esi para esp e executando uma sequência de lodsd / xchg reg, eax.

Peter Ferrie
fonte
Por que isso é melhor que pop eax/ pop edx/ ...? Se você precisar deixá-los na pilha, poderá pushdevolvê-los depois para restaurar o ESP, ainda com 2 bytes por objeto, sem necessidade mov esi,esp. Ou você quis dizer para objetos de 4 bytes no código de 64 bits onde popobteria 8 bytes? BTW, você ainda pode usar poppara fazer um loop sobre um tampão com melhor desempenho do que lodsd, por exemplo, para além estendida de precisão em Extrema Fibonacci
Peter Cordes
é mais corretamente útil após um "lea esi, [esp + tamanho do endereço de ret]]", que impediria o uso de pop, a menos que você tenha um registro sobressalente.
Peter Ferrie
Ah, para a função args? É muito raro você querer mais argumentos do que registradores ou que o chamador deixe um na memória em vez de passar todos eles nos registradores. (Eu tenho uma resposta semi-acabados sobre o uso de convenções de chamada personalizada, caso uma das convenções registo de chamadas padrão não se encaixa perfeitamente.)
Peter Cordes
cdecl em vez de fastcall deixará os parâmetros na pilha, e é fácil ter muitos parâmetros. Veja github.com/peterferrie/tinycrypt, por exemplo.
peter ferrie
0

Para codegolf e ASM: Use as instruções: use apenas registradores, pressione pop, minimize a memória de registro ou a memória imediata

RosLuP
fonte
0

Para copiar um registro de 64 bits, use push rcx; pop rdxem vez de 3 bytes mov.
O tamanho padrão do operando de push / pop é de 64 bits sem a necessidade de um prefixo REX.

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

(Um prefixo de tamanho de operando pode substituir o tamanho de push / pop para 16 bits, mas tamanho de operando de push / pop de 32 bits não pode ser codificado no modo de 64 bits, mesmo com REX.W = 0.)

Se um ou ambos os registradores forem r8.. r15, usemov porque push e / ou pop precisarão de um prefixo REX. Na pior das hipóteses, isso realmente perde se os dois precisarem de prefixos REX. Obviamente, você deve evitar r8..r15 de qualquer maneira no código golf.


Você pode manter sua fonte mais legível ao desenvolver com essa macro NASM . Lembre-se de que ele pisa nos 8 bytes abaixo do RSP. (Na zona vermelha no x86-64 System V). Mas, em condições normais, é um substituto para 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

Exemplos:

   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)

A xchgparte do exemplo é que, às vezes, você precisa adicionar um valor ao EAX ou RAX e não se preocupa em preservar a cópia antiga. push / pop não ajuda você realmente a trocar, no entanto.

Peter Cordes
fonte