domingo, 19 de enero de 2014

Ensamble de código para el μP 8086 I

La computadora Tandy TRS-80 CoCo construída en torno a un microprocesador considerado como uno de los más avanzados de su época fue la primera computadora de entretenimiento vendida masivamente en los establecimientos comerciales de la cadena de franquicias Radio Shack, y fue importante para la popularización del lenguaje BASIC en muchos aficionados entusiastas para quienes el poseer una máquina con capacidades similares que apenas hace una década tenía un costo en los cientos de miles de dólares con un lenguaje de programación cuya enseñanza estaba restringida a cursos formales impartidos en las universidades era casi un sueño hecho realidad. Tal computadora desde el momento en que era encendida empezaba a trabajar en un entorno de edición de texto para una versión del lenguaje BASIC conocida como COLOR BASIC, las aplicaciones en dicho lenguaje se tenían que elaborar independientemente por cada usuario, y si bien muchos la consideraban como un juguete, era un juguete avanzado en su momento que con sólo agregarle un cartucho optativo la podía convertir en una máquina lo suficientemente buena para aprendera programar en lenguaje ensamblador con el editor-ensamblador EDTASM.

El reinado de la computadora CoCo no duraría mucho, porque poco después de su aparición salió al mercado una computadora de uso personal casero respaldada por el prestigio del fabricante más poderoso de computadoras de alta potencia en su momento, la International Business Machines (IBM), la cual hizo su entrada con la computadora personal IBM PC XT, la cual sería reemplazada por una más poderosa, la computadora IBM PC AT. Interesantemente, aunque la IBM tenía los recursos en dinero y talento científico para fabricar masivamente sus propios microprocesadores, prefirió basar el diseño de sus máquinas en los procesadores que estaban siendo desarrollados por la empresa pionera en cuestión de microprocesadores, Intel. El impulso dado por IBM a Intel sería suficiente para que años después los procesadores fabricados por Intel comenzarían a ser lo suficientemente poderosos para empezar a arrebatarle a IBM su propia clientela en el extremo bajo de computadoras de capacidad mediana, hasta empezar a ponerla en jaque en la venta de sus máquinas de mayor capacidad (a principios del tercer milenio, casi todas las máquinas que estaban siendo fabricadas con procesadore de Intel igualaban o superaban las capacidades de cómputo de las máquinas IBM 360 e IBM 370). Como si no hubiera sido suficiente el haberle entregado a otra empresa el virtual monopolio en el diseño y la fabricación de microprocesadores para la fabricación de sus primeras computadoras caseras IBM PC XT e IBM PC AT, pese a contar con los mejores programadores de sistemas que se pudieran encontrar en la Unión Americana IBM prefirió contratar servicios externos para el diseño de un sistema operativo que se pudiera usar en las computadoras personales IBM, una empresa que empezaba a hacer un nombre para sí misma: Microsoft. Estas decisiones de entregarle a otros la fabricación de los componentes esenciales, en especial los circuitos integrados especializados, así como el desarrollo del software para el manejo de las máquinas, convirtió a IBM en una mera ensambladora de partes, lo cual facilitó la aparición de los famosos “clones” (máquinas baratas fabricadas en países con mano de obra barata, cien por ciento compatibles en hardware y software con las máquinas hechas por IBM) terminaría sacando a la IBM del mercado de las computadoras personales caseras y llevaría a la empresa a una crisis severa.

