Black Hole organizacion
CostaRica  
  Bienvenida
  Hackers
  Foro ingresa
  Noticias
  Contacto
  Imagenes
  Programas
  Visitantes
  Chat
  Libros
  => hacker
  => Virus
  => Cracker
  => Lecciones hackin
  => Programacion
  => Diseños web
  => Guia de hackin
  => Privasidad
  => Guia → 1
  => Guia → 2
  => Guia → 3
  => Guia → 4
  => Guia → 5
  => Guia → 6
  => Guia → 7
  => Guia → 8
  => Guia → 9
  => Guia → 10
  => Guia → 11
  => Como intrudusirse aun sistema
  => shellcodes_linux-1
  => shellcodes_linux -2
  => UN-scodes.
  => Guia version Deluxe
  => Ser Hacker dentro de Términos Legales
  => Como hackear una paginaweb
  => Comoprogramar un Virus
  => Como Crear un Virus
  => Cuantos tipos de Virus existen
  => Programaciones de un virus
  => Estructura de computadores
  => Fundamentos de SSOO
  => Sistemas de numeración
  => Ensamblador I: Conceptos básicos
  => Ensamblador II
  => Utilidades para la programación
  => Infección bajo Windows
  => Infección bajo Linux
  => Técnicas avanzadas
  => Apéndices
  => CONOCIENDO LA MAQUINA
  => DIRECCIONAMIENTO DE MEMORIA EN EL 8086
  => CHIPS DE APOYO (Ampliación de la lección 1)
  => LA PILA DEL 8086
  => CODIFICACIÓN DE LAS INSTRUCCIONES EN EL 8086
  => Manual HTML
  => Ataques basados en Desbordamiento de Buffer (Buffer Overflow)
  => Privasidad
  => Escaneo
  => anti Escaneo y Escaneo
  => Malianom
  Triang
  Tersirve esta paguina?
  -
  juegos
  Vagos
  mapa
  Mapa del sitio
  546
Ensamblador I: Conceptos básicos

Atras


Este, es el capítulo más largo probablemente del curso de programación de virus. Programar en ensamblador no es fácil, pero cuando se le coge el tranquillo es extremadamente gratificante; estás hablando diréctamente con la máquina, y aquello que le pides, ella lo hace. El control, es absoluto... nada mejor pues, a la hora de programar virus o cualquier tipo de aplicación crítica.

 

 

4.1.- Algunos conceptos previos

 

4.1.1.- Procesadores CISC/RISC

Según el tipo de juego de instrucciones que utilicen, podemos clasificar los microprocesadores en dos tipos distintos:

- RISC: Aquellos que utilizan un juego reducido de instrucciones. Un ejemplo de ello sería el ensamblador del Motorola 88110, que carece por ejemplo de más saltos condicionales que "salta si este bit es 1" y "salta si este bit es 0". Por un lado se obtiene la ventaja de que en cierto modo uno se fabrica las cosas desde un nivel más profundo, pero a veces llega a hacer la programación excesivamente compleja.

- CISC: Son los que usan un juego de instrucciones ampliado, incluyéndose en esta clasificación el lenguaje ensamblador de los 80x86 (el PC común de Intel, AMD, Cyrix...). Para el ejemplo usado antes, en el asm de Intel tenemos 16 tipos distintos de salto que abstraen el contenido del registro de flags y permiten comparaciones como mayor que, mayor o igual que, igual, menor, etc.

 

 

4.1.2.- Little Endian vs Big Endian

 

Existen dos formas de almacenar datos en memoria, llamados Little Endian y Big Endian. En el caso de los Big Endian, se almacenan tal cual; es decir, si yo guardo en una posición de memoria el valor 12345678h, el aspecto byte a byte de esa zona de memoria será ??,12h,34h,56h,78h,??

 

El caso de un Little Endian - y el PC de sobremesa es Little Endian, por cierto -, es distinto. Byte a byte, los valores son almacenados "al revés", el menos significativo primero y el más significativo despuñes. Normalmente no nos va a afectar puesto que las instrucciones hacen por si mismas la conversión, pero sí hay que tenerlo en cuenta si por ejemplo queremos acceder a un byte en particular de un valor que hemos guardado como un valor de 32 bits (como sería el caso de 12345678h). En ese caso, en memoria byte a byte quedaría ordenado como ??,78h,56h,34h,12h,??.

 

 

 

4.2.- Juego de registros de los 80x86

En las arquitecturas tipo 80x86 (esto es, tanto Intel como AMD o Cyrix, que comparten la mayoría de sus características en cuanto a registros e instrucciones en ensamblador), tenemos una serie de registros comunes; con algunos de ellos podremos realizar operaciones aritméticas, movimientos a y desde memoria, etc etc. Estos registros son:

EAX: Normalmente se le llama "acumulador" puesto que es en él donde se sitúan los resultados de operaciones que luego veremos como DIV y MUL. Su tamaño, como todos los que vamos a ver, es de 32 bits. Puede dividirse en dos sub-registros de 16 bits, uno de los cuales (el menos significativo, o sea, el de la derecha) se puede acceder directamente como AX. A su vez, AX podemos dividirlo en dos sub-sub-registros de 8 bits, AH y AL:

EBX: Aquí sucede lo mismo que con EAX; su división incluye subregistros BX (16 bits), BH y BL (8 bits).

ECX: Aunque este registro es como los anteriores (con divisiones CX, CH y CL), tiene una función especial que es la de servir de contador en bucles y operaciones con cadenas.

EDX: Podemos dividir este cuarto registro "genérico" en DX, DH y DL; además, tiene la característica de que es aquí donde se va a guardar parte de los resultados de algunas operaciones de multiplicación y división (junto con EAX). Se le llama "puntero de E/S", dada su implicación también en acceso directo a puertos.

ESI: Se trata de un registro de 32 bits algo más específico, ya que aunque tiene el sub-registro SI (16 bits) refiriéndose a sus bits 0-15, este a su vez no se divide como lo hacían los anteriores en sub-sub-registros de 8 bits. Además, ESI va a servir para algunas instrucciones bastante útiles que veremos, como LODSX, MOVSX y SCASX (operando origen siempre)

EDI: Aplicamos lo mismo que a ESI; tenemos un "DI" que son los últimos 16 bits de EDI, y una función complementaria a ESI en estos MOVSX, etc (el registro ESI será origen, y el EDI, el operando destino).

EBP: Aunque no tiene ninguna función tan específica como ESI y EDI, también tiene su particularidad; la posibilidad de que se referencien sus bits 0-15 mediante el sub-registro BP.

