lunes, 19 de noviembre de 2007

Suplemento # 4b: Programación del microprocesador




La construcción del microprocesador a través de la integración de circuitos en gran escala, así como la construcción de las unidades periféricas de apoyo, así como la construcción de memorias RAM, de discos duros y demás accesorios que constituyen lo que se comúnmente se conoce como el hardware de la máquina, es tan sólo una parte importante pero no suficiente para poder utilizar un microprocesador ventajosamente. No basta con tener una microcomputadora construída con todos sus componentes esenciales. Queda aún la tarea de poder programarla. Todos los programas que serán ejecutados por el microprocesador eventualmente deben ser cargados en la memoria RAM. Pero estos programas deben ser programas que puedan ser efectuados en el lenguaje que entiende la máquina, en su lenguaje de máquina, en su lenguaje de unos y ceros, en el lenguaje binario.

Podemos, si queremos, construír un sistema microprocesador completo, y podemos intentar programarlo en lenguaje de máquina, en el lenguaje de unos y ceros que puede ser procesado de alguna manera por las tres funciones lógicas básicas. Pero tal cosa se puede convertir en una labor extremadamente pesada. A manera de ejemplo, supóngase que queremos utilizar una microcomputadora para efectuar una adición sencilla, para sumar los números 17 y 35. Normalmente, no utilizaríamos una microcomputadora para algo tan sencillo, nosotros mismos haríamos el cálculo manualmente o inclusive mentalmente. Sin embargo, este crudo ejemplo ilustra los enormes problemas que tenemos que enfrentar para poder utilizar una microcomputadora inclusive para las funciones más elementales. Y debe quedar claro que mientras esto no se haya resuelto, no podremos utilizar el microprocesador para cosas más sofisticadas como un procesador de palabras o como instrumento para ejecutar melodías en formato MP3.

Los números 17 y 35 a ser sumados están en nuestro lenguaje natural, en nuestro sistema decimal. Pero la microcomputadora no entiende absolutamente nada de estas cosas, la microcomputadora sólo "comprende" el lenguaje de los unos y ceros eléctricos, la presencia o la ausencia de un voltaje. Para que podamos obtener la suma de la microcomputadora, primero se vuelve necesario convertir cada uno de los números 17 y 35 al sistema de numeración binaria, precisamente el mismo sistema que estudiamos al principio de esta obra. Nosotros mismos llevaremos a cabo esta conversión, postponiendo por lo pronto la posibilidad de dejarle a la máquina la tarea de que ella misma lleve a cabo la conversión de decimal a binario por nosotros.

Una vez hecha la conversión, tenemos que almacenar estos números binarios en alguna parte de la memoria RAM en donde la microcomputadora los pueda localizar. Si lo hacemos manualmente usando interruptores eléctricos, ello nos llevará algo de tiempo aunque tal cosa se puede hacer.

Pero almacenar los números binarios en la memoria RAM no basta. Tenemos que escribir un programa en lenguaje de máquina que, ejecutando una instrucción tras otra, pueda llevar a cabo las operaciones que produzcan el resultado apetecido. Es aquí en donde necesitamos tener acceso al código de operaciones (op-code) del microprocesador. Desde que los primeros microprocesadores aparecieron en el mercado, cada uno de ellos tiene su propio código de operaciones, y todos ellos lo seguirán teniendo mientras la arquitectura esencical en la cual están basados todas los microcomputadoras (conocida como la arquitectura von Neumann) no cambie.

Usando lenguaje de máquina, un programa tentativo podría ser el siguiente:

(1) Recoger del domicilio A00019 de la memoria RAM el primer dato (el número binario 17) y depositarlo en el registro interno X del microprocesador.

(2) Recoger del domicilio A00020 de la memoria RAM el segundo dato (el número binario 35) y almacenarlo en el registro interno Y del microprocesador.

(3) Sumar mediante una suma binaria en la unidad ALU del microprocesador las palabras binarias almacenadas en los registros X y Y del microprocesador, y depositar el resultado (el equivalente binario del número decimal 52) en el registro interno X del microprocesador.

(4) Sacar la información del registro interno X del microprocesador y depositarlo en el domicilio A00019 de la memoria RAM.

(5) Detener la ejecución del programa, una vez que el resultado ha sido puesto en la memoria RAM.

Obsérvese que, en el interior del microprocesador, al llevar a cabo una suma binaria, destruímos uno de los datos originales, el número binario 17 que estaba en el registro X, depositando en dicho registro el resultado final 52. Y al sacar el resultado hacia afuera, depositándolo en la memoria RAM, destruímos también allí el dato original que había en el domicilio A00019. Esto no es de consecuencia alguna, ya que a fin de cuentas estamos interesados en la obtención de un resultado final que una vez obtenido no requiere conservar los datos originales. Esto es algo muy frecuente en el mundo de la programación. Podemos, si queremos, conservar todos los datos originales. Podríamos haber depositado el resultado final en el domicilio A00021 de la memoria RAM, un domicilio que anteriormente no estaba siendo utilizado. Pero esto de inmediato nos exije tener una memoria RAM de mayor capacidad. En procesamientos más complejos en donde se manejan decenas de miles de datos, el tratar de conservar en la memoria RAM información no indispensable nos puede agotar rápidamente nuestros recursos de memoria rápida. Es por ello que, cuando se requiere conservar alguna información previa, se hace uso de una memoria no tan rápida como la memoria RAM pero sí más permanente, es entonces cuando se hace uso de algo como el disco duro de la máquina. En cualquier microcomputadora o computadora de escritorio o inclusive las super-computadoras más sofisticadas, el usuario siempre verá una interacción intensa entre la memoria RAM disponible y el espacio que hay en el disco duro. El disco duro, con las tecnologías disponibles a la entrada del tercer milenio, siempre ofrece una capacidad mayor y más duradera que la que ofrece la memoria RAM. Pero el disco duro es una memoria extremadamente lenta, en términos de rapidez desde el punto de vista de la máquina. Siempre habrá un compromiso entre el uso de la memoria RAM y el espacio disponible en el disco duro, lo cual en sistemas elaborados requerirá la mirada vigilante del encargado de optimizar el funcionamiento de los sistemas de cómputo que tiene bajo su cuidado; esta es precisamente la tarea del administrador de sistemas.

