Sei sulla pagina 1di 16

Curso de Assembler

By: Gerardo Richarte

Hola Gente!

Por fin me mande a escribir algo sobre esto, quiero agradecerles


especialmente a

Ariel Nardelli Federico Joselevich


Mauricio Taslik Diego Paz
Gastón Sanchez Nicolai Kuzminski
Eduardo Arias Fernando Bardelli
Luis Mamud Juan Esteva
Pablo Rotzyld Fabián Benavidez

Por haberse prendido de entrada en esta idea, y por haber hecho sugerencias,
y por haberme hecho decidirme. !!!

Bueno, Mmmm...

Vamos a empezar por algo que quizás muchos ya sepan, pero como es
indispensable, igual se lo tengo que contar:

La Memoria de Las PC:

Como ya sabrán, de haber escuchado por ahí, hay varios tipos de memoria, la
Memoria Base, la Memoria Expandida (EMS), Memoria Extendida (XMS),
Upper Memory Blocks (UMB), High Memory Area (HMA) y un montón mas de
referencias a distintos tipos de memoria, lo que les voy a contar es que
dirección de memoria le corresponde a un Byte dentro del primer MegaByte, es
decir, dentro de la memoria base (0-640K) y en la zona donde están las
plaquetas de expansión, Monitor, BIOS ROM, área de la controladora de Disco,
etc...

Tratemos de acostumbrarnos a ver los números en Hexadecimal (base 16). Los


dígitos que pueden formar un numero en esta base, son: 0, 1, 2, 3, 4, 5, 6, 7, 8,
9, A, B, C, D, E y F, tomando valores en ese mismo orden, el 0 vale 0 y la F
vale 15. De esta forma un numero, por ejemplo 0FA3h (la 'h' final indica que
esta en 'Hexa'), expresado en decimal valdría (3)+(Ah*16)+
(Fh*16*16)+(0*16*16*16), o lo que es lo mismo 3+160+3840+0=4003,
pongámonos de acuerdo que cuando un número lo escribimos en Haxa le
ponemos la 'h' al final, y si comienza con una letra, le agregamos un '0'
adelante para no confundirnos con un nombre de variable. Esto fue una breve
pero necesaria introducción al sistema Hexadecimal de numeración.

Ahora si, veamos como se direcciona la memoria de una PC.


Tengamos en cuenta que si queremos direccionar 1 Mb nos hacen falta 5
dígitos hexadecimales, es decir, poder escribir, de alguna forma 0FFFFFh o
100000h-1 que es 1*16^5=1 Mb (1 que se resta, corresponde al byte #0).
Como los 80x86 tienen registros de 16 bits (8 bits = 1 Byte, 16 bits=1 Word) o 4
dígitos hexa, solo podríamos llegar a direccionar 0FFFFh bytes, lo que
significan 64 Kb solamente (de aquí la limitación de los .COMs). Lo que se
hace es utilizar dos registros a la vez para apuntar a un solo byte, lo que nos
amplia el campo a poder diferenciar 0FFFFh:0FFFFh bytes o 1 Mg de Mb
muchísimo! Entonces, ¿Que pasa que no se puede tener tanta memoria sin
hacer cosas extrañas? Claro, lo que pasa es que para direccionar se usan dos
registros, pero no ubicados uno al lado del otro, sino desplazados un dígito.
¿Que? Si, ya se que esta confuso, ahora te cuento:

En un Registro, digamos que se llama DS, esta 0040h, y en otro, digamos BS,
hay 006Ch. La dirección a la que apunta DS:BS (así se escribe) es 0040:006C,
pero esta dirección, en números de 5 cifras se escribe 0046C, la conversión es
simplísima, lo que hay que hacer es sumar los registros de la siguiente forma:

BS -> 006C
DS -> + 0040
---------------
DS:BS -> 0046Ch

De esta suma, uno se puede dar cuenta que para referirse a una misma
posición de memoria puede haber muchas formas distintas.

Por ejemplo, para referirse a 0046C, se puede hacer:

000C 046C
+0046 +0000
------- --------
0046C = 0046:000C 0046C=0000:046C

023C 005C
+0023 +003F
-------- --------
0046C = 0023:023C 0046C = 003F:005C

Es decir, puede haber muchas formas de nombrar el mismo byte, y tenemos


que tratar de acostumbrarnos a darnos cuenta que, por ejemplo, 0F000:1234 y
0F123:0004 son equivalentes.

Tenemos que una dirección de memoria se separa en dos partes, la de la


izquierda del ':' es el famoso SEGMENT o segmento y lo de la derecha es el
OFFSET o desplazamiento, de una dirección.

Conclusión: Para referirnos a cualquier Posición de Memoria es necesario


conocer su Segment y su Offset, ya que sabiendo uno solo, nos podemos estar
refiriendo a muchos bytes distintos.

Algunas Posiciones de Memoria útiles:


de 0000:0000 a 0040:0000 la tabla de punteros de interrupciones. Cada
puntero toma dos Words (un DWord) el primero para el Offset y el segundo
para el Segment.
en 0040:0000 Word El numero de Port correspondiente al COM1
en 0040:0002 Word El numero de Port correspondiente al COM2
en 0040:0004 Word El numero de Port correspondiente al COM3
en 0040:0006 Word El numero de Port correspondiente al COM4
en 0040:0027 Byte Status de las teclas especiales.
en 0040:0049 Byte El modo actual de video.
en 0040:006C DWord La hora actual, primero el word menos significativo y
luego, en 40:(6C+2) el mas significativo. El numero es 18.2 veces la hora.

a partir de B000:0000 La memoria de video de la Hércules (Texto & Gráficos)


a partir de B800:0000 La memoria de video de las otras (Texto & Algunos
Gráficos)

Muchas mas posiciones de memoria se pueden sacar de varios lugares, donde


yo siempre busco es en ASM.NG (Norton Guide de Assembler) y en 2MchNfo2,
(- Too Much Info Two -) como no podía ser de otra manera. :)

