miércoles, 22 de febrero de 2012

Sintáxis AT&T(GAS)

Como previo a la tarea introductoria, decidí investigar un poco sobre la sintaxis que utiliza el GNU Assembler, al pasar el código de C a ensamblador. Esta sintaxis es llamada AT&T y su ventaja es la compatibilidad con GCC para generare el código ensamblador de un determinado código en lenguaje C. 

Para entender un poco del mismo, empezaré con un simple hola mundo, para comprender las operaciones con el stack y demás, y luego explicaré como funcionan los ciclos for y while, además de las condiciones en lenguaje ensamblador usando como ejemplo un programa sencillo para imprimir números pares.

Información General:
  • At&T tiene la forma instruccion orígen, destino. Como por ejemplo movl $1, %28(esp). La instrucción mueve el número uno, al offset 28 del stack pointer.
  • $ es usado para valores literales, % para registros.
  • Los sufijos al final de las instrucciones para designar el tamaño del operando que se manipulará:
    • b = byte (8 bit)
    • s = short (16 bit entero) o un solo (32-bit punto flotante)
    • w = word (16 bit)
    • l = long (32 bit entero or 64-bit punto flotante)
    • q = quad (64 bit)
    • t = ten bytes (80-bit punto flotante)
Hola Mundo

Que mejor forma de empezar en un nuevo lenguaje que con un hola mundo. Primero que nada lo haremos en C, y lo pasaremos a ensamblador usando el comando gcc -S nombrearchivo.c. El código en C, y su compilación-ejecución, es el siguiente:

Código:


Compilación y Ejecución:


Como se puede ver, todo funciona correctamente, ahora podemos pasarlo a ensamblador usando el siguiente comando:


Esto nos generará un archivo ".s"(en mi caso llamado hello.s), el cual podemos abrir para ver el código equivalente en lenguaje ensamblador. El código del hola mundo en ensamblador es el siguiente:


La información inicial, es agregada por el compilador gcc al generar el archivo .s, pero esta no afecta realmente el desempeño de nuestro programa, así que puede ser omitida. Después podemos encontrar la etiqueta .LC0, que declara un string llamado "Hola Mundo", que será utilizado más adelante para imprimirlo. 

Como etiqueta podemos usar prácticamente cualquier combinación alfanumérica, precedida por un punto, en este caso LC0 es el nombre por default que agrega el compilador gcc.

Después se declara la etiqueta main como global, para que todos puedan acceder a ella, y decimos que main es una función. Dentro de la etiqueta main es donde encontramos la parte importante. Las primeras cuatro líneas son típicas en una surutina, lo que hacen es guardar espacio para las variables locales.

Después, se mueve el string guardado en la etiqueta .LC0 hacia al inicio del stack, y lo imprimimos. En este caso gcc cambio la función printf del código en C, por la función puts, pero la aplicación es similar. Al final, guardamos 0 en eax, como nuestro valor del return(guardar valores de retorno en eax es algo común). Al final la instrucción "leave" libera la memoria del stack y retorna el control.

Ciclos y Condiciones

Para entender un poco más sobre ensamblador y los ciclos en este lenguaje, hice un simple programa que imprime los numeros pares entre el 1 y el 10(para hacerlo corto), y lo pase a lenguaje ensamblador para ver como se comportan. Los códigos en lenguaje C son los siguientes;


No tienen nada fuera de lo común, un simple main con una sola variable, un ciclo que se repite hasta el 10, y una condición que decide si el contador será par o no para poder imprimirlo si es par. Para pasarlos a ensamblador es el mismo procedimiento que en el hola mundo. 

Los códigos en ensamblador de los códigos pasados, son los siguientes:

                                    while                                                                            for


Mi primera impresión fue que los códigos eran muy parecidos, excepto que el código del for tenía una etiqueta más separada, explicaré por qué.

Ciclo For

Como se mencionó anteriormente las líneas que se ven de rojo son etiquetas, básicamente sirven para controlar el flujo del programa y moverse a ellas a determinadas condiciones y puntos en el programa. Al inicio de la etiqueta main, igual que en el hola mundo , empezamos con las mismas primeras cuatro líneas para reservar espacio para las variables locales. En este caso se reservan 32 bytes en el stack. La siguiente línea del main, mueve el valor 1, a la posición 28(%esp), esto ya que es un entero, y ocupara 4 bits(32-4 = 28). Si desearamos agregar alguna otra variable entera, e inicializarla sería simplemente agregar una línea como esta: movl $0, 24(%esp), restando cada vez 4 por entero.

Después se usa la instrucción jump para brincar desde main, a la etiqueta .L2, donde lo primero que hacemos es comparar si el valor guardado en la posición 28(%esp), que sería nuestra variable "i", es menor o igual a 10, si esto es así se manda el control del flujo a .L4.

Aqui se harán los contenidos del for, donde inicialmente movemos el valor de 28(%esp) a eax, y le aplicamos una operación AND, al este valor, y el valor 1. Esto es usado para saber si el número es par o impar, luego comparamos eax, consigo mismo, esto es para saber si es 0 o no, si no es 0, es decir no es par, se dará un brinco a .L3, donde se le sumará uno al valor guardado en la posición 28(%esp). Si es igual a 0, se continua en .L4, omitiendo la instrucción jne.

Las siguientes líneas son usadas para imprimir el número par. Primero, movemos el contenido de la etiqueta .LC0(el string "%d\n") hacia eax, después movemos el valor guardado en la posición 28(%esp) a edx. Movemos edx(ahora la variable "i"), a la posición 4(%esp), y después eax(el string) a la posición %esp. Esto podemos visualizarlo como posiciones en memoria conjuntas, ya que ponemos el string al inicio del stack, y justo después colocamos la variable que el string va a imprimir. Entonces llamamos printf para imprimir esos valores.

Ciclo While

En el ciclo while, se realiza prácticamente lo mismo, pero a diferencia del for, no agrega una etiqueta para realizar la suma al contador, y el brinco que se hace al comparar el número de repeticiones y el contador, se hace con jne, lo que significa que brinca cuando no sea igual a 10.

Al terminar, disminuyendo algunas lineas de código para el for, quite una etiqueta, y ajuste algunas lineas para que funcionara igualmente, y se redució un poco el código. Para terminar como el siguiente:


Comparaciones

De esta manera, el código del while y del for es prácticamente lo mismo, la única diferencia es la condición para brincar a la etiqueta que se repetirá(L3).
  • En el for, el brinco es jle, que significa, que se hará un brinco, si la condición anterior es menor o igual. En el código esto significa que si el valor del registro "28(%esp)" (que sería nuestra variable "i"), es menor o igual a 10, será mandado a la etiqueta L3.
  • En el while, el brinco es jn, que significa que se hará un brinco cuando la condición no sea igual. En el código esto significa que si el valor del registro "28(%esp)", no es igual a 10, brincará a la etiqueta L3. 
Referencias:

1 comentario: