Mostrando las entradas con la etiqueta assembler. Mostrar todas las entradas
Mostrando las entradas con la etiqueta assembler. Mostrar todas las entradas

viernes, 16 de septiembre de 2016

Assembler: ¿y esto con qué se come?

En los lejanos '90 Calamaro decía "¿de qué hablamos cuando hablamos de amor?", hoy podría quasi-citarlo: ¿de qué hablamos cuando hablamos de assembler? 
El Assembly o Assembler es el lenguaje de programación más cercano al CPU. Cada instrucción tiene una equivalencia -en general 1:1- con un código binario que CPU comprende. Esto significa que es el lenguaje del procesador, un idioma bastante ajeno a los seres humamos normales.
A diferencia de los lenguajes de programación de alto nivel, que se han construido con términos del lenguaje cotidiano (tal vez en inglés, pero bueno, cotidiano para quienes hablan ese idioma), el assembly se compone de las instrucciones que entiende el procesador. ¿Cuáles serán? Generalmente tareas muy sencillas y puntuales que involucren operaciones aritméticas, lógicas, de carga o almacenamiento en memoria, manejo binario, control de flujo, etc.
Resulta más sencillo comprender la programación de bajo nivel si entendemos cómo funciona el procesador. He aquí que a diferencia del alto nivel en que el hardware en que se ejecutará el programa tiene poca o ninguna importancia para el programador (en términos generales obviamente), cuando desarrollamos en bajo nivel estamos trabajando para un procesador o familia de procesadores específica. Esto significa que debemos conocer el procesador, al menos la parte que el fabricante quiso que conozcamos, lo que normalmente denominamos la arquitectura de programación: set de instrucciones, modos de direccionamiento, registros accesibles (dedicados o no), espacio de direccionamiento, etc.
Un programa en bajo nivel puede apoyarse en rutinas preexistentes del BIOS (Basic Input Output System) de la computadora, o incluso en algunas provistas por el sistema operativo. Sin embargo, dada la característica del acceso directo al hardware, Assembler es el único lenguaje de programación en el que podemos prescindir por completo de cualquier plataforma y operar en forma directa con el hardware. Hacerlo así convierte cada tarea rutinaria en alto nivel en un desafío artesanal considerable, por ello verán que los algoritmos de este blog en general se enfocan en un problema sencillo y no invertimos esfuerzos en hacer una entrada o salida elegante de la información. Cuando queremos cargar variables o ver el resultado de un programa, siempre podemos usar el simulador y "mirar" la memoria.
El micro en el que centraremos los esfuerzos es el Motorola 68HC11, así que conozcámoslo un poco:
  • Cuenta con dos acumuladores o registros de propósito general de 8 bits denominados A y B
  • Brinda la posibilidad de usar los dos registros A y B como un solo registro de 16 bits denominado D (tener en cuenta que D no es un registro independiente, sino solo la agrupación de A y B, donde A es la parte alta de D y B es la parte baja de D)
  • Un bus de direcciones de 16 bits, por lo tanto 2^16 direcciones de memoria de 8 bits, un total de 65536 (64K) direcciones. Sin embargo no estarán todas implementadas.
  • Entrada/Salida mapeada en memoria (estructura de 3 buses), lo que significa que un intervalo de direcciones de memoria realmente no son tales, sino interfaces de E/S.
  • Cuenta con una palabra de estado de 8 bits, que podremos utilizar para tomar decisiones condicionales, cuyos flags indicarán si la última operación arrojó acarreo (Carry), acarreo entre el cuarto y quinto bit (Half carry), desbordamiento (oVerflow), fue negativa (Negative) o cero (Zero), además de permitirnos algunas cosillas respecto a las interrupciones que veremos más adelante.
  • Modos de direccionamiento: inmediato (relativo al PC, el operando se indica a continuación de la instrucción), indexado (cuenta con dos índices de 16 bits: IX e IY, este es el modo más práctico para recorrer vectores), directo (paginado pero limitado a la página cero, de hecho no hay registro de página para escoger otra), relativo (utilizado para los saltos), inherente (los operandos están en registros), extendido (se indica la DRO: dirección real del operando en 16 bits).
  • Manejo de pila mediante un registro puntero de pila (SP: Stack Pointer). La pila puede ubicarse en cualquier lugar de la memoria, pero obviamente debe ubicarse en un área RW. Crecerá hacia las direcciones de memoria inferiores a la indicada en el SP.
