Fort Byte Coche (y más) Nativos americanos

imagen

Sí, sí, es el "byte" y está en indio (no indio). Comenzaré en orden. Recientemente aquí, en Habré, comenzaron a aparecer artículos sobre bytecode. Y una vez, me divertí escribiendo sistemas Fort. Por supuesto, en ensamblador. Eran de 16 bits. Nunca programé en x86-64. Incluso con 32 no pudo jugar. Entonces vino la idea: ¿por qué no? ¿Por qué no agitar el fuerte de 64 bits, e incluso con el código de bytes? Sí, y en Linux, donde tampoco escribí nada del sistema.

Tengo un servidor doméstico con Linux. En general, busqué en Google un poco y descubrí que el ensamblador en Linux se llama GAS y el comando as. Me estoy conectando a través de SSH al servidor, escribiendo como - ¡sí! Ya lo tengo instalado. Todavía necesita un enlazador, escriba ld, ¡sí! Entonces, e intente escribir algo interesante en ensamblador. Sin civilización, solo un bosque, como los verdaderos indios :) Sin un entorno de desarrollo, solo una línea de comando y Midnight Commander. El editor será Nano, que cuelga de mi F4 en mc. ¿Cómo está cantando el grupo "Zero"? Un verdadero indio solo necesita una cosa ... ¿Qué más necesita un verdadero indio? Por supuesto, un depurador. Escribimos gdb - is! Bueno, presiona Shift + F4 y listo.

Arquitectura


Para empezar, decidamos sobre arquitectura. Con profundidades de bits ya determinadas, 64 bits. En las implementaciones clásicas de Fort, el segmento de datos y código es el mismo. Pero trataremos de hacerlo bien. Solo tendremos el código en el segmento de código, los datos en el segmento de datos. Como resultado, obtenemos un núcleo para la plataforma y un código de bytes completamente independiente de la plataforma.

Tratemos de hacer la máquina de bytes apilada más rápida (pero sin JIT). Entonces, tendremos una tabla que contiene 256 direcciones, una para cada comando de byte. Menos que nada: una verificación adicional, estas son 1-2 instrucciones del procesador. Y necesitamos rápidamente, sin compromiso.

Pilas


Por lo general, en las implementaciones de Fort, la pila de retorno del procesador (* SP) se usa como una pila de datos, y la pila de retorno del sistema fort se implementa utilizando otros medios. De hecho, nuestra máquina estará apilada, y el trabajo principal está en la pila de datos. Por lo tanto, hagamos lo mismo: RSP será una pila de datos. Bueno, deje que la pila de retorno sea RBP, que también, por defecto, funciona con el segmento de pila. Por lo tanto, tendremos tres segmentos de memoria: un segmento de código, un segmento de datos y un segmento de pila (tendrá una pila de datos y una pila de retorno).

Registros


Entro en la descripción de los registros x86-64, y ¡Uy! Hay hasta 8 registros adicionales de propósito general (R8 - R16), en comparación con los modos de 32 o 16 bits. No está mal ...

Ya decidí que necesitarán RSP y RBP. Todavía necesita un puntero (contador) de los comandos de bytecode. De las operaciones en este registro, solo se necesita la lectura de memoria. Los registros principales (RAX, RBX, RCX, RDX, RSI, RDI) son más flexibles, universales, con ellos hay muchos comandos especiales. Nos serán útiles para diversas tareas, y para el contador de instrucciones de bytecode tomamos uno de los nuevos registros para mí, que sea R8.

Empecemos


No tengo experiencia programando en Linux en lenguaje ensamblador. Por lo tanto, para empezar, encontraremos el "Hola, mundo" terminado para comprender cómo se inicia el programa y muestra el texto. Inesperadamente para mí, encontré opciones con una sintaxis extraña donde incluso la fuente y el receptor están reorganizados. Resultó que esta es la sintaxis de AT&T, y está escrita principalmente bajo GAS. Pero se admite otra opción de sintaxis, se llama sintaxis Intel. Pensando, decidí usarlo de todos modos. Bueno, escriba al principio de .intel_syntax noprefix.

