SlideShare una empresa de Scribd logo
1 de 85
Descargar para leer sin conexión
MINICURSO
ASSEMBLY
Autor: Greythorne the Technomancer
Traducción: Eidan Yoson
Adaptación: Franciny Salles (#Bl4kd3m0n)
Modulo 1
Cuando termine de leer esta página deberá conocer:
 Sistemas de numeración
 Operaciones binarias
Sistema de numeración
Estamos habituados al sistema de numeración decimal y nos parece lógico usarlo en todo momento.
Pero hay ocasiones en donde no es el más apropiado. Uno de esos mundos en los que existen
sistemas más descriptivos de los fenómenos que el decimal es el de los procesadores. Por su
naturaleza digital, los procesadores son máquinas esencialmente binarias. Utilizan el sistema de
numeración llamado binario, en el que sólo se disponen dos signos: 0 y 1. Contando
correlativamente de manera binaria, diríamos: 0, 1, 10, 11, 100, 101, 110, 111, ... ¿complicado?
Pero es muy fácil!. Tanto el sistema binario, como el decimal y el hexadecimal, son sistemas en los
que la posición de cada dígito representa información de mucha importancia. Veamos un ejemplo
de cómo se descompone posicionalmente un numero decimal:
El número 7935 = 1000 * 7 + 100 * 9 + 10 * 3 + 1 * 5
Elemental ¿no?. Sin embargo, la numeración romana no goza de tan buenas propiedades y por eso
hace ya tiempo se lo reemplazó por el sistema decimal (a excepción de la numeración de las
páginas del prefacio en los libros y del numero de serie de las películas de Rocky :=)
Como hay diez símbolos (del 0 al 9), una decena representa 10 unidades, una centena representa 10
decenas, etc. Diez unidades de una posición, valen una unidad en la posición contigua a la
izquierda. En el sistema binario, con dos símbolos solamente, cada posición a la izquierda vale el
doble de la que le sigue a la derecha. O lo que es lo mismo decir, la relación entre las sucesivas
posiciones se da según la sucesión
1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536 .....
la que a su vez puede expresarse como potencias crecientes de 2:
20
, 21
, 22
, 23
, 24
, 25
, 26
, 27
, 28
, 29
, 210
, 211
, 212
, 213
, 214
, 215
, 216
.....
Para el sistema de numeración binaria, valen las dos reglas prácticas siguientes:
 Un número de n bits puede representar a un decimal de un valor de hasta 2n
- 1
 El multiplicador del bit de posición n vale 2n
Ejemplos: un número de 8 bits cuenta desde 0 hasta 255. El multiplicador del bit 7 es 128. Notar
que siempre se comienza a contar desde cero. En un número binario, al igual que en un decimal, el
bit menos significativo (correspondiente al multiplicador 20
, o sea 1) es el que se escribe más a la
derecha:
bit# 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
mult 32768 16384 8192 4096 2078 1024 512 256 128 64 32 16 8 4 2 1
Veamos como ejemplo práctico un número de 7 bits cualquiera como 1001101 (notar que los bits
se ordenan 6...0)
1001101 = 64 * 1 + 32 * 0 + 16 * 0 + 8 * 1 + 4 * 1 + 2 * 0 + 1 * 1
Esto nos proporciona una forma de traducir (cambiar de base) un número binario a decimal. Basta
sumar aquellos multiplicadores cuyos bits estén en 1 e ignorar aquellos cuyo bit es 0. En nuestro
anterior ejemplo es:
1001101 = 64 + 8 + 4 + 1 = 77 decimal
Para el traspaso de decimal a binario, hay que dividir siempre por 2 y contar sólo los restos, de atrás
hacia adelante. Observese que el resto no es otra cosa que el multiplicador de las potencias de dos
en las anteriores igualdades, las que pueden ser definidas como la sumatoria de los productos de los
restos por sus potencias de dos respectivas Por ejemplo, para el 77 decimal obtenemos los restos:
opreración resto pot.de 2
77 / 2 = 38 r=1 1
38 / 2 = 19 r=0 2
19 / 2 = 9 r=1 4
9 / 2 = 4 r=1 8
4 / 2 = 2 r=0 16
2 / 2 = 1 r=0 32
1 / 2 = 0 r=1 64
Ordenando los restos según las potencias decrecientes de 2, obtenemos nuevamente 1001101.
Los números binarios son los que efectivamente fluyen dentro del procesador en una PC, se
guardan en memoria o disco, o se transmiten (modulados) por modem. Pero un humano no puede
manipular con facilidad números como:
1101 0011 0101 0110 1010 0101 1100 0011
que es de 32 bits (hay 32 símbolos en el número, desde el bit 31 a la izquierda hasta el bit 0, a la
derecha) y se ha ordenado ex-profeso en grupos de a cuatro por cuestiones de comodidad que serán
evidentes algo más adelante. El procesador 80386 hace ya más de una década manipulaba sin
problemas números de 32 bits. Un humano necesita manejarlo de otra manera y por eso se inventó
el sistema hexadecimal, con 16 símbolos, ya que si uno agrupa cuatro bits obtiene 16
combinaciones posibles (24
= 16). Esto tiene una razón. Nuestro sistema decimal no se corresponde
en la cantidad de dígitos con el binario en cambio, el hexadecimal si, porque cada cuatro bits
representan un dígito hexadecimal exacto.
De tal manera, el anterior número de 32 bits se traduce al hexadecimal como uno de 8 dígitos (32
bits agrupados de a 4). Para la conversión podemos usar la tabla binario-decima-hexa qe está algo
más adelante. En un sistema hexadecimal, necesitamos 16 símbolos. Ya que somos muy buenos
manejando números decimales, adoptamos esos diez símbolos (0, 1, 2, 3, 4, 5, 6, 7, 8 y 9) para
empezar, pero hay que agregar otros seis. Mmh ! por qué no A, B, C, D, E y F ? De esta forma, si
me toca contar jugando a las escondidas y quiero hacerlo en hexadecimal (de puro tonto, porque
voy a contar un 60% más:=), tengo que decir: 0, 1,.......8, 9, A, B, C, D, E, F, 10, 11.........18, 19,
1A, 1B, 1C, 1D, 1E, 1F, 20, 21........29, 2A,.........2E, 2F, 30, 31 ...
El anterior e impronunciable numero binario de 32 bits pasa a ser:
0xD356A5C3 hexa, es igual a 3.545.671.107 en decimal
Por cierto que no hice la conversión de binario a decimal a mano con la fórmula anterior, sino que
usé la calculadora de Windows en modo científico, que permite operar o convertir números entre
bases binaria, octal, decimal y hexadecimal. Otra base de numeración posible con traducción de
dígitos exacta al binario es la octal que tiene sólo 8 símbolos (del 0 al 7), con lo cual cada dígito
representa a 3 dígitos binarios, pero está casi en desuso.
Note el lector el "0x" del comienzo, para significar que lo que sigue es un número hexadecimal.
Otro estilo es poner una "h" final, con la precaución de colocar un cero adelante si el número
comienza con A, B, C, D, E o F. Para aquél número de 32 bit utilizado como ejemplo, adoptamos
como notación :
0D356A5C3h
Cada trozo de información recibe un nombre propio según la cantidad de bits que posea:
 un bit es la unidad de información binaria y con él se puede contar desde 0 hasta 1
 un nibble son cuatro bits y se puede contar desde 0 hasta 15 (0xF en hexa)
 con un byte (8 bits) puedo contar desde 0 hasta 255 ó 0xFF hexa
 una word tiene 16 bits y permite contar desde 0 hasta 65535 ó 0xFFFF
 una double-word (32 bits) permite contar desde 0 hasta 4.294.967.295 ó 0xFFFFFFFF
Cuando usted escuche hablar de direcciones de 32 bits, sepa que hay un espacio de almacenamiento
de 4.294 ... millones de bytes o 4 Gigabytes (o de colores, si estamos hablando de color de 32 bits).
Para finalizar con este tema, aqui hay una tabla que convierte el primer nibble (los primeros 4 bits)
a decimal y a hexa. Usted con ella debe poder convertir cualquier numero binario en hexa y
viceversa:
binario decimal hexa binario decimal hexa
0000 0 0 1000 8 8
0001 1 1 1001 9 9
0010 2 2 1010 10 A
0011 3 3 1011 11 B
0100 4 4 1100 12 C
0101 5 5 1101 13 D
0110 6 6 1110 14 E
0111 7 7 1111 15 F
Operaciones Binarias
En lo que sigue se adopta como convención la lógica positiva, lo que implica:
verdadero = 1 = activo, ------, falso = 0 = inactivo
Hay cinco operaciones binarias básicas: AND, OR, NOT, XOR y ADD. La resta, multiplicación y
división se derivan de estas cinco anteriores. Cualquiera sea la longitud de la palabra o palabras
objeto de la operación, siempre se hace de a un bit por vez de derecha a izquierda (tal como si fuera
una suma o resta con números decimales). Esto permite una definición de cada operación que es
independiente de la longitud del o de los operando(s). La operación NOT es la única que se realiza
sobre un sólo operando (es unaria), y las otras cuatro sobre dos operandos.
o La operación AND (Y) tiene resultado 1 si sus dos operandos son ambos 1
o La operación OR (O) tiene resultado 1 si cualquiera de sus operandos es 1
o La operación XOR tiene resultado 1 si los operandos son distintos (uno en 0 y el otro
en 1)
o La operación NOT (NO) tiene resultado 1 si el operando es 0 y viceversa
o La operación ADD (SUMA) se define igual que con los números decimales
AND OR XOR NOT SUMA
0 * 0 = 0 0 + 0 = 0 0 X 0 = 0 NOT 1 = 0 0 + 0 = 0
0 * 1 = 0 0 + 1 = 1 0 X 1 = 1 NOT 0 = 1 0 + 1 = 1
1 * 0 = 0 1 + 0 = 1 1 X 0 = 1 --- 1 + 0 = 1
1 * 1 = 1 1 + 1 = 1 1 X 1 = 0 --- 1 + 1 = 10
Le extrañó el resultado de la suma? Sin embargo es lo que hacemos en la suma decimal 5+5=10
(nos llevamos "1" para la operación del dígito siguiente). Este llevarse "1" es vastamente usado
entre los procesadores digitales y tiene un nombre especial: carry (lo verá abreviado como CY, C o
CF-por carry flag), lo que en castellano se traduce como "acarreo" (que suena muy mal, asi que le
seguiremos llamando carry). Estas operaciones también se llaman "booleanas" ya que se basan en
el álgebra de Boole (invito al lector a rememorar cuando en la escuela secundaria se preguntaba,
igual que yo, si el álgebra de Boole le serviría alguna vez para algo).
MODULO 2
Cuando termine de leer esta página deberá conocer:
 Modelo de procesador X86
 Modos de direccionamiento
 Modelo de memoria de una PC
 Segmentos
Modelo de procesador X86
Los ancestros del bienamado Pentium III no fueron tan poderoso como él (por las dudas alguien lea
esto allá por el 2005 y le arranque una sonrisa el poder del Pentium III, debo decir que hoy,
mediados de 1999 es el procesador más potente disponible para PCs y acaba de salir a la venta).
Todo comenzó hace dos décadas con un oscuro (aunque revolucionario para la época) 8086, con
registros de 16 bits, que para colmo debió por cuestiones monetarias sufrir un "downsizing" hasta el
ridículo 8088 -motor de las renombradas IBM PC, con las mismas instrucciones pero con un bus de
8 bits.
Cuando hablamos de registros de 16 bits queremos decir que el procesador tiene posiciones de
almacenamiento especiales llamadas registros cuyo ancho de palabra es de 16 bits. Y cuando nos
referimos a bus, término de amplia aplicación queremos decir bus de procesador (no el de la placa
madre, ni el de I/O, ni el de los canales IDE). El procesador tiene dos buses pro uno saca
direcciones y por el otro entra instrucciones o entra y saca datos. En el 8088 el bus de datos era de 8
bits, aunque internamente sus registros manipulaban palabras de 16 bits.
Unos años después apareció el legendario 80386 DX, con arquitectura y bus de 32 bits y su
hermano menor, ese engendro con bus de 16 bits que fue el 386SX tan promocionado por las
revistas de vulgarización tipo PCmierdazine, quién sabe con qué oscuro y comercial designio.
Varios años más adelante quisieron darle auge a otro castrado, el no menos nombrado "celeron", un
Pentium II sin caché L2, que es precisamente aquello que hace muy veloz al original.
Todos estos procesadores (y algunos más como el 486) comparten el mismo juego de instrucciones
básico del 8086, al que cada generación le introdujo mejoras, alguna instrucción más, más registros,
multi-thread, predicción de saltos y hasta un fabuloso número de serie único en el Pentium III con
el que Intel no quiere perdernos pisada y al que puede accederse por instrucciones comunes que
permitirían a cualquier servidor Internet saber qué número de procesador tiene el hacker que se
acaba de conectar y con lo cual se acabaría toda diversión en la red (y toda privacidad!!!!!!!).
Pero tal vez el salto tecnológico más revolucionario lo inició el 80386 al permitir un modo de
funcionamiento con cualidades especiales al que se lo llamó "modo protegido". Debido a las
características de este modo, se podían generar "máquinas virtuales", cada una con su propio
espacio de memoria virtual, al que se acceden a través de vectores de 32 bits ubicados en dos tablas
conocidas como GDT y LDT. Este mecanismo permite que a partir del 386 los procesadores Intel
direccionen una memoria virtual de 64 Terabytes (o sea 16.384 espacios de direccionamiento reales
de 4 GB).
Programas
Todo programa fuente assembly, tienen la forma de una lista de instrucciones, rótulos (labels) y
decisiones parecida al siguiente pseudocódigo:
ORG 100h ;Directiva de Ensamblador
label1:
instrucción 1 ;comentario
label2:
instrucción 2
si (comparación) verdadera ir a label 2
instrucción 3
end ;esta es otra instrucción más
En lenguaje assembly, cada instrucción se compone de un nombre mnemónico que determina el
tipo de operación (por ejemplo MOV, PUSH, etc) y un campo de datos que especifica los
operandos sobre los que dicha operación se debe llevar a cabo.
Una línea de programa assembly tiene a su vez tres campos: el de rótulos (labels), el de la
instrucción y un campo de comentarios que siempre comienza con ";" (punto y coma).
El compilador assembly -llamado Assembler o Ensamblador- traduce las instrucciones en códigos
de operación del procesador según comandos especiales que se llaman Directivas de Ensamblador,
para producir un módulo de programa ejecutable. En los programas ejecutables estos códigos de
operación son valores binarios de uno o más bytes por cada mnemónico. De haber un dato, también
será compilado como binario, que es en definitiva la única base de numeración que pueden
interpretar los procesadores. Sin embargo, cuando un programador escribe un programa mediante
un editor, puede indicarle al compilador si un número es binario, decimal o hexa.
Al correr el programa, el procesador va ejecutando las instrucciones almacenadas en memoria e
incrementando el registro IP secuencialmente. Tanto CS (Code Segment) como IP (Instruction
Pointer) son los registros del procesador que direccionan el código ejecutable . La dirección de la
próxima instrucción a ejecutarse está dada por el vector CS:IP
Cuando se encuentra una instrucción de bifurcación, si se verifica la condición expresada por el
tipo de instrucción, el puntero de instrucciones (IP) cambia con un salto en lugar de incrementarse.
En el ejemplo de pseudo-programa anterior, en la instrucción de comparación el IP tomará el valor
de la dirección "label 2" toda vez que la comparación haya resultado verdadera o incrementará su
valor a la instrucción siguiente (instrucción 3) si la comparación resulta falsa. Hay que tener
presente que cada una de las instrucciones anteriores está almacenada en uno o varios bytes en la
memoria. El valor de label2 es en el caso anterior la dirección en donde esta almacenado el primer
byte de la instrucción 2.
Abramos las ventanas
Inicie usted una sesión DOS, teclee la orden DEBUG y cuando le aparezca el anodino prompt "-",
pulse "r" y enter. Los datos que usted ve desplegarse son los registros básicos y el contenido de los
mismos, del procesador de su PC:
(Notar que Debug supone que la notación es hexadecimal y que los registros son de 16 bits aunque
su PC sea Pentium)
AX=0000 BX=0000 CX=0000 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000
DS=1332 ES=1332 SS=1332 CS=1332 IP=0100 NV UP EI PL NZ NA PO NC
1332:0100 C3 RET .
Esto significa que su Pentium con corazón de 8086 tiene al menos:
o cuatro registros generales: AX, BX, CX y DX
o cuatro registros índices: SP, BP, SI y DI
o cuatro registros de segmento: DS, ES, SS y CS
o un registro que apunta a la próxima instrucción a ejecutar: IP
o un registro de banderas de uso general: F (banderas V, D, I, P, Z, A, S y C)
Todos los registros mencionados son de 16 bits y usted se preguntará ¿no es que a partir del 80386
los registros son de 32 bits?. Y está en lo cierto, los nuevos registros se llaman EAX, EBX, etc...
pero todo a su tiempo. Recuerde que estamos viendo por el momento sólo lo más básico y esto nos
remite al modelo del 8086. En este procesador, a su vez los registros AX, BX, CX y DX pueden
dividirse en dos registros de 8 bits (por ejemplo el AX en AH (bits 8 a 15) y AL (bits 0 a 7).
Cada registro tiene sus funciones específicas (aunque hay muchas que son compartidas):
 AX: Acumulador, principalmente usado para operaciones aritméticas
 BX: Base. Se usa para indicar un desplazamiento (offset) sobre una posición de memoria
 CX: Contador. Se usa para lazos y operaciones repetitivas
 DX: Dato. De uso general
 CS: Segmento de código. Indica el segmento donde residen las instrucciones
 SS: Segmento de Stack. Indica el segmento que utiliza el Stack
 DS y ES: Segmentos Data y Extra, segmentos donde residen los datos
 SP: Puntero de Stack. Indica el offset actual del Stack
 BP: Puntero de base, para operaciones de indexación
 SI: Indice de origen. Offset en segmento de datos de origen
 DI: Indice de destino: Offset en segmento de datos de destino
 F: Flags (hay nueve banderas importantes entre las 16)
Las flags (banderas) a tener en cuenta son:
 C: carry - indica si la operación anterior generó un carry
 Z: zero - indica si en la operación anterior se generó una igualdad
 S: sign - indica si en la operación anterior el resultado fue negativo
 AC: auxiliar carry - indica si hay que hacer un ajuste decimal en AX
 P: parity - indica si la paridad del último resultado fue par
 V: overflow (también simbolizada O)- indica desbordamiento aritmético en AX
 D: direction - indica si los indices SI o DI se incrementan (D=0) o decrementan (D=1)
 I: interrupt enable - indica si se permiten las interrupciones (I=1) o no (I=0)
 T: trap - controla la operación paso a paso del procesador
Asi como la dirección de la próxima instrucción a ejecutarse está apuntada por la pareja CS:IP, hay
un lugar de memoria especial apuntado por SS:SP llamado STACK y utilizado para guardar datos
transitorios, parámetros que se pasan a las funciones y direcciones de retorno de subrutinas o
interrupciones. Se llama stack porque opera como una pila de objetos, en donde el último en
ponerse es el primero en sacarse, mediante instrucciones especialmente diseñadas para eso que se
llaman PUSH y POP. Hay métodos para consultar o escribir otros valores que no son los apuntados
pos SS:SP (lo que equivale a sacar un objeto de la pila sin que se desmorone). El puntero SP está
dando el offset de la última posición de memoria escrita en la zona de stack y como se va llenando
desde posiciones altas hacia las más bajas, la próxima posición libre es la SS:SP-1.
Por ejemplo, supongamos que SP contiene el valor 0FFE4h (más adelante se verá qué papel juegan
los registros de segmento como el SS, en la determinación de la dirección de memoria real) y que
AX contiene el valor 2233h. La instrucción PUSH AX pondrá el valor 22 h (contenido en AH) en
la posición 0FFE3 h (0FFE4 - 1) y el valor 33 h almacenado en AL en la posición de memoria
0FFE2 h y deja SP apuntando al último byte ocupado, vale decir, que SP contendrá el valor
0FFE2h.. La instrucción POP AX realiza la operación inversa.
No es mi intención tratar de suplir un buen manual Intel (que puede bajarse gratis de internet del
sitio www.intel.com) en el que se describe qué es cada registro y cuáles son las instucciones en las
que está involucrado. Un buen sitio en castellano para consultar las instrucciones
es http://udgftp.cencar.udg.mx/tutoriales/TutorialEnsamblador/ensam.html de la Universidad de
Guadalajara, México, en donde además hay un tutorial de Assembly elemental con la fallida
denominación de Assembler.
Las operaciones del procesador se van ejecutando de manera secuencial tal como están
almacenadas las instrucciones en la memoria. Existen instrucciones (saltos) que permiten cambiar
la secuencia de ejecución en forma absoluta o condicionada al resultado de alguna operación
anterior, tal como se dijo de la instrucción de comparación en el ejemplo de pseudo-código antes
visto La instrucción más elemental es MOV, que permite copiar un dato de un origen a un destino.
OPERANDOS
Las instrucciones pueden tener ninguno, uno, dos o tres operandos. A su vez, los operandos pueden
ser inmediatos, registros, memoria o puertos. Un operando inmediato es un dato que viene en el
código del programa, por ejemplo, para cargar el registro AX con el número 20C5h se usa la
instrucción:
MOV AX,20C5h ;20C5h es un operando "inmediato"
;Otras instrucciones MOV pueden ser:
MOV BX,[0400h] ;0400h es una posicion de memoria
MOV DX,[BX] ;[BX] también es una posición de memoria
En el listado anterior, todo lo que hay después del punto y coma ";" son comentarios
extremadamente necesarios en programación Assembly. No es el compilador quien los debe
interpretar sino el propio programador o quien en el futuro deba modificar el programa. El número
entre corchetes indica que el 0400h debe interpretarse como una DIRECCION de memoria (los
corchetes deben leerse como "el contenido de", o sea : en esa operación cargamos el registro BX
con el contenido de la posición de memoria 0400hexa del actual segmento de datos) Este tipo de
referencia a memoria se llama Directo. En cambio, si expresamos [BX], nos estamos refiriendo a la
posición de memoria cuyo offset en el segmento actual de datos es el número contenido en el
registro BX; este tipo de referencia a las posiciones de memoria se denomina Indirecto. Por
ejemplo, supongamos para el código anterior que en la dirección [0400] hay una word cuyo valor
es 1234 h, y en la dirección de memoria [1234] hay una word cuyo valor es 56CCh, luego de
ejecutarse esas instrucciones el registro BX contiene el valor 1234h y el registro DX contiene el
valor 56CCh. En cambio el registro AX es cargado con el número 20C5h y a este direccionamiento
se lo llama Inmediato.
Los operandos pueden ser de 8, 16 o 32 bits, según se desprenda del contexto de la operación o del
otro operando (por ejemplo, en el anterior MOV BX,[0400h], dado que BX es de 16 bits, lo que se
va a mover es un word. Se deben incluír prefijos para especificar la longitud del dato cuando se de
lugar a ambigüedad como por ejemplo en la instrucción INC, en donde si el destino es una posición
de memoria, hay que especificar si es byte o word de la siguiente manera:
INC BYTE PTR [0406] ;incrementar el byte de offset 406h del segmento de datos
Modos de Direccionamiento
El modo de direccionamiento indica la forma en que el procesador calcula da dirección donde irá a
buscar el dato origen o grabará el resultado en el destino, tal como se dejó entrever en el punto
anterior. Existen ocho modos de direccionamiento en los procesadores X86
 Implicito: la misma operación lo indica (p.ej. PUSHA, siempre indica como destino el
Stack)
 Registro: la instrucción menciona el registro (p. ej MOV AL,CH)
 Inmediato: la instrucción proporciona el dato (p. ej. MOV DL,5Fh)
 Directo: la instrucción da la dirección de memoria (p. ej. MOV BX,[0400h])
 Registro-Indirecto: la dirección es el contenido de un registro (p.ej. MOV AX,[BX])
 Relativo a base: dirección = base + constante (p.ej. MOV CX,[BX+6])
 Directo Indexado: dirección = directo + índice (p.ej. MOV DH,[0400h+SI])
 Indexado a base: dirección = directo + base + índice (p.ej. MOV AL,[0400h+BX+SI])
Cada registro de uso general o índice tiene su propio registro de segmento asociado, según la tabla
siguiente:
AX, BX, CX, SI, DI DS
BP, SP SS
DI (instrucciones de strings) ES
IP CS
En instrucciones de strings se opera entre un operador fuente (DS:SI) y otro destino (ES:DI).
Aunque en estas instrucciones se lo vincula al segmento contenido en ES, en toda otra instrucción,
el registro DI está asociado con el registro de segmento DS. A pesar de esto, puede cambiarse esta
asociación default con prefijos de segmento. Por ejemplo, si queremos que el AX se cargue con el
contenido de la dirección de memoria 3C8, pero del segmento apuntado por ES, tenemos que usar:
MOV AX,ES:[3C8] ;cargar AX con el contenido de la dirección ES*10h+3C8h
A continuación veremos como calcular una dirección segmentada del tipo SEG:OFF, en donde
SEG es uno de los cuatro registros de segmento (DS, ES, SS o CS) y OFF es un registro de uso
general o puntero.
Modelo de Memoria de una PC
La capacidad de direccionamiento de un procesador está dada por la cantidad de líneas del bus de
direcciones (o sea el ancho en bits, de la palabra que el procesador es capaz de poner en el bus de
direcciones de la computadora). En un procesador típico de PC, tenemos 32 bits o sea 4 gigabytes
(2 elevado a la potencia 32) de posiciones de memoria distinguibles. Esto constituye el espacio de
direccionamiento real, pero no significa que nuestra PC tiene instalada esa cantidad de RAM, sino
que en caso de estar físicamente instalada, el procesador es capaz de direccionarla. Todo segmento
de programa que se está ejecutando debe residir en memoria real (no sólo el segmento de código
sino también el de datos).
Como Windows es un sistema multitarea, si alguna aplicación pasa a segundo plano, es posible que
en caso de escasez de memoria real, el sistema operativo decida guardar en memoria virtual parte o
toda la memoria real que la aplicación ocupa, y la almacena en el archivo de intercambio (por lo
general este archivo tiene varias decenas de megabytes y es de tipo oculto). Cuando la aplicación
vuelve a primer plano, el procesador al ver que no están en memoria real las recupera del archivo
de intercambio. Incluso si la aplicación es tan grande que excede la memoria real instalada, habrá
partes de ella en memoria física y otras partes en memoria virtual.
El modelo de memoria utilizado en Win32 se basa en dos tablas de vectores, GDT y LDT
apuntadas por registros específicos del procesador. Se llama "modelo de memoria plana" en
oposición con el más antiguo llamado "segmentado" (propio del DOS y Win16). La memoria en
lugar de dividirse en segmentos estancos, se divide en páginas contiguas. El procesador tiene la
posibilidad de detectar si una página no está presente en memoria real y a partir de ahí hay una
serie de procedimientos para recuperarla desde la memoria virtual. Los mecanismos de gestión de
memoria están integrados en el kernel de Windows32 y su explicación cae fuera de los alcances
previstos para este escrito.
Segmentación
Si observamos con atención la pantalla del DEBUG, notaremos los cuatro registros de segmento
denominados DS, ES, SS y CS. Tal como se ha dicho cada registro de segmento tiene la misión
específica de direccionar segmentos de datos, stack y código. Como cuando Intel dio a luz este
esquema de direccionamiento los registros de los procesadores eran de 16 bits y un MB de memoria
era una cantidad fabulosa reservada sólo para los computadores de laboratorio, se decidió que la
forma en que se direccionaría la memoria sería combinando dos segmentos como sigue:
DIRECCION EFECTIVA = 10h * SEGMENTO + OFFSET
tanto "segmento" como "offset" son registros que contienen un vector de 16 bits, y por lo tanto
pueden elegir entre 64 k direcciones distintas. En pocas palabras, elegido el segmento, el
procesador podía direccionar dentro del segmento 64 k posiciones de memoria distintas. Ejemplo
CS= 3701h IP= 0100h
10h*CS = 37010 h
+ IP = 0100 h
D.Eff. = 37110 h
La notación usada para expresar una dirección efectiva (o dirección absoluta) es SEG:OFFS; por
ejemplo la próxima instrucción a ejecutar está en la dirección CS:IP. De lo anterior obtenemos las
siguientes conclusiones:
 Existen 64 k segmentos posibles (los registros de segmento son de 16 bits)
 Con esta notación se pueden expresar direcciones entre 00000 y 10FFEFh, en decimal
1.114.095 (no hasta FFFFFh o 1.048.575 = 1 MB como parecería lógico)
 La alineación es cada 10h bits (la dirección efectiva de comienzo de segmento termina en
0h). 10h bytes se llaman parágrafos. Es común decir que las direcciones efectivas de
comienzo de segmento se alinean en parágrafos (lo cual es obvio, desde que en el comienzo
de segmento el offset es 0)
 Una misma dirección efectiva puede expresarse de muchas maneras usando combinaciones
entre segmento y offset (37110 h = 3701:0100 = 3600:0111, etc)
 Tanto segmento como offset son dos cantidades sin signo (no puede haber un offset
negativo)
MODULO 3
Cuando termine de leer esta página deberá conocer:
Instrucciones básicas del X86
Instrucciones básicas 8086
Este listado no pretende ser un substituto del manual Intel de instrucciones del 8086 -del que
fervientemente recomiendo una minuciosa lectura una vez que haya comprendido bien esto- sino la
más breve descripción posible para poder avanzar un poco más en los aspectos más básicos que se
precisan para comprender el tutorial de lenguaje Assembly de +gthorne. Esta es una lista completa
de instrucciones 8086 a las que sólo le faltan las instrucciones ESC, LOCK y WAIT, que no son
útiles a nuestros fines inmediatos.
En la siguiente tabla se muestran encolumnados los Mnemónicos (como MOV), los operandos
(como fuente, destino) y la descripción de la operación. Los operandos son combinaciones entre
tipos (registro, memoria e inmediato) con los direccionamientos admitidos en cada instrucción. Las
instrucciones IN y OUT admiten un cuarto tipo de operando: puertos de I/O, con direccionamiento
registro o inmediato.
Instrucciones de movimientos de datos
MOV destino,fuente ;la única instrucción que utiliza todos los tipos de direccionamiento
XCHG destino,fuente ;Intercambia los contenidos de destino y fuente
XLAT tabla_fuente ;carga el registro AL con el byte direccionado por (BX+AL)
LAHF ;carga las flags S, Z, A, P y C en AH
SAHF ;guarda AH en el registro de flags
LDS destino,fuente ;transfiere un puntero de 32 bits al registro DS y al registro destino
LES destino,fuente ;transfiere un puntero de 32 bits al registro ES y al registro destino
LEA destino,fuente ;transfiere el offset de fuente (una dirección) a destino (un registro)
PUSH fuente ;guarda fuente en el stack (en la dirección SS:SP)
POP destino ;recupera del stack (dirección SS:SP-1) y guarda en registro destino
PUSHF ;almacena el registro de flags en/desde el stack
POPF ;recupera el registro de flags en/desde el stack
PUSHA ; almacena los reg DI,SI,BP,SP,BX,DX,CX,AX en/desde el stack
POPA ;recupera los reg DI,SI,BP,SP,BX,DX,CX,AX en/desde el stack
IN origen ;carga desde un puerto origen un byte o word en AL o AX
OUT destino ;escribe Al o AX en el puerto destino (direccionam. inmediato o DX)
Las operaciones aritméticas
ADD destino,fuente ;suma fuente + destino y guarda el resultado en destino
ADC destino,fuente ;suma fuente + destino + Carry y guarda el resultado en destino
SUB destino,fuente ;resta destino - fuente y guarda el resultado en destino
SUB destino,fuente ;resta destino - fuente - Carry y guarda el resultado en destino
MUL fuente ;multiplica AL o AX * fuente y guarda el resultado en DX:AX
IMUL fuente ;igual que la anterior pero con numeros enteros con signo
DIV fuente ;divide DX:AX / fuente y guarda cociente en AX y resto en DX
IDIV fuente ;igual que la anterior pero con numeros enteros con signo
AND destino,fuente ;opera destino AND fuente y guarda resultado en destino
OR destino,fuente ;opera destino OR fuente y guarda el resultado en destino
XOR destino,fuente ;opera destino XOR fuente y guarda el resultado en destino
NOT destino ;el NOT cambia todos los 1 en 0 y los 0 en 1 de destino.
NEG destino ;NEG realiza el complemento a 2 de destino
INC destino ;Incremente an 1 el contenido de destino
DEC destino ;Decrementa en 1 el contenido de destino
DAA / DAS ;Efectúa el ajuste decimal en suma / resta del registro AL
AAA/AAD/
AAM/AAS
;ajustan el registro AL a valor decimal desempaquetado (para aplicar en operaciones suma,
resta, multiplicación y división)
Instrucciones de rotación
RCL destino,contador ;rota destino a traves de carry a la izquierda contador veces
RCR destino,contador ;rota destino a traves de carry a la derecha contador veces
ROL destino,contador ;rota destino a la izquierda contador veces
ROR destino,contador ;rota destino a la derecha contador veces
SAL destino,contador ;desplaza destino a la izquierda contador veces y rellena con ceros
SAR destino,contador ;desplaza destino a la derecha contador veces y rellena con bit SF
SHR destino,contador ;desplaza destino a la derecha contador veces y rellena con ceros
NOTAS SOBRE INSTRUCCIONES DE ROTACIÓN
 El bit SF (signo) es el que está más a la izquierda : si destino es operando es de 8 bits
"SF" es el bit número 7 y si destino es un operando de 16 bits, es el bit número 15
 En el procesador 8086 se permite un dato inmediato en lugar de especificar un registro
como contador solo si ese dato inmediato es 1. Por lo tanto, para uno de esos
procesadores la instrucción RCL AX,1 es válida mientras que la RCL AX,5 no lo es.
A partir de 80286 se puede especificar cualquier numero de rotaciones como dato
inmediato. Como DEBUG presupone 8086, cualquier valor inmediato distinto de 1 da
error.
 Si en un programa para 8086 se desean rotar más de un bit a la vez, el valor contador se
carga en CL
 Para rotar un nibble (lo que es muy común en la conversión de binario a BCD) es más
rápido y ocupa menos memoria si se utilizan 4 rotaciones de contador igual a 1 que si
se utiliza el registro CL
 Las instrucciones SAL y SHL son equivalentes
 La flag de Overflow cambia con una logica precisa si la rotación es de una posición. En
caso de rotaciones mayores, OVF queda indefinida.
 En los procesadores 80286 en adelante la rotación se hace MODULO 32, es decir que se
rotará la cantidad de veces igual al resto de la división contador/32, o sea que ROL
AX,33 equivale a ROL AX,1 o ROL AX,65.
 Una rotación con CL=0 equivale a un NOP de dos bytes
Instrucciones de comparación
CMP destino,fuente ;compara fuente y destino. Modifica las flags V, Z, S, C, P y AC
TEST destino,fuente ;AND entre fuente y destino . Ninguno de los operandos cambia.
TEST modifica las mismas flags que CMP pero siempre deja a V = 0 y C = 0.
Instrucciones de strings
CMPS string_destino,string_fuente ;compara las dos cadenas de a bytes o words
CMPSB string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (bytes)
CMPSW string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (words)
LODS string_fuente ;mueve un byte o una word desde fuente a AL o AX
LODSB string_fuente ;origen indicado por DS:SI (mueve un byte a AL)
LODSW string_fuente ;origen indicado por DS:SI (mueve una word a AX)
STOS string_destino ;mueve un byte o una word al destino desde AL o AX
STOSB string_destino ;destino indicado por ES:DI (mueve AL a un byte)
STOSW string_destino ;destino indicado por ES:DI (mueve AX a una word)
MOVS string_destino,string_fuente ;mueve un byte o word de fuente a destino
MOVSB string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (un byte)
MOVSW string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (una word)
SCAS string_destino ;compara la cadena de destino con AL o AX
SCASB string_destino ;destino indicado por ES:DI (compara AL con un byte)
SCASW string_destino ;destino indicado por ES:DI (compara AX con una word)
En todos los casos, si se utiliza el prefijo REP, la cantidad de elementos de la cadena a operar está dada por el contenido del registro
CX, si no es un solo elemento de la cadena. A cada operación, CX es decrementado y SI y DI son incrementados o decrementados de
acuerdo con el estado de la flag de dirección (Si D=0, se incrementan). El incremento o decremento de estos registros se hace de a uno
si son operaciones de bytes o de a dos si son de a words. Para los casos en que se especifica el largo del operando con la B o W final,
la string_destino está apuntada por ES:DI, la string_fuente está apuntada por DS:SI .
Instrucciones de repetición
LOOP offset ;decrementa CX. Si CX no es cero, salta a offset (IP = IP + offset)
LOOPZ offset ;decrementa CX, Si CX <> 0 y Z = 1 , salta a offset (IP = IP + offset)
LOOPNZ offset ;decrementa CX, Si CX <> 0 y Z = 0 , salta a offset (IP = IP + offset)
En todos los casos, si no se produce el salto, se ejecuta la próxima instrucción
REP instrucción ;decrementa CX y repite la siguiente instrucción MOVS o STOS hasta que CX=0
REPZ instrucción ;igual que REP, pero para CMPS y SCAS. Repite si la flag Z queda en 1 (igualdad)
REPNZ instrucción ;igual queREPZ, pero repite si la flag Z queda en 0 (las cadenas son distintas)
Instrucciones de salto
CALL destino ;llama a procedimiento. IP <-- offset de destino y CS <-- segmento de destino
RET valor ;retorna desde un procedimiento (el inverso de CALL), valor es opcional
INT número ;llamado a interrupción. CS:IP <-- vector de INT.Las flags se guardan en el stack
INTO ;llama a la INT 4 si la flag de overflow (V) está en 1 cuando se ejecuta la instrucción
IRET ;retorna de interrupción al programa restaurando flags
JMP dirección ;Salta incondicionalmente al lugar indicado por dirección
JA offset ;salta a IP + offset si las flags C=0 Y Z=0 (salta si primer operando es mayor)
JAE offset ;salta a IP + offset si la flag C=0 (salta si primer operando es mayor o igual)
JB offset ;salta a IP + offset si las flags C=1 (salta si primer operando es menor)(igual a JC)
JBE offset ;salta a IP + offset si las flags C=1 o Z=1 (salta si primer operando es menor o igual)
JZ offset ;salta a IP + offset si las flags Z=1 (salta si primer operando es igual al segundo)(=JE)
JG offset ;salta a IP + offset si las flags S=V Y Z=0 (salta si primer operando es mayor)
JGE offset ;salta a IP + offset si las flags S=V (salta si primer operando es mayor o igual)
JL offset ;salta a IP + offset si las flags S<>V (salta si primer operando es menor)
JLE offset ;salta a IP + offset si las flags S<>V o Z=1(salta si primer operando es menor o igual)
JNC offset ;salta a IP + offset si la flag C=0 (salta si no hay carry)
JNZ offset ;salta a IP + offset si la flag Z=0 (salta si no son iguales o no es cero)
JNO offset ;salta a IP + offset si la flag V=0 (salta si no hay overflow)
JNP offset ;salta a IP + offset si la flag P=0 (salta si no hay paridad -o la paridad es impar =JPO)
JNS offset ;salta a IP + offset si la flag S=0 (salta si no hay hay bit de signo)
JO offset ;salta a IP + offset si la flag V=1 (salta si hay desbordamiento -overflow)
JP offset ;salta a IP + offset si la flag P=1 (salta si la paridad es par ) (=JPE)
JS offset ;salta a IP + offset si la flag S=1 (salta si el signo es negativo)
JCXZ offset ;salta a IP + offset si la flag CX=0 (salta si el registro CX es cero)
Las instrucciones de saltos por Above o Below se refieren entre dos valores sin signo (JA, JAE, JB y JBE), mientras que las Greater y
Less se refieren a la relación entre dos valores con signo (JG, JGE, JL y JLE). .
Instrucciones que afectan flags
CLC/CMC/STC ;pone a cero / complementa / pone en 1 la flag C (carry)
CLD/STD ;pone a cero / uno la flag de dirección (D=0 hace que SI y DI se incrementen)
CLI/STI ;deshabilita / habilita las interrupciones por hardware enmascarables
Instrucciones misceláneas
NOP ;no-operación: el procesador pasa a la instrucción siguiente sin hacer nada
CBW ;convierte el byte de AL en palabra (AX), copiando el bit 7 a todo el registro AH
CWD ;convierte word en double-word, copiando bit 15 de AX a todo el registro DX
HLT ;el procesador se detiene hasta que llegue un Reset o una interrupción por hard.
Alguien puede preguntarse para qué puede servir una instrucción que no hace absolutamente nada
como la NOP. Simplemente para llenar espacio, y es realmente una de las instrucciones más útiles
para un cracker, a punto tal que algunos mecanismos anticracking sofisticados buscan durante la
ejecución de un programa si alguien lo arregló sustituyendo algunas instrucciones por NOPs, y en
caso de detectarlo, abortan la ejecución. Pero esto es tema para más adelante.
MODULO 4
Cuando termine de leer esta página deberá conocer:
 Uso del DEBUG
Posiblemente sea el debug el depurador más rudimentario que existe; pero el hecho que desde el
principio haya sido provisto con el sistema operativo, nos permite encontrarlo hoy en cualquier
máquina DOS o Windows. Muchas tareas elementales pueden realizarse sin otra ayuda que el
Debug y por eso vamos a ver algunos comandos básicos. Incluso es posible correr programas
cargados en memoria utilizando breakpoints elementales, ejecutar paso a paso, saltar sobre
procedimientos, editar programas en hexa y muchas más cosas. Ya hemos dicho cómo podemos
arrancarlo desde una ventana DOS, y usando el comando R (mostrar registros) nos mostrará algo
similar a esto:
AX=0000 BX=0000 CX=0000 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000
DS=1332 ES=1332 SS=1332 CS=1332 IP=0100 NV UP EI PL NZ NA PO NC
1332:0100 C3 RET .
Esto muestra el contenido de los registros del procesador incluyendo varias banderas: en el
ejemplo, y en el mismo orden tenemos: V=0, D=0, I=1, S=0, Z=0, AC=0, P=0 y C=0
Si ponemos después de la R el nombre de un registro, es posible modificar su contenido. Por
ejemplo, para editar el contenido de CX, hay que poner el comando RCX. Debug nos presenta el
contenido actual del registro y la posibilidad de ingresar un nuevo valor para sustituirlo.
Los comandos L y W se utilizan para leer y escribir en archivos de disco. La cantidad de bytes
transferida en cada operación es el contenido de BX:CX. Previamente es necesario darle un nombre
al archivo con el comando N. Se puede especificar la dirección a partir de la que se desea transferir
datos o bien usar el vector por defecto DS:DX.
Los comandos más útiles y más usados en Debug son:
A dirección Ensamblar (ingresar código assembly)
D dirección cantidad Mostrar en pantalla direcciones de memoria en presentación hexa
E dirección Editar memoria desde dirección
F direc1 direc2 valor Llenar memoria desde direc1 hasta direc2 con el dato valor
G dirección Ir (durante la ejecución) a la dirección dirección
H valor1 valor2 Muestra el resultado de la suma y resta hexadecimal entre valor1 valor2
I puerto Obtiene una entrada desde el puerto puerto
M direc1 direc2 direc3 Mueve el bloque de memoria direc1- direc2 a partir de direc3
P cant Salta sobre procedimientos cant de veces o hasta dirección direc
Q Sale de Debug
S direc1 direc2 valores Busca en bloque de memoria desde direc1 hasta direc2 los bytes valores
T cant Igual que P pero son instrucciones simples
U direc cant Desensambla cant bytes a partir de la dirección direc
XS Muestra estado de memoria expandida
? Presenta pantalla de ayuda
Nuestro primer programa
Usaremos el Debug para ensamblar un programa que realice algo tan útil (?) como dejar en alguna
parte de la memoria el nombre de nuestra escuela ECCE. Para sacar algo a pantalla, debemos leer
el tutorial de +gthorne, que será nuestro paso siguiente. Por ahora sólo queremos practicar de
manera que abramos una ventana DOS y escribamos DEBUG (enter). Nos proponemos hacer que
ECCE sea escrito en memoria, en el offset 200h de nuestro segmento de datos DS. Sabemos que los
códigos ASCII son E=45h y C=43h, de manera que nuestro programa puede lucir así:
a 100
1322:0100 mov ax,4543 ;cargamos el registro AX con el dato 4543 (EC en ASCII)
1322:0103 mov bx,4345 ;cargamos BX con "CE" en ASCII
1322:0106 mov [200],ax ;ponemos AX en la dirección de memoria 200
1322:0109 mov [202],bx ;idem para BX, pero en la 202 (AX ocupó la 200 y 201)
1322:010D int 20 ;finalizar y salir a Debug
1322:010F
Al apretar "enter" una vez más, Debug nos devuelve su prompt "-" y ya estamos listos para nuestro
próximo comando. Podemos ver algunas curiosidades del listado anterior: 1) Debug asume que los
números que le damos, sean direcciones o datos, son hexadecimales. 2) A medida que vamos
ingresando el programa, nos va devolviendo la dirección de almacenamiento de la próxima
instrucción que escribiremos. 3) Las tres primeras instrucciones MOV ocuparon de memoria de
programa 3 bytes cada una, pero la cuarta ocupó 4 bytes y la INT 20 sólo ocupó 2 bytes. 4) Aunque
nada se ha hablado de la INT 20, es lo que por el momento usaremos para terminar el programa . 5)
Cuando hacemos referencia al contenido de una posición de memoria, encerramos la dirección
entre corchetes []. Es muy importante saber distinguir entre la dirección y el valor almacenado en
esa dirección de memoria.
Nuestra lógica es muy simple: cargamos el ASCII "EC" en AX y lo dejamos en la dirección 200.
Luego cargamos "CE" en BX y lo dejamos en la 202. Tanto AX como BX han sido meros
vehículos para cargar la memoria con datos y sólo a los efectos didácticos porque también está
permitido :
MOV word ptr [200],4543 ; cargar la word de memoria 200 directamente con el dato 4543
Esta instrucción ocupa 6 bytes, de modo que no ganamos espacio poniéndola en lugar del más
elíptico procedimiento de cargar AX y con éste escribir en 200. El prefijo "word ptr" es para que el
procesador sepa que lo que moveremos a 200 es una word y no un byte o double-word.
Veamos cómo se ve nuestro programa usando el comando desensamblar:
-u 100 (desensamble a partir de la CS:100)
(Nótese que Debug listará usando sólo mayúsculas, sin importar cómo escribimos nuestro código)
1322:0100 B84345 MOV AX,4543
1322:0103 BB4543 MOV BX,4345
1322:0106 A30002 MOV [200],AX
1322:0109 891E0202 MOV [202],BX
1322:010D CD20 INT 20
NOTA: el valor de 1322 (el contenido del registro CS) es válido para la PC donde se escribió este
ensayo. Por lo general los valores no coinciden de una a otra PC, salvo que las instalaciones de
software sean idénticas y en ambas estén corriendo previamente al DEBUG los mismos programas.
El listado es más largo, pero las líneas que siguen hacia abajo son alguna cosa que estaba en
memoria, ya que Debug desensambla por defecto los 20h primeros bytes desde la dirección
indicada (o desde la que esté apuntando), y en nuestro programa sólo hemos usado 0Fh bytes (15 en
decimal). Echémosle un vistazo:
Ajá!!, Debug no deja de sorprendernos, en una columna entre la dirección y el listado en lenguaje
assembly puso unos números hexadecimales. Son los códigos de operación (opcodes) que es lo que
en definitiva se almacena en memoria y lo que nuestro Pentium debe interpretar y ejecutar. Debug
compiló nuestro programa ingresado en assembly y produjo ese código binario con representación
hexadecimal para que el Pentium lo interprete.
Antes de correr el fabuloso programa que hemos escrito, tenemos que ver qué hay en la posición de
memoria 200. Para ello usamos el comando D 200, que nos muestra la basura que hay en nuestra
RAM desde DS:0200 hasta DS:027F. Como deseamos leer claramente nuestro nombre ECCE,
vamos a llenar este espacio con ceros usando el comando
- F 200 23F 00
con lo que le indicamos a Debug que debe llenar el bloque de memoria que comienza en 200 y
termina en 23F con "00". Para estar seguros, escribamos nuevamente el comando D 200. Debemos
ver las cuatro primeras filas del listado con los datos en 00. Estamos listos para correr nuestra
maravilla.
Con el comando R nos aseguramos que CS:IP esté apuntando al inicio de nuestro programa (o sea a
CS:0100). Para nuestro caso CS vale 1322, pero como ya se ha dicho, puede que en otra PC tenga
otro valor. Corramos el programa con el comando G. Debug nos debe informar:
El programa ha finalizado con normalidad.
Bien! todo fue de maravillas. Veamos si nuestras siglas brillan en las posiciones 200 a 203 con el
comando D 200
Esperábamos los hexa 45,43,43,45 a partir de la 200 (miremos además en la columna ASCII del
Debug, en donde claramente nos dice CEEC) y están al revés. Qué habrá pasado? Será que hemos
escrito BX en 200 y AX en 202?. Usemos al Debug para depurar , que para eso Bill Gates lo ha
puesto donde está. Repitamos el comando F 200 23F 00 para dejar nuevamente en cero la memoria
y ejecutemos nuestro programa paso a paso.
Primero el comando R. Nos debe decir que IP apunta a 0100:
1322:0100 B84345 MOV AX,4543
-T (comando para ejecutar una sola instrucción). Lo relevante es:
AX=4543 e IP=0103
1322:0103 BB4543 MOV BX,4345 es la próxima instrucción. Ejecutemos con T:
1322:0106 A30002 MOV [200],AX
Ejecutemos el comando D 200 para ver qué hay en la memoria: hasta ahora 00 de la dirección 200
a la 203. Todo ok, porque hasta aquí sólo hemos cargado los registros AX y BX. Hagamos otro T.
1322:0109 891E0202 MOV [0202],BX es la próxima instrucción
Hemos guardado AX en la dirección 200 y por lo tanto debería haber un 4543 ("EC" en ASCII) en
las direcciones 200 y 201. Verifiquemos con el comando D 200:
1322:0200 43 45 00 00 ........ CE................
QUE PASO???? Está al revés. Tengo "CE" en lugar de "EC". Mmmmm!! Mr Intel tiene algo que
ver con esto: Resulta que lo que leemos en AX como "EC", en la realidad lo debemos asumir como
: En AL tengo un 43 ("C") y en AH un 45 ("E"). Y el procesador hace algo sumamente lógico, a la
porción más baja del registro (AL) la almacena en la dirección de memoria más baja (200) y a la
porción más alta del registro (AH) la almacena en la dirección de memoria más alta (201). Todo
parece bien pero no funciona?
Pero está bien tal como lo hizo Intel. Si leemos la memoria en sentido de direcciones ascendentes,
debemos acostumbrarnos a leer los registro (y a cargarlos, ahí fue donde nos equivocamos!) desde
la porción más baja hacia la más alta. Por lo tanto, debemos rescribir nuestro programa para que en
AL se almacene la primera letra ("E") y en AH la segunda ("C"), y lo mismo para BX:
a 100
1322:0100 MOV AX,4345
1322:0103 MOV BX,4543
(enter) nuevamente para salir del comando A.
Ahora debemos modificar el registro IP, que nos quedó apuntando a la mitad del programa:
RIP (enter) nuestro comando
IP 0109 respuesta de Debug
:100 (enter) este valor lo ingresamos nosotros para decirle que queremos a IP=0100
Ejecutamos el programa nuevamente con G y examinamos la memoria con D 200 para ver nuestro
hermosa sigla ECCE ya en su lugar y en el orden debido.
Acepte este buen consejo: No siga adelante si algo no quedó claro. Reléalo, busque otra fuente,
alguien que le pueda explicar más claro que yo, pero no lea +gthorne sin haber entendido aunque
sea la mecánica con que operan los procesadores. Con el tiempo podrá memorizar los mnemónicos
de las instrucciones, con muy poca práctica puede dominar Debug y sus comando heredados de una
era sombría de las PCs.
INTERRUPCIONES – Conceptos Basicos
1. Una historia vieja como la PC
Hace muchos años, en un país muy lejano, un gigante azul se sintió solo en sus alturas y dijo: "No es bueno que
el programador solo trabaje en su oficina. Hagamos una computadora personal para que también pueda llevarse
el trabajo a su casa". Y así lo hizo. Esa decisión nos puso, amigo deseoso de convertirse en cracker que estas
leyendo esto, en contacto unos 20 años después.
IBM tomó una decisión respecto a la arquitectura de sus computadoras personales destinada a marcar un cambio
notable en la historia de la tecnología. Adoptó una arquitectura abierta, esto es, utilizó componentes que estaban
en el mercado en lugar de fabricar chips propietarios. Al tomar esta resolución, Intel pasó a ser la opción más
clara como proveedor de procesadores y periféricos: por aquél entonces acababa de salir al mercado la línea de
16 bits 8086 y existían muchos periféricos de 8 bits de su predecesor, el 8085, tales como el controlador de
interrupciones 8259, el PPI 8255, DMA 8237, la UART 8251, el timer 8253.
En los procesadores Intel de la línea X86, hay dos tipos de interrupciones: por hardware y por software. En las
primeras, una señal llega a uno de los terminales de un controlador de interrupciones 8259 y éste se lo comunica
al procesador mediante una señal LOW en su pin INT. El procesador interroga al 8259 cuál es la fuente de la
interrupción (hay 8 posibles en un 8259) y este le muestra en el bus de datos un vector que la identifica. Por
instrucciones de programa, se puede instruir al 8086 para que ignore la señal en el pin INT, por lo que estas
interrupciones se denominan "enmascarables". Hay un pin adicional llamado NMI, que se comporta como una
interrupción, pero imposible de bloquear (Non-Maskable-Interrupt).
2. Tipos de interrupciones
Las interrupciones por software se comportan de igual manera que las de hardware pero en lugar de ser
ejecutadas como consecuencia de una señal física, lo hacen con una instrucción.
Hay en total 256 interrupciones, de la 0 a la 7 (excepto la 5) son generadas directamente por el procesador. Las
8 a 0Fh son interrupciones por hardware primitivas de las PC. Desde la AT en adelante, se incorporó un
segundo controlador de interrupciones que funciona en cascada con el primero a través de la interrupción 2 (de
ahí que en la tabla siguiente se la denomine múltiplex). Las 8 interrupciones por hardware adicionales de las AT
se ubican a partir del vector 70h.
Decimal Hexa Generada Descripción
0 0 CPU División por cero
1 1 CPU Single-step
2 2 CPU NMI
3 3 CPU Breakpoint
4 4 CPU Desbordamiento Aritmético
5 5 BIOS Imprimir Pantalla
6 6 CPU Código de operación inválido
7 7 CPU Coprocesador no disponible
8 8 HARD Temporizador del sistema (18,2 ticks por seg)
9 9 HARD Teclado
10 0A HARD Múltiplex
11 0B HARD IRQ3 (normalmente COM2)
12 0C HARD IRQ4 (normalmente COM1)
13 0D HARD IRQ5
14 0E HARD IRQ6
15 0F HARD IRQ7 (normalmente LPT1)
112 70 HARD IRQ8 (reloj de tiempo real)
113 71 HARD IRQ9
114 72 HARD IRQ10
115 73 HARD IRQ11
116 74 HARD IRQ12
117 75 HARD IRQ13 (normalmente coprocesador matemático)
118 76 HARD IRQ14 (normalmente Disco Duro)
119 77 HARD IRQ15
En cuanto a las interrupciones por software, están divididas entre las llamadas por el BIOS (desde la 10h a la
1Fh) y las llamadas por el DOS (desde la 20h hasta la 3Fh). Esto es sólo la versión oficial, ya que en realidad
las interrupciones entre BIOS y DOS se extienden hasta la 7Fh.
3. Cómo funciona una interrupción
A partir del offset 0 del segmento 0 hay una tabla de 256 vectores de interrupción, cada uno de 4 bytes de largo
(lo que significa que la tabla tiene una longitud de 1KB). Cada vector está compuesto por dos partes: offset
(almacenado en la dirección más baja) y segmento (almacenado en la dirección más alta). Cuando se llama a
una interrupción (no importa si es por hardware o por software), el procesador ejecuta las siguientes
operaciones:
1. PUSHF (guarda las banderas en el stack)
2. CTF/DI (borra la bandera de Trap y deshabilita interrupciones)
3. CALL FAR [4 * INT#] (salta a nueva CS:IP, almacenando dirección de retorno en stack)
La expresión 4 * INT# es la forma de calcular la dirección de inicio del vector de interrupción a utilizar en el
salto. Por ejemplo, el vector de la INT21h estará en la dirección 84h Al efectuarse el salto, la palabra
almacenada en la dirección más baja del vector sustituye al contenido del registro IP (que previamente fue
salvado en el stack) y la palabra almacenada en la dirección más alta sustituye al contenido del registro CS
(también salvado en el stack). Por ejemplo:
La instrucción INT 21h es la usada para efectuar llamadas a las funciones del DOS. Supongamos que en la
posición de memoria 0000:0084 está almacenada la palabra 1A40h y en la dirección 0000:0086 está
almacenada la palabra 208Ch. La próxima instrucción que se ejecute es la que está en la posición 20C8:1A40
(nuevo CS:IP).
El final de una rutina de interrupción debe terminarse con la instrucción IRET, que recupera del stack los
valores de CS, IP y Flags.
Notemos que un llamado a interrupción implica el cambio de estado automático de la bandera de
habilitación de interrupciones. En pocas palabras, esto significa que al producirse una interrupción,
esta bandera inhabilita futuras interrupciones. Como la instrucción IRET restablece el registro de
flags al estado anterior que tenia antes de producirse la interrupción, las próximas interrupciones se
habilitan en el mismo momento en que se produce el retorno desde la rutina de servicio.
4. Paso de parámetros desde el programa a la ISR
Cuando las interrupciones son llamadas por software mediante la instrucción INT xx, por lo general
se le deben pasar parámetros a la rutina de servicio de interrupción (ISR). Estos parámetros definen
la tarea que debe cumplir la ISR y son pasados en los registros del procesador, lo que es una opción
muy veloz.
Un ejemplo casi extremo, en donde muchos de los registros del 8086 son utilizados son algunos
servicios cumplidos por la INT 13h (disco). Para tomar sólo un caso, en una operación de escritura
de un sector, los parámetros se pasan de la siguiente manera:
Registro Asignación
AH 03 (servicio de escritura de sectores)
AL cantidad de sectores a escribir
CH 8 bits más bajos del número de cilindro
CL(bits 0-5) número de sector
CL(bits 6 y 7) 2 bits más altos del número de cilindro
DH número de cabeza
DL número de unidad de disco (hard: mayor a 80h)
BX offset del buffer de datos
ES segmento del buffer de datos
Si bien no está escrito en ningún lado, las interrupciones utilizan el registro AH para identificar el
tipo de operación que deben ejecutar. Cuando una interrupción devuelve códigos de error siempre
vienen en el registro AL, AX y/o en la bandera de Carry.
5. La interrupción más famosa
Sin lugar a dudas se trata de la INT 21h (funciones del DOS). El número de función se pasa en el
registro AH
Función Descripción
00h Terminar un programa
01h Entrada de caracteres con salida
02h Salida de un caracter
03h Recepción de un caracter por el puerto serial
04h Envío de un caracter por el puerto serial
05h Salida por puerto paralelo
06h Entrada/salida de caracteres directa
07h Entrada/salida de caracteres directa
08h Entrada de caracteres sin salida
09h Salida de un string de caracteres
0Ah Entrada de un string de caracteres
0Bh Leer estado de una entrada
0Ch Borra buffer de entrada y llama a entrada de caracteres
0Dh Reset de los drivers de bloques
0Eh Selección de unidad actual
0Fh Abrir archivo usando FCBs (File Control Blocks)
10h Cerrar archivo (FCBs)
11h Busca primera entrada de directorio (FCBs)
12h Busca siguiente entrada de directorio (FCBs)
13h Borrar archivo(s) (FCBs)
14h Lectura secuencial (FCBs)
15h Escritura secuencial (FCBs)
16h Crear o vaciar un archivo (FCBs)
17h Renombrar archivos (FCBs)
18h Obsoleta
19h Obtener denominación de dispositivo, unidad actual
1Ah Fijar dirección para DTA (Disk Transfer Area)
1Bh Obtener información sobre unidad actual
1Ch Obtener información sobre una unidad cualquiera
1Dh/1Eh Obsoletos
1Fh Fijar puntero a DPB (Drive Parameter Block) a la unidad actual
20h Obsoleta
21h Lectura random (FCB)
22h Escritura random (FCB)
23h Leer tamaño de archivo (FCB)
24h Establecer número de registro (FCB)
25h Establecer vector de interrupción
26h Crear nuevo PSP (Program Segment Prefix)
27h Lectura random de varios registros (FCB)
28h Escritura random de varios registros (FCB)
29h Transferir nombre de archivo al FCB
2Ah Obtener fecha
2Bh Establecer fecha
2Ch Obtener hora
2Dh Establecer hora
2Eh Fijar bandera de Verify
2Fh Obtener DTA
30h Obtener número de versión del DOS
31h Terminar programa pero dejarlo residente en memoria
32h Obtener puntero a DPB de una unidad específica
33h Leer/escribir bandera de break
34h Obtener dirección de bandera INDOS
35h Leer vector de interrupción
36h Obtener espacio de disco disponible
37h Obtener/fijar signo p/separador de línea de comandos
38h Obtener/fijar formatos específicos de un país
39h Crear subdirectorio
3Ah Borrar subdirectorio
3Bh Fijar directorio actual
3Ch Crear o vaciar archivo (handle)
3Dh Abrir archivo (handle)
3Eh Cerrar archivo (handle)
3Fh Leer desde archivo (handle)
40h Escribir en archivo (handle)
41h Borrar archivo (handle)
42h Mover puntero de archivo (handle)
43h Obtener/fijar atributo de archivo
44h Funciones IOCTL (control de I/O)
45h Duplicar handle
46h Duplicación forzosa de handles
47h Obtener directorio actual
48h Reservar memoria RAM
49h Liberar memoria RAM
4Ah Modificar tamaño de memoria reservada
4Bh EXEC: ejecutar o cargar programas
4Ch Terminar programa con valor de salida
4Dh Obtener valor de salida de un programa
4Eh Buscar primera entrada en el directorio (handle)
4Fh Buscar siguiente entrada en el directorio (handle)
50h Establecer PSP activo
51h Obtener PSP activo
52h Obtener puntero al DOS-info-block
53h Traducir Bios Parameter Block a Drive Parameter Block
54h Leer bandera Verify
55h Crear PSP nuevo
56h Renombrar o mover archivo
57h Obtener/fijar fecha y hora de modificación de archivo
58h Leer/fijar estrategia de alocación de memoria
59h Obtener informaciones de error ampliadas
5Ah Crear archivo temporal (handles)
5Bh Crear archivo nuevo (handles)
5Ch Proteger parte de un archivo contra accesos
5Dh Funciones de Share.exe
5Eh Obtener nombres de dispositivos de red
5Fh Obtener/fijar/borrar entrada de la lista de red
60h Ampliar nombre de archivo
61h No usada
62h Obtener dirección del PSP
63h/64h No usadas
65h Obtener información ampliada de pais específico
66h Obtener/fijar página de códigos actual
67h Determinar el número de handles disponibles
68h Vaciar buffer de archivos
69/6A/6B No usadas
6Ch Función Open ampliada
6. Intercepción de interrupciones (hooks)
Un programa puede necesitar "enganchar" una interrupción. Supongamos que hemos creado un
virus que debe autodestruir su copia en memoria cuando el comando a ejecutar es "scan.exe".
Evidentemente debemos interceptar la interrupción 21h, función 4Bh/00 (cargar un programa y
ejecutarlo), de tal manera que "nuestra" función verifique si el programa a cargar se llama scan.exe
y en tal caso, borre lo que haya que borrar.
Esta tarea, se logra en haciendo un programa residente (que puede ser parte del mismo código del
virus) para que
1. Cuando se produzca una llamada a la INT21h-4Bh, no se ejecute el código normal del DOS
sino nuestro código
2. En él chequearemos si la función es una 4Bh-00, y en caso afirmativo verificamos si el
programa a corres se llama scan.exe. Si todo esto es verdadero, sobrescribiremos las partes
sensibles a la detección del virus y lo descargaremos de la memoria.
3. Finalmente saltamos a la verdadera INT21h función 4Bh
Para lograr esto, es necesario contar con un loader que cargue en memoria nuestro programa. Este
loader debe:
1. Reservar un espacio de memoria adecuado al tamaño del código que quedará residente.
2. Averiguar (mediante INT21h-35h) cual es el vector de interrupción de la INT21h.
Supongamos que sea 0102:2C40h
3. Poner este vector como dirección de retorno del código residente (por lo general cargándolo
en una dirección conocida en donde tiene que estar este valor)
4. Cambiar el vector 4Bh origina por la dirección de inicio de nuestro código residente
(digamos 7E00:0000)
Lo que sucederá cuando la PC infectada con nuestro virus intente ejecutar un scan.exe es lo
siguiente:
1. Dentro del Command.com, se generará un llamado a la INT21-4Bh-00 con scan.exe como
parámetro.
2. El procesador buscará el vector para el servicio a la interrupción 21h en la dirección
0000:0084h
3. En ese lugar estará la dirección de inicio de nuestro residente, o sea 7E00:0000, y en ese
lugar se inicia el procesamiento de la interrupción.
4. Al ver que la llamada es para ejecutar un programa scan.exe nuestro residente vuelve a poner
el vector de INT21h en el valor que le dio el DOS y luego se autodestruye (primero traslada
a la parte más baja de la memoria la función de borrado). Como último acto, hace un salto
JMP FAR 0102:2C40
5. Esto último hará que se ejecute scan.exe como si nada hubiese sucedido.
Frecuentemente los virus utilizan interrupciones en desuso para sus fines (por ejemplo para saber si
están activos en memoria).
El tema de las interrupciones es tan inmenso que lo que acabamos de ver no es sino un pequeño
pantallazo. Quedan cuestiones muy delicadas como la bandera INDOS y las formas de evitar la
reentrada. Una descripción muy completa de cada interrupción, que incluye los registros usados
para el paso de parámetros, está en el archivo intdos.zip, por Ralph Brown (en inglés) que pueden
bajarse de sudden dischargeo asmcoder , dos sitios que les recomiendo si se buscan tutoriales o
archivos.
MANEJO DE STRINGS EN BIOS, DOS, y WINDOWS
1.- Función BIOS para manejo de strings
El BIOS interactúa principalmente de a un caracter por vez con el teclado, pantalla y puerto serial,
por lo que a estos se los conoce como dispositivos de caracteres, en contraposición con el drive de
diskettes o el disco duro, que son dispositivos de bloques. Aunque menos frecuente que las
funciones de manejo de strings del DOS, la función 13h de la INT10h tiene la ventaja que no
depende del sistema operativo. Su función es visualizar en la pantalla una cadena de caracteres que
deben estar almacenados en un buffer (en memoria).
El paso de parámetros se realiza mediante los siguientes registros:
Registro Parámetro
AH 13 h - define la operación
AL
Modo de salida:
0 Atributo en BL, mantiene la posición del cursor
1 Atributo en BL, actualiza la posición del cursor
2 Atributo en buffer, mantiene posición del cursor
3 Atributo en buffer, actualiza posición del cursor
BL Atributo de caracteres (solo modos 0 y 1)
CX Cantidad de caracteres a visualizar
DH Línea de la pantalla
DL Columna de la pantalla
BH Página de pantalla
ES:BX Puntero al buffer de memoria
Los modos que actualizan la posición del cursor se usan cuando se quieren escribir varios strings
uno a continuación del otro. En cambio los modos que la mantienen, se utilizan para escribir
mensajes siempre en el mismo lugar de la pantalla.
En los modos 0 y 1 todos los caracteres tienen el atributo especificado en BL, mientras que en los
modos 2 y 3 en el buffer, seguido a cada caracter esta su byte de atributo, lo que permite que cada
caracter tenga un atributo distinto. La cadena en memoria tiene una longitud igual al doble de los
caracteres a visualizar. El valor de CX debe ser no obstante igual a la cantidad de caracteres (la
mitad del tamaño del buffer). El byte de atributos tiene la siguiente estructura:
Bit # Función
7 Intermitencia (1=intermitente, 0=fijo)
6,5,4 Color de fondo (0=negro, 7=blanco)
3,2,1,0 Color del caracter (0=negro, 0Fh=blanco)
2. Funciones DOS de manejo de strings
2.1 Entrada de strings de caracteres: INT21h - función 0Ah
Se leen caracteres desde la entrada standard (normalmente teclado) y se transfieren a un búffer en
memoria. La operación termina cuando se lee el caracter ASCII 0Dh (CR o retorno de carro), que
corresponde a la tecla RETURN (o ENTER).
Registro Parámetro
AH 0Ah - código de la función
DS:DX Puntero al buffer de memoria
Estructura del Buffer:
Posición Significado
DS:DX
Cantidad máxima de caracteres admitida en el buffer (debe ser
inicializada por el programador)
DS:DX + 1 Cantidad de caracteres leída (la escribe el DOS)
DS:DX + 2 y
subsig.
Buffer donde se almacenan los caracteres leídos. La dirección del último
es DS:DX + byte ptr (DS:DX)
En los dos primeros bytes, la cantidad de caracteres incluye al CR final. Suponiendo que el
programador inicialice la posición de memoria DS:DX en 10h, el buffer tendrá un largo total de 16
caracteres, comenzando en DS:DX y finalizando en DS:DX + 0Fh, y podrá aceptar 13 caracteres
más el de retorno.
DOS no se preocupa por borrar la parte del buffer que no escribe. Veamos en la tabla siguiente,
para un buffer de 16 de largo qué caracteres encontramos luego de dos entradas sucesivas, la
segunda más corta que la primera:
Dirección DS:DX + ... 0 1 2 3 4 5 6 7 8 9 A B C D E F
primera entrada 10 0f B u e n o s A i r e s 0d ?
segunda entrada 100a C o r d o b a 0d i r e s 0d ?
Hay que notar que si bien esta función es muy cómoda, se queda esperando el caracter de retorno y
hasta que este no llegue el programa no puede hacer otra cosa que... esperar!. En cambio, si se
busca de a un caracter por vez, es posible hacer que el programa consulte el teclado como una de
las tantas actividades posibles dentro de un mismo lazo.
2.2 Salida de string de caracteres, INT 21h - función 9h
Con esta función se envía un string de caracteres al dispositivo designado como salida standard
(normalmente la pantalla). DOS permite redireccionar la salida a un archivo o a un puerto serial o
LPT desde la misma línea de comandos, por lo que al usar esta función no hay garantías de que el
string aparezca en pantalla. En realidad, esto también es válido para la entrada de caracteres vista
en el punto anterior, aunque es mucho más frecuente redireccionar la salida que la entrada. Por
ejemplo, el comando interno type archivo hará que el contenido del archivo sea visualizado en la
pantalla, pero si agregamos un redirector con un dispositivo de salida "> LPT1", los caracteres del
archivo serán direccionados al puerto de la impresora.
El string debe finalizar obligatoriamente con el caracter "$" (código ASCII 36). Los caracteres
especiales como Bell, Backspace, CR, etc serán tratados como tales. Bell (ASCII 07) hace sonar
una campana en el altavoz de la PC, CR vuelve al principio de la línea, Nueva_línea (ASCII 0Ah)
pasa a la línea de abajo, etc
Al igual que en la lectura de strings, los parámetros son:
Registro Parámetro
AH 09h - código de la función
DS:DX Puntero al buffer de memoria donde reside el string
En lenguaje Assembly, un string para usarse con esta función puede ser declarado como sigue:
mensaje1 DB "Todos los hombres de buena voluntad",0Dh,0Ah,"$"
y para utilizar la función 9h, el código a emplear sería:
display: MOV DX, offset mensaje1
MOV AH,9
INT 21H
RET
3. Funciones Windows de manejo de strings
3.1 CompareStrings
Esta función compara dos strings de caracteres usando como base el juego de caracteres del idioma
especificado por el identificador. La sintaxis del llamado es:
int CompareString(
LCID Locale, identificador de lenguaje del sistema
DWORD dwCmpFlags, opciones de comparación
LPCTSTR lpString1, puntero al primer string
int cchCount1, tamaño (bytes) del primer string
LPCTSTR lpString2, puntero al segundo string
int cchCount2 tamaño (bytes) del segundo string
);
3.2 GetDlgItemText
La función GetDlgItemText captura el titulo o texto asociando con un control en una caja de
diálogo. La sintaxis es:
UINT GetDlgItemText(
HWND hDlg, handle de la caja de diálogo
int nIDDlgItem, identificador del control
LPTSTR lpString, dirección del buffer para el texto
int nMaxCount máxima longitud del string
);
3.3 GetWindowText
La función GetWindowText copia el texto de una barra de título de una ventana especificada en un
buffer. Si la ventana especificada es un control, lo que se copia es el texto del control. Sintaxis:
int GetWindowText(
HWND hWnd, handle de la ventana o control
LPTSTR lpString, dirección del buffer de texto
int nMaxCount máximo número de caracteres a copiar
);
3.4 GetWindowTextLength
La función GetWindowTextLength obtiene la cantidad de caracteres que tiene el texto de la barra
de título de una ventana o (si la ventana especificada es un control), la cantidad de caracteres dentro
del control. La sintaxis es:
int GetWindowTextLength(
HWND hWnd handle de la ventana o control
);
3.5 lstrcat
La función lstrcat adiciona un strin a continuación de otro. Sintaxis:
LPTSTR lstrcat(
LPTSTR lpString1, dirección del buffer de strings concatenados
LPCTSTR lpString2 dirección del string a concatenar con string1
);
3.6 lstrcmp y lstrcmpi
La función lstrcmp compara dos strings de caracteres. La comparación discrimina entre mayúsculas
y minúsculas. La función lstrcmpi es idéntica pero no discrimina mayúsculas y minúsculas.-
Sintaxis:
int lstrcmp( // int lstrcmpi(
LPCTSTR lpString1, dirección del primer string
LPCTSTR lpString2 dirección del segundo string
);
3.7 lstrcpy
La función lstrcpy copia un string en un buffer. Sintaxis:
LPTSTR lstrcpy(
LPTSTR lpString1, dirección del buffer
LPCTSTR lpString2 dirección del string a copiar
);
3.8 lstrcpyn
La función lstrcpyn copia un número especificado de caracteres de un string dentro de un buffer.
LPTSTR lstrcpyn(
LPTSTR lpString1, dirección del buffer
LPCTSTR lpString2, dirección del string a copiar
int iMaxLength cantidad de caracteres o bytes a copiar
);
3.9 lstrlen
La función lstrlen devuelve la longitud en bytes (versión ANSI) o caracteres (versión Unicode) del
string especificado (no incluye el caracter NULL de terminación).
int lstrlen(
LPCTSTR lpString dirección del string
);
3.10 MultiByteToWideChar
La función MultiByteToWideChar despliega un string de caracteres en un string Unicode.
int MultiByteToWideChar(
UINT CodePage, código de página
DWORD dwFlags, opciones tipo de caracteres
LPCSTR lpMultiByteStr, dirección del string a mapear
int cchMultiByte, número de caracteres en el string
LPWSTR lpWideCharStr, dirección del buffer Unicode
int cchWideChar tamaño del buffer
);
3.11 SetDlgItemText
La función SetDlgItemText determina el texto de un control en un box de diálogo. Sintaxis:
BOOL SetDlgItemText(
HWND hDlg, handle del box de diálogo
int nIDDlgItem, identificador del control
LPCTSTR lpString puntero al texto
);
3.12 SetWindowText
La función SetWindowText cambia el texto en la barra de título de una ventana. Si la ventana es un
control, se cambia el texto del control. Sintaxis:
BOOL SetWindowText(
HWND hWnd, handle de la ventana o del control
LPCTSTR lpString dirección del string
);
PASO DE PARAMETROS EN LOS PROGRAMAS
Parte I: COMO PASAN LOS PARAMETROS A LAS INTERRUPCIONES BIOS Y DOS
PASO DE PARAMETROS
Es posible que una de las partes más tardíamente comprendidas por el principiante de ingeniería
inversa es la manera en que pasan los parámetros desde el programa a una función. Este concepto
es de fundamental importancia en el estudio de las protecciones y podemos decir sin lugar a dudas
que la comprensión de este mecanismo es crucial para el análisis del funcionamiento de un
programa DOS o Windows.
LA ANTIGUA HISTORIA DEL DOS
El viejo DOS en lugar de funciones API utilizaba interrupciones de software (INT 21h y
subsiguientes), y un poco más próximo al hardware, el mismo BIOS cuenta con su propio juego de
interrupciones. Estas interrupciones de software funcionan igual que cualquier llamada a función,
aunque el mecanismo de llamada es distinto, ya que se usa la instrucción INT en lugar de CALL.
Por lo general, tanto el DOS como el BIOS pasaban los argumentos en los registros del mismo
procesador. Si bien es una estrategia que optimiza la velocidad de procesamiento, tiene sus
limitaciones en cuanto a la cantidad de parámetros que se pueden pasar. Otro de los problemas que
tiene es que las funciones no pueden ser reentrantes a menos que se tomen previsiones
excepcionales, aunque esto no era de mucha importancia ya que el DOS no es multitarea, sería sólo
problema para programas residentes.
Por lo general se pasaban los parámetros por valor. Por ejemplo, en una interrupción de BIOS de
lectura de un sector de disco a memoria (INT 13, subfunción 02) tenemos:
reg var significado
AH 2 subfunción 2: lectura de un sector
AL n cantidad de sectores a leer
CH c0 8 bits más bajos del número de cilindro (track) a leer
CL s numero de sector a leer (bits 0 a 5)
CL c1 2 bits más altos del número de cilindro (bits 6 y 7)
DH h número de cabeza lectora
DL d número de disco lógico (bit 7 en 1 para discos duros)
ES:BX ba dirección de inicio del buffer de lectura en memoria
A menos que se trate de aplicaciones muy especiales en que estos valores pueden ser fijos, lo usual
es que cada uno de esos parámetros sea una variable que a su vez está almacenada en algún lugar
de la memoria. En el siguiente listado que sigue estos parámetros son referidos con nombres
simbólicos supuestos y el lector debe tener presente que en el listado de lenguaje de máquina lo que
se verán son las direcciones de almacenamiento de estos parámetros. Veremos cómo sería una
llamada a la interrupción que lea 4 sectores consecutivos del disco C, ubicados en la pista 801
(0321h), cabeza 3, a partir del sector 12 (0Ch), y que almacene lo leído en la dirección DS:0700.
El registro CX en binario debe ser: 0010 0001 11 001100 = 21CC h
Los bits 15 a 8 deben ser 21h (ocho bits menos significativos del número de track), los bits 7 y 6
ambos en uno (el 3 del número de track) y los bits 3 y 2 también en uno por en número de sector.
En algún lugar del programa se produce la carga de los valores iniciales:
PUSH DS ;haremos que los datos se escriban
POP AX ;en el segmento de datos DS
MOV segme,AX ;almacenamos en la variable segme
MOV AX,0700 ;en el offset 0700h
MOV offse,AX ;almacenamos en la variable offse
MOV AX,0380 ;disco C (80h), cabeza 3
Y luego se cargan los registros desde la memoria antes de llamar a la int 13h
MOV curdisk,AL ;almacena 80 en variable curdisk
MOV curhead,AH ;almacena 03 en variable curhead
MOV AX,21CC ;número de track y sector
MOV track0,AH
MOV sekt,AL
MOV AL,4 ;número de sectores a leer
CALL _leedisk ;leer
JC _error ;si CY vuelve en 1, hubo error de lectura
... ...
_leedisk: ;lectura de disco
... ...
MOV DH,AL ;salvar cantidad de sectores a leer
MOV AX,segme ;cargar segmento
MOV ES,AX
MOV BX,offse ;cargar offset de buffer
MOV CL,sekt s;ector y 2 bits más altos de track
MOV CH,track ;cargar track
MOV DL,curdisk ;unidad de disco a utilizar
MOV AL,curhead ;numero de cabeza
XCHG DH,AL cambiar número de sectores y cabeza
MOV AH,2 ;subfunción de lectura
INT 13 ;interrupción 13h BIOS disco
RET
Uno puede preguntarse cuál es el objeto de poner los parámetros en memoria en lugar de cargarlos
directamente en los registros apropiados para la llamada a la INT 13h. Es una cuestión de
practicidad y buen estilo de programación. Si las variables están en memoria, el programa puede
consultarlas en cualquier momento o modificarlas por ejemplo para hacer un lazo. Si se cargan
como constantes, tal como sucede en la primera parte de la rutina, en donde se inicializan las
variables, servirán solamente para efectuar esa llamada. Por ejemplo, si después de esa primera
lectura quisieramos leer los sectores 1 a 7 del mismo track, sólo habría que poner:
MOV AL,sekt ;nuevo sector inicial
AND AL,C0h ;dejamos solo los dos bits del track (6 y 7)
OR AL,1 ;ponemos en 1 el numero de sector
MOV sekt,AL ;guardamos nuevamente
MOV AL,7 ;numero de sectores a leer
CALL _leedisk
NOTA IMPORTANTE
Un lector de nivel intermedio podría objetar que es posible tratar parte del código como si fuesen variables y
de tal modo ahorrarnos un paso, dejando sólo la carga inmediata de registros. El programa se vería así
(incluímos ahora una columna para las direcciones del código por razones obvias)
CS:1000 MOV DL,80 ;código del disco duro, unidad C
CS:1002 MOV AL,4 ;leer cuatro sectores
CS:1006 etc etc
Si por ejemplo quisiesemos leer 2 sectores y cambiar la unidad C por la A, habría que poner:
XOR AL,AL ;poner a cero AL (unidad A)
MOV [CS:1001],AL ;cambia la carga de DL
MOV AL,2 ;numero de sectores
MOV [CS:1003],AL ;cambia carga de AL
CALL _leedisk
En algunas oportunidades se hace, es una técnica conocida como automodificación, pero no lo recomiendo
para principiantes. Por cierto que en lugar de poner la dirección absoluta como se hizo ahora en beneficio de la
claridad, es posible utilizar variables del compilador (que se traducen en constantes iguales a CS:1001 y
CS:1003 para el programa)
El lector puede encontrar en Internet la completa y muy extensa lista de llamadas a interrupción de
Ralph Brown (por ejemplo en el sitio sudden discharge ), unas 250 páginas tamaño oficio en letra
condensada a dos columnas en donde se incluyen hasta interrupciones propias de virus. Mi mejor
consejo si tiene que trabajar con programas DOS es que la consiga y la imprima (y a menos que su
vista sea excelente, no la imprima en condensada aunque le lleve el dobe de papel). Hay una
versión mucho más condensada y menos exhaustiva que viene para instalar residente, atribuida a
Peter Norton y que puede ser suficiente si los programas acceden a las interrupciones más comunes
(por ejemplo, no están ni las que se utilizan para redes ni las de los DOS-extenders). Trate de
bajarla de nuestro sitio usando este vínculo.
EN BUSCA DE ALTERNATIVAS
Un poco agotado en las complejidades crecientes, el modo de paso de parámetros mediante
registros iba a quedar acotado a rutinas del núcleo de sistemas operativos en donde la velocidad es
un factor de gran importancia. Había que buscar alternativas para mejorar la manera en que los
parámetros son pasados a las funciones. Consideraremos ahora tres temas íntimamente relacionados
con el paso de parámetros.
* Paso de valor versus uso de punteros
* Cómo opera el stack
* Estructuras de datos
1) Paso de argumentos por punteros.
En el punto anterior se vio con profundidad el paso de argumentos por valor, es decir, se le entrega
a la función convocada el VALOR con el que tendrá que operar. Dentro de las llamadas a
interrupcion más comunes, esto es algo inevitable porque los valores pasan en los mismos registros
del procesador. Sin embargo cuando se estructuró el ejemplo sobre la lectura de sectores de disco,
se hizo un pequeño avance: se colocaban los valores en direcciones de memoria y luego la rutina
los recuperaba antes de convocar a la interrupción 13h.
El paso de argumentos mediante punteros consiste en una técnica similar, en donde a la función
convocada se le dice en qué dirección están los valores con los que tiene que operar. Esta es la
manera en que trabajan los compiladores C y Pascal por ejemplo. Supongamos que queremos
sumar 7 y 11. En pseudo lenguaje C no sería correcto poner:
A = Suma (7,11)
que sería más propio de Basic, sino:
int A,B=7,C=11;
A= Suma(B,C);
printf A;
Se declaran tres enteros, definiendose el valor de dos de ellos, se llama a una función Suma(x,y)
que debe estar definida en otra parte del programa, que usa dos argumentos de entrada y devuelve
un entero. Finalmente se imprime el entero resultante. Esto corresponde más o menos con el
siguiente listado en lenguaje Assembly:
varA DW
varB DW 7
varC DW 11
.... ....
LEA AX, varA
PUSH AX
LEA AX, varB
PUSH AX
LEA AX, varC
PUSH AX
CALL _add
CALL _printAx
Lo que en realidad se le está entregando a la función _add son tres valores en el stack que no son
los que tiene que sumar, sino las direcciones en donde estan almacenados los datos de entrada y la
dirección donde debe almacenar el resultado. Consulte en el punto siguiente cómo opera el stack.
El presente ejemplo será resuelto con valores numéricos para que se aprecie bien la diferencia entre
puntero y valor.
Los valores de las direcciones de los operandos se denominan punteros (porque su valor está
"apuntando" al lugar donde está almacenado el dato). Entre otras cosas, esto implica que mientras
se está procesando una función tal como _add(x,y), otra tarea puede estar modificando los valores
contenidos en las direcciones apuntadas por x e y, lo cual no siempre es deseable.
2) Cómo opera el stack
El stack es un espacio particular de la memoria del sistema. Al stack se lo llama pila LIFO (Last In-
First Out, el último en entrar, el primero en salir) y es igual a tener una pila de diskettes: si quiero
sacar alguno, lo más sencillo es quitar primero todos los de arriba. El funcionamiento del stack se
rije por el par de registros SS:ESP (Stack Segment : Extended Stack Pointer), que apunta a la
última dirección ocupada por el stack.
El puntero al stack se decrementa a medida que el stack se va llenando (porque a medida que crece
el stack va ocupando posiciones de memoria cada vez más bajas) e inversamente el puntero crece a
medida que el stack se vacía. La instrucción para cargar al stack con parámetros es PUSH, mientras
que su inversa es POP. Al producirse una interrupción o un llamado a subrutina, se coloca
automáticamente la dirección de retorno (y en ocasiones las flags) en el stack, las que se restauran
con la instrucción POP.
Supongamos que en el momento antes de una operación PUSH AX, que pone el contenido de AX
en el stack, el par SS:SP apunta a 1800:FFEE, y que el contenido de AX es 1234h. Luego del
PUSH, la dirección de memoria 1800:FFED contendrá el valor de AH, o sea 12h, la dirección
inmediata inferior 1800:FFEC contendrá el valor de AL, o sea 34h y el puntero SS:SP tendrá igual
valor (1800:FFEC).
varA DW ;direccion de almacenamiento: DS:2000
varB DW 7 ;direccion de almacenamiento: DS:2002
varC DW 11 ;direccion de almacenamiento: DS:2004
A partir de la DS:2000 encontramos (se lista hasta la DS:2007):
DS:2000 00 00 07 00 11 00 xx xx
Supongamos que SS:SP vale SS:FF2E,
LEA AX, varA ;carga en AX la direccion 2000
PUSH AX ;carga en SS:FF2C el valor 20 00
LEA AX, varB ;carga en AX la direccion 2002
PUSH AX ;carga en SS:FF2A el valor 20 02
LEA AX, varC ;canga en AX la direccion 2004
PUSH AX ;carga en SS:FF28 el valor 20 04
El stack pointer ahora esta en FF28. Listemos desde SS:FF28 hasta FF2F:
SS:FF28 04 20 02 20 00 20 xx xx
Notemos que la carga en el stack sigue la convención Intel, poniendo el byte menos significativo en
la dirección más baja y el más significativo en la dirección más alta.
3) Estructuras de datos.
Con todo, hay veces en las que conviene no hacer referencia a variables aisladas sino manejarlas en
grupo, lo que se denomina estructura. Una estructura de datos se compone de miembros los que
pueden ser de distinta longitud o naturaleza. Cuando el programa se refiera a la estructura lo hará
usando un puntero a la estructura que no es nada más que la dirección de memoria donde comienza.
Veamos un ejemplo simple.
En un programa encontramos que hacemos constante referencia a la lectura de disco, y por lo tanto
decidimos crear nuestra propia estructura para facilitar la escritura del programa. Notemos que los
sistemas operativos tienen definidas estructuras para usos específicos. Utilizando lenguaje
Assembly, una estructura ejemplo se puede definir como:
lectura STRUCT
disco db 0 numero de disco
sector db 1 numero de sector
head db 0 numero de cabeza
reser db 0 reservado
track dw 0000 numero de track
cant dw 0001 cantidad de sectores a leer
bufseg dw 2000 segmento del buffer de lectura
bufoff dw 0000 offset del buffer de lectura
lectura ENDS
En las estructuras de datos propias, podemos usar nuestra inmaginación con total libertad, pero las
estructuras que necesita el sistema operativo debemos ajustarnos completamente a las posiciones y
longitud de los parámetros y disponerlos de la misma manera en que el sistema operativo espera
encontrarlos. Hemos reservado un byte para futuros usos y para que los valores de dos bytes se
alinien con direcciones pares de memoria. Si por ejemplo el valor del puntero "lectura" fuese 2800,
encontraríamos que:
la dirección DS:2800 almacena el número de disco
la dirección DS:2801 almacena el número de sector
la dirección DS:2802 almacena el número de cabeza
la dirección DS:2803 es un byte reservado para uso futuro
la dirección DS:2804 almacena en dos bytes el numero de track
la dirección DS:2806 almacena en dos bytes la cantidad de sectores leer
la dirección DS:2808 almacena el segmento del buffer de lectura
la dirección DS:280A almacena el offset del buffer de lectura.
Si dentro del programa queremos hacer referencia por ejempo a la cabeza lectora, podemos poner:
MOV lectura.head,5 seleccionar la cabeza lectora 5
El compilador buscará la dirección de la estructura "lectura", en nuestro ejemplo DS:2800. Luego
buscará el elemento "head", que por la definición de la estructura sabe que ocupa un byte y que es
el tercero. Por lo tanto el compilador generará una instrucción apropiada para que se almacene el
valor 5 en la dirección de memoria DS:2802.
PASO DE PARAMETROS EN LOS PROGRAMAS
Parte II: COMO PASAN LOS PARAMETROS A LAS FUNCIONES API DE WINDOWS
PARAMETROS PARA FUNCIONES API
Windows sigue las nuevas reglas sobre paso de parámetros tal como se ha visto en el módulo
anterior: pasa punteros en el stack y también hace uso de estructuras cuando esto resulte adecuado.
Para cualquier función API, los parámetros se almacenan en el stack en el orden inverso al que
figuran en la declaración. Igualmente, cualquier valor de retorno vendrá en el registro EAX si se
trata de un entero (si es mayor, será un puntero a una cadena o a una estructura). Tomemos un
ejemplo del API Help de Microsoft o del muy ágil y condensado similar elaborado por Sync+, por
ejemplo la función _lwrite:
definición de función API _lwrite extraída de las API Help
The _lwrite function writes data to the specified file. This function is provided for compatibility with 16-bit
versions of Windows. Win32-based applications should use the WriteFile function.
UINT _lwrite(
HFILE hFile, // handle to file
LPCSTR lpBuffer, // pointer to buffer for data to be written
UINT uBytes // number of bytes to write
);
OK, qué hay que hacer para llamar esta función? Supongamos que tengo abierto previamente un
archivo cuyo handle es 3CCh, en el cual quiero escribir 1000h bytes desde el buffer que está en la
dirección 40023300h
MOV EAX,1000 ;cantidad de bytes a escribir
PUSH EAX
LEA EAX,lpBuffer ;DIRECCION del buffer de escritura
PUSH EAX
MOV EAX,EBX ;normalmente el handle se guarda en EBX
PUSH EAX ;si se acaba de abrir el archivo
CALL _lwrite ;debe estar en memoria el Kernel32.dll
CMP EAX,1000 ver si se transfirierorn todos los bytes
JNZ _error
Nótese que si se ponen los parámetros en el stack en el orden inverso a lo que se declaran, significa
(por ser el stack un elemento LIFO) que serán extraídos por la función en el orden en que están
declarados.
Aqui hay dos detalles que considerar: primero el hecho de que el orden de declaración de
parámetros en Pascal es inverso al de C. Esto no presenta ningún problema, porque el compilador
llama siempre a los parámetros en el mismo orden (porque en realidad pasa a lenguaje de máquina
y en ese nivel no puede haber diferencias en el orden), de manera que de esto se encarga el
compilador y basta con recordar que es inverso al que aparecen en las API Help.
El segundo detalle es más interesante. Mientras Pascal vuelve de la función API con el stack ya
equilibrado, en el lenguaje C es el programador el que tiene que encargarse de esa tarea. Desde el
punto de vista de la ingeniería inversa, si nosotros seguimos una función API y vemos que termina
en RET (4*n) donde n es el número de parámetros, es seguro que el compilador es estilo Pascal,
mientras que si vemos que luego de retornar de la API, el programa acomoda el stack haciendo
POPs o ADD ESP,(4*n), se trata de un compilador C. Pongamos como ejemplo la función vista
_lwrite. Tiene tres parámetros y por lo tanto 4*n=0Ch, por lo tanto, si vemos algo asi:
CALL _lwrite
ADD ESP,0C
se tratará muy probablemente de un ejecutable generado por un compilador C. En cambio, en
Pascal la misma función _lwrite finaliza con un RET 0Ch y por lo tanto no es necesario el ADD
posterior.
NOTA PARA EL PRINCIPIANTE
Es muy, pero MUY importante que el stack pointer quede siempre equilibrado entre el valor que tenía antes de
ingresar los parámetros al stack y luego de ejecutada la función API. Y es fácil deducir por qué: si asi no fuera,
la instrucción RET siguiente a producirse el desequilibrio del stack retornaría a un lugar que en realidad lo más
probable es que sea un parámetro en lugar de código. Esto es igualmente válido si una función es llamada con
una cantidad de parámetros distinta a la que exige su definición.
Junto con las definiciones de función y los parámetros con los que hay que llamarlas hay en la API
Help menciones a flags que controlan la operación de la función. Quizás un función emblemática
en este sentido sea CreateWindow, que tiene una gran cantidad de banderas (por ejemplo,
WS_BORDER, que cuando está activada hace que la función cree una ventana que tiene una linea
fina como borde). Durante la construcción del programa, el compilador se encargará de activar el
bit correspondiente a WS_BORDER, dentro del parámetro dwStyle. Sin embargo, cuando
decompilamos un programa por ejemplo con el W32DASM, nos encontramos con instrucciones
como PUSH 10830041. Esto corresponda posiblemente a parámetros como el dwStyle, que
controlan mediante bits individuales el comportamiento de la función. Supongamos que
determinamos que el anterior push corresponde efectivamente al parámetro dwStyle, por ser el
antepenúltimo en ser cargado en el stack. Cómo saber cuáles son las banderas que el programador
quiso activar?. Hay un sólo camino, que es seguir los pasos que dio el compilador al generar el
ejecutable. En esto nos ayuda el archivo windows.inc, que viene con el compilador (también
disponible en el ensamblador MASM32).
Abrimos ese archivo (676 kB de definiciones!). Comenzamos a buscar WS_BORDER
y encontramos :
WS_BORDER equ 00800000h
esto significa que tiene activado el bit 23, Bingo! el push que estamos considerando lo activa y por
lo tanto vemos que la ventana tendrá borde con linea fina. De la misma manera tenemos que
proceder con todos los bits de todos los parámetros que modifican la función de acuerdo con el
estado de las banderas. Arduo? Si, nadie dijo que esto sea tarea fácil, sólo podemos afirmar que no
es muy complicada, sólo extensiva.
Por lo general no es necesario comprobar el 100% de las flags (lo que nos llevaría a perder un par
de horas en una función como CreateWindow). Tenemos que concentrar nuestra atención en el
problema que queremos resolver, por ejemplo, si la ventana de entrada de claves está inicialmente
maximizada, hay que ver aquellas llamadas a CreateWindow con la flag WS_MAXIMIZE
activada.
NOTA PARA EL PRINCIPIANTE
Es importante reconocer algunas características en las notaciones empleadas para nombrar funciones API. Las
que incluyen una A o W final son funciones de 32 bits con un equivalente de 16 bits que no lleva esa letra. Por
ejemplo
CreateWindow es de 16 bits, mientras que
CreateWindowA es de 32 bits, strings de un byte
CreateWindowW es de 32 bits, string de 2 bytes
Cuando una funcion termina en Ex tiene capacidades extendidas sobre la de igual nombre pero sin el Ex (y
también algún parámetro adicional para controlar esa capacidad). Por ejemplo:
CreateWindow: tiene 11 parámetros, en cambio
CreateWindowEx :12 parámetros: se agrega dwExStyle que controla el estilo extendido
GLOSARIO DEL CRACKING
AND
Operación binaria cuyo resultado es 1 sólo si ambos operadores son 1. También es el mnemónico de una
instrucción de procesador que consiste en realizar la operación binaria bit por bit entre los operandos
declarados en la instrucción. Por ejemplo, AND EAX,EBX instruye al procesador a realizar una operación
binaria AND bit por bit entre los registros EAX y EBX y almacenar el resultado en EAX.
ASSEMBLY
Lenguaje de programación que permite el más absoluto control sobre el procesador. Es fundamental un
aceptable manejo de este lenguaje a la hora de hacer ingeniería inversa sobre un target, ya que por lo
general no se dispone del programa fuente y debe utilizarse una dead list.
BACKDOOR
Literalmente "puerta trasera", es un mecanismo que se instala en los sistemas a los que un hacker accede,
con el objeto de sistematizar y ocultar futuros accesos.
BANNER El horrible aviso comercial que encabeza toda página de un sitio de hosting gratuito
BIOS
Se deriva de "Basic Input - Output System" (sistema básico de entrada-salida), por referirse de algún modo
a la interface necesaria entre el sistema operativo y el hardware. Aunque los dispositivos y el procedimiento
utilizado para controlarlos puede diferir en cada PC, los sistemas operativos tienen reglas fijas para utilizar
los recursos. Estas reglas son las funciones BIOS y la sintaxis empleada para convocarlas.
Cuando por ejemplo, corremos (es un decir) el Notepad de Windows, hay tres capas de software una dentro
de la otra: La exterior es la aplicación Notepad, la intermedia es el sistema operativo (Windows en este
caso) y la más interna el BIOS. Asi, éste avisa a Windows que el usuario apretó una tecla, Windows le avisa
a Notepad, y éste toma alguna acción al respecto, que puede ser algo tan simple como poner el caracter en
el buffer de edición, y avisar a Windows que tiene que sacar el caracter por pantalla, para lo cual este
avisará al BIOS de qué forma debe representarlo. Esta estructura en capas puede parecer compleja pero es
la única manera de permitir que distintos fabricantes puedan hacer PCs para un mismo sistema operativo o
que una misma PC pueda correr distintos sistemas operativos.
En nuesta página sobre interrupciones hay un breve listado de las interrupciones utilizadas por el BIOS y
por el DOS
BIT
La palabra se deriva de "Binary unIT" (unidad binaria). El ancho de las palabras binarias se especifica en
bits: p.e. decimos que los registros de los procesadores actuales es de 32 bits y que el del (hoy) futuro
Merced es de 64 bits. Esto da una idea de potencia de cálculo, ya que para obtener el mismo resultado para
una operación suma simple como ADD EAX,ECX el procesador 8088 de las primeras PCs tenía que hacer
dos sumas sucesivas porque el ancho de palabra era de 16 bits.
BOOLEANO
Que sigue las reglas del álgebra de Boole o que opera con valores binarios de un bit. Función Booleana:
Aquella cuyo resultado puede ser cierto o falso (1 o 0), por ejemplo comprobar si durante un acceso a disco
se produjo un error o no.
BUFFER
Area de memoria que se utiliza para realizar operaciones en las que un dispositivo deja datos a los que el
programa consulta asincrónicamente. El búffer más fácil de entender es el de teclado: entre el BIOS y el
sistema operativo leen el teclado y dejan en el búffer el código de las teclas apretadas. El programa luego va
a esa área de memoria, normalmente mediante funciones del Sistema Operativo, para leer los códigos y
descargar el búffer (le saca los caracteres que va leyendo). Esto permite, por ejemplo, seguir escribiendo
mientras se produce un acceso a disco sin que se pierdan caracteres, ya que si bien el programa debe esperar
a que termine la operación de disco, la función BIOS de lectura de teclado es llamada por la interrupción de
hard y es atendida con mayor prioridad.
El búffer para accesos a disco es el área de memoria destinada a recibir los datos que se leen o en donde el
programa escribe los datos que se deben transferir al disco.
BYTE
Palabra de 8 bits. Con 8 bits se pueden representar 256 (=28
) números decimales distintos desde 0 (todos los
bits en cero) hasta 255 (todos los bits en 1)
CARRY
Flag de los procesadores que indica un desborde aritmético que debe ser tenido en cuenta cuando se opere
el dígito siguiente. Es el "me llevo uno" que decimos cuando hacemos una suma decimal y una columna nos
da 10 o más: lo que estamos haciendo es un "carry" (llevarse) a la siguiente posición decimal. En un
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly
Mini curso assembly

