Lenguaje ensamblador | lenguaje de programación

Un lenguaje ensamblador es un lenguaje de programación que puede utilizarse para decirle directamente al ordenador lo que debe hacer. Un lenguaje ensamblador es casi exactamente como el código máquina que puede entender un ordenador, excepto que utiliza palabras en lugar de números. Un ordenador no puede realmente entender un programa en ensamblador directamente. Sin embargo, puede convertir fácilmente el programa en código máquina sustituyendo las palabras del programa por los números que representan. Un programa que hace eso se llama ensamblador.

Los programas escritos en lenguaje ensamblador suelen estar formados por instrucciones, que son pequeñas tareas que el ordenador realiza cuando está ejecutando el programa. Se llaman instrucciones porque el programador las utiliza para indicar al ordenador lo que debe hacer. La parte del ordenador que sigue las instrucciones es el procesador.

El lenguaje ensamblador de un ordenador es un lenguaje de bajo nivel, lo que significa que sólo puede utilizarse para realizar las tareas simples que un ordenador puede entender directamente. Para realizar tareas más complejas, hay que decirle al ordenador cada una de las tareas simples que forman parte de la tarea compleja. Por ejemplo, un ordenador no entiende cómo imprimir una frase en su pantalla. En su lugar, un programa escrito en ensamblador debe decirle cómo realizar todos los pequeños pasos que intervienen en la impresión de la frase.

Un programa en ensamblador de este tipo estaría compuesto por muchísimas instrucciones que, en conjunto, hacen algo que a un humano le parece muy sencillo y básico. Esto hace que sea difícil para los humanos leer un programa en ensamblador. Por el contrario, un lenguaje de programación de alto nivel puede tener una única instrucción como PRINT "¡Hola, mundo!" que le dirá al ordenador que realice todas las pequeñas tareas por usted.



   Zoom
 

Desarrollo del lenguaje ensamblador

Cuando los informáticos construyeron por primera vez máquinas programables, las programaron directamente en código máquina, que es una serie de números que indicaban al ordenador lo que debía hacer. Escribir el lenguaje de la máquina era muy difícil y llevaba mucho tiempo, así que finalmente se creó el lenguaje ensamblador. El lenguaje ensamblador es más fácil de leer para un humano y se puede escribir más rápido, pero sigue siendo mucho más difícil de utilizar para un humano que un lenguaje de programación de alto nivel que intenta imitar el lenguaje humano.

Programación en código máquina

Para programar en código máquina, el programador necesita saber qué aspecto tiene cada instrucción en binario (o hexadecimal). Aunque es fácil para un ordenador averiguar rápidamente lo que significa el código máquina, es difícil para un programador. Cada instrucción puede tener varias formas, todas las cuales sólo parecen un montón de números para la gente. Cualquier error que alguien cometa al escribir el código máquina sólo se notará cuando el ordenador haga lo incorrecto. Descubrir el error es difícil porque la mayoría de la gente no puede saber qué significa el código máquina con sólo mirarlo. Un ejemplo de cómo es el código máquina:

05 2A 00

Este código máquina hexadecimal indica a un procesador de ordenador x86 que añada 42 al acumulador. Es muy difícil para una persona leerlo y entenderlo incluso si esa persona conoce el código máquina.

Utilizar el lenguaje ensamblador en su lugar

Con el lenguaje ensamblador, cada instrucción puede escribirse como una palabra corta, llamada mnemónica, seguida de otras cosas como números u otras palabras cortas. El mnemónico se utiliza para que el programador no tenga que recordar los números exactos en código máquina necesarios para decirle al ordenador que haga algo. Algunos ejemplos de mnemotecnia en el lenguaje ensamblador son add, que añade datos, y mov, que mueve datos de un lugar a otro. Como "mnemónico" es una palabra poco común, a veces se utiliza en su lugar la frase tipo de instrucción o simplemente instrucción, a menudo de forma incorrecta. Las palabras y números que siguen a la primera palabra dan más información sobre lo que hay que hacer. Por ejemplo, las cosas que siguen a una suma pueden ser qué dos cosas hay que sumar y las que siguen a mov dicen qué hay que mover y dónde hay que ponerlo.

Por ejemplo, el código máquina de la sección anterior (05 2A 00) puede escribirse en ensamblador como

añadir ax,42

