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 II

Atras


En este capítulo vamos a presentar algunos conceptos avanzados en la programación en lenguaje ensamblador, como la relación con la API. También, listaré toda una serie de instrucciones que me parecen importantes a la hora de programar, y que aún no han sido mencionadas. Todo, irá acompañado de ejemplos de código, que a estas alturas ya deberíamos de saber manejar un poco.

Así como los apartados 5.1, 5.2 y 5.3 son bastante importantes, a quienes se vean abrumados por todo esto ya les digo que pueden saltarse tranquilamente el apartado 5.4 (dedicado al coprocesador) . Se van a perder poco si se los saltan en el sentido de que si les resulta ya agotador todo lo aprendido hasta el momento, esto puede que les despiste del verdadero objetivo en el sentido de que no es necesario entenderlo para escribir virus ni para aprender ensamblador; se trata de un poquito de "cultura general" sobre cómo funcionan las cosas internamente (en cualquier caso he intentado dar una visión poco profunda en ese apartado precisamente para no marear a nadie).

 

 

5.1.- Interrupciones y API

 

5.1.1.- Introducción

La API es, como decíamos en el segundo capítulo de este tutorial, la herramienta por la cual nos comunicamos con el sistema operativo y las funciones que este tiene para hacernos la vida más fácil. Una operación puede ser abrir un fichero, escribir sobre él, cambiar el directorio actual o escribir en la pantalla.

Hay dos métodos que vamos a ver en que se usa API del sistema operativo; por interrupciones pasando los parámetros en registros como hace Ms-Dos, por llamadas a subrutina como hace Win32, y un híbrido con llamadas a interrupción y paso de parámetros en pila, el sistema operativo Linux.

 

 

5.1.2.- Interrupciones en Ms-Dos

Vale, lo primero que hay que tener en la cabeza es que en Ms-Dos *todo* se hace a través de interrupciones; y que distintas interrupciones llaman a servicios orientados hacia algo distinto.

¿Qué es una interrupción software?. Se trata de un tipo muy especial de llamada a una función del sistema operativo (o de otros programas residentes, el sistema es bastante flexible). La instrucción para hacerlo es INT, y viene acompañada siempre de un número del 0 al 255 (decimal), es decir, del 00h al 0FFh en hexadecimal.

¿Dónde se va la ejecución cuando escribimos por ejemplo "INT 21h"? Bien, en Ms-Dos, en la posición de memoria 0000:0000 (en Ms-Dos usamos un direccionamiento de 16 bits pero paso de explicarlo porque a estas alturas es un tanto ridículo jugar con el Ms-Dos) hay una "Tabla de Vectores de Interrupción" o IVT. Esta IVT, contiene 256 valores que apuntan a distintas direcciones de memoria, a las que va a saltar la ejecución cuando se haga una INT.

Entonces, si escribimos algo como "INT 21h", lo que va a hacer es leer en la posición de memoria 0000 + (21*4), el valor que hay, para luego pasar (como si fuera un CALL, empujando en la pila la dirección de retorno) a ejecutar en esa posición de memoria. En realidad, la única diferencia con un CALL es que no le indicamos la dirección a la que saltar (el procesador la lee de la tabla de interrupciones), y que además de empujar el valor de la dirección de retorno, se empuja también el registro de FLAGS.

Por ello, cuando se acaba de ejecutar el servicio solicitado de la interrupción, esta rutina no acaba en un RET, sino en lo que antes habíamos mencionado, en un IRET; la función de esta instrucción es sencilla: saca la dirección de retorno y los flags, en lugar de tan sólo la dirección de retorno.

Como ejemplo práctico, el tipo de función en Ms-Dos dentro de una interrupción suele indicarse en EAX, y los parámetros en el resto de registros (EBX, ECX y EDX normalmente). Por ejemplo, cuando queramos abrir un fichero como sólo lectura tenemos que hacer lo siguiente:

 mov ax, 3D02h		; el 3D indica "abrir fichero", y el 02h indica "en lectura y escritura" mov dx, offset Fichero		; Apuntamos al nombre del fichero int 21h			; Ahora, se abrirá el fichero (paso de explicar todavia qué es un handler xD)  Fichero: 	db 	'fichero.txt',0  