Obsérvese que en este ejemplo los números binarios 17 y 35 no fueron colocados en los primeros domicilios de la memoria RAM, o sea en los domicilios A00000 y A00001. Esto se debe a que precisamente en estos domicilios es en donde tenemos que depositar las instrucciones del programa que se irá ejecutando en lenguaje de máquina sobre los números que irán a ser sumados. Así, al arrancar con sus operaciones, el microprocesador irá de inmediato al primer domicilio en donde encontrará la primera instrucción a ser ejecutada así como el domicilio de donde deberá tomar el dato binario a ser procesado. El formato básico será el siguiente:


Op-Code - Operando


Si la memoria RAM tiene capacidad para almacenar en cada domicilio una palabra binaria de 15 bits de capacidad, podemos asignar los primeros tres bits de cada palabra para poner allí la instrucción (en lenguaje de máquina) que deberá ser ejecutada, y los siguientes doce bits para poner allí el operando (domicilios de memoria RAM, etc.) sobre el cual se actuará. Esto ya de por sí nos está diciendo que, dentro del microprocesador, además de la unidad ALU, además de los registros internos de uso temporal, además del Contador de Programa, tenemos que tener un decodificador interno que pueda convertir los tres bits iniciales en ocho instrucciones diferentes en lenguaje de máquina, algo como lo que se muestra en el siguiente dibujo:





En el esquema mostrado, podemos ver que cada microinstrucción interna al microprocesador tiene un propósito diferente. Por ejemplo, la microinstrucción 000 denotada como:

LOAD M, R0

indica al microprocesador que hay que tomar un dato de la memoria RAM y depositar (o mejor dicho, cargar, que en inglés se traduce como LOAD) dicho dato en el registro interno R0 del microprocesador. ¿Y de qué domicilio M de la memoria vamos a tomar el dato? Pues precisamente del domicilio especificado por el operando de diez bits, que en este caso será algo como el domicilio 000000000019 (en donde está depositado el número binario 17). Del mismo modo, la microinstrucción 001 denotada como:

LOAD M, R1

indica al microprocesador que hay que tomar otro dato de la memoria RAM y cargar dicho dato en el registro interno R1 del microprocesador.

Por otro lado, la microinstrucción 100 denotada como:

ADD R0, R1

le indica al microprocesador que debe sumar dentro de su unidad ALU el contenido de los registros internos R0 y R1, depositando el resultado en el registro R0.

Por último, la microinstrucción 010 denotada como:

STORE R0, M

le indica al microprocesador que deposite en la memoria RAM el contenido del registro interno R0 del microprocesador, o sea el resultado previo obtenido de la suma de los registros internos R0 y R1. ¿Y en qué domicilio M de la memoria vamos a depositar el dato? Pues nuevamente en el domicilio especificado por el operando de diez bits. Para nuestros propósitos, estas son todas las instrucciones que necesitamos para poder llevar a cabo nuestra suma binaria de los números 17 y 35.

Hay otra microinstrucción de particular interés en el conjunto del Código de Operaciones de este microprocesador. Es la microinstrucción 110 denotada como:

BRA T

la cual indica que se lleve a cabo un salto incondicional hacia el domicilio de la memoria RAM identificado con la letra T, el cual puede ser ya cualquier domicilio de la memoria RAM y no uno que venía de una secuencia en orden. ¿Y qué sucedería si dentro de cierto domicilio de la memoria RAM hubiese una instrucción especificando un salto hacia el mismo domicilio de la memoria? Pues en tal caso, el microprocesador entraría en lo que se conoce como un bucle perpetuo (infinite loop). Estaría saltando todo el tiempo hacia un domicilio de la memoria RAM que le indica un salto incondicional hacia el mismo domicilio en donde se encuentra otra vez con la misma microinstrucción indicándole un salto hacia el mismo domicilio, repitiéndose la secuencia de modo perpetuo.