Nota: Si un DWord es YYYYXXXX, el Word menos significativo es XXXX, y el


mas significativo es YYYY. Si un Word es YYXX, el Byte mas significativo es
YY y el menos significativo es XX.

Para probar las posiciones de memoria, desde pascal pueden hacer:

If Mem[$0040:$0049]=7 Then { Modo Texto } para leer un byte (el '$' significa
Hexa)
Mem[$B800:(Columna+File*80)*2]:='A' Para la letra
Mem[$B800:(Columna+File*80)*2+1]:=White; Para el atributo
MemW[$0040:0000] (o MemW[$40:0]) para un Word
MemL[$40:$6C] para un LongInt (o DWord Signado)

Desde C:

if (peekb(0x0040,0x0017)==7) { /*Modo Texto*/ } para un byte (o char)


pokeb(0xB800,(Columna+File*80)*2,'A') Para la letra
pokeb[0xB800,(Columna+File*80)*2+1,WHITE) Para el atributo
peek(0x40,2) y poke(0x40,2) para un int (Word Signado)

y para un DWord hay que hacer un #define porque el C no trae:

#define peekpokel(seg,ofs) (*(unsigned long far *)(MK_FP(seg,ofs)))

ahora si:

if (peekpokel(0x40,0x6C)==0x0F0342034) {} y
peekpokel(0x40,0x6C)=0
para un DWord
Muy bien, ahora ya sabemos que la máquina tiene memoria y también
sabemos como apuntar a una posición en esta memoria, pero bueno, este
curso es de Assembler, no de memorias, por lo tanto, lo interesante y
apropiado sería que les comentara, así como si de pasada fuera, como se hace
en Assembler para cargar o grabar algo desde o hacia la memoria. Pero,
primero hay que saber otras cositas... No es tan
fácil la cosa... Al "hablar" en Assembler, estamos diciéndole a la máquina en lo
mas cercano a su idioma posible (ya que su verdadero idioma es de "1" y "0"
totalmente) lo que tiene que hacer. Estamos hablando directamente, nosotros
mismos, sin la ayuda de un traductor (compilador), con el cerebro propiamente
dicho de la máquina. Estamos hablando con el vendito 80x86. Entonces para
decirle algo tenemos que conocerlo un poco mas. Como es muy probable que
muchos tengan 8086/88 o 80286
vamos a ver especialmente, la estructura interna de estos "micros"
(microprocesadores), por arriba, solo lo necesario para programar en
assembler, obviamente también sirve para los iluminados que tengan 386 o
486, o quizás 586 o P-5. Internamente todos los micros (entiendo que todos),
tiene unas pocas variables, llamadas REGISTROS que son en donde se
procesan los datos, se hacen operaciones; son por las cuales se puede
acceder a memoria, etc. Sirven para todo. Pero no todas sirven para lo mismo,
y aunque muchas si sean intercambiables, todas están destinadas para algo en
especial, y tienen una que otra función propia. Los registros de la familia 80x86
son:

AX Acumulator Register Acumulador


BX Base Index Register Indice Base
CX Counter Register Contador
DX Data Register Dato
SI Source Index Indice de Origen
DI Destination Index Indice de Destino
BP Base Pointer Puntero Base
SP Stack Pointer Puntero de Pila
IP Instruction Pointer Puntero de Instrucciones
CS Current Segment Segmento Actual del Programa
DS Data Segment Segmento de Datos
ES Extra Segment Segmento Extra
SS Stack Segment Segmento de Pila

Flags Señalizadores de Estado:

Todos estos registros son de 16 bits, es decir de un Word. Pero los registros
que terminan en X (AX, BX, CX, DX) pueden ser manejados, también, como si
fueran dos Bytes (Que lo son), por separado, sus nombres son, AH para el
Byte mas significativo de AX y AL para el menos significativo, BH y BL para BX,
CH y CL para CX y DH y DL para DX (la H y la L hacen referencia a "Hi" y "Lo",
es decir Alto y Bajo, alto es sinónimo de mas significativo y bajo de menos. Si
por ejemplo:
AX = 437Ah Entonces AH = 43h y AL = 7Ah
BX = 0145h BH = 01h y BL = 45h
CX = 0AABBh CH = 0AAh y CL = 0BBh
DX = 1h DH = 00h y DL = 01h

De igual manera, si:

AL = 10h y AH = 32h AX = 3210h


BL = 08h y BH = 0CAh BX = 0CA08h
CL = 1h y CH=1h CX = 0101h
DL = 00 y DX=00 DX = 0000h

Estos cuatro son los únicos segmentos que se pueden separar en byte alto y
byte bajo.

Funciones especificas:

AX, Acumulator: Sirve para hacer todas las operaciones aritméticas, y algunas,
como Multiplicar y Dividir, le son exclusivas. AX (y AL por su versión de un solo
byte) son los únicos registros que pueden ser multiplicados y divididos por otro
registro. La resta de AX y AL, por ejemplo, ocupan un byte menos que la de
cualquier otro registro, pero esto no es para preocuparse, un byte, realmente
no es nada, por mas que se acumule.

BX, Base Index: Sirve para ser usado como registro de base para un índice o
array, es decir, una posición de memoria puede ser apuntada por BX (su
offset), igualmente también se lo puede usar para hacer sumas restas y todo
tipo de operaciones lógicas. AX, CX y DX no sirven para apuntar a memoria.

CX, Counter: Es el registro reservado para contar, como su nombre lo indica.


Para este propósito hay órdenes especiales que lo decrementan o incrementan
y hacen algo según el resultado. También hay ciertas órdenes repetitivas que
necesitan saber cuanto repetirse, por medio de CX (o CL en su versión Byte)
se les indica.

DX, Data: Este registro no tiene definido un uso, en general es utilizado para
pasar ciertos parámetros, pero si cumple una función, por ejemplo en la
multiplicación, si se multiplica AX=1000h (un Word) por, simplemente 10h (un
Byte) el resultado es 00010000h (Un DWord), entonces, el Word Alto del
resultado de la multiplicación se deposita en DX, y el Bajo en AX.

SI, Source Index: Puede ser utilizado como índice a posiciones de memoria, es
decir se puede poner un número en SI (Offset) y leer el dato de esta posición.
Pero a la vez tiene una función específica, la de Registro Fuente para las
órdenes de tratamiento de cadenas. Hay ciertas órdenes en Assembler que son
para, por ejemplo mover toda una cadena de bytes, de un lugar a otro, la
dirección de la cual se leen los bytes se pone en SI antes de decir que lea.

DI, Destination Index: Como SI, puede ser usado como índice. Pero su función
específica es la de Registro de Destino para las operaciones de cadena, lo que
se lee de el contenido de SI (no de SI mismo, sino de la posición de memoria a
la que apunta SI) es depositado en la posición de memoria a la que apunta DI,
expresada por [DI]. Al la vez, igual que con SI, se pueden hacer operaciones
aritméticas simples (suma y resta), y también todo tipo de operaciones lógicas
(AND, OR, XOR).

BP, Base Pointer: Puntero a una posición de memoria, muy parecido a BX,
pero generalmente usado para facilitar el pasaje de parámetros en funciones
hechas con lenguajes de alto nivel, por una característica propia que ya voy a
explicar.

SP, Stack Pointer: Puntero que indica en que Offset termina el Stack, o pila. El
Stack, es un área de la memoria principal de la máquina, (no esta dentro del
Micro, ni tampoco es fija) que sirve para preservar cosas, la estructura del
Stack, que ya explicare mas a fondo, es simple, esta estructura es llamada
LIFO (Last In First Out) o lo que es lo mismo, lo último que entra, es lo primero
que sale, es como si tuviéramos una pila de cosas, lo último que apoyamos
arriba va a ser lo primero que podamos sacar después. Si no esta claro, no se
preocupen, ya voy a explicarlo bien, y voy a decir para que se usa, y cuando.

IP, Instruction Pointer: El puntero de instrucción es el que le indica al Micro cual


va a ser la próxima instrucción que debe ejecutar (Solo el Offset). El programa
en Assembler tiene una estructura lógica, la cual se puede seguir. IP comienza
al principio del programa (la próxima orden que se debe ejecutar es la primera
del programa), se ejecuta esa orden e IP es incrementado tanto como Bytes
ocupe la orden recién ejecutada (no todas las órdenes ocupan un byte) luego
sigue con la próxima y así sucesivamente. Si pudiéramos de alguna forma
cambiar el contenido de IP lo que estaríamos haciendo seria una desviación, o
un Jump (salto) a otro lado del programa, y efectivamente se puede hacer esto,
pero no diciendo IP = 1234h, sino haciendo un salto, que es equivalente a esto
último: JMP 1234h. Ya lo voy a explicar, esto también.

Se habrán dado cuenta, que siempre que dije que apuntaba a una posición de
memoria, hice notar que solo era el Offset lo que estaba comprendido, por
ejemplo en DI o SI, BX o SP. Pero entonces, como es posible que con
solamente el offset alcance para identificar una posición de memoria? si yo
mismo dije:

"Para referirnos a cualquier posición de memoria es necesario conocer su


Segment y su Offset, ya que sabiendo uno solo, nos podemos estar refiriendo a
muchos bytes distintos."

Bueno, acá esta la cuestión:

CS, Current Segment: Es el Segmento actual de ejecución, juntos CS:IP


apuntan a la próxima orden que el Micro debe leer para ejecutar. Veamos algo:

CS:IP = 1000:FFFA hay una orden entonces IP se incrementa:


CS:IP = 1000:FFFE Que pasa si ahora hay otra orden, e IP se incrementa de
nuevo?
Fácil: FFFE+1 = FFFF pero FFFF+1 = 10000 !!!

Que pasa en este caso? CS no se incrementa, e IP no puede contener un


numero de 5 cifras! Lo que pasa es que IP vuelve a 0000 y el programa sigue
en CS:IP o sea 1000:0000, lo que es una posición 64k mas baja de memoria, o
sea cualquier lado... Este es el problema por lo que los .COMs no pueden tener
mas de 64k de largo sin hacer nada raro (cambiar CS cuando sea necesario).

DS, Data Segment: Es el segmento destinado a ser usado junto con BX, SI y DI
para apuntar a una dirección de memoria. También puede ser usado con BP y
SP, pero hace falta expresarlo concretamente. (Mas adelante aclaro esto)

ES, Extra Segment: Es un segmento Extra para ser utilizado cuando haga falta.
También tiene una función propia: Junto con DI indican el destino en las
"órdenes de cadena" (el dato leído de DS:SI es puesto en ES:DI, en las
ordenes de movimiento).

SS, Stack Segment: El segmento de Stack; junto con SP apuntan a la última


posición que fue utilizada para "depositar" datos en el Stack.

Por ultimo:

Flags: Es un registro especial en el cual se guardan los resultados de la última


operación matemática o de comparación. No se guardan los resultados
numéricos, solo los de estado, es decir:

CF Carry Flag Bandera de por Acarreo


PF Parity Flag Bandera de por Paridad
AF Auxiliary Flag Bandera Auxiliar
ZF Zero Flag Bandera por Cero
SF Sign Flag Bandera de Signo
TF Trap Flag Bandera de Paso Simple
IF Interrupt Flag Bandera de Interrupción
DF Direction Flag Bandera de Dirección
OF Overflow Flag Bandera por Sobre flujo
IOPL I/O Privilege Level Solo en 286 o superior
NT Nested Task Flag Solo en 286 o superior
RF Resume Flag Solo en 386 o superior
VM Virtual Mode Flag Solo en 386 o superior

Hasta aquí la explicación de los registros internos del Micro.

Para apuntar a una posición, hace falta indicar su Offset y su Segment, esto se
hace mediante un registro de segmento (DS, CS, SS, ES) y un Offset, que
puede ser un número o un registro como BS, SI, DI, BP, SP:

DS:[3304h] Apunta al Segment DS y al offset 3304h. Los corchetes indican que


lo
que se quiere es el contenido de esa posición, no la posición en si.

ES:[BX] Indica a la posición que esta en el Segmento DS y el Offset BX.

SS:[SP] Indica la posición de arriba de todo del Stack, apuntada por el


Segmento SS y el Offset SP.

No siempre hace falta indicar el Segment al que se apunta. Como el segment


DS es el destinado para poner los datos, la gente de Intel decidió que si no se
especifica segmento para un offset absoluto (un número como el primer caso),
o para los registros BS, SI y DI, se asume DS. Para los registros SP y BP, se
asume SS como Segmento por defecto. Entonces:

[1304h] Indica el contenido de DS:[1304h]


[BX] Apunta a DS:[BX], pero
ES:[BX] Apunta a ES:[BX], porque se especifica ES
ES:[0017h] -> a ES:[0017h]
[SP] -> a SS:[SP]
CS:[SP] -> a CS:[SP]
CS:[DI] -> a CS:[DI]
SS:[SI] -> a SS:[SI]
[SI] -> a DS:[SI]
[DI] -> a DS:[DI]
[BP] -> a SS:[BP]
ES:[BP] -> a ES:[BP]

Es decir, cualquier combinación de Segmento y Registro Índice o Base o Offset


Absoluto puede ser formada, pero no siempre hace falta aclarar el Segmento.

Hasta ahora vimos que en los microprocesadores hay variables llamadas


REGISTROS, que cada uno esta destinado a una función especial, pero que en
general se dividen en dos grandes grupos, los registros de uso general, por
decirles de alguna forma, y los exclusivamente para ser usados como
segmentos. Vamos a ver como es un programa en Assembler, pero antes,
quiero aclarar que es un Assembler.

Assembler, propiamente dicho, es el programa (TASM, MASM, A86, etc.) que


se ocupa de transformar lo que nosotros escribimos, en palabras, se podría
decir, a número, que es lo que realmente entiende la máquina. En realidad, lo
convierte, actualmente, en un código objeto, .OBJ, que es una herramienta que
se creo para poder unir o Linkear varias rutinas hechas en distintos momentos
y/o lenguajes. Lo que completa el proceso es el Linker, TLINK, LINK, OPTLINK,
BLINKER, etc. Lo que yo uso, particularmente, son el Assembler y Linker de
Borland, el TASM y el TLINK, pero bueno, si alguien quiere usar alguno mas,
es cosa de darse maña, o preguntar que hay que cambiar.

Primero, para comenzar por algo fácil, y para que por fin, de una vez por todas,
puedan comenzar a hacer algo, vamos a hacer un .COM, es lo mas simple,
mas rápido, y mas fácil de recordar.
En TASM, hacer un .COM se simplifica mucho, se limita a poner al principio del
programa:

EJEMPLO.ASM:

.MODEL SMALL ; Tipo .COM, este modelo de memoria


; significa que SS, DS, ES y CS son iguales al
; comenzar el programa. Y que los datos y el stack
; están sobre el mismo segmento pero claro, en
; distinto offset.
.CODE ; Para decirle al Assembler que aquí
; comienza el código. En otros modelos, como .HUGE
; hay que definir la parte de datos, con .DATA y
; la parte de Stack, con .STACK.
ORG 100h ; Esta orden, para el compilador también
; le dice que comience el programa en el offset
; 100h. Todos los .COMs deben comenzar en el
; offset 100h. Mas adelante voy a explicar bien el
; funcionamiento de esta orden.

; Y luego completar el programa con lo que sea.

EJEMPLO: ; Esta es una etiqueta, es un nombre que


; se le da a una posición para, luego, poder
; referirnos a ella por su nombre. Esta es una
; ayuda que nos da el Assembler para no tener que
; referirnos a números todo el tiempo. Acá hace
; falta para que el compilador sepa que todo
; comienza allí. No es necesario que sea el nombre
; del programa, puede ser cualquier otra cosa pero
; yo acostumbro esto.

; La orden MOV (MOVE), lleva dos parámetros. Lo


; que hace es copiar el contenido del segundo
; parámetro al primero

mov di,offset mensaje


; Esta orden, significa poner en DI el offset de
; la posición que representa mensaje. (mirar mas
; abajo para ver mensaje).
; DI = Offset Mensaje

Repetir: ; Pongo un 'label' o etiqueta, para


; usarlo luego.

mov al,byte ptr [di]


; Pongo en AL el contenido de la posición que
; indica [DI].
; Recordemos que [DI] solo, significa DS:[DI],
; porque el segmento por omisión para DI es DS.
; Con 'byte ptr' indico que DS:[DI] es un
; puntero a un BYTE. AL = DS:[DI]

add al,40h ; le sumo a AL 40h.


; AL+=40h (AL=AL+40h)

mov byte ptr [di],al


; Pongo en DS:[DI] (lo que apunta DS:DI) el
; contenido de AL. DS:[DI]=AL
inc di ; Incrementa DI en 1.
; DI++ (DI=DI+1)

cmp al,'$' ; !!! Compara AL con '$', es decir


; con el valor ASCII de '$'. El resultado de la
; comparación es guardado en los flags del micro.
; AL??'$'

jne Repetir ; Salta si el resultado de la última


; comparación fue que son iguales.
; Jump if Not Equal. Si no eran iguales salta a
; Repetir, el label que habíamos puesto al
; principio. AL=='$' ? Go Repetir

mov ah,09h ; Pongo en AH un 09h


; AH=09h

mov dx,offset mensaje ; DX = Offset Mensaje

int 21h ; !!! Ejecuto la Interrupción 21h

mov ax,4c00h ; AX=4C00h

int 21h ; INT 21h

Mensaje db 08h, 2Fh, 2Ch, 21h, 0E1h, 0E1h, 0E1h


; Aca estoy definiendo una lista de números,
; cada uno es un byte. (DB = Define Byte). El
; primer byte de la lista esta bajo el nombre
; mensaje. También podría haber puesto:
; Mensaje db 'El día esta lindo!'
; Se toma cada caracter como un byte.

db 0CDh, 0CAh, 0E4h


; Esta línea es la continuación de la anterior.
; Es necesario volver a poner DB.

END EJEMPLO ; Con esta orden, le digo al


; assembler que mi programa termina acá.
; El nombre después de END tiene que ser el
; mismo del principio.

Dentro de este código hay, fundamentalmente, dos tipos de órdenes: Las


ordenes propiamente de Assembler. (MOV, CMP, JNE, INT, ADD, INC) Y las
órdenes al compilador, que le dicen como tratar el archivo. (.MODEL, .CODE,
END)

Por otro lado, están las variables y labels, que son ayudas que nos da el
compilador. Si esto no existiera tendríamos que recordar, en vez del nombre de
una variable, su posición, y además tendríamos que calcularla a mano. De
todo esto se ocupa el compilador (ensamblador). Este programa, funciona y
hace algo, pero, claro, no todo es tan simple. Traten de analizarlo antes de
ejecutarlo, para eso van a necesitar una lista de interrupciones. Si no la tienen,
vayan consiguiéndola, casi todo programa en assembler se aprovecha de las
interrupciones. Tengan en cuenta que en un .COM, al comienzo, DS, ES, CS y
SS tienen el mismo valor, y claro que lo conservan a nos ser que se lo
cambiemos nosotros.

Con estos básicos conocimientos ya se pueden ir dando maña para hacer otras
cosas, traten de ver si les sale algo. Piensen que quieren hacer y como, si hay
algo que no se les ocurre, traten otra vez, que esa es la mejor forma de
aprender, tratar hasta que salga, si no les sale, pregunten, que por esta vez
puede pasar... :)