EIP: Este es el PC (Program Counter) o Contador de Programa. Esto es, que en este registro de 32 bits (que no puede ser accedido por métodos normales) se almacena la dirección de la próxima instrucción que va a ejecutar el procesador. Existe también una subdivisión como "IP" con sus 16 bits menos significativos como con EBP, EDI, etc, pero no lo vamos a tener en cuenta; en un sistema como Linux o Windows se va a usar la combinación CS:EIP para determinar lo que hay que ejecutar siempre, y sólo en sistemas antiguos como Ms-Dos se utiliza el CS:IP para ello.

ESP: Se trata del registro de pila, indicando la dirección a la que esta apunta (que sí, que lo de la pila se explica más tarde).

 

Además de estos registros, tenemos otros llamados de segmento, cuyo tamaño es de 16 bits, y que se anteponen a los anteriores para formar una dirección virtual completa. Recordemos en cualquier caso que estamos hablando de direcciones virtuales, así que el procesador cuando interpreta un segmento no está operando con nada; simplemente se hace que direcciones de la memoria física se correspondan con combinaciones de un segmento como puede ser CS y un registro de dirección como puede ser EIP. La función de estos registros de segmento es la de separar por ejemplo datos de código, o zonas de acceso restringido. Así, los 2 últimos bits en un registro de segmento indican normalmente el tipo de "ring" en el que el procesador está corriendo (ring3 en windows es usuario, con lo que los dos últimos bits de un segmento reservado a un usuario serían "11"... ring0 es supervisor, con lo que los dos últimos bits de un segmento con privilegio de supervisor serían "00")

- CS es el registro de segmento de ejecución, y por tanto CS:EIP es la dirección completa que se está ejecutando (sencillamente anteponemos el CS indicando que nos estamos refiriendo a la dirección EIP en el segmento CS).

- SS es el registro de segmento de pila, por lo que tal y como sucedía con CS:EIP, la pila estará siendo apuntada por SS:ESP.

- DS normalmente es el registro de datos. Poniendo ya un ejemplo de acceso con la instrucción ADD (sumar), una forma de utilizarla sería "add eax,ds:[ebx]", que añadiría al registro EAX el contenido de la dirección de memoria en el segmento DS y la dirección EBX.

- ES, al igual que FS y GS, son segmentos que apuntan a distintos segmentos de datos, siendo los dos últimos poco utilizados.

 

Tenemos algunos otros registros, como el de flags que se detallará en un apartado específico (si se recuerda del capítulo 1 la descripción genérica, contiene varios indicadores que serán muy útiles).

Finalmente, están (aunque probablemente no los usaremos), los registros del coprocesador (8 registros de 80 bits que contienen números representados en coma flotante, llamados R0..R7), el GDTR (global descriptor table) e IDTR (interrupt descriptor table), los registros de control (CR0, CR2, CR3 y CR4) y algunos más, aunque como digo es difícil que lleguemos a usarlos.

 

 

 

4.3.- La órden MOV y el acceso a memoria

 

4.3.1.- Usos de MOV

Vamos con algo práctico; la primera instrucción en ensamblador que vamos a ver en detalle. Además, MOV es quizá la instrucción más importante en este lenguaje sicontamos la cantidad de veces que aparece.

Su función, es la transferencia de información. Esta transferencia puede darse de un registro a otro registro, o entre un registro y la memoria (nunca entre memoria-memoria), y también con valores inmediatos teniendo como destino memoria o un registro. Para ello, tendrá dos operandos; el primero es el de destino, y el segundo el de origen. Así, por ejemplo:

MOV EAX, EBX

Esta operación copiará los 32 bits del registro EBX en el registro EAX (ojo, lo que hay en EBX se mantiene igual, sólo es el operando de destino el que cambia). Ya formalmente, los modos de utilizar esta operación son:

- MOV reg1, reg2: Como en el ejemplo, MOV EAX, EBX, copiar el contenido de reg2 en reg1.

- MOV reg, imm: En esta ocasión se copia un valor inmediato en reg. Un ejemplo sería MOV ECX, 12456789h. Asigna diréctamente un valor al registro.

- MOV reg, mem: Aquí, se transfiere el contenido de una posición de memoria (encerrada entre corchetes) al registro indicado. Lo que está entre los corchetes puede ser una referencia directa a una posición de memoria como MOV EDX, [DDDDDDDDh] o un acceso a una posición indicada por un registro como MOV ESI, [EAX] (cuando la instrucción sea procesada, se sustituirá internamente "EAX" por su contenido, accediendo a la dirección que indica).

También hay una variante en la que se usa un registro base y un desplazamiento, esto es, que dentro de los corchetes se señala con un registro la dirección, y se le suma o resta una cantidad. Así, en MOV ECX,[EBX+55] estamos copiando a ECX el contenido de la dirección de memoria suma del registro y el número indicado.

 

Finalmente, se pueden hacer combinaciones con más de un registro al acceder en memoria si uno de ellos es EBP, por ejemplo MOV EAX,[EBP+ESI+10]

 

- MOV mem, reg: Igual que la anterior, pero al revés. Vamos, que lo que cogemos es el registro y lo copiamos a la memoria, con las reglas indicadas para el caso en que es al contrario. Un ejemplo sería MOV [24347277h], EDI

 

- MOV mem, imm: Exáctamente igual que en MOV reg, imm sólo que el valor inmediato se copia a una posición de memoria, como por ejemplo MOV [EBP],1234h

 

 

4.3.2.- Bytes, words y dwords

La instrucción MOV no se acaba aquí; a veces, vamos a tener problemas porque hay que ser más específico. Por ejemplo, la instrucción que puse como último ejemplo, MOV [EBP],1234h, nos daría un fallo al compilar. El problema es que no hemos indicado el tamaño del operando inmediato; es decir, 1234h es un número que ocupa 16 bits (recordemos que por cada cifra hexadecimal son 4 bits). Entonces, ¿escribimos los 16 bits que corresponden a [EBP], o escribimos 32 bits que sean 00001234h?.

Para solucionar este problema al programar cuando haya una instrucción dudosa como esta (y también se aplicará a otras como ADD, SUB, etc, cuando se haga referencia a una posición de memoria y un valor inmediato), lo que haremos será indicar el tamaño con unas palabras específicas.

En el ensamblador TASM (el más utilizado para Win32/Dos), será con la cadena byte ptr en caso de ser de 8 bits, word ptr con 16 bits y dword ptr con 32. Por lo tanto, para escribir 1234h en [EBP] escribiremos MOV word ptr [EBP],1234h. Sin embargo, si quisiéramos escribir 32 bits (o sea, 00001234h), usaríamos MOV dword ptr [EBP],1234h.

Usando el NASM para linux, olvidamos el "ptr", y los ejemplos anteriores se convertirán en MOV word [EBP],1234h y MOV dword [EBP],1234h.

Recordemos, una vez más, que un dword son 32 bits (el tamaño de un registro), un word 16 bits y un byte, 8 bits.

 

 

