Programación en Lenguaje Ensamblador

-El Verdadero Lenguaje de las Máquinas-

Proceso, Subrutina, Función y Método son cosas diferentes

–¿Y ustedes cuando aprendieron a programar?–

Es posible darse una idea de en qué año aprendió alguien a programar por la forma como llama a los grupos de instrucciones que son llamados por un programa cuando interrumpe su ejecución. Quienes aprendieron a programar antes de los 60 del siglo pasado les llaman «procesos», los de los 70 y principios de los 80 los llaman subrutinas. De finales de los ochenta y toda la década de los 90 les llaman funciones y los que aprendieron a programar ya entrado el siglo XXI les llaman Métodos. Esto tiene que ver con las modas vigentes de la programación en cada una de estas épocas pero aunque tienen varias cosas en común. No son lo mismo. Y como este es un lugar donde se habla de ensamblador me voy a concentrar en describir como se han implementado este tipo de códigos para soportar estas técnicas de programación.

PROCESOS
resultado = a + b

Primero veamos lo que es un proceso. de acuerdo con los manuales de Intel. Un proceso es un grupo de instrucciones que cumplen una tarea determinada. Por ejemplo en ensamblador para sumar 2 números primero hay que recibir las posiciones de memoria en las que se encuentran, cargar el primero de ellos en el acumulador. Sumarle al acumulador el contenido de la segunda celda de memoria y luego guardar el resultado en la celda de memoria donde debe de almacenarse el resultado. Esto en un procesador actual son unas 5 instrucciones (aunque la suma en si es solo una). Ahora bien, la computadora ejecuta una tras otra las instrucciones que se le dan sin tener la menor idea de lo que está haciendo. Eso de agrupar el código lineal en procesos es tan solo para que los programadores humanos lo entiendan. Y la computadora va a ejecutar esas instrucciones hasta que reciba la orden de detenerse.

Los grandes códigos muy rara vez son lineales. Y en los códigos muy grandes es común que se necesite llamar a los mismos procedimientos muchas veces. Para eficientizar el uso de la memoria y la reutilización de código a alguien se le ocurrió que esos fragmentos de instrucciones se podían guardar en un lugar separado de la linea principal de ejecución del programa. De modo que cuando se necesitara ejecutar ese grupo específico de instrucciones tan solo había que hacer que la ejecución del código saltara hacia ese sitio y que al terminar regresara al punto exacto a donde se había dado el salto. Esto lo puede hacer porque cuando se le dice que tiene que dar ese salto el sistema guarda la posición donde se encuentra y luego tan solo la lee para regresar a donde estaba. Como si tuviéramos una cuerda a la que le amarramos un extremo al sitio donde empezamos para poder regresar con tan solo seguirla. Este sistema es tan viejo como las primeras computadoras de la década de los 50 aunque hay rumores extraoficiales que existían computadoras primitivas de tiempos de la Segunda Guerra que eran capaces de hacer esto. Ahora veamos como esta manera de llamar trozos de código separados en cualquier momento ha evolucionado con el paso de la tecnología.

Subrutinas:
GOSUB suma

El primer intento de organizar el código de programación fueron las subrutinas. Una subrutina era un grupo de instrucciones que se escribía al final del código principal justo después de la instrucción que indicaba el fin del programa. Aunque en algunos casos había quien las mezclaba con el propio flujo del programa principal y las aislaba por medio de saltos incondicionales. En el caso del antiguo BASIC se usaba la instrucción GoSub seguida del nombre de la subrutina. Cuando el código llegaba a esta parte primero guardaba la posición donde iba y luego saltaba a la subrutina. Ya en la subrutina, la última instrucción era RETURN. Cuando el procesador llegaba ahí hacía un salto hacia la dirección que había guardado previo al salto y seguía su camino en la linea principal del programa.

En ensamblador, este proceso fue copiado directamente con instrucciones como CALL y RET. CALL hace lo mismo que GoSub, guarda la posición en la que se encuentra y luego salta a donde se encuentra la subrutina. Cuando llega a RET tan solo toma la posición desde la que saltó y continúa su camino como si nada hubiera pasado. Aquí la clave es el STACK. Como ya saben el STACK es una zona de la memoria donde se guardan datos de manera que el último en entrar es el primero en salir. Esto permite que las subrutinas llamen a su vez a todas las subrutinas que quieran y que el CPU siempre pueda regresar al punto del programa del que la primera de ellas fue llamada.