Acá esta lo que van a necesitar, sobre las interrupciones que usa el programa.

INT 21 - DOS 1+ - WRITE STRING TO STANDARD OUTPUT


AH = 09h
DS:DX -> '$'-terminated string
_______________________________________________________

INT 21 - DOS 2+ - "EXIT" - TERMINATE WITH RETURN CODE


AH = 4Ch
AL = return code
_______________________________________________________

Interprétenlo como les parezca, para que se vayan acostumbrando, vio?

Anteriormente hicimos un COM. En Assembler, entre hacer un COM o un EXE,


casi no hay diferencia, ya que todo lo tenemos que hacer nosotros
explícitamente, el compilador se ocupa solamente de lo básico. Les dije que
para hacer un .COM (siempre en TASM), lo mas fácil es poner:

.MODEL TINY
.CODE
ORG 100h

NomProg:
END NomProg

Entre el Label "NomProg:" y el fin del archivo "END NomProg", hay que poner
todo el código del programa en si. Mas adelante vamos a ver que significa cada
una de estas
cosas, y como cambiarlas para crear EXEs de distintos tipos, por ahora,
recuérdenlas para poder probar sus cositas.

Como en todo lenguaje, además de saber como programar hay que saber las
órdenes, para poder decirle que hacer a la máquina. Yo voy a ir explicando de
a poco todas las
órdenes que se me ocurran, pero como buena guía de consulta sugiero la
Norton Guide de Assembler, (Salió una versión nueva que esta en Kansas
City). Esta NG es una buena guía y explica bastante bien que hace cada orden,
igualmente, me ofrezco para que si alguien quiere hacer algo en Assembler, y
le surge alguna duda, me lo pregunte; ahora tenemos el área Assembler.

