Mostrando entradas con la etiqueta x86. Mostrar todas las entradas
Mostrando entradas con la etiqueta x86. Mostrar todas las entradas

sábado, 17 de enero de 2015

Cuestión A: Las arquitecturas ARM / x86 al detalle

Este post lo vamos a dedicar a saber y entender las principales diferencias que existen entre las arquitecturas ARM y x86.
Antes de hablar de las diferencias, vamos a hacer una pequeña introducción de lo que es "ARM" y "x86", para tener una pequeña base sobre ambas arquitecturas y ser capaces de asimilar y entender mejor las diferencias, de las que hablaremos al final de este post.



¿QUÉ ES ARM? 


ARM es una arquitectura RISC (Reduced Instruction Set Computer) de 32 bits, desarrollada por ARM Holdings. En sus albores, fue concebida por Acorn Computers para uso en ordenadores personales, de hecho, los primeros productos basados en ARM fueron los Acorn Archimedes (primer ordenador personal de propósito general, basado en su propia CPU RISC ARM de 32 bits) en 1987.



Acorn Archimedes, 1987


En la recta final de los 80, Acorn se encontraba evolucionando su tecnología, presentado las arquitecturas ARMv2 y preparando la ARMv3. Fue en ese momento cuando Apple, se interesó en colaborar con Acorn para diseñar un nuevo procesador para un dispositivo que presentarían años después, en 1993: Apple Newton (La primera PDA de la historia). En esos años se acabó el diseño de la arquitectura ARMv3 y se presentó la familia de procesadores ARM6.

Características

ARM nació como una arquitectura para uso en ordenadores y dispositivos de bolsillo con aquella primera Newton de Apple que ya hemos mencionado, y siendo este uno de los primeros grandes dispositivos en movilidad.


Los procesadores ARM poseen una arquitectura de muy bajo consumo energético, al menos en comparación con las alternativas existentes en el mercado. Muchas veces incluso podría pasar desapercibido respecto del consumo de otros componentes del mismo dispositivo, por ejemplo en un coche eléctrico. Además, la fabricación de estos chips es bastante barata.

ARM, a partir de su primera versión comercial en los años 80, ha ido evolucionando notablemente a lo largo del tiempo, y se han ido introduciendo instrucciones con cada nueva versión de la arquitectura, no solo para mejorar el rendimiento, sino para mejorar la seguridad.


Hay otros fabricantes que han creado sus arquitecturas basándose en las de ARM, como pro ejemplo Qualcomm, nVidia, Apple o Samsung.


Una de las principales características de ARM es que utiliza relativamente pocos transistores, o al menos, muchos menos que los procesadores de otras arquitecturas. Esta es una de las razones por la que ofrece un rendimiento aceptable con un consumo energético muy bajo, y no solo eso: fabricar un procesador ARM también es notablemente más barato, lo cual los hace ideales en muchos casos.


Diseño

Para mantener el concepto tradicional de RISC, se estableció la ejecución de una orden en un tiempo, por lo general, de un ciclo. La característica más interesante es el uso de los cuatro bits superiores como código de condición, haciendo que cualquier instrucción pueda ser condicional. Este corte reduce el espacio para algunos desplazamientos en el acceso a memoria, pero permite evitar perder ciclos de reloj en el pipeline al ejecutar pequeños trozos de código con ejecución condicional.

Otra característica única del juego de instrucciones es la posibilidad de añadir "shifts" y rotar en el procesamiento de datos. Por ejemplo, la instrucción en C:

a += (j << 2)

Puede ser mejorada como una única instrucción en ARM, permitiendo la reubicación del registro. Eso hace que necesitemos menos operaciones de carga y almacenamiento, mejorando, por ende, el rendimiento.

El procesador ARM, también posee otras características "raras" en otras arquitecturas consideradas también RISC, como el direccionamiento relativo, y el pre y post incremento en el modo de direccionamiento.

Futuro de ARM

En el actual panorama de crisis económica en el que vivimos y de interés por el cuidado de la naturaleza, ARM es una opción que se está estudiando para su uso en grandes centros de datos como sustituto de las actuales tecnologías. Su principal finalidad es la de abaratar los costes relacionados con el enorme consumo energético, no solo por los propios computadores sino también por los ingentes sistemas de ventilación y seguridad asociados.


ARM no solo consume menos energía, sino que también genera menos calor, otra característica muy importante en los datacenters. Ya existen varios ejemplos que han creado sistemas de unos cuantos cientos de procesadores, y si bien, no son los más potentes (no tenemos ningún ARM en el top500 de supercomputadores) si ofrecen un rendimiento más que digno para muchísimas de las tareas más profesionales de la actualidad. Puede ser cuestión de tiempo que ARM entre a formar parte de esta prestigiosa lista.



¿QUÉ ES x86?

Al igual que ARM, x86 es una arquitectura, pero en este caso de tipo CISC. Es una arquitectura poco limpia, el conjunto de instrucciones es titánico, pero a pesar de ello, la popularidad comercial de esta arquitectura (propia de Intel) hizo que muchos fabricantes comenzaran a fabricar en masa microprocesadores compatibles, compañías como AMD, Cyrix o NEC Corporation.


Procesador comercial Intel 4004