Bueno, nos hemos encontrado (qué remedio) una cosa nueva de la que no habíamos hablado antes... esto de "db" significa "data byte", vamos, que estamos indicando datos "a pelo", en este caso el nombre de un fichero. Y sí, hay una coma, indicando que después de esos datos "a pelo" se ponga un byte con valor cero (para delimitar el fin del nombre del fichero). DX va a apuntar a ese nombre de fichero y AX indica la función... y voilá, fichero abierto.

Destacar otra cosa: existen dos instrucciones que sirven para activar o inhabilitar las interrupciones (ojo, que inhabilitar no las deshace por completo, pero sí impide la mayor parte; es útil por ejemplo al cambiar los valores de SS/SP para que no nos pete en la cara). CLI (CLear Interrupts) inhabilita las interrupciones, y STI (SeT Interrupts) las activa.

Otra cosa: se puede ver que no estoy usando registros extendidos, que uso AX y DX en vez de EAX y EDX... en fin, recordad hace cuánto que existe el Ms-Dos y así respondéis a la pregunta :-)

Y en fin, que esto es el sistema de interrupciones en Ms-Dos, que me niego a volver a tocar porque es perder el tiempo: a quien le interese que escriba en un buscador algo así como "Ralf Brown Interrupt List", que es una lista salvaje que tiene todas las funciones habidas y por haber para interrupciones de Ms-Dos. Las más importantes están dentro de la INT 21h, que controla cosas como el acceso a ficheros (creacion, lectura/escritura, borrado...) y directorios.

 

 

5.1.3.- La Int80h y Linux

En Linux pasa tres cuartas de lo mismo, pero todas las funciones del sistema están reunidas bajo una sóla interrupción, la 80h. Vamos a tener 256 posibilidades, que se indican en AL (bueno, podemos hacer un MOV EAX,<valor> igualmente).

Hay algunas diferencias básicas con el sistema de Ms-Dos. La primera es más "teórica" y hace referencia a la seguridad. Cuando estamos ejecutando normalmente, el procesador tiene privilegio de "usuario". Cuando llamamos a la INT80h, pasamos a estado de supervisor y el control de todo lo toma el kernel. Al terminar de ejecutarse la interrupción, el procesador vuelve a estar en sistema usuario (y por supuesto con nivel de usuario el proceso no puede tocar la tabla de interrupciones). Con Ms-Dos digamos que siempre estamos en supervisor, podemos cambiar los valores que nos salga de la tabla de interrupciones y hasta escribir sobre el kernel... pero vamos, lo importante, que con este sistema, en Linux está todo "cerrado", no hay fisuras (excepto posibles "bugs", que son corregidos, a diferencia de Windows).