Compile y ejecute "Hola, mundo" para asegurarse de que todo funcione. Al leer la ayuda y los experimentos, comencé a usar el siguiente comando para compilar:
$ as fort.asm -o fort.o -g -ahlsm >list.txt
Aquí, el modificador -o indica el archivo de resultados, el modificador -g indica que genere información de depuración y el modificador -ahlsm establece el formato de listado. Y mantengo la salida en la lista, en ella puedes ver muchas cosas útiles. Admito que, al comienzo del trabajo, no hice el listado y ni siquiera especifiqué el modificador -g. Comencé a usar el modificador -g después del primer uso del depurador y comencé a hacer la lista después de que aparecieran las macros en el código :)

Después de eso, usamos el enlazador, pero aquí no hay nada más simple:

$ ld forth.o -o forth
Bueno, corre!
$ ./forth
Hello, world!

Funciona

Esta fue la primera cuarta vez. Asm (en realidad es 'Hellow, world!', Por supuesto)
 .intel_syntax noprefix .section .data msg: .ascii "Hello, world!\n" len = . - msg #  len    .section .text .global _start #     _start: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, OFFSET FLAT:msg #     mov edx, len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit xor ebx, ebx #    0 int 0x80 #   

Por cierto, más tarde descubrí que en x86-64 es más correcto usar syscall para una llamada al sistema, en lugar de int 0x80. La llamada 0x80 se considera obsoleta para esta arquitectura, aunque es compatible.

Se ha comenzado y ahora ...

Vamos!


Para que haya al menos algunos detalles, escribiremos el código de un comando de byte. Deje que sea la palabra Fort "0", poniendo 0 en la parte superior de la pila:

 bcmd_num0: push 0 jmp _next 

En el momento en que se ejecuta este comando, R8 ya apunta al siguiente comando de byte. Es necesario leerlo, aumentar R8, determinar la dirección ejecutable por el código del comando byte y transferirle el control.

Pero ... ¿qué profundidad de bits será la tabla de direcciones de byte-command? Luego tuve que profundizar en el nuevo sistema de comando x86-64 para mí. Por desgracia, no encontré comandos que le permitan ir al desplazamiento en la memoria. Por lo tanto, calcule la dirección o la dirección estará lista: 64 bits. No hay tiempo para que calculemos, lo que significa - 64 bits. En este caso, el tamaño de la tabla será 256 * 8 = 4096 bytes. Bueno, finalmente, codifique la llamada _next:

 _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] # bcmd -   - 

No está mal, me parece ... Solo hay tres instrucciones de procesador, cuando se cambia de un comando de byte a otro.

De hecho, estos comandos no fueron tan fáciles para mí. Tuve que profundizar en el sistema de comando 0x86-64 nuevamente y encontrar un nuevo comando MOVZX para mí. De hecho, este comando convierte un valor de 8, 16 o 32 bits en un registro de 64 bits. Hay dos variantes de este comando: sin signo, donde los dígitos más altos se rellenan con ceros, y el firmado es MOVSX. En la versión firmada, el signo se expande, es decir, para los números positivos, los ceros irán a los dígitos más altos, y para los negativos, los números. Esta opción también nos es útil para el comando de byte iluminado.

Por cierto, ¿es esta opción la más rápida? ¿Quizás alguien sugiera aún más rápido?

Bueno, ahora tenemos una máquina de bytes que puede ejecutar una secuencia de comandos de bytes y ejecutarlos. Es necesario probarlo en la práctica, para forzar la ejecución de al menos un equipo. Pero cual? Cero en la pila? Pero aquí ni siquiera sabe el resultado, si no mira la pila debajo del depurador ... Pero si el programa comenzó, se puede completar :)

Escribimos un comando bye que completa el programa y escribe sobre él, especialmente porque tenemos "Hellow, world!".

 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   

Lo único que queda es crear una tabla de direcciones de comando de bytes, inicializar los registros e iniciar la máquina de bytes. Entonces ... hay 256 valores en la tabla, y hay dos comandos. ¿Qué hay en las otras celdas?
El resto tendrá un código de operación no válido. Pero no puede verificarlo, estos son equipos adicionales, tenemos tres ahora y con el cheque serán cinco. Por lo tanto, haremos un comando tan difícil: un mal equipo. Primero, completamos toda la tabla y luego comenzamos a ocupar las celdas con comandos útiles. Deje que el equipo malo tenga el código 0x00, el equipo adiós - 0x01, y el '0' tendrá el código 0x02, una vez que ya esté escrito. El equipo malo por ahora hará lo mismo que adiós, solo que con un código de finalización y texto diferentes (lo pondré en el spoiler, casi lo mismo que adiós):