4.3.3.- Referencia a segmentos

Cuando estamos accediendo a una posición de memoria (y no ya sólo en el ámbito del MOV), estamos usando también un registro de segmento. Normalmente el segmento DS va implícito (de hecho, si en un programa de ensamblador escribimos MOV DS:[EAX],EBX, al compilar obviará el DS: para ahorrar espacio puesto que es por defecto). No obstante, podemos indicar nosotros mismos a qué segmento queremos acceder siempre que hagamos una lectura/escritura en memoria, anteponiendo el nombre del registro de segmento con un signo de dos puntos al inicio de los corchetes.

 

 

4.3.4.- Operandos de distinto tamaño

Vale, tengo un valor en AL que quiero mover a EDX. ¿Puedo hacer un MOV EDX,AL?. Definitivamente no, porque los tamaños de operando son diferentes.

Para solucionar este problema, surgen estas variantes de MOV:

- MOVZX (MOV with Zero Extend): Realiza la función del MOV, añadiendo ceros al operando de destino. Esto es, que si hacemos un MOV EDX, AL y AL vale 80h, EDX valdrá 00000080h, dado que el resto se ha rellenado con ceros.

- MOVSX (MOV with Sign Extend): Esta forma lo que hace es, en lugar de 0s, poner 0s o 1s dependiendo del bit más significativo del operando de mayor tamaño. Es decir, si en este MOV EDX, AL se da que el bit más significativo de AL es 1 (por ejemplo, AL = 10000000b = 80h), se rellenará con 1s (en este caso, EDX valdría FFFFFF80h). Si el bit más significativo es 1 (por ejemplo, AL = 01000000b = 40h), se rellenará con 0s ( EDX será pues 00000040h).

 

 

4.3.5.- MOVs condicionales

Una nueva característica presente a partir de algunos modelos de Pentium Pro y en siguientes procesadores de Intel, y en AMD a partir de K7 y posiblemente K6-3, son los MOVs condicionales; esto es, que se realizan si se cumple una determinada condición. La instrucción es CMOVcc, donde "cc" es una condición como lo es en los saltos condicionales (ver más adelante), p.ej CMOVZ EAX, EBX.

No obstante, de momento no recomendaría su implementación; aunque terriblemente útil, esta instrucción no es standard hasta en procesadores avanzados, y podría dar problemas de compatibilidad. Para saber si el procesador tiene disponible esta operación, podemos ejecutar la instrucción CPUID, la cual da al programador datos importantes acerca del procesador que está corriendo el programa, entre otras cosas si los MOVs condicionales son utilizables.

 

 

 

4.4.- Codificación de una instrucción

Ahora que ya sabemos utilizar nuestra primera instrucción en lenguaje ensamblador puede surgir una duda: ¿cómo entiende esta instrucción el procesador?. Es decir, evidentemente nosotros en la memoria no escribimos las palabras "MOV EAX, EBX", sin embargo esa instrucción existe. ¿Cómo se realiza pues el paso entre la instrucción escrita y el formato que la computadora sea capaz de entender?.

En un programa, el código es indistinguible de los datos; ambos son ristras de bits si no hay nadie allí para interpretarlos; el programa más complejo no tendría sentido sin un procesador para ejecutarlo, no sería más que una colección de unos y ceros sin sentido. Así, se establece una convención para que determinadas cadenas de bits signifiquen cosas en concreto.

Por ejemplo, nuestra instrucción "MOV EAX, EBX" se codifica así:

 

08Bh, 0C3h

Supongamos que EIP apunta justo al lugar donde se encuentra el 08Bh. Entonces, el procesador va a leer ese byte (recordemos que cada cifra hexadecimal equivale a 4 bits, por tanto dos cifras hexadecimales son 8 bits, o sea, un byte). Dentro del micro se interpreta que 08Bh es una instrucción MOV r32,r/m32. Es decir, que dependiendo de los bytes siguientes se va a determinar a qué registro se va a mover información, y si va a ser desde otro registro o desde memoria.

El byte siguiente, 0C3h, indica que este movimiento se va a producir desde el registro EBX al EAX. Si la instrucción fuera "MOV EAX, ECX", la codificación sería así:

 

08Bh, 0C1h

 

Parece que ya distinguimos una lógica en la codificación que se hace para la instrucción "MOV EAX,algo". Al cambiar EBX por ECX, sólo ha variado la segunda cifra del segundo byte, cambiando un 3 por un 1. Podemos suponer entonces que se está haciendo corresponder al 3 con EBX, y al 1 con ECX. Si hacemos más pruebas, "MOV EAX,EDX" se codifica como 08Bh, 0C2h. "MOV EAX,ESI" es 08BH, 0C6h y "MOV EAX,EAX" (lo cual por cierto no tiene mucho sentido), es 08Bh, 0C0h.

Vemos pues que el procesador sigue su propia lógica al codificar instrucciones; no es necesario que la entendamos ni mucho menos que recordemos su funcionamiento. Sencillamente merece la pena comprender cómo entiende aquello que escribimos. Para nosotros es más fácil escribir "MOV EAX, EBX" puesto que se acerca más a nuestro lenguaje; MOV recuerda a "movimiento", al igual que "ADD" a añadir o "SUB" a restar. Al computador "MOV" no le recuerda nada, así que para él resulta mucho mejor interpretar secuencias de números; la equivalencia entre nuestro "MOV EAX,EBX" y su "08Bh, 0C3h" es exacta, la traducción es perfecta y procesador y humano quedan ambos contentos.

El sentido pues de este apartado es entender cómo va a funcionar cualquier programa que escribamos en lenguaje ensamblador; cuando escribamos nuestros programas, utilizaremos un compilador: una especie de traductor entre la notación en ensamblador que más se parece a nuestro lenguaje con instrucciones como "MOV EAX, EBX" y la notación en bits, la que la máquina entiende diréctamente. En realidad, podríamos considerar que ambos son el mismo lenguaje; la única diferencia es la forma de representarlo.

Por supuesto, quien quiera meterse más a fondo en esto puede disfrutar construyendo instrucciones por sí mismo jugando con estos bytes; es algo interesante de hacer en virus cuando tenemos engines polimórficos, por ejemplo. Hay, de hecho, listas muy completas acerca de cómo interpretar la codificación en bits que entiende la máquina, que pueden ser consultadas sin problemas (en la propia web de Intel vienen toda una serie de tablas indicando cómo se hace esto con todas y cada una de las instrucciones que entienden sus procesadores).

 

 

 

4.5.- Las operaciones lógicas

Las operaciones con registros se dividen en dos tipos: aritméticas y lógicas. A las aritméticas estamos muy acostumbradas, y son la suma, la resta, multiplicación, división... las lógicas operan a nivel de bit, lo que las distingue de las aritméticas (si "a nivel de bit" resulta algo oscuro, da igual, seguid leyendo).