En 1971 Intel presenta su primer procesador comercial, el 4004. Es considerado como la primera piedra de todo lo que vino después. A él lo siguieron los Intel 8008 y 8080 en 1972 y 1974 respectivamente. Todos ellos diseñados para utilizarse en las calculadoras de la época. No fue hasta 1981 que Intel posicionó como su principal producto, tras una larga historia de éxitos, a la nueva familia de chips: los procesadores x86. Definidos así por usar el mismo juego de instrucciones que, si bien ha ido evolucionando con los años, sigue utilizando muchas de las características originales. El 8086 es considerado el primer x86 de la historia, aunque el que catapultó la fama de Intel fue su sucesor, el 8088. A él le siguieron los 80186, 80286, 80386 y 80486, presentados a lo largo de la década de los 80 y principios de los 90.


Características

La larga experiencia de Intel ha repercutido en la evolución y cambios de la arquitectura en muchos aspectos, que van desde la mejora en el tamaño de los buses de datos (16, 32 y ahora 64 bits) a muchísimas instrucciones nuevas que se han ido añadiendo a lo largo del tiempo.

Por ejemplo, el 8086 no disponía de operaciones en coma flotante. Y desde hace un par de décadas todos los procesadores x86 comerciales incluyen instrucciones para operar en coma flotante, con lo que no es necesario un coprocesador adicional (como lo era en tiempos del 8086) salvo en ciertos usos muy concretos en los que se necesite de una gran potencia de cálculo, como por ejemplo con sistemas NVidia Tesla.


Diagrama esquemático del Intel 8086


x86 también ha ido incluyendo nuevos sets de instrucciones y mayores registros para afrontar todo tipo de tareas. x86 dispone de cientos de instrucciones, muchas de ellas complejas que podrían subdividirse en varias instrucciones más pequeñas. Sin embargo, la filosofía de CISC es precisamente esta: proporcionar un amplio conjunto de instrucciones que pueden abarcar múltiples tareas más simples.




Futuro de x86


El rendimiento ha ido creciendo en gran manera con cada nueva generación de procesadores. El futuro de esta arquitectura, mezclará su negocio tradicional de equipos de sobremesa portátiles junto a esa nueva categoría de dispositivos "portátiles", móviles y tabletas principalmente.

Es aquí donde aparece su gran rival, ARM. Una arquitectura que en los últimos años creció de una manera sorprendente consiguiendo abrir mercado en los dispositivos pequeños. Son tremendamente eficientes y tienen un precio muy atractivo. Momento es ya de ver las diferencias entre ambas arquitecturas.



x86 vs ARM


Después de haber tomado cierto contacto con ambas arquitecturas, vamos a ver las diferencias entre ambas, lo mejor y lo peor de cada una de ellas cara a cara.


Tanto x86 como ARM son conjuntos de instrucciones o ISA (del inglés Instruction Set Architecture), básicamente se trata de la parte relacionada con la programación, el tipo de máquina o traductor que convierte las líneas de código en instrucciones ejecutables. Si alguien quiere ejecutar un programa fabricado para una de las dos arquitecturas, se encontrará que el otro procesador, no tiene ni la menor idea de como utilizarlo. Esto provoca que sea imposible la ejecución de programas x86 (juegos, sistema operativo Windows7 ...) en ARM.


El procesador más utilizado en equipos de escritorio es el x86, basado en arquitectura CISC, con soporte para operaciones complejas, simultáneas y de ejecución más lenta, que a pesar de simplificar la estructura de la programación, desemboca en un mayor consumo de energía (dada la complejidad de las instrucciones) y también de la necesidad de más espacio físico.


Por otro lado, los procesadores ARM están basados en arquitectura RISC (Reduced Instruction Set Computer); cuya propiedad más destacada e importante es que posee instrucciones de tamaño fijo y con pocos formatos, y que sólo las instrucciones de carga y almacenamiento acceden a la memoria de datos. El objetivo de diseñar máquinas con esta arquitectura es facilitar el paralelismo en la ejecución de instrucciones y permitir realizar tareas menores con procesos más cortos, lo que al final desemboca en una disminución de la energía empleada.


Intel y AMD son los dos grandes fabricantes que han hecho evolucionar los procesadores x86 en entornos de escritorio en su mayoría. Mientras que el desarrollo de los chips ARM ha sido gracias a distintos fabricantes, como Qualcomm, Texas Instruments, Apple o Samsung entre otros muchos. Esto es así porque ARM Holdings, la empresa que desarrolló ARM, decidió licenciarla haciéndola accesible a los diferentes fabricantes por unas pequeñas tasas.




Esto ha permitido que los procesadores ARM se encuentren en multitud de dispositivos de marcas diversas, mientras que la arquitectura x86 originaria de Intel, solo se encuentra en sus procesadores y en los de AMD.




Si hablamos de sistemas operativos, podemos clasificarlos en los que están orientados a x86 y los que lo están en ARM. La arquitectura ARM, al tener una eficiencia energética superior, está presente en sistemas como Android o iOS, pero recientemente, empresas como Microsoft se han dado cuenta de las restricciones que esto conlleva y están luchando contra esto con sistemas como Windows8 y sus aplicaciones Metro, diseñadas para ser ejecutadas por ambas arquitecturas.


Diferencias a nivel de hardware

A nivel de hardware, la diferencia principal entre ARM y x86 es que tienen distintos conjuntos de instrucciones. Por así decirlo, hablan un lenguaje distinto, lo que hace totalmente incompatible un sistema con el otro. Es decir, que no se pueden compartir binarios entre ambos sistemas.

Si pasamos a explorar cómo se comportan los procesadores, veremos que ARM tiene una gran ventaja sobre x86 en el consumo de energía. Al tener una estructura e instrucciones más sencillas, el ARM consume muchísima menos energía que los procesadores Intel normales. Como podréis imaginar, esto le hace el candidato perfecto para dispositivos móviles como teléfonos o tablets.