bcmd_bad
 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   
Ahora dibuja una tabla de direcciones. Para mayor comodidad, colocaremos ocho en cada fila, habrá 16 filas. La tabla es bastante grande:

Tabla de direcciones de comando de bytes
 bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad 
Escribimos el cuerpo del programa de bytes. Para hacer esto, asigne códigos de comando a las variables de ensamblador. Tendremos los siguientes acuerdos:

  • Las direcciones para ejecutar comandos de bytes comenzarán en bcmd_
  • Los códigos de comando se almacenarán en variables que comienzan con b_

Por lo tanto, el cuerpo del programa de bytes será así:

 start: .byte b_bye 

Declare el tamaño de la pila de datos como stack_size. Que sea hasta ahora 1024. En la inicialización, haremos RBP = RSP - stack_size.

En realidad, obtenemos un código de programa de este tipo (forth.asm)
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_bye .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_num0 = 0x02 bcmd_num0: push 0 jmp _next 


Compilar, ejecutar:

$ as fort.asm -o fort.o -g -ahlsm >list.txt
$ ld forth.o -o forth
$ ./forth
bye!

Funciona! Nuestro primer programa de bytecode de un byte fue lanzado :)
Por supuesto, esto será así si todo se hace correctamente. Y si no, entonces el resultado probablemente sea este:

$ ./forth


Por supuesto, otras opciones son posibles, pero me he encontrado con esto con mayor frecuencia. Y necesitamos un depurador.

Letras de Debugger
Como ya se mencionó, usé GDB. Este es un depurador bastante potente, pero con una interfaz de línea de comandos. Ejecutarlo es muy simple:

 $ gdb ./forth GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1 Copyright (C) 2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from ./forth...done. (gdb) 

Luego, al ingresar comandos, depuramos. Tenía suficientes horas para encontrar algunos comandos necesarios y aprender a usarlos para la depuración. Aquí están:
b <etiqueta> - establece un punto de interrupción
l <etiqueta> - ver código fuente
r - inicia o reinicia el programa
ir - ver el estado de los registros del procesador
s - paso

Por cierto, ¿recuerdas que necesitas compilar el programa con el modificador -g? De lo contrario, las etiquetas y el código fuente no estarán disponibles. En este caso, será posible depurar solo mediante código desmontado y usar las direcciones en la memoria. Nosotros, por supuesto, somos indios, pero no en la misma medida ...

Pero de alguna manera el programa hace muy poco. Solo le decimos "Hola" e inmediatamente ella dice "¡Adiós!". Hagamos el verdadero "¡Hola mundo!" en bytecode. Para hacer esto, coloque la dirección y la longitud de la cadena en la pila, luego ejecute el comando que muestra la cadena y luego el comando bye. Para hacer todo esto, se requieren nuevos comandos: escriba para generar la cadena y se enciende para poner la dirección y la longitud de la cadena. Primero escribimos tipo, dejemos que su código sea 0x80. Nuevamente, necesitamos ese código con la llamada sys_write:

 b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next 

Aquí tomamos la dirección y la longitud de la cadena de la pila de datos usando comandos POP. Llamar a int 0x80 puede cambiar el registro de R8, por lo que lo guardamos. No hicimos esto antes porque el programa estaba terminando. El contenido de estos registros no le importaba. Ahora, este es un comando de byte normal, después del cual el código de byte continúa ejecutándose, y debe comportarse bien.

Ahora escribamos el encendido. Este será nuestro primer equipo con parámetros. Después del byte con el código para este comando, habrá bytes que contienen el número que colocará en la pila. La pregunta surge de inmediato: ¿qué profundidad de bits se necesita aquí? Para poner cualquier número, necesitas 64 bits. Pero, cada vez que el comando ocupará 9 bytes, ¿qué pondría un número? Así que perdemos compacidad, una de las principales propiedades del código de bytes, y el código de Fort también ...