Las subrutinas fueron muy útiles y todavía se siguen usando para programas pequeños y aislados. Aunque conforme el código si fue haciendo mas y mas estructurado mostraron serias desventajas. La primera de ellas fue que trabajaban directamente con los datos del programa principal, de modo que si uno quería reutilizarlas en otro programa debía copiar también cualquier estructura de datos, información o pieza de código con la que interactuara. Para que una subrutina fuera verdaderamente portable no debía de interactuar con ningúna variable o estructura ajena a si misma. Algunos programadores astutos escribieron subrutinas con una sección propia de la memoria de esa misma subrutina y evitaron su ejecución por medio de un salto. El problema con esto era que ya no se podía llamar a las subrutinas de manera tan libre como antes y la recursión (una rutina que se llama a si misma) resultaba imposible. Era necesario que cada que se llamara a una subrutina se creara un espacio de memoria en el que pudiera hacer lo que quisiera sin alterar el resto del programa y que cuando terminara este mismo espacio desapareciera. Además de que había que tener control sobre la información que esta subrutina regresara. Y fue así como nacieron las funciones.

Funciones:
resultado = suma(a, b);

Como respuesta a las debilidades de las subrutinas se crearon las funciones. Una función es en esencia una subrutina que se comporta del mismo modo que lo hace una función matemática. Como supongo que quienes leen mis escritos no saben nada de cálculo voy a explicarles. Una función matemática recibe un grupo de argumentos, hace una serie de cálculos con ellos y regresa un único resultado. Tanto las funciones matemáticas como las de programación toman una serie de números a los que llaman argumentos y devuelven un único resultado. Una función que no recibe ningún argumento, que tampoco retorna un resultado y que de preferencia tampoco tiene un espacio para datos internos es idéntica a una subrutina.

Ahora veamos como se implementa una función en ensamblador. Lo primero que tenemos que entender es que las funciones tienen su propio espacio de memoria que toman prestado cuando son llamadas y que devuelven cuando terminan de ejecutarse. Este espacio lo tomamos del STACK y formamos lo que llaman un Stack Frame. Un Stack Frame es una sección de la memoria gestionada por el STACK donde guardamos variables internas y recibimos argumentos externos. El tema del Stack Frame requiere mas de una entrada para explicarlo en su totalidad así que no voy a entrar en detalles. En ensamblador lo primero que hacemos antes de llamar a la función es introducir al STACK los argumentos del último al primero usando la instrucción PUSH. Una vez hecho esto llamamos a la función con CALL. Ya en el cuerpo de la función lo primero que hacemos es definir el stack frame guardando el apuntador base del stack y creando un espacio de direcciones virtuales (ya se que esto se oye muy complicado pero no es imposible) en el que el Stack Frame tiene de un lado los argumentos de entrada, luego la posición en la que va a regresar, luego los datos del stack frame anterior y al final el espacio para variables locales. Luego sigue el código de la función. En el cual debemos de trabajar solamente con los datos del stack frame y evitar relacionarlos con cualquier variable o estructura de datos externa. Solo así podemos garantizar que la función sea independiente y reutilizable en otros proyectos.

Para terminar la función tenemos que destruir el Stack Frame que creamos al principio. Esto es sencillamente un llamado a la instrucción LEAVE seguida de un RET con un argumento extra igual a la cantidad de bytes que ocuparon los argumentos de entrada. La instrucción LEAVE deshace el stack frame de manera automática y aunque se supone que trabaja con la instrucción ENTER. Yo en lo personal nunca he visto que ENTER se use.

Por cierto. Para que una función sea considerada como tal debe de retornar un único valor numérico. Como este valor numérico no puede ser guardado en ninguna parte por el código de la función lo que se hace es lo siguiente. Antes de ejecutar las instrucciones que destruyen el Stack Frame guardamos en el registro acumulador el valor de retorno de la función. De modo que cuando la función termine de ejecutarse y el CPU vuelva al código del que fue llamada. En el registro acumulador se encuentra el resultado de la función. De este modo podemos guardarlo en donde queramos, procesarlo o simplemente ignorarlo y seguir con el programa. El como hacer funciones en ensamblador verdaderamente portables es un tema del que un día voy a hacer una serie de entradas. Pues se trata de un tema complejo pero muy importante que debe de ser dominado.

Pero como era de esperarse, las funciones no son la solución para todo. Pues con la llegada de la programación orientada a objetos o simplemente OOP surgió la necesidad de asociar las funciones con eso que llaman objetos. Antes una función podía ser llamada desde cualquier lugar por cualquier código. Pero cuando se programa orientado a objetos cada función es ejecutada por un objeto o interviene un objeto en particular. La única manera de lograr esta lealtad entre las funciones y los objetos fue la creación de lo que los jóvenes programadores de la época actual conocen como Métodos.

Métodos:
resultado = Calculadora.suma(a, b);

