Análisis del programa “hello” generado por cc65

Programación retro del Commodore 64

  • Programación retro del Commodore 64
  • “Programación Retro del Commodore 64” es un blog sobre el hardware, el sistema operativo, y la programación del Commodore 64. Y más específicamente sobre programación en ensamblador. Pretende ser un blog con información de calidad, y referencia en español de la programación retro de esta maravillosa máquina.
    • Mi blog
« Publicado el: 21/08/2022 »

Como hemos dicho, el programa generado por cc65 está en formato PRG. Por tanto, los dos primeros octetos del fichero indican la dirección de carga del programa en memoria del C64. Si abrimos el fichero con un editor hexadecimal para Windows como HxD:

vemos que esa dirección de carga es $0801, es decir, justo la posición inicial de la RAM de BASIC. Esto confirma que, como sabemos, cc65 genera un programa PRG con una pequeña parte de BASIC, una especie de cargador.

De hecho, si con HxD analizamos no sólo los dos primeros octetos, sino los 14 primeros octetos, vemos que son así:

Es decir:

  • $0801 es la dirección de carga del programa, es decir, el comienzo de la RAM de BASIC.
  • $080b es la dirección de la segunda instrucción de BASIC, la que sigue a la primera instrucción. Luego veremos en qué consiste.
  • $0213 es el número de línea de la primera instrucción BASIC, es decir, 531 en decimal.
  • $9E es el token de la primera instrucción BASIC. Se trata de la instrucción SYS, que sirve para llamar desde BASIC a código máquina.
  • $32303631 es la representación en PETSCII de 2061, que es la dirección de memoria a la que llama la instrucción SYS.
  • $000000 son tres octetos a cero. Obsérvese que el primero de ellos está en la dirección $080b, es decir, donde iría esa hipotética segunda instrucción de BASIC. Esto significa que el programa BASIC termina aquí; sólo tiene la instrucción SYS y ninguna más.

Esto confirma lo que vimos con LIST en la entrada anterior, que el cargador BASIC es así:

531 SYS2061

(o 531 sys2061 si la pantalla del C64 está en minúsculas)

El valor 2061 en hexadecimal es $080d. Por tanto, el programa en código máquina que se llama desde BASIC empieza en la dirección 2061 = $080d, justo después del propio cargador BASIC, que ocupa desde $0801 hasta $080c.

Todo esto lo podemos comprobar si arrastramos el fichero “hello” hasta el emulador VICE:

Y si entramos en el monitor de VICE con ALT + M y vemos el contenido de la memoria a partir de la dirección $0801 vemos el contenido esperado (el mismo que ya vimos en HxD):

Y si desensamblamos a partir de la dirección $080d vemos el comienzo del programa en código máquina que se está ejecutando:

Este programa en código máquina no es fácil de interpretar sin mayor investigación ya que, en primer lugar, no está completo (el pantallazo anterior sólo es el comienzo) y, en segundo lugar, la parte que vemos ahí no corresponde directamente con lo que conocemos / esperamos, es decir, con la impresión de “Hello world!”, sino que cc65 incluye trozos de código correspondientes al entorno de ejecución de cc65 (runtime) y a librerías de C.

Sin embargo, si volvemos a pulsar “m” dentro del monitor de VICE pronto veremos contenido que nos resulta familiar (la cadena “Hello world!”):

Habría que seguir investigando un poco más para conocer la estructura del programa en código máquina, qué parte corresponde al runtime de cc65, qué parte corresponde a librerías de C, y qué parte corresponde a nuestro programa en C ya compilado.

Una buena forma de empezar puede ser analizando el programa en ensamblador “hello.s”, que resultó de compilar el programa en C “hello.c”:

Inspeccionado este código en ensamblador parece que:

  • Hay un primer parámetro llamado S0001 que apunta al valor $25530d00, es decir, “%S\n” en PETSCII. Este es el formato de impresión que se le pasa a la función printf(). Este parámetro se mete en una pila mediante una llamada a una rutina “pushax”. La pila no es la pila del C64 (página 1 de la RAM), sino la pila del runtime de cc65, es decir, la estructura de datos que usa cc65 para implementar las llamadas a funciones y el paso de parámetros entre ellas.
  • Hay un segundo parámetro que es el texto “text” (“Hello world!”). Este texto, o más bien su dirección, también se mete en la pila mediante una segunda llamada a “pushax”.
  • Mediante el registro Y se pasa un valor 4, que seguramente es el número de bytes (4 bytes = 2 direcciones o punteros) que se han metido en la pila. De este modo la función printf() sabe cuántos parámetros debe procesar, dos en este caso, el puntero al formato “%S\n” y el puntero al texto “Hello world!”.
  • Se llama a la rutina “_printf”, que tiene toda la pinta de ser la versión compilada para el 6502 de la clásica función de C printf().
  • Carga un valor cero en el registro X y en el acumulador, que seguramente sea el retorno correcto (EXIT_SUCESS).
  • Termina con “rts”.

Pues bien, este código de alguna manera u otra tiene que estar dentro del fichero PRG. Y, efectivamente, está un poco antes de la cadena “Hello world!”, en el rango de direcciones de $0840 a $085a:

De este código máquina se puede deducir que la rutina “pushax” empieza en la dirección $0f06 y la rutina “_printf” en la dirección $0eca, al menos en este programa compilado (en otro programa las direcciones podrían ser diferentes). De este modo, podríamos seguir investigando sobre el runtime y las librerías de cc65.

En definitiva, es posible analizar el programa en código máquina que genera cc65, e incluso localizar dentro de él estructuras de datos (ej. cadena “Hello world!”), nuestro programa en C ya compilado, piezas del runtime de cc65 (ej. pila de llamadas) y librerías de C (ej. printf()).

Sin embargo, una compresión completa del programa generado por cc65 requiere un análisis más profundo, ya que cc65 incluye su entorno de ejecución (runtime) y las librerías de C utilizadas por el programa. Se podría abordar, pero llevaría su tiempo, y tampoco sé si tiene mucho interés más allá de las ideas generales ya expuestas.