Sin embargo,los procesadores x86 son los que destacan en rendimiento. Esa arquitectura más compleja permite más optimizaciones que se hacen mientras se ejecuta la aplicación, como por ejemplo el intercambio de orden de instrucciones para mejorar el tiempo de ejecución.

A pesar de que tanto ARM como Intel trabajan para reducir esas ventajas de uno sobre el otro, las diferencias siguen siendo suficientemente notables para que cada procesador se use en un tipo distinto de computadoras: los Intel a los ordenadores, y los ARM a los móviles y tablets.

Diferencias trasladadas al software

En el punto anterior, decíamos que, debido a los diferentes conjuntos de instrucciones, los binarios no son compatibles entre ambas arquitecturas. Entonces, ¿por qué sí que se pueden ejecutar las mismas aplicaciones Metro en Windows8 y Windows RT?

La respuesta es que las aplicaciones Metro no son exactamente código binario. Normalmente, cuando estáis en un ordenador (Sea Windows, Max o Linux) y compilas un programa, se crea un archivo que contiene las instrucciones que se ejecutarán directamente en el procesador.

Sin embargo, las aplicaciones Metro se compilan en un lenguaje intermedio, MSIL, que es luego interpretado por el sistema. ¿Qué ventaja tiene? Que un mismo ejecutable funciona en cualquier arquitectura.

En cuanto al rendimiento, no podremos instalar un software potente como Mathematica o Visual Studio en un sistema con procesador ARM y pretender que funcione igual que en un procesador Intel.

También ARM restringe lo que podemos hacer en el ordenador. Por ejemplo, instalar Linux en un tablet ARM no será tan fácil como hacerlo con un ordenador Intel. No sólo por temas de drivers, sino porque necesitaríamos versiones específicas del sistema hechas para procesadores ARM. Tampoco podremos controlar el arranque del sistema y nos tendremos que olvidar de quitar el "Secure Boot" o de modificar los ajustes de la BIOS.

La conclusión en este punto es que ARM es un tipo de procesador dirigido especialmente a móviles y tablets y windows RT aprovecha las posibilidades que da. Más autonomía y un rendimiento más que suficiente para un sistema en el que la actividad que vayamos a desarrollar no sea demasiado grande.


Bibliografía.

Cuestión B: Codificando en ARM / x86. Pros y contras.

El programa que hemos propuesto, se trata de una calculadora de áreas.

La aplicación, inicialmente, mostrará un menú con tres opciones disponibles:

            Figuras:
            1. Cuadrado
            2. Rectángulo
            3. Triángulo

            Opción:


El usuario introducirá una opción [1-3] y posteriormente se le solicitarán por teclado los datos necesarios para calcular el área seleccionada.

El dato se mostrará por pantalla, y se le dará la oportunidad al usuario de introducir una nueva opción.

Este programa se presentará tanto en ARM, como para x86.

"ÁREAS" EN x86

En primer lugar vamos a ir viendo poco a poco las diferentes partes en las que se divide el código, y ver la función que desempeña cada una de ellas.

Como bien sabemos, un programa en x86, posee una estructura bien definida. Dicha estructura se divide en tres secciones: .DATA, .CODE y .CONST. Cada una de ellas tiene un propósito bien claro y definido. En lugar de dar una definición teórica de lo que son cada una de ellas, vamos a ir viéndolas paso a paso tomando como ejemplo nuestro programa.

Sección .DATA


.Data

operador DD 0, 0, 0
solucion DD 0
sino DD 's', 'n', 0



En esta sección, lo que vamos a hacer es incluir los datos que queremos definir en memoria. Definiremos tres etiquetas ('operador', 'solucion' y 'sino'), a través de las cuales, accederemos posteriormente al dato que contienen.

Todos los datos que hemos incluido en memoria tienen un tamaño de 32 bits, esto es muy importante, ya que se van a reservar en memoria tantos bits como hayamos designado. (DD = Doble palabra, 32 bits).


  • Operador: Es un vector de tres posiciones. Una para la opción seleccionada y las otras dos para almacenar los parámetros que vamos a necesitar por cada operación.
  • solucion: Dato donde albergaremos la solución.
  • sino: Variable de tipo "vector" con tres posiciones que utilizaremos para controlar si el usuario quiere salir de la aplicación o vuelve al menú principal para seleccionar otra opción.
Sección .CODE

En esta sección escribiremos el grueso de las operaciones que se van a llevar a cabo en esta práctica. Dado que es un código bastante extenso como para verlo en un solo bloque, vamos a ir desglosando la sección .Code en otras más sencillas para comprender mejor lo que se hace en el programa.


.Code

start:
     ;Inicio del programa principal.
     Invoke puts, "Figuras:"
     Invoke puts, "1 - cuadrado"
     Invoke puts, "2 - rectangulo"
     Invoke puts, "3 - triangulo"
     Invoke puts, ""
     Invoke printf, "Opcion: "
     Invoke scanf, "%d", Addr operador
 
     Jmp opciones
fin:
     Invoke puts, ""
     Invoke system, "pause"
     Xor Eax, Eax
     Invoke ExitProcess, Eax

     ;Fin del programa principal



En este código, se crea el menú principal. Para ello hemos hecho usode la pseudoinstrucción "Invoke". Con esta pseudoinstrucción, vamos a realizar llamadas a la API de C para utilizar sus funciones. Además de pasarle como parámetro la función que queremos utilizar, también hay que pasarle los parámetros (en el mismo orden en el que se pondrían en un programa de C) que esta función recibiría. 