El lenguaje ensamblador también permite a los programadores escribir los datos reales que utiliza el programa de forma más sencilla. La mayoría de los lenguajes ensambladores tienen soporte para hacer fácilmente números y texto. En código máquina, cada tipo diferente de número como positivo, negativo o decimal, tendría que ser convertido manualmente en binario y el texto tendría que ser definido una letra a la vez, como números.

El lenguaje ensamblador proporciona lo que se llama una abstracción del código máquina. Al utilizar el ensamblador, los programadores no necesitan conocer los detalles de lo que significan los números para el ordenador, el ensamblador lo resuelve en su lugar. En realidad, el lenguaje ensamblador sigue permitiendo al programador utilizar todas las características del procesador que podría utilizar con el código máquina. En este sentido, el lenguaje ensamblador tiene un rasgo muy bueno y poco común: tiene la misma capacidad de expresar cosas que lo que está abstrayendo (el código máquina) y, al mismo tiempo, es mucho más fácil de usar. Por ello, el código máquina casi nunca se utiliza como lenguaje de programación.

Desmontaje y depuración

Cuando los programas están terminados, ya se han transformado en código máquina para que el procesador pueda ejecutarlos realmente. Sin embargo, a veces, si el programa tiene algún fallo (error), los programadores querrán poder saber qué hace cada parte del código máquina. Los desensambladores son programas que ayudan a los programadores a hacer eso transformando el código máquina del programa de nuevo en lenguaje ensamblador, que es mucho más fácil de entender. Los desensambladores, que convierten el código máquina en lenguaje ensamblador, hacen lo contrario que los ensambladores, que convierten el lenguaje ensamblador en código máquina.



 

Organización informática

Para entender cómo funciona un programa en lenguaje ensamblador es necesario comprender cómo están organizados los ordenadores, cómo parecen funcionar a un nivel muy bajo. En el nivel más simplista, los ordenadores tienen tres partes principales:

  1. memoria principal o RAM que contiene datos e instrucciones,
  2. un procesador, que procesa los datos ejecutando las instrucciones, y
  3. Entrada y salida (a veces abreviadas como E/S), que permiten al ordenador comunicarse con el mundo exterior y almacenar datos fuera de la memoria principal para poder recuperarlos más tarde.

Memoria principal

En la mayoría de los ordenadores, la memoria se divide en bytes. Cada byte contiene 8 bits. Cada byte de la memoria tiene también una dirección, que es un número que dice dónde se encuentra el byte en la memoria. El primer byte de la memoria tiene una dirección de 0, el siguiente tiene una dirección de 1, y así sucesivamente. Dividir la memoria en bytes la hace direccionable por bytes porque cada byte tiene una dirección única. Las direcciones de las memorias de bytes no pueden utilizarse para referirse a un solo bit de un byte. Un byte es la pieza más pequeña de memoria que puede ser direccionada.

Aunque una dirección se refiere a un byte concreto de la memoria, los procesadores permiten utilizar varios bytes de memoria seguidos. El uso más común de esta característica es utilizar 2 o 4 bytes en una fila para representar un número, normalmente un entero. A veces también se utilizan bytes individuales para representar números enteros, pero como sólo tienen 8 bits de longitud, sólo pueden contener 28 o 256 valores posibles diferentes. El uso de 2 o 4 bytes en una fila eleva el número de valores posibles diferentes a 216 , 65536 o 232 , 4294967296, respectivamente.

Cuando un programa utiliza un byte o un número de bytes en una fila para representar algo como una letra, un número o cualquier otra cosa, esos bytes se denominan objeto porque todos forman parte de la misma cosa. Aunque los objetos se almacenan todos en bytes idénticos de memoria, se tratan como si tuvieran un "tipo", que dice cómo deben entenderse los bytes: ya sea como un entero o un carácter o algún otro tipo (como un valor no entero). El código máquina también puede considerarse como un tipo que se interpreta como instrucciones. La noción de tipo es muy, muy importante porque define qué cosas se pueden y no se pueden hacer al objeto y cómo interpretar los bytes del objeto. Por ejemplo, no es válido almacenar un número negativo en un objeto numérico positivo y no es válido almacenar una fracción en un número entero.

