En la entrega anterior aprendimos a programar en BrainF*ck y las bases de como programar los procesadores RISC-V en ensamblador para Linux. También vimos como configurar rápida y fácilmente un sistema en el que desarrollar y probar nuestros programas. En esta entrega vamos a continuar con nuestra implementación del intérprete de BrainFuck.
Vamos a comenzar con una sencilla modificación con la que eliminar los caracteres de control como tabuladores o retornos de carro y así introducir algunas instrucciones más y mostraros la base para sanear la entrada de datos de nuestro programa. En el caso general deberíamos eliminar cualquier cosa que no sea una de las instrucciones del lenguaje.
Filtrando Caracteres de control
El código sería algo así:
_start:
# Lee datos desde entrada standard, prog
la a1, zero
move t1, 0x20 *** NUEVO!!!!
li t2bucle01:
, 63
li a7, 0
li a0, 1
li a2
ecall<=0 (EOF) terminamos bucle
# Si , cont01
blez a0
, 0(a1) *** NUEVO!!!
lb t0,t2, bucle01 # Elimina espacios *** NUEVO!!!
ble t0
add t1, t1, 1
add a1, a1, 1
j bucle01
cont01:
Os hemos marcado las líneas añadida para que sea más fácil seguir la
explicación, Comencemos por la instrución lb
Load
Byte. Esta instrucción nos permite cargar en un registro un byte
leído desde una posición de memoria. El parámetro 0(a1)
se
puede leer como: el contenido de la posición de memoria
a1 + 0
.
También podemos ver la instrucción ble
que como os
podéis imaginar realiza un salto si el primer parámetro es menor o igual
(Less or Equal) que el segundo. En este caso nos estamos
saltando todos los caracteres menores que 0x20
que
representa el espacio. Estos valores bajos del código ASCII se utilizan
para representar secuencias de control como el retorno de carro o el
tabulador.
Como podéis ver, en el caso de que el valore leído sea una secuencia
de control no incrementamos el puntero dentro de nuestro buffer de
lectura de tal suerte que la siguiente llamada a SYS_read
volverá a escribir el caracter en la posición actual.
Preparándonos para la interpretación
Para poder interpretar nuestros programas en BrainFuck, tendremos que leer los distintos comandos uno a uno, y según el comando realizar una operación u otra. Así que el siguiente paso que vamos a hacer es imprimir nuestro programa en memoria, pero caracter a caracter, como paso previo para la implementación de nuestros programas.
Para ello, eliminamos la llamada a SYS_write
con el
tamaño del buffer, por un bucle en el que imprimiremos el buffer byte a
byte. El fragmento de código sería algo así:
cont01:
# t1 contiene longitud del programa
# Muestra la cadena volcada desde memoriaes el bucle base de ejecución
# Este , prog # Contador de programa
la t2add t3, t2, t1 # Apunta al final del programa
bucle02:
, 64
li a7, t2
move a1, 1
li a2
ecall
add t2, t2, 1 # Incrementa contador del programa
, t3, bucle02 # Mientras no alcancemos el final, repetimos
blt t2exit:
Puesto que ya hemos introducido todos los elementos vamos a comentar brevemente el código. En el bucle principal de ejecución vamos a utilizar los registros temporales. Por el momento esto es lo que tenemos:
t1
contiene la longitud del programa a ejecutar (lo calculamos al leer la entrada de usuario).t2
va a ser el puntero de programa, el que iremos moviendo por el buffer para ir leyendo cada uno de los comandos que conforman el programa.t3
representa la dirección inmediatamente después del último comando del programa. No es más que el inicio del buffer +t1
, así que, realmente, no necesitaremost1
más en el resto del programa.
Una vez inicializados t2
y t3
, el bucle
principal irá incrementando el valor de t2
hasta que sea
igual a t3
, momento en el cual habremos alcanzado el final
del programa y debemos parar su ejecución.
Implementando los comandos básicos
Ahora que ya podemos extraer los comandos de nuestro buffer de programa, vamos a interpretarlos. Esto es muchísimo más fácil de lo que parece. Primero tendremos que añadir una nueva variable local para poder acceder a la memoria de datos.
cont01:
# t1 contiene longitud del programa
# Muestra la cadena volcada desde memoriaes el bucle base de ejecución
# Este , prog # Contador de programa
la t2, data # Puntero de datos
la t6add t3, t2, t1 # Apunta al final del programa
Sin sorpresas. data
es otra etiqueta en la sección
.data
definiendo un buffer en el que almacenar datos. En
nuestro programa, el puntero a la memoria de datos se almacenará en el
registro temporal t6
.
Ahora veamos como interpretar los comandos básicos
(+-<>,.[]
).
bucle02:
, 0(t2) # Leemos comando de memoria de programa
lb t4
, '<'
li t5, t5, inc_data_ptr
beq t4, '>'
li t5, t5, dec_data_ptr
beq t4, '+'
li t5, t5, inc_data
beq t4, '-'
li t5, t5, dec_data
beq t4, '.'
li t5, t5, print_data
beq t4, ','
li t5, t5, read_data
beq t4, '['
li t5, t5, inicia_bucle
beq t4, ']'
li t5, t5, fin_bucle
beq t4
# Error command desconocido va aquí j cont02
La instrucción lb
ya la conocemos. En esta ocasión
estamos leyendo en el registro t4
el operador BrainFuck al
que apunta el puntero de programa t2
.
Puesto que RISC-V solo ofrece instrucciones de salto condicional
donde el condicional se debe aplicar sobre dos registros, debemos cargar
el valor que queremos comparar en un registro antes de poder procesarlo.
Los distintos comandos los cargamos en t5
y usando la
instrucción beq
(Branch if EQual Salta si es
Igual), comparamos los dos valores y si coincide saltamos a una
determinada etiqueta en nuestro programa. En un segundo implementaremos
esas etiquetas.
Si el valor leído de la memoria no coincide con ningún comando,
tenemos la opción de mostrar un error, si bien, en este caso es mejor
ignorar el caracter. De esta forma podemos incluir comentarios en
nuestros programas bf
, simplemente escribiendolos. Por
ejemplo:
, Lee un valor y lo almacena en DP
++ Suma 2 al valor leido
. Imprimimos el valor
Para comprobar esto, vamos a incluir la siguiente implementación temporal para nuestros comandos. Simplemente mostraremos una cadena que representa el tipo de operación. Veremos como los comentarios simplemente desaparecen.
Comprobando que filtramos los comentarios
La implementación de los comandos básicos es muy sencilla. Aquí
tenéis el código, de los dos primeros comandos. El resto son igual,
simplemente cambiando el valor que cargamos en a1
inc_data_ptr:
, label1
la a1
j cont02dec_data_ptr:
, label2
la a1
j cont02
cont02:
, 64
li a7, 1
li a0, 10
li a2
ecall
add t2, t2, 1 # Incrementa contador del programa
, t3, bucle02 # Mientras blt t2
Al final del código podemos añadir las etiquetas. En este caso se
trata de literales o valores de solo lectura, así que los podemos dejar
en la sección .text
.
label1:.asciz "Comando <\n"
label2:.asciz "Comando >\n"
label3:.asciz "Comando +\n"
label4:.asciz "Comando -\n"
label5:.asciz "Comando .\n"
label6:.asciz "Comando ,\n"
label7:.asciz "Comando [\n"
label8:.asciz "Comando ]\n"
Si ahora ejecutáis el intérprete, veréis como podéis escribir lo que queráis entre medias del programa, solamente los caracteres que forman parte del lenguaje serán interpretados (tened cuidado con las comas y puntos).
Implementando los comandos básicos
Comenzaremos implementando los comandos de manipulación de la memoria de datos.
inc_data_ptr: # Comando >
add t6, t6, 1
j cont02dec_data_ptr: # Comando <
add t6, t6, -1
j cont02inc_data: # Comando +
, 0(t6)
lb t1add t1, t1, 1
, 0(t6)
sb t1
j cont02dec_data: # Comando -
, 0(t6)
lb t1add t1, t1, -1
, 0(t6)
sb t1 j cont02
La nueva instrucción que aparece en este caso es sb
y
os podéis imaginar que hace. Si, almacena un byte en memoria (Store
Byte). Los comandos <
y >
simplemente modifican el valor de t6
, nuestro puntero de
datos. Nótese que estas funciones deberían de comprobar que el valor
t6
permanece en rango.
Los comandos +
y -
leen el byte en la
posición de memoria apuntada por t6
, le suman
1
, y lo vuelven a almacenar. Más fácil no se puede.
Veámos ahora los comandos ,
y .
, si bien, a
estas alturas deberíais de ser capaces de implementarlos vosotros
mismos.
print_data:
, 64
li a7, 1
li a0, t6
move a1, 1
li a2
ecall
j cont02read_data:
, 63
li a7, 0
li a0, t6
move a1, 1
li a2
ecall
j cont02
Si, una vez más, simplemente llamamos a SYS_read
y
SYS_write
pero esta vez utilizando como puntero la memoria
de datos. Con este último cambio podremos ejecutar cualquier programa
BF
que no utilice bucles.
El intérprete por ahora
Para terminar esta entrega y ya que hemos hecho muchas cambios en distintas partes del programa, os dejamos la versión actual y completa del código para los que os hayáis podido perder.
.text
.global _start
.equ MEM_SIZE, 4096
:
_start# Lee datos desde entrada standard
, prog
la a1, zero
move t1, 0x20
li t2:
bucle01, 63
li a7, 0
li a0, 1
li a2
ecall# Si <=0 (EOF) terminamos bucle
, cont01
blez a0
, 0(a1)
lb t0, t2, bucle01 # Elimina espacios
ble t0
, t1, 1
add t1, a1, 1
add a1
j bucle01
:
cont01# Este es el bucle base de ejecución
, prog # Contador de programa
la t2, data # Puntero de datos
la t6, t2, t1 # Apunta al final del programa
add t3
:
bucle02, 0(t2)
lb t4, '<'
li t5, t5, inc_data_ptr
beq t4, '>'
li t5, t5, dec_data_ptr
beq t4, '+'
li t5, t5, inc_data
beq t4, '-'
li t5, t5, dec_data
beq t4, '.'
li t5, t5, print_data
beq t4, ','
li t5, t5, read_data
beq t4
j cont02:
inc_data_ptr, t6, 1
add t6
j cont02:
dec_data_ptr, t6, -1
add t6
j cont02:
inc_data, 0(t6)
lb t1, t1, 1
add t1, 0(t6)
sb t1
j cont02:
dec_data, 0(t6)
lb t1, t1, -1
add t1, 0(t6)
sb t1
j cont02:
print_data, 64 # SYS_write
li a7, 1
li a0, t6
move a1, 1
li a2
ecall
j cont02:
read_data, 63 # SYS_read
li a7, 0
li a0, t6
move a1, 1
li a2
ecall# Aqui no hace falta el salto, pero la necesitaremos luego
j cont02 :
cont02, t2, 1 # Incrementa contador del programa
add t2, t3, bucle02 # Mientras no alcancemos el final, repetimos
blt t2
:
exit, 93 # SYS_exit
li a7, 0
li a0
ecall
.data
:
prog.fill MEM_SIZE
:
data.fill MEM_SIZE
COLOFÓN
Ya tenemos casi listo nuestro intérprete, solo nos faltan los bucles, que son la parte que tiene algo de complicación y pulirlo un poco. Pero eso lo veremos en la siguiente entrega.
Continua Leyendo...
Intérprete BrainFuck para RISC-V. Parte III■