Aquí lo vemos sonriendo para la foto:
Esta belleza es el modelo A1FN del HC11, incluye 256 bytes de memoria R/W, 512 de EEPROM y 8 Kbytes de ROM. Existen diversos modelos, cada uno con distintas configuraciones de memoria y características particulares.

Un concepto básico de la programación es la ejecución en secuencia de las instrucciones que componen el programa. En otras palabras, luego de cada línea se ejecuta la siguiente, a menos que... lo que se haya ejecutado altere esa secuencia. Conociendo el funcionamiento de una CPU vemos la relación de esto con el PC (Program Counter) y su incansable incremento. En Assembly la siguiente instrucción a ejecutar será siempre la apuntada por el PC. Cuando querramos generar bucles o sentencias condicionales tendremos que generar estos comportamientos en base a comparaciones y saltos (go to) condicionales.
Los programas en assembler se almacenan como texto simple (plain text in english, puedo discutir varias horas respecto a que plain significa simple en este contexto y no plano), lo que significa que podemos abrirlo con cualquier editor de texto (desde el insípido Notepad hasta algo más profesional como el Vim o Notepad++).
Al programar en alto nivel no nos preocupamos por muchas cosas que en bajo nivel necesitan más atención. Tal es el caso de la ubicación en memoria del programa. En alto nivel no nos preocupa porque es tarea del sistema operativo cargar el programa en memoria (lo que incluye encontrar dónde cargarlo o avisar al usuario que compre más memoria o cierre programas si no lo puede lograr). Sin embargo aquí no hay sistema operativo... será nuestra tarea determinar dónde se debe cargar. Dado que trabajaremos en un entorno simulado, siempre ubicaremos el programa en un área de memoria de solo lectura (ROM), manteniendo por separado las variables en un área de lectura-escritura (RWM). Siendo que no todo el espacio direccionable está implementado -ni siquiera en el entorno simulado- deberemos prestar atención a los rangos de memoria disponibles.
Contaremos con un puñado de directivas al ensamblador: sentencias que no serán ejecutadas por el procesador sino que le indicarán al ensamblador -en nuestro caso el incluido en THRSim- por ejemplo dónde ubicar cada porción de programa para asegurarnos de que vaya al tipo de memoria que queremos. Otras directivas se pueden emplear para generar valores constantes, sean escalares o vectoriales. Incluso podremos reservar memoria, en la aproximación más asintótica que hallaremos al uso de variables en la aventura con el HC11.
Los programas tienen una estructura de cuatro columnas, que podemos generar con espacios o tabulaciones de forma indistinta. Las columnas contienen:
Sin embargo estas columnas son meramente lógicas: no espere verlas delimitadas como si de una planilla o tabla se tratara. Note que todo lo que escribamos a continuación de los operandos se considerará comentario, por tanto será ignorado por el ensamblador. No hay necesidad de utilizar un carácter especial para indicar el comienzo del comentario o su final, aunque a algunos se les escapa el // y no hay nada malo en eso. Tampoco se requiere el uso de punto y coma para delimitar sentencias. No se usan llaves para los bloques, de hecho no hay bloques en el sentido pleno de la palabra, aunque podremos generarlos mediante el control de flujo.
Asimismo es posible generar comentarios de línea completa iniciando con el caracter * (asterisco), lo que le dirá al ensamblador que ignore esa línea.
En una siguiente entrega veremos un programa sencillo en el que intentaremos distinguir algunos de los conceptos aquí vertidos.
Una aclaración final: lo expuesto aquí aplica al HC11, un microcontrolador que no está pensado en emplearse con un sistema operativo dada su simpleza y el tipo de implementación en que se utiliza. Cuando se trabaja en assembler de otros procesadores es posible utilizar rutinas del BIOS y/o del sistema operativo para simplificar algunas de las tareas tales como la entrada/salida.

lunes, 18 de julio de 2016

¿Par o impar? ¿Cero o no cero? ¿Multiplo de algo? ¿Bit o no bit? (parte 2)