Una dirección que apunta a (es la dirección de) un objeto de varios bytes es la dirección del primer byte de ese objeto - el byte que tiene la dirección más baja. Como apunte, una cosa importante a tener en cuenta es que no se puede saber cuál es el tipo de un objeto -o incluso su tamaño- por su dirección. De hecho, ni siquiera se puede saber de qué tipo es un objeto con sólo mirarlo. Un programa en lenguaje ensamblador necesita llevar la cuenta de qué direcciones de memoria contienen qué objetos, y de qué tamaño son esos objetos. Un programa que lo hace es seguro en cuanto al tipo porque sólo hace cosas a los objetos que son seguras en cuanto a su tipo. Un programa que no lo haga probablemente no funcionará correctamente. Tenga en cuenta que la mayoría de los programas en realidad no almacenan explícitamente cuál es el tipo de un objeto, simplemente acceden a los objetos de forma consistente - el mismo objeto es tratado siempre como del mismo tipo.

El procesador

El procesador ejecuta las instrucciones, que se almacenan como código máquina en la memoria principal. Además de poder acceder a la memoria para el almacenamiento, la mayoría de los procesadores tienen unos pequeños espacios rápidos de tamaño fijo para mantener los objetos con los que se está trabajando en ese momento. Estos espacios se denominan registros. Los procesadores suelen ejecutar tres tipos de instrucciones, aunque algunas instrucciones pueden ser una combinación de estos tipos. A continuación se muestran algunos ejemplos de cada tipo en el lenguaje ensamblador x86.

Instrucciones que leen o escriben en la memoria

La siguiente instrucción en lenguaje ensamblador x86 lee (carga) un objeto de 2 bytes del byte en la dirección 4096 (0x1000 en hexadecimal) en un registro de 16 bits llamado 'ax':

mov ax, [1000h]

En este lenguaje ensamblador, los corchetes alrededor de un número (o de un nombre de registro) significan que el número debe utilizarse como dirección a los datos que deben utilizarse. El uso de una dirección para apuntar a los datos se llama indirección. En el siguiente ejemplo, sin los corchetes, otro registro, bx, recibe realmente el valor 20 cargado en él.

mov bx, 20

Como no se utilizó ninguna indirección, el valor real se puso en el registro.

Si los operandos (las cosas que vienen después del mnemónico), aparecen en el orden inverso, una instrucción que carga algo de la memoria en lugar de escribirlo en la memoria:

mov [1000h], bx

Aquí, la memoria en la dirección 1000h recibe el valor de bx. Si este ejemplo se ejecuta justo después del anterior, los 2 bytes en 1000h y 1001h serán un entero de 2 bytes con el valor de 20.

Instrucciones que realizan operaciones matemáticas o lógicas

Algunas instrucciones hacen cosas como la resta o las operaciones lógicas como la no:

El ejemplo de código máquina anterior en este artículo sería esto en lenguaje ensamblador:

añadir eje, 42

Aquí, 42 y ax se suman y el resultado se almacena de nuevo en ax. En ensamblador x86 también es posible combinar un acceso a memoria y una operación matemática como ésta:

añadir ax, [1000h]

Esta instrucción suma el valor del entero de 2 bytes almacenado en 1000h a ax y almacena la respuesta en ax.

o ax, bx

Esta instrucción calcula la or del contenido de los registros ax y bx y almacena el resultado de nuevo en ax.

Instrucciones que deciden cuál va a ser la siguiente instrucción

Normalmente, las instrucciones se ejecutan en el orden en que aparecen en la memoria, que es el orden en que están escritas en el código ensamblador. El procesador se limita a ejecutarlas una tras otra. Sin embargo, para que los procesadores puedan hacer cosas complicadas, necesitan ejecutar diferentes instrucciones en función de los datos que se les hayan dado. La capacidad de los procesadores de ejecutar diferentes instrucciones en función del resultado de algo se llama ramificación. Las instrucciones que deciden cuál debe ser la siguiente instrucción se llaman instrucciones de bifurcación.

En este ejemplo, supongamos que alguien quiere calcular la cantidad de pintura que necesitará para pintar un cuadrado con una determinada longitud de lado. Sin embargo, debido a la economía de escala, la tienda de pinturas no les venderá menos que la cantidad de pintura necesaria para pintar un cuadrado de 100 x 100.

