INSTRUCCIONES DE CARGA | ||||||||||||||||||||||||||||||||||
Las instrucciones de carga transfieren contenidos de memoria a registros, de registros a memoria y entre registros. Se trata del grupo principal de instrucciones del microprocesador, y su necesidad queda justificada, ya que todas las operaciones aritméticas y lógicas se hacen sobre registros del microprocesador, o entre estos y posiciones de memoria y casi siempre será necesario almacenar los resultados sobre la memoria. Por otra parte, gran número de instrucciones utilizan registros para direccionar posiciones de memoria, bien sea mediante direccionamiento absoluto o indexado. El formato básico de estas instrucciones es:
El código LD del inglés "LOAD" (carga), indica al microprocesador que debe cargar en el "DESTINO" el valor contenido en el "ORIGEN". El "DESTINO" y el "ORIGEN", pueden ser tanto registros, como posiciones de memoria, utilizaremos "r" y "r'" para referirnos a los registros de 8 bits, afectados por la instrucción, y "dd" para referirnos a los de 16 bits (pares de registros). Los valores de "r" y "r'" usados para el código de máquina en este grupo de instrucciones, son los siguientes:
Los valores de "dd" usados para el código de máquina en este grupo de instrucciones, son los siguientes:
|
Grupo de instrucciones de carga en memoria
|
Grupo de instrucciones de carga en registro acumulador
|
Grupo de instrucciones para salvar el registro acumulador
|
Grupo de instrucciones de carga en registros de 16 bits
Recuerde:
|
Grupo de instrucciones de carga en memoria, 16 bits
|
Grupo de instrucciones de carga en registro SP
|
Grupo de instrucciones de manejo de pila
Una pila es una cola LIFO (last input first output), último en entrar primero en salir. El termino pila es de uso habitual, se apilan cajas, revistas, etc. Pues bien, una pila en términos informáticos funciona igual, por ejemplo: una persona se compra todos los meses una revista, es fácil que las ordene en una pila; es decir, irá poniendo una encima de la anterior, de tal forma que la última colocada siempre estaría más al alcance. De la misma manera, en un ordenador se pueden ir guardando en una tabla en memoria, mejor denominada cola, una serie de octetos, u en una palabra de control de dos octetos se guardaría la última dirección usada de la tabla, de forma que: para meter un nuevo octeto se sumaría uno a la palabra de control de tabla y se cargaría el octeto en esa dirección; para sacar un octeto se leería el octeto direccionado por la palabra de control y se le restaría uno a ésta. Eso es lo que se pretende con las instrucciones que siguen, las cuales utilizan el registro puntero de pila "SP". Para identificar los pares de registros usaremos el siguiente código:
En el Spectrum, la pila se coloca en la parte alta de memoria, el sistema operativo la sitúa inmediatamente debajo de RAMTOP, durante la rutina de inicialización. Esto lo hace, cargando el registro "SP" con la dirección inmediatamente inferior a la de RAMTOP. Cada vez que utilicemos la instrucción PUSH, meteremos en la pila el contenido de un par de registros y cada vez que utilicemos la instrucción POP, sacaremos el dato más alto de la pila y lo asignaremos a un par de registros. Nuestra pila se expande "hacia abajo", lo cual quiere decir que cuando hablemos de "la parte superior de la pila", en realidad, nos estaremos refiriendo a la dirección más baja de ésta. Por otro lado, todos los datos que se almacenan en la pila, tienen dos bytes de longitud, por lo cual, el registro "SP" se incrementa o se decrementa de 2 en 2. El proceso de introducir el contenido de un par de registros en la pila, conlleva las siguientes operaciones:
El proceso de sacar un número de la pila, implica que el microprocesador realice las mismas operaciones a la inversa:
Algunos microprocesadores trabajan con dos pilas, una se denomina "pila de máquina" y otra "pila de usuario". La pila de máquina la utiliza el microprocesador para introducir sus datos y la pila de usuario, es la que el programador puede utilizar. En el Z-80 no existe "pila de usuario", de forma que el programador debe usar la misma pila que la máquina. Esto lleva aparejados ciertos inconvenientes, así que vamos a ver para qué utiliza la máquina esta pila. Cada vez que el microprocesador recibe una instrucción que le haga saltar a una subrutina, almacena en la pila la dirección a la que debe retornar cuando termine esa rutina. Por tanto, siempre que dentro de una subrutina utilicemos la pila, deberemos asegurarnos de sacar todos los datos que hayamos introducido antes de intentar retornar, ya que de lo contrario, el microprocesador tomaría nuestro último dato como dirección de retorno; si esto ocurriera, se diría que nuestra subrutina "corrompe la pila". Es imposible retornar con éxito desde una subrutina que corrompa la pila, por lo que hay que procurar que esto nunca ocurra. A continuación, vamos a ver las instrucciones que puede utilizar el programador para trabajar sobre la pila.
Observe que la secuencia de instrucciones de los seis últimos ejemplos da como resultado el siguiente intercambio de registros:
El uso principal de las instrucciones PUSH (empujar) y POP (explotar) --la traducción al castellano no tiene un significado muy completo--, es salvar el contenido de los registros para poder usarlos y después recuperar sus valores. Esto es muy útil en el empleo de sub-rutinas. EJEMPLO:Una sub-rutina que quiera usar los registros BC, DE y HL sin variar su contenido, comenzaría:
Y terminaría:
Observe cómo se recupera al revés de cómo se salvó, es decir, el primer registro que se recupera, es el último que se salvó. Recuerde que debe sacar de la pila todo lo que metió, antes de intentar retornar desde una subrutina. |
Una mirada gráfica a la pila
Para quien no esté familiarizado con los ordenadores, el funcionamiento de una pila, puede resultar algo dificil de comprender. Haciendo cierto el refrán "una imagen vale más que mil palabras", vamos a ver de un modo gráfico, lo que ocurre en la pila y en los registros correspondientes, durante la ejecución de las anteriores instrucciones. Miremos la FIGURA 5-1A, que representa al situación inicial de la que partimos. A la izquierda de la figura, vemos cuatro "ventanas" etiquetadas: "HL", "IX", "IY" y "SP"; se trata de una representación gráfica de los registros del microprocesador.
Cada ventana muestra un número hexadecimal, que representa el contenido del registro correspondiente, por ejemplo, el registro "HL" contiene AAB5h, el "IX" contiene EEF1h, etc. El registro "SP" contiene 4B89h, que es la dirección de memoria a partir de donde crecerá la pila. En la parte derecha de la figura, vemos una representación gráfica de la zona de memoria donde está situada la pila. A la derecha de cada casilla está su dirección y dentro de la casilla, su contenido hexadecimal. En principio, todas las casillas contienen "XX", lo que significa que su verdadero contenido nos es indiferente. Vemos un cuadradito con las letras "SP" dentro de él; este cuadrado, apunta a la casilla cuya dirección es precisamente, el contenido del registro "SP"; de esta forma, nos indica cuál es el último dato introducido en la pila, es decir, el primero que podemos leer. Partiendo de la situación que muestra esta figura, vamos a ejecutar la primera de nuestras instrucciones:
Esta instrucción debe guardar en la pila, el contenido del par de registros "HL"; el registro "SP" se decrementará dos veces, y por tanto, el cuadradito que apunta a la memoria bajará dos casillas. En la FIGURA 5-1B, podemos ver la situación después de que esta instrucción haya sido ejecutada. El registro "HL" contiene el mismo valor que antes, ya que éste ha sido copiado en la pila, pero no se ha destruido. Vemos que la dirección 4B88h contiene el número AAh y la dirección 4B87h, el número B5h, por tanto, las dos juntas componen el número AAB5h que es, precisamente, el contenido de "HL" que queríamos preservar. Por otro lado, vemos que el cuadradito (a partir de ahora, lo llamaremos puntero) ha bajado dos casillas, precisamente, para apuntar al último dato introducido.
Si ahora utilizáramos la instrucción POP para recuperar un dato de la pila, sería precisamente este dato el que podríamos recuperar. Vamos con la segunda de nuestras instrucciones:
En este caso, vamos a guardar en la pila el contenido del registro "IX"; sin por ello perder el dato que habíamos guardado anteriormente. En la FIGURA 5-1C, se puede ver cómo quedan los contenidos después de esta última instrucción. La posición de memoria 4B86h contiene el número EEh, y la 4B85h el número F1h; juntos forman EEF1h, que es, de nuevo, el contenido que queríamos preservar. El puntero (cuadradito) ha vuelto a bajar dos casillas, para apuntar, de nuevo, al último dato introducido.
Vamos ahora, a meter en la pila el último de nuestros datos: el contenido del registro "IY".
Con esta instrucción, entra en la pila el número 86FFh. En la FIGURA 5-1D, podemos ver, de nuevo, cómo queda la pila después de esta instrucción. Ahora el puntero ha bajado a la casilla 4B83h, con lo que otra vez, apunta al último dato introducido.
Podríamos seguir metiendo datos en la pila indefinidamente, hasta que agotáramos la memoria disponible, pero con estos tres ejemplos, ya hemos visto el proceso de expansión de la pila. Vamos a ver ahora, el proceso inverso: sacar datos de la pila. Nuestra primera instrucción será:
Que toma el último dato que hayamos introducido en la pila y lo coloca dentro del registro "HL". En la FIGURA 5-1E, podemos ver cómo quedan pila y registros, después de esta instrucción. El último dato introducido en la pila (86FFh) ha pasado a ser el contenido del registro "HL" y el puntero ha subido dos casillas, para apuntar al dato anteriormente introducido.
Una observación interesante, es que el contenido de las casillas que componen la pila, no se ha modificado; el número 86FFh sigue estando ahí, aunque a nosotros nos da igual, recuérdese que sólo podemos acceder cada vez, al dato señalado por el puntero; las casillas 4B84h y 4B83h que contienen el dato 86FFh, sólo se borrarán totalmente cuando la pila vuelva a expandirse. Ahora, vamos a recuperar el siguiente dato de la pila y lo asignaremos al registro "IX":
Esta instrucción toma el último dato de la pila y lo asigna al registro "IX"; recuérdese que el último dato, es siempre el que es apuntado por el puntero. Recuérdese también, que la dirección de la casilla apuntada por el puntero es el contenido del registro "SP"; las letras "S" y "P", son las iniciales de "Stack Pointer", en inglés, "Puntero de Pila". La FIGURA 5-1F, muestra los contenidos después de ejecurar la instrucción "POP IX", vemos otra vez que los datos de la pila no se han perdido, pero para nosotros no existen, ya que el puntero ha vuelto a subir dos casillas y ahora se considera que el último dato de la pila es AAB5h.
Vamos a recuperar, por último, este dato:
Esta instrucción, toma el último dato de la pila y lo asigna al registro "IY". En la FIGURA 5-1G, vemos la situación final, el dato AAB5h ha sido asignado al registro "IY" y el puntero se ha vuelto a incrementar dos veces, para apuntar al mismo sitio que lo hacía al principio de estos ejemplos.
Ya hemos sacado de la pila todos los datos que habíamos introducido. El puntero ha quedado en la misma posición donde estaba al principio. Si nuestro ejemplo fuese una subrutina de un programa, en la dirección donde apunta ahora el puntero se encontraría almacenada la dirección de retorno y ahora sería posible retornar al punto desde donde se llamó a esta subrutina. Con la pila se pueden hacer muchas cosas. Supongamos que en Basic queremos intercambiar el contenido de dos variables "a" y "b"; en ese caso, necesitamos generar una tercera variable que nos sirve de "puente", de la forma:
Trabajando en código máquina, podemos intercambiar el contenido de dos registros, utilizando la pila en lugar de la variable "puente" del Basic, supongamos que queremos intercambiar los contenidos de los registros "BC" y "DE"; podríamos hacer:
Por supuesto, esto es sólo un ejemplo, no se quedan aquí las utilidades de la pila. Su función principal es la de salvar temporalmente, el contenido de algún registro, mientras se utiliza para algo y luego, restituirle, de nuevo su contenido. No obstante, con la pila se pueden hacer muchas más cosas, en capítulos posteriores veremos cómo utiliza la pila el intérprete de Basic, para poder retornar desde cualquier punto en caso de error. |
Tablas de codificación
A continuación, vamos a ver una serie de tablas que nos han de servir para codificar las instrucciones rápidamente, cuando ensamblemos a mano. En las tablas se ha representado el código máquina de cada instrucción, tanto en decimal como en binario. Cuando el código máquina ocupa más de un byte, se han puesto uno a continuación del otro, separados por comas. Donde pone "d", se entiende que ese byte va ocupado por un entero de despazamiento en complemento a dos. Donde pone "n", debe ir el operando "n" que aparece en el código fuente de la instrucción. Donde aparecen dos bytes seguidos con "n", debe ir el operando "nn" del código fuente de la instrucción; primero irá el octeto menos significativo y luego el más significativo; por ejemplo: supongamos que el operando "nn" fuera 2A4Bh, primero iría 4Bh y luego 2Ah. En el apartado de ejemplos, veremos con claridad la forma de ensamblar a mano pequeños programas. La disposición de las tablas es la siguiente:
|
Carga del registro «PC»
Seguramente, el lector ya se habrá dado cuenta de que no hemos mencionado en ninguna instrucción al registro "PC"; este hecho se debe a que se trata de un registro especial, que tiene adignada una función muy específica. El registro "PC" o "Contador de programa", contiene siempre la dirección en memoria de la siguiente instrucción a ejecutar, por lo que el hecho de cargarlo con un número, implica que la siguiente instrucción será leída desde la posición de memoria apuntada por ese número, es decir, se producirá un salto o bifurcación en el flujo del programa. Vamos a verlo con un ejemplo. Supongamos que acabamos de leer una instrucción de tres bytes de longitud, que ocupaba las posiciones 40000, 40001 y 40002. En este momento, el registro "PC" contiene el valor 40003 que es la dirección desde donde se leerá la siguiente instrucción; si la instrucción que estamos ejecutando, modifica el contenido del "PC", digamos que lo pone a 60000, la siguiente instrucción será leida desde esta dirección, con lo que se habrá producido un salto en el programa. Los saltos y bifurcaciones tienen una importancia tan grande en cualquier lenguaje, que se ha reservado un grupo de instrucciones para este fin; se trata del grupo de instrucciones de "cambio de secuencia", que se estudiarán en el capítulo 10 de este curso. Hasta ese momento, suponemos que los programas se ejecutan en un orden secuencial, desde la primera instrucción hasta la última. |
Ejemplos
A continuación, vamos a ver una serie de ejemplos prácticos que el lector podrá introducir en su ordenador, tanto si dispone de ensamblador, como si no. A través de estos ejemplos, se pretende no sólo aprender a utilizar las instrucciones de carga, sino también, aprender a ensamblar un programa "a mano" y cargarlo desde Basic en cualquier lugar de la memoria. Antes de eso, y como nota previa, vamos a ver la forma de retornar a Basic desde código máquina cuando finalice la ejecución de cada uno de nuestros programas. En general, llamaremos a nuestros programas con la función USR del Basic, esta función ejecutará nuestras rutinas como si se tratase de subrutinas del sistema operativo, por lo que el procedimiento de retornar a éste, será el mismo que para retornar desde cualquier subrutina, es decir, la instrucción "RET" que se ensambla como C9h (201) y es equivalente al RETURN del Basic. Quizá esto se comprenda mejor cuando estudiemos las subrutinas en código máquina. Por ahora, nos basta con saber que al final de cada uno de nuestros programas, deberá ir la instrucción RET. Empecemos por lo más sencillo, vamos simplemente a cargar un número en el registro "BC". Escogemos este registro, por que es su contenido el que nos devuelve la función USR cuando retorna a Basic. Nuestro primer programa en código máquina podría ser el siguiente:
Que también podría haberse escrito en hexadecimal de la siguiente forma:
Vamos a ensamblar a mano este sencillo ejemplo, y luego lo cargaremos en uno de los lugares que indicábamos en el capítulo 4, el buffer de impresora. Cogemos las tablas de codificación, y vemos que la instrucción
tiene el código 01h (1), de forma que éste será el primer byte de nuestro programa. A continuación, deberemos poner el operando de dos bytes "nn", con el orden de los octetos invertido. Como en este caso, el operando es 6A7Fh, deberemos poner primero 7Fh (127) y luego, 6Ah (106). Finalmente, pondremos el código de RET, C9h (201). Nuestro programa queda, por tanto, de la siguiente forma:
O escrito en decimal:
Hasta ahora, hemos hecho todo esto sobre el papel; por fin llega el momento de poner en marcha nuestro querido Spectrum. Para introducir los cuatro valores que componen nuestro código máquina, podemos POKEarlos en memoria ayudándonos de un bucle FOR ... NEXT:
Nuestro programa está en los datos de la línea 40, las líneas 10, 20 y 30 los van introduciendo secuencialmente en memoria, finalmente la línea 50 lo ejecuta imprimiendo el resultado de USR en el retorno. Teclee el programa, revise que no haya habido errores, y pulse RUN... Si todo ha ido correctamente, deberá ver el número 27263 en la esquina superior izquierda de la pantalla. No parece un resultado muy espectacular comparado con los prodigios semimágicos que se suelen esperar del código máquina. Ciertamente, no se puede pretender más con cuatro bytes, un simple "PRINT a" de Basic implica la ejecución de cientos de instrucciones en código máquina. No debe desanimarse el lector ni pretender hacer maravillas desde el primer momento. Lo más importante es ir aprendiendo todo claramente; las "virguerías" podrá hacerlas luego cada uno, no obstante, a lo largo del curso tenemos reservadas para nuestros lectores, maravillosas sorpresas. Vamos con nuestro siguiente ejemplo, esta vez vamos a leer desde código máquina un número que habremos almacenado desde Basic en la variable del Sistema "SEED". Se trata de leer el contenido de SEED y sacarlo a pantalla a través del registro "BC". En esta ocasión, almacenaremos el programa a partir de la dirección de memoria 30000, para lo cual, bajaremos primero la RAMTOP a 29999. Estas direcciones son válidas tanto para usuarios de 16K como de 48K. Nuestro programa es el siguiente:
La primera línea: "ORG 30000" es un pseudo-nemónico, no se puede ensamblar y su única finalidad es indicarle al ensamblador que deberá ensamblar el programa a partir de la dirección 30000. La última línea "SEED EQU #5C76" tampoco se puede ensamblar, se trata de una definición de etiqueta, su finalidad es asignarle a la etiqueta "SEED" el valor 5C76h (23670). El programa simplificado, quedaría:
Para codificarlo, tomamos de nuevo las tablas y buscamos el código de:
que resulta ser 2Ah (42). A continuación, vendrá el operando invertido: 76h (118) y 5Ch (92). Ahora buscamos:
y
cuyos códigos resultan ser respectivamente: 44h (68) y 4Dh (77). Finalmente, ponemos el código de RET, es decir, C9h (201). Nuestro programa queda de la siguiente forma:
O escrito en decimal:
Vamos a construir el programa Basic que lo introduce en memoria y lo ejecuta:
La línea 10 baja la RAMTOP para preservar nuestro programa contra borrados accidentales. Las líneas 20 y 30 cargan en memoria el programa que se encuentra en los datos de la línea 40. La línea 50 nos pide un valor para SEED, y la línea 60 lo introduce en la variable "SEED" siempre que este valor no sea cero. Finalmente, la línea 70 ejecuta nuestro programa en código máquina e imprime en pantalla el resultado. En este ejemplo, vemos que es posible establecer una comunicación bidireccional entre Basic y Código Máquina para transferir datos; existen otras muchas formas de realizar esta comunicación que se irán viendo en ejemplos sucesivos. En nuestro tercer ejemplo, vamos a leer la variable del Sistema RAMTOP desde código máquina, y utilizaremos la pila para sacarla a pantalla por el registro "BC". Asimismo, veremos cómo almacenar una rutina en código máquina dentro de una línea REM del programa Basic. Primero leeremos el contenido de la variable RAMTOP, cargándolo sobre el registro "HL", luego transferiremos este contenido al "BC" a través de la pila; el programa podría ser el siguiente:
De nuevo, utilizamos una etiqueta que definimos en la última línea, antes de codificar el programa, eliminamos la etiqueta, quedando el programa simplificado:
Ahora, codificamos el programa, buscamos en las tablas el código de:
que resulta ser 2Ah (42), a continuación van los operandos B2h (178) y 5Ch (92). El código de:
es E5h (229), y el de:
es C1h (193). Por último, colocamos el código de RET: C9h (201). El programa completo, queda de la siguiente forma:
O prar quienes lo prefieran en decimal:
Ahora, sólo nos falta cargarlo en una línea REM de un programa Basic. Nuestra rutina tiene 6 bytes, por lo que crearemos una línea REM con, por ejemplo, 6 asteriscos. Estos asteriscos serán sustituidos por los bytes del programa cuando éste se cargue. El programa podría ser el siguiente:
Hay muchos puntos sutiles en este programa que conviene analizar detenidamente; como dijimos antes, la línea 10 contiene el espacio donde se cargará nuestra rutina en C/M. En la línea 20, leemos la variable del Sistema PROG, para saber a partir de qué dirección de memoria está ubicado el programa Basic. Los dos primeros bytes de esta zona, constituyen el número de línea, los dos siguientes la longitud, y el quinto es el código de REM; a partir de ahí empiezan los asteriscos, que es donde deberemos cargar el código máquina, es decir, desde "prog+5" hasta "prog+10" tal y como se ve en las líneas 30, 40 y 50. La línea 60 contiene el programa en DATAs. Finalmente, la línea 70 ejecuta el programa desde la dirección "prog+5". En este caso, es imprescindible que el argumento de USR vaya entre paréntesis; es muy fácil omitir los paréntesis, olvidándose de que la función USR tiene una prioridad más alta que la suma. Una vez ejecutado el programa, la línea 10 quedaría:
Nuestro siguiente ejemplo es más vistoso, y algo más complejo. Vamos a dibujar una silueta en pantalla, y dado que la cosa va de pantalla, almacenaremos esta rutina en el archivo de presentación visual, con lo que veremos físicamente los bytes que la componen, en forma de pixels en la primera línea. El objetivo del programa es dibujar en la casilla central, la silueta de un muñeco, como si se tratara de un UDG. Dado que sólo podemos utilizar instrucciones de carga, el programa resulta considerablemente más largo de lo que es normal para trabajar con la pantalla. En sucesivos ejemplos de capítulos más avanzados, iremos viendo otras formas más sencillas de imprimir en pantalla; y veremos también, cómo la peculiar manera en que está organizado el archivo de pantalla, que tan incómoda se hacía en Basic, resulta una gran ventaja cuando se trabaja en código máquina. La forma más sencilla de imprimir un gráfico en pantalla, es almacenar en las ocho direcciones que componen una casilla, los ocho números que definen ese gráfico. En la FIGURA 5-10, vemos las direcciones de las posiciones de memoria correspondientes a la casilla central de la pantalla, así como los datos que vamos a almacenar en esas posiciones, para visualizar nuestro muñeco.
El método general que vamos a utilizar, es cargar el registro "A" con el dato a almacenar, el registro "HL" con la dirección, y almacenar el dato "A" en la dirección apuntada por "HL". Como todavía no hemos aprendido a hacer bucles, tendremos que repetir esta secuencia 8 veces, si bien, las veces sucesivas será suficiente con que modifiquemos el valor del registro "H", ya que el del "L" permanece constante. El listado sería el siguiente:
Obsérvese que, dado que las tres últimas líneas del dibujo son iguales, no ha sido necesario cargar el registro "A" más que 6 veces. A la derecha del listado Assembler, está el código de cada una de las instrucciones. En este caso, vamos a utilizar una técnica más refinada para cargar el programa; escribiremos el código máquina en hexadecimal sobre una línea DATA del Basic, y utilizaremos una suma de control para detectar posibles errores. El programa en Basic sería el siguiente:
Primero fijamos la dirección de carga al inicio del archivo de presentación visual. En la línea 20, definimos una función que nos ayuda a convertir de hexadecimal a decimal, la línea 30 lee el código máquina en la variable "a$", la suma de comprobación en la variable "s" y pone a cero el acumulador de checksum "cs". Las líneas 40 a 80 van pasando los códigos a decimal (línea 50), acumulándolos en el checksum (línea 60) y metiéndolos en sucesivas direcciones de memoria (línea 70). La línea 90 comprueba que el valor acumulado en checksum sea igual a la suma de comprobación, y detiene el programa en caso contrario. Finalmente, la línea 100 ejecuta nuestra rutina en código máquina. La línea 110 contiene el código máquina en hexadecimal, y la 120 la suma de todos los bytes en decimal, que se utiliza como suma de comprobación. Cuando se ejecute el programa, en la pantalla del ordenador tiene que aparecer algo similar a lo que se ve en la FIGURA 5-11.
En el siguiente capítulo, veremos las instrucciones que nos permiten realizar operaciones aritméticas y lógicas sobre los registros del microprocesador. Antes de ello, le recomendamos al lector que intente resolver los siguientes ejercicios, que le ayudarán a afianzar conocimientos. |
|