Así, en el caso de "puts", también se le pasa la cadena que queremos que se pinte por pantalla. Lo mismo pasa con "printf" o con "scanf". Como caso especial, fijémonos en "scanf". Los parámetros que le pasamos son "%d" y "Addr operador". ¿Por qué usamos ADDR?. Porque si recordamos, "scanf" leía la dirección del dato que se pretende leer. 

Una vez leído el dato, pasaríamos a procesar la opción introducida por teclado, para posteriormente, ejecutar la subrutina asociada a dicha opción.



opciones:
     Mov eax, [operador]

     Cmp eax, 1
     Je > cuadrado

     Cmp eax, 2
     Je > rectangulo

     Cmp eax, 3
     Je > triangulo

     Invoke puts, "Elige una opcion valida."
     Jmp start



En el código de arriba, gestionamos la opción solicitada por el usuario. En primer lugar vamos a almacenar la opción en el registro EAX. Una vez que el dato ya está en el registro, vamos averiguar de que opción se trata. Comparamos si el dato almacenado en EAX (y que se corresponde con la opción seleccionada) es igual a 1, a 2 o a 3, para posteriormente hacer un salto a la subrutina asociada a dicha opción.

Una vez que la subrutina acabe su ejecución, el programa volverá al punto desde donde fue llamada dicha subrutina y se nos mostrará el menú de nuevo (Jmp start)

A continuación, vamos a ver el código que implementa las operaciones que se llevan a cabo en esta práctica:



cuadrado:
    Invoke printf, "Introduzca lado: "
    Invoke scanf, "%d", Addr operador + 4

    Xor eax, eax
    Add eax, [operador + 4]
    Mul D[operador + 4]

    Jmp sol

rectangulo:
    Invoke printf, "Introduzca ancho altura: "
    Invoke scanf, "%d %d", Addr operador + 4, Addr operador + 8

    Xor eax, eax
    Add eax, [operador + 4]
    Mul D[operador + 8]

    Jmp sol

triangulo:
    Invoke printf, "Introduzca base altura: "
    Invoke scanf, "%d %d", Addr operador + 4, Addr operador + 8

    Xor eax, eax
    Add eax, [operador + 4]
    Mul D[operador + 8]
    Shr eax, 1

    Jmp sol



En las tres subrutinas, vemos que utilizamos a "Invoke" tanto para escribir mensajes como para solicitar los datos necesarios para la correcta ejecución de las subrutinas. Veamos en detalle que hacemos en "Cuadrado", paso a paso:

  • Hacemos uso de la pseudoinstrucción Invoke para llamar a la función "printf" de la API de C pasándole, además, la cadena que queremos mostrar, en este caso "Introduzca lado:"
  • Volvemos a hacer uso de Invoke, en esta ocasión para leer el lado y almacenarlo en la segunda posición del vector "operador" (Addr operador + 4)
  • Inicializamos el registro EAX (Xor eax, eax). No queremos obtener resultados indeseados, así que borramos el contenido que tuviese antes el registro.
  • Añade el dato almacenado en [operador + 4] (segunda posición del vector) sobre el registro EAX y finalmente realiza la multiplicación (Mul D[operador + 4]), se multiplica el valor almacenado en EAX por el dato almacenado en [operador + 4].

Estos cuatro pasos son los que, a priori, se realizan en las cuatro subrutinas; lectura de datos, inicilización de registros y operar. Así mismo, al final de cada subrutina se hace un salto a la etiqueta "sol"; Esto se hace para mover el contenido del registro EAX (que almacena el resultado) a la variable "solucion", para posteriormente imprimirla por pantalla y volver pedir al usuario la opción de introducir una nueva operación de nuestra calculadora de áreas, o si prefiere salir, esto se hace en el siguiente código:



volver:
    Pusha
    Invoke scanf, "%c", Addr sino + 2
    Invoke printf, "Desea volver a calcular un area (s/n): "
    Invoke scanf, "%c", Addr sino + 2
    Popa

    Xor eax, eax
    Xor ebx, ebx

    Mov al, [sino + 2]
    Mov bl, [sino]
    Cmp eax, ebx
    Je start

    Mov bl, [sino + 1]
    Cmp eax, ebx
    Je fin

    Invoke puts, "Introduzca una opcion valida."
    Jmp volver



Una vez que el usuario obtiene el resultado, se le da la opción de seguir en la calculadora o volver a ejecutar una de las tres opciones disponibles inicialmente.

Finalmente, se presenta todo el código en un bloque:


;EasyCodeName=Module1,1
.Const

.Data

operador DD 0, 0, 0
solucion DD 0
sino DD 's', 'n', 0

.Code

start:
     ;Inicio del programa principal.
     Invoke puts, "Figuras:"
     Invoke puts, "1 - cuadrado"
     Invoke puts, "2 - rectangulo"
     Invoke puts, "3 - triangulo"
     Invoke puts, ""
     Invoke printf, "Opcion: "
     Invoke scanf, "%d", Addr operador
 
     Jmp opciones
fin:
     Invoke puts, ""
     Invoke system, "pause"
     Xor Eax, Eax
     Invoke ExitProcess, Eax

     ;Fin del programa principal

     ;Subprogramas:

opciones:
     Mov eax, [operador]

     Cmp eax, 1
     Je > cuadrado

     Cmp eax, 2
     Je > rectangulo

     Cmp eax, 3
     Je > triangulo

     Invoke puts, "Elige una opcion valida."
     Jmp start

