Cree el castor más ocupado en código de máquina x86 en 32 bytes o menos

8

Su tarea es escribir un programa en lenguaje de máquina x86 (cualquier versión que desee) que ejecutará tantas instrucciones como sea posible y luego se detendrá, utilizando un máximo de 32 bytes de código y comenzando con registros cerados.

Puede asumir cualquier cosa sobre la memoria de acceso aleatorio que desee, siempre que esté en un formato utilizable para la máquina x86.

Joe Z.
fuente
3
Hay máquinas Turing de castores bastante pequeñas y ocupadas cuyo comportamiento de detención es desconocido ( en.wikipedia.org/wiki/Busy_beaver#Known_values ). ¿Qué sucede cuando alguien implementa uno de ellos?
asmeurer
Hay algunos detalles que podrían completarse aquí. ¿Tenemos una pila configurada para nosotros? ¿Como es de grande? ¿El programa es libre de acceder a la memoria? Si es así, ¿cuánto? ¿Tenemos acceso de lectura y escritura a los 4 GB? Si es así, ¿podemos elegir dónde se encuentra nuestro programa?
breadbox
Supongo que puede asumir cualquier cosa sobre la memoria que desee, siempre y cuando los registros reales se pongan a cero al comienzo de la ejecución del código. (¿Esto causará problemas? No tengo mucha experiencia con el lenguaje de máquina).
Joe Z.
¿Podemos suponer que el procesador está en modo real y / o en modo protegido? Porque si podemos asumir el modo protegido, eso abre muchas otras preguntas sobre la Tabla de descriptores globales y la tabla de paginación, etc. Dados estos problemas, podría ser mejor simplemente aplicar el modo real, lo que significa mucho menos memoria disponible . A menos que podamos asumir un modo irreal, que ahora me doy cuenta de que estaba suponiendo implícitamente que estaba en. Pero, en cualquier caso, esto probablemente debería mencionarse explícitamente en la descripción del problema.
breadbox

Respuestas:

8

2 524224 instrucciones

Mi programa:

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

(Nota técnica: esto está escrito para nasm. Esta a32es la sintaxis de nasm para el byte alternativo de prefijo de tamaño de dirección. Para masm, debe reemplazar el a32con defb 0x67.)

Para mayor claridad, aquí está el resultado de la lista:

 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:

El programa asume que el procesador está en modo real y que el programa está ubicado en la parte inferior de un segmento de memoria de 64k que, de lo contrario, se inicializa en todos los bits cero. Su diseño es simple: trate la memoria como un único entero gigante sin signo, e increméntelo a través de todos los valores posibles, hasta que vuelva a cero. Repita esto 2 32 veces. Entonces detente.

El bucle más interno (líneas 4‒6) es responsable de incrementar el número entero enorme. Cada iteración agrega cero o uno a un solo byte, dependiendo de si hubo una ejecución del byte anterior. Tenga en cuenta que se accede a cada byte en el gran número entero, ya sea que haya cambiado o no, por lo que este ciclo siempre itera 2 16 - 14 veces.

Por cierto, en caso de que te lo estés preguntando, este código ilustra la razón por la cual las instrucciones x86 inc/ decno afectan el indicador de acarreo: solo para simplificar este tipo de patrón de acarreo de varios bytes. (Este patrón surgió con mayor frecuencia en los días de los microprocesadores de 8 bits, cuando se definió el conjunto de instrucciones 8080 original).

La línea 7 hace que el proceso de incremento se repita hasta que se realiza uno desde el último byte, lo que indica que el entero enorme se ha restablecido a todos los bits cero. Esto lleva mucho tiempo.

Las líneas 8‒9 marcan el bucle más externo y hacen que este proceso se repita 2 32 veces, hasta que el ecxregistro rueda a cero. Esto es efectivamente equivalente a agregar otros 32 bits al entero gigante.

Sería posible agregar otro ciclo externo y hacer esto nuevamente usando (digamos) el edxregistro, y luego quizás usando esiy edipara aún más repeticiones. Sin embargo, no vale la pena hacerlo. Las instrucciones para incrementar y recorrer requieren cuatro bytes. Esos cuatro bytes son quitados del entero gigante. Entonces perdemos 32 bits en el contador de RAM, solo para agregar 32 bits a través de un registro: termina siendo un lavado. La única razón ecxes una excepción es que tiene una loopinstrucción especializada que se ajusta a solo tres bytes. Así, el programa intercambia 24 bits por 32, una ganancia pequeña pero aún positiva de 8 bits.

No es demasiado difícil calcular directamente la cantidad de instrucciones que ejecuta el programa antes de detenerse. Sin embargo, hay una forma mucho más simple de estimar este número. El programa modifica toda su memoria, menos los 14 bytes que contienen el programa y los registros bxy ecx. Esto suma hasta 2 16 - 14 + 2 + 4 = 65528 bytes, para un total de 524224 bits. El acceso directo implica darse cuenta de que, en el proceso de ejecución, cada patrón posible de 524224 bits aparece exactamente una vez. Para la RAM y el ecxregistro esto es fácil de ver, ya que el programa aumenta a través de cada valor individual. porbxEsto es un poco menos obvio, ya que se está cambiando al mismo tiempo que se actualiza el valor en la memoria. Sin embargo, se puede demostrar que, dada la estructura del programa, si un patrón de bits completo apareciera realmente dos veces, entonces el programa tendría que estar en un bucle infinito. Como este no es el caso, cada patrón de bits debe ser visitado una sola vez. (La prueba completa se deja como ejercicio para el lector, naturalmente).

Dado que todos los patrones de bits posibles aparecen en el curso del programa, el programa debe ejecutar al menos 2 524224 instrucciones, que es aproximadamente igual a 1.4 × 10 157807 . (El número acutal es muy levemente mayor debido a las instrucciones de salto, pero la diferencia a esta magnitud es insignificante).

Obviamente, esto podría mejorarse significativamente utilizando más de 64k de RAM. Espero la próxima versión del código hasta que averigüe exactamente cuánta RAM se puede acceder.

caja de pan
fuente
¿No son 17 instrucciones (34 bytes)? ¿O me estoy perdiendo algo?
Joe Z.
@JoeZ. inc es 1 byte (si no lo ensamblas en modo de 16 bits)
copia el
Oh. Entonces, ¿este programa realmente tendría 25 bytes de longitud?
Joe Z.
Sí, son 25 bytes
copie el
Lo siento, debería haber sido más explícito. Agregué el resultado de la lista para evitar la ambigüedad.
breadbox
4

~ 2 ^ (2 ^ 67) instrucciones

(sintaxis de at & 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

Similar a la solución de breadbox, pero no hay razón para detenerse en 16 bits de dirección. Mi código usa x86-64 y supone que tenemos 2 ^ 64 bytes de memoria y usamos todo menos la memoria que contiene el código como un contador gigante.

Keith Randall
fuente