ESTRUCTURA DE UN PROGRAMA EN ASSEMBLY
La estructura de un programa en assemby esta determinada por diferentes zonas en las cuales se declarán variables, se inicializan y posteriormente se utilizan por diferentes rutinas que realizan una función definida, las zonas principales en un programa en assembly son las siguientes:
.data: Segmento de inicialización de datos, como cadenas o valores numéricos
.bss: Segmento donde se indican las variables sin inicializar
.text: Segmento donde se realizará la ejecución propiamente dicha del programa, las instrucciones en este segmento están diferenciadas en diferentes subsegmentos dentro del segmento .text, el más importante que es donde se inicia la ejecución del programa es el subsegmento _start, este subsegmento puede compararse como la rutina main en un programa escrito en C/C++, por otro lado también se encuentra el segmento .globl _start, donde se ejecutaran funciones externas, aquí se definen invocaciones a librerías externas y demás dependencias del programa.
SYSTEM CALLS EN GNU/LINUX USANDO ASSEMBLY
En GNU/Linux se encuentran definidas una serie de funciones disponibles a programas escritos en C y Assembly, estas funciones permiten la ejecución de tareas que controlan el flujo de ejecución de un programa y/o permiten acceder a recursos físicos de la maquina en donde se ejecutan.
Dependiendo de la arquitectura de la maquina (32 o 64) la ubicación de las funciones disponibles es la siguiente:
32: /usr/include/asm/unistd.h o /usr/include/asm/unistd_32.h
64: /usr/include/asm/unistd_64.h
En este fichero se establece la función y el identificador de la función para ser utilizado posteriormente por el programa, estas funciones pueden ser exit(), write(), read(), etc.
El listado completo, con parámetros incluidos pueden verse aquí:
http://asm.sourceforge.net/syscall.html
Otro punto importante es que todas las funciones del sistema son invocadas por el proceso del programa usando la interrupción de software INT 0x80 esta interrupción permite la ejecución efectiva de la función invocada.
PASANDO ARGUMENTOS A SYSTEM CALLS
Cuando se utiliza una función determinada muy probablemente está tenga que recibir algún tipo de parámetro para que su ejecución pueda llevarse a cabo, en assembly hacemos uso de los registers indicados anteriormente en los tópicos relacionados con el modelo de la memoria virtual, sin embargo desde la perspectiva de un programa en assembly los registers asumen los siguientes significados:
EAX/RAX = Numero de SysCall
EBX/RBX = Primer Parámetro de la función.
ECX/RCX = Segundo Parámetro de la función.
EDX/RDX = Tercer Parámetro de la función.
ESI/RSI = Cuarto Parámetro de la función.
EDI/RDI = Quinto Parámetro de la función.
EJECUCIÓN DE PROGRAMAS:
Para crear un programa en assembly, basta con usar cualquier editor de texto como vi, vim, nano, gedit, etc. y posteriormente definir los segmentos de ejecución obligatorios, un ejemplo básico es este:
.text.globl _start_start:movl $1, %eax
movl $0, %ebx int $0x80 |
En este programa solamente establecemos al register la función del sistema con identificador 1 (que corresponde a la función exit), luego establecemos el parámetro 0 (cero) a dicha función y finalmente es invocada por medio de la interrupción de software para ejecutar dicha función, en este caso, el programa solamente ejecutará la función exit, que terminará inmediatamente la ejecución del programa.
Ahora bien, debemos compilar el programa, para esto podemos utilizar el compilador GNU/AS el cual nos permite compilar este fragmento de código y generar un fichero objeto que puede ser posteriormente enlazado a un ejecutable valido.
Compilación:
as -o JustExit.o JustExit.s
Linker para enlazar el fichero objeto en ejecutable:
ld -o JustExit JustExit.o
Finalmente podemos ejecutar este sencillo programa como cualquier ejecutable en gnu/linux.
A continuación crearemos un programa un poco mas complejo que inicialice una variable y posteriormente la utilice usando la función del sistema write:
#primer programa en Assembly
.data VariableAlmacenada: .ascii «Variable de Texto Inicializada» .text .globl _start _start: #Cargar todos los argumentos para usar la funcion write() #La signatura de la funcion es write(FileDescriptor, Buffer, StringLength) #Cargamos la funcion write() movl $4, %eax #Establecemos el file descriptor 1 correspondiente a STDOUT movl $1, %ebx #Puntero a memoria donde se almacena la variable inicializada. movl $VariableAlmacenada, %ecx #Numero de caracteres de la variable de texto a escribir. movl $30, %edx int $0x80 #Salimos del programa movl $1, %eax movl $0, %ebx int $0x80 |
Como se puede apreciar se utilizan los parámetros adecuados en el orden en el que se deben de especificar en la función, posteriormente es invocada por la interrupción de software y finalmente invocamos la función exit para salir del programa.
El proceso de compilación y enlace es exactamente igual que el descrito anteriormente.
TIPOS DE DATOS EN ASSEMBLY
Los tipos de datos de datos en assembly son empleados en el segmento .data del programa, como en la ejecución del programa anterior se ha definido una variable de tipo texto con la asignación .ascii, es importante anotar que las variables definidas en el segmento .data reservan espacio en tiempo de compilación, los tipos de datos aceptados en assembly son:
.data Variables (Espacio de memoria creado en tiempo de compilación)
.byte = 1 Byte.
.ascii = Cadena de Texto.
.asciiz = Cadena de texto terminada en null
.int = Número entero de 32 bits
.short = Número entero de 16 bits
.float = Número de punto flotante de simple precisión
.double = Número de punto flotante de doble precisión
.bss Variables (Espacio de memoria creado en tiempo de ejecución)
.comm = declara una área de memoria común
.lcomm = declara una área de memoria local común.
Programa Simple con Variables.
.data
HelloWorld: .ascii «Hello» ByteLocation: .byte 10 Int32bytes: .int 6 Int16bytes: .short 3 Float: .float 10.10 IntAsArrays: .int 10,20,30,40,50 .bss .comm BufferLargo, 10000 .text .globl _start _start: nop movl $1, %eax movl $0, %ebx int $0x80 |
En el listado anterior se definen variables de diferentes tipos, solamente aquellas definidas en el segmento .data se almacenarán en tiempo de compilación, mientras que las variables definidas en el segmento .bss se almacenarán en tiempo de ejecución.
Para compilar el programa anterior:
as –gstabs -o /home/adastra/Desktop/VariableDemo.o VariablesSimple.s
Para enlazar el ejecutable:
ld -o VariableDemo VariableDemo.o
Posteriormente podemos utilizar el depurador gdb para verificar las direcciones de memoria del programa:
gdb VariableDemo
En el programa establecemos un punto de ruptura en la linea 29, justo en el inicio del programa ( movl $1, %eax )
y ejecutamos con run
Una vez se ha interrumpido la ejecución del programa en el punto definido, podemos obtener información de las variables almacenadas en el programa con el siguiente comando:
(gdb) info variables
All defined variables:
Non-debugging symbols:
0x00000000006000c0 HelloWorld
0x00000000006000c5 ByteLocation
0x00000000006000c6 Int32bytes
0x00000000006000ca Int16bytes
0x00000000006000cc Float
0x00000000006000d0 IntAsArrays
0x00000000006000f0 BufferLargo
Tenemos la dirección de memoria y la etiqueta correspondiente a la variable que hemos definido en el programa, con esta información es posible hacer uso del comando x, para examinar la memoria, en concreto, cada una de estas direcciones de memoria.
Examinando HelloWorld:
(gdb) x/6cb 0x00000000006000c0
0x6000c0 <HelloWorld>: 72 ‘H’ 101 ‘e’ 108 ‘l’ 108 ‘l’ 111 ‘o’ 10 ‘\n’
Donde especificamos, numero de objetos en la dirección de memoria, formato carácter (c) y en bytes (b)
Examinando ByteLocation:
(gdb) x/1db 0x00000000006000c5
0x6000c5 <ByteLocation>: 10
Donde especificamos, numero de objetos en la dirección de memoria (1) formato decimal (d) y en bytes (b)
Examinando Int32bytes:
(gdb) x/1dw 0x00000000006000c6
0x6000c6 <Int32bytes>: 6
Donde especificamos, numero de objetos en la dirección de memoria (1) formato decimal (d) y en words(w)
Examinando Int16bytes:
(gdb) x/1dh 0x00000000006000ca
0x6000ca <Int16bytes>: 3
Donde especificamos, numero de objetos en la dirección de memoria (1) formato decimal (d) y en halfword(h)
Examinando Float:
(gdb) x/1fw 0x00000000006000cc
0x6000cc <Float>: 10.1000004
Donde especificamos, número de objetos en la dirección de memoria (1) formato float (f) y en words(w)
Examinando IntAsArrays:
(gdb) x/5dw 0x00000000006000d0
0x6000d0 <IntAsArrays>: 10 20 30 40
0x6000e0 <IntAsArrays+16>: 50
Donde especificamos, número de objetos en la dirección de memoria (5) formato decimal (d) y en words(w)
Examinando BufferLargo:
(gdb) x/10db 0x00000000006000f0
0x6000f0 <BufferLargo>: 0 0 0 0 0 0 0 0
0x6000f8 <BufferLargo+8>: 0 0
Donde especificamos, numero de objetos en la dirección de memoria (10) formato decimal (d) y en bytes(b)