sol:
     Mov [solucion], eax

     Pusha
     Invoke printf, "solucion: %d (unidad cuadrada)", [solucion]
     Invoke puts, ""
     Popa

     Xor eax, eax

     Jmp volver

volver:
    Pusha
    Invoke scanf, "%c", Addr sino + 2
    Invoke printf, "Desea volver a calcular un area (s/n): "
    Invoke scanf, "%c", Addr sino + 2
    Popa

    Xor eax, eax
    Xor ebx, ebx

    Mov al, [sino + 2]
    Mov bl, [sino]
    Cmp eax, ebx
    Je start

    Mov bl, [sino + 1]
    Cmp eax, ebx
    Je fin

    Invoke puts, "Introduzca una opcion valida."
    Jmp volver

cuadrado:
    Invoke printf, "Introduzca lado: "
    Invoke scanf, "%d", Addr operador + 4

    Xor eax, eax
    Add eax, [operador + 4]
    Mul D[operador + 4]

    Jmp sol

rectangulo:
    Invoke printf, "Introduzca ancho altura: "
    Invoke scanf, "%d %d", Addr operador + 4, Addr operador + 8

    Xor eax, eax
    Add eax, [operador + 4]
    Mul D[operador + 8]

    Jmp sol

triangulo:
    Invoke printf, "Introduzca base altura: "
    Invoke scanf, "%d %d", Addr operador + 4, Addr operador + 8

    Xor eax, eax
    Add eax, [operador + 4]
    Mul D[operador + 8]
    Shr eax, 1

    Jmp sol




"ÁREAS" EN ARM

A continuación vamos a ver el mismo programa, pero desarrollado en ARM y comentado exhaustivamente.



AREA  areas,CODE,READWRITE

SWI_EscrCar EQU &0   ; codigo de impresion de caracter (0) 
                         ; asignado a SWI_EscrCar
SWI_Salir EQU &11    ; codigo de impresion de salida del programa(11)
SWI_write0 EQU  &2
SWI_ReadC EQU  &4


 ENTRY               ; Punto de entrada del código
 
 ADRL r0, cad1       ; Obtenemos la direccion de la cadena
 SWI SWI_write0      ; Interrupcion de Soft. para mostrar la cadena
 SWI SWI_ReadC       ; Interrupcion de Soft. para leer un caracter de
                     ;teclado

 CMP r0, #49         ; tecla 1 ??
 BLEQ CUADR          ; Calcular area del cuadrado
 CMP r0,#50          ; tecla 2 ??
 BLEQ RECTA          ; Calcular area del rectangulo
 CMP r0,#51          ; tecla 3 ??
 BLEQ TRIAN          ; Calcular area del triangulo

              
DATO1 SWI SWI_write0 ; Interrupcion de Soft. para mostrar la cadena   
 ; poner punto de interrupcion en la linea siguiente
 MOV r1, r0          ; Movemos el valor introducido en r0 a r1
 MOV pc, r14
          
DATO2 SWI SWI_write0 ; Interrupcion de Soft. para mostrar la cadena
 ; poner punto de interrupcion en la linea siguiente
 MOV r2, r0          ; Movemos el valor introducido en r0 a r1
 MOV pc, r14



SALIR ADRL     r0, cad5  ; Obtenemos la direccion de la cadena
 SWI      SWI_write0     ; Interrupcion de Soft. para mostrar la cadena
 SWI      SWI_Salir      ; Sale del programa



CUADR ADRL  r0, cad2     ; Obtenemos la direccion de la cadena
 BL  DATO1               ; Pedimos el dato
 MUL  r3,r1,r1           ; lado * lado
 B  SALIR

RECTA ADRL  r0, cad3     ; Obtenemos la direccion de la cadena
 BL  DATO1               ; Pedimos el dato
 ADRL  r0, cad4          ; Obtenemos la direccion de la cadena
 BL  DATO2               ; Pedimos el dato
 MUL  r3,r1,r2           ; base * altura
 B  SALIR

TRIAN ADRL  r0, cad3     ; Obtenemos la direccion de la cadena
 BL  DATO1               ; Pedimos el dato
 ADRL  r0, cad4          ; Obtenemos la direccion de la cadena
 BL  DATO2               ; Pedimos el dato
 MUL  r4,r1,r2           ; dividendo = base * altura
 B  DIVID                ; dividimos entre 2

DIVID MOV  r2, #2        ; divisor = 2
 MOV  r3, #0             ; cociente = 0
 BL  DIVCOND             ; condicion
 B  SALIR                ; terminar

DIVBUC SUB  r4, r4, r2   ; dividendo = dividendo - divisor
 ADD  r3, r3, #1         ; cociente++
DIVCOND CMP  r4, r2      ; dividendo >= divisor
 BGE  DIVBUC             ; hacer bucle
 B  SALIR



cad0  = "", 0
cad1  = "Calcular area de:", &0a, &0d, "1. CUADRADO", &0a, &0d, 
        "2. RECTANGULO", &0a, &0d,"3. TRIANGULO", &0a, &0d, "Opcion: ",0
cad2 = &0a, &0d, "Introduce el lado en r0 y pulsa F5", &0a, &0d, 0
cad3  = &0a, &0d, "Introduce la base en r0 y pulsa F5", &0a, &0d, 0
cad4  = "Introduce la altura en r0 y pulsa F5", &0a, &0d, 0
cad5  = &0a, &0d, "Calculo terminado. Resultado en r3", &0a, &0d, 0

 END


PROS Y CONTRAS

