11 agosto 2008

Un compilador sencillo paso a paso (11 - Saltos incondicionales)

Las estructuras condicionales, como "if", y las repetitivas como "while" y "for", cuando se conviertan a código máquina, necesitarán realizar saltos a partes anteriores o posteriores del programa. Por eso, como forma de acercarnos a esa problemática, vamos a crear saltos incondicionales: la peligrosa orden "goto".

Eso supone que también podamos definir etiquetas, para indicar a qué lugar queremos saltar. En Pascal, quedaría algo como:

label destino;
begin
...
destino:
...
goto destino;
...
end.


La forma más sencilla (aunque poco eficiente) de convertirlo a código máquina es usar saltos a direcciones absolutas de memoria. Por ejemplo, un salto atrás en ensamblador quedaría así:


...
.destino
...
jp destino
...


La traducción a ensamblador es prácticamente inmediata. Pero nosotros no queremos compilar a ensamblador, sino directamente a código máquina, de modo que no podemos emplear etiquetas, sino que debemos indicar a qué posicion de memoria queremos saltar. Si se trata de una posición de memoria anterior, no es díficil: como ya la conocemos, podríamos construir sin problemas la orden de salto:


30000: ...
...
C3 30 75; jp 30000
...


Pero si se trata de un salto hacia un punto posterior, estamos atados de manos: todavía no sabemos de qué dirección de memoria se trata, lo que nos obliga a "retroceder" cuando ya la conozcamos: en ensamblador sería


...
jp destino
...
.destino
...


Que en código máquina se convertiría en:


...
C3 ?? ?? ; jp ????
...


Esas "interrogaciones" representan lo que todavía no sabemos, huecos que tendremos que rellenar más adelante, y que nos obligarán a dar una segunda pasada (aunque sencilla). Como estamos volcando a fichero a medida que compilamos, tenemos dos opciones


  • Conservar todo el programa en memoria, para poder dar la segunda
    pasada antes de volcar a disco.

  • Tras terminar la primera pasada, volver a abrir el fichero generado
    para dar la segunda pasada, volcando el resultado a un nuevo fichero.



La primera opción sería la más eficiente en cuanto a velocidad, pero necesita más espacio en memoria, y partimos de la idea de que quizá algún día el compilador llegue a poder compilarse a sí mismo y funcionar desde la máquina de destino, un equipo con unos 64 Kb de memoria. Por eso, consideraremos la memoria como un bien muy valioso, y seguiremos volcando a disco, para dar una segunda pasada sobre ese fichero resultante en disco.

Así, el resultado de la primera salida para esa instrucción de
salto podría ser algo como:


...
DATA,C3,destinoLOW,destinoHIGH ; jp destino
...


(Lo de descomponer "destino" en "destinoLOW" y "destinoHIGH" es porque en la máquina que estamos tomando como destino se sigue el criterio habitual de codificar los datos de más de un byte como el byte menos significativo seguido por el más significativo -"little endian").


La segunda pasada podría buscar cada uno de los identificadores de la tabla de símbolos (como "destino") para ver si aparecen descompuestos en "destinoLOW" y "destinoHIGH" y reemplazarlos por sus valores.


Ya que vamos a dar dos pasadas, podemos optimizar algo que antes habíamos hecho de una forma sencilla pero no demasiado correcta:podemos dejar las variables a continuación del código, en vez de colocarlas al principio y separadas del código por un espacio arbitrario. Ahora otros fragmentos de nuestro código serán cosas como:


...
DATA,3E,[xBYTE] ; LD A, variableX
...


Y el espacio para esa "variableX" se reservaría al final, de la primera pasada, tras el código del programa, y el valor exacto de la dirección se sustituiría en la segunda pasada.



Aun así, esa mejora de momento la aplazamos para algo más adelante...

Como siempre, para más detalles, todo el código está en la página del proyecto en Google Code:

http://code.google.com/p/cpcpachi/