Respecto al paso de parámetros, se utilizan por órden EBX, ECX, etc (y si la función de interrupción requiere muchos parámetros y no caben en los registros, lo que se hace es almacenar en EBX un puntero a los parámetros.

 mov eax, 05h			; Función OpenDir (para abrir un directorio para leer sus contenidos) lea ebx, [diractual]	; LEA funciona como un "MOV EBX, offset diractual"; es más cómodo. xor ecx, ecx int 080h  diractual: 	db 	'.',0			; Queremos que habra el directorio actual, o sea, el '.'  

Para más información, recomiendo echar un vistazo a www.linuxassembly.org, de donde tiene que colgar algún link hacia listas de funciones. Y también cuidado porque aunque en Linux parece que todo está documentado hay mucho que o no lo está o incluso está mal (si alguien se ha tenido que mirar la estructura DIRENT sabrá de qué le hablo, es más difícil hacer un FindFirst/FindNext en ASM en Linux que infectar un ELF xD)

 

 

5.1.4.- DLL's y llamadas en Windows

Bajo Win32 (95/98/Me/NT/etc) no vamos a utilizar interrupciones por norma general. Resulta que la mayor parte de funciones del sistema están en una sóla librería, "KERNEL32.DLL", que suelen importar todos los programas.

DLL significa Librería Dinámica, y no solo tiene porque tener funciones "básicas" (por ejemplo, en Wininet.DLL hay toda una serie de funciones de alto nivel como enviar/coger fichero por FTP). Lo que sucede es que cuando un programa quiere hacer algo así (pongamos estas funciones de FTP) tiene dos posibilidades: uno, las incorpora a su código, y dos, las coge de una librería dinámica. ¿Cuál es la ventaja de esto? Bien, cuando usamos una librería dinámica no tenemos que tener diez copias de esa rutina en cada uno de los programas compilados; al estar en la DLL, el primer programa que la necesite la pide, la DLL se carga en memoria, y se usa una sóla copia en memoria para todos los programas que pidan servicios de ella.

En palabras sencillas; tenemos la función MessageBox, por ejemplo, que abre una ventana en pantalla mostrando un mensaje y con algún botón del tipo OK, Cancelar y tal. ¿Qué es más eficiente, tener una librería que sea consultada por cada programa, o tener una copia en cada uno?. Si cada programa ocupa 100Kb de media y la librería 10Kb, al arrancar 10 veces el programa si tuviéramos el MessageBox en DLLs, el espacio en memoria sería de 1010Kb (y en disco, igual). En caso de que no usáramos DLLs y la función MessageBox estuviera en cada programa, tendríamos 1100Kb de memoria ocupada (y de disco). Por cierto, que el Linux también usa librerías dinámicas, sólo que para programar en ASM sobre él normalmente nos va a sobrar con lo que tengamos en la Int80h.

Volviendo al tema, la forma de llamar a una función de la API en Win32 es como lo que comentábamos de paso de parámetros al final del apartado dedicado a subrutinas. Todos los valores que han de pasársele a la función se empujan a la pila, y luego se hace un CALL a la dirección de la rutina. El aspecto de una llamada a la API de Win32 (exáctamente a MessageBox), es así:

 push    MB_ICONEXCLAMATION		; El tipo de ventana a mostrar push    offset Azathoth			; Esto, el título de la ventana que va a aparecer. push    offset WriteOurText		; Y esto el texto de dentro de la ventana. push    NULL call    MessageBoxA				; Llamamos tras empujar los parámetros, y luego seguimos ejecutando <resto del código>  WriteOurText:   db      'H0 H0 H0 NOW I HAVE A MACHINE GUN',0	; El 0 delimita el final del texto. Azathoth:		db		'Hiz ',0  

La mayoría de las funciones que se van a utilizar están en KERNEL32.DLL. No obstantes hay otras, como USER.DLL, bastante importantes. Podemos ver si un ejecutable las importa si están en su "tabla de importaciones" dentro del fichero (es decir, que está indicado que se usen funciones de ellas). Una de las cosas más interesantes sobre este sistema, será que podemos cargar DLLs (con la API LoadLibrary) aún cuando ya se haya cargado el programa, y proveernos de servicios que nos interesen.

Una lista bastante interesante de funciones de la API de Windows está en el típico CD del SDK de Windows; puede encontrarse también por la Red, se llama win32.hlp y ocupa más de 20Mb (descomprimido).

 

 

 

5.2.- Representación de datos, etiquetas y comentarios

Buena parte de lo que voy a explicar ahora ha aparecido irremediablemente en ejemplos de código anteriores; no obstante, creo que ya es hora de "formalizarlo" y detallarlo un poco. Así pues, hablemos de estos formalismos utilizados en lenguaje ensamblador (tomaré como base los del Tasm, algunos de los cuales lamentablemente no son aplicables al Nasm de Linux):

 

 

5.2.1.- Datos

La forma más básica de representar datos "raw", o sea, "a pelo", es usar DB, DW o DD. Como se puede uno imaginar, B significa byte, W word y D Dword (es decir, 8, 16 y 32 bits). Cuando queramos usar una cadena de texto - que encerraremos entre comillas simples -, usaremos DB. Así, son válidas expresiones como las siguientes:

db 	00h, 7Fh, 0FFh, 0BAh dw	5151h dd	18E7A819h db 	'Esto también es una cadena de datos' db	'Y así también',0 db	?,?,?				; así también... esto indica que son 3 bytes cuyo valor nos es indiferente. 

Hay una segunda forma de representar datos que se utiliza cuando necesitamos poner una cantidad grande de ellos sin describir cada uno. Por ejemplo, pongamos que necesito un espacio vacío de 200h bytes cuyo contenido quiero que sea "0". En lugar de escribirlos a pelo, hacemos algo como esto:

db	200h dup (0h)  

 

5.2.2.- Etiquetas

Ya hemos visto el modo más sencillo de poner una etiqueta; usar un nombre (ojo, que hay que estar pendiente con mayúsculas/minúsculas porque para ensambladores como Tasm, "Datos" no es lo mismo que "dAtos" o que "datos"), seguido de un símbolo de ":". Cualquier referencia a esa etiqueta (como por ejemplo, MOV EAX,[Datos]), la utiliza para señalar el lugar donde ha de actuar.

Pero hay más formas de hacer referencias de este tipo; podemos marcar con etiqueta un byte escribiendo el nombre y a continuación "label byte" (etiquetar byte). Un ejemplo (y de paso muestro algo más sobre lo que se puede hacer) sería esto:

virus_init 		label 	byte <código> <código> virus_end		label 	byte virus_length	equ		virus_end - virus_init

Parece que siempre que meto un ejemplo saco algo nuevo de lo que antes no había hablado... pero bueno, creo que se entiende; marcamos con label byte inicio y fin del virus, y hacemos que el valor virus_length equivalga gracias al uso de "equ", a la diferencia entre ambos (es decir, que si el código encerrado entre ambas etiquetas ocupa 300 bytes, si hacemos un "MOV EAX, virus_length" en nuestro código, EAX pasará a valer 300).

 

 

5.2.3.- Comentarios

Conocemos en este punto de sobra la forma standard de incluir comentarios al código, esto es, utilizando el punto y coma. Todo lo que quede a la derecha del punto y coma será ignorado por el programa ensamblador, con lo que lo utilizaremos como comentarios al código.

Hay otro método interesante presente en el ensamblador Tasm, que señala el inicio de un comentario por la existencia de la cadena "Comment %". Todo lo que vaya después de esto será ignorado por el ensamblador hasta que encuentre otro "%", que marcará el final:

Comment % 	Esto es un comentario 		para Tasm 			y puedes escribir 				lo que quieras 					entre los porcentajes. %

 

 

 

5.3.- Otras instrucciones importantes

En este apartado, veremos unas cuantas instrucciones que me ha faltado mencionar hasta ahora y que son muy útiles a la hora de programar en ensamblador. Para ver una lista completa recomiendo ir a www.intel.com y buscar su documentación (se encuentra en formato PDF). En el siguiente apartado, el 5.4, menciono otras cuantas operaciones que pueden resultar útiles, aunque no sean tan necesarias como estas.

 

 

5.3.1.- CLC/STC

Se trata de dos instrucciones que manejan diréctamente los valores del Carry Flag. CLC significa CLear Carry (lo pone a cero como es de suponer) y STC es SeT Carry (poniéndolo a 1).

 

 

5.3.2.- Instrucciones de desplazamiento lateral

Ya hemos visto como podemos operar con un registro o una posición de memoria con operaciones aritméticas (ADD, SUB, etc) y lógicas (AND, OR, etc). Nos faltan pues las instrucciones de desplazamiento lateral, de las que hay de dos tipos:

- Rotación: Aquí tenemos ROL y ROR, que significa ROtate Left y ROtate Right. El modo de funcionamiento es sencillo; si ejecutamos un ROR EAX,1, todos los bits de EAX se moverán un paso a la derecha; el primer bit pasará a ser el segundo, el segundo será el tercero, etc. ¿Qué pasa con el último bit, en este caso el bit 32?. Bien, es sencillo, este bit pasará a ser ahora el primero.

En el caso de ROL, la rotación es a izquierdas; viendo un caso práctico, supongamos la instrucción ROL AL,2, donde AL valga 01100101. El resultado de esta operación sería 10010101. Se puede comprobar que se han movido en dos posiciones los bits hacia la izquierda; y si alguno "se sale por la izquierda", entra por la derecha.

- Desplazamiento aritmético: Tenemos aquí a las instrucciones SHL y SHR, es decir, SHift Left y SHift Right. La diferencia con ROL/ROR es sencilla, y consiste en que todo lo que sale por la izquierda o por la derecha se pierde en lugar de reincorporarse por el otro lado.

Es decir, si hacemos un SHL AL,2 al AL aquel que decíamos antes y que valía 01100101, el resultado será en esta ocasión 10010100 (los dos últimos espacios se rellenan con ceros). Es interesante notar que un SHL de una posición es como multiplicar por dos la cifra, dos posiciones multiplicar por 4, tres por 8, etc.

 

 

5.3.3.- De cadena

Existe toda una serie de instrucciones en el ensamblador 80x86 dedicadas a tratar cadenas largas de bytes; en estas instrucciones vamos a encontrar algunas como MOVSx, CMPSx, SCASx, LODSx y STOSx:

- MOVSx: La x (y lo mismo sucede con el resto) ha de ser sustituída por una B, una W o una D; esto, indica el tamaño de cada unidad con la que realizar una operación (B es Byte, 8 bits, W es Word, 16 bits, y D es Dword, 32 bits). Cuando se ejecuta un MOVSx, se leen tantos bytes como indique el tamaño de la "x" de la dirección DS:[ESI], y se copian en ES:[EDI]. Además, los registros ESI y EDI se actualizan en consecuencia según el "movimiento realizado".

Es decir, que si tenemos en DS:[ESI] el valor "12345678h" y en ES:[EDI] el valor "87654321h", la instrucción MOVSD hará que en ES:[EDI] el nuevo contenido sea ese "12345678h".

- CMPSx: En esta instrucción de cadena, se comparará el contenido del byte/word/dword (dependiendo del valor de "x", según sea B, W o D) presente en la dirección DS:[ESI] con el de la dirección ES:[EDI], actualizando los flags de acuerdo a esta comparación (como si se tratara de cualquier otro tipo de comparación). Como sucedía antes, ESI y EDI se incrementan en la longitud de "x".

- SCASx: Compara el byte, word o dword (según el valor de "x") en ES:EDI con el valor de AL, AX o EAX según la longitud que le indiquemos, actualizando el registro de flags en consecuencia. La utilidad de esta instrucción, reside en la búsqueda de un valor "escaneando" (de ahí el nombre de la instrucción) a través del contenido de ES:EDI (recordemos que, como en las anteriores, EDI se incrementa tras la instrucción en el valor indicado por la "x").

- LODSx: Carga en AL, AX o EAX el contenido de la dirección de memoria apuntada por DS:ESI, incrementando luego ESI según el valor de la "x". Es decir, que si tenemos en DS:ESI los valores "12h, 1Fh, 6Ah, 3Fh", un LODSB pondría 12h en AL, LODSW haría AX como 1F12h, y LODSD daría a EAX el valor de 03F6A1F12h.

- STOSx: La operación contraria a LODSx, almacena AL, AX o EAX (según lo que pongamos en la "x") en la dirección apuntada por ES:EDI, incrementando después EDI según el valor de la "x". Ejemplificando, si por ejemplo AX vale 01FF0h y ES:EDI es "12h, 1Fh, 6Ah, 3Fh", un STOSW hará que en ES:EDI ahora tengamos "F0h, 1Fh, 6Ah, 3Fh".

- REP: La potencia de las anteriores operaciones se vería mermada si no contáramos con una nueva instrucción, REP, que les confiere una potencia mucho mayor. Este REP es un prefijo que se antepone a la operación (por ejemplo, REP MOVSB), y que lo que hace es repetir la instrucción tantas veces como indique el registro ECX (cada vez que lo repite, se incrementará ESI, EDI o los dos según corresponda y se decrementará ECX hasta que llegue a cero, momento en que para).

La utilidad es muy grande por ejemplo cuando queremos copiar una buena cantidad de datos de un lugar a otro de memoria. Supongamos que tenemos 200h datos a transferir en un lugar que hemos marcado con la etiqueta Datos1, y que queremos trasladarlos a una zona de memoria marcada por Datos2 como etiqueta:

lea esi,Datos1	; la utilidad de LEA está explicada más adelante; carga en ESI la dirección de Datos1. lea edi,Datos2  ; lo mismo pero con EDI. mov ecx,200h	; Cantidad de datos a copiar rep movsb		; Y los copiamos...  

 

- STD/CLD: Aunque es de un uso escaso, hay un flag en el registro de EFLAGS que controla algo relacionado con todo esto de las instrucciones de cadena. Estamos hablando del "Direction Flag", que por defecto está desactivado; cuando así es y se lleva a cabo alguna (cualquiera) de las operaciones de cadena anteriormente especificadas, ESI y/o EDI se incrementan de la forma ya mencionada. Sin embargo, cuando este flag está a "1", activado, ESI y/o EDI en la operación se decrementan en lugar de incrementarse.

La función entonces de las dos instrucciones indicadas, STD y CLD, es la de tener un control directo sobre ésta cuestión. La órden STD significa Set Direction Flag; pone a "1" este flag haciendo que al realizarse la operación los punteros ESI y/o EDI se decremente(n). La órden CLD significa Clear Direction Flag, y lo pone a "0" (su estado habitual), tornando en incremental la variación del valor de los punteros al llevarse a cabo las operaciones de cadena.

 

 

5.3.4.- LEA

El significado de LEA, es "Load Effective Adress"; calcula la dirección efectiva del operando fuente (tiene dos operandos) y la guarda en el primer operando. El operando fuente es una dirección de memoria (su offset es lo que se calcula), y el destino es un registro de propósito general. Por ejemplo:

LEA EDX, [Etiqueta+EBP]: En EDX estará la dirección de memoria a la que equivale Etiqueta + EBP (ojo, la dirección, NO el contenido).

 

 

5.3.5.- LOOP

La instrucción LOOP es un poco como la forma "oficial" de hacer bucles en ensamblador, y el porqué de que ECX sea considerado como "el contador". Este comando tiene un sólo parámetro, una posición de memoria (que normalmente escribiremos como una etiqueta). Cuando se ejecuta comprueba el valor de ECX; si es cero no sucede nada, pero si es mayor que cero lo decrementará en 1 y saltará a la dirección apuntada por su operando.

mov ecx, 10h	; Queremos que se repita 10h veces. Bucle: <codigo bucle> <codigo bucle> loop Bucle

Como es lógico, el loop actuará ejecutando lo que hay entre "Bucle" y él 10h veces, cada vez que llegue al LOOP decrementando ECX en uno.

 

 

5.3.6.- XCHG

La operación XCHG, intercambia el contenido de dos registros, o de un contenido de un registro y la memoria. Son válidos, pues:

XCHG EAX,[EBX+12]: Intercambia el valor contenido en la posición de memoria apuntada por [EBX+12], y el registro EAX.

XCHG ECX, EDI

Existe una variante, XADD, que lo que hace es intercambiarlos igual, pero al tiempo sumarlos y almacenar el resultado en el operando destino. Esto es:

XADD EAX,EBX: Lo que hará es que al final EBX valdrá EAX, y EAX, la suma de ambos.

 

 

 

 

5.4.- Otras instrucciones interesantes

 

5.4.1.- Instrucciones a nivel de Bit

A veces no queremos operar con bytes completos, sino quizá poner a 1 un sólo bit, a 0, o comprobar en qué estado se encuentra. Bien que esto se puede hacer utilizando instrucciones como el AND (por ejemplo, si hacemos AND AX,1 y el primer bit de AX no está activado el flag de Zero se activará, y no así en otro caso), OR (los bits a 1 de la cifra con la que hagamos el OR activarán los correspondientes del origen y un 0 hará que nada varíe), y de nuevo AND para poner a cero (los bits del operando con el AND puestos a 1 no variarán, y los 0 se harán 0...).

El caso, que tenemos instrucciones específicas para jugar con bits que nos pueden ser útiles según en qué ocasiones (un ejemplo de utilización se puede ver en el código de encriptación de mi virus Unreal, presente en 29A#4). Estas son:

- BTS: Bit Test and Set, el primer operando indica una dirección de memoria y el segundo un desplazamiento en bits respecto a esta. El bit que toque será comprobado; si es un 1, se activa el carry flag (comprobable con JC, ya se sabe) y si es 0, pues no. Además, cambia el bit, fuera cual fuese en principio, a un 1 en la localización de memoria indicada. Por ejemplo, un "BTS [eax+21],16h" contaría 16h bits desde la dirección "eax+21", comprobaría el bit y lo cambiaría por un 1.

- BTR: Bit Test and Reset, como antes el primer operando se refiere a una dirección y el segundo a un desplazamiento. Hace lo mismo que el BTS, excepto que cambia el bit indicado, sea cual sea, por un 0.

- BT: Bit Test, hace lo mismo que las dos anteriores pero sin cambiar nada, sólo se dedica a comprobar y cambiar el Carry Flag.

- BSWAP: Pues eso, Bit Swap... lo que hace es ver el desplazamiento con el segundo operando (el formato, como en las anteriores), y cambiar el bit; si era un 1 ahora será un 0 y viceversa.

- BSF: Bit Scan Forward, aquí varía un poco el tema; se busca a partir de la dirección indicada por el segundo operando para ver cuál es el bit menos significativo que está a 1. Si al menos se encuentra uno, su desplazamiento se almacenará en el primer operando. Es decir, si hacemos BSF EAX,[EBX] y en EBX tenemos la cadena 000001xxx, EAX valdrá 5 tras ejecutar esta instrucción. Tenemos también una instrucción, BSR, que hace lo mismo pero buscando hacia atrás (Bit Scan Reverse).

 

 

5.4.2.- CPUID

Se trata de una instrucción que devuelve el tipo de procesador para el procesador que ejecuta la instrucción. También indica las características presentes en el procesador, como si tiene coprocesador (FPU o Floating Point Unit). Funciona en todo procesador a partir de 80486.

Dependiendo del valor de EAX devuelve cosas distintas:

- Si EAX = 0: Aquí lo que estamos haciendo es comprobar la marca. Por ejemplo, con un Intel, tendríamos EBX = "Genu", ECX = "ineI' y EDX = "ntel". En el caso de AMD, tendremos EBX = "Auth", ECX = "enti" y EDX = "cAMD".

- Si EAX = 1: En este caso, se intenta averiguar características de la familia del procesador. En EAX, se devuelve la información de esta, así:

31-14: Sin usar 13-12: Tipo de Procesador 11-8: Familia del Procesador 7-4: Modelo de Procesador 3-0: Stepping ID 

También en la web de Intel se puede encontrar información más detallada a este respecto. Reproduzco de todas formas, una tabla con los valores de diferentes procesadores que saqué hace tiempo (es escasa y no tiene en cuenta K6-3, K7 y Pentium III, pero sirve como muestra):

 

Familia Procesador Modelo
0100 1000 Intel DX4
0101 0001 Pentium (60-66 Mhz)
0101 0010 Pentium (75-200 Mhz)
0101 0100 Pentium MMX (166-200 Mhz)
0110 0001 Pentium II
0110 0011 Pentium II, model 3
0110 0101 Pentium II model 5 y Celeron
0101 0110 K6
0101 1000 K6-2

 

 

5.4.3.- RDTSC

Esta instrucción, significa "ReaD TimeStamp Counter". Su función, es la de devolvernos en el par EDX:EAX el "Timestamp" almacenado por el procesador. Este contador es almacenado por el procesador y se resetea cada vez que lo hace el procesador, incrementándose en una unidad cada vez que sucede un ciclo de reloj. Por lo tanto, es obvia su utilidad como generador de números aleatorios, aunque por algún motivo que no alcanzo a comprender, dependiendo de un flag en el registro interno CR4 (el flag TSD, Time Stamp Disable) esta instrucción puede ser restringida para ser ejecutada en el modo User del procesador (en la práctica, por ejemplo bajo Windows 98 la instrucción no devuelve nada pues está deshabilitada en este flag, sin embargo en Linux sí lo da...).

 

 

 

5.4.- Introducción al coprocesador matemático

 

5.4.1.- Conceptos básicos

El coprocesador matemático o FPU (Floating-Point Unit) utiliza números tanto reales como enteros, y se maneja mediante instrucciones integradas en el grupo común que utilizamos; es decir, que simplemente tiene sus instrucciones específicas que él va a utilizar.

Los números se representan normalizados, con 1 bit de signo, exponente y mantisa. Me gustaría poder explicar esto al detalle pero necesitaría demasiado espacio y no creo que resulte tan útil... tan sólo destacar que este es el tipo de formato que se usa para representar números reales, y se basa en que el primer bit del registro indica el signo (0 es positivo, 1 negativo), otros cuantos bits (en este caso 13) representan un exponente, y otros cuantos (64 esta vez) una mantisa. Es decir, que por decirlo de una forma algo burda, para obtener el número real deberíamos coger la mantisa y elevarla a este exponente.

Así, podemos ver que son 80 los bits que tiene cada registro de la FPU. Estos registros son ocho en total, y sus nombres consisten en una R seguida de un número, yendo desde R0 a R7.

 

 

5.4.2.- La pila del coprocesador

La forma de acceder a los registros no es como en la CPU. Por decirlo de alguna manera, se forma una pila de registros con estos ocho registros de datos, operándose con el primero en esta pila (que llamamos ST(0)) y con varias instrucciones para mover los propios registros del copro con este objetivo.

Para referenciar en ensamblador a estos registros se hará a traves de la mencionada pila. Lo más alto de la pila será ese ST(0) - o simplemente, ST - , y podemos hacer operaciones como la siguiente:

 

FADD ST, ST(2)

Por supuesto, el resultado de esta suma entre el primer y tercer valor de la pila del coprocesador, almacenará el resultado en esta parte superior de la pila (ST), desplazando al resto en una posición hacia abajo (el anterior ST será ahora ST(1), y el ST(2) será ahora ST(3)). El sistema puede resultar - y de hecho es - algo lioso, pero todo se aclara si se utiliza un buen debugger y se comprueba a mano lo que estoy diciendo, y cómo la FPU administra sus registros.

 

 

5.4.3.- Ejemplo de una multiplicación con la FPU

Como no me quiero entretener mucho con esto - que probablemente nadie vaya a usar -, pasaré diréctamente a utilizar un ejemplo bastante ilustrativo de lo que sucede cuando estamos utilizando la FPU.

Pretendemos, en el ejemplo siguiente, llevar a cabo la operación (10 x 15) + (17 x 21):

fld	10			; Cargamos en ST(0) el valor 10 fmul 15			; Ahora 10 x 15 se almacena en ST(0) fld 17			; ST(0) vale 17, y OJO, el resultado de 10x15 (150) estará en ST(1) fmul 21			; ST(0) pasa a valer 21 x 17, es decir, 191. Tenemos pues que 				;ST(0)=21x17=191 y ST(1)=10x15=150 fadd ST(1)		; Con esta instrucción añadimos a ST(0)(operando por defecto) ST(1), luego 				;acabamos teniendo ST(0) = (21x17)+(10x15) = 341, resultado final. 

Una referencia completa de las instrucciones utilizadas por la FPU (que son muchas y permiten coger o guardar en memoria, transferir a un formato que entiendan los registros del procesador y viceversa y unas cuantas operaciones como incluso senos y cosenos), recomiendo echarle un vistazo a los manuales de Intel - aunque son medianamente oscuros con el tema.

Black Hole  
   
Facebook botón-like  
 
 
Hoy hay 19 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