Más contenido relacionado

La actualidad más candente

Tema2 arquitectura del ordenador hardware
Tema2 arquitectura del ordenador hardwareTema2 arquitectura del ordenador hardware
Tema2 arquitectura del ordenador hardware
tecnologiabsotos
 
Metodos de convercion
Metodos de convercionMetodos de convercion
Metodos de convercion
guest0adb65c
 
Aritmetica del computador
Aritmetica del computadorAritmetica del computador
Aritmetica del computador
profruiloba
 

La actualidad más candente (16)

Asignacion #3
Asignacion #3Asignacion #3
Asignacion #3
 
Tema2 arquitectura del ordenador hardware
Tema2 arquitectura del ordenador hardwareTema2 arquitectura del ordenador hardware
Tema2 arquitectura del ordenador hardware
 
Aritmética de Computadores
Aritmética de ComputadoresAritmética de Computadores
Aritmética de Computadores
 
Guía de informática binarios
Guía de informática binariosGuía de informática binarios
Guía de informática binarios
 
Presentacion sistema binario
Presentacion sistema binarioPresentacion sistema binario
Presentacion sistema binario
 
Sistemas de números binarios
Sistemas de números binariosSistemas de números binarios
Sistemas de números binarios
 
Sistemas de numeracion
Sistemas de numeracionSistemas de numeracion
Sistemas de numeracion
 