Para calcular la cantidad de pintura que necesitarán en función de la longitud del cuadrado que quieren pintar, se les ocurre esta serie de pasos:

  • restar 100 a la longitud del lado
  • si la respuesta es menor que cero, fije la longitud del lado en 100
  • multiplicar la longitud del lado por sí mismo

Ese algoritmo puede expresarse en el siguiente código donde ax es la longitud del lado.

mov bx, ax sub bx, 100 jge continue mov ax, 100 continue: mul ax

Este ejemplo introduce varias cosas nuevas, pero las dos primeras instrucciones son familiares. Copian el valor de ax en bx y luego restan 100 a bx.

Una de las novedades de este ejemplo se llama etiqueta, un concepto que se encuentra en los lenguajes de ensamblaje en general. Las etiquetas pueden ser cualquier cosa que el programador quiera (a menos que sea el nombre de una instrucción, lo que confundiría al ensamblador). En este ejemplo, la etiqueta es 'continue'. El ensamblador la interpreta como la dirección de una instrucción. En este caso, es la dirección de mult ax.

Otro concepto nuevo es el de banderas. En los procesadores x86, muchas instrucciones establecen "banderas" en el procesador que pueden ser utilizadas por la siguiente instrucción para decidir qué hacer. En este caso, si bx era menor que 100, sub fijará una bandera que diga que el resultado era menor que cero.

La siguiente instrucción es jge, que es la abreviatura de "saltar si es mayor o igual que". Es una instrucción de bifurcación. Si las banderas del procesador especifican que el resultado ha sido mayor o igual que cero, en lugar de pasar a la siguiente instrucción el procesador saltará a la instrucción de la etiqueta de continuación, que es mul ax.

Este ejemplo funciona bien, pero no es lo que escribirían la mayoría de los programadores. La instrucción de sustracción establece la bandera correctamente, pero también cambia el valor sobre el que opera, lo que requiere que el ax se copie en bx. La mayoría de los lenguajes de ensamblaje permiten instrucciones de comparación que no cambian ninguno de los argumentos que se les pasan, pero siguen fijando las banderas correctamente y el ensamblaje x86 no es una excepción.

cmp ax, 100 jge continue mov ax, 100 continue: mul ax

Ahora, en lugar de restar 100 a ax, ver si ese número es menor que cero, y asignarlo de nuevo a ax, ax se deja sin modificar. Las banderas se siguen estableciendo de la misma manera, y el salto se sigue dando en las mismas situaciones.

Entrada y salida

Aunque la entrada y la salida son una parte fundamental de la informática, no hay una única forma de realizarlas en lenguaje ensamblador. Esto se debe a que la forma en que funcionan las E/S depende de la configuración del ordenador y del sistema operativo que ejecute, no sólo del tipo de procesador que tenga. En la sección de ejemplos que sigue, el ejemplo de "Hola Mundo" utiliza llamadas del sistema operativo MS-DOS y el ejemplo que le sigue utiliza llamadas de la BIOS.

Es posible hacer E/S en lenguaje ensamblador. De hecho, el lenguaje ensamblador puede expresar en general cualquier cosa que un ordenador sea capaz de hacer. Sin embargo, aunque hay instrucciones para sumar y bifurcar en lenguaje ensamblador que siempre harán lo mismo, no hay instrucciones en lenguaje ensamblador que siempre hagan E/S.

Lo importante es tener en cuenta que el funcionamiento de la E/S no forma parte de ningún lenguaje ensamblador porque no forma parte del funcionamiento del procesador.



 

Lenguajes ensambladores y portabilidad

Aunque el lenguaje ensamblador no es ejecutado directamente por el procesador -el código máquina sí-, sigue teniendo mucho que ver con él. Cada familia de procesadores admite diferentes características, instrucciones, reglas sobre lo que pueden hacer las instrucciones y reglas sobre qué combinación de instrucciones se permite en cada lugar. Debido a esto, los diferentes tipos de procesadores siguen necesitando diferentes lenguajes de ensamblaje.

Dado que cada versión del lenguaje ensamblador está ligada a una familia de procesadores, carece de algo llamado portabilidad. Algo que tiene portabilidad o es portátil puede transferirse fácilmente de un tipo de ordenador a otro. Mientras que otros tipos de lenguajes de programación son portables, el lenguaje ensamblador, en general, no lo es.



 

Lenguaje ensamblador y lenguajes de alto nivel

