miércoles, 26 de marzo de 2014

Introducción a Ensamblador (ASM)

Introducción a Ensamblador


Hola de nuevo. Hace ya dos semanas que no subo ninguna entrada, así que aquí va. Vamos a ver una breve introducción a Ensamblador, para quién lo quiera programar.
No es el lenguaje que recomiendo para empezar a programar, pero es un lenguaje muy importante.
Con el Cheat-Engine, con el botón "Memory View", podéis ver todas las instrucciones del proceso elegido. Las instrucciones de los programas, están en hexadecimal, que se traduce a Ensamblador. Con el Cheat-Engine podéis echarle un vistazo. Ahora nos centraremos en programar Ensamblador, y hacer programas con él.

Para empezar, un compilador. Yo uso Flat Assembler. Si lo queréis, lo tenéis en su web: flatassembler.net.

Ensamblador es un lenguaje sencillo, en el sentido de que hay unas 256 instrucciones diferentes (de las cuales la mayoría son parecidas entre sí). Además, también habrá que llamar a funciones de librerías, como la WinApi, por ejemplo, o hacer vuestras propias funciones. Por otra parte, ASM es un lenguaje rápido, por el simple hecho de que las instrucciones que escribáis, serán aproximadamente las mismas que haya en memoria. Es decir: el código que escribáis, será el código del proceso.

Una vez descargado el FASM (u otro), lo abrimos, y empezamos un nuevo programa. Para los nuevos en ASM, usaré esta entrada para explicar algunas isntrucciones en ASM, y otras cosas básicas.

Empecemos:

En ASM, no hay variables, como en otros lenguajes. Hay registros y la pila o stack. Para tratar con datos, usaremos los registros. Para almacenar datos, usaremos la pila. (Generalmente: En la pila se suele guardar punteros a datos además de datos en sí)
Los registros básicos son: AX, BX, CX y DX. Estos registros, además, se pueden descomponer en 2 partes: HIGH y LOW (AH y AL o BH y BL).
Veamos más claramente cómo son los registros (en hexadecimal):
(8 bits = 1 byte = '00')

RAX: 64bits  00 00 00 00 00 00 00 00
EAX: 32 bits 00 00 00 00
AX: 16 bits   00 00
AH: 8 bits     00 . .
AL: 8 bits      . . 00

Según el hardware, se suelen usar actualmente RAX o EAX. Esto no excluye de usar los demás.

La pila es donde se guarda información, llamadas a funciones, etc. Es una zona de memoria, donde "Lo último en escribirse, es lo primero en leerse", o lo que es lo mismo, "Lo último en entrar, es lo primero en salir". Simplemente, para que os hagáis una idea, es como una pila de libros. El último libro que apiles, será el primero en cogerse.
Igulamente, también podréis acceder al "centro" de la pila. Pero eso ya se verá más adelante.

Para acceder a la pila, necesitamos un puntero. Un puntero que apunte a ella. Para ello, tenemos otros 2 registros: EBP y ESP. Cada uno de ellos apunta a la pila.Ya veremos más adelante las diferencias entre los dos.

Además, hay otro pequeño pero muy importante "registro": las flags o banderas. Estas tienen 2 valores: 1 y 0. Cada una de ellas, se activa o desactiva (1 o 0) dependiendo del resultado de algunas operaciones. Por ejemplo, la flag zero, se activa si alguna operación da 0.

Ahora que vimos lo básico, empezaremos con algunas instrucciones:

MOV A,B: Esta instrucción, copia B sobre A. A será igual a B. Se puede poner: MOV EAX, 8. Así, el registro EAX será igual a 8. Suponiendo que EAX fuera un puntero a una zona de memoria, y quisieramos copiar 8 a esa dirección, haríamos: MOV [EAX],8. Los corchetes indican que tratamos ese valor como un puntero. También podríamos poner: MOV EAX,[EBX]. Esto copiaría el valor del puntero EBX a EAX. Múltiples posibilidades.

INC A: Incrementa A en 1. Si EAX fuera 10, y ejecutamos INC EAX, EAX pasaría a ser 11.
DEC A: Decrementa A. Igual que INC, pero decrementando.

ADD A,B: Añade B a A. A será igual a A+B.
DEC A,B: Resta B a A. A será igual a A-B.