Formato general de una orden:


mnemónico [Param1 [,Param2]]

Donde:
Mnemónico es la orden en si misma, es llamada mnemónico porque se utilizan
estas letras, para no tener que recordar el número de la orden. Son letras que
parecen
no tener significado, pero si lo tienen, y una vez que se lo conoce, no solo se
recuerdan, también se aciertan algunos que no se conocían antes...

Param1, no siempre se utiliza, hay Mnemónicos que no requieren parámetros.


Este parámetro, cuando una orden lleva dos [parámetros], es llamado
Destination, es donde se guardan los resultados de las operaciones, claro que
puede no hacer falta, pero, digamos, que como siempre, estas son las
excepciones a las reglas...

Param2, muchas veces, este parámetro no existe, pero normalmente, cuando


es necesario, es llamado Source, y es el origen de las operaciones en las que
interviene. Otra vez, tenemos que decir, que hay excepciones a esta regla...

Ahora si, algunas órdenes concretas:

MOV Param1,Param2

Esta orden, MOVe, mueve, o mejor dicho, copia el contenido de Param2 a


Param1. Por ejemplo, MOV AX,BX es el equivalente a decir AX=BX, el valor de
BX no cambia, y el de AX se transforma en el de BX. Param1, puede ser tanto,
un registro como una posición de memoria. Param2 puede ser una constante,
un registro o una posición de memoria. Pero hay ciertas restricciones, a una
posición de memoria no se le puede asignar el contenido de otra posición de
memoria, es decir