En la primera parte de este tema vimos que luego de una operación aritmética, de carga, etc, que actualice el flag Z de la palabra de estado podemos verificar si el resultado fue cero (o si se cargó o guardó un cero) y actuar conforme a ello. Las dos instrucciones de salto que solo actúan por la condición Zero son BNE (branch not equal) y BEQ (branch equal). ¿Por qué tienen ese nombre?
Entender bien esto le ayudará a emplear bien las instrucciones de la familia Branch. Consideremos por un momento lo siguiente:
¿Cómo se determina si dos operandos son iguales? En alto nivel seguramente alcanzará con hacer algo como:
if (a == b)  { 
       algunas otras instrucciones 
}
¿Qué es lo que hace CPU para saber si a y b son iguales? Seguramente ya lo dedujo: los resta. Si la operación da por resultado cero, son iguales. Caso contrario, son distintos. Aun cuando estemos comparando dos cadenas de texto, estructuras o construcciones más complejas, determinar si son iguales se reduce a restar cada byte de su contraparte. ¿Cómo se comparan dos valores en bajo nivel? De la misma manera: restando. Veamos en el set de instrucciones el detalle de las instrucciones de comparación:


Note la diferencia con las instrucciones de resta (recorté una parte del set):

¿En qué se diferencian? La comparación es una resta de la que no nos interesa el resultado. Por lo demás, se comportan igual. Por lo tanto, ¿cómo sabemos en bajo nivel si dos operandos son iguales? Comparándolos y luego realizando un branch en función del flag Z. 
Vayamos entonces a un dilema existencial de muchos newbies: si hay que determinar si un valor es cero... ¿tengo que compararlo con cero? En otras palabras, supongamos que estamos recorriendo un vector usando el índice IX y estamos llevando la cuenta de los ceros en la posición de memoria CUENTAZ, ¿estaría bien hacer esto...?

        CLR CuentaZ
        ....
        LDAA 0,x
        CMPA #0
        BNE noCero
        INC CuentaZ
noCero  ....

Si leyó el post anterior ya debería saber la respuesta (sugiero releerlo para llegar a la conclusión por usted mismo).
El caso es que como hemos visto la instrucción LDAA también actualiza el flag Z en función del operando. Por lo tanto aunque no realicemos ninguna comparación, también podemos usar el flag Z para decidir un salto si el resultado de la última operación (o el operando cargado) es cero.

El flag N (Negative) es copia del MSB (most significant bit, bit más significativo) del operando cargado o del resultado de la última operación. Cuando representamos negativos en complemento a la base (Cb), complemento a la base menos uno (Cb-1) o módulo y signo (MyS) el flag N nos indica si se trata de un número negativo. 
Obviamente, si no manejamos números signados, si estamos manejando números negativos representados en exceso (como ocurre con el exponente de las representaciones de punto flotante), o si es otro dato como ser un caracter ascii, por solo citar dos casos, el flag N se actualizará pero no nos indicará el signo del número. Es el programador el que sabe en qué formato representa los operandos y por tanto puede determinar si aplica el uso del flag N como indicador de números negativos.
Por lo tanto, si el formato de representación empleado implica que el MSB coincide con el bit de signo, podremos usar los branch BMI (salta si N=1) y BPL (salta si N=0) para tomar decisiones en función del bit negative.

Supongamos entonces que necesitamos contar los números negativos de un vector, sabiendo que se almacenan en Cb. Siguiendo una pauta similar al resto de los ejemplos del post podríamos hacer algo así:
        CLR CuentaNeg
        ....
        LDAA 0,x

        BPL positi
        INC CuentaNeg
positi  ....

¿Y si necesitásemos contar tanto los ceros como los negativos? Dado que las instrucciones de la familia branch no modifican el estado de los flags, podríamos incluir varios branch consecutivos:

        CLR CuentaNeg
        CLR CuentaZ
        ....
        LDAA 0,x
        BEQ esCero     ; puedo hacer dos BRANCH seguidos, o mas!

        BPL positi     ; porque el estado de los flags no cambia
        INC CuentaNeg
positi  ...
        BRA noZ
esCero  INC CuentaZ
noZ     ...

El ejemplo no busca ser la solución óptima al problema, solo ilustrar el uso de dos branch consecutivos.
En la próxima parte veremos cómo verificar fácilmente si un número es par o impar.

lunes, 20 de junio de 2016

¿Par o impar? ¿Cero o no cero? ¿Multiplo de algo? ¿Bit o no bit?