Aunque hayamos mencionado cuáles son estas operaciones lógicas en el primer capítulo, volvemos a repasarlas una a una y con detalle:

 

4.5.1.- AND

El AND lógico realiza bit a bit una operación consistente en que el bit resultado es 1 sólo si los dos bits con los que se opera son 1. Equivale a decir que el resultado "es verdad" si lo son los dos operandos.

Actuará así con cada uno de los bits de los dos operandos, almacenando en el de destino el resultado. Por ejemplo:

    10001010 AND 11101010      --------      10001010

La forma de utilizar el AND es muy similar al MOV que ya hemos visto; algunas formas de utilizarlo podrían ser AND EAX,EBX, o AND EAX,[1234h], o AND ECX,[EDX], etc. El resultado se almacena en el operando de destino, esto es, EAX en los dos primeros casos y ECX en el tercero.

 

 

4.5.2.- OR

El OR lógico también opera bit a bit, poniendo el resultado a 1 si al menos uno de los dos bits con los que operamos están a 1, siendo lo mismo que decir que el resultado es "cierto" si lo es al menos uno de sus constituyentes.

Almacenará, como el AND, el resultado en el operando de destino:

    10001010 OR  11101010      --------     11101010

La forma de utilizarlo es el común a todas las operaciones lógicas, como el AND mencionado anteriormente.

 

 

4.5.3.- XOR

La operación XOR, operando bit a bit, da como resultado un 1 si uno y sólo uno de los dos bits con los que se opera valen 1, es por ello que se llama OR exclusivo o eXclusive OR:

    10001010 XOR 11101010      --------     01100000

 

 

4.5.4.- NOT

Esta operación sólo tiene un operando, puesto que lo que hace es invertir los bits de este operando que evidentemente será de destino:

NOT 11101010      --------     00010101

 

 

 

4.6.- Operaciones aritméticas

En los procesadores 80x86, tenemos una buena gama de operaciones aritméticas para cubrir nuestras necesidades. Estas son, básicamente:

 

4.6.1.- ADD

ADD significa añadir. Tendremos con esta instrucción las posibilidades típicas de operación; sobre memoria, sobre registros, y con valores inmediatos (recordando que no podemos operar con dos posiciones de memoria y que el destino no puede ser un valor inmediato). Así, un ejemplo sería:

 

ADD EAX, 1412h

Algo tan sencillo como esto añade 1412h hexadecimal a lo que ya hubiera en EAX, conservando el resultado final en EAX. Por supuesto podemos usar valores decimales (si quitamos la h a 1412h, sumará 1412h decimal... creo que no lo mencioné, pero esto vale siempre, tanto para MOV como para cualquier otra operación lógica o aritmética). Otros ejemplos podrían ser ADD ECX, EDI (sumar ECX y EDI y almacenar el resultado en ECX), ADD dword ptr [EDX], ESI (coger lo que haya en la dirección de memoria cuyo valor indique EDX, sumarle el valor del registro ESI y guardar el resultado en esa dirección de memoria), etc.

 

 

4.6.2.- SUB

Esta es la operación de resta; las reglas para utilizarla, las mismas que las del ADD. Tan sólo cabría destacar el hecho de que si estamos restando un número mayor a uno menor, además de una modificación en los FLAGS para indicar que nos hemos pasado, lo que sucederá es que al llegar a 0000 en el resultado el siguiente número será FFFF. Es decir, que al pasarnos por abajo del todo el resultado comienza por arriba del todo.

Supongamos que queremos restar 1 - 2. El resultado no es -1, sino el máximo número representable por la cantidad de bits que tuviéramos. Vamos, que si son 8 bits (que representan un valor entre 0 y 255), el resultado de 1 - 2 será 255. Para los curiosos, este 255 en complemento a 2 equivale al -1, por lo que si operamos en este complemento a 2 la operación de resta tiene completo sentido para los números negativos.

Lo mismo sirve para el ADD cuando sumamos dos números y el resultado no es representable con el número de bits que tenemos. Si hicieramos 255 + 1 y el máximo representable fuera 255 (o FFh en hexadecimal, usando 8 bits), el resultado de 255 + 1 sería 0.

Como decía, las posibilidades para usar el SUB son como las del ADD, con lo que también es válido esto:

 

SUB EAX, 1412h

Los ejemplos mencionados con el ADD también valen: SUB dword ptr [EDX], ESI va a restar al contenido de la dirección de memoria apuntada por EDX el valor almacenado en ESI, y el resultado se guardará en esta dirección [EDX]. SUB ECX, EDI restará al valor de ECX el de EDI, guardando el resultado en el registro ECX.

 

 

4.6.3.- MUL

Pasamos a la multiplicación; aquí el tratamiento es un tanto distinto al que se hacía con la suma y la resta. Sólo vamos a indicar un parámetro que va a ser un registro o una dirección de memoria, y según su tamaño se multiplicará por el contenido de AL (8 bits), AX (16) o EAX (32). El resultado se guardará entonces en AX si se multiplicó por AL, en DX:AX si se multiplicó por AX, y en EDX:EAX si se multiplicó por EAX. Como vemos se utiliza para guardar el resultado el doble de bits que lo que ocupan los operandos; así no se pierde información si sale un número muy grande.

Veamos un ejemplo por cada tamaño:

MUL CL: Coge el valor de CL, lo multiplica por AL, y guarda el resultado en AX.

MUL word ptr [EDX]: Obtiene los 16 bits presentes en la dirección de memoria EDX (ojo, que el tamaño de lo que se escoge lo indica el "word ptr", EDX sólo indica una dirección con lo que aunque sean 32 bits esto no influye, el tamaño, repito, es determinado por el "word ptr"). Una vez coge esos 16 bits los multiplica por AX, y el resultado se va a guardar en DX:AX. Esto significa, que los 16 bits más significativos los guarda en DX y los 16 menos significativos en AX. Si el resultado de la multiplicación fuera 12345678h, el registro DX contendría 1234h, y el registro AX, 5678h.

MUL ESI: Coge el contenido del registro ESI, y lo multiplica por EAX. El resultado es almacenado en EDX:EAX del mismo modo en que antes se hacía con DX:AX, sólo que esta vez tenemos 64 bits para guardarlo. La parte de más peso, más significativa, se guardará en EDX, mientras que la de menor peso será puesta en EAX. Si el resultado de ESI x EAX fuera 1234567887654321h, EAX contendría 87654321h y EDX 12345678h.

 

 

4.6.4.- DIV

Por suerte, aunque a quien se le ocurrió esto de los nombres de las instrucciones fuera anglosajón, siguen pareciéndose bastante al castellano; la instrucción DIV es la que se dedica a la división entre números.

