sábado, 22 de febrero de 2014

Cuando tenía que redescubrir la pólvora

El otro día estuve mirando una página sobre el Spectrum y de ahí salté al blog de José Ramón Palacios, un programador de Spectrum a cuyo único juego nunca había jugado, pero que resultó ser paisano mío e inspirador de uno de los primeros juegos de PC a los que jugué, y, por otro lado, al compilador ZX Basic Compiler. Ambas páginas me trajeron un montón de recuerdos y nostalgia, pero no solamente del viejo Spectrum, sino también —al leer las condiciones en que programaba Palacios— sobre cómo hacía yo las cosas en el PC antes de que los discos de Shareware primero e internet después me pusieran en condiciones de usar un compilador.

Hoy en día, encontrar información sobre cómo programar es sencillísimo. De hecho, me extraña que no haya, por lo menos, un programador en cada hogar de España. Quizá porque a los adolescentes les atrae más jugar que programar, no lo sé. El caso es que en mi primer PC busqué lo que me había ofrecido el Spectrum: la posibilidad de programar. El problema es que el lenguaje interpretado incluido con MS-DOS era lentísimo, como solía suceder en aquellos tiempos (no es que los lenguajes actuales sean más rápidos: es que hoy en día son ejecutados por procesadores que funcionan a cientos de veces su velocidad). Por eso había dos opciones: usar ensamblador o compilar, es decir, convertir un programa del lenguaje utilizado por el humano al usado por la máquina, para que funcionase rápidamente.
¡Compilar! Yo había oído hablar de los compiladores desde los tiempos del Spectrum, y recuerdo haberme acercado al Corte Inglés de electrónica de Sol a principios de los 90 buscando uno.

No sé si recordáis cómo era aquella sección entonces: los juegos de ordenador en la planta baja o quizá el sótano, rodeados por artefactos tales como procesadores de texto (de hardware), unos bicharracos que parecían ordenadores pero que se anunciaban como "simples y robustos" frente a los ordenadores; secciones donde todavía aparecían los últimos modelos de Spectrum, Amstrad y Commodore junto a estanterías donde casi todos los juegos para PC funcionaban en CGA pero mostraban pantallas de la versión Amiga, muy superior, discos de 5 1/4 y... ¡programas serios! una gran cantidad de programas serios, muy superior a la que hoy en día encontraríais en la FNAC o en el propio Corte Inglés.

Interrogado un comercial, me dijo el precio del compilador de basic... Creo que era el QuickBasic de Microsoft, pero no estoy seguro (quizá fuera el anterior compilador de Basic de IBM, que a los años me he encontrado rondando por la red). Su precio, astronómico (muy superior a lo que hoy costaría un Office). Natural, porque era para programadores profesionales. Pregunté por Pascal, C... Todos carísimos.

Así que me volqué en el ensamblador. Como no era aficionado a recorrer bibliotecas, mi fuente eran libros de informática, leídos a vistazos rápidos en librerías. En una descubrí cómo usar la interrupción 10h para trazar puntos en la pantalla(método lento, lentísimo). En otra, traté de averiguar cómo convertir ondas desde un .VOC en espectrogramas (por aquel entonces cursaba una asignatura de fonética). El problema era que programar en ensamblador sin tener un ensamblador era harto complicado.

Para explicar la complejidad, primero debería indicar qué es eso del ensamblador. Un PC de hacia 1990 tenía un procesador de un solo núcleo que iba leyendo secuencias de instrucciones en bloques de 8 bits. Cuando recibía, por ejemplo, la secuencia "B8", sabía que las siguientes secuencias serían valores a almacenar en el "registro AX", una especie de memoria a muy corto plazo cuyo valor podía ser sumado, restado o multiplicado (pero que, por ejemplo, no servía para hacer referencia a una dirección de la memoria RAM). Instrucciones como la serie "B8-NN-NN" constituirían el complicado "código máquina" o "código binario", distinto para cada familia de procesadores (los primeros ordenadores, como el Spectrum 48K, traían una lista de tales instrucciones). Como el "código máquina" es complicado de usar, los programadores de la época empleaban un lenguaje intermedio, llamado "mnemónico" (es decir, "fácil de recordar"), cuyas instrucciones se corresponden una a una con el código máquina. Por ejemplo, la instrucción "B80000" correspondería al "MOV AX,0000". Ese lenguaje mnemónico se llamaba también "lenguaje de ensamblador", pues se empleaba normalmente en unos programas llamados ensambladores.

Sin embargo, yo no poseía un ensamblador. Pero poseía algo parecido. Hasta muy recientemente, todos los sistemas operativos de Microsoft llevaban un programa llamado debug.exe que podía emplearse para escribir instrucciones en ensamblador y convertirlas a código binario o viceversa. Sin embargo, no era un auténtico ensamblador. ¿Por qué? Porque, a diferencia del auténtico lenguaje de ensamblador, donde se puede saltar a puntos del "código" nombrados con etiquetas inteligibles, como "nueva_partida", en debug (como en el código máquina) había que indicar exactamente a qué posición de la memoria habría que saltar. De forma que para algo tan simple como lo siguiente:

Comentario: Factorizar 3!
  1. Paso 1: Haz que B valga 3
  2. Paso 2: Haz que A valga 1
  3. Paso 3: Haz que C valga 1
  4. Paso 4: C= C*B
  5. Paso 5: Aumenta en 1 el valor de C
  6. Paso 6: Si C es menor o igual que A, vuelve al paso 4
  7. Paso 7: Finaliza.
Se convertía en código máquina en algo como:
[100] BB 01 00 (MOV BX,0001 )
[103] B8 03 00 (MOV AX,0003 )
[106] B9 01 00 (MOV CX,1 )
[109] F7 E1    (MUL CX)

[10B] 41       (INC CX)
[10C] 39 D9    (CMP CX,BX)
[10E] 7E F9    (JLE 0109)
[110] C3       (RET)
En la lista anterior aparecen a la izquierda, entre paréntesis, las posiciones de memoria en que está el código; en el centro, las instrucciones que lee el procesador y a la derecha, el código mnemónico equivalente. En debug yo podía introducir el código mnemónico (y esto era genial, porque no tenía que andar consultando un manual a cada paso en busca de la cifra equivalente). Pero detengámonos en esta línea:
[10E] 7E F9    (JLE 0109)

Su significado es el siguiente "En caso de que el resultado de una comparación previa sea menor o igual, salta al byte 109 y continúa desde ahí". En un ensamblador yo habría podido dar una dirección simbólica a la operación de multiplicación y saltar ahí; ensamblando con debug, yo necesitaba saber dónde estaría la instrucción de multiplicación, es decir, cuántos bytes ocupaba cada una de las instrucciones anteriores (así, MUL CX ocupa 2 pero INC CX ocupa 1). El riesgo de equivocación era alto, puesto que el listado anterior habría tenido una lectura distinta leído desde diferentes posiciones de memoria (de modo que un salto a 108 en lugar de a 109 hubiera hecho que el procesador leyera las instrucciones "ADD BH, DH" (instrucciones 00 F7) y "LOOPZ 14D (instrucciones E1 41)", que pudieran haber provocado un salto a una dirección arbitraria de la memoria. Por eso mis desvelos por conseguir, aunque fuera, un auténtico ensamblador que permitiera construir programas usando etiquetas.

Hice muchísimos intentos por crear mi propio ensamblador, intentos en que suplía con ingenio la falta de documentación --no tenía a mano manuales ni internet--. Usando el propio Debug sobre sí mismo intentaba conseguir listados de instrucciones, y luego, probando todas las combinaciones posibles, trataba de ver qué bytes de código máquina equivalían a cada instrucción de ensamblador. Pero finalmente desistí.

No fue hasta 1995 que concebí un método rápido y sencillo para conseguir hacer mi propio ensamblador. Tan fea era la solución que llamé a mi experimento "cutressembler". Mi programa leía un listado en ensamblador con etiquetas marcadas de forma especial (puesto que no realizaba ningún análisis de la sintaxis). Sustituía todas las etiquetas por "0000", alimentaba con el resultado al programa "debug" y capturaba la salida, almacenando las direcciones que en debug correspondían a cada etiqueta. A continuación, volvía a pasar el programa por Debug, sustituyendo las direcciones por las obtenidas en la pasada anterior. Puesto que las instrucciones de salto ocupaban más o menos bytes según lo lejano que fuera el salto, había que repetir el proceso varias veces hasta que el resultado dejara de cambiar. Sin embargo, la mejora respecto al solo uso de Debug era tan grande, que al poco empecé a escribir bibliotecas para "acelerar" diversos programas BASIC.

Pero para la época tenía ya internet, hacía tiempo que las unidades de CD-ROM se habían popularizado y además había sustituido el 286 por un 486, lo que me había permitido acceder a compiladores de C de linux. Además, el mundo del MS-DOS iba cayendo frente a Windows, un sistema para el cual no podría ensamblar con mi cutressembler. Así que mi original ensamblador acabó dedicándose mayoritariamente a proyectos "retro" en mi viejo 286.

Todo esto he pensado esta mañana, cuando buscando viejos programas de Basic de Spectrum he encontrado los folios en que pergeñé el proyecto de mi cutressembler. Era infinita mi sensación de nostalgia, pensando en aquellos tiempos, en que a cada momento tenía que redescubrir la pólvora.

1 comentario:

Elisma dijo...

Estoy seguro que a pesar de ser obsoleto ahora, ese proyecto Cutressembler te dejó aprender mucho del propio lenguaje de programación, y de la época en que era más exigente que ahora. Admiro siempre a los programadores, ojalá yo tuviera la constancia para aprender a leer, interpretar y construir a partir de esos lenguajes tan basados en silogismos y aritmética.
Un saludo José!