Asignacion 3
Asignacion 3 Asignacion 3
Asignacion 3
 
Metodos de convercion
Metodos de convercionMetodos de convercion
Metodos de convercion
 
Sistemas y codigos numericos.
Sistemas y codigos numericos.Sistemas y codigos numericos.
Sistemas y codigos numericos.
 
Material de ayuda
Material de ayudaMaterial de ayuda
Material de ayuda
 
Sistema Binario
Sistema BinarioSistema Binario
Sistema Binario
 
Aritmetica del computador
Aritmetica del computadorAritmetica del computador
Aritmetica del computador
 
Sistemas Numericos
Sistemas NumericosSistemas Numericos
Sistemas Numericos
 
Sistema binario
Sistema binarioSistema binario
Sistema binario
 
Conversión Entre Sistemas de Numeración
Conversión Entre Sistemas de NumeraciónConversión Entre Sistemas de Numeración
Conversión Entre Sistemas de Numeración
 

Similar a Mini curso assembly

Sistemas Numericos y conversiones(Powerpoint aplicaciones m. 1)
Sistemas Numericos y conversiones(Powerpoint aplicaciones m. 1)Sistemas Numericos y conversiones(Powerpoint aplicaciones m. 1)
Sistemas Numericos y conversiones(Powerpoint aplicaciones m. 1)
guffygram
 
