Está en la página 1de 49

INDICE

Contenido Pág.

TÉCNICAS DE PROGRAMACIÓN

ORGANIZACIÓN EN BASE DE RUTINAS

PROCEDIMIENTOS……………………………………………………………

Técnicas de invocación de subrutinas……………………………………….

Ejecución De Subrutinas……………………………………………………….
Ventajas De La Ejecución De Subrutinas……………………………………..
Las Instrucciones De Llamada Y Retorno De Una Subrutina……………….

Call………………………………………………………………………
Call Cercana………………………………………………………..
Call Lejana…………………………………………………………..
Instrucciones Call Con Operandos Registro…………………………
Instrucciones Call A Direcciones Indirectas En La Memoria………..
Ret………………………………………………………………………
Paso De Parámetros Y Devolución De Resultados…………………

Paso De Parámetros A Través De Registro………………….


Paso De Parámetros A Través De Memoria………………...
Paso De Parámetros A Través De La Pila……………………
Almacenamiento De Variables Locales A Una Subrutina…..
Gestión Del Bloque De Activación………………………………………….

ACCESOS CONDICIONALES…………………………………………………
CONDICIONALES…………………………………………………

Grupo De Instrucciones De Salto……………………………………………

MANEJO DE TABLAS…………………………………………………………
TABLAS…………………………………………………………

Conversión de binario a ASCII……………………………………………….

Conversión de ASCII a binario………………………………………………..

Empleo De Tablas Para Conversiones De Datos…………………………….

Empleo de una tabla para accesar a datos ASCII……………………………


DOCUMENTACIÓN DEL SOFTWARE…………………………………….
SOFTWARE…………………………………….

Como documentar un sistema de software………………………………

Objetivo………………………………………………………………

Esquema………………………………………………………………

1. Requisitos…………………………………………………

2. Diseño…………………………………………………….

3. Pruebas…………………………………………………….

4. Reflexión…………………………………………………..

5. Apéndice…………………………………………………..

ELABORACIÓN DE COMENTARIOS……………………………………….
COMENTARIOS……………………………………….

Documentación del código…………………………………….

Comentarios a nivel de especificación……………………………

Comentarios a nivel de implementación…………………………

PRÁCTICA #7…………………………………………………………………
#7…………………………………………………………………

Estudio Del Sistema De Audio Del Computador………………………..


Síntesis De Sonido…………………………………………………………..
Control Directo Del Altavoz………………………………………………
Control Del Altavoz Por El Temporizador………………………………
PRÁCTICA #8…………………………………………………………………
#8…………………………………………………………………

Desarrollo De Una Aplicación Con El Sistema De Audio Del Computador

CONCLUSIÓN……………………………………………………
CONCLUSIÓN…………………………………………………………………
…………………………………………………………………

BIBLIOGRAFÍA…………………………………………………………………
BIBLIOGRAFÍA…………………………………………………………………

ANEXOS……………………………………………………………
ANEXOS………………………………………………………………………..
………………………………………………………………………..
INTRODUCCIÓN
Las técnicas de desarrollo y diseño de programas que se utilizan en la
programación convencional tienen inconvenientes, sobre todo a la hora de
verificar y modificar un programa. En la actualidad están adquiriendo gran
importancia las técnicas de programación, cuyo objetivo principal es el de
facilitar la comprensión del programa, y además permiten, de forma rápida, las
ampliaciones y modificaciones que surjan en la fase de explotación del ciclo de
vida de un programa o una aplicación informática

Algunas técnicas de programación, con el empleo de programas como el macro


ensamblador, emu8686, turbo C, serán ampliadas en este texto, las llamadas a
funciones saltos así como empleo de rutinas y subrutinas en conjunto sirven de n
enorme ayuda. Algunas técnicas de programación incluyen secuencias de macros,
manejo de teclado y el monitor, módulos de programas, archivos de bibliotecas,
interrupciones y otras importantes técnicas de programación.

Esta sección es una introducción a la programación, incluyendo técnicas


valiosas de para programación que son de enorme ayuda para poder desarrollar
programas para computadora personal.

El programa ensamblador convierte un modulo simbólico fuente (archivo)


en un archivo objeto hexadecimal. Las instrucciones para control del programa
dirigen su flujo y permiten cambiarlo. A menudo ocurre un cambio de fujo
después que las decisiones están tomadas con una instrucción CMP o TEST van
regidas de una instrucción para brinco condicional.

Un procedimiento es un grupo de instrucciones que, por lo general,


desempeñan una tarea. Un procedimiento es una sección de un programa que se
puede volver a utilizar y que se almacene una vez en la memoria, pero se
emplea tan a menudo como se necesite. Esto ahorra espacio en la memoria y
facilita el desarrollo de la programación. La única desventaja de un
procedimiento es que la computadora requiere un poco de tiempo para ligarse
con el procedimiento y retornar desde el. La instrucción CALL liga el
procedimiento y la instrucción RET retorna del procedimiento.

La dirección para el retorno se almacena en la pila siempre que se llama a


un procedimiento durante la ejecución de un programa. La instrucción CALL
salva la dirección de la instrucción, que va después de ella, en la pila. La
instrucción RET recupera una dirección de la pila a fin de que el programa
retorne a la instrucción que sigue al CALL.
RUTINAS
Para crear un programa ejecutable a partir de un código objeto se
requiere que se resuelvan las llamadas a otros programas y a los servicios del
sistema operativo, y agregar las rutinas o información de runtime para que el
programa pueda ser cargado a memoria y ejecutado. Las rutinas de runtime
son necesarias, puesto que el sistema operativo requiere tener control sobre el
programa en cualquier momento, además de que la asignación de recursos y su
acceso deben hacerse solamente a través del sistema operativo.

Las rutinas son uno de los recursos más valiosos cuando se trabaja en
programación; ellas permiten que los programas sean más simples, debido a que
el programa principal se disminuye cuando algunas tareas se escriben en forma
de programas pequeños e independientes, en otras palabras se define como un
procedimiento independiente (un bloque), que realiza una labor específica y a la
cual se puede llamar desde cualquier parte del programa principal dado que cada
rutina realiza una labor en particular, como encender un LED o establecer una
base de tiempo de un segundo, por ejemplo, el programador, cuando está
seguro de su funcionamiento, puede mantenerla almacenada y disponible en un
«banco» o «librería», para utilizarla en cualquier programa, sin volver a escribir las
líneas que realicen lo deseado: sólo necesitará copiar el bloque de la rutina o
tener el archivo disponible para cuando se realice la compilación del programa.
Posteriormente, deberá hacer el llamado al procedimiento adecuado, en el
instante preciso.

Para la elaboración de cualquier rutina, es necesario conocer más a fondo


las instrucciones que la forman y cuales son las condiciones o estados que se
modifican ante cada una de ellas. Los estados, banderas o flags son bits que se
afectan por algunas operaciones lógicas o aritméticas y la consulta de estos se
utiliza para tomar decisiones dentro del programa.
Además, una interrupción es una operación que invoca la ejecución de
una rutina específica que suspende la ejecución del programa que la llamó, de tal
manera que el sistema toma control del computador colocando en el stack el
contenido de los registros CS e IP. El programa suspendido vuelve a activarse
cuando termina la ejecución de la interrupción y son restablecidos los registros
salvados.

PROCEDIMIENTOS

El procedimiento o subrutina es parte importante de la arquitectura de


cualquier sistema de computadora. Un procedimiento es un grupo de
instrucciones que, por lo general, desempeñan una tarea. Un procedimiento es
una sección de un programa que se puede volver a utilizar y que se almacena
una vez en la memoria, pero se emplea tan a menudo como se necesite. Esto
ahorra espacio en la memoria y facilita el desarrollo de la programación. La
única desventaja de un procedimiento es que la computadora requiere un poco
de tiempo para ligarse con el procedimiento y retornar desde el. La instrucción
CALL liga el procedimiento y la instrucción RET retorna del procedimiento.

La dirección para el retorno se almacena en la pila siempre que se llama a


un procedimiento durante la ejecución de un programa. La instrucción CALL
salva la dirección de la instrucción, que va después de ella, en la pila. La
instrucción RET recupera una instrucción de la pila a fin de que el programa
retorne a la instrucción que sigue al CALL.

La mayor parte de los procedimientos que se utilizan con todos los


programas (globales) se deben redactar como procedimientos lejanos. Los
procedimientos que se utilizan para una tarea dada (locales) se suelen definir
como procedimientos cercanos.

EJECUCIÓN DE SUBRUTINAS

La estructura global de un programa es una combinación de bloques para


los cuales existe una traducción sistemática. Esta y la codificación de los datos son
las dos principales tareas de un compilador para obtener un ejecutable.

El mecanismo que merece un estudio aparte es el de llamada a subrutinas.


El desarrollo de programas modulares se basa en la posibilidad de ejecutar un
bloque de código múltiples veces con diferentes valores de un conjunto de
variables denominadas ‘parámetros’ que produce un resultado. Este mecanismo,
con diferentes matices, es lo que se denomina como procedimientos, funciones,
subprogramas o métodos y están presentes en prácticamente todos los lenguajes
de programación de alto nivel.

En el contexto del lenguaje ensamblador se define una subrutina como


una porción de código que realiza una operación en base a un conjunto de
valores dados como parámetros de forma independiente al resto del programa y
que puede ser invocado desde cualquier lugar del código, incluso desde dentro
de ella misma.
La ejecución de subrutinas tiene las siguientes ventajas:

Evita código redundante. Durante el diseño de un programa suelen existir