El formato de esta instrucción es muy similar al MUL, y va a tener también tres posibilidades, con 8, 16 y 32 bits. En ellas, AX, AX:DX o EAX:EDX van a dividirse por el operando indicado en la instrucción, y cociente y resto van a almacenarse en AL y AH, AX y DX o EAX y EDX, respectivamente:

DIV CL: Se divide el valor presente en AX por CL. El cociente de la división se guardará en AL, y el resto en AH. Si teníamos CL = 10 y AL = 6, al finalizar la ejecución de esta instrucción tendremos que CL no ha variado y que AH = 4 mientras que AL = 1.

DIV BX: Se divide el valor de DX:AX por el de BX. El cociente que resulte de esto se guardará en AX, mientras que el resto irá en DX. El dividendo (DX:AX) está formado de la misma manera en que lo estaba el un MUL el resultado de una operación: la parte más "grande", los bits más significativos, irán en DX mientras que los menos significativos irán en AX.

DIV dword ptr [EDI]: El valor contenido en la combinación EDX:EAX (64 bits) se dividirá por los 32 bits que contiene la dirección de memoria EDI; el cociente de la división se va a guardar en EAX, y el resto en EDX.

 

 

4.6.5.- INC y DEC

Tan sencillo como INCrementar y DECrementar. Estas dos instrucciones sólo tienen un operando que hace al tiempo de origen y destino, y lo que hacen con él es "sumar uno" o "restar uno":

INC AX: Coge el contenido de AX, le suma 1 y almacena el resultado en AX

DEC dword ptr [EDX]: Obtiene el valor de la posición de memoria a la que apunta EDX, le resta 1 y almacena allí el resultado.

INC y DEC, como veremos cuando lleguemos a los saltos condicionales, se suelen utilizar bastante para hacer contadores y bucles; podemos ir decrementando el valor de un registro y comprobar cuando llega a cero, para repetir tantas veces como indique ese contador un trozo de código, una operación en particular.

 

 

 

4.7.- Registro de estado (FLAGS) e instrucciones de comparación

 

4.7.1.- Flags

Como ya vimos, hay un registro bastante especial que es el de flags; la traducción literal de esta palabra es "bandera", y lo que significa realmente es que no se toma el valor de este registro como una cantidad en sí misma, sino que cada uno de sus bits significa algo en particular según su valor sea 0 o 1. El registro EFLAGS, de 32 bits, tiene como bits más importantes los 16 menos significativos (EFLAGS viene de Extended Flags, se añadieron 16 bits para indicar algunas otras cosas).

La forma de acceder a estos registros será de forma implícita cuando hagamos saltos condicionales (por ejemplo, hemos hecho una comparación entre dos términos y saltamos si son iguales; la instrucción JE, Jump if Equal, comprobará por si misma el ZF o Zero Flag para ver si ha de saltar o no), y de forma explícita con funciones de pila como PUSHF, POPF, PUSHFD y POPFD, que serán explicadas en el apartado referente a la pila. De todas formas, indicar ya que los únicos bits que se pueden modificar con un POPF son los 11, 10, 8, 7, 6, 4, 2 y 0 (y los 12 y 13 si tenemos IOPL = 0, es decir, nivel de administrador... estos 12 y 13 indican el nivel de ejecución del procesador).

En fin, veamos qué tiene que ofrecernos este registro:

 

Desglose del registro EFLAGS

Los bits que tienen puestos un "0" como indicador, no tienen función definida y conservan siempre ese valor 0 (también sucede con el bit 1, que está a 1). Los más importantes, los vemos a continuación:

- IOPL (12 y 13): IOPL significa "I/O priviledge level", es decir, el nivel de privilegio en que estamos ejecutando. Recordemos que normalmente vamos a tener dos niveles de privilegio que llamabamos de usuario y supervisor, o ring3 y ring0. Aquí podemos ver en cuál estamos; si los dos bits están activos estamos en ring3 o usuario, y si están inactivos, en ring0 (sólo pueden modificarse estos bits de los flags si estamos en ring0, por supuesto).

- IF (9): El "Interrupt Flag", controla la respuesta del procesador a las llamadas de interrupción; normalmente está a 1 indicando que pueden haber interrupciones. Si se pone a 0 (poner a 0 se hace diréctamente con la instrucción CLI (Clear Interrupts), mientras que STI (Set Interrupts) lo activa), se prohibe que un tipo bastante amplio de interrupciones pueda actuar mientras se ejecuta el código del programa (viene bien en algunas ocasiones en que estamos haciendo algo crítico que no puede ser interrumpido).

- ZF (6): El Zero Flag, indica si el resultado de la última operación fue 0. Téngase en cuenta que si hemos hecho una comparación entre dos términos, se tomará como si se hubiera hecho una operación de resta; así, si los dos términos son iguales (CMP EAX, EBX donde EAX = EBX p.ej), se dará que el resultado de restarlos es 0, con lo que se activará el flag (se pondrá a 1). Tal y como sucede con CF y OF, este flag es afectado por operaciones aritméticas (ADD, SUB, etc) y de incremento/decremento (INC/DEC).

- CF (0): Carry Flag o Flag de Acarreo. En ocasiones se da un desbordamiento en la operación aritmética, esto es, que no cabe. Si nos pasamos por arriba o por abajo en una operación (p.ej, con 16 bits hacer 0FFFFh + 01h), este resultado no va a caber en un destino de 16 bits (el resultado es 10000h, lo cual necesita 17 bits para ser codificado). Así pues, se pone este flag a 1. Hay también un flag parecido, OF (11) (Overflow Flag), que actúa cuando en complemento a 2 se pasa del mayor número positivo al menor negativo o viceversa (por ejemplo, de 0FFFFh a 0000h o al revés). También nos interesará para ello el SF (7) o flag de signo, que estará activo cuando el número sea negativo según la aritmética de complemento a dos (en realidad, cuando el primer bit del resultado de la última operación sea un 1, lo que en complemento a 2 indica que se trata de un número negativo).

Otros, de menor importancia (se puede uno saltar esta parte sin remordimientos de conciencia), son:

 

- ID (21): El Identification Flag, señala si se puede modificar que se soporta la instrucción CPUID

- VM (17): Este flag controla si se está ejecutando en Virtual Mode 8086; cuando está a 0 se vuelve a modo protegido (el modo virtual 8086 se usa por ejemplo para ejecutar las ventanas Ms-Dos bajo Win32).

- DF(10): El "Direction Flag", va a indicar en qué dirección se realizan las instrucciones de cadena (MOVS, CMPS, SCAS, LODS y STOS). Estas instrucciones, que veremos más adelante, actúan normalmente "hacia adelante". Activar este flag hará que vayan "hacia atrás"; no hace falta preocuparse más por esto, ya se recordará. Tan sólo añadir, que este bit se activa diréctamente con la instrucción STD (Set Direction Flag), y se desactiva (se pone a 0) con la instrucción CLD (Clear Direction Flag).