En lo que toca al desarrollo del software, no sólo la empresa Microsoft que fabricó el primer sistema operativo usado por las computadoras IBM (conocido como PC-DOS, el cual sería superado por su símil MS-DOS) resultó beneficiada. Hicieron su aparición otras empresas como Borland, la cual puso a la venta compiladores en los lenguajes Pascal y C. Apenas una década atrás, la construcción de un compilador era un asunto bastante oneroso, y si el compilador iba a ser usado a lo más por unos cuantos miles de programadores altamente especializados y bien pagados, sólo las empresas fuertes, las empresas gubernamentales con amplios presupuestos, y el conglomerado militar podían erogar las enormes cantidades de dinero para la construcción de compiladores. Al popularizarse las computadoras personales IBM compatibles, aunque la construcción de compiladores seguía siendo algo extremadamente caro los empresarios de Borland hicieron una apuesta arriesgada: si se ponía a la venta un compilador a precio muy bajo (con un costo no mayor de unos cien dólares, muchas veces vendiéndose a tan solo unos cincuenta dólares en comparación con el precio de un compilador equivalente de la empresa IBM para su serie de máquinas que se cotizaba en unos 50 mil ó 100 mil dólares), entonces aunque el costo de la construcción del compilador seguía siendo extraordinariamente alto la venta masiva de copias a un amplio mercado de usuarios caseros sería no sólo suficiente para amortiguar los costos en la elaboración del compilador, sino que se podría obtener una buena ganancia. La arriesgada apuesta de Borland rindió frutos, y por vez primera cientos de miles de personas fueron introducidas al sofisticado arte de programación de sistemas y elaboración de programas ejecutables de todo tipo. Es con este tipo de recursos que se crearon procesadores de palabras como WordStar (predecesora del procesador de palabras Word de Microsoft) o como la hoja de trabajo Lotus 1-2-3 (predecesora de la hoja de trabajo Excel elaborada por Microsoft).

Puesto que todo el diseño de las computadoras personales IBM y sus clones estaba basado en torno a los microprocesadores fabricados por Intel, a partir de los microprocesadores 8008, el 8086, el 80286 y el 80386, de cuyas arquitecturas y conjuntos de instrucciones derivan los microprocesadores Intel fabricados hoy en día, estamos interesados en obtener una familiarización con el conjunto de instrucciones básicos a nivel de lenguaje de máquina de los procesadores Intel, y la mejor manera de lograrlo es aprendiendo algo acerca de la programación en un lenguaje assembler de dichos procesadores. Microsoft desarrolló su propio ensamblador para tal propósito, el Microsoft Macro Assembler (MASM). Aunque el MASM en sí no era muy caro, una alternativa de bajo costo al MASM que usa la misma sintaxis para la elaboración de programas para el sistema operativo MS-DOS que a su vez precedió al sistema operativo basado en “ventanas” (los íconos gráficos puestos en la pantalla del monitor que a su vez simbolizan ventanitas en miniatura) precisamente el editor-ensamblador Turbo Assembler fabricado por la empresa Borland. Veremos aquí una introducción a la programación a nivel de lenguaje de máquina del procesador Intel 8086 con la ayuda del Turbo Assembler, con la tranquilidad de saber que cualquiera que haya aprendido a elaborar programas usando el MASM de Microsoft puede empezar a trabajar con el Turbo Assember de Borland sin mayor problema (y viceversa). Primero que nada, antes de tratar de programar en assembler los procesadores Intel, tenemos que obtener una familiaridad con los recursos esenciales de hardware. En el caso del 8086, los registros para fines de programación con los que se cuenta son los siguientes:




Estos mismos registros están disponibles en los modelos posteriores que identificaremos genéricamente como 80x86. Como puede verse, todos los registros tiene una capacidad de 16 bits. Si el lector se ha tomado el tiempo en llevar a cabo una lectura de las entradas previas, descubrirá una semejanza en el diseño de los procesadores fabricados por Intel y los procesadores fabricados por Motorola y RCA (no en los detalles finos, pero sí en los conceptos generales). Por principio de cuentas, tenemos el concepto de las banderas (flags), flip-flops independientes que son puestos en “1” (encendido) o en “0” (apagado) dependiendo del resultado de alguna operación aritmética previa o alguna comparación que se llevó a cabo. Esta información está contenida en el registro “Banderas de Status” (Status Flags). Si el lector intuye que el bit B (hexadecimal) que contiene la bandera “OF” representa el estado de un sobreflujo (overflow) si se ha obtenido un valor positivo demasiado grande o un valor negativo demasiado pequeño que está fuera de las capacidades de almacenamiento del registro de 16 bits, estará en lo correcto. Y si intuye que el bit 6 que contiene la bandera “ZF” representa un estado que depende de que se haya obtenido un resultado igual a puros ceros (zero flag), también estará en lo correcto. Por su parte, el bit “0” que contiene la bandera “CF” representa el resultado de un “llevar” (carry flag) obtenido en la operación aritmética llevada a cabo por la instrucción previa.

Como puede verse en la figura anterior, los registros del 8086 están agrupados en cinco categorías:

1) Registros de propósito general (AX, BX, CX, DX).

2) Registros de índice y de puntero ó apuntador (SI, DI, SP, BP).