Aunque el lenguaje ensamblador permite utilizar fácilmente todas las características del procesador, no se utiliza en los proyectos de software modernos por varias razones:

  • Se necesita mucho esfuerzo para expresar un programa sencillo en ensamblador.
  • Aunque no es tan propenso a los errores como el código máquina, el lenguaje ensamblador sigue ofreciendo muy poca protección contra los errores. Casi todos los lenguajes ensambladores no aplican la seguridad de tipos.
  • El lenguaje ensamblador no promueve las buenas prácticas de programación, como la modularidad.
  • Aunque cada instrucción individual del lenguaje ensamblador es fácil de entender, es difícil saber cuál era la intención del programador que la escribió. De hecho, el lenguaje ensamblador de un programa es tan difícil de entender que las empresas no se preocupan de que la gente desensamble (obtenga el lenguaje ensamblador de) sus programas.

Como consecuencia de estos inconvenientes, en la mayoría de los proyectos se utilizan lenguajes de alto nivel como Pascal, C y C++. Permiten a los programadores expresar sus ideas de forma más directa en lugar de tener que preocuparse de decirle al procesador lo que tiene que hacer en cada momento. Se llaman de alto nivel porque las ideas que el programador puede expresar en la misma cantidad de código son más complicadas.

Los programadores que escriben código en lenguajes de alto nivel compilados utilizan un programa llamado compilador para transformar su código en lenguaje ensamblador. Los compiladores son mucho más difíciles de escribir que los ensambladores. Además, los lenguajes de alto nivel no siempre permiten a los programadores utilizar todas las características del procesador. Esto se debe a que los lenguajes de alto nivel están diseñados para soportar todas las familias de procesadores. A diferencia de los lenguajes de ensamblaje, que sólo admiten un tipo de procesador, los lenguajes de alto nivel son portátiles.

Aunque los compiladores son más complicados que los ensambladores, décadas de fabricación e investigación de compiladores los han hecho muy buenos. Ahora, ya no hay muchas razones para utilizar el lenguaje ensamblador para la mayoría de los proyectos, porque los compiladores suelen poder averiguar cómo expresar los programas en lenguaje ensamblador tan bien o mejor que los programadores.



 

Programas de ejemplo

Un programa ¡Hola, mundo! escrito en ensamblador x86:

adosseg .model small .stack 100h .data hello_message db '¡Hola, mundo! ',0dh,0ah,'$' .code main proc mov ax,@data mov ds,ax mov ah,9 mov dx,offset hello_message int 21h mov ax,4C00h int 21h main endp end main.

Una función que imprime un número en la pantalla utilizando las interrupciones de la BIOS escrita en ensamblador NASM x86. Es posible escribir código modular en ensamblador, pero requiere un esfuerzo adicional. Tenga en cuenta que todo lo que viene después de un punto y coma en una línea es un comentario y es ignorado por el ensamblador. Poner comentarios en el código en lenguaje ensamblador es muy importante porque los programas grandes en lenguaje ensamblador son muy difíciles de entender.

; void printn(int número, int base); printn: push bp mov bp, sp push ax push bx push cx push dx push si mov si, 0 mov ax, [bp + 4] ; número mov cx, [bp + 6] ; base gloop: inc si ; longitud de la cadena mov dx, 0 ; cero dx div cx ; dividir por la base cmp dx, 10 ; ¿es ge 10? jge num add dx, '0' ; añade cero a dx jmp anum num: add dx, ('A'- 10) ; valor hexadecimal, añade 'A' a dx - 10 anum: push dx ; poner dx en la pila. cmp ax, 0 ; ¿debemos continuar? jne gloop mov bx, 7h ; para interrupción tloop: pop ax ; obtener su valor mov ah, 0eh ; para interrupción int 10h ; escribir carácter dec si ; deshacerse del carácter jnz tloop pop si pop dx pop cx pop bx pop ax pop bp ret 4



 