ciertos cálculos que deben realizarse en diferentes lugares del código. La
alternativa a replicar las instrucciones es encapsularlas en una subrutina e invocar
esta cada vez que sea necesario lo cual se traduce en código más compacto.

Facilita
Facilita la descomposición de tareas.
tareas La descomposición de tareas
complejas en secuencias de subtareas más simples facilita enormemente el
desarrollo de programas. Esta técnica se suele aplicar de forma sucesiva en lo que
se denomina ‘diseño descendente’ de programas. Cada subtarea se implementa
como una rutina.

Facilita el encapsulado de código.


código El agrupar una operación y sus datos en
una subrutina y comunicarse con el resto de un programa a través de sus
parámetros y resultados, hace que si en algún momento se cambia su
implementación interna, el resto del programa no requiera cambio alguno.

Además de estas ventajas, el encapsulado de código también facilita la


reutilización de su funcionalidad en más de un programa mediante el uso de
‘bibliotecas’. Una biblioteca de funciones es un conjunto de subrutinas que
realizan cálculos muy comunes en la ejecución de programas y que pueden ser
utilizados por éstos. Java es un ejemplo de lenguaje que dispone de bibliotecas
de clases que en su interior ofrecen multitud de métodos.

La desventaja de las subrutinas es que es necesario establecer un protocolo


que defina dónde y cómo se realiza esta transferencia de datos para la que se
requieren múltiples instrucciones máquina adicionales.

Tecnicas de programación de una subrutina


subrutina

La politica general que se recomienda para la escritura de subrutina es:


1.-
1.- Si la subrutina va a llamar a otras subrutinas, almacenar en pila la dirección de
retorno, para permitir el anidamiento de subrutinas.

2.-
2.- Almacenar en la pila el contenido de los registros que utilice para cálculos
temporales.

El punto numero dos, es crucial en la programación en esamblador, debido a


que si una subrutina cambia el contenido de registros que el programa principal
utiliza, el resultado puede ser impredecible.

Para evitar que esto suceda, existen dos técnicas que se denominan:

a) El invocador guarda los registros (caller save)

b) El invocado guarda los registros (callee save)

En la primera tecnica, el programa que invoca a la subrutina tiene la


responsabilidad de guardar en pila los registros que le son necesarios, para que
las rutinas las modifique a su antojo.

En la segunda técnica, la subrutina invocada es la responsable de dejar el


contenido de los registros tal y como estaban en el momento en el que fue
invocada. Para ello los salva inicialmente en la pila y restaura su valor justo antes
del retorno.

LAS INSTRUCCIONES DE LLAMADA Y RETORNO DE UNA SUBRUTINA

En ensamblador la llamada a una subrutina se realiza mediante la


instrucción CALL cuyo único operando es la dirección de memoria, generalmente
una etiqueta, en la que comienza su código. Tras ejecutar esta instrucción el
procesador continua ejecutando la primera instrucción de la subrutina hasta que
encuentra la instrucción RET que no tiene operandos y transfiere la ejecución a la
instrucción siguiente al CALL que inició el proceso. La siguiente figura ilustra esta
secuencia.

CALL

La instrucción CALL transfiere el flujo del programa al procedimiento. Esta


instrucción difiere de las instrucciones para brinco (salto) porque CALL salva una
dirección para retorno en la pila. La dirección de retorno devuelve el control a la
instrucción que sigue a CALL en un programa, cuando se ejecuta una instrucción
RET.

 CALL cercana
La instrucción CALL cercana tiene 3 bytes de longitud; el primer byte contiene
el código y el segundo y tercero contiene el desplazamiento o distancia de ±
32K en los 8086-80286. La forma de instrucción es idéntica a la de brinco
cercano. En los 80386 y 80486 se emplea un desplazamiento de 32 bits cuando
trabajan en el modo protegido a fin de permitir una distancia de ± 2G bytes.
Cuando se ejecuta un CALL cercano, primero salva en la pila la dirección de
desplazamiento de la siguiente instrucción. Esa dirección está en el apuntador de
instrucciones (IP o EIP). Después de salvar la dirección de retorno, suma los
desplazamientos de los bytes 2 y 3 a IP para transferir el control al
procedimiento. No hay instrucción CALL corta.
¿Por qué salvar IP o EIP en la pila?

El apuntador de instrucciones siempre apunta a la siguiente instrucción en el


programa. Para la instrucción CALL, se salva el contenido de IP o EIP dentro de
la pila, con lo cual el control del programa pasa a la instrucción que sigue al
CALL después de que termina un procedimiento. En la siguiente figura se ilustran
la dirección de retorno (IP) salva en la pila y la llamada a procedimiento para los
8086-80286.

El efecto de una instrucción CALL cercana en la pila y en los registros SP, SS e


IP. Se ve la forma en que se almacena IP viejo en la pila.

 CALL lejana

La instrucción CALL lejana es como un brinco lejano porque puede llamar


a un procedimiento almacenado en cualquier localidad de la memoria en el
sistema. La instrucción CALL lejana tiene 5 bytes y tiene un código de operación
seguido por el valor de los registros IP y CS. Los bytes 2 y 3 tienen el nuevo
contenido de IP y los bytes 4 y 5 tienen el nuevo contenido de CS.

La instrucción CALL lejana salva el contenido de IP y de CS en la pila antes


de brincar a la dirección indicada por los bytes 2 a 5 de la instrucción. Esto
permite que la CALL lejana llame a un procedimiento ubicado en cualquier lugar
en la memoria y que retorne desde el procedimiento.

La instrucción CALL lejana

En la figura anterior se ilustra la forma en que CALL lejana llama a un


procedimiento lejano. En este caso se salva el contenido de IP y CS dentro de la
pila. Luego, el programa se transfiere al procedimieto.

Llamada y retorno de una subrutina

INSTRUCCIONES CALL CON OPERANDOS REGISTRO.


REGISTRO

Las instrucciones CALL al igual que las JMP pueden contener un operando
registro y un ejemplo es la instrucción CALL BX. Esta instrucción salva el
contenido de IP dentro de la pila. Después, brinca a la direccion de
desplazamiento, ubicada en el registro BX en el segmento de código actual. Este
tipo de llamada siempre utiliza una dirección de desplazamiento de 16 bits
almacenada en cualquier registros de 16 bits, excepto los registros de segmento.

En el ejemplo anterior, se ilustra el ejemplo de una instrucción CALL con


un registro para llamar a un procedimiento que comienza en la direccion de
desplazamiento COMP, la cual se carga al registro SI y, luego la instrucción CALL
SI llama al procedimiento que empieza en la dirección COMP.

INSTRUCCIONES CALL A DIRECCIONES INDIRECTAS


INDIRECTAS EN LA MEMORIA

La instrucción CALL a una dirección indirecta en la memoria es de particular


utilidad siempre que se desea esciger diferentes subrutinas en un programa. Este
proceso de selección va ligado frecuentemente con un numero que direcciona a
una direccion de llamada en una tabla de consulta.

INVOCACION ANIDADA DE SUBRUTINAS.

La instrucción RET de la subrutina B retorna la ejecución a la subrutina A


en su primera ejecución (denotada por la flecha número 3) y al programa
principal en su segunda ejecución (denotada por la flecha número 6). Esto hace
suponer, por tanto, que la dirección de retorno no puede ser decidida cuando se
ejecuta esta instrucción sino en un momento anterior.

El instante en el que se sabe dónde ha de retomarse la ejecución tras una


subrutina es precisamente en el momento de su invocación. Cuando el
procesador está ejecutando la instrucción CALL obtiene la dirección de retorno
como la de la instrucción siguiente en la secuencia.

Por lo tanto, el procesador, además de modificar la secuencia, al ejecutar


la instrucción CALL debe guardar la dirección de retorno en un lugar prefijado
del cual será obtenido por la instrucción RET. Pero, durante la ejecución de un
programa es preciso almacenar múltiples direcciones de retorno de forma
simultánea.

El que las subrutinas se puedan invocar de forma anidada hace que la


utilización de los registros de propósito general para almacenar la dirección de
retorno no sea factible. La alternativa es almacenarlas en memoria, pero la
instrucción RET debe tener acceso a su operando implícito siempre en el mismo
lugar. Además, esta zona de memoria debe poder almacenar un número
arbitrario de direcciones de retorno, pues la invocación de subrutinas se puede
anidar hasta niveles arbitrarios de profundidad.

Por tanto, se necesita un área de memoria que pueda almacenar tantas


direcciones de retorno como subrutinas están siendo invocadas de forma anidada
en un momento de la ejecución de un programa. La propiedad que tienen estas
direcciones es que se almacenan por la instrucción CALL en un cierto orden, y
son utilizadas por la instrucción RET en el orden inverso.

La estructura especialmente concebida para este propósito es la pila. En


ella se almacena la dirección de retorno mientras se ejecuta el cuerpo de una
subrutina. En caso de invocaciones anidadas, las direcciones de retorno
pertinentes se guardan en la pila y están disponibles para la instrucción RET en el
orden preciso.

La instrucción CALL, por consiguiente, realiza dos tareas: pasa a ejecutar la


instrucción en la dirección dada como operando y almacena en la cima de la pila
la dirección de la instrucción siguiente (al igual que lo haría una instrucción
PUSH) que será la instrucción de retorno. Por su parte, la instrucción RET
obtiene el dato de la cima de la pila (igual que lo haría la instrucción POP) y
ejecuta un salto incondicional al lugar que indica. Ambas instrucciones, modifican
el contador de programa.