La solución es simple: crearemos varios comandos para diferentes profundidades de bits. Estos serán lit8, lit16, lit32 y lit64. Para números pequeños usaremos lit8 y lit16, para números más grandes: lit32 y lit64. Los números pequeños se usan con mayor frecuencia, y para ellos habrá el comando más corto, que toma dos bytes. ¡No está mal! .. Haremos que los códigos de estos comandos sean 0x08 - 0x0B.

 b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next 

Aquí usamos el comando MOVSX: esta es una versión icónica del comando MOVZX que ya conocemos. R8 tenemos un contador de comando de bytes. Cargamos el valor del tamaño deseado en él, lo movemos al siguiente comando y colocamos el valor convertido a 64 bits en la pila.

No olvide agregar las direcciones de los nuevos equipos en la tabla a las posiciones deseadas.

Todo está listo para escribir su primer programa "¡Hola, mundo!" en nuestro bytecode. ¡Trabajemos con el compilador! :)

 start: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_bye 

Utilizamos dos comandos iluminados diferentes: lit64, que colocaría la dirección de la cadena en la pila, y lit8, con el que colocamos la longitud en la pila. A continuación, ejecutamos dos comandos de byte más: type y bye.
Compilar, ejecutar:

 $ as fort.asm -o fort.o -g -ahlsm >list.txt $ ld forth.o -o forth $ ./forth Hello, world! bye! 

Ganó nuestro bytecode! Este es el resultado que debería ser, si todo está bien.

Fuente completa
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00 .quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x10 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_bye .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_num0 = 0x02 bcmd_num0: push 0 jmp _next b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next 


Pero las posibilidades siguen siendo muy primitivas, no se puede hacer una condición, un ciclo.

Como es imposible ¡Puedes, todo está en nuestras manos! Hagamos esta línea en el bucle 10 veces. Esto requerirá un comando de ramificación condicional, así como un poco de aritmética de pila: un comando que disminuye el valor en la pila en 1 (en fort "1-") y un comando de duplicación de vértices ("dup").

Con la aritmética, todo es simple, ni siquiera comentaré:

 b_dup = 0x18 bcmd_dup: push [rsp] jmp _next b_wm = 0x20 bcmd_wm: decq [rsp] jmp _next 

Ahora un salto condicional. Para empezar, simplifiquemos la tarea: una transición incondicional. Está claro que solo necesita cambiar el valor del registro R8. Lo primero que viene a la mente es un comando de bytes, seguido de un parámetro: la dirección de transición es de 64 bits. De nuevo nueve bytes. ¿Necesitamos estos nueve bytes? Las transiciones generalmente ocurren en distancias cortas, a menudo dentro de unos pocos cientos de bytes. Por lo tanto, no utilizaremos la dirección, sino el desplazamiento.

¿Profundidad de bits? En muchos casos, 8 bits (127 hacia adelante / hacia atrás) serán suficientes, pero a veces esto no será suficiente. Por lo tanto, haremos lo mismo que con el comando encendido, haremos dos opciones: 8 y 16 dígitos, los códigos de comando serán 0x10 y 0x11:

 b_branch8 = 0x10 bcmd_branch8: movsx rax, byte ptr [r8] add r8, rax jmp _next b_branch16 = 0x11 bcmd_branch16: movsx rax, word ptr [r8] add r8, rax jmp _next 
Ahora la transición condicional es fácil de implementar. Si la pila es 0, vaya a _next, y si no, vaya al comando de bifurcación.
 b_qbranch8 = 0x12 bcmd_qbranch8: pop rax or rax, rax jnz bcmd_branch8 inc r8 jmp _next b_qbranch16 = 0x13 bcmd_qbranch16: pop rax or rax, rax jnz bcmd_branch16 add r8, 2 jmp _next 
Ahora tenemos todo para hacer un bucle:
 start: .byte b_lit8 .byte 10 #  #  m0: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye 