- TF (8): Es el "Trap Flag" o flag de trampa; se utiliza para debugging, y cuando está activo, por cada instrucción que el procesador ejecute saltará una interrupción INT 1 (se utiliza para depuración de programas).

- AF (4): Flag de acarreo auxiliar o "Adjust Flag". Se usa en aritmética BCD; en otras palabras, pasad de él ;=)

- PF (2): Es el flag de paridad; indica si el resultado de la última operación fue par, activándose (poniéndose a 1) cuando esto sea cierto.

- VIP (20), VIF (19), RF (16), NT(14): No nos van a resultar muy útiles; para quienes busquen una referencia, sus significados son "Virtual Interrupt Pending", "Virtual Interrupt Flag", "Resume Flag" y "Nested Task".

 

 

4.7.2.- Instrucciones de comparación

Los flags son activados tanto por las instrucciones de operación aritmética (ADD, SUB, MUL, DIV, INC y DEC) como por otras dos instrucciones específicas que describo a continuación:

- CMP: Esta es la más importante; el direccionamiento (es decir, aquello con lo que se puede operar) es el mismo que en el resto, y lo que hace es comparar dos operandos, modificando los flags en consecuencia. En realidad, actúa como un SUB sólo que sin almacenar el resultado pero sí modificando los flags, con lo que si los dos operandos son iguales se activará el flag de cero (ZF), etc. No vamos a necesitar recordar los flags que modifican, puesto que las instrucciones de salto condicional que usaremos operarán diréctamente sobre si el resultado fue "igual que", "mayor que", etc, destaquemos no obstante que los flags que puede modificar son OF (Overflow), SF (Signo), ZF (Cero), AF (BCD Overflow), PF (Parity) y CF (Carry).

- TEST: Tal y como CMP equivale a un SUB sin almacenar sus resultados, TEST es lo mismo que un AND, sin almacenar tampoco resultados pero sí modificando los flags. Esta instrucción, sólo modifica los flags SF (Signo), ZF (Cero) y PF (Paridad).

 

 

 

4.8.- Saltos, y saltos condicionales

La ejecución de un programa en ensamblador no suele ser lineal por norma general. Hay ocasiones en las que querremos utilizar "saltos" (que cambien el valor del registro EIP, es decir, el punto de ejecución del programa).

Estos saltos pueden ser de dos tipos; incondicionados y condicionales.

 

4.8.1.- Saltos incondicionados (JMP)

La instrucción JMP es la que se utiliza para un salto no condicional; esto, significa que cuando se ejecuta una instrucción JMP, el registro EIP que contiene la dirección de la siguiente instrucción a ejecutar va a apuntar a la dirección indicada por el JMP.

Existen básicamente tres tipos de salto:

- Salto cercano o Near Jump: Es un salto a una instrucción dentro del segmento actual (el segmento al que apunta el registro CS).

- Salto lejano o Far Jump: Se trata de un salto a una instrucción situada en un segmento distinto al del segmento de código actual.

- Cambio de Tarea o Task Switch: Este salto se realiza a una instrucción situada en una tarea distinta, y sólo puede ser ejecutado en modo protegido.

Cuando estemos programando, lo normal es que utilicemos etiquetas y saltos cercanos. En todo compilador, si escribimos la instrucción "JMP <etiqueta>", al compilar el fichero la etiqueta será sustituida por el valor numérico de la dirección de memoria en que se encuentra el lugar donde queremos saltar.

 

 

4.8.2.- Saltos condicionales (Jcc)

Un "Jcc" es un "Jump if Condition is Met". La "cc" indica una condición, y significa que debemos sustituirlo por las letras que expresen esta condición.

Cuando el procesador ejecuta un salto condicional, comprueba si la condición especificada es verdadera o falsa. Si es verdadera realiza el salto como lo hacía con una instrucción JMP, y en caso de ser falsa simplemente ignorará la instrucción.

A continuación se especifican todos los posibles saltos condicionales que existen en lenguaje ensamblador. Algunas instrucciones se repiten siendo más de una forma de referirse a lo mismo, como JZ y JE que son lo mismo (Jump if Zero y Jump if Equal son equivalentes). En cualquier caso hay que tener lo siguiente en cuenta:

- Las instrucciones de salto condicional más comunes son JE (Jump if Equal), JA (Jump if Above) y JB (Jump if Below), así como las derivadas de combinar estas (por ejemplo, una N entre medias es un Not, con lo que tenemos JNE, JNA y JNB... por otro lado, tenemos JAE como Jump if Above or Equal o JBE, Jump if Below or Equal)

- Puede resultar extraño el hecho de que hay dos formas de decir "mayor que" y "menor que". Es decir, por un lado tenemos cosas como JB (Jump if Below) y por otro JL (Jump if Less). La diferencia es que Below y Above hacen referencia a aritmética sin signo, y Less y Greater hacen referencia a aritmética en complemento a dos.

- Hay un tercer tipo de salto condicional, que comprueba diréctamente el estado de los flags (como pueda ser el de paridad). Entre ellos incluímos también dos especiales; uno que considera si salta dependiendo de si el valor del registro CX es 0 (JCXZ) y otro que considera si el valor de ECX es 0 (JECXZ).

 

Instrucción Descripción Flags
 
Aritmética sin signo
JZ, JE Jump if Zero, Jump if Equal ZF = 1
JNE, JNZ Jump if Not Equal, Jump if Not Zero ZF = 0
JA Jump if Above CF = 0 and ZF = 0
JNA, JBE Jump if Not Above, Jump if Below or Equal CF = 1 or ZF = 1
JNC, JNB, JAE
Jump if Not Carry, Jump if Not Below, Jump if Above or Equal
CF = 0
JNBE Jump if Not Below or Equal CF = 0 and ZF = 0
 
Aritmética en complemento a 2
JNAE, JB, JC Jump if Not Above or Equal, Jump if Below, Jump if Carry CF = 1
JGE, JNL Jump if Greater or Equal, Jump if Not Less SF = OF
JL, JNGE Jump if Less, Jump if Not Greater or Equal SF <> OF
JLE, JNG Jump if Less or Equal, Jump if Not Greater ZF = 1 or SF <> OF
JNG, JLE Jump if Not Greater, Jump if Less or Equal ZF = 1 or SF <> OF
JNGE, JL Jump if Not Greater or Equal, Jump if Less SF <> OF
JNL, JGE Jump if Not Less, Jump if Greater or Equal SF = OF
JNLE, JG Jump if Not Less or Equal, Jump if Greater ZF = 0 and SF = OF
 
Comprobación directa de flags
JNO Jump if Not Overflow OF = 0
JNP Jump if Not Parity PF = 0
JNS Jump if Not Sign SF = 0
JO Jump if Overflow OF = 1
JP, JPE Jump if Parity, Jump if Parity Even PF = 1
JPO Jump if Parity Odd PF = 0
JS Jump if Sign SF = 1
JCXZ Jump if CX is 0 CX = 0
JECXZ Jump if ECX is 0 ECX = 0

 

 

4.8.3.- Ejemplo de programación con saltos condicionales

A estas alturas del curso de ensamblador, creo que estamos abusando mucho de la teoría; ciertamente esto es ante todo teoría, pero no está de más ver un ejemplo práctico de programa en el que usamos saltos condicionales y etiquetas. El programa escrito a continuación, imita una operación de multiplicación utilizando tan sólo la suma, resolviéndolo mediante el algoritmo de que N * M es lo mismo que (N+N+N+...+N), M veces:

; Suponemos que EAX contiene N, y EBX, contiene M.  xor edx,edx 	; Aquí vamos a almacenar el resultado final. La operación xor edx,edx hace EDX = 0. LoopSuma:					; Esto, es una etiqueta add edx,eax		; A EDX, que contendrá el resultado final, le sumamos el primer multiplicando dec ebx jnz LoopSuma	; Si el resultado de decrementar el multiplicando EBX es cero, no sigue sumando el factor de EAX.

Un programa tan sencillo como este, nos dará en EDX el producto de EAX y EBX. Veamos uno análogo para la división:

; Suponemos que EAX contiene el dividendo y EBX el resto.
xor ecx,ecx 	; ecx contendrá el cociente de la división xor edx,edx 	; edx va a contener el resto de la división RepiteDivision: inc ecx			; incrementamos en 1 el valor del cociente que queremos obtener sub eax,ebx		; al dividendo le restamos el valor del divisor cmp eax,ebx		; comparamos dividendo y divisor jna RepiteDivision   	; si el divisor es mayor que el dividendo, ya hemos acabado de ver el cociente mov edx,eax  

Se ve desde lejos que este programa es muy optimizable; el resto quedaba en EAX, con lo que a no ser que por algún motivo en particular lo necesitemos en EDX, podríamos prescindir de la última línea y hacer que el cociente residiera en ECX mientras que el resto sigue en EAX. También sería inútil, la línea "xor edx,edx" que pone EDX a cero, dado que luego es afectado por un "mov edx,eax" y da igual lo que hubiera en EDX.

Hemos visto, además, cómo hacer un bucle mediante el decremento de una variable y su comprobación de si llega a cero, y en el segundo caso, mediante la comprobación entre dos registros; para el primer caso vamos a tener en el ensamblador del PC un método mucho más sencillo utilizando ECX como contador como va a ser el uso de la instrucción LOOP, que veremos más adelante, y que es bastante más optimizado que este decremento de a uno.

 

 

 

4.9.- La pila

 

4.9.1.- PUSH, POP y demás fauna

La pila es una estructura de datos cuya regla básica es que "lo primero que metemos es lo último que sacamos". El puntero que indica la posición de la pila en la que estamos es el SS:ESP, y si pudiéramos verlo gráficamente sería algo como esto:

¿Qué significa este dibujo? Que SS:ESP está apuntando a ese byte de valor 91h; los valores que vienen antes no tienen ninguna importancia (y dado que esta misma pila es utilizada por el sistema operativo cuando se produce una interrupción, es improbable que podamos considerar "fijos" estos valores que hayan en el lugar de las interrogaciones).

La primera instrucción que vamos a ver y que opera sobre la pila, es el PUSH, "empujar". Sobre el dibujo, un PUSH de 32 bits (por ejemplo un PUSH EAX) será una instrucción que moverá "hacia atrás" el puntero de pila,añadiendo el valor de EAX allá. Si el valor del registro EAX fuera de 0AABBCCDDh, el resultado sobre esta estructura de un PUSH EAX sería el siguiente:

Un par de cosas a notar aquí: por una parte sí, el puntero se ha movido sólo (y seguirá moviéndose hacia la izquierda - hacia "atrás" - si seguimos empujando valores a la pila). Por otra, quizá resulte extraño que AABBCCDDh se almacene como DDh, CCh, BBh, AAh, es decir, al revés. Pero esto es algo común; cuando guardamos en alguna posición de memoria un dato mayor a un byte (este tiene cuatro), se van a almacenar "al revés"; este tipo de ordenación, se llama little endian, opuesta a la big endian que almacena diréctamente como AAh BBh CCh DDh un valor así.

La instrucción PUSH, en cualquier caso, no está limitada a empujar el valor de un registro: puede empujarse a la pila un valor inmediato (p.ej, PUSH 1234h), y pueden hacerse referencias a memoria, como PUSH [EBX+12].

Otra instrucción bastante importante es PUSHF (y PUSHFD), que empujan el contenido del registro de Flags en la pila (un buen modo de que lo podamos sacar a un registro y lo analicemos). Como se indica en el gráfico de los Flags en su capítulo correspondiente, PUSHFD empuja los EFlags (flags extendidos, 32 bits), y PUSHF los Flags (los 16 bits menos significativos de este registro).

Ahora, no sólo querremos meter cosas en la pila, estaría interesante poder sacarlas y tal. Para ello, también tenemos una instrucción, el POP, que realiza la acción exáctamente opuesta al PUSH. En particular, va a aumentar el puntero ESP en cuatro unidades y al registro o posición donde se haga el POP, transferir los datos a los que se apuntaba. En el caso anterior, volveríamos a tener el puntero sobre el 91h:

Ya no podemos fiarnos de que el contenido de posiciones anteriores sigue siendo DDh,CCh,BBh,AAh. En cuanto el procesador haga una interrupción va a usar la pila para almacenar datos, luego serán sobreescritos. Si nuestra órden hubiera sido un POP ECX, ahora ECX contendría el valor 0AABBCCDDh.

Otra cosa a tener en cuenta, es que la pila no es más que una estructura fabricada para hacernos más fácil la vida; pero no es una entidad aparte, sigue estando dentro de la memoria principal. Por ello, además de acceder a ella mediante ESP, podríamos acceder con cualquier otro registro sin tener que utilizar las órdenes PUSH/POP. Esto no es usual, pero es bueno saber al menos que se puede hacer. Si en una situación como la del último dibujo hacemos un MOV EBP, ESP y un MOV EAX, SS:[EBP], el registro EAX pasará a valer 07A5F0091h.

Destacan también otras dos instrucciones: PUSHA y POPA. Estas instrucciones lo que hacen es empujar/sacar múltiples registros (por si tenemos que salvarlos todos, resultaría un coñazo salvarlos uno a uno). Exáctamente, estas dos instrucciones afectan a EAX, EBX, ECX, EDX, EBP, ESI y EDI.

 

 

 