3) Registros de segmento (CS, DS, SS, ES).

4) Puntero o apuntador de instrucción (IP).

5) Banderas (OF, DF, IF, TF, SF, ZF, AF, PF, CF)

Los cuatro registros de propósito general están subdivididos a su vez en mitades alta (high) y baja (low). El registro de 16 bits AX está compuesto de dos partes de 8 bits, AH y AL; el registro BX está subdividido en BH y BL; el registro cx está subdividido en CH y CL, y el registro DX está subdividido en DH y DL. Este arreglo flexible nos permite operar directamente en la amplitud de 16 bits de un registro o trabajar por separado en cualquiera de las dos mitades de un registro.

Sin intenciones de que sea un programa completo listo para ser ensamblado, lo siguiente nos ilustra la estructura de un programa elaborado en Turbo Assembler para el procesador 8086:




Podemos apreciar a primera vista que el programa fuente está subdividido en cuatro columnas, en cuatro campos, la primera columna es una columna usada para poner las etiquetas (el campo de comandos), la segunda columna es una columna usada para poner las mnemónicas que representan los códigos de operación (op codes) identificada como el campo de comandos, la tercera columna es para poner los operandos (campo de operandos), y la cuarta columna es utilizada para poner comentarios. Para quienes leyeron las entradas tituladas “El ensamblador EDTASM para el μP 6809”, la forma general usada para escribir programas destinados a ser ensamblados el Turbo Assembler para el procesador 8086 es esencialmente la misma que la usada para el procesador 6809. De hecho, la gran mayoría de los ensambladores en uso actual tienden a adherirse a esta convención aunque nadie la haya decretado excepto el uso común.

En los programas assembler que hemos visto en las entradas previas, nos hemos acostumbrado a que haya números de línea identificando cada línea de un programa escrito en assembler, lo cual resulta muy conveniente a la hora de tratar de documentar el propósito de un programa en el camino de purgarlo de errores de programación. Y de hecho, el editor-ensamblador Turbo Assembler va poniendo números de línea en una quinta columna puesta en el extremo izquierdo, el campo de números de línea. A continuación se presenta un programa completo escrito en Turbo Assembler que puede ser ensamblado en cualquier máquina que tenga instalado el Turbo Assember (obsérvese que podemos meter líneas en blanco, las cuales no son tomadas en cuenta al llevarse a cabo el ensamblaje del programa, pero pueden aumentar la legibilidad de un programa separándolo en secciones conceptuales):




Una vez ensamblado este programa, ya convertido a código ejecutable en lenguaje de máquina, si se ejecuta en una computadora que aún tenga un sistema operativo MS-DOS instalado en ella, hará que la computadora envíe directamente una orden a la impresora haciendo que se avance el papel perforado hacia una página nueva (esta acción es lo que se conoce como una “alimentación de forma” o form feed).

Veamos si podemos entender el funcionamiento del programa anterior.

Primero que nada, tenemos dentro del programa el número 21h. ¿Y qué significa la “h” puesta al lado derecho del número 21 formando parte del mismo? Significa que se trata de un número hexadecimal. En los programas en lenguaje de ensamblador, podemos representar los números en sistema hexadecimal, en sistema binario o en sistema decimal. Puesto que los tres sistemas numéricos comparten los mismos símbolos, tenemos que decirle al ensamblador qué base numérica estamos utilizando. Para este propósito, añadimos b a la derecha del número para indicar que se trata de un número binario, y añadimos h para indicar que se trata de un número hexadecimal. Podemos agregarle una d o nada en caso de que se trate de un número decimal. De este modo, los siguientes números representan al mismo número pero en bases diferentes:

      0100111101011100b   →  Binario

      04F5Ch   →  Hexadecimal

      20316   →  Decimal

      20316d   →  Decimal

La primera línea que empieza con %TITLE es optativa y describe el propósito del programa, es algo así como el título de un libro, causando que el texto puesto entre comillas se imprima en el tope de cada página que sea impresa al pedir un listado del programa.

Se ha destacado en color azul lo que se conoce como la cabecera del programa assembler en Turbo Debugger.