Libros

  • Michael Singer, PDP-11. Assembler Language Programming and Machine Organization, John Wiley & Sons, NY: 1980.
  • Peter Norton, John Socha, Peter Norton's Assembly Language Book for the IBM PC, Brady Books, NY: 1986.
  • Dominic Sweetman: Véase MIPS Run. Morgan Kaufmann Publishers, 1999. ISBN 1-55860-410-3
  • John Waldron Introduction to RISC Assembly Language Programming. Addison Wesley, 1998. ISBN 0-201-39828-1
  • Jeff Duntemann: Lenguaje ensamblador paso a paso. Wiley, 2000. ISBN 0-471-37523-3
  • Paul Carter: Lenguaje ensamblador para PC. Libro electrónico gratuito, 2001.
     Sitio web
  • Robert Britton: MIPS Assembly Language Programming. Prentice Hall, 2003. ISBN 0-13-142044-5
  • Randall Hyde: El arte del lenguaje ensamblador. No Starch Press, 2003. ISBN 1-886411-97-2
    Versiones preliminares disponibles en línea Archivado el 2011-01-28 en la
    Wayback Machine como PDF y HTML
  • Jonathan Bartlett: Programming from the Ground Up. Bartlett Publishing, 2004. ISBN 0-9752838-4-7
    Disponible en línea como PDF y como HTML
  • Libro de la Comunidad ASM "Un libro online lleno de información útil sobre ASM, tutoriales y ejemplos de código" por la Comunidad ASM

Software

  • MenuetOS - Sistema operativo escrito completamente en lenguaje ensamblador de 64 bits
  • SB-Assembler para la mayoría de los procesadores/controladores de 8 bits
  • GNU lightning, una biblioteca que genera código en lenguaje ensamblador en tiempo de ejecución que es útil para los compiladores Just-In-Time
  • WinAsm Studio, The Assembly IDE - Free Downloads, Source Code , un IDE de ensamblaje gratuito, un montón de programas de código abierto para descargar y un tablero popular Archivado el 2008-08-05 en la Wayback Machine
  • El ensamblador de redes
  • GoAsm - un componente gratuito de herramientas "Go": soporta la programación de Windows de 32 y 64 bits


 

Preguntas y respuestas

P: ¿Qué es un lenguaje ensamblador?


R: Un lenguaje ensamblador es un lenguaje de programación que se puede utilizar para decirle directamente al ordenador lo que tiene que hacer. Es casi exactamente como el código de máquina que un ordenador puede entender, excepto que utiliza palabras en lugar de números.

P: ¿Cómo entiende un ordenador un programa en ensamblador?


R: Un ordenador no puede realmente entender un programa en ensamblador directamente, pero puede cambiar fácilmente el programa a código máquina sustituyendo las palabras del programa por los números que representan. Este proceso se realiza mediante un ensamblador.

P: ¿Qué son las instrucciones en un lenguaje ensamblador?


R: Las instrucciones en un lenguaje ensamblador son pequeñas tareas que el ordenador realiza cuando está ejecutando el programa. Se llaman instrucciones porque indican al ordenador lo que debe hacer. La parte del ordenador responsable de seguir estas instrucciones se llama procesador.

P: ¿Qué tipo de lenguaje de programación es el ensamblador?


R: El lenguaje ensamblador es un lenguaje de programación de bajo nivel, lo que significa que sólo puede utilizarse para realizar tareas sencillas que un ordenador pueda entender directamente. Para realizar tareas más complejas, hay que descomponer cada tarea en sus componentes individuales y proporcionar instrucciones para cada componente por separado.

P: ¿En qué se diferencia esto de los lenguajes de alto nivel?


R: Los lenguajes de alto nivel pueden tener comandos únicos como PRINT "¡Hola, mundo!" que le dirán al ordenador que realice todas esas pequeñas tareas automáticamente sin necesidad de especificarlas individualmente como tendría que hacer con un programa en ensamblador. Esto hace que los lenguajes de alto nivel sean más fáciles de leer y entender para los humanos que los programas en ensamblador compuestos por muchas instrucciones individuales.

P: ¿Por qué puede ser difícil para los humanos leer un programa en ensamblador?


R: Porque hay que especificar muchas instrucciones individuales para realizar una tarea compleja, como imprimir algo en la pantalla o realizar cálculos sobre conjuntos de datos -cosas que parecen muy básicas y sencillas cuando se expresan en lenguaje humano natural-, por lo que puede haber muchas líneas de código que componen una instrucción, lo que hace que los humanos que no conocen el funcionamiento interno de los ordenadores a tan bajo nivel tengan dificultades para seguir e interpretar lo que ocurre en ellos.

AlegsaOnline.com - 2020 / 2023 - License CC3