Existen herramientas y técnicas en bajo nivel con las que no se cuenta -o no se conocen popularmente- en alto nivel. Es por ello que para el programador que viene del alto mundo suponga un reto hacer cosas que en el bajo fondo de assembler ... son muy sencillas (tanto como lo puede ser resolver cosas en assembler).
Para comprender lo que sigue a continuación es necesario que tenga un manejo medianamente fluido del sistema binario. Los ejemplos serán de operandos en 8 bits, aunque por supuesto las técnicas aplicarán a palabras de cualquier longitud.
La unidad aritmético-lógica (ALU) cuenta con una palabra de estado donde cada bit corresponde a una bandera o indicador (flag) que se actualiza en función de la última operación. 
A esta palabra de estado se la conoce como CCR (Condition Code Register), Status Word, etc. En el Motorola 68HC11 tiene una longitud de 8 bits, en el gráfico a continuación vemos el significado de cada bit:

¿Cuándo se actualizan los bits de la palabra de estado? La respuesta -citando a una autoridad en la materia- es: depende. ¿De qué depende? De la instrucción. En el set de instrucciones encontramos columnas con los flags de la palabra de estado y una indicación del efecto de la instrucción en ellos. Tomemos como ejemplo la primera de las instrucciones del set: ABA:
En este caso cada flag tiene un guión o un delta (eso que parece un triángulo). El guión significa que la ejecución de la instrucción no cambia el estado del flag, por tanto mantiene el valor que tenia. El delta indica que la ejecución actualizará el flag según el resultado de la operación. Por tanto adoptará el valor 0 o 1 según lo que resulte de la operación. Algunas instrucciones provocan que un determinado flag adopte el valor 0 o 1 sin importar los operandos. Observe por ejemplo la instrucción CLR:
Al ejecutar CLR los valores de N, Z, V y C serán siempre los mismos. Este es un caso particular en el que cuatro flags podríamos decir que se actulizan de forma determinística (sin importar el operando), la gran mayoría de las instrucciones cambian los valores de los flags en función del resultado, por lo que pueden adoptar el valor 0 o 1.
Nos centraremos en dos de los flags: Z (cero) y N (negative). El primero será 1 si la ejecución de la instrucción dio por resultado cero. En el caso de las instrucciones aritméticas es sencillo de comprender: si una suma o resta dan cero, entonces Z=1. Lo mismo ocurre si realizamos una comparación, ya que no se trata de otra cosa que una resta de la que no nos importa el resultado. Pero note que las instrucciones de carga (Load) también actualizan Z, como podemos ver en LDAA:
Por tanto al ejecutar alguna instrucción de carga (LDAA, LDAB, LDD, LDX, LDY) lo que se carga -sea en A, B, D, IX, IY- es cero, el flag Z tomará el valor 1. No es necesario comparar lo cargado con cero para actualizar el flag. 
Entonces, ¿cómo determinar si un byte o palabra es cero? Basta con cargarlo en un acumulador y emplear el flag Zero para saberlo. Si estuviéramos recorriendo un vector y contando los ceros en "ceros" podríamos por ejemplo hacer algo así:

        LDX dirIni
        LDAA 0,x
        BNE noCero
        INC ceros
noCero  ...


Si la consigna fuera contar los que no son cero, alcanza con cambiar BNE por BEQ en esta porción de programa de ejemplo.

En la próxima veremos como seguir contestando las preguntas trascendentales que hemos planteado.

domingo, 22 de mayo de 2016

Ensamblado manual (continuación): ¡ya sáquenme de la matrix!

Retomemos el programa que estábamos ensamblando. Lo copio de nuevo:


VEC     EQU    $0040

        ORG    0000
MENOR   RMB    1      Aqui quedara el menor
DIRINI  FDB    VEC    Direccion inicial del vector
CANT    FCB    07     Cantidad de elementos del vector

* El vector lo estoy ubicando en otro lugar de memoria R/W
        ORG    VEC
VECTOR  FCB    $14,$33,$FF,$E0,$09,$11,$10

* Notar que la dir inicial del programa es la que cargo en el vector de reset al final, para que inicialice el PC.


        ORG    $C000
main    LDX    DIRINI   * Cargo IX con la direccion inicial
        LDAA   0,X      * Cargo el primer elemento del vector en A
        DEC    CANT     * Decremento la cant directamente en memoria
SIGO    INX       
        LDAB   0,X      * Cargo el segundo elemento del vector
        CBA             * y los comparo
        BLS    Amenor   * Si el primero ya era menor, no lo cambio
        TBA             * Copio B en A
Amenor  DEC    CANT     * Decremento la cant directamente en memoria
        BNE    SIGO     * Si aun quedan elementos, sigue
        STAA   MENOR    * Sino guarda el menor en donde se pidió