La directiva IDEAL puesta en la línea 3 (destacada en color azul) es para hacer el cambio a lo que se llama el modo “Ideal” del Turbo Assembler, la línea 3 es dejada fuera si el programa va a ser ensamblado por el MASM de Microsoft. Tras esto se tiene la directiva MODEL (destacada en color azul) puesta en la línea 5, precedida opcionalmente por DOSSEG. Esta directiva selecciona uno de varios modelos de memoria, la mayoría de los cuales son usados solo cuando se combina el lenguaje de ensamble con algún lenguaje de alto nivel como Pascal o como C. En programación de ensamble que se lleva a cabo por sí sola, el modelo de memoria small (pequeño) es usualmente la mejor opción, pero no hay que dejarse engañar por el nombre, porque dicho modelo de memoria nos permite hasta 64 Kbytes de código más otros 64K para datos para un tamaño total máximo para el programa de 128 Kbytes, lo cual es casi un horizonte sin límites en el mundo eficiente de uso de memoria del código elaborado en lenguaje de máquina. La directiva STACK (destacada en color azul) reserva espacio para la pila que será usada por el programa, una área de memoria que almacena dos tipos de datos: (1) valores temporalmente almacenados por o pasados a subrutinas, y (2) los domicilios a los cuales las subrutinas regresan el control. El valor puesto después de la directiva STACK le dice al Turbo Assembler cuántos bytes debe reservar para el segmento de la pila. Muchos programas requieren sólo una pila pequeña, y aún los programas más grandes rara vez requieren más de 8K bytes.

Después de la cabecera del programa vienen las declaraciones de constantes y variables. En el lenguaje de ensambladores, los valores constantes son conocidos como igualaciones, en referencia con la directiva EQU usada para estas igualaciones.

Tras la cabecera y las igualaciones, viene lo que constituye el cuerpo del programa, y damos aviso de esto al editor-ensamblador con la directiva CODESEG, tras la cual podemos empezar a escribir el código fuente que será ensamblado usando los cuatro campos (columnas) destinados para tal efecto. En la primera columna se han puesto dos etiquetas, Start y Exit.




Antes de proseguir, tenemos que plantearnos con seriedad la siguiente pregunta: ¿Estamos perdiendo el tiempo al estudiar los conjuntos de instrucciones de procesadores Intel de 8 bits y 16 bits que al inicio del tercer milenio han pasado a la obsolescencia al hacer su aparición los procesadores de 64 bits? En rigor de verdad, no, porque todo lo que aprendamos de los conjuntos básicos de instrucciones ha sido incorporado con una fidelidad casi religiosa en cada nuevo procesador que ha estado apareciendo por una virtud impuesta por las condiciones del mercado llamada backward compatibility (compatibilidad hacia atrás). Al hacer su aparición las computadoras IBM y sus clones compatibles, una cosa que esperaban y exigían los usuarios era que el dinero que invertían en sus programas de aplicaciones tales como WordStar o Harvard Graphics fuera un gasto de una sola ocasión, esperando que sus programas se pudieran instalar y correr en las máquinas más avanzadas sin necesidad de tener que volver a gastar dinero para comprar versiones actualizadas de dichos programas para poder correrlos en las nuevas máquinas. Esto significa que, al hacer su aparición el procesador Intel 80286 que reemplazó al procesador Intel 8086, todos los programas de aplicaciones que se podían correr en el 8086 se podían correr también en máquinas que usaran el nuevo y más avanzado 80286. Y cuando hizo su aparición el procesador 80386, todos los programas de aplicaciones que se podían correr en el 80286 se podían correr también en máquinas que usaran el nuevo y más avanzado 80386, lo cual implicaba ipso facto que los programas que corrían en las máquinas construídas con el procesador 8086 podrían ser instalados y correr en máquinas construídas con el procesador 80386. En pocas palabras, el procesador 80386 incorporó las instrucciones en lenguaje de máquina que se podían ejecutar en el procesador 80286, el cual a su vez incorporó las instrucciones que se podían ejecutar en el procesador 8086. Así han ido creciendo las familias de procesadores que manejan el conjunto de instrucciones x86. Es importante destacar que son compatibles en forma ascendente únicamente en lo que al conjunto de instrucciones en lenguaje de máquina se refiere. En lo que toca al hardware en sí, los circuitos integrados así como la distribución de pins son completamente diferentes; no podemos sacar de una tablilla de circuito impreso un procesador 80386 y esperar poder meterlo en un “socket” de procesador 8086 porque por principio de cuentas un 80386 tiene muchas más terminales que un 8086.