4.10.- Subrutinas

 

4.10.1.- Introducción al uso de subrutinas

Es bastante común utilizar subrutinas al programar; podemos verlas como el equivalente a las funciones, de tal forma que se puedan llamar desde cualquier punto de nuestro programa. Supongamos que nuestro programa tiene que recurrir varias veces a un mismo código dedicado, por ejemplo, a averiguar el producto de dos números como hacíamos en el ejemplo de código anterior (vale, el ensamblador del 80x86 admite multiplicación, pero repito que esto es un ejemplo ).

La instrucción que vamos a utilizar para "llamar" a nuestra función de multiplicar, es el CALL. CALL admite, como es habitual, referencias directas a memoria, a contenidos de registros y a contenidos de direcciones de memoria apuntadas por registros. Podríamos hacer un CALL EAX, un CALL [EAX] o diréctamente un CALL 12345678h. Al programar, utilizaremos normalmente un CALL <etiqueta>, que ya se encargará el compilador de traducir a dirección inmediata de memoria.

Luego, dentro de la propia rutina tenemos que devolver el control al código principal del programa, esto es, al punto en el que se había ejecutado un CALL. Esto se hace mediante la instrucción RET, que regresará al punto en que se llamó ejecutándose después la instrucción que venga a continuación.

Como ejemplo con el código anterior:

 

 
<código de nuestro programa> mov eax,<valor1> mov ebx,<valor2> call Producto <resto de nuestro código>  [...]  ; Suponemos que EAX contiene N, y EBX, contiene M.  Producto: xor edx,edx 	; Aquí vamos a almacenar el resultado final. La operación xor edx,edx hace EDX = 0. LoopSuma:					; Esto, es una etiqueta add edx,eax		; A EDX, que contendrá el resultado final, le sumamos el primer multiplicando dec ebx jnz LoopSuma	; Si el resultado de decrementar el multiplicando EBX es cero, no sigue sumando el factor de EAX. ret

 

 

4.10.2.- Cómo funcionan CALL/RET

Cuando llamamos a una subrutina, en realidad internamente está pasando algo más que "pasamos el control a tal punto"; pensemos que se pueden anidar todas las subrutinas que queramos, es decir, que pueden hacerse CALLs dentro de CALLs sin ningún problema.

¿Por qué? Pues por la forma en que funcionan específicamente estas instrucciones:

- CALL, lo que realmente está haciendo es empujar a la pila la dirección de ejecución de la instrucción siguiente al CALL, y hacer un JMP a la dirección indicada por el CALL. Así, al inicio de la subrutina la pila habrá cambiado, y si hiciéramos un POP <registro>, sacaríamos la dirección siguiente a la de desde donde se llamó.

- RET, lo que va a hacer es sacar de la pila el último valor que encuentre (nótese que no sabe que ese sea el correcto, con lo que si en medio de la subrutina hacemos un PUSH o un POP sin controlar que esté todo al final tal y como estaba al principio, el programa puede petar), y saltar a esa dirección. En caso de que no hayamos hecho nada malo, va a volver donde nosotros queríamos.

Jugar con esto nos va a ser muy necesario cuando programemos virus. Hay un sistema muy standard de averiguar la dirección actual de memoria en que se está ejecutando el programa (y que es necesario utilizar normalmente, a no ser que lo hagamos por algún otro método), que funciona como sigue:

 call delta_offset 	; normalmente este método se llama "delta offset", que hace referencia a esta dirección. delta_offset: pop ebp      ; Ahora ebp tiene la dirección de memoria indicada por "delta_offset" en el momento actual.  

No abundaré en más detalles; sólo, que esta es la mejor forma de saber cuánto vale el registro EIP, lo cual nos va a ser de bastante utilidad al programar.

 

 

4.10.3.- Funciones avanzadas en CALL/RET

Para terminar, tenemos que hablar de la existencia de otra forma de RET que es IRET, retorno de interrupción; la trataremos en el siguiente apartado junto con el uso de interrupciones por ser un tanto especial.

Por otro lado, a veces veremos una opción que puede parecernos "extraña", y es que a veces el RET viene acompañado de un número, por ejemplo, RET 4. El número que viene junto con la instrucción, indica que además de sacar el valor de retorno de la pila tenemos que aumentar el valor del puntero de pila en tantas unidades como se indique (téngase en cuenta que 4, p.ej, representan 32 bits, o sea, un registro).

¿Cuál es el sentido de esto? Bien, una forma estándar de llamar a funciones consiste en lo siguiente: si tenemos que pasarle parámetros, lo que hacemos es empujarlos en la pila y después llamar a la función. Leemos los valores de la pila dentro de la subrutina sin cambiar el puntero de pila, y cuando queramos regresar no sólo queremos que el RET saque su dirección de retorno sino que además la pila aumente lo suficiente como para que la pila vuelva a estar en su lugar, como si no hubiéramos empujado los parámetros.

Es decir, pongamos que hacemos PUSH EAX y PUSH EBX y luego un CALL <funcion>. En esta leemos diréctamente los valores empujados a la pila con un MOV <registro>,[ESP+4] y MOV <registro>,[ESP+8] (sí, podemos leer así de la pila sin problemas y sin modificar ESP). Ahora, al volver queremos que la pila se quede como estaba antes de ejecutar el primer PUSH EAX. Pues bien, entonces lo que hacemos es escribir al final de la subrutina un RET 8, lo que equivale a los dos registros que habíamos empujado como parámetros.

Como tampoco me voy a morir si lo hago, adaptaré el código anterior a esta forma de hacer las cosas (que personalmente no es que me guste mucho pero vamos, el caso es que se usa...)

 

<código de nuestro programa> mov eax,<valor1> mov ebx,<valor2> push eax push ebx call Producto <resto de nuestro código>  [...]  ; Suponemos que EAX contiene N, y EBX, contiene M.  Producto:  mov eax,dword ptr [ESP+8] mov ebx,dword ptr [ESP+4] xor edx,edx 	; Aquí vamos a almacenar el resultado final. La operación xor edx,edx hace EDX = 0.  LoopSuma:					; Esto, es una etiqueta  add edx,eax		; A EDX, que contendrá el resultado final, le sumamos el primer multiplicando dec ebx jnz LoopSuma	; Si el resultado de decrementar el multiplicando EBX es cero, no sigue sumando el factor de EAX.
Black Hole  
   
Facebook botón-like  
 
 
Hoy hay 21 visitantes¡Aqui en esta página!
Este sitio web fue creado de forma gratuita con PaginaWebGratis.es. ¿Quieres también tu sitio web propio?
Registrarse gratis