Una de las cosas más atractivas de x86, es que nos da la posibilidad de poder utilizar funciones de C, usando la API de C mediante la pseudoinstrucción INVOKE. Al poder hacer esto, hemos podido "ahorrar" tiempo de desarrollo, ya que no hemos tenido que implementar dicha funcionalidad. En ARM no hemos podido llevar a cabo esta ventaja, en su lugar hay que utilizar interrupciones del sistema.

En cuanto al carácter "CISC" de la arquitectura x86, hace que esta tenga un amplio conjunto de instrucciones, y muchas de ellas son de gran complejidad, por lo cual las operaciones que realizan son mucho más completas. Es decir, lo que podemos ser capaces de hacer en x86 con una única instrucción, en ARM necesitaríamos varias líneas, lo cual hace una vez más a x86 como la opción más sencilla a la hora de elaborar un programa.

miércoles, 14 de enero de 2015

Cuestión D: Programando en x86

En la última cuestión de esta práctica, se nos propone la resolución de un problema sencillo en cualesquiera de las arquitecturas que hemos dado en clase, a saber: ARM o x86.

Nosotros hemos decidido hacerlo en x86. Aunque su conjunto de instrucciones es extenso y complejo, nos brida la ayuda "especial" de poder hacer uso de la API de C. Además, las instrucciones de x86, por el mero hecho de su complejidad intrínseca, hacen que sea posible realizar una  misma operación utilizando menos instrucciones que en ensamblador ARM.

El problema que hemos decidido desarrollar, se trata de un verificador de fechas. El usuario introducirá la fecha en el formato "dd mm aaaa" ó 0 para salir. A la hora de verificar la fecha, se tendrán en cuenta las siguientes cuestiones:
            
         1. Que el año sea bisiesto, por lo que si el mes       
            introducido es febrero, tendremos que comprobar que 
            tenga 29 días, y no 28, que sería lo normal.
         2. Que se trate de un mes de 30 días, 31 días ó 29 días.

Una vez visto el enunciado del problema, pasamos a la codificación del mismo.

Vamos a ver un desglose de las partes más importantes del programa programa, para comprender los entresijos del mismo.

Variables

En primer lugar, vamos a crear las variables que vamos a usar en este ejercicio: fecha, que será un vector de tres posiciones en el que guardaremos el día, el mes y el año (en esa posición) y bis, que será otro vector de tres enteros que nos harán falta más adelante para comprobar si un año es o no es bisiesto.


;EasyCodeName=Module1,1
.Const

.Data

fecha DD 0, 0, 0
bis DD 4, 400, 100

Programa principal

A continuación presentamos lo que podemos llamar "programa principal". Lo primero que se hace en este caso (start), es saltar a la etiqueta "introducir" para comenzar a leer los datos necesarios.

Bajo la etiqueta "fvalida" lo único que hacemos es mostrar el mensaje avisando que la fecha introducida es válida y volvemos a pedir una nueva fecha.

Comentar también que cuando el usuario introduzca el valor "0" en la fecha, se hará un salto a la etiqueta "fin" con la idea de terminar la ejecución del programa. Cuando se finaliza la ejecución, inicializamos el contenido del registro EAX y salimos.

.Code

start:
 ; comienzo del programa
 Jmp introducir

fvalida:
 Invoke puts, "fecha valida"
 Jmp start

fin:
 Invoke puts, ""
 Invoke system, "pause"
 Xor Eax, Eax
 Invoke ExitProcess, Eax

;Fin del programa principal

Introducción de datos

Mediante la API de C y haciendo uso de la pseudoinstrucción INVOKE, vamos a llamar las funciones "puts" y "scanf" de C. Recordar que INVOKE sustituye a las interrupciones al sistema que se hacen cuando programamos en ARM y necesitamos pedir un dato o escribir un texto.

También aprovechamos y comparamos si la fecha introducida es "0". Si esto es así, saltamos a la etiqueta "fin" y acabamos el programa.

introducir:
 Invoke puts, ""
 Invoke puts, "Introduce fecha (dd mm aaaa) (0 mm aaaa para salir):"
 Invoke scanf, "%d %d %d", Addr fecha, Addr fecha + 4, Addr fecha + 8

 Cmp D[fecha], 0  ; 0 para salir
 Je fin

 Jmp validar

Validación

Podemos decir que éste es el "corazón" de nuestro problema. Como siempre, empezamos inicializando los registros que vamos a usar: EAX, EBX, ECX y EDX; para, posteriormente introducir en el registro EAX el día ([fecha]), en EBX el mes (siguiente elemento del vector fecha [fecha+4]) y en ECX el año (tercer y último elemento del citado vector [fecha + 8])

A continuación vamos a comprobar que los límites sean los correctos. Si el día es menor que 1 o mayor que 31, la fecha no será considerada válida; Si el mes es menor que 1 o mayor que 12, por razones obvias tampoco será considerada la fecha válida. Y finalmente, hemos fijado como intervalo de años posibles aquel comprendido entre 1600 y 9999, por lo que cualquier año introducido que esté fuera de ese rango, será considerado no válido y por consiguiente, la fecha tampoco.

Una vez que hemos pasado la primera "criba" hay que saber que los meses pares de Enero a Julio, tienen 30 días (tomando febrero como una excepción) y que los meses impares de Agosto a Diciembre, tienen 30 días.