Sistemas de numeracion
Sistemas de numeracionSistemas de numeracion
Sistemas de numeracion
gysors
 
Tarea1 daniel garcía_delicado
Tarea1 daniel garcía_delicadoTarea1 daniel garcía_delicado
Tarea1 daniel garcía_delicado
gysors
 
Sistemabinario
SistemabinarioSistemabinario
Sistemabinario
jhooos
 
Sistemabinario
SistemabinarioSistemabinario
Sistemabinario
jhooos
 

Similar a Mini curso assembly (20)

sistema numérico binario y todos sus componentes.
sistema numérico binario y todos sus componentes.sistema numérico binario y todos sus componentes.
sistema numérico binario y todos sus componentes.
 
Clase de sistema de numeración
Clase de sistema de numeraciónClase de sistema de numeración
Clase de sistema de numeración
 
Eventos digitales y analógicos (tema 2)
Eventos digitales y analógicos (tema 2)Eventos digitales y analógicos (tema 2)
Eventos digitales y analógicos (tema 2)
 
Eventos digitales y analógicos (tema 2)
Eventos digitales y analógicos (tema 2)Eventos digitales y analógicos (tema 2)
Eventos digitales y analógicos (tema 2)
 
Sistema Numeración
Sistema NumeraciónSistema Numeración
Sistema Numeración
 