Echando un vistazo a los listados de los conjuntos de instrucciones de la gran familia x86 así como a la estructura general de los lenguajes ensambladores de la familia x86, resulta evidente la forma en la que han ido creciendo los conjuntos de instrucciones. Pero si cada nuevo procesador Intel tiene un conjunto de instrucciones que incorpora el conjunto de instrucciones del predecesor, podemos preguntarnos entonces, ¿en dónde está la mejora?

La mejora se refleja en las nuevas versiones de los programas de aplicaciones que de manera muy específica imponen un mínimo de requisitos de hardware. Si una nueva versión de Mathematica requiere de un procesador más avanzado como el Itanium de Intel, entonces durante el proceso de instalación del programa se checará si la máquina cumple con los requisitos mínimos que se piden al hardware, y si no los cumple la máquina el proceso de instalación será detenido dejándolo trunco sin haberse instalado nada en el disco duro de la máquina.

Una forma de elaborar un programa de aplicación que pueda ejecutarse en varios procesadores consiste en escribir dos o más versiones del mismo programa, por ejemplo una versión elaborada para los procesadores de 16 bits y otra versión elaborada para procesadores de 32 bits. Al escribir un programa de aplicación, podemos hacerlo de tal manera, y durante el proceso de instalación se puede checar el tipo de procesador que tiene la máquina, instalándose la versión que le corresponda. Sin embargo, esta práctica que era válida en otros tiempos ha estado siendo descontinuada por los fabricantes de programas de aplicaciones por el hecho de que, aunque se pueda hacer tal cosa, es dudoso que un sistema operativo mantenga su compatibilidad hacia atrás en lo que al software respecta por razones de fondo. El viejo sistema operativo DOS basado en una pantalla para entradas en líneas de comandos (al estilo UNIX) fue desechado para introducir sistemas operativos basados en las más intuitivas “ventanas” (nombre dado a cada ícono puesto en la pantalla con el que se puede echar a andar una aplicación sin necesidad de tener conocimientos en ventanas de líneas de comandos). No hay manera en la cual se pueda ejecutar un programa elaborado originalmente para un entorno gráfico en una máquina que opera en un entorno basado en caracteres de texto. Inclusive en un entorno gráfico, la incorporación de las nuevas instrucciones en lenguaje de máquina que vayan saliendo en los nuevos procesadores a los sistemas operativos actualizados implicará que habrá programas que no “sabrán” como interpretar las nuevas instrucciones. Por otro lado, los programadores preferirán recurrir a instrucciones en lenguaje de máquina capaces de poder manejar espacios de 32 bits que a instrucciones que solo son capaces de poder manejar espacios de 16 bits por la simple razón de que los espacios más amplios pueden aumentar considerablemente la velocidad de la máquina.

En la primera década del tercer milenio, la arquitectura mínima de facto era la que estaba basada en procesadores de 64 bits. Obsérvese que a partir del tercer milenio la palabra microprocesador empieza a ser obsoleta, usándose en su lugar la palabra procesador (sin el “micro” como prefijo). En la década de los setentas era común referirse a los procesadores CPU construídos en chips de circuitos integrados como microprocesadores, tomando en cuenta que los primeros conjuntos de instrucciones de los procesadores de 8 bits era bastante limitado, además de que todo el procesador CPU cabía en un paquetito de plástico que a su vez cabía en una mano, de modo tal que el prefijo “micro” estaba plenamente justificado. Pero aunque los procesadores actuales siguen cabiendo en una mano, al menos en lo que a su capacidad de cómputo se refiere estamos hablando de algo que supera el poder de las computadoras de la Estación Espacial Internacional. En relación con el tema tratado en esta entrada, la herramienta de Microsoft para facilitar un entorno de elaboración de programas en lenguaje ensamblador, el MASM en su versión 8.0 que fue liberado junto con el paquete Visual Studio 2005, ésta ya proporcionaba soporte para los procesadores de 64 bits, lo cual significa que da apoyo al conjunto de instrucciones x86-64 el cual a su vez apoya en el hardware el uso de registros de 64-bits de capacidad (la especificación original fue creada por AMD, el principal competidor de Intel, y es compatible “hacia atrás” con código elaborado para programas de 16 bits y 32 bits; el procesador genérico AMD-K8 cuyo miembro representativo es el Athlon 64 fué el primero que implementó esta arquitectura).