Del funcionamiento de estas instrucciones se concluye que la cima de la


pila justo antes de la ejecución de la primera instrucción de una subrutina
contiene la dirección de retorno, entonces, antes de ejecutar la instrucción RET
debe apuntar exactamente a la misma posición. Aunque esta condición es
esencial para que el retorno de la subrutina se haga al lugar correcto, los
procesadores no realizan comprobación alguna de que así se produce. Es
responsabilidad del programador en ensamblador el manipular la pila en una
subrutina de forma que la cima de la pila al comienzo de la ejecución sea
exactamente la misma que justo antes de ejecutar la última instrucción.

Durante la ejecución de la subrutina se pueden hacer las operaciones


necesarias sobre la pila siempre y cuando se conserve la dirección de retorno. El
programa en ensamblador que comienza a ejecutar a partir de la etiqueta main
también es una subrutina que invoca el sistema operativo, y por lo tanto se debe
garantizar que la cima es idéntica al comienzo y al final del programa pues
contiene la dirección de retorno.

RET

La instrucción para retorno (RET) extrae un numero de 16 bits (retorno


cercano) de la pila y lo coloca en IP o bien un numero de 32 bits (retorno
lejano) y lo coloca en IP y CS. Las instrucciones para retorno cercano y lejano
estan definidas en el directivo PROC para el procedimiento. Esto selecciona en
forma automática la instrucción adecuada para el retorno. En los 80386 y 80486
que trabajen en el modo protegido, el retorno lejano extrae 6 bytes de la pila;
los primeros 4 contienen el nuevo valor para EIP y los dos ultimos el nuevo
valor para CS. El retorno cercano para los 80386 y 80486 en modo protegido,
extrae 4 bytes de la pila y los coloca en EIP.

Cuando se cambian IP o EIP, o IP o EIP y CS, la direccion de la siguiente


instrucción esta en una nueva localidad en la memoria, la cual es la direccion de
la instrucción que va inmediatamente despues de la llamada mas reciente a un
procedimiento. En la siguiente figura se ilustra la forma en que una instrucción
CALL se liga con un procedimiento y la forma en que la instrucción RET retorna
en los microprocesadores 8086-80286.
El efecto de una instrucción RET cercana en la pila y los registros SP, SS e IP.

Hay otra forma mas para la instrucción de retorno, la cual suma un


numero al contenido del apuntador de pila (SP) antes del retorno. Si hay que
borrar lo salvado en la pila antes de un retorno, el desplazamiento de 16 bits se
suma a SP antes de que el retorno recupere la direccion de retorno en la pila. El
efecto es omitir o borrar datos de la pila.
EJEMPLO

En este ejemplo se muestra la forma en que este tipo de retorno borra los
datos salvados en la pila por algunas instrucciones PUSH. RET 4 suma 4 a SP
antes de recuperar la direccion de retorno de la pila. Debido a que PUSH AX y
PUSH BX, juntas, salvaron 4 bytes de datos en la pila, este retorno borra
efectivamente a AX y BX de la pila. Este tipo de retorno rara vez aparece en los
programas

PASO DE PARÁMETROS Y DEVOLUCIÓN DE RESULTADOS

En general una subrutina consiste en una porción de código que realiza


una operación con un conjunto de valores proporcionados por el programa que
la invoca denominados parámetros, y que devuelve un resultado. Los
parámetros son copias de ciertos valores que se ponen a disposición de la
subrutina y que tras acabar su ejecución se descartan. El resultado, en cambio, es
un valor que la subrutina calcula y copia en un lugar para que el programa
invocador lo utilice. La figura siguiente ilustra la manipulación de parámetros y
resultado.

Parámetros y resultado de una subrutina

Se necesita establecer las reglas que estipulen cómo y dónde deposita el


programa que invoca una subrutina estos valores y cómo y dónde se deposita el
resultado. En adelante, a la porción de código que realiza la llamada a la
subrutina se le denominará ‘programa llamador’ mientras que al código de la
subrutina se le denominará ‘programa llamado’. Las llamadas a subrutinas se
puede hacer de forma ‘anidada’, es decir, un programa llamado invoca a su vez
a otra subrutina con lo que pasa a comportarse como programa llamador.

 Paso de parámetros a través de registro

El paso de parámetros a través de registro consiste en que el programa


llamador y el llamado asumen que los parámetros se almacenan en ciertos
registros específicos. Antes de la instrucción de llamada el programa llamador
deposita los valores pertinentes en estos registros y la subrutina comienza a
procesarlos directamente.

En general, dada una rutina que recibe n parámetros y devuelve m


resultados, se necesita definir en qué registro deposita el programa llamador la
copia de cada uno de los n parámetros, y en qué registro deposita la subrutina la
copia del resultado obtenido. El ejemplo siguiente muestra las instrucciones
necesarias en el caso de una subrutina que recibe como parámetros dos enteros a
través de los registros %eax y %ebx y devuelve el resultado a través del registro
%ecx.
Ejemplo: Paso de parámetros a través de registros

Al utilizar los registros %eax y %ebx para pasar los parámetros la


subrutina no salva su contenido pues dispone de esos valores como si fuesen
suyos. El registro %ecx, al contener el resultado, tampoco se debe salvar ni
restaurar.

El principal inconveniente que tiene este esquema es el número limitado


de registros de propósito general. En los lenguajes de alto nivel no hay límite en
el número de parámetros que puede tener una función o método en su
definición, y por tanto, si este número es muy alto, el procesador puede no tener
registros suficientes.

A pesar de esta limitación, en el caso de subrutinas con muy pocos


parámetros y que devuelve un único resultado, este mecanismo es muy eficiente
pues el procesador no precisa almacenar datos en memoria. Los sistemas
operativos suelen utilizar esta técnica para invocaciones de subrutinas internas de
estas características.

 Paso de
de parámetros a través de memoria

El paso de parámetros a través de memoria consiste en definir una zona de


memoria conocida tanto para el programa llamador como para el llamado y en
ella se copia el valor de los parámetros y el del resultado para su intercambio. La
ventaja de esta técnica radica en que permite tener un número arbitrario de
parámetros, pues tan sólo se requiere una zona más grande de memoria.

En general, para una subrutina que recibe n parámetros y devuelve m


resultados se define una zona de memoria cuyo tamaño es la suma de los
tamaños de todos ellos así como el orden en el que estarán almacenados. El
siguiente ejemplo muestra las instrucciones necesarias para el caso de una
subrutina que precisa tres parámetros de tamaño 32 bits y devuelve dos
resultados de tamaño 8 bits. Se asume que la zona de memoria está definida a
partir de la etiqueta params.
Ejemplo: Paso de parámetros a través de memoria

El principal inconveniente de esta técnica es que necesita tener estas zonas


de memoria previamente definidas. Además, en el caso de invocación anidada
de subrutinas, se necesitan múltiples espacios de parámetros y resultados pues
mientras la ejecución de una subrutina no termina, éstos siguen teniendo validez.
El incluir esta definición junto con el código de una subrutina parecería una
solución idónea, pues al escribir sus instrucciones se sabe el número y tamaño de
parámetros y resultados. Pero existen subrutinas denominadas ‘recursivas’ que se
caracterizan por contener una invocación a ellas mismas con un conjunto de
parámetros diferente.

La conclusión es que se precisan tantas zonas para almacenar parámetros y


devolver resultados como invocaciones pendientes de terminar en cada
momento de la ejecución. Pero este requisito de vigencia es idéntico al que tiene
la dirección de retorno de una subrutina. Es más, la dirección de retorno se
puede considerar un valor más que el programa llamador pasa al llamado para
que éste lo utilice. En esta observación se basa la siguiente técnica de paso de
parámetros.
 Paso de parámetros a través de la pila

El paso de parámetros a través de la pila tiene múltiples ventajas. En


primer lugar, tanto parámetros como resultados se pueden considerar resultados
temporales que tienen validez en un período muy concreto de la ejecución de un
programa por lo que la pila favorece su manipulación. Además, dada una
secuencia de llamadas a subrutinas, el orden de creación y destrucción de estos
parámetros es el inverso tal y como permiten las instrucciones de gestión de la
pila.

En general, para una subrutina que recibe n parámetro y devuelve m


resultados el programa llamador reserva espacio en la cima de la pila para
almacenar estos datos justo antes de ejecutar la instrucción CALL y lo elimina
justo a continuación. Pero en la subrutina es necesario un mecanismo eficiente
para acceder a la zona de parámetros y resultados. Al estar ubicada en la pila lo
más intuitivo es utilizar el registro %esp que apunta a la cima y el modo de
direccionamiento base + desplazamiento mediante la utilización de los
desplazamientos pertinentes. Pero el inconveniente de este método es que la
cima de la pila puede fluctuar a lo largo de la ejecución de la subrutina y por
tanto los desplazamientos a utilizar varían.

Para que el acceso a los parámetros no dependa de la posición de la cima


de la pila y se realice con desplazamientos constantes a lo largo de la ejecución
de la subrutina, las primeras instrucciones almacenan una copia del puntero de
pila en otro registro (generalmente %ebp) y al fijar su valor, los accesos a la
zona de parámetros y resultados se realizan con desplazamientos constantes.
Pero para preservar el valor de los registros, antes de crear este duplicado es
preciso guardar en la pila una copia de este registro.

El ejemplo muestra las instrucciones necesarias para el caso de una


subrutina que precisa tres parámetros de tamaño 32 bits y devuelve un resultado
de 8 bits.
Ejemplo: Paso de parámetros a través de la pila