Para solucionar este problema que se nos plantea, movemos el mes a un registro auxiliar, que previamente hemos inicializado (dx). Usaremos la operación AND contra el primer bit, y si dx = 1, quiere decir que dicho mes es impar. Con esto sabremos la paridad del mes. Por lo que si lo que hay almacenado en dx es diferente de 1, saltamos a la validación de un mes par (saltamos a mpares), en caso contrario, saltamos a mimpares.

Si una vez que el programa comprueba si los días del mes son válidos, una vez que acaben de ejecutarse las subrutinas que hemos llamado (mimpares, mpares), si estas acaban con éxito, volverán al punto desde donde se produjo el salto, continuará con la ejecución y saltará a la etiqueta fvalida, ya que, si vuelve a ese punto del programa es porque el día del mes ha sido validado con éxito. En otro caso, la subrutina adecuada será la que envíe el flujo del programa hacia otra subrutina para mostrar el error.

validar:
 Xor Eax, Eax
 Xor Ebx, Ebx
 Xor Ecx, Ecx
 Xor Edx, Edx

 Mov Eax, [fecha]
 Mov Ebx, [fecha + 4]
 Mov Ecx, [fecha + 8]

 ; validamos limites minimos y maximos
 Cmp Eax, 1  ; dia < 1
 Jl > fnovalida
 Cmp Eax, 31  ; dia > 31
 Jg > fnovalida

 Cmp Ebx, 1  ; mes < 1
 Jl > fnovalida
 Cmp Ebx, 12  ; mes > 12
 Jg > fnovalida

 Cmp Ecx, 1600       ; año aprox de calendario gregoriano
 Jl > fnovalida
 Cmp Ecx, 9999       ; fijamos un año maximo
 Jg > fnovalida

 ; los meses pares de enero a julio tiene 30 dias
 ; excepto febrero

 Mov dx, bx     ; movemos a un registro auxiliar
 And dx, 1      ; usamos la operacion logica and contra el primer bit
 Cmp dx, 1      ; si dx=1 es impar
 Jne > mpares   ; validacion de meses pares

 ; los meses impares de agosto a diciembre tienen 30 dias
 Cmp dx, 1
 Je > mimpares  ; validacion de meses impares

 Jmp fvalida    ; si llega aqui es fecha valida

Comprobamos los días del mes

En este trozo de código comprobamos si el día del mes, según su paridad (mes par o impar) tiene un valor exacto. En el caso de "mimpares", comprobamos primero si el mes es menor que Agosto (Cmp Ebx, 8) y si este tiene menos de 30 días, si esto es cierto (si el valor de EBX, que es el mes, es menor que 8) la fecha es válida. Recordar que JL salta si EBX es menor que 8 teniendo en cuenta el signo). En el caso que no se cumpla alguna de las condiciones para que la fecha sea válida, se saltará a la etiqueta "fnovalida" y se volverá a pedir una nueva fecha por teclado.

En este punto debemos tener una consideración especial; el mes de febrero. Cuando se trata de febrero, aunque es un mes par, necesitamos saber si el año de la fecha es o no bisiesto. En ese caso, si es bisiesto, no puede tener más de 29 días, y si no es bisiesto, no puede tener más de 28 días.

Para entender este código, primero debemos entender cuando un año es bisiesto. Como regla general, un año es bisiesto siempre que es divisible por cuatro. Pero hay una excepción, si ese mismo año es divisible por 100 pero no lo es por 400, el año NO será bisiesto aunque cumpla la regla general.

Así que almacenamos en EAX, el valor del año ([fecha+8]) y comparamos si es divisible por cuatro e inicializamos el registro EDX, ¿por qué hacemos esto? porque DIV coloca el resultado de la división en el registro EDX. Una vez que tengo el resto almacenado en EDX (Div D[bis]), comprobamos si es igual a 0, (es divisible por 4) en el caso que sea divisible por 4, salta a "bisiesto", si no es divisible por 4 sigue  adelante. (Tener en cuenta que el hecho de que el año sea múltiplo de 4, no nos asegura que el año sea bisiesto, hay que hacer más comprobaciones que vemos a continuación, en el código siguiente)


mimpares:
 Cmp Ebx, 8     ; vemos si es menor a agosto
 Jl fvalida     ; fecha valida

 Cmp Eax, 30    ; vemos si tiene mas de 30 dias
 Jg > fnovalida

 Jmp fvalida    ; si llega aqui es fecha valida

mpares:
 Cmp Ebx, 2     ; vemos si es febrero
 Je > febrero   ; validacion para febrero

 Cmp Ebx, 7     ; vemos si es mayor a julio
 Jg fvalida     ; fecha valida

 Cmp Eax, 30    ; vemos si tiene mas de 30 dias
 Jg > fnovalida

 Jmp fvalida    ; si llega aqui es fecha valida
febrero:
 ; vemos si es bisiesto
 ; multiplo de 4 y no multiplo de 100 o multiplo de 400
 Mov Eax, [fecha + 8]    ;dividendo
 Xor Edx, Edx
 Div D[bis]              ; multiplo de 4 (divisor)
 Cmp Edx, 0              ; comparamos resto division
 Je > bisiesto        ; si no es bisiesto continua


Comprobamos si el año es bisiesto

Si el año es divisible por 4 (esto lo comprobamos en el código anterior), y no lo es por 100 el año es bisiesto y ya no necesito comprobar nada más. 

Si no es divisible por 100 tenemos que comprobar si lo es por 400. En caso de que sea múltiplo de 400 es bisiesto, pero si no lo es, definitivamente el año no sería bisiesto.

Una vez que ya sabemos si el año es bisiesto, solo queda comprobar que no tenga más de 29 días (Cmp Eax, 29). En caso contrario, la fecha no sería válida.