AND A,B: Operación AND a nivel de bit. El resultado se guarda en A.
OR A,B: Operación OR.
XOR A,B: Operación XOR u OR-exclusiva.

CMP A.B: Esta es muy impotante. Resta B a A, pero no guarda el resultado. Simplemente, se activan las flags correspondientes. Se usa en conjunto con saltos condicionales.
TEST A,B: Como CMP, pero en evz de una resta, hace una operación AND a nivel de bit.

JMP A: Mueve el registro IP (guarda la dirección de la instrucción actual) a la dirección A. Es decir, salta a otra parte del código.

Saltos condicionales: Actuan como JMP, pero saltan o no según las flags activas. Por ejemplo, el salto JZ, "jump if zero" salta solo si está activa la flag zero. JNZ, sería lo contrario. Salta si la flag zero NO está activa. Generalmente se usa CMP antes de un salto condicional.

CALL A: Hace un salto como JMP, pero, además, guarda la dirección de la instrucción en la pila, entre otras. Se usa para llamar a subprogramas, a "funciones".
RET A: Pone el registro de instrucción en la instrucción marcada por el registro de pila EBP más A. Se puede no poner A, y dejarlo como "RET". En resumen, esto retorna a la dirección desde la que se llamó a la función con un CALL.

;comentario: Esto no es una instrucción, y no aparecerá en el ejecutable compilado, pero cuando se coloca ';', lo que le sigue es un comentario.

Además, hay instrucciones para trabajar con decimales (float). Pero eso ya no entra aquí. Para que podáis compilar, y hacer pruebas, os dejo un pequeño código, para que vayáis descifrando. Además, FASM trae en su carpeta, la carpeta "EXAMPLES", donde veréis códigos variados, todos ellos en ASM.


format PE GUI 4.0

section '.data' data readable writeable
     var db 'Cadena'   ; <nombre> db <cadena> : Pone en memoria la cadena
                       ; para acceder a ella, se usa el identificador,
                       ; en este caso "var"
include 'win32ax.inc'  ; WinApi. Include sirve para usar funciones de librerías
.code  ;Empieza el código

  start:   ; Esto es una etiqueta. Con esto se puede hacer JMP <etiqueta>.
    call icf_init  ;llamada a una función. (Más abajo está la etiqueta)
    mov eax,var  ;EAX tendrá la dirección de la cadena (var)
    call write   ;otra etiqueta
    invoke MessageBoxA,HWND_DESKTOP,var,"Titulo",0 ; Una ventana (MessageBox)
    invoke  Sleep,-1    ;WinApi. Deja el programa en pausa

.end start

  icf_init:
        invoke  AllocConsole
        ret
  malloc: ; EAX: Size
        invoke  VirtualAlloc,NULL,eax,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE
        ret

  free: ; EAX: Address
        invoke  VirtualFree,eax,edx,MEM_RELEASE
        ret

  write: ; EAX: String
        push ECX   ;Uso esta función para escribir en consola
        push EDX
        mov edx,eax
        xor ecx,ecx ;en ECX se guardará el tamaño de la string
        dec edx  ;para compensar el inc que sobra en el loop
        write_loop_1:
           inc edx
           inc ecx
           cmp byte[edx],0 ;hasta que el char de la string sea 0
        jnz write_loop_1
        dec ecx  ;se elimina el contador sobrante.
        invoke  WriteConsole,<invoke GetStdHandle,STD_OUTPUT_HANDLE>,eax,ecx,1,0
        pop EDX
        pop ECX
        ret     

Suerte, y hasta una próxima entrada!

PD: Las funciones las hice yo, y quizás tengan fallos. Tomadlas solo como ejemplos. Si algo no os funciona, podéis dejar un comentario, y veré que puede pasar.

2 comentarios:

  1. como puedo usar lo opcion call para llamar a un programa.exe

    ResponderEliminar
    Respuestas
    1. No puedes llamar directamente a un exe con un call. Call lo que hace es saltar a una etiqueta, o más internamente, a una dirección de memoria. Un exe ha de ser cargado primero a la memoria para poder ser utilizado.
      E incluso cargado, probablemente salte una excepción de intentar acceder a la memoria de otros procesos.

      Eliminar