FIN     BRA    FIN

        ORG    $FFFE
RESET   FDB    main


Esta es la tabla de símbolos como la dejamos:

identificador valor (hexadecimal)
VEC           0040
MENOR         0000
DIRINI        0001
CANT          0003 
VECTOR        0040 
main          C000
Amenor
SIGO
FIN
RESET         FFFE

y hasta aquí habíamos llegado ensamblando:

                         ORG    $C000
$C000  DE 01     main    LDX    DIRINI

$C002  A6 00             LDAA   0,X     
$C004                    DEC    CANT

Para continuar tenemos que ensamblar la instrucción DEC. Observemos los modos de direccionamiento que soporta:
En este caso DEC no admite modo directo, sino solo indexado y extendido. No estamos usando indexado porque la referencia CANT es una dirección de memoria, así que el código de operación es 7A. Observe que el operando se debe expresar en 16 bits, por eso en el set de instrucciones aparece como "hh ll", o sea parte alta (High) y parte baja (Low) de la dirección. En la tabla de símbolos vemos que CANT es 0003, por tanto al ensamblarlo queda así:

                         ORG    $C000
$C000  DE 01     main    LDX    DIRINI

$C002  A6 00             LDAA   0,X     
$C004  7A 00 03          DEC    CANT 
$C007            SIGO    INX       

Dado que la instrucción DEC CANT ensamblada ocupa tres bytes, la dirección de la siguiente instrucción es $C007 ($C0004 + 3). Seguramente ya habrá comprendido la dinámica del cálculo de la dirección de la siguiente instrucción. También habrá concluido que no es posible "predecir" la dirección de una instrucción cualquiera sin antes ensamblar todas las anteriores. Hay que tener especial cuidado de no asumir que podemos usar modo directo con todas las instrucciones, ya que existen algunas que no lo admiten (DEC, INC, CLR por ejemplo).

La siguiente instrucción tiene una etiqueta (SIGO) por tanto podemos completar esa entrada de la tabla de símbolos: SIGO equivale a $C007. El código de operación de INX es fácil de determinar porque opera solo en modo inherente. Las dos instrucciones que siguen (LDAB en modo indexado y CBA, que solo admite inherente) no representan mayor dificultad, así que también las ensamblamos sin mayor inconveniente. Llegamos a este punto, donde BLS también admite un solo modo de direccionamiento: relativo.

                         ORG    $C000
$C000  DE 01     main    LDX    DIRINI

$C002  A6 00             LDAA   0,X     
$C004  7A 00 03          DEC    CANT 
$C007            SIGO    INX       
$C008  E6 00             LDAB   0,X     
$C00A  11                CBA             

$C00B  23 ??             BLS    Amenor  
                         TBA            
                 Amenor  DEC    CANT     

                         BNE    SIGO    
                         STAA   MENOR    

                 FIN     BRA    FIN
La cuestión es: ¿cómo completamos lo marcado con signos de interrogación? El modo relativo de BLS (o de cualquier branch) utiliza un operando de 8 bits, por eso en el set de instrucciones vemos que dice rr:

Por tanto aunque aun no calculamos el operando, sabemos que es de 8 bits. En un post anterior sobre Direccionamiento relativo hemos visto cómo operan las instrucciones de salto (tal vez merezca un repaso). Como tenemos que calcular el salto a la etiqueta Amenor pero aun no la completamos en la tabla de símbolos, continuemos el ensamblado dejando el lugar para el operando que nos falta. Luego volveremos a ella. Dejaremos también pendientes los otros saltos relativos.


                         ORG    $C000
$C000  DE 01     main    LDX    DIRINI

$C002  A6 00             LDAA   0,X     
$C004  7A 00 03          DEC    CANT 
$C007  08        SIGO    INX       
$C008  E6 00             LDAB   0,X     
$C00A  11                CBA             

$C00B  23 ??             BLS    Amenor  
$C00D  17                TBA            
$C00E  7A 00 03  Amenor  DEC    CANT     

$C011  26 ??             BNE    SIGO    
$C013  97 00             STAA   MENOR    

$C015  20 ??     FIN     BRA    FIN
$C017
Aunque todavía nos faltan determinar tres operandos de los branch del ejercicio, ya podemos completar la tabla de símbolos:

identificador valor (hexadecimal)
VEC           0040
MENOR         0000
DIRINI        0001
CANT          0003 
VECTOR        0040 
main          C000
Amenor        C00E
SIGO          C007
FIN           C015
RESET         FFFE

¿Cómo calculamos el direccionamiento relativo de los saltos? Tal como vimos en el post sobre ese tema, lo que acompaña la instrucción de salto es el offset que se aplica al PC (claro está, cuando se cumple la condición del branch). Este offset puede ser positivo, lo que provocaría un salto hacia adelante, o negativo, que se traduce en un salto hacia atrás.
En el ejemplo tenemos los dos tipos de saltos. La línea BLS Amenor es un salto hacia adelante. Cuando CPU haya leído la instrucción completa (pero aun antes de ejecutarla) el PC tendrá el valor de la siguiente instruccion $C00D. La etiqueta Amenor corresponde a la dirección C00E -tal como apuntamos en la tabla de símbolos-. Haciendo la resta, C00E - C00D (donde queremos estar menos donde estamos) obtenemos el offset, en este caso 01.
Consideremos otro de los saltos: BNE SIGO. Este salto -de ejecutarse- es hacia atrás. Luego de leer la instrucción completa "BNE SIGO", el valor del PC es C013. La etiqueta SIGO segun la tabla de símbolos corresponde a la dirección C007. El offset lo obtenemos entonces restando donde queremos estar de donde estamos: C007-C013. Esta cuenta en hexadecimal nos da un valor negativo (podemos restar al revés y recordar aplicarle el signo negativo): -12 (en decimal). Nos queda expresar -12 en negativo como complemento a la base, lo cual equivale a 11110100 en binario, o F4 en hexadecimal.
Finalmente hay un salto en la última instrucción: FIN BRA FIN. En este caso el PC queda con el valor C017 y queremos que salte a C015, por tanto el offset es -2 (en decimal). Expresado en hexadecimal sería FE (negativo en complemento a la base).

Completemos el programa:


                         ORG    $C000
$C000  DE 01     main    LDX    DIRINI

$C002  A6 00             LDAA   0,X     
$C004  7A 00 03          DEC    CANT 
$C007  08        SIGO    INX       
$C008  E6 00             LDAB   0,X     
$C00A  11                CBA             

$C00B  23 01             BLS    Amenor  
$C00D  17                TBA            
$C00E  7A 00 03  Amenor  DEC    CANT     

$C011  26 F4             BNE    SIGO    
$C013  97 00             STAA   MENOR    

$C015  20 FE     FIN     BRA    FIN 
$C017
Note que la última dirección la anotamos para guiarnos en el cálculo del último offset, pero realmente no es parte de la respuesta a la consigna.
¿Qué les pareció el procedimiento? ¿Alguna duda?
En otro post les cuento el método más práctico -a mi criterio- para obtener rápidamente la representación en complemento a la base de un número.

lunes, 16 de mayo de 2016

Simulando programas: ejecución a lo mostaza (paso a paso)

Más de una vez alguien que entendía mucho más que uno nos muestra su programa (el código fuente) diciendo"bla, bla, bla, funciona perfecto, bla, bla", y ante la avalancha de conceptos nos quedamos... 

 

El gran problema es que aun cuando lo ejecutamos -o simulamos- las cosas pasan tan rápido que seguimos igual de perdidos que al principio. Algo parecido ocurre cuando creemos haber resuelto un ejercicio, lo ejecutamos... y nos llevamos la decepción de ver que no hace lo que queríamos. ¿Qué herramienta tenemos para paliar estas situaciones? La ejecución paso a paso. En el simulador THRsim podemos lograrlo de dos maneras:
La primera sería estableciendo puntos de interrupción (breakpoints) en el programa. Para ello cargamos el archivo del código fuente (o guardamos lo que hayamos hecho) y vamos al menú Breakpoint -> Set...


La ventana de diálogo que se presenta  nos permitirá indicar en qué punto del programa debe interrumpirse la ejecución. 


Atención! Si le indicamos que interrumpa la ejecución en la primer instrucción, no lo hará. En realidad lo tomará como que ya había sido interumpida... así que para que funcione debemos indicar una dirección del PC distinta de la inicial. En el ejemplo el programa inicia en el ORG $C000, por tanto fijaremos un breakpoint en la siguiente instrucción: $C003. Para determinar dónde queremos frenarlo, podemos guiarnos por el ensamblado, al que accedemos con menú File->Assemble (partiendo de la ventana del código fuente). En el ejercicio que voy a usar como ejemplo la ventana de ensamblado se ve así:


El programa que presento como ejemplo informa las unidades, decenas y centenas de un número entero sin signo de ocho bits. Fijé un breakpoint en PC==$C003. Luego iniciamos la ejecución como lo hacemos normalmente (menú Execute -> Run...) y frena en el punto que se muestra en la captura anterior. Notar que en rojo señala el punto en el que frenó la ejecución. En este punto podemos continuar la ejecución hasta el siguiente breakpoint, o ir "paso a paso" con menú Execute->Step (o usando la barra espaceadora o F7). Veamos cómo sería el paso a paso...

 
Ahora es más sencillo comprender el algoritmo empleado, verificando pacientemente una y otra vez el programa. En el ejemplo el número a desglosar es 123 (en hexadecimal 7B). 

La segunda es: ensamblar el programa (con el menú File -> Assemble o con la hot key Ctrl+A) y luego desde esa ventana usar la tecla F7 para ejecutar paso a paso. Si el programa es relativamente corto, este método es más conveniente.

Es posible crear puntos de interrupción para condiciones respecto a acumuladores, registros índice, y posiciones de memoria, entre otros.  
A propósito el fuente que usé es el siguiente:

* Dado un numero entero sin signo de 8 bits, informar:
* centenas, decenas y unidades en tres posiciones de memoria
         ORG    $0000
num    RMB    1    * el mister en cuestion
cen    RMB    1   
dec    RMB    1   
uni    RMB    1   

       ORG    $C000
       CLR    cen    * arranco poniendo todo en cero
       CLR    dec    * porque voy a incrementar cada uno
       LDAA   num
rec    SUBA   #100   * intento restarle 100...
       BLO    mpc    * si me pase, no hay mas centenas
       INC    cen    * sino, es que habia alguna centena mas
       BRA    rec
mpc    ADDA   #100   * como me pase, le vuelvo a sumar 100
red    SUBA   #10    * intento restarle 10...
       BLO    mpd    * si me pase, no hay mas decenas
       INC    dec    * sino, es que habia alguna decena mas
       BRA    red
mpd    ADDA   #10    * recupero los ultimos 10 que le reste
       STAA   uni    * quedaron las unidades
fin    BRA    fin


Me despido con el recuerdo del gran Mostaza... tipo sufrido si los hay (para dirigir Racing hay que ser sufrido)... en un gesto propio de alumno que ve cómo se hacia un ejercicio en la clase posterior al examen:

sábado, 14 de mayo de 2016

Simulando programas assembler con THRSim

Aprender programación mirando el pizarrón es como aprender cocina mirando la tele... todo muy lindo, pero la verdad está ahí afuera... cuando llega el momento de sentarse frente a la compu y ver si el profesor nos mintió como a niños, ¡ahí se pone complicado! Veamos entonces cómo simular un programa con THRSim.
Partiré de la premisa de que tenemos un programa para probar, en este caso uno bien sencillito: determinar el tipo de triángulo e informarlo con una letra. La validación de si realmente es un triángulo (verificable por las longitudes de los lados) la dejamos fuera.

El programa podría ser así:

       org     $0000
l1     rmb     1   
l2     rmb     1
l3     rmb     1
tipot  rmb     1    Guardo el ASCII del tipo de triangulo:
* I para isosceles; Q para equilatero; E para escaleno

       org     $c000
       ldaa    l1
       ldab    l2
       cba
       beq     igual    si l1==l2, es equilatero?
       cmpa     l3    caso contrario, todavia puede ser isosceles
       beq     isos
       cmpb    l3    si l2 y l3 son iguales, tambien es isosceles
       beq     isos
       ldab     #'E    son todos distintos, es escaleno   
       bra    listo
igual  cmpa     l3    si l1==l2 y l1==l3 son iguales...
       bne     isos    (sino es isosceles)
       ldab     #'Q    ... entonces es equilatero
       bra     listo
isos   ldab     #'I
listo  stab     tipot  todos confluyen aqui
fin    bra     fin



En este momento nos centraremos en simular el programa, así que la explicación del algoritmo la dejamos para otro momento. Tampoco es tan complicado... y además puse comentarios para que se pueda seguir.

La primera sección del programa, a partir de la directiva de ORG (origen) en 0000, indica que tenemos cuatro reservas de un byte: los lados del triángulo (l1, l2, l3) y el resultado de la determinación de su tipo (tipoT). Para simular el programa necesitamos primero cargar los valores de los lados. ¿Cómo se hace? Veamos distintas formas.

1. Cargando posiciones de memoria individuales.
Menu View -> Memory -> Memory List...
Al acceder a esa opción nos preguntará desde qué dirección de memoria deseamos listar:
Como sugiere la dirección $0000, que es justamente donde definimos la directiva de origen en cuestión, el valor por default es el que buscamos, le damos OK y presenta la memoria:
¿Cómo se completa ahora? Hacemos doble click en el valor actual ($ff indica en este caso un valor no inicializado) y se habilita el cursor para tipear los valores:

Pero... ¿no sería mucho más fácil que la ventana de Memory List nos muestre a qué reserva de memoria (RMB) corresponde cada dirección? Efectivamente, y puede hacerlo. Pero para ello debemos ensamblar el programa. Si solo queremos ensamblarlo, pero no ejecutarlo, vamos al menu "File -> Assemble".
Attenti! Primero hacer foco en la ventana del código fuente. Entonces menú File->Assemble:
Cuando hagamos esto se presentará la ventana LST con el ensamblado... si es que no tenemos errores! Ahora si abrimos nuevamente la ventana Memory List, veremos que se asignaron las etiquetas a las posiciones de memoria:
Esto hace más fácil la carga del juego de prueba.

2. Cargando posiciones de memoria estilo vector
Otra forma de cargar juegos de prueba es editar la memoria en una vista más cercana al concepto de vector, esto es, un valor al lado del otro. Menu View -> Memory -> Memory Dump...
Nuevamente preguntará a partir de qué dirección hacer el volcado (dump), y tal como antes, para este caso nos sirve el valor default ($0000). Se presenta una ventana donde la memoria aparece como renglones de 16 bytes:

Aquí no tenemos las etiquetas de las reservas. Podemos editar la memoria haciendo doble click (tal como antes). La parte derecha de la ventana (que en la captura muestra puntos) es la representación ASCII del valor de memoria. ¿Por qué muestra tantísimos puntos? Porque no todos los códigos ASCII son caracteres imprimibles. Muchos de ellos, particularmente los valores bajos, corresponden a caracteres de control (cosas tales como inicio de archivo, fin de archivo, retorno de línea, etc) que no tienen una representación visual.

3. Inicializando directo en el código
Aunque visualizar la memoria se hace necesario para verificar el resultado de la ejecución del programa, disponemos de una herramienta para cargar los juegos de prueba directamente en el fuente. Lo que haremos es usar la directiva db (define byte) en lugar de RMB (reserve memory byte). Modificamos el fuente:

       org     $0000
l1     db     5   
l2     db     6
l3     db     2
tipot  rmb     1    Guardo el ASCII del tipo de triangulo:
* I para isosceles; Q para equilatero; E para escaleno


De esta forma estamos incluyendo el juego de prueba directamente en el código. En este caso no representa cambio alguno en la forma de simularlo.

¿Qué diferencia tiene esta forma de simularlo con la carga directa en la memoria? Al usar DB/DW el juego de prueba se carga al momento de ensamblar el fuente. Si deseamos cambiar los valores de prueba, debemos volver a ensamblar. ¡No alcanza con reinicializar el PC y ejecutarlo de nuevo!

Se puede verificar que los valores están en memoria usando las opciones antes descritas (Memory view y memory dump) y en la ventana LST (habiendo ensamblado nuevamente) donde aparece como contenido de la memoria:


Llegado a este punto hemos cargado el juego de prueba. Lo que sigue es ejecutar el programa y verificar si determina correctamente el tipo de triángulo. Para ello desde la ventana de ensamblado (LST) o desde la ventana del fuente ejecutamos el programa :
* menu Execute -> Run
* tecla F9
* icono "play" en azul

El programa quedará en el bucle final, hay que detenerlo (el botón STOP, tecla F2 o Menu Execute->Stop).
Luego hacemos un Memory Dump... (tal como se explica anteriormente) y podemos verificar que el cuarto byte de la memoria (dirección 0003) tiene el ASCII E, correspondiente a Escaleno:


Repasemos lo visto aquí:
* uso de la ventana Memory list
* uso de la ventana Memory dump
* carga de variables directa en el codigo con define byte (db)
* ejecución del programa

¿Preguntas, comentarios?


Mientras practican con el simulador, escuchen El Gran Simulador...