El ejemplo crudo que acabamos de ver nos demuestra que, antes de que podamos utilizar ventajosamente algo tan sofisticado como un microprocesador, tenemos que invertir una buena cantidad de tiempo en elaborar programas que puedan convertir nuestro lenguaje humano a un lenguaje de máquina que el microprocesador sea capaz de "comprender", y que puedan llevar a cabo también el proceso inverso para que la microcomputadora nos pueda entregar resultados que nosotros seamos capaces de comprender. Estos programas que son capaces de convertir nuestro lenguaje humano a lenguaje de máquina y el lenguaje de máquina a lenguaje humano, así como los programas ejecutables en sí con los cuales la intención primaria se pueda llevar a cabo (operaciones aritméticas, procesadores de palabras, programas de diseño gráfico, etc.) es lo que se conoce comunmente como el software, y requiere la inversión de cientos de miles de horas-hombre para su elaboración. En programas tan sofisticados como Linux y Windows, se han invertido decenas de millones de horas-hombre, literalmente hablando, para poder tener lo que tenemos en la actualidad. Por sí sólo, el microprocesador es un robot "idiota" incapaz de hacer nada. Son los programas los que realmente le pueden sacar amplia ventaja a lo que las maravillas de la microelectrónica nos pueden ofrecer.

El conjunto de ocho microinstrucciones vistas en el esquema del ejemplo anterior es un conjunto extremadamente limitado de instrucciones. Aunque se puede construír una computadora funcional con un conjunto de instrucciones tan limitado, tal esquema sería muy ineficiente para lo que estamos acostumbrados en la actualidad, y en la práctica se desea tener un conjunto mayor de instrucciones disponibles. Y esto es precisamente lo que se ha venido haciendo conforme ha ido evolucionando el microprocesador. Al ir aumentando la densidad de los circuitos integrados, se ha ido ampliando el conjunto de instrucciones disponibles en lenguaje de máquina.

Habiendo agregado ya un decodificador interno de instrucciones al microprocesador de la manera en la que se ha señalado arriba, tenemos ya un esquema completo de lo que cualquier microprocesador (o para el caso general, cualquier computadora) debe tener en su interior:




Como puede verse, todo microprocesador incluye en su interior una Unidad de Lógica Aritmética ALU, varios registros internos que pueden ser implementados con flip-flops o con una memoria interna RAM como se muestra arriba, un registro de instrucciones, el decodificador de las instrucciones, y el Contador del Programa.

Un buen programa ensamblador no solo debe poder convertir nuestro programa fuente o código fuente en instrucciones en lenguaje de máquina que el microprocesador pueda ejecutar, paso a paso, instrucción por instrucción. También nos debe poder dar la capacidad de poder correr un programa en lenguaje de máquina no "de corrido" de principio a fin ejecutando todas las instrucciones de un golpe continuo sino ejecutando cada instrucción deteniéndose al finalizar la ejecución de cada instrucción, dándonos la capacidad de poder "ver" todo lo que está contenido dentro de cada uno de los registros internos del microprocesador, así como el resultado de la ejecución de cada instrucción. Para esto, existen programas emuladores en el mercado en los cuales el programa se escribe en lenguaje ensamblador y se corre en una computadora de escritorio que tenga un programa emulador cargado y trabajando. Se les llama programas emuladores porque debido a la sofisticación de los mismos no es realmente el microprocesador el que está llevando a cabo la ejecución directa de un programa escrito en lenguaje ensamblador sino es un programa mucho más sofisticado que emula las acciones del microprocesador. Es como si el microprocesador estuviera ejecutando las instrucciones directamente aunque de hecho está ejecutando una cantidad mucho mayor de instrucciones que emulan una cosa más rudimentaria que debería estar sucediendo a su nivel. Un buen ejemplo de un programa emulador al momento de escribirse este Suplemento # 4 es el programa 8086 Microprocessor Emulator, el cual en su versión de demostración gratuita se puede descargar de Internet del siguiente domicilio:

http://www.emu8086.com/

Este emulador es un programa que puede correr en cualquier computadora que tenga el sistema operativo Windows instalado en ella. Como su nombre lo indica, este emulador emula el comportamiento del microprocesador Intel 8086, el pionero de los microprocesadores. A continuación se muestra una de las "ventanas" propias del emulador:




Del lado izquierdo de la ventana, podemos ver, en un momento dado de la ejecución de un programa en lenguaje de máquina que está siendo emulado, los contenidos de los registros del microprocesador Intel 8086. Podemos ver que este microprocesador 8086 contiene cuatro registros de propósito general: el registro AX, el registro BX, el registro CX y el registro DX. El registro AX que vemos en la "ventana" contiene el número hexadecimal (en sistema numérico base 16) 0003, y lo contiene en dos bytes separados, un byte alto (identificado como H, del inglés "High") y un byte bajo (identificado como L, del inglés "Low"). Juntos, el byte alto H y el byte bajo L de cada registro permiten almacenar un número binario de 16 bits en lo que parece ser un solo registro "virtual" como el registro A o el registro D. Además de este registro, podemos ver la presencia del registro IP, acrónimo del inglés Instruction Pointer. En realidad, este registro es el mismo que anteriormente hemos denominado como el Contador de Programa, el cual es complementado con el registro CS para aumentar su capacidad.

En la "ventana" en el extremo derecho del emulador, tenemos las instrucciones escritas en lenguaje ensamblador con notación mnemónica que nos es más humana, más familiar, que el críptico lenguaje de máquina de unos y ceros, el cual nos permite utilizar instrucciones como JMP (acrónimo del inglés JUMP, el salto incondicional que ya se vió anteriormente) o como INC (acrónimo del inglés INCREMENT) utilizado para incrementar el operando en una unidad. Así, la instrucción en assembler:

JMP 011Eh

significa "saltar al domicilio 011E hexadecimal" (la letra h puesta al final del número deja en claro que se trata de un número hexadecimal), mientras que la instrucción:

INC DH

se lee como "incrementar los contenidos del registro DH en una unidad".

Sin lugar a dudas, escribir un programa así sea elemental en lenguaje ensamblador resulta mucho más ameno que escribirlo en lenguaje de máquina. Pero aunque al principio escribir programas computacionales en lenguaje ensamblador pueda resultar divertido, llega un momento en el que la extensión de dichos programas puede convertirse en algo exasperante. Escribir un programa muy grande siempre tiene la enorme desventaja de que cualquier omisión o equivocación cometida por el programador en una sola línea entre cientos de miles de líneas de código puede hacer que la ejecución del programa se estrelle, produciendo lo que en el argot técnico se conoce como un computer crash. Casi todos los usuarios han experimentado algún problema de este tipo en el cual debido a una situación o combinación de instrucciones no anticipada por los programadores la computadora se "cuelga" y es necesario apagarla para volver a inicializar de nuevo todo el sistema. En este respecto, los programas computacionales son muy poco tolerantes, no admiten ni un solo error humano. El menor error humano en la elaboración de un programa complicado puede salir a flote desagradablemente en lo que comúnmente se denomina como una "falla insecto" o computer bug. Esta es la razón por la cual sistemas operativos complejos como Windows XP, Linux o Windows Vista elaborados sobre millones de líneas de código constantemente tienen que ser actualizados con parches o (patches), debido a situaciones no anticipadas por los programadores que son descubiertas por los usuarios en el campo.

Aunque la creación de los lenguajes ensambladores representa un gran paso evolutivo hacia la reducción de errores de programación, elaborar un programa largo en lenguaje ensamblador puede ser un proceso tardado y costoso. A modo de ejemplo, el procedimiento de sumar dos números enteros "largos" y almacenar el resultado en un tercer entero largo requiere únicamente de una sola intrucción en el lenguaje de alto nivel C:

i = j + k;

pero en un microprocesador como el 8086 esto mismo requiere de seis instrucciones en lenguaje de máquina a través de un programa ensamblador:

...
mov ax,[j]
mov dx,[j+2]
add ax,[k]
adc dx,[k+2]
mov [i],ax
mov [i+2],dx
...
Obviamente, resulta más fácil escribir una sola línea de código en lenguaje C que seis líneas de código en lenguaje ensamblador. Y cuando se trata de elaborar un programa que requiere cientos de líneas de código en lenguaje C, elaborar en lenguaje ensamblador un programa que haga lo mismo posiblemente requerirá miles de líneas de código. Entonces, ¿para qué programar en lenguaje ensamblador, si es más difícil que cualquier otro lenguaje de mayor nivel? Una razón para ello es que la programación en lenguaje ensamblador nos permite tener acceso directo a cualquier parte de la memoria RAM y controlar directamente hasta sus más íntimos detalles la entrada y salida de cualquier unidad periférica conectada al microprocesador. Además, el lenguaje ensamblador es el lenguaje nativo de la máquina, un programa bien escrito en lenguaje ensamblador será el programa más veloz de todos los programas posibles. Sin embargo, la necesidad de mantener bajo control los costos involucrados en un proyecto para la elaboración de un programa complicado prácticamente exige que se recurre directamente a la programación en lenguaje de máquina sólo en aquellos casos en los que sea absolutamente necesario interactuar con el "hardware" de la máquina (los programas drivers proporcionados en un disco CD que requieren ser instalados para que la máquina y el sistema operativo que está corriendo en ella puedan reconocer y utilizar cierta impresora láser ó alguna novedosa cámara digital son ejemplo de ello).

Precisamente por lo arriba señalado, se vuelve deseable tener lenguajes más "compactos" y más parecidos a nuestro lenguaje humano, en los cuales el programa fuente elaborado por el programador contenga la menor cantidad posible de líneas de código. Esta es la razón por la cual de los programas ensambladores se vió la necesidad de tener que evolucionar a lenguajes de alto nivel, lenguajes como BASIC. Con un lenguaje como BASIC, en vez de tener que escribir algo como lo que arriba fue señalado para la suma de los números 17 y 35, escribiríamos algo como lo siguiente:
10_A=17
20_B=35
30_LET C=A+B
40_PRINT C
50_END
Esto es indudablemente mucho más "humano" y mucho más ameno para la búsqueda de posibles errores dentro del mismo programa que lo anterior que teníamos escrito en lenguaje de máquina o lenguaje ensamblador. Traducido a lenguaje cotidiano, lo anterior nos está diciendo, línea por línea, lo siguiente:

Línea 10 → "Asígnese a una localidad A de la memoria RAM el número entero 17"
Línea 20 → "Asígnese a una localidad B de la memoria RAM el número entero 35"
Línea 30 → "Súmense los números en las localidades de memoria A y B, y asígnese el resultado a la localidad C de la memoria RAM" (la palabra reservada LET en la instrucción es optativa, muchas versiones de BASIC la proporcionan con la intención de que el programador le pueda dar una mayor claridad a sus programas)
Línea 40 → "Imprímase (en el monitor o en una impresora) el número contenido en la localidad C de la memoria RAM"
Línea 50 → "El programa ha terminado. Deténgase la ejecución"