Gill-Sistema de numeraciòn
Gill-Sistema de numeraciònGill-Sistema de numeraciòn
Gill-Sistema de numeraciòn
 
Asignacion #3
Asignacion  #3Asignacion  #3
Asignacion #3
 
Sistemas Numéricos
Sistemas NuméricosSistemas Numéricos
Sistemas Numéricos
 
Sistemas de Numeración y conversiones
Sistemas de Numeración y conversionesSistemas de Numeración y conversiones
Sistemas de Numeración y conversiones
 
Sistemas numeracion
Sistemas numeracionSistemas numeracion
Sistemas numeracion
 
Sistemas Numericos y conversiones(Powerpoint aplicaciones m. 1)
Sistemas Numericos y conversiones(Powerpoint aplicaciones m. 1)Sistemas Numericos y conversiones(Powerpoint aplicaciones m. 1)
Sistemas Numericos y conversiones(Powerpoint aplicaciones m. 1)
 
compresor de archivos
compresor de archivos compresor de archivos
compresor de archivos
 
Conversión de binario a decimal
Conversión de binario a decimalConversión de binario a decimal
Conversión de binario a decimal
 
Logica computacional
Logica computacionalLogica computacional
Logica computacional
 
Sistemas de numeracion
Sistemas de numeracionSistemas de numeracion
Sistemas de numeracion
 
Tarea1 daniel garcía_delicado
Tarea1 daniel garcía_delicadoTarea1 daniel garcía_delicado
Tarea1 daniel garcía_delicado
 
Sistemabinario
SistemabinarioSistemabinario
Sistemabinario
 
Sistemabinario
SistemabinarioSistemabinario
Sistemabinario
 
Conversion de binarios a decimales y decimales a
Conversion de binarios a decimales y decimales aConversion de binarios a decimales y decimales a
Conversion de binarios a decimales y decimales a
 
Exposición.ppt
Exposición.pptExposición.ppt
Exposición.ppt
 

Más de Franciny Salles (8)

Aprendendo python
Aprendendo pythonAprendendo python
Aprendendo python
 
C++ for hackers
C++ for hackersC++ for hackers
C++ for hackers
 
Clase 02 - Curso Hacker Ético
Clase 02 - Curso Hacker ÉticoClase 02 - Curso Hacker Ético
Clase 02 - Curso Hacker Ético
 
Basico TCP/IP
Basico TCP/IPBasico TCP/IP
Basico TCP/IP
 
Uso de-telnet-708-mddd5d
Uso de-telnet-708-mddd5dUso de-telnet-708-mddd5d
Uso de-telnet-708-mddd5d
 
Operadores google
Operadores googleOperadores google
Operadores google
 
Hydan
HydanHydan
Hydan
 
Clase01 - Curso Hacker Ético
Clase01 - Curso Hacker ÉticoClase01 - Curso Hacker Ético
Clase01 - Curso Hacker Ético
 

Último

Antenas, tipos de antenas, diseño basico de una antena y parámetros.pdf
Antenas, tipos de antenas, diseño basico de una antena y parámetros.pdfAntenas, tipos de antenas, diseño basico de una antena y parámetros.pdf
Antenas, tipos de antenas, diseño basico de una antena y parámetros.pdf
perezreyesalberto10
 

Último (6)

Biología Células Musculares presentación
Biología Células Musculares presentaciónBiología Células Musculares presentación
Biología Células Musculares presentación
 
Emprende en SPA Segundo día CENEC Mexico
Emprende en SPA Segundo día CENEC MexicoEmprende en SPA Segundo día CENEC Mexico
Emprende en SPA Segundo día CENEC Mexico
 
Corte de luz 2024 Guayaquil Guayas ecuad
Corte de luz 2024 Guayaquil Guayas ecuadCorte de luz 2024 Guayaquil Guayas ecuad
Corte de luz 2024 Guayaquil Guayas ecuad
 
¡Descubre el Poder del Masaje Holístico en nuestra Primera Sesión del Seminar...
¡Descubre el Poder del Masaje Holístico en nuestra Primera Sesión del Seminar...¡Descubre el Poder del Masaje Holístico en nuestra Primera Sesión del Seminar...
¡Descubre el Poder del Masaje Holístico en nuestra Primera Sesión del Seminar...
 
Antenas, tipos de antenas, diseño basico de una antena y parámetros.pdf
Antenas, tipos de antenas, diseño basico de una antena y parámetros.pdfAntenas, tipos de antenas, diseño basico de una antena y parámetros.pdf
Antenas, tipos de antenas, diseño basico de una antena y parámetros.pdf
 
Presentacion Seguridad y Privacidad en la Web
Presentacion Seguridad y Privacidad en la WebPresentacion Seguridad y Privacidad en la Web
Presentacion Seguridad y Privacidad en la Web
 