Los dos primeros comandos: colocamos el contador de bucles en la pila. A continuación, imprima la cadena Hola. Luego restamos 1 del contador, lo duplicamos y realizamos (o no realizamos) la transición. El comando de duplicación es necesario porque el comando de ramificación condicional toma el valor de la parte superior de la pila. La transición aquí es de ocho bits, ya que la distancia es de solo unos pocos bytes.

Ponemos las direcciones de los nuevos comandos en una tabla, compilamos y ejecutamos.

Lo pondré en un spoiler, de lo contrario nuestro programa se ha vuelto detallado)
 $ as fort.asm -o fort.o -g -ahlsm >list.txt $ ld forth.o -o forth $ ./forth Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! bye! 


Bueno, ya podemos hacer condiciones y ciclos!

Fuente completa
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00 .quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_branch8, bcmd_branch16, bcmd_qbranch8, bcmd_qbranch16, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x10 .quad bcmd_dup, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_wm, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_lit8 .byte 10 #  #  m0: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_num0 = 0x02 bcmd_num0: push 0 jmp _next b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next b_dup = 0x18 bcmd_dup: push [rsp] jmp _next b_wm = 0x20 bcmd_wm: decq [rsp] jmp _next b_branch8 = 0x10 bcmd_branch8: movsx rax, byte ptr [r8] add r8, rax jmp _next b_branch16 = 0x11 bcmd_branch16: movsx rax, word ptr [r8] add r8, rax jmp _next b_qbranch8 = 0x12 bcmd_qbranch8: pop rax or rax, rax jnz bcmd_branch8 inc r8 jmp _next b_qbranch16 = 0x13 bcmd_qbranch16: pop rax or rax, rax jnz bcmd_branch16 add r8, 2 jmp _next 

Pero hasta que a la máquina de bytes completada le falte otra función muy importante. No podemos llamar a otro desde el código de bytes. No tenemos lo que se llama rutinas, procedimientos, etc. Y en el fuerte, sin esto, no podemos usar palabras que no sean palabras del núcleo en algunas palabras.

Llevamos el trabajo al final. Aquí por primera vez necesitamos una pila de devoluciones. Se necesitan dos comandos: el comando de llamada y el comando de retorno (llamar y salir).

El comando de llamada, en principio, hace lo mismo que la rama: transfiere el control a otro código de bytes. Pero, a diferencia de la rama, aún necesita guardar la dirección de retorno en la pila de retorno para poder regresar y continuar la ejecución. Hay otra diferencia: tales llamadas pueden ocurrir a distancias mucho mayores. Por lo tanto, hacemos el comando de llamada en forma de rama, pero en tres versiones: 8, 16 y 32 bits.

 b_call8 = 0x0C bcmd_call8: movsx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 add r8, rax jmp _next b_call16 = 0x0D bcmd_call16: movsx rax, word ptr [r8] sub rbp, 8 add r8, 2 mov [rbp], r8 add r8, rax jmp _next b_call32 = 0x0E bcmd_call32: movsx rax, dword ptr [r8] sub rbp, 8 add r8, 4 mov [rbp], r8 add r8, rax jmp _next 

Como puede ver, aquí, a diferencia de las transiciones, se agregan 3 equipos. Uno de ellos reorganiza R8 al siguiente comando de byte, y los dos restantes almacenan el valor recibido en la pila de retorno. Por cierto, aquí intenté no poner las instrucciones del procesador una al lado de la otra, de modo que el transportador del procesador pudiera ejecutar los comandos en paralelo. Pero no sé cuánto esto da un efecto. Si lo desea, puede verificar las pruebas.

Debe tenerse en cuenta que la formación de un argumento para el comando de llamada es algo diferente que para la rama. Para la bifurcación, el desplazamiento se calcula como la diferencia entre la dirección de la bifurcación y la dirección del byte que sigue al comando de byte. Y para el comando de llamada, esta es la diferencia entre la dirección de salto y la dirección del siguiente comando. ¿Por qué se necesita esto?Esto da como resultado menos instrucciones del procesador.

Ahora el comando de retorno. En realidad, su trabajo es solo restaurar R8 de la pila de retorno y transferir el control a la máquina de bytes:

 b_exit = 0x1F bcmd_exit: mov r8, [rbp] add rbp, 8 jmp _next 