El lenguaje BASIC original fue desarrollado en Dartmouth College en 1963 por John George Kemeny y Thomas Eugene Kurtz con el fin de proporcionar a los estudiantes no-especialistas en ciencias computacionales un medio para poder tener acceso a las computadoras. El lenguaje BASIC fue precisamente el primer lenguaje con capacidades de programación que fue distribuído en las primeras computadoras personales caseras, fue el primer lenguaje en ser incorporado dentro del sistema operativo MS-DOS (escrito en varias versiones tales como GW-BASIC y BASICA y QuickBASIC) por una entonces desconocida empresa fundada por dos empresarios igualmente desconocidos, Bill Gates y Paul Allen.

Posiblemente mis lectores se estén preguntando ya cómo se puede llevar a cabo la conversión de algo que está escrito en lenguaje "alfabético" que asemeja al lenguaje de los humanos en las instrucciones de "unos" y "ceros" que la microcomputadora está acostumbrada a manejar en su lenguaje de máquina. Hay varias formas de hacerlo. En la actualidad, en una computadora de escritorio, la conversión inicial a "unos" y "ceros" del texto alfanumérico se comienza a llevar a cabo precisamente cuando se van "tecleando" cada uno de los caracteres en el teclado de la computadora. Al oprimir una tecla, esencialmente se "cierra" un contacto eléctrico que permite que le llegue al microprocesador un código binario correspondiente a cada tecla que fue oprimida. El código binario a ser enviado al microprocesador es enteramente arbitrario, aunque podemos usar como punto de partida alguna convención que haya sido utilizada ampliamente en el pasado reciente. Una convención tal es el código ASCII (American Standard Code for Information Interchange). Este código asigna una secuencia binaria específica a dígito numérico del cero al nueve:


y a cada letra del alfabeto:


Hay también secuencias binarias reservadas para símbolos tales como el que se usa en la adición aritmética (+), así como otras secuencias binarias reservadas para lo que llamamos caracteres de control, cuyo propósito no es representar en forma binaria una letra o un dígito numérico o un símbolo aritmético sino el proporcionarle información adicional a la computadora sobre cosas tan importantes como el punto de inicio de un programa que va a ser ejecutado y el punto de terminación en el cual la ejecución del programa llega a su conclusión:



Bajo este esquema, un programa elemental BASIC para la suma de los números decimales 5 y 8 asignados primero a localidades en la memoria identificadas como A y B iría entrando a la computadora como un torrente continuo de "unos" y "ceros" en el siguiente orden:


Todavía hasta en tiempos recientes, esta información en formato de "unos" y "ceros" era almacenada no en los discos duros magnéticos sellados que hoy conocemos y mucho menos en los drives portátiles "flash" USB, sino en simples rollos de cinta magnética e inclusive en cintas de papel perforado, en las cuales la presencia de un "uno" era tomada como una perforación en la cinta y la ausencia de una perforación podía ser tomada como un "cero". Estos rollos de cinta permitían almacenar programas completos, los cuales afortunadamente eran extremadamente pequeños en comparación con los que hoy conocemos, programas que en muchas ocasiones en tamaño no excedían ni siquiera los 64K (hoy en día, nadie en su sano juicio pensaría almacenar un programa tan grande como el sistema operativo Windows XP o Windows Vista en tal medio, ya que se requerirían varios miles de rollos abarcando una extensión de varios kilómetros al ser extendidos). Una de las máquinas perforadoras de cinta más populares (de las cuales todavía hay algunas en uso) es el teletipo ASR-33:




en el cual podemos ver a la izquierda del teclado una cinta que está siendo perforada.

La conversión a código binario (usando algún estándard como ASCII) de los símbolos numéricos y alfabéticos de los que consta un programa como el programa BASIC arriba mostrado es tan sólo el primer paso previo a la ejecución del programa escrito por un programador. Conforme va entrando este torrente de "unos" y "ceros" a una sección de la memoria RAM reservada para almacenar esta información preliminar, la información sigue sin tener significado alguno para la máquina. Aunque en formato binario, este conjunto de "unos" y "ceros" conocido como código fuente (source code) tiene que ser convertido al lenguaje de máquina que "entiende" el microprocesador, tiene que ser convertido a una secuencia de instrucciones en lenguaje de máquina que puedan ser ejecutadas por la misma máquina, tiene que ser convertido a código objeto (object code). Y esto tiene que ser llevado a cabo por un programa conversor independiente del programa ejecutor que va a llevar a cabo la ejecución del código objeto. Esto significa que, además del programa que se va a ejecutar en la máquina, el cual es un programa ejecutable desarrollado para procesar en un lenguaje como BASIC ó FORTRAN ó COBOL ó cualquiera de las muchas otras variedades existentes, la máquina debe contar también con un programa traductor que convierta al código fuente en código objeto. Y aquí lo que puede fijar un límite absoluto es el tamaño de la memoria RAM disponible. Aunque hoy estamos acostumbrados a memorias RAM que se miden en los gigabytes, hace varias décadas había que conformarse con memorias RAM muchísimo más modestas, a grado tal que una de las primeras memorias RAM construídas a base de ferritas magnéticas (conocida como magnetic core memory) rara vez excedía del medio megabyte para las computadoras más costosas y sofisticadas en el mercado fabricadas entonces por IBM, Honeywell y Burroughs. Inclusive, ante la falta de capacidad de muchas computadoras para poder tener residentes en la memoria RAM al mismo tiempo tanto un programa traductor como el programa ejecutor del código objeto, frecuentemente lo que hacían los administradores del sistema era acomodar las cosas de modo tal de que en vez de tener a varios programas residentes al mismo tiempo en la memoria RAM, lo que se hacía era cargar primero la memoria RAM con el programa traductor, el cual se encargaba de convertir el código fuente a código objeto, tras lo cual con el código objeto ya residente en la memoria RAM se sacaba fuera de la memoria el programa traductor con el fin de cargar el programa ejecutor encargado de procesar el código objeto. Y de hecho, para programas excesivamente grandes, esto es precisamente lo que se lleva a cabo en muchas computadoras de escritorio de uso actual, y quien se encarga de hacer tal cosa automáticamente es ese otro programa que está corriendo todo el tiempo desde el momento en que se enciende la computadora de escritorio hasta el momento en el que se apaga la máquina, ese otro programa conocido como el sistema operativo. Cuando abrimos un programa que requiere de un consumo enorme de memoria RAM, el sistema operativo "guarda" los demás programas en el disco duro hasta que se requiera utilizar uno de dichos programas, en cuyo caso lo vuelve a cargar. Esta es la razón por la cual un sistema multitareas que está corriendo varios programas a la vez en lugar de uno solo en ocasiones manifiesta tardanzas que nos parecen exasperantes.