MOV DS:[1200],ES:[3020]

NO es una orden valida. Si se desea hacer esto, hay que desdoblarlo en dos
ordenes:

MOV AX,ES:[3020]
MOV DS:[1200],AX

Si lo que se quería mover era un word. O cambiando por AL (en vez de AX) si
se quería mover un byte. (AX y AL son ejemplos, podría haber sido cualquier
registro). Hay otras formas de hacerlo sin destruir el contenido de AX, ya lo
vamos a ver. Hay otra limitación mas, a un registro de segmento, no se le
puede asignar directamente un valor, sólo esta permitido asignarle el contenido
de otro registro, pero que tampoco sea de segmento, es decir, para hacer:

DS=CS

hay que hacer, otra vez:


MOV AX,CS
MOV DS,AX

Y otra vez hay mas formas, y también, ya las vamos a ver.

Otra orden muy utilizada, es la orden:

INT IntNum

Esta orden llama a una interrupción, es decir, busca en el área de memoria


donde están los punteros a las subrutinas de interrupciones (ISR), y llama a
donde apunta el número de interrupción especificado. (Un puntero, es una
dirección de memoria, que apunta a algo, en este caso a la posición donde
comienza cada ISR. Las interrupciones son rutinas que están a disposición
nuestra, para que las utilicemos cuando nos sean necesarias. Son rutinas que
nos facilitan mucho la tarea de programar. Hay varios tipos de interrupciones:

Interrupciones de BIOS: estas interrupciones son las que instalan las


Plaquetas que ponemos en el motherboard, también esta el BIOS propio de la
máquina. Los BIOS, son ejecutados al encenderse la computadora, primero se
ejecuta el BIOS propio del mother (son, normalmente dos chips con etiquetas,
que se pueden ver al abrir la máquina), este BIOS "principal" se ocupa de
instalar sus interrupciones y de verificar si hay otros BIOS agregados en
plaquetas, si los hay, los ejecuta y ellos se encargan de instalar sus propias
interrupciones. Las interrupciones de BIOS, son las que nos facilitan el acceso
a los distintos periféricos, ya que se encargan de "hablar" directamente con el
HardWare, cosa bastante pesada de hacer, y que por otro lado, puede cambiar
de Hard en Hard. Gracias al BIOS, podemos acceder al monitor sin que nos
importe la marca ni el modelo, solamente llamamos a las interrupciones
necesarias.

Las Interrupciones de DOS, son el sistema operativo en si, una máquina es


completamente funcional sin BIOS, pero claro, tendríamos que estar
haciéndonos nosotros las rutinas necesarias para que nuestros programas
funcionen en todas las máquinas (serian todos enormes!!!), bueno, el sistema
operativo, es la interfase entre el usuario y el BIOS, el sistema operativo es el
que nos da la facilidad de manejar archivos, de reservar memoria, en fin, de
administrar los recursos de la máquina, de una forma que no cree conflictos
entre los programas de distintos programadores. Es el que pone las reglas de
como utilizar la máquina. No es complicado, si pesado, manejar archivos sin
DOS, de hecho varios programas, como el DiskExplorer de Norton, lo hacen,
pero, otra vez, que sentido tiene tener que poner un sistema operativo en cada
programa que hacemos...* Entonces, las interrupciones de DOS, son las que
nos permiten manejar los distintos recursos de la máquina, es decir, los
archivos, los directorios. La memoria y el flujo de programas en si. Es el que
nos facilita la entrada de teclado o disco (haciéndola indistinguible), etc.

Luego están las IRQ (Interrupt Request), estas interrupciones son


interrupciones propiamente dichas, no son normalmente llamadas por un
programa. Estas IRQs son llamadas directamente por el HardWare, para
indicar alguna situación especial. Por ejemplo, el Mouse llama a una IRQ para
avisar que surgió algún cambio de estado en el propio Mouse. El Teclado,
llama a una IRQ para decir que hay algún mensaje proveniente de el hacia la
CPU, etc. Normalmente, lo que se hace con las IRQs es reemplazar la rutina a
la que apunta la tabla de punteros y entonces cuando sucede una IRQ, nuestra
rutina es llamada directamente por el microprocesador, desde allí se lee el
Mouse o el Teclado, o lo que corresponda. Muy extrañamente, por estas
razones, nuestro programa llama a una IRQ.

Bueno, entonces tenemos que las Interrupciones son rutinas que nos facilitan
la programación, son rutinas ya instaladas en la máquina, que hacen tareas
complejas por nosotros. Y como tales rutinas, llevan parámetros, pero ¿donde?
si la orden es: INT IntNum Las Interrupciones toman los parámetros de los
registros, y su resultado también vuelve en registros...

Las Interrupciones BIOS, por ejemplo son las 13h, 10h, 11h, 12h, 16h, 17h,
19h, etc...
Las DOS, son las 20h, 22h, 23h, 24h, 25h, 26h, 27h, 28h, 29h.

Pero la fundamental es la INT 21h, por medio de ella es posible hacer casi todo
lo necesario para llevar un programa a la marcha... Pero ¿como puede ser que
con una sola INT sea posible hacer tantas cosas? Muy simple, muchas INTs
tienen funciones y sub-funciones, las cuales se identifican según el valor de los
registros, normalmente AX o AH son utilizados para decir a que número de
función/sub-función queremos acceder. Así, por ejemplo, la INT 21h si AH =
09h, sirve para mostrar un String en pantalla, pero si AH = 39h sirve para hacer
un directorio. Como ven, la utilidad de una INT no solo esta limitada a una
función, si no que puede tener múltiples usos.

Algo fundamental en la utilización de un Assembler, es la posibilidad que nos


da de ponerle nombres a las posiciones de memoria, es decir, podemos poner
una cadena en memoria, y asignarle un nombre, tal como si fuera una variable,
y de hecho, lo es. La forma de reservar lugar para una variable, dentro de un
programa, no es complicada:

Nombre XX Valor

Nombre es el nombre con el que nos vamos a referir, mas tarde a esa posición
de memoria, es el nombre de la variable. XX, es la definición del tipo de la
variable:

Para definir un byte, dice que la variable es un byte, o un array de Bytes,


DB
es decir, un byte atras de otro.
DW Para definir un Word o un array de Word.
DD Para definir un DWord, o un array de DWord.
DQ Para definir un QWord (QuadWord, u ocho bytes).
DT Para definir Ten Bytes (diez).
Valor es el valor inicial, no puede faltar nunca, claro que si no se le quiere dar
ningún valor inicial, es factible poner, en el lugar del valor, u '?', que significa
que reserve espacio sin importar que valor le de.

Definiciones validas, podrían ser:

Letra_A DB 'A' ; Un caracter **


Number_1 DW 1 ; Un Numero 1, pero Word.
Pointer DD 0B000h:8000h; Puntero a la pantalla de video.
String DB 'Hola, Como estas!'
; Cadena definida como array
; de bytes.
Array DB 1,2,4,8,16,32,64,128
; Array de Bytes...

etc...

La vez pasada me pase con el programa, quizás era un poco complicado, pero
era para que tengan una idea de como es un programa en Assembler, quizás
muchos le encontraron la vuelta y animaron a largarse a programar. Sugiero,
que traten de entender programitas en Assembler, no es ninguna ciencia este
lenguaje, es uno de los mas simples, pero lleva su tiempo encontrarle la
vuelta... Acá va otro programita, también rebuscado, pero, yo se que si no
entienden van a preguntar... O no? (:

.model tiny
.code
org 100h
Ejemplo_2:
jmp Comienzo ; Salteo el área de datos, si se
; ejecutara (no hay diferencia entre datos y
; programas) seria una colgada mas...
Matching DB '*.*',0
; Array: Una cadena mas un número.
Comienzo:
mov ah,4eh
mov cx,0
mov dx,offset Matching ; DX = Offset de Matching
; DS, al entrar a un .COM es igual a CS.
int 21h ; Que hace?
jc Error ; Si esta prendido el Carry Flag, salto
mov ah,2fh
int 21h ; Y esta?
mov byte ptr es:[bx+1eh+13],'$'
; Byte ptr para decirle que a lo que apunta
; [bx+13] es un byte, podría ser un word ptr, o
; un dword ptr. El '$' es el fin de una cadena
; a imprimir por el DOS.
mov dx,es
mov ds,dx
lea dx,[bx+1eh]
; Pone en DX el Offset al que apunta
; BX+13, es decir DX = BX+13

mov ah,9
int 21h ; Imprime una cadena.
Error:
mov ax,4c00h
int 21h
END Ejemplo_2

* Fuera de tema, pero interesante quizás, es el hecho de ampliar cada vez mas
este concepto de reutilización de código hasta llegar al Windows, donde ningún
programa
trae las rutinas para hacer menús, botones o ventanas, pero todos las utilizan.
También crearon las DLL (Dinamic Link Libraries) que pueden ser reutilizadas
por varios programas, pero claro, hay que saber aprovecharlo...

** Desde el ';' hasta el final de la línea, es considerado comentario por el


compilador.

Bueno, espero que entiendan algo, porque la verdad, que aunque lo leí varias
veces, tengo mis dudas, todavía...

A continuación la última entrega de interrupciones útiles...


--------------------------------------------------------------------------

INT 21 / AH=4Eh Find First Matching File


Entrada:
AH = 4Eh
CX = Attributo a buscar.
DS:DX -> puntero a un ASCIIZ (Cadena terminada en 0) con el nombre a
buscar, wildcards permitidos.

Devuelve:
AX = Codigo de Error, si El Carry Flag (CF) esta en uno.
DTA = Contiene la informacion de retorno.
--------------------------------------------------------------------------

DTA = Offset Tamaño Significado


00 Byte Atributo de la búsqueda

[...]

1E 13 bytes ASCIIZ del nombre del archivo encontrado.


--------------------------------------------------------------------------

INT 21 / AH=2Fh Get Disk Transfer Address (DTA)


Entrada:
AH=2Fh

Devuelve:
ES:BX -> Puntero con la dirección de la DTA actual.
--------------------------------------------------------------------------

Potrebbero piacerti anche