Mini curso assembly

  • 1. MINICURSO ASSEMBLY Autor: Greythorne the Technomancer Traducción: Eidan Yoson Adaptación: Franciny Salles (#Bl4kd3m0n)
  • 2. Modulo 1 Cuando termine de leer esta página deberá conocer:  Sistemas de numeración  Operaciones binarias Sistema de numeración Estamos habituados al sistema de numeración decimal y nos parece lógico usarlo en todo momento. Pero hay ocasiones en donde no es el más apropiado. Uno de esos mundos en los que existen sistemas más descriptivos de los fenómenos que el decimal es el de los procesadores. Por su naturaleza digital, los procesadores son máquinas esencialmente binarias. Utilizan el sistema de numeración llamado binario, en el que sólo se disponen dos signos: 0 y 1. Contando correlativamente de manera binaria, diríamos: 0, 1, 10, 11, 100, 101, 110, 111, ... ¿complicado? Pero es muy fácil!. Tanto el sistema binario, como el decimal y el hexadecimal, son sistemas en los que la posición de cada dígito representa información de mucha importancia. Veamos un ejemplo de cómo se descompone posicionalmente un numero decimal: El número 7935 = 1000 * 7 + 100 * 9 + 10 * 3 + 1 * 5 Elemental ¿no?. Sin embargo, la numeración romana no goza de tan buenas propiedades y por eso hace ya tiempo se lo reemplazó por el sistema decimal (a excepción de la numeración de las páginas del prefacio en los libros y del numero de serie de las películas de Rocky :=) Como hay diez símbolos (del 0 al 9), una decena representa 10 unidades, una centena representa 10 decenas, etc. Diez unidades de una posición, valen una unidad en la posición contigua a la izquierda. En el sistema binario, con dos símbolos solamente, cada posición a la izquierda vale el doble de la que le sigue a la derecha. O lo que es lo mismo decir, la relación entre las sucesivas posiciones se da según la sucesión 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536 ..... la que a su vez puede expresarse como potencias crecientes de 2: 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 210 , 211 , 212 , 213 , 214 , 215 , 216 ..... Para el sistema de numeración binaria, valen las dos reglas prácticas siguientes:  Un número de n bits puede representar a un decimal de un valor de hasta 2n - 1  El multiplicador del bit de posición n vale 2n Ejemplos: un número de 8 bits cuenta desde 0 hasta 255. El multiplicador del bit 7 es 128. Notar que siempre se comienza a contar desde cero. En un número binario, al igual que en un decimal, el bit menos significativo (correspondiente al multiplicador 20 , o sea 1) es el que se escribe más a la derecha: bit# 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
  • 3. mult 32768 16384 8192 4096 2078 1024 512 256 128 64 32 16 8 4 2 1 Veamos como ejemplo práctico un número de 7 bits cualquiera como 1001101 (notar que los bits se ordenan 6...0) 1001101 = 64 * 1 + 32 * 0 + 16 * 0 + 8 * 1 + 4 * 1 + 2 * 0 + 1 * 1 Esto nos proporciona una forma de traducir (cambiar de base) un número binario a decimal. Basta sumar aquellos multiplicadores cuyos bits estén en 1 e ignorar aquellos cuyo bit es 0. En nuestro anterior ejemplo es: 1001101 = 64 + 8 + 4 + 1 = 77 decimal Para el traspaso de decimal a binario, hay que dividir siempre por 2 y contar sólo los restos, de atrás hacia adelante. Observese que el resto no es otra cosa que el multiplicador de las potencias de dos en las anteriores igualdades, las que pueden ser definidas como la sumatoria de los productos de los restos por sus potencias de dos respectivas Por ejemplo, para el 77 decimal obtenemos los restos: opreración resto pot.de 2 77 / 2 = 38 r=1 1 38 / 2 = 19 r=0 2 19 / 2 = 9 r=1 4 9 / 2 = 4 r=1 8 4 / 2 = 2 r=0 16 2 / 2 = 1 r=0 32 1 / 2 = 0 r=1 64 Ordenando los restos según las potencias decrecientes de 2, obtenemos nuevamente 1001101. Los números binarios son los que efectivamente fluyen dentro del procesador en una PC, se guardan en memoria o disco, o se transmiten (modulados) por modem. Pero un humano no puede manipular con facilidad números como: 1101 0011 0101 0110 1010 0101 1100 0011 que es de 32 bits (hay 32 símbolos en el número, desde el bit 31 a la izquierda hasta el bit 0, a la derecha) y se ha ordenado ex-profeso en grupos de a cuatro por cuestiones de comodidad que serán evidentes algo más adelante. El procesador 80386 hace ya más de una década manipulaba sin problemas números de 32 bits. Un humano necesita manejarlo de otra manera y por eso se inventó el sistema hexadecimal, con 16 símbolos, ya que si uno agrupa cuatro bits obtiene 16 combinaciones posibles (24 = 16). Esto tiene una razón. Nuestro sistema decimal no se corresponde en la cantidad de dígitos con el binario en cambio, el hexadecimal si, porque cada cuatro bits representan un dígito hexadecimal exacto. De tal manera, el anterior número de 32 bits se traduce al hexadecimal como uno de 8 dígitos (32 bits agrupados de a 4). Para la conversión podemos usar la tabla binario-decima-hexa qe está algo
  • 4. más adelante. En un sistema hexadecimal, necesitamos 16 símbolos. Ya que somos muy buenos manejando números decimales, adoptamos esos diez símbolos (0, 1, 2, 3, 4, 5, 6, 7, 8 y 9) para empezar, pero hay que agregar otros seis. Mmh ! por qué no A, B, C, D, E y F ? De esta forma, si me toca contar jugando a las escondidas y quiero hacerlo en hexadecimal (de puro tonto, porque voy a contar un 60% más:=), tengo que decir: 0, 1,.......8, 9, A, B, C, D, E, F, 10, 11.........18, 19, 1A, 1B, 1C, 1D, 1E, 1F, 20, 21........29, 2A,.........2E, 2F, 30, 31 ... El anterior e impronunciable numero binario de 32 bits pasa a ser: 0xD356A5C3 hexa, es igual a 3.545.671.107 en decimal Por cierto que no hice la conversión de binario a decimal a mano con la fórmula anterior, sino que usé la calculadora de Windows en modo científico, que permite operar o convertir números entre bases binaria, octal, decimal y hexadecimal. Otra base de numeración posible con traducción de dígitos exacta al binario es la octal que tiene sólo 8 símbolos (del 0 al 7), con lo cual cada dígito representa a 3 dígitos binarios, pero está casi en desuso. Note el lector el "0x" del comienzo, para significar que lo que sigue es un número hexadecimal. Otro estilo es poner una "h" final, con la precaución de colocar un cero adelante si el número comienza con A, B, C, D, E o F. Para aquél número de 32 bit utilizado como ejemplo, adoptamos como notación : 0D356A5C3h Cada trozo de información recibe un nombre propio según la cantidad de bits que posea:  un bit es la unidad de información binaria y con él se puede contar desde 0 hasta 1  un nibble son cuatro bits y se puede contar desde 0 hasta 15 (0xF en hexa)  con un byte (8 bits) puedo contar desde 0 hasta 255 ó 0xFF hexa  una word tiene 16 bits y permite contar desde 0 hasta 65535 ó 0xFFFF  una double-word (32 bits) permite contar desde 0 hasta 4.294.967.295 ó 0xFFFFFFFF Cuando usted escuche hablar de direcciones de 32 bits, sepa que hay un espacio de almacenamiento de 4.294 ... millones de bytes o 4 Gigabytes (o de colores, si estamos hablando de color de 32 bits). Para finalizar con este tema, aqui hay una tabla que convierte el primer nibble (los primeros 4 bits) a decimal y a hexa. Usted con ella debe poder convertir cualquier numero binario en hexa y viceversa: binario decimal hexa binario decimal hexa 0000 0 0 1000 8 8 0001 1 1 1001 9 9 0010 2 2 1010 10 A 0011 3 3 1011 11 B 0100 4 4 1100 12 C 0101 5 5 1101 13 D 0110 6 6 1110 14 E
  • 5. 0111 7 7 1111 15 F Operaciones Binarias En lo que sigue se adopta como convención la lógica positiva, lo que implica: verdadero = 1 = activo, ------, falso = 0 = inactivo Hay cinco operaciones binarias básicas: AND, OR, NOT, XOR y ADD. La resta, multiplicación y división se derivan de estas cinco anteriores. Cualquiera sea la longitud de la palabra o palabras objeto de la operación, siempre se hace de a un bit por vez de derecha a izquierda (tal como si fuera una suma o resta con números decimales). Esto permite una definición de cada operación que es independiente de la longitud del o de los operando(s). La operación NOT es la única que se realiza sobre un sólo operando (es unaria), y las otras cuatro sobre dos operandos. o La operación AND (Y) tiene resultado 1 si sus dos operandos son ambos 1 o La operación OR (O) tiene resultado 1 si cualquiera de sus operandos es 1 o La operación XOR tiene resultado 1 si los operandos son distintos (uno en 0 y el otro en 1) o La operación NOT (NO) tiene resultado 1 si el operando es 0 y viceversa o La operación ADD (SUMA) se define igual que con los números decimales AND OR XOR NOT SUMA 0 * 0 = 0 0 + 0 = 0 0 X 0 = 0 NOT 1 = 0 0 + 0 = 0 0 * 1 = 0 0 + 1 = 1 0 X 1 = 1 NOT 0 = 1 0 + 1 = 1 1 * 0 = 0 1 + 0 = 1 1 X 0 = 1 --- 1 + 0 = 1 1 * 1 = 1 1 + 1 = 1 1 X 1 = 0 --- 1 + 1 = 10 Le extrañó el resultado de la suma? Sin embargo es lo que hacemos en la suma decimal 5+5=10 (nos llevamos "1" para la operación del dígito siguiente). Este llevarse "1" es vastamente usado entre los procesadores digitales y tiene un nombre especial: carry (lo verá abreviado como CY, C o CF-por carry flag), lo que en castellano se traduce como "acarreo" (que suena muy mal, asi que le seguiremos llamando carry). Estas operaciones también se llaman "booleanas" ya que se basan en el álgebra de Boole (invito al lector a rememorar cuando en la escuela secundaria se preguntaba, igual que yo, si el álgebra de Boole le serviría alguna vez para algo).
  • 6. MODULO 2 Cuando termine de leer esta página deberá conocer:  Modelo de procesador X86  Modos de direccionamiento  Modelo de memoria de una PC  Segmentos Modelo de procesador X86 Los ancestros del bienamado Pentium III no fueron tan poderoso como él (por las dudas alguien lea esto allá por el 2005 y le arranque una sonrisa el poder del Pentium III, debo decir que hoy, mediados de 1999 es el procesador más potente disponible para PCs y acaba de salir a la venta). Todo comenzó hace dos décadas con un oscuro (aunque revolucionario para la época) 8086, con registros de 16 bits, que para colmo debió por cuestiones monetarias sufrir un "downsizing" hasta el ridículo 8088 -motor de las renombradas IBM PC, con las mismas instrucciones pero con un bus de 8 bits. Cuando hablamos de registros de 16 bits queremos decir que el procesador tiene posiciones de almacenamiento especiales llamadas registros cuyo ancho de palabra es de 16 bits. Y cuando nos referimos a bus, término de amplia aplicación queremos decir bus de procesador (no el de la placa madre, ni el de I/O, ni el de los canales IDE). El procesador tiene dos buses pro uno saca direcciones y por el otro entra instrucciones o entra y saca datos. En el 8088 el bus de datos era de 8 bits, aunque internamente sus registros manipulaban palabras de 16 bits. Unos años después apareció el legendario 80386 DX, con arquitectura y bus de 32 bits y su hermano menor, ese engendro con bus de 16 bits que fue el 386SX tan promocionado por las revistas de vulgarización tipo PCmierdazine, quién sabe con qué oscuro y comercial designio. Varios años más adelante quisieron darle auge a otro castrado, el no menos nombrado "celeron", un Pentium II sin caché L2, que es precisamente aquello que hace muy veloz al original. Todos estos procesadores (y algunos más como el 486) comparten el mismo juego de instrucciones básico del 8086, al que cada generación le introdujo mejoras, alguna instrucción más, más registros, multi-thread, predicción de saltos y hasta un fabuloso número de serie único en el Pentium III con el que Intel no quiere perdernos pisada y al que puede accederse por instrucciones comunes que permitirían a cualquier servidor Internet saber qué número de procesador tiene el hacker que se acaba de conectar y con lo cual se acabaría toda diversión en la red (y toda privacidad!!!!!!!). Pero tal vez el salto tecnológico más revolucionario lo inició el 80386 al permitir un modo de funcionamiento con cualidades especiales al que se lo llamó "modo protegido". Debido a las características de este modo, se podían generar "máquinas virtuales", cada una con su propio espacio de memoria virtual, al que se acceden a través de vectores de 32 bits ubicados en dos tablas conocidas como GDT y LDT. Este mecanismo permite que a partir del 386 los procesadores Intel
  • 7. direccionen una memoria virtual de 64 Terabytes (o sea 16.384 espacios de direccionamiento reales de 4 GB). Programas Todo programa fuente assembly, tienen la forma de una lista de instrucciones, rótulos (labels) y decisiones parecida al siguiente pseudocódigo: ORG 100h ;Directiva de Ensamblador label1: instrucción 1 ;comentario label2: instrucción 2 si (comparación) verdadera ir a label 2 instrucción 3 end ;esta es otra instrucción más En lenguaje assembly, cada instrucción se compone de un nombre mnemónico que determina el tipo de operación (por ejemplo MOV, PUSH, etc) y un campo de datos que especifica los operandos sobre los que dicha operación se debe llevar a cabo. Una línea de programa assembly tiene a su vez tres campos: el de rótulos (labels), el de la instrucción y un campo de comentarios que siempre comienza con ";" (punto y coma). El compilador assembly -llamado Assembler o Ensamblador- traduce las instrucciones en códigos de operación del procesador según comandos especiales que se llaman Directivas de Ensamblador, para producir un módulo de programa ejecutable. En los programas ejecutables estos códigos de operación son valores binarios de uno o más bytes por cada mnemónico. De haber un dato, también será compilado como binario, que es en definitiva la única base de numeración que pueden interpretar los procesadores. Sin embargo, cuando un programador escribe un programa mediante un editor, puede indicarle al compilador si un número es binario, decimal o hexa. Al correr el programa, el procesador va ejecutando las instrucciones almacenadas en memoria e incrementando el registro IP secuencialmente. Tanto CS (Code Segment) como IP (Instruction Pointer) son los registros del procesador que direccionan el código ejecutable . La dirección de la próxima instrucción a ejecutarse está dada por el vector CS:IP Cuando se encuentra una instrucción de bifurcación, si se verifica la condición expresada por el tipo de instrucción, el puntero de instrucciones (IP) cambia con un salto en lugar de incrementarse. En el ejemplo de pseudo-programa anterior, en la instrucción de comparación el IP tomará el valor de la dirección "label 2" toda vez que la comparación haya resultado verdadera o incrementará su valor a la instrucción siguiente (instrucción 3) si la comparación resulta falsa. Hay que tener presente que cada una de las instrucciones anteriores está almacenada en uno o varios bytes en la
  • 8. memoria. El valor de label2 es en el caso anterior la dirección en donde esta almacenado el primer byte de la instrucción 2. Abramos las ventanas Inicie usted una sesión DOS, teclee la orden DEBUG y cuando le aparezca el anodino prompt "-", pulse "r" y enter. Los datos que usted ve desplegarse son los registros básicos y el contenido de los mismos, del procesador de su PC: (Notar que Debug supone que la notación es hexadecimal y que los registros son de 16 bits aunque su PC sea Pentium) AX=0000 BX=0000 CX=0000 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=1332 ES=1332 SS=1332 CS=1332 IP=0100 NV UP EI PL NZ NA PO NC 1332:0100 C3 RET . Esto significa que su Pentium con corazón de 8086 tiene al menos: o cuatro registros generales: AX, BX, CX y DX o cuatro registros índices: SP, BP, SI y DI o cuatro registros de segmento: DS, ES, SS y CS o un registro que apunta a la próxima instrucción a ejecutar: IP o un registro de banderas de uso general: F (banderas V, D, I, P, Z, A, S y C) Todos los registros mencionados son de 16 bits y usted se preguntará ¿no es que a partir del 80386 los registros son de 32 bits?. Y está en lo cierto, los nuevos registros se llaman EAX, EBX, etc... pero todo a su tiempo. Recuerde que estamos viendo por el momento sólo lo más básico y esto nos remite al modelo del 8086. En este procesador, a su vez los registros AX, BX, CX y DX pueden dividirse en dos registros de 8 bits (por ejemplo el AX en AH (bits 8 a 15) y AL (bits 0 a 7). Cada registro tiene sus funciones específicas (aunque hay muchas que son compartidas):  AX: Acumulador, principalmente usado para operaciones aritméticas  BX: Base. Se usa para indicar un desplazamiento (offset) sobre una posición de memoria  CX: Contador. Se usa para lazos y operaciones repetitivas  DX: Dato. De uso general  CS: Segmento de código. Indica el segmento donde residen las instrucciones  SS: Segmento de Stack. Indica el segmento que utiliza el Stack  DS y ES: Segmentos Data y Extra, segmentos donde residen los datos  SP: Puntero de Stack. Indica el offset actual del Stack  BP: Puntero de base, para operaciones de indexación  SI: Indice de origen. Offset en segmento de datos de origen  DI: Indice de destino: Offset en segmento de datos de destino  F: Flags (hay nueve banderas importantes entre las 16)
  • 9. Las flags (banderas) a tener en cuenta son:  C: carry - indica si la operación anterior generó un carry  Z: zero - indica si en la operación anterior se generó una igualdad  S: sign - indica si en la operación anterior el resultado fue negativo  AC: auxiliar carry - indica si hay que hacer un ajuste decimal en AX  P: parity - indica si la paridad del último resultado fue par  V: overflow (también simbolizada O)- indica desbordamiento aritmético en AX  D: direction - indica si los indices SI o DI se incrementan (D=0) o decrementan (D=1)  I: interrupt enable - indica si se permiten las interrupciones (I=1) o no (I=0)  T: trap - controla la operación paso a paso del procesador Asi como la dirección de la próxima instrucción a ejecutarse está apuntada por la pareja CS:IP, hay un lugar de memoria especial apuntado por SS:SP llamado STACK y utilizado para guardar datos transitorios, parámetros que se pasan a las funciones y direcciones de retorno de subrutinas o interrupciones. Se llama stack porque opera como una pila de objetos, en donde el último en ponerse es el primero en sacarse, mediante instrucciones especialmente diseñadas para eso que se llaman PUSH y POP. Hay métodos para consultar o escribir otros valores que no son los apuntados pos SS:SP (lo que equivale a sacar un objeto de la pila sin que se desmorone). El puntero SP está dando el offset de la última posición de memoria escrita en la zona de stack y como se va llenando desde posiciones altas hacia las más bajas, la próxima posición libre es la SS:SP-1. Por ejemplo, supongamos que SP contiene el valor 0FFE4h (más adelante se verá qué papel juegan los registros de segmento como el SS, en la determinación de la dirección de memoria real) y que AX contiene el valor 2233h. La instrucción PUSH AX pondrá el valor 22 h (contenido en AH) en la posición 0FFE3 h (0FFE4 - 1) y el valor 33 h almacenado en AL en la posición de memoria 0FFE2 h y deja SP apuntando al último byte ocupado, vale decir, que SP contendrá el valor 0FFE2h.. La instrucción POP AX realiza la operación inversa. No es mi intención tratar de suplir un buen manual Intel (que puede bajarse gratis de internet del sitio www.intel.com) en el que se describe qué es cada registro y cuáles son las instucciones en las que está involucrado. Un buen sitio en castellano para consultar las instrucciones es http://udgftp.cencar.udg.mx/tutoriales/TutorialEnsamblador/ensam.html de la Universidad de Guadalajara, México, en donde además hay un tutorial de Assembly elemental con la fallida denominación de Assembler. Las operaciones del procesador se van ejecutando de manera secuencial tal como están almacenadas las instrucciones en la memoria. Existen instrucciones (saltos) que permiten cambiar la secuencia de ejecución en forma absoluta o condicionada al resultado de alguna operación anterior, tal como se dijo de la instrucción de comparación en el ejemplo de pseudo-código antes visto La instrucción más elemental es MOV, que permite copiar un dato de un origen a un destino. OPERANDOS
  • 10. Las instrucciones pueden tener ninguno, uno, dos o tres operandos. A su vez, los operandos pueden ser inmediatos, registros, memoria o puertos. Un operando inmediato es un dato que viene en el código del programa, por ejemplo, para cargar el registro AX con el número 20C5h se usa la instrucción: MOV AX,20C5h ;20C5h es un operando "inmediato" ;Otras instrucciones MOV pueden ser: MOV BX,[0400h] ;0400h es una posicion de memoria MOV DX,[BX] ;[BX] también es una posición de memoria En el listado anterior, todo lo que hay después del punto y coma ";" son comentarios extremadamente necesarios en programación Assembly. No es el compilador quien los debe interpretar sino el propio programador o quien en el futuro deba modificar el programa. El número entre corchetes indica que el 0400h debe interpretarse como una DIRECCION de memoria (los corchetes deben leerse como "el contenido de", o sea : en esa operación cargamos el registro BX con el contenido de la posición de memoria 0400hexa del actual segmento de datos) Este tipo de referencia a memoria se llama Directo. En cambio, si expresamos [BX], nos estamos refiriendo a la posición de memoria cuyo offset en el segmento actual de datos es el número contenido en el registro BX; este tipo de referencia a las posiciones de memoria se denomina Indirecto. Por ejemplo, supongamos para el código anterior que en la dirección [0400] hay una word cuyo valor es 1234 h, y en la dirección de memoria [1234] hay una word cuyo valor es 56CCh, luego de ejecutarse esas instrucciones el registro BX contiene el valor 1234h y el registro DX contiene el valor 56CCh. En cambio el registro AX es cargado con el número 20C5h y a este direccionamiento se lo llama Inmediato. Los operandos pueden ser de 8, 16 o 32 bits, según se desprenda del contexto de la operación o del otro operando (por ejemplo, en el anterior MOV BX,[0400h], dado que BX es de 16 bits, lo que se va a mover es un word. Se deben incluír prefijos para especificar la longitud del dato cuando se de lugar a ambigüedad como por ejemplo en la instrucción INC, en donde si el destino es una posición de memoria, hay que especificar si es byte o word de la siguiente manera: INC BYTE PTR [0406] ;incrementar el byte de offset 406h del segmento de datos Modos de Direccionamiento El modo de direccionamiento indica la forma en que el procesador calcula da dirección donde irá a buscar el dato origen o grabará el resultado en el destino, tal como se dejó entrever en el punto anterior. Existen ocho modos de direccionamiento en los procesadores X86  Implicito: la misma operación lo indica (p.ej. PUSHA, siempre indica como destino el Stack)  Registro: la instrucción menciona el registro (p. ej MOV AL,CH)
  • 11.  Inmediato: la instrucción proporciona el dato (p. ej. MOV DL,5Fh)  Directo: la instrucción da la dirección de memoria (p. ej. MOV BX,[0400h])  Registro-Indirecto: la dirección es el contenido de un registro (p.ej. MOV AX,[BX])  Relativo a base: dirección = base + constante (p.ej. MOV CX,[BX+6])  Directo Indexado: dirección = directo + índice (p.ej. MOV DH,[0400h+SI])  Indexado a base: dirección = directo + base + índice (p.ej. MOV AL,[0400h+BX+SI]) Cada registro de uso general o índice tiene su propio registro de segmento asociado, según la tabla siguiente: AX, BX, CX, SI, DI DS BP, SP SS DI (instrucciones de strings) ES IP CS En instrucciones de strings se opera entre un operador fuente (DS:SI) y otro destino (ES:DI). Aunque en estas instrucciones se lo vincula al segmento contenido en ES, en toda otra instrucción, el registro DI está asociado con el registro de segmento DS. A pesar de esto, puede cambiarse esta asociación default con prefijos de segmento. Por ejemplo, si queremos que el AX se cargue con el contenido de la dirección de memoria 3C8, pero del segmento apuntado por ES, tenemos que usar: MOV AX,ES:[3C8] ;cargar AX con el contenido de la dirección ES*10h+3C8h A continuación veremos como calcular una dirección segmentada del tipo SEG:OFF, en donde SEG es uno de los cuatro registros de segmento (DS, ES, SS o CS) y OFF es un registro de uso general o puntero. Modelo de Memoria de una PC La capacidad de direccionamiento de un procesador está dada por la cantidad de líneas del bus de direcciones (o sea el ancho en bits, de la palabra que el procesador es capaz de poner en el bus de direcciones de la computadora). En un procesador típico de PC, tenemos 32 bits o sea 4 gigabytes (2 elevado a la potencia 32) de posiciones de memoria distinguibles. Esto constituye el espacio de direccionamiento real, pero no significa que nuestra PC tiene instalada esa cantidad de RAM, sino que en caso de estar físicamente instalada, el procesador es capaz de direccionarla. Todo segmento de programa que se está ejecutando debe residir en memoria real (no sólo el segmento de código sino también el de datos). Como Windows es un sistema multitarea, si alguna aplicación pasa a segundo plano, es posible que en caso de escasez de memoria real, el sistema operativo decida guardar en memoria virtual parte o toda la memoria real que la aplicación ocupa, y la almacena en el archivo de intercambio (por lo general este archivo tiene varias decenas de megabytes y es de tipo oculto). Cuando la aplicación vuelve a primer plano, el procesador al ver que no están en memoria real las recupera del archivo de intercambio. Incluso si la aplicación es tan grande que excede la memoria real instalada, habrá partes de ella en memoria física y otras partes en memoria virtual.
  • 12. El modelo de memoria utilizado en Win32 se basa en dos tablas de vectores, GDT y LDT apuntadas por registros específicos del procesador. Se llama "modelo de memoria plana" en oposición con el más antiguo llamado "segmentado" (propio del DOS y Win16). La memoria en lugar de dividirse en segmentos estancos, se divide en páginas contiguas. El procesador tiene la posibilidad de detectar si una página no está presente en memoria real y a partir de ahí hay una serie de procedimientos para recuperarla desde la memoria virtual. Los mecanismos de gestión de memoria están integrados en el kernel de Windows32 y su explicación cae fuera de los alcances previstos para este escrito. Segmentación Si observamos con atención la pantalla del DEBUG, notaremos los cuatro registros de segmento denominados DS, ES, SS y CS. Tal como se ha dicho cada registro de segmento tiene la misión específica de direccionar segmentos de datos, stack y código. Como cuando Intel dio a luz este esquema de direccionamiento los registros de los procesadores eran de 16 bits y un MB de memoria era una cantidad fabulosa reservada sólo para los computadores de laboratorio, se decidió que la forma en que se direccionaría la memoria sería combinando dos segmentos como sigue: DIRECCION EFECTIVA = 10h * SEGMENTO + OFFSET tanto "segmento" como "offset" son registros que contienen un vector de 16 bits, y por lo tanto pueden elegir entre 64 k direcciones distintas. En pocas palabras, elegido el segmento, el procesador podía direccionar dentro del segmento 64 k posiciones de memoria distintas. Ejemplo CS= 3701h IP= 0100h 10h*CS = 37010 h + IP = 0100 h D.Eff. = 37110 h La notación usada para expresar una dirección efectiva (o dirección absoluta) es SEG:OFFS; por ejemplo la próxima instrucción a ejecutar está en la dirección CS:IP. De lo anterior obtenemos las siguientes conclusiones:  Existen 64 k segmentos posibles (los registros de segmento son de 16 bits)  Con esta notación se pueden expresar direcciones entre 00000 y 10FFEFh, en decimal 1.114.095 (no hasta FFFFFh o 1.048.575 = 1 MB como parecería lógico)  La alineación es cada 10h bits (la dirección efectiva de comienzo de segmento termina en 0h). 10h bytes se llaman parágrafos. Es común decir que las direcciones efectivas de comienzo de segmento se alinean en parágrafos (lo cual es obvio, desde que en el comienzo de segmento el offset es 0)  Una misma dirección efectiva puede expresarse de muchas maneras usando combinaciones entre segmento y offset (37110 h = 3701:0100 = 3600:0111, etc)  Tanto segmento como offset son dos cantidades sin signo (no puede haber un offset negativo)
  • 13. MODULO 3 Cuando termine de leer esta página deberá conocer: Instrucciones básicas del X86 Instrucciones básicas 8086 Este listado no pretende ser un substituto del manual Intel de instrucciones del 8086 -del que fervientemente recomiendo una minuciosa lectura una vez que haya comprendido bien esto- sino la más breve descripción posible para poder avanzar un poco más en los aspectos más básicos que se precisan para comprender el tutorial de lenguaje Assembly de +gthorne. Esta es una lista completa de instrucciones 8086 a las que sólo le faltan las instrucciones ESC, LOCK y WAIT, que no son útiles a nuestros fines inmediatos. En la siguiente tabla se muestran encolumnados los Mnemónicos (como MOV), los operandos (como fuente, destino) y la descripción de la operación. Los operandos son combinaciones entre tipos (registro, memoria e inmediato) con los direccionamientos admitidos en cada instrucción. Las instrucciones IN y OUT admiten un cuarto tipo de operando: puertos de I/O, con direccionamiento registro o inmediato. Instrucciones de movimientos de datos MOV destino,fuente ;la única instrucción que utiliza todos los tipos de direccionamiento XCHG destino,fuente ;Intercambia los contenidos de destino y fuente XLAT tabla_fuente ;carga el registro AL con el byte direccionado por (BX+AL) LAHF ;carga las flags S, Z, A, P y C en AH SAHF ;guarda AH en el registro de flags LDS destino,fuente ;transfiere un puntero de 32 bits al registro DS y al registro destino LES destino,fuente ;transfiere un puntero de 32 bits al registro ES y al registro destino LEA destino,fuente ;transfiere el offset de fuente (una dirección) a destino (un registro) PUSH fuente ;guarda fuente en el stack (en la dirección SS:SP) POP destino ;recupera del stack (dirección SS:SP-1) y guarda en registro destino PUSHF ;almacena el registro de flags en/desde el stack POPF ;recupera el registro de flags en/desde el stack PUSHA ; almacena los reg DI,SI,BP,SP,BX,DX,CX,AX en/desde el stack POPA ;recupera los reg DI,SI,BP,SP,BX,DX,CX,AX en/desde el stack IN origen ;carga desde un puerto origen un byte o word en AL o AX OUT destino ;escribe Al o AX en el puerto destino (direccionam. inmediato o DX) Las operaciones aritméticas ADD destino,fuente ;suma fuente + destino y guarda el resultado en destino ADC destino,fuente ;suma fuente + destino + Carry y guarda el resultado en destino
  • 14. SUB destino,fuente ;resta destino - fuente y guarda el resultado en destino SUB destino,fuente ;resta destino - fuente - Carry y guarda el resultado en destino MUL fuente ;multiplica AL o AX * fuente y guarda el resultado en DX:AX IMUL fuente ;igual que la anterior pero con numeros enteros con signo DIV fuente ;divide DX:AX / fuente y guarda cociente en AX y resto en DX IDIV fuente ;igual que la anterior pero con numeros enteros con signo AND destino,fuente ;opera destino AND fuente y guarda resultado en destino OR destino,fuente ;opera destino OR fuente y guarda el resultado en destino XOR destino,fuente ;opera destino XOR fuente y guarda el resultado en destino NOT destino ;el NOT cambia todos los 1 en 0 y los 0 en 1 de destino. NEG destino ;NEG realiza el complemento a 2 de destino INC destino ;Incremente an 1 el contenido de destino DEC destino ;Decrementa en 1 el contenido de destino DAA / DAS ;Efectúa el ajuste decimal en suma / resta del registro AL AAA/AAD/ AAM/AAS ;ajustan el registro AL a valor decimal desempaquetado (para aplicar en operaciones suma, resta, multiplicación y división) Instrucciones de rotación RCL destino,contador ;rota destino a traves de carry a la izquierda contador veces RCR destino,contador ;rota destino a traves de carry a la derecha contador veces ROL destino,contador ;rota destino a la izquierda contador veces ROR destino,contador ;rota destino a la derecha contador veces SAL destino,contador ;desplaza destino a la izquierda contador veces y rellena con ceros SAR destino,contador ;desplaza destino a la derecha contador veces y rellena con bit SF SHR destino,contador ;desplaza destino a la derecha contador veces y rellena con ceros NOTAS SOBRE INSTRUCCIONES DE ROTACIÓN
  • 15.
  • 16.  El bit SF (signo) es el que está más a la izquierda : si destino es operando es de 8 bits "SF" es el bit número 7 y si destino es un operando de 16 bits, es el bit número 15  En el procesador 8086 se permite un dato inmediato en lugar de especificar un registro como contador solo si ese dato inmediato es 1. Por lo tanto, para uno de esos procesadores la instrucción RCL AX,1 es válida mientras que la RCL AX,5 no lo es. A partir de 80286 se puede especificar cualquier numero de rotaciones como dato inmediato. Como DEBUG presupone 8086, cualquier valor inmediato distinto de 1 da error.  Si en un programa para 8086 se desean rotar más de un bit a la vez, el valor contador se carga en CL  Para rotar un nibble (lo que es muy común en la conversión de binario a BCD) es más rápido y ocupa menos memoria si se utilizan 4 rotaciones de contador igual a 1 que si se utiliza el registro CL  Las instrucciones SAL y SHL son equivalentes  La flag de Overflow cambia con una logica precisa si la rotación es de una posición. En caso de rotaciones mayores, OVF queda indefinida.  En los procesadores 80286 en adelante la rotación se hace MODULO 32, es decir que se rotará la cantidad de veces igual al resto de la división contador/32, o sea que ROL AX,33 equivale a ROL AX,1 o ROL AX,65.  Una rotación con CL=0 equivale a un NOP de dos bytes Instrucciones de comparación CMP destino,fuente ;compara fuente y destino. Modifica las flags V, Z, S, C, P y AC TEST destino,fuente ;AND entre fuente y destino . Ninguno de los operandos cambia. TEST modifica las mismas flags que CMP pero siempre deja a V = 0 y C = 0. Instrucciones de strings CMPS string_destino,string_fuente ;compara las dos cadenas de a bytes o words CMPSB string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (bytes) CMPSW string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (words) LODS string_fuente ;mueve un byte o una word desde fuente a AL o AX LODSB string_fuente ;origen indicado por DS:SI (mueve un byte a AL) LODSW string_fuente ;origen indicado por DS:SI (mueve una word a AX) STOS string_destino ;mueve un byte o una word al destino desde AL o AX STOSB string_destino ;destino indicado por ES:DI (mueve AL a un byte) STOSW string_destino ;destino indicado por ES:DI (mueve AX a una word) MOVS string_destino,string_fuente ;mueve un byte o word de fuente a destino MOVSB string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (un byte) MOVSW string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (una word) SCAS string_destino ;compara la cadena de destino con AL o AX SCASB string_destino ;destino indicado por ES:DI (compara AL con un byte)
  • 17. SCASW string_destino ;destino indicado por ES:DI (compara AX con una word) En todos los casos, si se utiliza el prefijo REP, la cantidad de elementos de la cadena a operar está dada por el contenido del registro CX, si no es un solo elemento de la cadena. A cada operación, CX es decrementado y SI y DI son incrementados o decrementados de acuerdo con el estado de la flag de dirección (Si D=0, se incrementan). El incremento o decremento de estos registros se hace de a uno si son operaciones de bytes o de a dos si son de a words. Para los casos en que se especifica el largo del operando con la B o W final, la string_destino está apuntada por ES:DI, la string_fuente está apuntada por DS:SI . Instrucciones de repetición LOOP offset ;decrementa CX. Si CX no es cero, salta a offset (IP = IP + offset) LOOPZ offset ;decrementa CX, Si CX <> 0 y Z = 1 , salta a offset (IP = IP + offset) LOOPNZ offset ;decrementa CX, Si CX <> 0 y Z = 0 , salta a offset (IP = IP + offset) En todos los casos, si no se produce el salto, se ejecuta la próxima instrucción REP instrucción ;decrementa CX y repite la siguiente instrucción MOVS o STOS hasta que CX=0 REPZ instrucción ;igual que REP, pero para CMPS y SCAS. Repite si la flag Z queda en 1 (igualdad) REPNZ instrucción ;igual queREPZ, pero repite si la flag Z queda en 0 (las cadenas son distintas) Instrucciones de salto CALL destino ;llama a procedimiento. IP <-- offset de destino y CS <-- segmento de destino RET valor ;retorna desde un procedimiento (el inverso de CALL), valor es opcional INT número ;llamado a interrupción. CS:IP <-- vector de INT.Las flags se guardan en el stack INTO ;llama a la INT 4 si la flag de overflow (V) está en 1 cuando se ejecuta la instrucción IRET ;retorna de interrupción al programa restaurando flags JMP dirección ;Salta incondicionalmente al lugar indicado por dirección JA offset ;salta a IP + offset si las flags C=0 Y Z=0 (salta si primer operando es mayor) JAE offset ;salta a IP + offset si la flag C=0 (salta si primer operando es mayor o igual) JB offset ;salta a IP + offset si las flags C=1 (salta si primer operando es menor)(igual a JC) JBE offset ;salta a IP + offset si las flags C=1 o Z=1 (salta si primer operando es menor o igual) JZ offset ;salta a IP + offset si las flags Z=1 (salta si primer operando es igual al segundo)(=JE) JG offset ;salta a IP + offset si las flags S=V Y Z=0 (salta si primer operando es mayor) JGE offset ;salta a IP + offset si las flags S=V (salta si primer operando es mayor o igual) JL offset ;salta a IP + offset si las flags S<>V (salta si primer operando es menor) JLE offset ;salta a IP + offset si las flags S<>V o Z=1(salta si primer operando es menor o igual) JNC offset ;salta a IP + offset si la flag C=0 (salta si no hay carry) JNZ offset ;salta a IP + offset si la flag Z=0 (salta si no son iguales o no es cero) JNO offset ;salta a IP + offset si la flag V=0 (salta si no hay overflow) JNP offset ;salta a IP + offset si la flag P=0 (salta si no hay paridad -o la paridad es impar =JPO) JNS offset ;salta a IP + offset si la flag S=0 (salta si no hay hay bit de signo) JO offset ;salta a IP + offset si la flag V=1 (salta si hay desbordamiento -overflow) JP offset ;salta a IP + offset si la flag P=1 (salta si la paridad es par ) (=JPE)
  • 18. JS offset ;salta a IP + offset si la flag S=1 (salta si el signo es negativo) JCXZ offset ;salta a IP + offset si la flag CX=0 (salta si el registro CX es cero) Las instrucciones de saltos por Above o Below se refieren entre dos valores sin signo (JA, JAE, JB y JBE), mientras que las Greater y Less se refieren a la relación entre dos valores con signo (JG, JGE, JL y JLE). . Instrucciones que afectan flags CLC/CMC/STC ;pone a cero / complementa / pone en 1 la flag C (carry) CLD/STD ;pone a cero / uno la flag de dirección (D=0 hace que SI y DI se incrementen) CLI/STI ;deshabilita / habilita las interrupciones por hardware enmascarables Instrucciones misceláneas NOP ;no-operación: el procesador pasa a la instrucción siguiente sin hacer nada CBW ;convierte el byte de AL en palabra (AX), copiando el bit 7 a todo el registro AH CWD ;convierte word en double-word, copiando bit 15 de AX a todo el registro DX HLT ;el procesador se detiene hasta que llegue un Reset o una interrupción por hard. Alguien puede preguntarse para qué puede servir una instrucción que no hace absolutamente nada como la NOP. Simplemente para llenar espacio, y es realmente una de las instrucciones más útiles para un cracker, a punto tal que algunos mecanismos anticracking sofisticados buscan durante la ejecución de un programa si alguien lo arregló sustituyendo algunas instrucciones por NOPs, y en caso de detectarlo, abortan la ejecución. Pero esto es tema para más adelante.
  • 19. MODULO 4 Cuando termine de leer esta página deberá conocer:  Uso del DEBUG Posiblemente sea el debug el depurador más rudimentario que existe; pero el hecho que desde el principio haya sido provisto con el sistema operativo, nos permite encontrarlo hoy en cualquier máquina DOS o Windows. Muchas tareas elementales pueden realizarse sin otra ayuda que el Debug y por eso vamos a ver algunos comandos básicos. Incluso es posible correr programas cargados en memoria utilizando breakpoints elementales, ejecutar paso a paso, saltar sobre procedimientos, editar programas en hexa y muchas más cosas. Ya hemos dicho cómo podemos arrancarlo desde una ventana DOS, y usando el comando R (mostrar registros) nos mostrará algo similar a esto: AX=0000 BX=0000 CX=0000 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=1332 ES=1332 SS=1332 CS=1332 IP=0100 NV UP EI PL NZ NA PO NC 1332:0100 C3 RET . Esto muestra el contenido de los registros del procesador incluyendo varias banderas: en el ejemplo, y en el mismo orden tenemos: V=0, D=0, I=1, S=0, Z=0, AC=0, P=0 y C=0 Si ponemos después de la R el nombre de un registro, es posible modificar su contenido. Por ejemplo, para editar el contenido de CX, hay que poner el comando RCX. Debug nos presenta el contenido actual del registro y la posibilidad de ingresar un nuevo valor para sustituirlo. Los comandos L y W se utilizan para leer y escribir en archivos de disco. La cantidad de bytes transferida en cada operación es el contenido de BX:CX. Previamente es necesario darle un nombre al archivo con el comando N. Se puede especificar la dirección a partir de la que se desea transferir datos o bien usar el vector por defecto DS:DX. Los comandos más útiles y más usados en Debug son: A dirección Ensamblar (ingresar código assembly) D dirección cantidad Mostrar en pantalla direcciones de memoria en presentación hexa E dirección Editar memoria desde dirección F direc1 direc2 valor Llenar memoria desde direc1 hasta direc2 con el dato valor G dirección Ir (durante la ejecución) a la dirección dirección H valor1 valor2 Muestra el resultado de la suma y resta hexadecimal entre valor1 valor2 I puerto Obtiene una entrada desde el puerto puerto M direc1 direc2 direc3 Mueve el bloque de memoria direc1- direc2 a partir de direc3 P cant Salta sobre procedimientos cant de veces o hasta dirección direc Q Sale de Debug S direc1 direc2 valores Busca en bloque de memoria desde direc1 hasta direc2 los bytes valores T cant Igual que P pero son instrucciones simples U direc cant Desensambla cant bytes a partir de la dirección direc XS Muestra estado de memoria expandida
  • 20. ? Presenta pantalla de ayuda Nuestro primer programa Usaremos el Debug para ensamblar un programa que realice algo tan útil (?) como dejar en alguna parte de la memoria el nombre de nuestra escuela ECCE. Para sacar algo a pantalla, debemos leer el tutorial de +gthorne, que será nuestro paso siguiente. Por ahora sólo queremos practicar de manera que abramos una ventana DOS y escribamos DEBUG (enter). Nos proponemos hacer que ECCE sea escrito en memoria, en el offset 200h de nuestro segmento de datos DS. Sabemos que los códigos ASCII son E=45h y C=43h, de manera que nuestro programa puede lucir así: a 100 1322:0100 mov ax,4543 ;cargamos el registro AX con el dato 4543 (EC en ASCII) 1322:0103 mov bx,4345 ;cargamos BX con "CE" en ASCII 1322:0106 mov [200],ax ;ponemos AX en la dirección de memoria 200 1322:0109 mov [202],bx ;idem para BX, pero en la 202 (AX ocupó la 200 y 201) 1322:010D int 20 ;finalizar y salir a Debug 1322:010F Al apretar "enter" una vez más, Debug nos devuelve su prompt "-" y ya estamos listos para nuestro próximo comando. Podemos ver algunas curiosidades del listado anterior: 1) Debug asume que los números que le damos, sean direcciones o datos, son hexadecimales. 2) A medida que vamos ingresando el programa, nos va devolviendo la dirección de almacenamiento de la próxima instrucción que escribiremos. 3) Las tres primeras instrucciones MOV ocuparon de memoria de programa 3 bytes cada una, pero la cuarta ocupó 4 bytes y la INT 20 sólo ocupó 2 bytes. 4) Aunque nada se ha hablado de la INT 20, es lo que por el momento usaremos para terminar el programa . 5) Cuando hacemos referencia al contenido de una posición de memoria, encerramos la dirección entre corchetes []. Es muy importante saber distinguir entre la dirección y el valor almacenado en esa dirección de memoria. Nuestra lógica es muy simple: cargamos el ASCII "EC" en AX y lo dejamos en la dirección 200. Luego cargamos "CE" en BX y lo dejamos en la 202. Tanto AX como BX han sido meros vehículos para cargar la memoria con datos y sólo a los efectos didácticos porque también está permitido : MOV word ptr [200],4543 ; cargar la word de memoria 200 directamente con el dato 4543 Esta instrucción ocupa 6 bytes, de modo que no ganamos espacio poniéndola en lugar del más elíptico procedimiento de cargar AX y con éste escribir en 200. El prefijo "word ptr" es para que el procesador sepa que lo que moveremos a 200 es una word y no un byte o double-word. Veamos cómo se ve nuestro programa usando el comando desensamblar:
  • 21. -u 100 (desensamble a partir de la CS:100) (Nótese que Debug listará usando sólo mayúsculas, sin importar cómo escribimos nuestro código) 1322:0100 B84345 MOV AX,4543 1322:0103 BB4543 MOV BX,4345 1322:0106 A30002 MOV [200],AX 1322:0109 891E0202 MOV [202],BX 1322:010D CD20 INT 20 NOTA: el valor de 1322 (el contenido del registro CS) es válido para la PC donde se escribió este ensayo. Por lo general los valores no coinciden de una a otra PC, salvo que las instalaciones de software sean idénticas y en ambas estén corriendo previamente al DEBUG los mismos programas. El listado es más largo, pero las líneas que siguen hacia abajo son alguna cosa que estaba en memoria, ya que Debug desensambla por defecto los 20h primeros bytes desde la dirección indicada (o desde la que esté apuntando), y en nuestro programa sólo hemos usado 0Fh bytes (15 en decimal). Echémosle un vistazo: Ajá!!, Debug no deja de sorprendernos, en una columna entre la dirección y el listado en lenguaje assembly puso unos números hexadecimales. Son los códigos de operación (opcodes) que es lo que en definitiva se almacena en memoria y lo que nuestro Pentium debe interpretar y ejecutar. Debug compiló nuestro programa ingresado en assembly y produjo ese código binario con representación hexadecimal para que el Pentium lo interprete. Antes de correr el fabuloso programa que hemos escrito, tenemos que ver qué hay en la posición de memoria 200. Para ello usamos el comando D 200, que nos muestra la basura que hay en nuestra RAM desde DS:0200 hasta DS:027F. Como deseamos leer claramente nuestro nombre ECCE, vamos a llenar este espacio con ceros usando el comando - F 200 23F 00 con lo que le indicamos a Debug que debe llenar el bloque de memoria que comienza en 200 y termina en 23F con "00". Para estar seguros, escribamos nuevamente el comando D 200. Debemos ver las cuatro primeras filas del listado con los datos en 00. Estamos listos para correr nuestra maravilla. Con el comando R nos aseguramos que CS:IP esté apuntando al inicio de nuestro programa (o sea a CS:0100). Para nuestro caso CS vale 1322, pero como ya se ha dicho, puede que en otra PC tenga otro valor. Corramos el programa con el comando G. Debug nos debe informar: El programa ha finalizado con normalidad. Bien! todo fue de maravillas. Veamos si nuestras siglas brillan en las posiciones 200 a 203 con el comando D 200 Esperábamos los hexa 45,43,43,45 a partir de la 200 (miremos además en la columna ASCII del Debug, en donde claramente nos dice CEEC) y están al revés. Qué habrá pasado? Será que hemos
  • 22. escrito BX en 200 y AX en 202?. Usemos al Debug para depurar , que para eso Bill Gates lo ha puesto donde está. Repitamos el comando F 200 23F 00 para dejar nuevamente en cero la memoria y ejecutemos nuestro programa paso a paso. Primero el comando R. Nos debe decir que IP apunta a 0100: 1322:0100 B84345 MOV AX,4543 -T (comando para ejecutar una sola instrucción). Lo relevante es: AX=4543 e IP=0103 1322:0103 BB4543 MOV BX,4345 es la próxima instrucción. Ejecutemos con T: 1322:0106 A30002 MOV [200],AX Ejecutemos el comando D 200 para ver qué hay en la memoria: hasta ahora 00 de la dirección 200 a la 203. Todo ok, porque hasta aquí sólo hemos cargado los registros AX y BX. Hagamos otro T. 1322:0109 891E0202 MOV [0202],BX es la próxima instrucción Hemos guardado AX en la dirección 200 y por lo tanto debería haber un 4543 ("EC" en ASCII) en las direcciones 200 y 201. Verifiquemos con el comando D 200: 1322:0200 43 45 00 00 ........ CE................ QUE PASO???? Está al revés. Tengo "CE" en lugar de "EC". Mmmmm!! Mr Intel tiene algo que ver con esto: Resulta que lo que leemos en AX como "EC", en la realidad lo debemos asumir como : En AL tengo un 43 ("C") y en AH un 45 ("E"). Y el procesador hace algo sumamente lógico, a la porción más baja del registro (AL) la almacena en la dirección de memoria más baja (200) y a la porción más alta del registro (AH) la almacena en la dirección de memoria más alta (201). Todo parece bien pero no funciona? Pero está bien tal como lo hizo Intel. Si leemos la memoria en sentido de direcciones ascendentes, debemos acostumbrarnos a leer los registro (y a cargarlos, ahí fue donde nos equivocamos!) desde la porción más baja hacia la más alta. Por lo tanto, debemos rescribir nuestro programa para que en AL se almacene la primera letra ("E") y en AH la segunda ("C"), y lo mismo para BX: a 100 1322:0100 MOV AX,4345 1322:0103 MOV BX,4543 (enter) nuevamente para salir del comando A. Ahora debemos modificar el registro IP, que nos quedó apuntando a la mitad del programa:
  • 23. RIP (enter) nuestro comando IP 0109 respuesta de Debug :100 (enter) este valor lo ingresamos nosotros para decirle que queremos a IP=0100 Ejecutamos el programa nuevamente con G y examinamos la memoria con D 200 para ver nuestro hermosa sigla ECCE ya en su lugar y en el orden debido. Acepte este buen consejo: No siga adelante si algo no quedó claro. Reléalo, busque otra fuente, alguien que le pueda explicar más claro que yo, pero no lea +gthorne sin haber entendido aunque sea la mecánica con que operan los procesadores. Con el tiempo podrá memorizar los mnemónicos de las instrucciones, con muy poca práctica puede dominar Debug y sus comando heredados de una era sombría de las PCs.
  • 24. INTERRUPCIONES – Conceptos Basicos 1. Una historia vieja como la PC Hace muchos años, en un país muy lejano, un gigante azul se sintió solo en sus alturas y dijo: "No es bueno que el programador solo trabaje en su oficina. Hagamos una computadora personal para que también pueda llevarse el trabajo a su casa". Y así lo hizo. Esa decisión nos puso, amigo deseoso de convertirse en cracker que estas leyendo esto, en contacto unos 20 años después. IBM tomó una decisión respecto a la arquitectura de sus computadoras personales destinada a marcar un cambio notable en la historia de la tecnología. Adoptó una arquitectura abierta, esto es, utilizó componentes que estaban en el mercado en lugar de fabricar chips propietarios. Al tomar esta resolución, Intel pasó a ser la opción más clara como proveedor de procesadores y periféricos: por aquél entonces acababa de salir al mercado la línea de 16 bits 8086 y existían muchos periféricos de 8 bits de su predecesor, el 8085, tales como el controlador de interrupciones 8259, el PPI 8255, DMA 8237, la UART 8251, el timer 8253. En los procesadores Intel de la línea X86, hay dos tipos de interrupciones: por hardware y por software. En las primeras, una señal llega a uno de los terminales de un controlador de interrupciones 8259 y éste se lo comunica al procesador mediante una señal LOW en su pin INT. El procesador interroga al 8259 cuál es la fuente de la interrupción (hay 8 posibles en un 8259) y este le muestra en el bus de datos un vector que la identifica. Por instrucciones de programa, se puede instruir al 8086 para que ignore la señal en el pin INT, por lo que estas interrupciones se denominan "enmascarables". Hay un pin adicional llamado NMI, que se comporta como una interrupción, pero imposible de bloquear (Non-Maskable-Interrupt). 2. Tipos de interrupciones Las interrupciones por software se comportan de igual manera que las de hardware pero en lugar de ser ejecutadas como consecuencia de una señal física, lo hacen con una instrucción. Hay en total 256 interrupciones, de la 0 a la 7 (excepto la 5) son generadas directamente por el procesador. Las 8 a 0Fh son interrupciones por hardware primitivas de las PC. Desde la AT en adelante, se incorporó un segundo controlador de interrupciones que funciona en cascada con el primero a través de la interrupción 2 (de ahí que en la tabla siguiente se la denomine múltiplex). Las 8 interrupciones por hardware adicionales de las AT se ubican a partir del vector 70h. Decimal Hexa Generada Descripción 0 0 CPU División por cero 1 1 CPU Single-step 2 2 CPU NMI 3 3 CPU Breakpoint 4 4 CPU Desbordamiento Aritmético 5 5 BIOS Imprimir Pantalla 6 6 CPU Código de operación inválido 7 7 CPU Coprocesador no disponible 8 8 HARD Temporizador del sistema (18,2 ticks por seg) 9 9 HARD Teclado 10 0A HARD Múltiplex
  • 25. 11 0B HARD IRQ3 (normalmente COM2) 12 0C HARD IRQ4 (normalmente COM1) 13 0D HARD IRQ5 14 0E HARD IRQ6 15 0F HARD IRQ7 (normalmente LPT1) 112 70 HARD IRQ8 (reloj de tiempo real) 113 71 HARD IRQ9 114 72 HARD IRQ10 115 73 HARD IRQ11 116 74 HARD IRQ12 117 75 HARD IRQ13 (normalmente coprocesador matemático) 118 76 HARD IRQ14 (normalmente Disco Duro) 119 77 HARD IRQ15 En cuanto a las interrupciones por software, están divididas entre las llamadas por el BIOS (desde la 10h a la 1Fh) y las llamadas por el DOS (desde la 20h hasta la 3Fh). Esto es sólo la versión oficial, ya que en realidad las interrupciones entre BIOS y DOS se extienden hasta la 7Fh. 3. Cómo funciona una interrupción A partir del offset 0 del segmento 0 hay una tabla de 256 vectores de interrupción, cada uno de 4 bytes de largo (lo que significa que la tabla tiene una longitud de 1KB). Cada vector está compuesto por dos partes: offset (almacenado en la dirección más baja) y segmento (almacenado en la dirección más alta). Cuando se llama a una interrupción (no importa si es por hardware o por software), el procesador ejecuta las siguientes operaciones: 1. PUSHF (guarda las banderas en el stack) 2. CTF/DI (borra la bandera de Trap y deshabilita interrupciones) 3. CALL FAR [4 * INT#] (salta a nueva CS:IP, almacenando dirección de retorno en stack) La expresión 4 * INT# es la forma de calcular la dirección de inicio del vector de interrupción a utilizar en el salto. Por ejemplo, el vector de la INT21h estará en la dirección 84h Al efectuarse el salto, la palabra almacenada en la dirección más baja del vector sustituye al contenido del registro IP (que previamente fue salvado en el stack) y la palabra almacenada en la dirección más alta sustituye al contenido del registro CS (también salvado en el stack). Por ejemplo: La instrucción INT 21h es la usada para efectuar llamadas a las funciones del DOS. Supongamos que en la posición de memoria 0000:0084 está almacenada la palabra 1A40h y en la dirección 0000:0086 está almacenada la palabra 208Ch. La próxima instrucción que se ejecute es la que está en la posición 20C8:1A40 (nuevo CS:IP). El final de una rutina de interrupción debe terminarse con la instrucción IRET, que recupera del stack los valores de CS, IP y Flags. Notemos que un llamado a interrupción implica el cambio de estado automático de la bandera de habilitación de interrupciones. En pocas palabras, esto significa que al producirse una interrupción, esta bandera inhabilita futuras interrupciones. Como la instrucción IRET restablece el registro de
  • 26. flags al estado anterior que tenia antes de producirse la interrupción, las próximas interrupciones se habilitan en el mismo momento en que se produce el retorno desde la rutina de servicio. 4. Paso de parámetros desde el programa a la ISR Cuando las interrupciones son llamadas por software mediante la instrucción INT xx, por lo general se le deben pasar parámetros a la rutina de servicio de interrupción (ISR). Estos parámetros definen la tarea que debe cumplir la ISR y son pasados en los registros del procesador, lo que es una opción muy veloz. Un ejemplo casi extremo, en donde muchos de los registros del 8086 son utilizados son algunos servicios cumplidos por la INT 13h (disco). Para tomar sólo un caso, en una operación de escritura de un sector, los parámetros se pasan de la siguiente manera: Registro Asignación AH 03 (servicio de escritura de sectores) AL cantidad de sectores a escribir CH 8 bits más bajos del número de cilindro CL(bits 0-5) número de sector CL(bits 6 y 7) 2 bits más altos del número de cilindro DH número de cabeza DL número de unidad de disco (hard: mayor a 80h) BX offset del buffer de datos ES segmento del buffer de datos Si bien no está escrito en ningún lado, las interrupciones utilizan el registro AH para identificar el tipo de operación que deben ejecutar. Cuando una interrupción devuelve códigos de error siempre vienen en el registro AL, AX y/o en la bandera de Carry. 5. La interrupción más famosa Sin lugar a dudas se trata de la INT 21h (funciones del DOS). El número de función se pasa en el registro AH Función Descripción 00h Terminar un programa 01h Entrada de caracteres con salida 02h Salida de un caracter 03h Recepción de un caracter por el puerto serial 04h Envío de un caracter por el puerto serial 05h Salida por puerto paralelo 06h Entrada/salida de caracteres directa
  • 27. 07h Entrada/salida de caracteres directa 08h Entrada de caracteres sin salida 09h Salida de un string de caracteres 0Ah Entrada de un string de caracteres 0Bh Leer estado de una entrada 0Ch Borra buffer de entrada y llama a entrada de caracteres 0Dh Reset de los drivers de bloques 0Eh Selección de unidad actual 0Fh Abrir archivo usando FCBs (File Control Blocks) 10h Cerrar archivo (FCBs) 11h Busca primera entrada de directorio (FCBs) 12h Busca siguiente entrada de directorio (FCBs) 13h Borrar archivo(s) (FCBs) 14h Lectura secuencial (FCBs) 15h Escritura secuencial (FCBs) 16h Crear o vaciar un archivo (FCBs) 17h Renombrar archivos (FCBs) 18h Obsoleta 19h Obtener denominación de dispositivo, unidad actual 1Ah Fijar dirección para DTA (Disk Transfer Area) 1Bh Obtener información sobre unidad actual 1Ch Obtener información sobre una unidad cualquiera 1Dh/1Eh Obsoletos 1Fh Fijar puntero a DPB (Drive Parameter Block) a la unidad actual 20h Obsoleta 21h Lectura random (FCB) 22h Escritura random (FCB) 23h Leer tamaño de archivo (FCB) 24h Establecer número de registro (FCB) 25h Establecer vector de interrupción 26h Crear nuevo PSP (Program Segment Prefix) 27h Lectura random de varios registros (FCB) 28h Escritura random de varios registros (FCB) 29h Transferir nombre de archivo al FCB 2Ah Obtener fecha 2Bh Establecer fecha 2Ch Obtener hora 2Dh Establecer hora 2Eh Fijar bandera de Verify 2Fh Obtener DTA 30h Obtener número de versión del DOS 31h Terminar programa pero dejarlo residente en memoria 32h Obtener puntero a DPB de una unidad específica 33h Leer/escribir bandera de break
  • 28. 34h Obtener dirección de bandera INDOS 35h Leer vector de interrupción 36h Obtener espacio de disco disponible 37h Obtener/fijar signo p/separador de línea de comandos 38h Obtener/fijar formatos específicos de un país 39h Crear subdirectorio 3Ah Borrar subdirectorio 3Bh Fijar directorio actual 3Ch Crear o vaciar archivo (handle) 3Dh Abrir archivo (handle) 3Eh Cerrar archivo (handle) 3Fh Leer desde archivo (handle) 40h Escribir en archivo (handle) 41h Borrar archivo (handle) 42h Mover puntero de archivo (handle) 43h Obtener/fijar atributo de archivo 44h Funciones IOCTL (control de I/O) 45h Duplicar handle 46h Duplicación forzosa de handles 47h Obtener directorio actual 48h Reservar memoria RAM 49h Liberar memoria RAM 4Ah Modificar tamaño de memoria reservada 4Bh EXEC: ejecutar o cargar programas 4Ch Terminar programa con valor de salida 4Dh Obtener valor de salida de un programa 4Eh Buscar primera entrada en el directorio (handle) 4Fh Buscar siguiente entrada en el directorio (handle) 50h Establecer PSP activo 51h Obtener PSP activo 52h Obtener puntero al DOS-info-block 53h Traducir Bios Parameter Block a Drive Parameter Block 54h Leer bandera Verify 55h Crear PSP nuevo 56h Renombrar o mover archivo 57h Obtener/fijar fecha y hora de modificación de archivo 58h Leer/fijar estrategia de alocación de memoria 59h Obtener informaciones de error ampliadas 5Ah Crear archivo temporal (handles) 5Bh Crear archivo nuevo (handles) 5Ch Proteger parte de un archivo contra accesos 5Dh Funciones de Share.exe 5Eh Obtener nombres de dispositivos de red 5Fh Obtener/fijar/borrar entrada de la lista de red
  • 29. 60h Ampliar nombre de archivo 61h No usada 62h Obtener dirección del PSP 63h/64h No usadas 65h Obtener información ampliada de pais específico 66h Obtener/fijar página de códigos actual 67h Determinar el número de handles disponibles 68h Vaciar buffer de archivos 69/6A/6B No usadas 6Ch Función Open ampliada 6. Intercepción de interrupciones (hooks) Un programa puede necesitar "enganchar" una interrupción. Supongamos que hemos creado un virus que debe autodestruir su copia en memoria cuando el comando a ejecutar es "scan.exe". Evidentemente debemos interceptar la interrupción 21h, función 4Bh/00 (cargar un programa y ejecutarlo), de tal manera que "nuestra" función verifique si el programa a cargar se llama scan.exe y en tal caso, borre lo que haya que borrar. Esta tarea, se logra en haciendo un programa residente (que puede ser parte del mismo código del virus) para que 1. Cuando se produzca una llamada a la INT21h-4Bh, no se ejecute el código normal del DOS sino nuestro código 2. En él chequearemos si la función es una 4Bh-00, y en caso afirmativo verificamos si el programa a corres se llama scan.exe. Si todo esto es verdadero, sobrescribiremos las partes sensibles a la detección del virus y lo descargaremos de la memoria. 3. Finalmente saltamos a la verdadera INT21h función 4Bh Para lograr esto, es necesario contar con un loader que cargue en memoria nuestro programa. Este loader debe: 1. Reservar un espacio de memoria adecuado al tamaño del código que quedará residente. 2. Averiguar (mediante INT21h-35h) cual es el vector de interrupción de la INT21h. Supongamos que sea 0102:2C40h 3. Poner este vector como dirección de retorno del código residente (por lo general cargándolo en una dirección conocida en donde tiene que estar este valor) 4. Cambiar el vector 4Bh origina por la dirección de inicio de nuestro código residente (digamos 7E00:0000) Lo que sucederá cuando la PC infectada con nuestro virus intente ejecutar un scan.exe es lo siguiente:
  • 30. 1. Dentro del Command.com, se generará un llamado a la INT21-4Bh-00 con scan.exe como parámetro. 2. El procesador buscará el vector para el servicio a la interrupción 21h en la dirección 0000:0084h 3. En ese lugar estará la dirección de inicio de nuestro residente, o sea 7E00:0000, y en ese lugar se inicia el procesamiento de la interrupción. 4. Al ver que la llamada es para ejecutar un programa scan.exe nuestro residente vuelve a poner el vector de INT21h en el valor que le dio el DOS y luego se autodestruye (primero traslada a la parte más baja de la memoria la función de borrado). Como último acto, hace un salto JMP FAR 0102:2C40 5. Esto último hará que se ejecute scan.exe como si nada hubiese sucedido. Frecuentemente los virus utilizan interrupciones en desuso para sus fines (por ejemplo para saber si están activos en memoria). El tema de las interrupciones es tan inmenso que lo que acabamos de ver no es sino un pequeño pantallazo. Quedan cuestiones muy delicadas como la bandera INDOS y las formas de evitar la reentrada. Una descripción muy completa de cada interrupción, que incluye los registros usados para el paso de parámetros, está en el archivo intdos.zip, por Ralph Brown (en inglés) que pueden bajarse de sudden dischargeo asmcoder , dos sitios que les recomiendo si se buscan tutoriales o archivos.
  • 31. MANEJO DE STRINGS EN BIOS, DOS, y WINDOWS 1.- Función BIOS para manejo de strings El BIOS interactúa principalmente de a un caracter por vez con el teclado, pantalla y puerto serial, por lo que a estos se los conoce como dispositivos de caracteres, en contraposición con el drive de diskettes o el disco duro, que son dispositivos de bloques. Aunque menos frecuente que las funciones de manejo de strings del DOS, la función 13h de la INT10h tiene la ventaja que no depende del sistema operativo. Su función es visualizar en la pantalla una cadena de caracteres que deben estar almacenados en un buffer (en memoria). El paso de parámetros se realiza mediante los siguientes registros: Registro Parámetro AH 13 h - define la operación AL Modo de salida: 0 Atributo en BL, mantiene la posición del cursor 1 Atributo en BL, actualiza la posición del cursor 2 Atributo en buffer, mantiene posición del cursor 3 Atributo en buffer, actualiza posición del cursor BL Atributo de caracteres (solo modos 0 y 1) CX Cantidad de caracteres a visualizar DH Línea de la pantalla DL Columna de la pantalla BH Página de pantalla ES:BX Puntero al buffer de memoria Los modos que actualizan la posición del cursor se usan cuando se quieren escribir varios strings uno a continuación del otro. En cambio los modos que la mantienen, se utilizan para escribir mensajes siempre en el mismo lugar de la pantalla. En los modos 0 y 1 todos los caracteres tienen el atributo especificado en BL, mientras que en los modos 2 y 3 en el buffer, seguido a cada caracter esta su byte de atributo, lo que permite que cada caracter tenga un atributo distinto. La cadena en memoria tiene una longitud igual al doble de los caracteres a visualizar. El valor de CX debe ser no obstante igual a la cantidad de caracteres (la mitad del tamaño del buffer). El byte de atributos tiene la siguiente estructura: Bit # Función 7 Intermitencia (1=intermitente, 0=fijo) 6,5,4 Color de fondo (0=negro, 7=blanco) 3,2,1,0 Color del caracter (0=negro, 0Fh=blanco)
  • 32. 2. Funciones DOS de manejo de strings 2.1 Entrada de strings de caracteres: INT21h - función 0Ah Se leen caracteres desde la entrada standard (normalmente teclado) y se transfieren a un búffer en memoria. La operación termina cuando se lee el caracter ASCII 0Dh (CR o retorno de carro), que corresponde a la tecla RETURN (o ENTER). Registro Parámetro AH 0Ah - código de la función DS:DX Puntero al buffer de memoria Estructura del Buffer: Posición Significado DS:DX Cantidad máxima de caracteres admitida en el buffer (debe ser inicializada por el programador) DS:DX + 1 Cantidad de caracteres leída (la escribe el DOS) DS:DX + 2 y subsig. Buffer donde se almacenan los caracteres leídos. La dirección del último es DS:DX + byte ptr (DS:DX) En los dos primeros bytes, la cantidad de caracteres incluye al CR final. Suponiendo que el programador inicialice la posición de memoria DS:DX en 10h, el buffer tendrá un largo total de 16 caracteres, comenzando en DS:DX y finalizando en DS:DX + 0Fh, y podrá aceptar 13 caracteres más el de retorno. DOS no se preocupa por borrar la parte del buffer que no escribe. Veamos en la tabla siguiente, para un buffer de 16 de largo qué caracteres encontramos luego de dos entradas sucesivas, la segunda más corta que la primera: Dirección DS:DX + ... 0 1 2 3 4 5 6 7 8 9 A B C D E F primera entrada 10 0f B u e n o s A i r e s 0d ? segunda entrada 100a C o r d o b a 0d i r e s 0d ? Hay que notar que si bien esta función es muy cómoda, se queda esperando el caracter de retorno y hasta que este no llegue el programa no puede hacer otra cosa que... esperar!. En cambio, si se busca de a un caracter por vez, es posible hacer que el programa consulte el teclado como una de las tantas actividades posibles dentro de un mismo lazo. 2.2 Salida de string de caracteres, INT 21h - función 9h Con esta función se envía un string de caracteres al dispositivo designado como salida standard (normalmente la pantalla). DOS permite redireccionar la salida a un archivo o a un puerto serial o LPT desde la misma línea de comandos, por lo que al usar esta función no hay garantías de que el string aparezca en pantalla. En realidad, esto también es válido para la entrada de caracteres vista
  • 33. en el punto anterior, aunque es mucho más frecuente redireccionar la salida que la entrada. Por ejemplo, el comando interno type archivo hará que el contenido del archivo sea visualizado en la pantalla, pero si agregamos un redirector con un dispositivo de salida "> LPT1", los caracteres del archivo serán direccionados al puerto de la impresora. El string debe finalizar obligatoriamente con el caracter "$" (código ASCII 36). Los caracteres especiales como Bell, Backspace, CR, etc serán tratados como tales. Bell (ASCII 07) hace sonar una campana en el altavoz de la PC, CR vuelve al principio de la línea, Nueva_línea (ASCII 0Ah) pasa a la línea de abajo, etc Al igual que en la lectura de strings, los parámetros son: Registro Parámetro AH 09h - código de la función DS:DX Puntero al buffer de memoria donde reside el string En lenguaje Assembly, un string para usarse con esta función puede ser declarado como sigue: mensaje1 DB "Todos los hombres de buena voluntad",0Dh,0Ah,"$" y para utilizar la función 9h, el código a emplear sería: display: MOV DX, offset mensaje1 MOV AH,9 INT 21H RET 3. Funciones Windows de manejo de strings 3.1 CompareStrings Esta función compara dos strings de caracteres usando como base el juego de caracteres del idioma especificado por el identificador. La sintaxis del llamado es: int CompareString( LCID Locale, identificador de lenguaje del sistema DWORD dwCmpFlags, opciones de comparación LPCTSTR lpString1, puntero al primer string int cchCount1, tamaño (bytes) del primer string LPCTSTR lpString2, puntero al segundo string int cchCount2 tamaño (bytes) del segundo string
  • 34. ); 3.2 GetDlgItemText La función GetDlgItemText captura el titulo o texto asociando con un control en una caja de diálogo. La sintaxis es: UINT GetDlgItemText( HWND hDlg, handle de la caja de diálogo int nIDDlgItem, identificador del control LPTSTR lpString, dirección del buffer para el texto int nMaxCount máxima longitud del string ); 3.3 GetWindowText La función GetWindowText copia el texto de una barra de título de una ventana especificada en un buffer. Si la ventana especificada es un control, lo que se copia es el texto del control. Sintaxis: int GetWindowText( HWND hWnd, handle de la ventana o control LPTSTR lpString, dirección del buffer de texto int nMaxCount máximo número de caracteres a copiar ); 3.4 GetWindowTextLength La función GetWindowTextLength obtiene la cantidad de caracteres que tiene el texto de la barra de título de una ventana o (si la ventana especificada es un control), la cantidad de caracteres dentro del control. La sintaxis es: int GetWindowTextLength( HWND hWnd handle de la ventana o control ); 3.5 lstrcat La función lstrcat adiciona un strin a continuación de otro. Sintaxis: LPTSTR lstrcat(
  • 35. LPTSTR lpString1, dirección del buffer de strings concatenados LPCTSTR lpString2 dirección del string a concatenar con string1 ); 3.6 lstrcmp y lstrcmpi La función lstrcmp compara dos strings de caracteres. La comparación discrimina entre mayúsculas y minúsculas. La función lstrcmpi es idéntica pero no discrimina mayúsculas y minúsculas.- Sintaxis: int lstrcmp( // int lstrcmpi( LPCTSTR lpString1, dirección del primer string LPCTSTR lpString2 dirección del segundo string ); 3.7 lstrcpy La función lstrcpy copia un string en un buffer. Sintaxis: LPTSTR lstrcpy( LPTSTR lpString1, dirección del buffer LPCTSTR lpString2 dirección del string a copiar ); 3.8 lstrcpyn La función lstrcpyn copia un número especificado de caracteres de un string dentro de un buffer. LPTSTR lstrcpyn( LPTSTR lpString1, dirección del buffer LPCTSTR lpString2, dirección del string a copiar int iMaxLength cantidad de caracteres o bytes a copiar ); 3.9 lstrlen La función lstrlen devuelve la longitud en bytes (versión ANSI) o caracteres (versión Unicode) del string especificado (no incluye el caracter NULL de terminación).
  • 36. int lstrlen( LPCTSTR lpString dirección del string ); 3.10 MultiByteToWideChar La función MultiByteToWideChar despliega un string de caracteres en un string Unicode. int MultiByteToWideChar( UINT CodePage, código de página DWORD dwFlags, opciones tipo de caracteres LPCSTR lpMultiByteStr, dirección del string a mapear int cchMultiByte, número de caracteres en el string LPWSTR lpWideCharStr, dirección del buffer Unicode int cchWideChar tamaño del buffer ); 3.11 SetDlgItemText La función SetDlgItemText determina el texto de un control en un box de diálogo. Sintaxis: BOOL SetDlgItemText( HWND hDlg, handle del box de diálogo int nIDDlgItem, identificador del control LPCTSTR lpString puntero al texto ); 3.12 SetWindowText La función SetWindowText cambia el texto en la barra de título de una ventana. Si la ventana es un control, se cambia el texto del control. Sintaxis: BOOL SetWindowText( HWND hWnd, handle de la ventana o del control LPCTSTR lpString dirección del string );
  • 37. PASO DE PARAMETROS EN LOS PROGRAMAS Parte I: COMO PASAN LOS PARAMETROS A LAS INTERRUPCIONES BIOS Y DOS PASO DE PARAMETROS Es posible que una de las partes más tardíamente comprendidas por el principiante de ingeniería inversa es la manera en que pasan los parámetros desde el programa a una función. Este concepto es de fundamental importancia en el estudio de las protecciones y podemos decir sin lugar a dudas que la comprensión de este mecanismo es crucial para el análisis del funcionamiento de un programa DOS o Windows. LA ANTIGUA HISTORIA DEL DOS El viejo DOS en lugar de funciones API utilizaba interrupciones de software (INT 21h y subsiguientes), y un poco más próximo al hardware, el mismo BIOS cuenta con su propio juego de interrupciones. Estas interrupciones de software funcionan igual que cualquier llamada a función, aunque el mecanismo de llamada es distinto, ya que se usa la instrucción INT en lugar de CALL. Por lo general, tanto el DOS como el BIOS pasaban los argumentos en los registros del mismo procesador. Si bien es una estrategia que optimiza la velocidad de procesamiento, tiene sus limitaciones en cuanto a la cantidad de parámetros que se pueden pasar. Otro de los problemas que tiene es que las funciones no pueden ser reentrantes a menos que se tomen previsiones excepcionales, aunque esto no era de mucha importancia ya que el DOS no es multitarea, sería sólo problema para programas residentes. Por lo general se pasaban los parámetros por valor. Por ejemplo, en una interrupción de BIOS de lectura de un sector de disco a memoria (INT 13, subfunción 02) tenemos: reg var significado AH 2 subfunción 2: lectura de un sector AL n cantidad de sectores a leer CH c0 8 bits más bajos del número de cilindro (track) a leer CL s numero de sector a leer (bits 0 a 5) CL c1 2 bits más altos del número de cilindro (bits 6 y 7) DH h número de cabeza lectora DL d número de disco lógico (bit 7 en 1 para discos duros) ES:BX ba dirección de inicio del buffer de lectura en memoria A menos que se trate de aplicaciones muy especiales en que estos valores pueden ser fijos, lo usual es que cada uno de esos parámetros sea una variable que a su vez está almacenada en algún lugar de la memoria. En el siguiente listado que sigue estos parámetros son referidos con nombres
  • 38. simbólicos supuestos y el lector debe tener presente que en el listado de lenguaje de máquina lo que se verán son las direcciones de almacenamiento de estos parámetros. Veremos cómo sería una llamada a la interrupción que lea 4 sectores consecutivos del disco C, ubicados en la pista 801 (0321h), cabeza 3, a partir del sector 12 (0Ch), y que almacene lo leído en la dirección DS:0700. El registro CX en binario debe ser: 0010 0001 11 001100 = 21CC h Los bits 15 a 8 deben ser 21h (ocho bits menos significativos del número de track), los bits 7 y 6 ambos en uno (el 3 del número de track) y los bits 3 y 2 también en uno por en número de sector. En algún lugar del programa se produce la carga de los valores iniciales: PUSH DS ;haremos que los datos se escriban POP AX ;en el segmento de datos DS MOV segme,AX ;almacenamos en la variable segme MOV AX,0700 ;en el offset 0700h MOV offse,AX ;almacenamos en la variable offse MOV AX,0380 ;disco C (80h), cabeza 3 Y luego se cargan los registros desde la memoria antes de llamar a la int 13h MOV curdisk,AL ;almacena 80 en variable curdisk MOV curhead,AH ;almacena 03 en variable curhead MOV AX,21CC ;número de track y sector MOV track0,AH MOV sekt,AL MOV AL,4 ;número de sectores a leer CALL _leedisk ;leer JC _error ;si CY vuelve en 1, hubo error de lectura ... ... _leedisk: ;lectura de disco ... ... MOV DH,AL ;salvar cantidad de sectores a leer MOV AX,segme ;cargar segmento MOV ES,AX MOV BX,offse ;cargar offset de buffer MOV CL,sekt s;ector y 2 bits más altos de track MOV CH,track ;cargar track MOV DL,curdisk ;unidad de disco a utilizar MOV AL,curhead ;numero de cabeza XCHG DH,AL cambiar número de sectores y cabeza MOV AH,2 ;subfunción de lectura INT 13 ;interrupción 13h BIOS disco RET Uno puede preguntarse cuál es el objeto de poner los parámetros en memoria en lugar de cargarlos directamente en los registros apropiados para la llamada a la INT 13h. Es una cuestión de practicidad y buen estilo de programación. Si las variables están en memoria, el programa puede consultarlas en cualquier momento o modificarlas por ejemplo para hacer un lazo. Si se cargan
  • 39. como constantes, tal como sucede en la primera parte de la rutina, en donde se inicializan las variables, servirán solamente para efectuar esa llamada. Por ejemplo, si después de esa primera lectura quisieramos leer los sectores 1 a 7 del mismo track, sólo habría que poner: MOV AL,sekt ;nuevo sector inicial AND AL,C0h ;dejamos solo los dos bits del track (6 y 7) OR AL,1 ;ponemos en 1 el numero de sector MOV sekt,AL ;guardamos nuevamente MOV AL,7 ;numero de sectores a leer CALL _leedisk NOTA IMPORTANTE Un lector de nivel intermedio podría objetar que es posible tratar parte del código como si fuesen variables y de tal modo ahorrarnos un paso, dejando sólo la carga inmediata de registros. El programa se vería así (incluímos ahora una columna para las direcciones del código por razones obvias) CS:1000 MOV DL,80 ;código del disco duro, unidad C CS:1002 MOV AL,4 ;leer cuatro sectores CS:1006 etc etc Si por ejemplo quisiesemos leer 2 sectores y cambiar la unidad C por la A, habría que poner: XOR AL,AL ;poner a cero AL (unidad A) MOV [CS:1001],AL ;cambia la carga de DL MOV AL,2 ;numero de sectores MOV [CS:1003],AL ;cambia carga de AL CALL _leedisk En algunas oportunidades se hace, es una técnica conocida como automodificación, pero no lo recomiendo para principiantes. Por cierto que en lugar de poner la dirección absoluta como se hizo ahora en beneficio de la claridad, es posible utilizar variables del compilador (que se traducen en constantes iguales a CS:1001 y CS:1003 para el programa) El lector puede encontrar en Internet la completa y muy extensa lista de llamadas a interrupción de Ralph Brown (por ejemplo en el sitio sudden discharge ), unas 250 páginas tamaño oficio en letra condensada a dos columnas en donde se incluyen hasta interrupciones propias de virus. Mi mejor consejo si tiene que trabajar con programas DOS es que la consiga y la imprima (y a menos que su vista sea excelente, no la imprima en condensada aunque le lleve el dobe de papel). Hay una versión mucho más condensada y menos exhaustiva que viene para instalar residente, atribuida a Peter Norton y que puede ser suficiente si los programas acceden a las interrupciones más comunes (por ejemplo, no están ni las que se utilizan para redes ni las de los DOS-extenders). Trate de bajarla de nuestro sitio usando este vínculo. EN BUSCA DE ALTERNATIVAS Un poco agotado en las complejidades crecientes, el modo de paso de parámetros mediante
  • 40. registros iba a quedar acotado a rutinas del núcleo de sistemas operativos en donde la velocidad es un factor de gran importancia. Había que buscar alternativas para mejorar la manera en que los parámetros son pasados a las funciones. Consideraremos ahora tres temas íntimamente relacionados con el paso de parámetros. * Paso de valor versus uso de punteros * Cómo opera el stack * Estructuras de datos 1) Paso de argumentos por punteros. En el punto anterior se vio con profundidad el paso de argumentos por valor, es decir, se le entrega a la función convocada el VALOR con el que tendrá que operar. Dentro de las llamadas a interrupcion más comunes, esto es algo inevitable porque los valores pasan en los mismos registros del procesador. Sin embargo cuando se estructuró el ejemplo sobre la lectura de sectores de disco, se hizo un pequeño avance: se colocaban los valores en direcciones de memoria y luego la rutina los recuperaba antes de convocar a la interrupción 13h. El paso de argumentos mediante punteros consiste en una técnica similar, en donde a la función convocada se le dice en qué dirección están los valores con los que tiene que operar. Esta es la manera en que trabajan los compiladores C y Pascal por ejemplo. Supongamos que queremos sumar 7 y 11. En pseudo lenguaje C no sería correcto poner: A = Suma (7,11) que sería más propio de Basic, sino: int A,B=7,C=11; A= Suma(B,C); printf A; Se declaran tres enteros, definiendose el valor de dos de ellos, se llama a una función Suma(x,y) que debe estar definida en otra parte del programa, que usa dos argumentos de entrada y devuelve un entero. Finalmente se imprime el entero resultante. Esto corresponde más o menos con el siguiente listado en lenguaje Assembly: varA DW varB DW 7 varC DW 11 .... .... LEA AX, varA PUSH AX LEA AX, varB PUSH AX LEA AX, varC PUSH AX CALL _add CALL _printAx
  • 41. Lo que en realidad se le está entregando a la función _add son tres valores en el stack que no son los que tiene que sumar, sino las direcciones en donde estan almacenados los datos de entrada y la dirección donde debe almacenar el resultado. Consulte en el punto siguiente cómo opera el stack. El presente ejemplo será resuelto con valores numéricos para que se aprecie bien la diferencia entre puntero y valor. Los valores de las direcciones de los operandos se denominan punteros (porque su valor está "apuntando" al lugar donde está almacenado el dato). Entre otras cosas, esto implica que mientras se está procesando una función tal como _add(x,y), otra tarea puede estar modificando los valores contenidos en las direcciones apuntadas por x e y, lo cual no siempre es deseable. 2) Cómo opera el stack El stack es un espacio particular de la memoria del sistema. Al stack se lo llama pila LIFO (Last In- First Out, el último en entrar, el primero en salir) y es igual a tener una pila de diskettes: si quiero sacar alguno, lo más sencillo es quitar primero todos los de arriba. El funcionamiento del stack se rije por el par de registros SS:ESP (Stack Segment : Extended Stack Pointer), que apunta a la última dirección ocupada por el stack. El puntero al stack se decrementa a medida que el stack se va llenando (porque a medida que crece el stack va ocupando posiciones de memoria cada vez más bajas) e inversamente el puntero crece a medida que el stack se vacía. La instrucción para cargar al stack con parámetros es PUSH, mientras que su inversa es POP. Al producirse una interrupción o un llamado a subrutina, se coloca automáticamente la dirección de retorno (y en ocasiones las flags) en el stack, las que se restauran con la instrucción POP. Supongamos que en el momento antes de una operación PUSH AX, que pone el contenido de AX en el stack, el par SS:SP apunta a 1800:FFEE, y que el contenido de AX es 1234h. Luego del PUSH, la dirección de memoria 1800:FFED contendrá el valor de AH, o sea 12h, la dirección inmediata inferior 1800:FFEC contendrá el valor de AL, o sea 34h y el puntero SS:SP tendrá igual valor (1800:FFEC). varA DW ;direccion de almacenamiento: DS:2000 varB DW 7 ;direccion de almacenamiento: DS:2002 varC DW 11 ;direccion de almacenamiento: DS:2004 A partir de la DS:2000 encontramos (se lista hasta la DS:2007): DS:2000 00 00 07 00 11 00 xx xx Supongamos que SS:SP vale SS:FF2E, LEA AX, varA ;carga en AX la direccion 2000 PUSH AX ;carga en SS:FF2C el valor 20 00 LEA AX, varB ;carga en AX la direccion 2002 PUSH AX ;carga en SS:FF2A el valor 20 02
  • 42. LEA AX, varC ;canga en AX la direccion 2004 PUSH AX ;carga en SS:FF28 el valor 20 04 El stack pointer ahora esta en FF28. Listemos desde SS:FF28 hasta FF2F: SS:FF28 04 20 02 20 00 20 xx xx Notemos que la carga en el stack sigue la convención Intel, poniendo el byte menos significativo en la dirección más baja y el más significativo en la dirección más alta. 3) Estructuras de datos. Con todo, hay veces en las que conviene no hacer referencia a variables aisladas sino manejarlas en grupo, lo que se denomina estructura. Una estructura de datos se compone de miembros los que pueden ser de distinta longitud o naturaleza. Cuando el programa se refiera a la estructura lo hará usando un puntero a la estructura que no es nada más que la dirección de memoria donde comienza. Veamos un ejemplo simple. En un programa encontramos que hacemos constante referencia a la lectura de disco, y por lo tanto decidimos crear nuestra propia estructura para facilitar la escritura del programa. Notemos que los sistemas operativos tienen definidas estructuras para usos específicos. Utilizando lenguaje Assembly, una estructura ejemplo se puede definir como: lectura STRUCT disco db 0 numero de disco sector db 1 numero de sector head db 0 numero de cabeza reser db 0 reservado track dw 0000 numero de track cant dw 0001 cantidad de sectores a leer bufseg dw 2000 segmento del buffer de lectura bufoff dw 0000 offset del buffer de lectura lectura ENDS En las estructuras de datos propias, podemos usar nuestra inmaginación con total libertad, pero las estructuras que necesita el sistema operativo debemos ajustarnos completamente a las posiciones y longitud de los parámetros y disponerlos de la misma manera en que el sistema operativo espera encontrarlos. Hemos reservado un byte para futuros usos y para que los valores de dos bytes se alinien con direcciones pares de memoria. Si por ejemplo el valor del puntero "lectura" fuese 2800, encontraríamos que: la dirección DS:2800 almacena el número de disco la dirección DS:2801 almacena el número de sector
  • 43. la dirección DS:2802 almacena el número de cabeza la dirección DS:2803 es un byte reservado para uso futuro la dirección DS:2804 almacena en dos bytes el numero de track la dirección DS:2806 almacena en dos bytes la cantidad de sectores leer la dirección DS:2808 almacena el segmento del buffer de lectura la dirección DS:280A almacena el offset del buffer de lectura. Si dentro del programa queremos hacer referencia por ejempo a la cabeza lectora, podemos poner: MOV lectura.head,5 seleccionar la cabeza lectora 5 El compilador buscará la dirección de la estructura "lectura", en nuestro ejemplo DS:2800. Luego buscará el elemento "head", que por la definición de la estructura sabe que ocupa un byte y que es el tercero. Por lo tanto el compilador generará una instrucción apropiada para que se almacene el valor 5 en la dirección de memoria DS:2802.
  • 44. PASO DE PARAMETROS EN LOS PROGRAMAS Parte II: COMO PASAN LOS PARAMETROS A LAS FUNCIONES API DE WINDOWS PARAMETROS PARA FUNCIONES API Windows sigue las nuevas reglas sobre paso de parámetros tal como se ha visto en el módulo anterior: pasa punteros en el stack y también hace uso de estructuras cuando esto resulte adecuado. Para cualquier función API, los parámetros se almacenan en el stack en el orden inverso al que figuran en la declaración. Igualmente, cualquier valor de retorno vendrá en el registro EAX si se trata de un entero (si es mayor, será un puntero a una cadena o a una estructura). Tomemos un ejemplo del API Help de Microsoft o del muy ágil y condensado similar elaborado por Sync+, por ejemplo la función _lwrite: definición de función API _lwrite extraída de las API Help The _lwrite function writes data to the specified file. This function is provided for compatibility with 16-bit versions of Windows. Win32-based applications should use the WriteFile function. UINT _lwrite( HFILE hFile, // handle to file LPCSTR lpBuffer, // pointer to buffer for data to be written UINT uBytes // number of bytes to write ); OK, qué hay que hacer para llamar esta función? Supongamos que tengo abierto previamente un archivo cuyo handle es 3CCh, en el cual quiero escribir 1000h bytes desde el buffer que está en la dirección 40023300h MOV EAX,1000 ;cantidad de bytes a escribir PUSH EAX LEA EAX,lpBuffer ;DIRECCION del buffer de escritura PUSH EAX MOV EAX,EBX ;normalmente el handle se guarda en EBX PUSH EAX ;si se acaba de abrir el archivo CALL _lwrite ;debe estar en memoria el Kernel32.dll CMP EAX,1000 ver si se transfirierorn todos los bytes JNZ _error Nótese que si se ponen los parámetros en el stack en el orden inverso a lo que se declaran, significa (por ser el stack un elemento LIFO) que serán extraídos por la función en el orden en que están declarados. Aqui hay dos detalles que considerar: primero el hecho de que el orden de declaración de
  • 45. parámetros en Pascal es inverso al de C. Esto no presenta ningún problema, porque el compilador llama siempre a los parámetros en el mismo orden (porque en realidad pasa a lenguaje de máquina y en ese nivel no puede haber diferencias en el orden), de manera que de esto se encarga el compilador y basta con recordar que es inverso al que aparecen en las API Help. El segundo detalle es más interesante. Mientras Pascal vuelve de la función API con el stack ya equilibrado, en el lenguaje C es el programador el que tiene que encargarse de esa tarea. Desde el punto de vista de la ingeniería inversa, si nosotros seguimos una función API y vemos que termina en RET (4*n) donde n es el número de parámetros, es seguro que el compilador es estilo Pascal, mientras que si vemos que luego de retornar de la API, el programa acomoda el stack haciendo POPs o ADD ESP,(4*n), se trata de un compilador C. Pongamos como ejemplo la función vista _lwrite. Tiene tres parámetros y por lo tanto 4*n=0Ch, por lo tanto, si vemos algo asi: CALL _lwrite ADD ESP,0C se tratará muy probablemente de un ejecutable generado por un compilador C. En cambio, en Pascal la misma función _lwrite finaliza con un RET 0Ch y por lo tanto no es necesario el ADD posterior. NOTA PARA EL PRINCIPIANTE Es muy, pero MUY importante que el stack pointer quede siempre equilibrado entre el valor que tenía antes de ingresar los parámetros al stack y luego de ejecutada la función API. Y es fácil deducir por qué: si asi no fuera, la instrucción RET siguiente a producirse el desequilibrio del stack retornaría a un lugar que en realidad lo más probable es que sea un parámetro en lugar de código. Esto es igualmente válido si una función es llamada con una cantidad de parámetros distinta a la que exige su definición. Junto con las definiciones de función y los parámetros con los que hay que llamarlas hay en la API Help menciones a flags que controlan la operación de la función. Quizás un función emblemática en este sentido sea CreateWindow, que tiene una gran cantidad de banderas (por ejemplo, WS_BORDER, que cuando está activada hace que la función cree una ventana que tiene una linea fina como borde). Durante la construcción del programa, el compilador se encargará de activar el bit correspondiente a WS_BORDER, dentro del parámetro dwStyle. Sin embargo, cuando decompilamos un programa por ejemplo con el W32DASM, nos encontramos con instrucciones como PUSH 10830041. Esto corresponda posiblemente a parámetros como el dwStyle, que controlan mediante bits individuales el comportamiento de la función. Supongamos que determinamos que el anterior push corresponde efectivamente al parámetro dwStyle, por ser el antepenúltimo en ser cargado en el stack. Cómo saber cuáles son las banderas que el programador quiso activar?. Hay un sólo camino, que es seguir los pasos que dio el compilador al generar el ejecutable. En esto nos ayuda el archivo windows.inc, que viene con el compilador (también disponible en el ensamblador MASM32). Abrimos ese archivo (676 kB de definiciones!). Comenzamos a buscar WS_BORDER y encontramos :
  • 46. WS_BORDER equ 00800000h esto significa que tiene activado el bit 23, Bingo! el push que estamos considerando lo activa y por lo tanto vemos que la ventana tendrá borde con linea fina. De la misma manera tenemos que proceder con todos los bits de todos los parámetros que modifican la función de acuerdo con el estado de las banderas. Arduo? Si, nadie dijo que esto sea tarea fácil, sólo podemos afirmar que no es muy complicada, sólo extensiva. Por lo general no es necesario comprobar el 100% de las flags (lo que nos llevaría a perder un par de horas en una función como CreateWindow). Tenemos que concentrar nuestra atención en el problema que queremos resolver, por ejemplo, si la ventana de entrada de claves está inicialmente maximizada, hay que ver aquellas llamadas a CreateWindow con la flag WS_MAXIMIZE activada. NOTA PARA EL PRINCIPIANTE Es importante reconocer algunas características en las notaciones empleadas para nombrar funciones API. Las que incluyen una A o W final son funciones de 32 bits con un equivalente de 16 bits que no lleva esa letra. Por ejemplo CreateWindow es de 16 bits, mientras que CreateWindowA es de 32 bits, strings de un byte CreateWindowW es de 32 bits, string de 2 bytes Cuando una funcion termina en Ex tiene capacidades extendidas sobre la de igual nombre pero sin el Ex (y también algún parámetro adicional para controlar esa capacidad). Por ejemplo: CreateWindow: tiene 11 parámetros, en cambio CreateWindowEx :12 parámetros: se agrega dwExStyle que controla el estilo extendido
  • 47. GLOSARIO DEL CRACKING AND Operación binaria cuyo resultado es 1 sólo si ambos operadores son 1. También es el mnemónico de una instrucción de procesador que consiste en realizar la operación binaria bit por bit entre los operandos declarados en la instrucción. Por ejemplo, AND EAX,EBX instruye al procesador a realizar una operación binaria AND bit por bit entre los registros EAX y EBX y almacenar el resultado en EAX. ASSEMBLY Lenguaje de programación que permite el más absoluto control sobre el procesador. Es fundamental un aceptable manejo de este lenguaje a la hora de hacer ingeniería inversa sobre un target, ya que por lo general no se dispone del programa fuente y debe utilizarse una dead list. BACKDOOR Literalmente "puerta trasera", es un mecanismo que se instala en los sistemas a los que un hacker accede, con el objeto de sistematizar y ocultar futuros accesos. BANNER El horrible aviso comercial que encabeza toda página de un sitio de hosting gratuito BIOS Se deriva de "Basic Input - Output System" (sistema básico de entrada-salida), por referirse de algún modo a la interface necesaria entre el sistema operativo y el hardware. Aunque los dispositivos y el procedimiento utilizado para controlarlos puede diferir en cada PC, los sistemas operativos tienen reglas fijas para utilizar los recursos. Estas reglas son las funciones BIOS y la sintaxis empleada para convocarlas. Cuando por ejemplo, corremos (es un decir) el Notepad de Windows, hay tres capas de software una dentro de la otra: La exterior es la aplicación Notepad, la intermedia es el sistema operativo (Windows en este caso) y la más interna el BIOS. Asi, éste avisa a Windows que el usuario apretó una tecla, Windows le avisa a Notepad, y éste toma alguna acción al respecto, que puede ser algo tan simple como poner el caracter en el buffer de edición, y avisar a Windows que tiene que sacar el caracter por pantalla, para lo cual este avisará al BIOS de qué forma debe representarlo. Esta estructura en capas puede parecer compleja pero es la única manera de permitir que distintos fabricantes puedan hacer PCs para un mismo sistema operativo o que una misma PC pueda correr distintos sistemas operativos. En nuesta página sobre interrupciones hay un breve listado de las interrupciones utilizadas por el BIOS y por el DOS BIT La palabra se deriva de "Binary unIT" (unidad binaria). El ancho de las palabras binarias se especifica en bits: p.e. decimos que los registros de los procesadores actuales es de 32 bits y que el del (hoy) futuro Merced es de 64 bits. Esto da una idea de potencia de cálculo, ya que para obtener el mismo resultado para una operación suma simple como ADD EAX,ECX el procesador 8088 de las primeras PCs tenía que hacer dos sumas sucesivas porque el ancho de palabra era de 16 bits. BOOLEANO Que sigue las reglas del álgebra de Boole o que opera con valores binarios de un bit. Función Booleana: Aquella cuyo resultado puede ser cierto o falso (1 o 0), por ejemplo comprobar si durante un acceso a disco se produjo un error o no. BUFFER Area de memoria que se utiliza para realizar operaciones en las que un dispositivo deja datos a los que el programa consulta asincrónicamente. El búffer más fácil de entender es el de teclado: entre el BIOS y el sistema operativo leen el teclado y dejan en el búffer el código de las teclas apretadas. El programa luego va a esa área de memoria, normalmente mediante funciones del Sistema Operativo, para leer los códigos y descargar el búffer (le saca los caracteres que va leyendo). Esto permite, por ejemplo, seguir escribiendo mientras se produce un acceso a disco sin que se pierdan caracteres, ya que si bien el programa debe esperar a que termine la operación de disco, la función BIOS de lectura de teclado es llamada por la interrupción de hard y es atendida con mayor prioridad. El búffer para accesos a disco es el área de memoria destinada a recibir los datos que se leen o en donde el programa escribe los datos que se deben transferir al disco. BYTE Palabra de 8 bits. Con 8 bits se pueden representar 256 (=28 ) números decimales distintos desde 0 (todos los bits en cero) hasta 255 (todos los bits en 1) CARRY Flag de los procesadores que indica un desborde aritmético que debe ser tenido en cuenta cuando se opere el dígito siguiente. Es el "me llevo uno" que decimos cuando hacemos una suma decimal y una columna nos da 10 o más: lo que estamos haciendo es un "carry" (llevarse) a la siguiente posición decimal. En un