La primera instrucción del programa llamador modifica el puntero de la


pila para reservar espacio donde almacenar el resultado.

Al ser una posición de memoria sobre la que se escribirá el resultado no es


preciso escribir ningún valor inicial, de ahí que no se utilice la instrucción push. A
continuación se depositan en la pila los valores de los parámetros. El orden en
que se almacenan debe ser conocido por el programa llamador y el llamado.
Tras la ejecución de la subrutina se eliminan de la pila los parámetros, que al
desempeñar ningún papel, basta con corregir el valor de la cima dejando la pila
preparada para obtener el valor del resultado.

Por su parte, el programa llamado guarda la copia del registro %ebp para
justo a continuación copiar el valor de %esp y fija su valor a la cima de la pila. A
partir de este instante, cualquier dato que se ponga en la pila no afecta el valor
de %ebp y el desplazamiento para acceder a los parámetros es respectivamente
de 8, 12 y 16 pues en la posición debajo de la cima se encuentra la dirección de
retorno. Para depositar el resultado se utiliza el desplazamiento 20. Tras terminar
el cálculo del resultado se procede a deshacer la estructura de datos creada en la
pila en orden inverso. Primero se descargan de la pila los registros salvados y a
continuación se restaura el valor del registro %ebp dejando en la cima la
dirección de retorno que necesita la instrucción ret.
A la porción de memoria en la pila que contiene el espacio para la
devolución de resultados, los parámetros, la dirección de retorno, la copia de
%ebp se le denomina el ‘bloque de activación’. Al registro %ebp que ofrece un
punto fijo de referencia a los datos se le denomina el ‘puntero’ al bloque de
activación.

 Almacenamiento de variables locales a una subrutina

Además de la capacidad de definir y ejecutar subrutinas, los lenguajes de


programación de alto nivel permiten la definición de variables locales. El ámbito
de validez se reduce al instante en que se está ejecutando el código de la
subrutina. De nuevo se precisa un mecanismo que gestione de forma eficiente
estas variables. El ejemplo siguiente muestra la definición de un método en Java
en el que las variables i, str y p son de ámbito local.

Ejemplo: Definición de variables locales a un método

El ámbito de estas variables no impide que el valor de alguna de ellas sea


devuelto como resultado tal y como muestra el método del ejemplo. La última
línea copia el valor de la variable local en el lugar en el que se devuelve el
resultado, y por tanto está disponible para el programa llamador.

El ámbito de estas variables es idéntico al de los parámetros y al de la


dirección de retorno, por lo que para almacenar estas variables se pueden utilizar
cualquiera de las tres técnicas descritas anteriormente: en registros, en posiciones
arbitrarias de memoria y en la pila.
El almacenamiento en la pila se hace en el bloque de activación justo a
continuación de haber establecido el registro %ebp como puntero al bloque de
activación. De esta forma, como el número de variables locales es siempre el
mismo, utilizando desplazamientos con valores negativos y el registro base %ebp
se accede a ellas desde cualquier punto de la subrutina.
GESTIÓN DEL BLOQUE DE ACTIVACIÓN

De las técnicas descritas para la invocación de subrutinas, la que crea el


bloque de activación en la pila es la más utilizada por los lenguajes de alto nivel.
Se muestran los pasos a seguir por el programa llamador y el llamado para crear
y destruir el bloque de activación.

Tras restaurar el valor de los registros utilizados por la subrutina, el estado


de la pila es tal que en la cima se encuentra el espacio para las variables locales y
a continuación la copia del valor anterior de %ebp. Como el propio %ebp
apunta a esa misma posición, la forma más fácil de restaurar la cima de la pila al
valor correcto es asignándole a %esp el valor de %ebp. De esta forma no es
preciso tener en cuenta el tamaño de la zona reservada para las variables locales.
Esta técnica funciona incluso en el caso de que una subrutina no tenga variables
locales.

Ejemplo de evolución del bloque de activación


activación

Se considera un programa que invoca a la subrutina cuenta que dada la


dirección de un string terminado en cero y un carácter, devuelve el número de
veces que el carácter aparece en el string como entero. La pila que recibe la
subrutina tiene, en orden creciente de desplazamiento desde el puntero al
bloque de activación, la dirección de retorno (siempre está presente como
primer valor), la dirección del string, el carácter a comprobar como byte menos
significativo del operando en la pila y el espacio para el resultado. El fragmento
de código para invocar a esta subrutina se muestra en el siguiente ejemplo, se
asume que la letra a buscar está almacenada en la etiqueta letra y el string en la
etiqueta mensaje.
mensaje
Pasos para la gestión del bloque de activación

Ejemplo: Invocación de la rutina cuenta

La instrucción push letra tiene por operando una etiqueta que apunta a un
dato de tamaño byte. Como los operandos de la pila son de 4 bytes, en ella se
depositan la letra y los tres siguientes bytes. Esto no tiene importancia porque la
subrutina únicamente accede al byte de menos peso tal y como se ha
especificado en su definición. La figura muestra la evolución de la pila desde el
punto de vista del programa llamador.
Evolución de la pila desde el punto de vista del programa llamador

Ejemplo: Código de la rutina cuenta


ACCESOS CONDICIONALES
Los accesos condicionales son aquellos que nos permiten controlar el flujo
de ejecución de un programa escrito en lenguaje ensamblador es por ello que se
van a describir de manera breve las instrucciones de control disponibles, la
mayor parte de ellas se utilizan en conjunción con alguna instrucción que afecta
el estado de las banderas la instrucción CMP sirve para este propósito por
ejemplo la instrucción JA genera un salto si, después de la instrucción de
comparación CF=ZF=0, la ejecución de las demás instrucciones de salto
depende del estado de varios registros y banderas con excepción de la
instrucción de salto incondicional JMP.

Por otra parte algunas de estas instrucciones de salto solo trabajan con
cantidades sin signos mientras otras se aplican a operando con signos todos los
saltos con excepción de JMP trasfieren control a una etiqueta destino
denominada etiqueta corta lo anterior significa que la etiqueta debe encontrarse
dentro del intervalo que va de -128 a + 127 bytes de la instrucción de salto.

En la siguiente secuencia de instrucciones:

CMP AH, AL

JAE CONT1

Se resta el registro AL al registro AH y la bandera de acarreo se activa, si el