Estos comandos se utilizarán con mucha frecuencia y deben optimizarse al máximo. El comando de byte de salida ocupa tres instrucciones de máquina. ¿Es posible reducir algo aquí? Resulta que puedes! Simplemente puede eliminar el comando de transición :)

Para hacer esto, colóquelo sobre el punto de entrada de la máquina _next byte:

 b_exit = 0x1F bcmd_exit: mov r8, [rbp] add rbp, 8 _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] 

Por cierto, los comandos más importantes y de uso más frecuente (por ejemplo, llamada) deben colocarse más cerca de la máquina de bytes para que el compilador pueda formar un comando de salto corto. Esto es claramente visible en el listado. Aquí hay un ejemplo.

  262 0084 490FBE00 bcmd_lit8: movsx rax, byte ptr [r8] 263 0088 49FFC0 inc r8 264 008b 50 push rax 265 008c EB90 jmp _next 266 267 b_lit16 = 0x09 268 008e 490FBF00 bcmd_lit16: movsx rax, word ptr [r8] 269 0092 4983C002 add r8, 2 270 0096 50 push rax 271 0097 EB85 jmp _next 272 273 b_lit32 = 0x0A 274 0099 496300 bcmd_lit32: movsx rax, dword ptr [r8] 275 009c 4983C004 add r8, 4 276 00a0 50 push rax 277 00a1 E978FFFF jmp _next 277 FF 278 

Aquí, en la línea 265 y 271, el comando jmp toma 2 bytes cada uno, y en la línea 277, el mismo comando ya está compilado a 5 bytes, ya que la distancia de salto excedió la longitud del comando corto.

Por lo tanto, los comandos de byte como bad, bye, type se reorganizan aún más, y como call, branch, lit están más cerca. Desafortunadamente, no hay mucho que pueda caber en una transición de 127 bytes.
Agregamos nuevos comandos a la tabla de direcciones de comandos de acuerdo con sus códigos.

Entonces, ahora tenemos un desafío y un retorno, ¡los probaremos! Para hacer esto, seleccione la impresión de línea en un procedimiento separado, y lo llamaremos en un bucle dos veces. Y el número de repeticiones del ciclo se reduce a tres.

 start: .byte b_lit8 .byte 3 #  #  m0: .byte b_call16 .word sub_hello - . - 2 .byte b_call16 .word sub_hello - . - 2 .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye sub_hello: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_exit 

Call8 podría usarse aquí, pero decidí usar call16 como el más usado. El valor 2 se resta debido a las peculiaridades de calcular la dirección para el comando de byte de llamada sobre el que escribí. Para call8, 1 se deducirá aquí, para call32, respectivamente, 4. Compilamos
y llamamos:

 $ as forth.asm -o forth.o -g -ahlsm>list.txt $ ld forth.o -o forth $ ./forth Hello, world! Bad byte code! 

Vaya ... como dicen, algo salió mal :) Bueno, lanzamos GDB y vemos qué sucede allí. Establecí un punto de interrupción inmediatamente en bcmd_exit, ya que está claro que la llamada sub_hello está pasando, y el cuerpo de la rutina se ejecuta ... comenzó ... y el programa no alcanzó el punto de interrupción. Inmediatamente hubo una sospecha de un código de comando de byte. Y, de hecho, la razón estaba en él. b_exit Asigne el valor 0x1f, y la dirección en sí se colocó en el número de celda de la tabla 0x17. Bueno, entonces corregiré el valor de b_exit a 0x17 e intentaré nuevamente:

 $ as forth.asm -o forth.o -g -ahlsm>list.txt $ ld forth.o -o forth $ ./forth Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! bye! 

Exactamente seis veces saludo, y una vez adiós. Como debería ser :)