Así pues, para poder usar ventajosamente cualquier microcomputadora, a menos de que queramos llevar a cabo la programación en lenguaje de máquina nosotros mismos debemos contar por los menos con tres programas: el programa "fuente" como el que hemos puesto arriba en lenguaje BASIC, el programa traductor que convierta las instrucciones del programa fuente en código objeto, y el programa ejecutor en sí (en este caso, BASIC), el cual tomará las instrucciones del código objeto procesándolas una por una en lenguaje de máquina para proporcionarle al usuario las respuestas deseadas. Esto no incluye ese cuarto programa que vendría siendo un sistema operativo, un programa que está corriendo todo el tiempo y el cual nos facilita enormemente las tareas más sencillas tales como el copiado y el almacenado de archivos. El sistema operativo es una gran comodidad para el uso de cualquier computadora, pero aún sin un sistema operativo es posible programas computadoras; y de hecho ninguna de las primeras computadoras contaba con sistema operativo alguno. En su esencia básica, el sistema operativo no tiene mucha ciencia: al encender la máquina el sistema operativo se va cargando en una buena parte de la memoria RAM; una vez que termina de cargarse comienza a ejecutarse esperando instrucciones del usuario. Si el usuario quiere correr un programa como Mathematica o como Photoshop, entonces al seleccionar dicho programa del menú visual el sistema operativo transfiere el control de la computadora a dicho programa. Una vez que el usuario ha terminado de usar la aplicación, el control de la computadora es transferido nuevamente al sistema operativo. De este modo, el día ordinario en cualquier computadora de escritorio transcurre transfiriendo el control de un programa a otro, hasta que llega el momento de apagar la máquina con la orden final en la cual el usuario le pide al sistema operativo que se encarge de "cerrar" todos los archivos pendientes de ser cerrados y llevar la computadora hasta el punto en el cual pueda ser apagada ya sea por el usuario o por el mismo sistema operativo.

El programa anteriormente proporcionado en lenguaje BASIC es en realidad demasiado elemental para lo que BASIC puede hacer. Por principio de cuentas, BASIC puede efectuar operaciones no sólo sobre números enteros sino inclusive sobre números reales en notación decimal, e inclusive es capaz de llevar a cabo algunas operaciones matemáticas sofisticadas. A manera de ejemplo, considérese el siguiente programa BASIC:
10 A=45.68
20 B=17.29
30 C=A+B
40 D=SQR(C)
50 PRINT D
60 END
Esto ya es algo más sofisticado, y está mucho más "en línea" con lo que los diseñistas de las primeras computadoras querían que sus máquinas lograran. La ejecución del programa hará que los números 45.68 y 17.29 sean asignados a localidades en la memoria RAM en las que puedan ser localizados. La presencia del punto decimal prácticamente garantiza que este lenguaje BASIC necesitará más de una sola localidad de memoria RAM para poder almacenar en formato binario cada uno de estos números decimales. Tras esto, los números serán sumados y el resultado será asignado a localidades en la memoria RAM identificados en conjunto como C en donde pueda ser localizado. A continuación, el número identificado como D será llamado por la computadora para sacarle la raíz cuadrada con la ayuda de una palabra reservada por BASIC para su uso exclusivo, SQR, lo cual requerirá de una buena cantidad de instrucciones en lenguaje de máquina que nos resultaría extremadamente tedioso escribir sin incurrir en equivocación alguna. Al final, se imprime el resultado en la pantalla o en alguna impresora con el comando PRINT. Es motivo de reflexión el pensar que el anterior programa, el cual ocupa tan sólo seis líneas de código fuente, para ser ejecutado tendrá que ser convertido por el mismo microprocesador a cientos de instrucciones en lenguaje de máquina.

El lenguaje BASIC que hemos descrito con ejemplos es un lenguaje BASIC en el cual la microcomputadora va "traduciendo" cada línea de código fuente conforme la va encontrando. Así, primero se encuentra con la línea 10, la traduce a lenguaje de máquina, y la ejecuta. Hecho esto, se topa con la siguiente línea, la línea 20, la traduce a lenguaje de máquina, y la ejecuta. La mecánica de traducción línea-por-línea se repite hasta que se llega a la instrucción final en la cual se le indica a la computadora que el procesamiento ha terminado. Este tipo de programas, en los cuales cada instrucción se va traduciendo paso-a-paso, es conocido como interpretadores.