contenido de AH es menor que el de AL. El salto JAE ( salta si esta por encima o
es igual a la etiqueta CONT1.todas estas instrucciones de accesos condicionales
son similares asi que debemos familiarizarnos y comprender cabalmente como
se lleva a cabo la transferencia de control de un programa

Otro ejemplo de uso de las instrucciones de salto es la siguiente:

CMP AH,DDDV ; Compare contenido de AH y DDDV

JNE CONT1 ; Salta a CONT1 si no igual a …

JMP CONT2 ; Vete a CONT1( incondicional)

CONT1: ……..

CONT2: …….

Primero se compara el contenido del registro AH con el de DDDV y si


estos no son iguales entonces se transfiere el control a la etiqueta CONT1 de lo
contrario se ejecuta el código que se encuentra inmediatamente después de la
instrucción JNE
GRUPO DE INSTRUCCIONES DE SALTO
Instrucción Propósito
JA etiqueta corta Salta si, por arriba/sino por debajo o es igual
(JNBE)

JAE etiqueta corta Salta si, por arriba o es igual/ si no por debajo
(JNB)

JB etiqueta corta Salta si por debajo/ si no por arriba y no es igual/


(JNAE) (JC) si existe acarreo

JBE etiqueta corta Salta si por debajo o es igual/ si, no por arriba
(JNA)

JCXZ etiqueta corta Salta si CX es cero

JE etiqueta corta Salta si, es igual/ si, es cero


(JZ)

JG etiqueta corta Salta, si es mayor/ si, no es menor o no es igual


(JNLE)

JGE etiqueta corta Salta si es mayor o igual / si, no es menor


(JNL)

JL etiqueta corta Salta, si es menor/ si, no es mayor o no es igual


(JNGE)

JLE etiqueta corta Salta, sí es menor o igual/ si no es mayor


(JNG)

JMP destino Salta

JNC etiqueta corta Salta, si no existe acarreo

JNE etiqueta corta Salta, si no es igual/ si, no es cero


(JNZ)

JNO etiqueta corta Salta, si no existe sobreflujo

JNP etiqueta corta Salta, si no es paridad par/ si la paridad es impar


(JPO)

JNS etiqueta corta Salta si, no hay signo/ si, positivo

JO etiqueta corta Salta si, existe sobreflujo

JP etiqueta corta Salta si, paridad activada/ si, la paridad es par


(JPE)
JS etiqueta corta Salta si, signo activado
CONVERSIONES DE DATOS
En los sistemas de computadoras, los datos rara vez están en la forma
correcta. Una tarea principal del sistema es convertir los datos de una forma a
otra. En esta sección se describen las conversiones entre binario y ASCII. Los
datos binarios se extraen de un registro o de la memoria y se convierten en ASCII
para exhibirlos en la pantalla. Los datos en ASCII se convierten a binarios al
escribirlos en el teclado. También se aplica la conversión entre datos ASCII y
hexadecimales.

Conversión de binario a ASCII

La conversión de binario a ASCII se logra en dos formas:

 Con la instrucción AAM si el numero es menor de 100.


 Con una serie de divisiones decimales (dividir entre 10).

La instrucción AAM convierte el valor que hay en AX a un número de dos


dígitos en BCD no empacado. Si el numero en AX es 0062H (98 decimal) antes
de ejecutar AAM, AX contendrá un 0908H después de que se ejecute AAM. No
trata de un código ASCII, sino que se convierte a ASCII al agregar un 303011 a
AX. En el ejemplo se ilustra un procedimiento que procesa el valor binario en
AL (0 a 99) y lo exhibe en la pantalla como decimal. Este procedimiento borra
un cero precedente, lo cual ocurre para los 0 a 9 con un código especial ASCII.

Ejemplo
DESP PROC FAR
PUSH DX ;salvar DX
XOR AH,AN ;borrar AN
AAM ;convertir a BCD
ADD AH,20H ;sumar 20H
CMP AH,20H ;probar si hay un cero avanzado
JE DESP1 ;si hay cero inicial
ADD AH,10H ;convertir a ASCII

DESP1:

PUSH AX ;
MOV DL,AH ;exhibir primer digito
MOV AH,6 ;
INT 21H ;
POP AK ;
ADD AL,30H ;convertir a ASCII
MOV DL,AL ;
MOV AH,6 ;exhibir segundo digito
INT 21H ;
POP DX ;recuperar DX
RET
DESP END P

La razón por la cual AAM convierte cualquier número entre 0 y 99 a un


número BCD sin empacar, de dos dígitos, es porque divide AX entre 10. El
resultado se deja en AX, con lo cual AH contiene el cociente y AL el residuo. Este
mismo sistema de dividir entre 10 se puede ampliar o expandir para convertir
cualquier número completo de binario a una cadena de caracteres ASCII que se
puede exhibir en la pantalla. El algoritmo para convertir de binario a ASCII es:

1. Dividir entre 10 y conservar el cociente en la pila como digito BCD


significativo.
2. Repetir el paso 1 hasta que el cociente sea 0.
3. Recuperar cada residuo y sumarle un 30H para convertir a ASCII antes
de exhibir o imprimir.

Ejemplo:
Ejemplo:

DESPX PROC FAR

PUSH DX ;salvar BX, CX y DX

PUSH CX ;

PUSH BX ;

XOR CX,CX ;borrar CX

MOV BX,10 ;cargar 10

DESPX1:

XOR DX,DX ;borrar DX

DIV BX

PUSH DX ;salvar el residuo

INC CX ;contador del residuo

OR AX,AX ;probar el cociente

JNZ DESPX1 ;si no es cero

DESPX2:
POP DX ;exhibir numero

MOV AH,6

ADD DL,30H ;convertir a ASCII

INT 21H

LOOP DESPX2 ;repetir

POP BX ;recuperar BX, CX y DX

POP CX

POP DX

RET

DESPX ENDP

En el ejemplo se muestra la forma en que el contenido de 16 bits sin signo


de AX se convierte a ASCII y se escribe en la pantalla. En este caso se divide entre
10 y se salva el residuo en la pila después de cada división, para su posterior
conversión a ASCII. Después de haber convertido todos los dígitos, se extraen
los residuos de la pila y se los convierte a códigos ASCII para exhibir el resultado
en la pantalla. Este procedimiento también borra cualesquiera ceros precedentes
que pudieran ocurrir.

Conversión de ASCII a binario


Las conversiones de ASCII a binario suelen comenzar con una entrada del
teclado. Si se oprime una sola tecla, la conversión ocurre cuando se resta 30H,
pero hay un paso adicional. Después de restar 30H, el número se suma al
resultado un ves que el resultado anterior se multiplico por 10. El algoritmo para
convertir de binario a ASCII es:

1. Empezar con un resultado binario de cero.


2. Restar 30H del carácter tecleado en el teclado para convertir a
BCD.
3. Multiplicar el resultado por 10 y sumar el nuevo digito en BCD.
4. Repetir los pasos 2 y 3 hasta que el carácter tecleado no sea un
número en código ASCII.

Ejemplo LEEN PROC FAR

PUSH BX ;salvar BX y CX

PUSH CX

MOV CX, 10 ; cargar 10

XOR BX, BX ; borrar el resultado


LEEN1:

MOV AH, 6 ;leer tecla

MOV DL, OFFH

INT 21H

JE LEEN1 ; esperar a la tecla

CMP AL,´D´ ;probar contra de 0

JB LEEN2 ; si es menor de 0

CMP AL, ´9´ ; probar contra de 9

JA LEEN2 ; si es mayor de 9

MOV DL, AL ; eco tecla

INT 21H

SUB AL,´0´ ;convertir a BCD

PUSH AX

MOV AX,BX ;multiplicar por 10

MUL CX

MOV BX,AX ;salvar el producto

POP AX

XOR AH,AH

ADD BX,AX ;sumar BCD al producto

JMP LEEN1 ;repetir

LEEN2:

MOV AX,BX ;cargar binario en AX

POP CX ; recuperar BX y CX

POP BX

RET

LEEN ENDP
En el ejemplo se ilustra el procedimiento para poner en ejecución este
algoritmo. En este caso, el número binario retorna al registro AX con un
resultado de 16 bits. Si se requiere un resultado más grande, hay que volver a
efectuar el procedimiento para una suma de 32 bits. Cada vez que se llama a este
procedimiento, lee un número en el teclado hasta que se oprima una tecla que
no sea del 0 al 9.

Empleo De Tablas Para Conversiones De Datos

Las tablas se utilizan a menudo para convertir datos de una forma a otra.
Una tabla esta formada en la memoria como una lista de datos que se consultan
por un procedimiento para efectuar conversiones. En el caso de muchas tablas, a
menudo se puede utilizar la instrucción XLAT para buscar o consultar datos en
una tabla siempre y cuando a tabla contenga datos de 8 bits de ancho y con
longitud igual o menor de 256 bytes.

Conversión de BSD a un código de 7 segmentos. Una aplicación sencilla


en la que se emplea una tabla de conversión de BCD al código de 7 segmentos.
En el ejemplo se ilustra un atabla que contiene los códigos de 7 segmentos para
los numero 0 a 9. Estos códigos se emplean con la exhibición visual de 7
segmentos que se muestran en la figura. En esta exhibición de 7 segmentos se
utilizan entradas activas en alto ( 1 lógico) para encender un segmento. El código
esta dispuesto de modo que el segmento a este en la posición de bit 0 y el
segmento g este en la posición de bit 6. A posición de bit 7 es cero en este
ejemplo, pero se puede emplear para exhibir un punto decimal.

Ejemplo

SEG PROC FAR

PUSH BX MOV BX, OFFSET TABLA

XLAT CS: TABLA

POP BX

RET

TABLA DB 3FH

DB 6

DB 5BH

DB 4FH

DB 66H
DB 5DH

DB 7DH

DB 7

DB 7FH

DB 6FH

SEG7 ENDP

EXHIBICION
EXHIBICION VISUAL 7 SEGMENTOS

Byte de control

0 g f e d c b a

El procedimiento que efectúa la conversión contiene solo dos


instrucciones y se supone que AL contiene el digito que se va a convertir al
código de 7 segmentos. Una de las instrucciones direccionara a la tabla porque
carga su dirección en BX y la otra efectúa la conversión y retorna el código de 7
segmentos en AL.

Debido a que la tabla de consulta esta ubicada en el segmento de código


y a que la instrucción XLAT accede al segmento de datos en forma implícita la
instrucción XLAT incluye un cambio de segmento. Se debe tener en cuenta que se
agrega un operando mudo (TABLA) a l instrucción XLAT para poder agregar el
prefijo (CS:) de cambio al segmento de código para la instrucción. Las
instrucciones LODS y MOVS también cambian de segmento en la misma forma
que XLAT con el empleo de un operador ficticio.
Empleo de una tabla para accesar a datos ASCII

Algunas técnicas para programación requieren convertir los códigos


numéricos a cadenas de caracteres en ASCII. Por ejemplo, se necesita exhibir los
días de la semana para un programa de calendario. Debido a que la cantidad de
caracteres en ASCII para cada día es diferente, se debe emplear algún tipo de
tabla de los días de la semana que están en código ASCII.

Ejemplo

DIAS PROC FAR

PUSH DX

PUSH SI ; salvar DX y SI

MOV SI, OFFSET TABD ; direccionar TABD

XOR AH, AH ; borrar AX

ADD AX,AX ; duplicar AX

ADD SI, AX ; modificar dirección de la tabla

MOV DX, CS: [SI] ; obtener dirección de la cadena

MOV AX , CS; cambiar segmento de dato

PUSH DS

MOV DS, AX

MOV AH, 9

INT 21H ; exhibir la cadena

POP DS

POP SI ; recuperar DX y SI

POP DX

RET

DTAB DW DOM, LUN, MAR, MIE, JUE, VIER, SAB

SUN DB ‘DOMINGO S’

MON DB ‘LUNES S’
TUE DB ‘MARTES S’

WEN DB ‘MIERCOLES S’

THU DB ‘JUEVES S’

FRI DB ‘VIERNES S’

SAT DB ‘SABADO S’

DIAS ENDP

En el ejemplo se presenta una tabla de consulta para cadenas de caracteres


ASCII en el segmento de código. Cada cadena de caracteres contiene un día de la
semana en código ASCII. L tabla señala cada día de la semana. El procedimiento
se accesa a cada dia de la semana utiliza el registro AL y los números 0 a 6
significan domingo hasta sábado. Si AL contiene un dos cuando se llama a este
procedimiento entonces aparece en pantalla la palabra martes.

Para que este procedimiento direccione a la tabla, primero hay que cargar
su dirección en el registro SI. Después el número que hay en AL se convierte a un
número de 16 bits y se duplica porque la tabla contiene 2 bytes en cada entrada.
Luego, este índice e agrega en SI para direccionar la entrada correcta en la tabla
de consulta. Ahora la dirección de la cadena de caracteres en ASCII se carga en
DX por medio de la instrucción MOV DX, CS:[SI].

Antes de llamar la instrucción INT 21H DOS, se salva el registro DS en la


pila y se carga con la dirección del segmento CS. Esto permite que la función
09H del DOS (exhibir cadena) se utilice para exhibir el día de la semana. Este
procedimiento convierte los números del 0 a 6 a los días de a semana.

COMO
COMO DOCUMENTAR UN SISTEMA DE SOFTWARE

Objetivo

Un sistema pobremente documentado carece de valor aunque haya


funcionado bien en alguna ocasión. En el caso de programas pequeños y poco
importantes que sólo se utilizan durante un corto periodo de tiempo, unos
cuantos comentarios en el código podrían ser suficientes. No obstante, la
mayoría de los programas cuya única documentación es el código, se quedan
obsoletos rápidamente y es imposible mantenerlos. El que la dedicación de un
poco de esfuerzo a la documentación sea recompensado incluso dentro de los
límites de un pequeño proyecto, constituye una sorpresa para la mayoría de los
novatos.
A menos que usted sea infalible y viva en un mundo en el que nada
cambia, tendrá que volver a consultar el código que ya está escrito, y pondrá en
duda decisiones que tomó durante el desarrollo del mismo. Si no documenta sus
decisiones, se verá siempre cometiendo los mismos errores y tratando de
comprender lo que pudo haber descrito fácilmente en una ocasión. La falta de
documentación no sólo genera trabajo adicional, sino que también tiende a
dañar la calidad del código. Si no posee una nítida caracterización del problema,
es imposible que desarrolle una solución clara.

Aprender a documentar software es una tarea complicada y exige un


criterio de ingeniería maduro. Documentar de forma concisa es un error habitual,
pero el otro extremo puede resultar igual de perjudicial: si escribe
documentaciones extensas, éstas atosigarán al lector y constituirán una carga a la
hora de conservarlas. Es esencial documentar sólo los asuntos correctos. La
documentación no sirve de ayuda para nadie si su extensión desanima a la gente
a la hora de leerla.

Los principiantes tienen la tentación de centrar sus esfuerzos en temas


sencillos, ya que éstos les resultan más fáciles de documentar. Esto es una pérdida
de tiempo; no se aprende nada del esfuerzo y se termina escribiendo una
documentación que es cualquier cosa excepto útil. Los principiantes también
tienden a mostrarse reacios con los problemas de documentación. Esto trae
consigo poca visión de futuro: si usted sabe que algún aspecto de su diseño no es
del todo correcto, que alguna parte del problema no se ha aclarado o que es
posible que parte del código tenga errores, ¡dígalo! Hará que el lector ahorre
tiempo dándole vueltas a algo que aparentemente es erróneo, se acordará de
dónde tiene que mirar si encuentra problemas y acabará teniendo una
documentación más útil y honesta.

Otro asunto es cuándo documentar. Aunque algunas veces es conveniente


posponer la tarea de la documentación mientras se realizan experimentos, los
programadores con experiencia suelen documentar de forma metódica incluso el
código provisional, los análisis de un problema inicial y los borradores de un
diseño. Ellos creen que esto hace que la experimentación sea más productiva.
Además, dado que han tomado la documentación como hábito, les resulta
normal documentar a medida que van avanzando.

Este boletín facilita directrices sobre cómo documentar un sistema de


software como el proyecto 6.170. Proporciona una estructura esquemática y
algunos elementos obligatorios, pero deja bajo su propio criterio todo lo
referente a los detalles. Es esencial que no piense que la documentación es un
asunto rutinario y aburrido; si lo hace, su documentación no servirá para nada y
será penosa a la hora de leerla y escribir. Así que documente de forma
consciente: pregúntese a medida que lo hace, por qué lo hace y si está
empleando su tiempo de la forma más eficaz.

Puede cortar y pegar en su documentación el texto de cualquiera de los


boletines que le hemos facilitado. En concreto, es posible que quiera usar partes
del boletín de problemas para describir los requisitos. No obstante, asegúrese de
indicar claramente cualquier cambio que realice, ¡para que su ayudante técnico
no tenga que emular manualmente el programa diff de Unix!

Esquema

Su documentación debería tener la siguiente estructura. Se facilita un


número de páginas orientativo, típico de un proyecto 6.170; lo que presentamos
a continuación son directrices, no un requisito.

1. Requisitos

La sección de requisitos describe el problema que se está solventando junto


con la solución. Esta sección de la documentación resulta interesante tanto para
los usuarios como para los implementadores; no debería contener detalles sobre
la estrategia de implementación en concreto. Las otras partes de la
documentación del sistema no serán de interés para los usuarios, únicamente
para los implementadores, los encargados del mantenimiento y demás personal.

1. Visión general (hasta 1 página) Una explicación del objetivo del sistema y
de la funcionalidad del mismo.
2. Especificación revisada. Si le facilitaron especificaciones detalladas del
comportamiento del sistema, es probable que encuentre que ciertas partes
del mismo se encuentran infraespecificadas o son ambiguas. En esta
sección debería aclarar tanto cualquier suposición que haya hecho sobre el
significado de los requisitos, como cualquier extensión o modificación de
los mismos.
3. Manual de usuario (1 - 5 páginas). Una descripción detallada sobre cómo
el usuario puede utilizar el sistema, qué operaciones puede llevar a cabo,
cuáles son los argumentos de la línea de comando, etc. Las
especificaciones detalladas de los formatos deberían relegarse al Apéndice.
Cualquier suposición relativa al entorno debería explicitarse aquí: por
ejemplo, observe si el programa sólo se ejecuta en ciertas plataformas, si
supone que la jerarquía de un cierto directorio está presente, si existen
otras aplicaciones, etc. Junto con la visión general, este manual debería
proporcionar toda la información que un usuario del sistema necesita.
4. Funcionamiento (1/2 página). ¿Qué recursos necesita el sistema para una
operación normal y qué espacio y tiempo se espera que consuma?
5. Análisis del problema (2 - 10 páginas). Una descripción clara del problema
subyacente. Esto incluye el modelo conceptual que hay detrás del diseño
(y posiblemente la interfaz de usuario), si no se ha tratado ya. El análisis
del problema generalmente incluye uno o más modelos de objeto del
problema, definiciones de sus conjuntos y relaciones y una discusión de
asuntos complicados. Los objetos en los modelos de objeto del problema
proceden del dominio del problema, no del código. Los modelos de
objeto deberían incluir tanto diagramas como cualquier restricción textual
fundamental, y deberían estar claramente expuestos para una correcta
legibilidad. Esta parte también debería describir alternativas que hayan
sido consideradas pero que se han rechazado, con razones, asuntos no
resueltos o aspectos que no hayan sido totalmente aclarados y que vayan
a solventarse más tarde.

Puede que los casos de uso le resulten útiles cuando escriba la especificación
revisada y/o el manual de usuario. Un caso de uso es un objetivo específico y
una lista de acciones que un usuario lleva a cabo para conseguir dicho objetivo.
Un cliente puede, entre otras cosas, examinar la lista de acciones para decidir si la
interfaz de usuario es aceptable. Si la colección de casos de uso abarca todos los
objetivos deseados por el usuario, el cliente puede tener la seguridad de que el
sistema cumplirá con su objetivo.

2. Diseño

La sección de diseño de su documentación proporciona un cuadro de alto


nivel de su estrategia de implementación.

1. Visión general (1/2 - 3 páginas). Una visión general del diseño:


organización de alto nivel, cuestiones de diseño especialmente
interesantes, uso de librerías y otros módulos de terceros e indicadores de
aspectos no resueltos o proclives al cambio. También incluye problemas
con el diseño: decisiones que pueden resultar erróneas y los análisis de las
consecuencias entre la flexibilidad y el funcionamiento que pueden ser
juzgadas negativamente.
2. Estructura en tiempo de ejecución (1 - 5 páginas). Una descripción de la
estructura del estado del programa en ejecución, expresada como un
modelo de objeto de código. Éste debería ocultar las representaciones de
los tipos de datos abstractos; su propósito consiste en mostrar las
relaciones entre objetos. Los modelos de objeto deberían incluir tanto
diagramas como cualquier restricción textual fundamental, y deberían
estar claramente expuestos para una correcta legibilidad. Las
representaciones de los tipos de datos deberían explicarse (con sus
funciones de abstracción e invariantes de representación) si esas
representaciones son poco comunes, especialmente complejas o decisivas
para el diseño global. Observe que las funciones de abstracción y los
invariantes de representación deberían aparecer aún así en su sitio normal
en el propio código.
3. Estructura del módulo (1 - 5 páginas). Una descripción de la estructura
sintáctica del texto del programa, expresada como un diagrama de
dependencia entre módulos. Debería incluir la estructura del paquete y
mostrar tanto las interfaces de Java™ del programa, calendario, material
de clase, trabajos, exámenes, lecturas obligatorias, otras fuentes, prácticas,
presentaciones, herramientas y proyectos, como las clases. No es necesario
exhibir las dependencias de las clases en la API de Java™ del programa,
calendario, material de clase, trabajos, exámenes, lecturas obligatorias,
otras fuentes, prácticas, presentaciones, herramientas y proyectos. Su
MDD (diagrama de dependencia entre módulos) debería estar claramente
expuesto para una correcta legibilidad. Explique por qué se eligió esa
estructura sintáctica en particular (ej.: introducción de interfaces para el
desacoplamiento: qué desacoplan y por qué), y cómo se utilizaron los
patrones de diseño concretos.

Para explicar la descomposición y otras decisiones de diseño, exponga que


aportan simplicidad, extensibilidad (facilidad para añadir características nuevas),
particionalidad (los distintos miembros del equipo pueden trabajar en las
diferentes partes del diseño sin necesidad de mantener una comunicación
constante) u objetivos similares relativos a la ingeniería de software.

3. Pruebas

La sección de pruebas de su documentación indica el enfoque que usted


ha elegido para verificar y validar su sistema. (En un sistema real, esto podría
incluir tanto las pruebas de usuario para determinar la idoneidad del sistema
como solución al problema descrito en la sección de requisitos, como la
ejecución de suites de prueba para verificar la corrección algorítmica del código).
Del mismo modo que no debería comunicar el diseño de su sistema presentando
el código o incluso enumerando las clases, no debería únicamente enumerar las
pruebas realizadas. En vez de hacer esto, explique cómo se seleccionaron las
pruebas, por qué éstas bastaron, por qué un lector debería creer que no se ha
omitido ninguna prueba importante y por qué debería creer que el sistema
realmente funcionará como se desee cuando se ponga en práctica.

1. Estrategia (1 - 2 páginas). Una explicación de la estrategia general de las


pruebas: blackbox y/o glassbox, top down (de arriba hacia abajo) y/o
bottom up (de abajo hacia arriba), tipos de test beds (bancos de prueba) y
manejadores de tests que se han utilizado, fuentes de datos del test, suites
de prueba, métrica de cobertura, comprobación en tiempo de
compilación frente a sentencias en tiempo de ejecución, razonamientos
sobre su código, etc. Es posible que quiera usar técnicas distintas (o
combinaciones de técnicas) en las diferentes partes del programa.
Justifique sus decisión en cada caso.

Explique qué tipo de errores espera encontrar (¡y cuáles no!) con su
estrategia. Comente qué aspectos del diseño dificultaron o facilitaron la
validación.

2. Resultados del test (1/2 - 2 páginas). Resumen de qué pruebas se han


realizado y cuáles no, si es que queda alguna: qué módulos se han
probado, y si se han probado a fondo. Indique el grado de confianza del
código: ¿Qué tipo de defectos se han eliminado? ¿Cuáles son los que
podrían quedar?

4. Reflexión

La sección de reflexión (vulgarmente conocida como "post mortem") del


documento es donde puede hacer generalizaciones partiendo de éxitos o fallos
concretos, hasta llegar formular reglas que usted u otros puedan utilizar en el
futuro desarrollo de software. ¿Qué fue lo que más le sorprendió? ¿Qué hubiera
deseado saber cuando comenzó? ¿Cómo podría haber evitado los problemas que
encontró durante el desarrollo?

1. Evaluación (1/2 - 1 páginas). Lo que usted considere éxitos o fracasos del


desarrollo: problemas de diseño no resueltos, problemas de
funcionamiento, etc. Identifique cuáles son los rasgos importantes de su
diseño. Señale técnicas de diseño o implementación de las que se sienta
especialmente orgulloso. Explique cuáles fueron los errores que cometió
en su diseño y los problemas que causaron.
2. Lecciones (0,2 - 1 página). Qué lecciones ha aprendido de la experiencia:
cómo podría haberlo hecho de otra forma una segunda vez, y cómo los
errores de diseño e implementación pueden corregirse. Describa qué
factores causaron problemas, como hitos errados, o errores y limitaciones
conocidos.
3. Errores y limitaciones conocidos. ¿De qué manera su implementación no
llega a alcanzar la especificación? Sea exacto. Aunque perderá puntos por
errores y características no presentes, obtendrá puntos parciales por
identificar de manera exacta esos errores y el origen del problema.
5. Apéndice

El apéndice contiene detalles de bajo nivel acerca del sistema, que no son
necesarios para comprenderlo en un nivel superior, pero que se exigen para
usarlo en la práctica o para verificar afirmaciones realizadas en cualquier parte
del documento.

1. Formatos. Una descripción de todos los formatos adoptados o


garantizados por el programa: para un fichero E/O (fichero para entrada
y salida de datos), argumentos de la línea de comando, diálogos de
usuario, formatos de los mensajes para las comunicaciones en red, etc.
Éstos deberían desglosarse en formatos visibles para el usuario, que son
parte conceptual de los requisitos visibles para el usuario y del manual de
usuario, y en formatos interiores que constituyen una parte conceptual de
otros componentes de su documentación.
2. Especificaciones del módulo. Debería extraer las especificaciones de su
código y presentarlas por separado aquí. Si escribe sus comentarios en el
estilo aceptado por Javadoc con el doclet de 6.170 , podrá generar
documentos de la especificación de forma automática a partir del código.
La especificación de un tipo abstracto debería incluir su visión general,
campos de la especificación e invariantes abstractas (restricciones de
especificación). La función de abstracción y el invariante de representación
no forman parte de la especificación de un tipo.
3. Casos de prueba. Idealmente, su banco de pruebas lee tests de un archivo
de casos de prueba en un formato que resulta cómodo de leer y escribir.
No es necesario que incluya casos de prueba muy extensos; por ejemplo,
simplemente podría observar el tamaño de una entrada aleatoria
generada para realizar pruebas de estrés y proveer al programa que
generó los tests. Indique cuál es la utilidad de cada grupo de tests (ej.
"pruebas de estrés, entradas enormes", "pruebas de partición, todas las
combinaciones de +/-/0 para argumentos enteros").

DOCUMENTACIÓN DEL CÓDIGO

Comentarios a nivel de especificación

Tipos de datos abstractos. Cada tipo de datos abstractos (clase o interfaz)


debería tener:

1. Una sección de visión general que dé una explicación de una o dos líneas
sobre qué objetos del tipo representan y si son mutables.
2. Una lista de campos de especificación.
especificación Podría haber sólo uno; por
ejemplo, un conjunto podría tener el campo elems representando al
conjunto de elementos. Cada campo debería tener un nombre, un tipo y
una breve explicación. Podría resultarle útil definir campos derivados
adicionales que facilitaran la escritura de las especificaciones de los
métodos; para cada uno de éstos, debería indicar que es derivado y
explicar cómo se ha obtenido a partir de los otros campos. Puede que
haya invariantes de especificación que restrinjan los posibles valores de los
campos de la especificación; si es así, debería precisarlos.

Especificaciones del método. Todos los métodos públicos de las clases


deberían tener especificaciones; los métodos privados complicados también
deberían especificarse. Las especificaciones del método deberían seguir la
estructura requires (requiere), modifies (modifica), throws (lanza), effects
(resulta), returns (devuelve) que aparece descrita en el boletín de
especificacionesy en clase. Observe que para el curso 6.170, puede suponer que
los argumentos no son nulos, de no especificarse lo contrario.

Comentarios a nivel de implementación

Notas para la implementación. Los comentarios de una clase deberían


incluir los siguientes elementos (los cuales, en el caso de clases destacadas,
aparecen también en la sección Estructura en tiempo de ejecución de la
documentación del diseño):

1. Una función de abstracción que defina cada campo de la especificación en


función de los campos de representación. Las funciones de abstracción
solamente son necesarias para las clases que son tipos de datos abstractos,
y no para clases de tipo excepciones o como cualquier objeto de la GUI.
representación Las RIs (implementaciones de referencia)
2. Un invariante de representación.
son necesarias para cualquier clase que posea una representación (ej. no la
mayoría de las excepciones). Es muy recomendable que pruebe los
invariantes en un método checkRep() donde sea viable. Tenga cuidado al
incluir en sus invariantes suposiciones acerca de lo que puede o no puede
ser nulo.
3. Para las clases con representaciones complejas, añada una nota que
explique la elección de la representación (también conocida como
representación racional):
racional qué análisis de ventajas y desventajas se
realizaron y qué alternativas se han considerado y cuáles se han rechazado
(y por qué).

Sentencias en tiempo de ejecución. Éstas deberían utilizarse juiciosamente,


como se explicó en clase. Puede consultar Maguire Steve, Writing Solid Code,
Microsoft Press, 1995, donde encontrará una discusión más extensa sobre la
forma en que las sentencias.
PRÁCTICA #7

Estudio Del Sistema De Audio Del Computador

SÍNTESIS DE SONIDO

La producción de sonido es uno de los puntos más débiles de los


ordenadores compatibles, que sólo superan por muy escaso margen a alguno de
los micros legendarios de los 80, si bien las tarjetas de sonido han solventado el
problema. Pero aquí nos conformaremos con describir la programación del
altavoz. En todos los PCs existen dos métodos diferentes para generar sonido,
con la utilización del 8254 o sin él, que veremos por separado.

Control directo del altavoz.

El altavoz del ordenador está ligado en todas las máquinas al bit 1 del
puerto E/S 61h. Si se hace cambiar este bit (manteniéndolo durante cierto tiempo
alto y durante cierto tiempo bajo, repitiendo el proceso a gran velocidad) se
puede generar una onda cuadrada de sonido. Cuanto más deprisa se realice el
proceso, mayor será la frecuencia del sonido. Por fortuna, la baja calidad del
altavoz del PC redondea la onda cuadrada y produce un sonido algo más
musical de forma involuntaria. No existe, en cualquier caso, control sobre el
volumen, que dada la calidad del altavoz también está en función de la
frecuencia.

Este método de producción de sonido tiene varios inconvenientes. Por un


lado, la frecuencia con que se hace vibrar al bit que lo produce, si no se tiene
mucho cuidado, está a menudo más o menos ligada a la capacidad de proceso
del ordenador: esto significa que el sonido es más grave en máquinas lentas y
más agudo en las rápidas. Esto es particularmente grave y evidente cuando las
temporizaciones se hacen con bucles de retardo con registros de la CPU: la
frecuencia del sonido está totalmente a merced de la velocidad de la máquina en
que se produce. Es por ello que el pitido de error que produce el teclado es a
menudo distinto de unos ordenadores a otros, aunque tengan el mismo KEYB
instalado.

Otro gran inconveniente de este método es que las interrupciones,


fundamentalmente la del temporizador, producen fuertes interferencias sobre el
sonido. Por ello, es normal tenerlas inhibidas, con el consiguiente retraso de la
hora. Por último, un tercer gran inconveniente es que la CPU está
completamente dedicada a la producción de sonido, sin poder realizar otras
tareas mientras tanto.
Antes de comenzar a producir el sonido con este método hay que bajar la
línea GATE del 8254, ya que cuando está en alto y se activa también el bit 1 del
puerto E/S 61h, el temporizador es el encargado de producir el sonido (este es el
segundo método, como veremos). Por tanto, es preciso poner primero a cero el
bit 0 del mismo puerto (61h):

CLI ; evitar posible INT 8, entre otras


IN AL,61h
AND AL,11111110b
JMP SHORT $+2 ; estado de espera para E/S
OUT 61h,AL ; bajar GATE del contador 2 del 8254
MOV CX,100h ; 256 vueltas
otro_ciclo: PUSH CX
IN AL,61h
XOR AL,2 ; invertir bit 1
JMP SHORT $+2
OUT 61h,AL
MOV CX,300 ; constante de retardo
retardo: LOOP retardo
POP CX
LOOP otro_ciclo
STI

Control Del Altavoz Por El Temporizador.


Temporizador.

El otro método posible consiste en emplear el contador 2 del


temporizador conectado al altavoz; así, enviando el período del sonido
(1.193.180/frecuencia_en_Hz) a dicho contador (programado en modo 3), éste
se encarga de generar el sonido. Esto permite obtener sonidos idénticos en todos
los ordenadores. Existe el pequeño problema de que la duración del sonido ha
de ser múltiplo de 1/18,2 segundos si se desea utilizar el reloj del sistema para
determinarla (un bucle de retardo sería, una vez más, dependiente de la
máquina) ya que el contador 2 está ahora ocupado en la producción de sonido y
no se puede usar para temporizar (al menos, no sin hacer malabarismos).
Alternativamente, se podría evaluar la velocidad de la CPU para ajustar las
constantes de retardo o aumentar la velocidad de la interrupción periódica.

Para emplear este sistema, primero se prepara el contador 2 para


temporizar (poniendo a 1 el bit 0 del puerto 61h) y luego se conecta su salida al
altavoz (poniendo a 1 el bit 1 del puerto 61h). Al final, conviene borrar ambos
bits de nuevo. Ahora no es preciso inhibir las interrupciones para garantizar la
calidad del sonido:
MOV AL,10110110b ; contador 2, modo 3, operación 11b, datos
binarios
OUT 43h,AL ; programar contador 2
MOV AX,2711 ; 1.193.180 / 440 Hz (nota LA) = 2711
JMP SHORT $+2
OUT 42h,AL
MOV AL,AH
JMP SHORT $+2
OUT 42h,AL ; frecuencia programada
JMP SHORT $+2
IN AL,61h
OR AL,00000011b
JMP SHORT $+2
OUT 61h,AL ; altavoz sonando
MOV CX,0
demora: LOOP demora ; esperar un cierto tiempo por el peor
método
IN AL,61h
AND AL,11111100b
JMP SHORT $+2
OUT 61h,AL ; altavoz callado

Las frecuencias en Hz de las distintas notas musicales están oficialmente


definidas y los músicos suelen tenerlas en cuenta a la hora de afinar los
instrumentos. La escala cromática temperada, adoptada por la American
Standards Asociation en 1936, establece el LA como nota de referencia en 440
Hz. En general, una vez conocidas las frecuencias de las notas de una octava, las
de la octava siguiente o anterior se obtienen multiplicando y dividiendo por dos,
respectivamente.

La fórmula de abajo permite obtener las frecuencias de las notas


asignándolas un número (a partir de 6 y hasta 88; el LA de 440 Hz es la nota 49)
con una precisión razonable, máxime teniendo en cuenta que van a ir a parar al
altavoz del PC. Tal curiosa relación se verifica debido a que la respuesta del oído
humano es logarítmica, lo que ha permitido reducir a simples matemáticas el
viejo saber milenario de los músicos.
PRÁCTICA #8
Desarrollo De Una Aplicación Con El Sistema De Audio Del Computador

TITLE Generación de Sonido (COM)

SOUNSG SEGMENT PARA 'code'

ASSUME CS:SOUNSG,DS:SOUNSG,SS:SOUNSG

ORG 100H

BEGIN: JMP SHORT MAIN

;----------------------------------------------------------
----------------------

DURTION DW 1000 ;duración del tono

TONE DW 256H ;frecuencia

;----------------------------------------------------------
----------------------

MAIN PROC NEAR

IN AL,61H ;obtener datos del puerto

PUSH AX ; y guardar

CLI ;limpiar interrupciones

CALL B10SPKR ;producir sonido

POP AX ;restablecer

OUT 61H,AL ;numero del puerto

STI ;restablecer interrupciones

RET

MAIN ENDP

B10SPKR PROC NEAR

B20: MOV DX,DURTION ;fijar duración del sonido

B30:

AND AL,11111100B ;poner en cero los bits 0 y 1


OUT 61H,AL ;trasmitir a la bocina

MOV CX,TONE ;fijar duración

B40:

LOOP B40 ;retraso

OR AL,00000010B ;poner en uno el bit 1

OUT 61H,AL ;transmitir a la bocina

MOV CX,TONE ;fijar duración

B50:

LOOP B50 ;retraso

DEC DX ;reducir duración

JNZ B30 ;¿Continuar?

SHL DURTION,1 ;reducir frecuencia

SHR TONE,1 ;¿ahora es cero?

JNZ B20 ;si regresar

RET

B10SPKR ENDP

SOUNSG ENDS

END BEGIN
CONCLUSIÓN

AL culminar la elaboración de este trabajo de investigación se han podido


conocer las distintas técnicas de programación así como sus funcionamientos,
importancias, ventajas a la hora de elaborar un programa en ASM, ya sea TASM,
NASM, hasta el emu8086, entre otros. Además se estudiaron diferentes ejemplos
de dichas técnicas para ilustrar la explicación de cada una. En fin, se pueden
acotar los diferentes aspectos para el análisis final de esta investigación:

 En general una subrutina consiste en una porción de código que realiza


una operación con un conjunto de valores proporcionados por el
programa que la invoca denominados parámetros, y que devuelve un
resultado.

 De las técnicas descritas para la invocación de subrutinas, la que crea el


bloque de activación en la pila es la más utilizada por los lenguajes de alto
nivel. Se muestran los pasos a seguir por el programa llamador y el
llamado para crear y destruir el bloque de activación.

 Los accesos condicionales permiten controlar el flujo de ejecución de un


programa escrito en lenguaje ensamblador es por ello que se van a
describir de manera breve las instrucciones de control disponibles.

 En los sistemas de computadoras, los datos rara vez están en la forma


correcta. Una tarea principal del sistema es convertir los datos de una
forma a otra.

 Las tablas se utilizan a menudo para convertir datos de una forma a otra.
Una tabla esta formada en la memoria como una lista de datos que se
consultan por un procedimiento para efectuar conversiones.

 Un sistema pobremente documentado carece de valor aunque haya


funcionado bien en alguna ocasión.
BIBLIOGRAFÍA:

• PETER Abel, “Abel”. Lenguaje Ensamblador y Programación para IBM PC


y sus Compatibles.

• GARCIA De Celis, Ciriaco. El Universo Digital del IBM PC, AT Y PS/2.


Edición 4.0 (4º)

• http://books.google.com/books?ct=result&hl=es&id=vPmSWr-
wDt8C&dq=invocacion+a+subrutinas+%2B+tecnicas+de+programacion
&pg=PA368&lpg=PA368&sig=ACfU3U1JlKb81_PbnyATtfvsHTUnpxTipQ
&q=paginas+369+a+372#PPR1,M1

• http://www.it.uc3m.es/ttao/html/SUB.html#sub:sec:subroutines

• http://perso.wanadoo.es/pictob/tecprg.htm

• http://www.rinconsolidario.org/eps/asm8086/asm.html

• http://homepage.mac.com/eravila/asmix862.html
ANEXOS

CALL: Instrucción de llamada a subrutina

El efecto relevante de esta instrucción es que deposita en la cima de la pila


la dirección de retorno, o lo que es lo mismo, la dirección de la instrucción que
sigue a esta en el flujo de ejecución. Esta instrucción no modifica los flags de la
palabra de estado.

RET: Instrucción de retorno de subrutina

El aspecto relevante de esta instrucción es que supone que la dirección a la


que debe retornar está almacenada en la cima de la pila. Esta instrucción no
modifica los flags de la palabra de estado.
INSTRUCCIONES PARA EL 8086

También podría gustarte