nobisiesto:
 Mov Eax, [fecha]
 Cmp Eax, 28             ; vemos si tiene mas de 28 dias
 Jg > fnovalida

 Jmp fvalida             ; si llega aqui es fecha valida

esbisiesto:
 Mov Eax, [fecha]
 Cmp Eax, 29             ; vemos si tiene mas de 29 dias
 Jg > fnovalida

 Jmp fvalida             ; si llega aqui es fecha valida

bisiesto:
 Mov Eax, [fecha + 8] ;dividendo
 Xor Edx, Edx
 Div D[bis + 8] ; no multiplo de 100 (divisor)
 Cmp Edx, 0  ; comparamos resto division
 Jne esbisiesto ; validamos para año bisiesto, sino continua

bisiesto2:
 Mov Eax, [fecha + 8] ;dividendo
 Xor Edx, Edx
 Div D[bis + 4] ; multiplo de 400 (divisor)
 Cmp Edx, 0  ; comparamos resto division
 Je esbisiesto ; es año bisiesto

 Jmp nobisiesto ; validamos para año no bisiesto

Código completo

Finalmente vemos el código en conjunto.


;EasyCodeName=Module1,1
.Const

.Data

fecha DD 0, 0, 0
bis DD 4, 400, 100

.Code

start:
 ; comienzo del programa
 Jmp introducir

fvalida:
 Invoke puts, "fecha valida"
 Jmp start

fin:
 Invoke puts, ""
 Invoke system, "pause"
 Xor Eax, Eax
 Invoke ExitProcess, Eax

;Fin del programa principal

;Subprogramas:

introducir:
 Invoke puts, ""
 Invoke puts, "Introduce fecha (dd mm aaaa) (0 mm aaaa para salir):"
 Invoke scanf, "%d %d %d", Addr fecha, Addr fecha + 4, Addr fecha + 8

 Cmp D[fecha], 0  ; 0 para salir
 Je fin

 Jmp validar

validar:
 Xor Eax, Eax
 Xor Ebx, Ebx
 Xor Ecx, Ecx
 Xor Edx, Edx

 Mov Eax, [fecha]
 Mov Ebx, [fecha + 4]
 Mov Ecx, [fecha + 8]

 ; validamos limites minimos y maximos
 Cmp Eax, 1  ; dia < 1
 Jl > fnovalida
 Cmp Eax, 31  ; dia > 31
 Jg > fnovalida

 Cmp Ebx, 1  ; mes < 1
 Jl > fnovalida
 Cmp Ebx, 12  ; mes > 12
 Jg > fnovalida

 Cmp Ecx, 1600       ; año aprox de calendario gregoriano
 Jl > fnovalida
 Cmp Ecx, 9999       ; fijamos un año maximo
 Jg > fnovalida

 ; los meses pares de enero a julio tiene 30 dias
 ; excepto febrero

 Mov dx, bx     ; movemos a un registro auxiliar
 And dx, 1      ; usamos la operacion logica and contra el primer bit
 Cmp dx, 1      ; si dx=1 es impar
 Jne > mpares   ; validacion de meses pares

 ; los meses impares de agosto a diciembre tienen 30 dias
 Cmp dx, 1
 Je > mimpares  ; validacion de meses impares

 Jmp fvalida    ; si llega aqui es fecha valida

mimpares:
 Cmp Ebx, 8     ; vemos si es menor a agosto
 Jl fvalida     ; fecha valida

 Cmp Eax, 30    ; vemos si tiene mas de 30 dias
 Jg > fnovalida

 Jmp fvalida    ; si llega aqui es fecha valida

mpares:
 Cmp Ebx, 2     ; vemos si es febrero
 Je > febrero   ; validacion para febrero

 Cmp Ebx, 7     ; vemos si es mayor a julio
 Jg fvalida     ; fecha valida

 Cmp Eax, 30    ; vemos si tiene mas de 30 dias
 Jg > fnovalida

 Jmp fvalida    ; si llega aqui es fecha valida

febrero:
 ; vemos si es bisiesto
 ; multiplo de 4 y no multiplo de 100 o multiplo de 400
 Mov Eax, [fecha + 8]    ;dividendo
 Xor Edx, Edx
 Div D[bis]              ; multiplo de 4 (divisor)
 Cmp Edx, 0              ; comparamos resto division
 Je > bisiesto        ; si no es bisiesto continua

nobisiesto:
 Mov Eax, [fecha]
 Cmp Eax, 28             ; vemos si tiene mas de 28 dias
 Jg > fnovalida

 Jmp fvalida             ; si llega aqui es fecha valida

esbisiesto:
 Mov Eax, [fecha]
 Cmp Eax, 29             ; vemos si tiene mas de 29 dias
 Jg > fnovalida

 Jmp fvalida             ; si llega aqui es fecha valida

bisiesto:
 Mov Eax, [fecha + 8] ;dividendo
 Xor Edx, Edx
 Div D[bis + 8] ; no multiplo de 100 (divisor)
 Cmp Edx, 0  ; comparamos resto division
 Jne esbisiesto ; validamos para año bisiesto, sino continua

bisiesto2:
 Mov Eax, [fecha + 8] ;dividendo
 Xor Edx, Edx
 Div D[bis + 4] ; multiplo de 400 (divisor)
 Cmp Edx, 0  ; comparamos resto division
 Je esbisiesto ; es año bisiesto

 Jmp nobisiesto ; validamos para año no bisiesto


fnovalida:
 Invoke puts, "Fecha no valida"

 Jmp start