Además de los lenguajes interpretadores como BASIC, existe otro tipo más sofisticado de lenguajes de alto nivel conocidos como compiladores. La diferencia principal entre un programa interpretador y un programa compilador estriba en que el programa compilador no va traduciendo y ejecutando cada instrucción conforme se la va encontrando, sino que primero traduce todas las instrucciones juntas a lenguaje de máquina, tomando el código fuente y produciendo un código objeto, y una vez hecho esto procede a ejecutar todas las instrucciones ya traducidas de un solo golpe. Este último procedimiento es mucho más eficiente porque permite identificar muchos patrones repetitivos eliminando una gran cantidad de redundancias. En castellano, esto significa que los programas de computadora basados en lenguajes de alto nivel que actúan bajo el proceso de compilación suelen ser mucho más rápidos y suelen contener una cantidad menor de instrucciones en lenguaje de máquina que sus equivalentes en lenguajes interpretados. Una forma de poder reconocer de inmediato un programa que ha sido compilado es que, por regla general, son programas que se pueden ejecutar de inmediato porque todas las instrucciones en lenguaje de máquina están contenidas en un solo archivo. Bajo un sistema operativo como Windows, estos son precisamente los programas ejecutables que tienen una extensión como .exe, .bin y .com, son programas que han sido compilados.

En un principio, escribir un programa ejecutable para computadoras personales haciéndolo de la forma más cercana posible al lenguaje de máquina del microprocesador, sin necesidad de tener que ingresar hasta el fondo en los laboriosos detalles del lenguaje ensamblador con sus instrucciones como MOV y JMP, se llevaba a cabo usando un lenguaje como el lenguaje C, descrito por los expertos como "un lenguaje ensamblador de alto nivel", el cual evolucionó posteriormente al lenguaje C++ que permitía incorporar algo novedoso en aquél entonces conocido como la programación orientada-a-objetos (object oriented programming), lo cual se llevaba a cabo escribiendo líneas de código exclusivamente a través de texto escrito. Esto evolucionó posteriormente a una programación con una interfaz visual con muchas ayudas para el programador, de lo cual los ejemplos más notorios son el compilador Borland C++ y el compilador Visual C++ de Microsoft. Una de las enormes ventajas de utilizar un lenguaje como el C++ para redactar programas ejecutables en lugar de utilizar un lenguaje ensamblador es que las instrucciones proporcionadas bajo C++ no son tan específicas para cierto microprocesador en particular, por el contrario, son lo suficientemente generales como para permitir la creación de programas ejecutables para otros microprocesadores con el simple remedio expedito de volver a compilar el mismo código fuente (con cambios mínimos) para producir programas ejecutables para computadoras construídas con otros microprocesadores. A continuación se muestra un primer ejemplo de un programa escrito en el lenguaje C (obsérvese cómo para meter un comentario en cualquier parte dentro del programa abrimos la entrada del comentario con una pareja de símbolos formada por una diagonal y un asterisco en ese orden, y cómo para cerrar la entrada del comentario utilizamos una pareja de símbolos formada por un asterisco y una diagonal en ese orden):


Como puede apreciarse en este breve ejemplo, el lenguaje C permite liberalmente la inserción de comentarios en cualquier parte del programa encerrando los mismos entre los símbolos apareados "/*" y "*/". Todo lo que aparezca entre estos símbolos será ignorado por el compilador C al momento de estar compilando el programa. Al igual que el hardware que está basado en las tres funciones lógicas básicas OR, NOT y AND, el lenguaje C está basado en bloques básicos conocidos como funciones. Un programa C es una colección de una o más funciones. Para escribir un programa en lenguaje C, primero creamos las funciones, tras lo cual las agrupamos para formar el programa. Le podemos dar a las funciones creadas por nosotros cualquier nombre, excepto el nombre main, el cual está reservado para la función principal que echa a andar la ejecución del programa. En el ejemplo mostrado, el programa compilado empieza al igual que todo programa C con la ejecución de la función main, cuya acción está definida entre los corchetes "{" y "}". Este programa hace la invocación a una sola función, la función hola, dentro de la cual se invoca también dentro de sus respectivos corchetes "{" y "}" una de varias funciones reservadas por el lenguaje C, la función denominada printf que lleva a cabo la impresión de la hilera de caracteres hola\n puestos entre las dobles comillas. El símbolo "\n" puesto al final de la hilera ordena que una vez que se haya escrito la hilera indicada hola el cursor de la computadora deberá "saltar" hacia la siguiente línea (esto es el equivalente a la acción de la tecla de "Entrada" al estar escribiendo manualmente un texto en un programa como Microsoft Word). El programa mostrado simplemente imprimirá la palabra "hola" en una ventana para el manejo exclusivo de caracteres alfanuméricos (nada de gráficos), tras lo cual regresará el control de la máquina al sistema operativo. Por último, la directiva de preprocesador include puesta al inicio del programa instruye al compilador C que incluya en el proceso de compilación el archivo STDIO.H; este archivo contiene información requerida por el programa para asegurar la operación correcta de las funciones de librería estándard de Entrada/Salida, de las cuales la función printf es una de ellas.

