Crie o castor mais ocupado no código de máquina x86 em 32 bytes ou menos

8

Sua tarefa é escrever um programa em linguagem de máquina x86 (qualquer versão que você queira) que execute todas as instruções possíveis e depois pare, usando no máximo 32 bytes de código e iniciando com registros zerados.

Você pode assumir qualquer coisa sobre a memória de acesso aleatório que desejar, desde que esteja em um formato utilizável para a máquina x86.

Joe Z.
fonte
3
Existem máquinas Turing de castores muito pequenas e ocupadas, cujo comportamento de parada é desconhecido ( en.wikipedia.org/wiki/Busy_beaver#Known_values ). O que acontece quando alguém implementa um deles?
asmeurer
Existem alguns detalhes que podem ser preenchidos aqui. Temos uma pilha configurada para nós? Quão grande é isso? O programa está livre para acessar a memória? Se sim, quanto disso? Temos acesso de leitura e gravação a todos os 4 GB? Em caso afirmativo, podemos escolher onde nosso programa está localizado?
breadbox
Você pode assumir qualquer coisa sobre a memória que deseja, eu acho, desde que os registros reais sejam zerados no início da execução do código. (Isso causará problemas? Eu não sou muito experiente com linguagem de máquina.) #
318 Joe Z.
Podemos assumir que o processador está no modo real e / ou protegido? Porque, se pudermos assumir o modo protegido, isso abre várias outras perguntas sobre a Tabela Global de Descritores e a tabela de paginação, etc. etc. Dadas essas questões, talvez seja melhor aplicar apenas o modo real, o que significa muito menos memória disponível . A menos que possamos assumir o modo irreal, que eu percebo agora que estava implicitamente assumindo que estava. Mas, de qualquer forma, isso provavelmente deveria ser explicitamente mencionado na descrição do problema.
breadbox

Respostas:

8

2 524224 instruções

Meu programa:

_start:
        mov     bl, _end
        stc
iloop:  adc     [bx], al
        inc     bx
        jnz     iloop
        jnc     _start
        a32
        loop    _start
        hlt
_end:

(Nota técnica: isso é escrito para nasm. a32É a sintaxe do nasm para o byte de prefixo de tamanho de endereço alternativo. Para masm, você substitui a32por defb 0x67.)

Para maior clareza, aqui está o resultado da listagem:

 1                  _start:
 2 0000 B310                mov     bl, _end
 3 0002 F9                  stc
 4 0003 1007        iloop:  adc     [bx], al
 5 0005 43                  inc     bx
 6 0006 75F9                jnz     iloop
 7 0008 73F4                jnc     _start
 8 000A 67                  a32
 9 000B E2F1                loop    _start
10 000D F4                  hlt
11                  _end:

O programa assume que o processador está no modo real e que o programa está localizado na parte inferior de um segmento de 64k de memória que, de outra forma, seria inicializado com zero de todos os bits. Seu design é simples: trate a memória como um único inteiro gigante não assinado e a incremente através de todos os valores possíveis, até reverter para todos os zeros. Repita isso 2 32 vezes. Então pare.

O loop mais interno (linhas 4-6) é responsável por incrementar o número inteiro enorme. Cada iteração adiciona zero ou um a um único byte, dependendo da execução do byte anterior. Observe que cada byte no número inteiro enorme é acessado, independentemente de ter sido alterado ou não, portanto esse loop sempre itera 2 16 a 14 vezes.

A propósito, no caso de você estar se perguntando, esse código ilustra o motivo pelo qual as instruções x86 inc/ decnão afetam o sinalizador de transporte: apenas para simplificar esse tipo de padrão de transporte de vários bytes. (Esse padrão surgiu com mais frequência nos dias de microprocessadores de 8 bits, quando o conjunto de instruções 8080 original foi definido.)

A linha 7 faz com que o processo de incremento se repita até que um seja executado no último byte, indicando que o número inteiro enorme foi redefinido para todos os bits-zero. Isso leva muito tempo.

As linhas 8‒9 marcam o loop mais externo e fazem com que esse processo se repita 2 32 vezes, até o ecxregistro rolar para zero. Isso é efetivamente equivalente a adicionar outros 32 bits ao número inteiro gigante.

Seria possível adicionar outro loop externo e fazer isso novamente usando (digamos) o edxregistrador e, talvez, usando esie edipara ainda mais repetições. No entanto, não vale a pena fazer. As instruções para incrementar e fazer loop exigem quatro bytes. Esses quatro bytes são retirados do número inteiro gigante. Portanto, perdemos 32 bits no contador de RAM, apenas para adicionar 32 bits através de um registro: acaba sendo uma lavagem. O único motivo ecxé uma exceção: ele possui uma loopinstrução especializada que se encaixa em apenas três bytes. Assim, o programa troca 24 bits por 32, um ganho pequeno, mas ainda positivo, de 8 bits.

Não é muito difícil calcular diretamente o número de instruções que o programa executa antes de parar. No entanto, há uma maneira muito mais simples de estimar esse número. Os modifica programa de toda a sua memória, menos os 14 bytes que contêm o programa, e os bxe ecxregistros. Isso adiciona até 2 16 - 14 + 2 + 4 = 65528 bytes, para um total de 524224 bits. O atalho envolve perceber que, no processo de execução, todos os padrões possíveis de 524224 bits aparecem exatamente uma vez. Para a RAM e o ecxregistro, é fácil ver isso, pois o programa aumenta em cada valor. Parabxisso é um pouco menos óbvio, pois está sendo alterado ao mesmo tempo em que o valor na memória está sendo atualizado. No entanto, pode-se mostrar que, dada a estrutura do programa, se um padrão de bits completo realmente aparecer duas vezes, o programa deverá estar em um loop infinito. Como esse não é o caso, cada padrão de bits deve ser visitado apenas uma vez. (A prova completa é deixada como exercício para o leitor, naturalmente.)

Como todo padrão de bits possível aparece no decorrer do programa, o programa deve executar pelo menos 2 instruções 524224 , o que é aproximadamente igual a 1,4 × 10 157807 . (O número acutal é muito ligeiramente maior, por causa das instruções de salto, mas a diferença nessa magnitude é insignificante.)

Obviamente, isso poderia ser significativamente aprimorado usando mais de 64k de RAM. Estou adiando a próxima versão do código até descobrir exatamente quanta RAM pode ser acessada.

caixa de pão
fonte
Não são 17 instruções (34 bytes)? Ou eu estou esquecendo de alguma coisa?
Joe Z.
@JoeZ. inc é 1 byte (se você não o montar no modo de 16 bits)
copie
Oh. Então esse programa teria na verdade 25 bytes de comprimento?
Joe Z.
Sim, são 25 bytes
copie
Desculpe, eu deveria ter sido mais explícito. Eu adicionei a saída da lista para evitar ambiguidade.
breadbox
4

~ 2 ^ (2 ^ 67) instruções

(na sintaxe t)

start:
    movq    $0x1a,%rax
    stc
loop:
    adcb    %cl,(%rax)
    incq    %rax
    jnz     loop
    jnc     start
    hlt

Desmontado, 26 bytes:

start:
0000000000000000    movq    $0x0000001a,%rax
0000000000000007    stc
loop:
0000000000000008    adcb    %cl,(%rax)
000000000000000a    incq    %rax
000000000000000d    jne loop
0000000000000013    jae start
0000000000000019    hlt

Semelhante à solução da breadbox, mas não há razão para parar com 16 bits de endereço. Meu código usa x86-64 e assume que temos 2 ^ 64 bytes de memória e usamos quase toda a memória que contém o código como um contador gigante.

Keith Randall
fonte