Los métodos son como las funciones en cuanto a estructura interna pero se diferencian de estas en que solo pueden ser ejecutadas por el objeto al que pertenecen. Y es en los métodos en donde se ve la verdadera diferencia entre un objeto y una simple estructura de datos. En el ejemplo del subtítulo. El método suma solo puede ser ejecutado por una instancia del objeto llamado Calculadora. Las ventajas de esto es que (dicen los que defienden la OOP) se tiene mayor control de quién está haciendo que cosa. Personalmente a mi me parece un desperdicio irresponsable de memoria, burocracia excesiva y una sobrecarga peligrosa en la gestión interna de la memoria del sistema operativo. La única ventaja que hasta ahora le he encontrado a la OOP es que es muy facil modelar sistemas muy grandes en papel antes de sentarse a programar.

En fin, en la OOP cuando queremos que algo se haga debemos de averiguar que objeto es capaz de hacer aquello que queremos, crear una instancia y darle la orden de que haga lo que queremos. El sistema tiene que asegurarse de que no sea posible llamar al método sin pasar primero por el objeto. Piensen en un jefe que le ordena a sus trabajadores especializados que hagan labores en lugar de hacerlos él mismo. Ahora veamos como se hace en ensamblador para echar a andar un sistema de llamada que usa objetos y métodos.

Un método en ensamblador es idéntico a una función excepto por una cosa: Uno de sus parámetros lo relaciona directamente con el objeto al que pertenece. En el resto de sus componentes internos es igual a una función. La diferencia mas grande en los métodos no radica en el método en sí sino en el sistema que los manda ejecutar. Ahora veamos un poco de como se define un objeto.

En programación se define objeto como una entidad abstracta moldeada a partir de una clase que tiene un conjunto de atributos y métodos propios que bien pueden ser privados, públicos o protegidos. En ensamblador un objeto es una simple estructura de datos cuya primera mitad guarda datos propios como cualquier otro conjunto de variables y la segunda mitad contiene las posiciones de memoria donde se guardan los métodos que tiene derecho a ejectuar. Algunos sistemas tienen estructuras internas que gobiernan a los objetos o que simplifican su estructura interna. Por ejemplo en ciertos modelos de programación basados en Windows existe una tabla intermedia que asocia los objetos con los métodos para mejorar la seguridad. Creo que la mayoría de los que leen esto ya saben todo esto así que no voy a profundizar mas. Ahora veamos lo interesante que es ver como se maneja un sistema de métodos orientados a objetos en ensamblador.

Cuando queremos llamar un método perteneciente a un objeto desde ensamblador. Lo primero que debemos de hacer es crear una instancia del objeto. Para lo cual llamamos a la función constructora que recibe una serie de argumentos entre los cuales destaca la posición de memoria donde el objeto va a ser creado. Lo creamos e identificamos el valor que asocia el método con el objeto. Una vez encontrado este valor hacemos la llamada al método como si fuera una función. Por desgracia no hay una forma general de hacer esto y cada sistema tiene sus propias reglas para manejar los objetos y los métodos. En tiempos del MS-DOS Borland tenía la suya. Windows en los tiempos del modelo objeto componente COM tenía el suyo y los sistemas basados en iOS tienen su sistema propio. Por suerte es muy raro que tengamos que construir uno de estos sistemas desde cero como cuando comenzaron las funciones. Casi siempre el propio sistema operativo tiene su manera de gestionar todo esto. Casi siempre…

En fin, personalmente llamo procedimiento a cualquier grupo de instrucciones que llevan a cabo una sola tarea y llamo subrutina o función a un segmento de código «llamable» dependiendo del manejo de los argumentos y el humor del que amaneza ese dia. El término Método solo lo uso en relación con sistemas orientados a objetos. Lo que no deben de olvidar es que no importa que tan extraña sea la moda vigente en programación. Si corre en una computadora puede programarse en ensamblador. No piensen que programación orientada a objetos solo es Java o C++. Siempre se puede combinar la velocidad y control del Ensamblador con cualquier rareza intelectual que se ponga de moda en los ambientes tecnológicos. Y aunque no es sencillo, el resultado bien lo vale. Pues solo cuando programan en ensamblador la computadora va a hacer lo que ustedes le ordenan. De lo contrario, ustedes tendrán que hacer lo que les ordene la computadora. O en el peor de los casos, el inventor del software con el que tienen que trabajar para ganarse la vida en sus trabajos honrados.

May 19, 2012 - Posted by | Uncategorized | ,

1 comentario »

  1. Interesante!

    Comentarios por Carlos M Gómez | marzo 28, 2016 | Responder


Deja un comentario