A continuación tenemos un segundo ejemplo de otro programa escrito en lenguaje C:


En el primer ejemplo, la función definida como hola era una función que no recibe ningún tipo de argumento, simplemente imprime la palabra hola en la pantalla. En este segundo ejemplo, se ha definido la función cuadrado, la cual recibe como argumento un número entero identificado arbitrariamente por el usuario con la hilera num, el cual tiene que ser declarado previamente dentro de la función principal main como un argumento de tipo entero con la declaración "int num;" tras lo cual durante la ejecución del programa se le asigna un valor numérico entero a dicha variable con la declaración "num=100". Al ejecutarse la tercera línea del programa invocando la función cuadrado se le pasa el argumento num. Obsérvese que entre los dos paréntesis "(" y ")" que identifican a cuadrado como una función C, el contenido ya no está vacío, el contenido indica que cuadrado recibe como argumento un valor de tipo entero denotado como x (se podría haber usado cualquier otra letra o palabra, esto está a discreción del programador). Obsérvese también que la función printf es una función capaz también de poder recibir argumentos. Aquí, tras recibir como argumento de entrada el entero 100, en la hilera de impresión el símbolo reservado %d invoca el primer argumento puesto después de la hilera principal, o sea x, y el segundo símbolo reservado %d invoca el segundo argumento puesto después de la hilera principal, o sea x*x, lo cual requiere que se lleve a cabo una operación matemática de multiplicación. La acción final resultante de este programa una vez compilado como un archivo ejecutable será que la línea "100 al cuadrado es 10000" sea impresa en la pantalla.

El lenguaje C++ es un paso posterior en la evolución del lenguaje C, y la principal y más importante diferencia con respecto a su predecesor es que como ya se mencionó anteriormente permite la programación orientada-a-objetos, en la cual se diseña una "plantilla" de la cual se pueden obtener instancias que heredan de la misma las características que le programador desee que sean heredables, lo cual evita la repetición de mucho código redundante. Esto es algo muy parecido a lo que hace un usuario de un programa como Microsoft Word cuando al estar trabajando sobre un documento decide abrir un documento nuevo sin dejar el documento anterior sobre el cual está trabajando. Al abrir el segundo documento, el cual es asignado a otra ventana nueva, se abre una segunda instancia de Word con todas las capacidades que tiene la primera instancia del mismo, tras lo cual se puede abrir una tercera instancia, una cuarta instancia, etc., hasta donde lo permita la capacidad de la memoria de la máquina. A continuación se muestra un ejemplo de un programa escrito en el lenguaje C++ (obsérvese cómo los comentarios dentro del programa empiezan con una doble diagonal, y cómo podemos combinar textos y números en corrientes -conocidas como streams- mediante los enunciados cout para la salida y cin para la entrada):




El breve programa, después de saludar al usuario con el mensaje de "Hola mundo!" impreso en el monitor de la computadora y escribir en una segunda línea el texto "Hoy es 12 de octubre", le pide al usuario un número decimal, y una vez que el usuario ha escrito dicho número a través del teclado y ha oprimido la tecla de entrada "Enter" o "Intro", recibe como respuesta en dos líneas diferentes primero el equivalente en numeración octal del número decimal y tras esto el equivalente en numeración hexadecimal.

Para quienes quieran aprender a construír compiladores (por construír un compilador entendemos escribir en un lenguaje como C++ las líneas de código necesarias para producir un programa lo suficientemente potente como para que este programa pueda a su vez ser clasificado como un compilador BASIC o un compilador FORTRAN o inclusive como un compilador C, un compilador que sea capaz de traducir instrucciones en un programa fuente escritas en BASIC o en FORTRAN o en C como los dos ejemplos que vimos arriba convirtiendo dichas instrucciones en código ejecutable) se recomienda la lectura del libro clásico en la materia "Compilers: Principles, Techniques and Tools" de Alfred V Aho, Ravi Sethi y Jeffrey D. Ullman.

Para quienes quieran aprender el lenguaje de programación C/C++, Internet ofrece la posibilidad de poder descargar gratuitamente varios compiladores C/C++. Un portal que contiene una lista actualizada de varios lugares de los cuales se pueden descargar gratuitamente compiladores es el siguiente:

http://www.thefreecountry.com/compilers/cpp.shtml

Al cierre del año 2007, hay dos sitios de especial interés de los cuales se pueden descargar sin costo alguno compiladores C++ que pueden producir programas ejecutables para el sistema operativo Windows. El primero es Microsoft, del cual se puede efectuar la descarga gratuita del Visual C++ 2008 Express Edition de la siguiente dirección:

http://www.microsoft.com/express/download/

(De este sitio también se puede descargar en forma gratuita el programa-compilador Visual Basic 2008 Express Edition.)

El otro sitio es el de la empresa Borland, del cual se puede hacer una descarga gratuita de Turbo C++ del siguiente domicilio:

http://www.turboexplorer.com/

Además, de la misma empresa, se ofrece el Borland C++ Compiler en su versión 5.5 liberada el 24 de agosto de 2000, procurable del siguiente domicilio:

http://www.codegear.com/downloads/free/cppbuilder

Al momento de escribirse este Suplemento # 4, el BASIC para computadoras personales trabajando bajo Windows XP y Windows Vista ha evolucionado a fines del 2007 a lo que se conoce como VisualBASIC.net, el cual seguramente seguirá evolucionando en los años que habrán de venir.