Fuente completa
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00 .quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_call8, bcmd_call16, bcmd_call32, bcmd_bad .quad bcmd_branch8, bcmd_branch16, bcmd_qbranch8, bcmd_qbranch16, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_exit # 0x10 .quad bcmd_dup, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_wm, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_lit8 .byte 3 #  #  m0: .byte b_call16 .word sub_hello - . - 2 .byte b_call16 .word sub_hello - . - 2 .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye sub_hello: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_exit .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next b_exit = 0x17 bcmd_exit: mov r8, [rbp] add rbp, 8 _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_num0 = 0x02 bcmd_num0: push 0 jmp _next b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_call8 = 0x0C bcmd_call8: movsx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 add r8, rax jmp _next b_call16 = 0x0D bcmd_call16: movsx rax, word ptr [r8] sub rbp, 8 add r8, 2 mov [rbp], r8 add r8, rax jmp _next b_call32 = 0x0E bcmd_call32: movsx rax, dword ptr [r8] sub rbp, 8 add r8, 4 mov [rbp], r8 add r8, rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next b_dup = 0x18 bcmd_dup: push [rsp] jmp _next b_wm = 0x20 bcmd_wm: decq [rsp] jmp _next b_branch8 = 0x10 bcmd_branch8: movsx rax, byte ptr [r8] add r8, rax jmp _next b_branch16 = 0x11 bcmd_branch16: movsx rax, word ptr [r8] add r8, rax jmp _next b_qbranch8 = 0x12 bcmd_qbranch8: pop rax or rax, rax jnz bcmd_branch8 inc r8 jmp _next b_qbranch16 = 0x13 bcmd_qbranch16: pop rax or rax, rax jnz bcmd_branch16 add r8, 2 jmp _next b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next 


Cual es el resultado


Hicimos y probamos una máquina de bytes de pila completa y bastante rápida de 64 bits. En velocidad, quizás esta máquina de bytes sea una de las más rápidas de su clase (una máquina de bytes de pila sin JIT). Ella sabe cómo ejecutar comandos secuencialmente, hacer saltos condicionales e incondicionales, llamar a procedimientos y regresar de ellos. Al mismo tiempo, el código de bytes utilizado es razonablemente compacto. Básicamente, los comandos de bytes toman 1-3 bytes, más es muy raro (solo números grandes y llamadas a procedimientos muy distantes). También se dibuja un pequeño conjunto de comandos de bytes, que es fácil de expandir. Supongamos que todos los comandos básicos para trabajar con la pila (drop, swap, over, root, etc. se pueden escribir en 20 minutos, la misma cantidad se destinará a los comandos de enteros aritméticos).

Otro punto importante. El bytecode, a diferencia del clásico código de costura directa fuerte, no contiene instrucciones de la máquina, por lo que puede transferirse sin recompilación a otra plataforma. Es suficiente reescribir el núcleo una vez en el sistema de instrucciones del nuevo procesador, y esto se puede hacer muy rápidamente.

La versión actual de la máquina de bytes no es específica de ningún idioma en particular. Pero quiero implementar el lenguaje Fort en él porque tengo experiencia con él y el compilador se puede hacer muy rápidamente.

Si hay interés en esto, en base a esta máquina, en el próximo artículo, haré entradas y salidas de cadenas y números, un diccionario fuerte y un intérprete. Puede "tocar" al equipo con las manos. Bueno, en el tercer artículo haremos un compilador y obtendremos un sistema fort casi completo. Entonces será posible escribir y compilar algunos algoritmos estándar y comparar el rendimiento con otros lenguajes y sistemas. Puede usar, por ejemplo, el tamiz de Eratóstenes, y similares.

Es interesante experimentar con opciones. Por ejemplo, cree la tabla de comandos de 16 bits y vea cómo esto afectará el rendimiento. También puede convertir el punto de entrada _next en una macro, en cuyo caso el código de máquina de cada comando de byte aumentará de tamaño en dos comandos (menos la transición y más tres comandos de _next). Es decir, al final no habrá transición a _next, pero el contenido del _next apunta en sí mismo (esto es 14 bytes). Es interesante saber cómo esto afectará el rendimiento. También puede intentar hacer la optimización usando registros. Por ejemplo, un bucle estándar con un contador en el fuerte almacena el contador en la pila de devolución. Puede hacer una versión de registro y también probarla.

También puede hacer un compilador de expresiones escritas en la forma clásica (por ejemplo, A = 5 + (B + C * 4)).

¡En general, hay espacio para la experimentación! :)

Continuación: Byte-máquina para el fuerte (y no solo) en nativos americanos (parte 2)

Source: https://habr.com/ru/post/es431932/


All Articles