Вы находитесь на странице: 1из 57

1.1.

Sistemas numéricos
1.1.1. Sistema decimal
El sistema decimal es el sistema numérico natural humano, en el que todos
pensamos naturalmente. Pero, ¿qué es?. Es un sistema numérico en el que la
base es el número 10 (bastante redondo). Veámoslo con un ejemplo.
Supongamos que tenemos el número 5013. Vamos a ver cuál es su estructura
en base 10:

5013 = 5 * 103 + 0 * 102 + 1 * 101 + 3 * 100 = 5000 + 0 + 10 + 3.

Puedes ver que hemos empleado el 10 como base en todas las


exponenciaciones, por eso se le llama de base 10.

1.1.2. Sistema binario


Cualquier número en cualquier base puede ser representado exactamente por
otro número en cualquier otra base numérica. Esto implica que da
exactamente igual pensar en base 10 (como hacemos los humanos) o en base
binaria (como hacen las computadoras). La base binaria es la base 2. Las
computadoras piensan en este sistema porque funcionan con energía eléctrica.
De esta manera le asignan 5 voltios al número 1, y ningún voltio al 0, con lo
que pueden representar cualquier número formado por ceros y unos (sistema
binario). Y esto es todo lo que una computadora necesita para poder trabajar.
Pues, como veremos más adelante, cualquier tipo de operación y de dato está
representado por un número.
1.1.3. Conversión entre decimal y binario
Vamos a ver cómo se pasa un número binario a decimal. Supongamos el 1101
binario:

1101 = 1 * 23 + 1 * 22 + 0 * 21 + 1 * 20 = 8 + 4 + 0 + 1 = 13

Fig. 01-01 - Tma del Cociente

Para pasar un número de decimal a binario utilizaremos el teorema del cociente


en el anillo Ζ, véase Fig. 01-01.
Con lo cuál, 13d = 01101b Esta es una notación muy común en programación,
13d significa 13 decimal y 1101b, 1101 binario. De la misma forma que los
humanos solemos representar un número decimal separando cada tres cifras
para un visionado más cómodo, en computación, se representará el número
binario separando cada cuatro dígitos (cada cuatro, porque 4 es potencia de 2,
que es la base del entendimiento de la computadora).

1.1.4. Operaciones matemáticas básicas en base dos


Actualmente en casi cualquier calculadora se puede trabajar en base binaria,
decimal, hexadecimal e incluso más, con lo cuál, no merece la pena entrar en
profundidades. Lo más aconsejable es, por tanto, echar las cuentas con una
calculadora y hacer las equivalencias entre las distintas bases también con ésta,
pues de esta forma nos ahorraremos fallos innecesarios. No obstante, he elegido
a modo de ejemplo una suma y una resta en base binaria:

Suma en base dos:

1101 = 23 + 22 + 01 + 20 = 8 + 4 + 1 = 13d
1011 = 23 + 02 + 21 + 20 = 8 + 2 + 1 = 11d
------ ---- +
11000 = 24 + 23 = 16 + 8 = 24d

Explicación:

Como en base decimal, siempre empezamos por la derecha y decimos:

 1 más 1 son 2 (en decimal), que en binario se representa con un 10, por lo tanto,
ponemos un cero y nos llevamos una.
 0 más 1 es 1, más 1 que nos llevábamos de antes, son 2 (en decimal), que de
nuevo, en binario se representa por un 10, por lo que ponemos un cero y nos
llevamos una.
 1 más 0 es 1, más 1 que nos llevábamos de antes, son 2 (en decimal), que en
binario es 10, con lo que ponemos un cero y nos llevamos 1.
 1 más 1 son 2, más 1 que nos llevábamos antes son 3 (en decimal), que en
binario se representa por un 11, que es lo que ponemos, et voilà.

Resta en base dos

1101 = 23 + 22 + 20 = 8 + 4 + 1 = 13d
1011 = 23 + 21 + 20 = 8 + 2 + 1 = 11d
------ --- -
0010 = 21 = 2 = 2d

Explicación:

De nuevo, al igual que en base 10 empezamos por la derecha y decimos:


 De 1 a 1 van 0 y nos llevamos 0.
 De 1 a 10b (2d) va 1 y nos llevamos 1.
 0, más la que nos llevábamos de antes, es 1, que hasta 1 van 0, y nos llevamos
0.
 De 1 a 1 van 0 y nos llevamos 0, c'est fini.
1.1.5. Operaciones lógicas
Además de las conocidas operaciones matemáticas, en el sistema binario se
pueden definir otra serie de funciones especiales conocidas como operaciones
lógicas que surgen de la idea de asignar el valor "falso" al dígito 0 y el valor
"verdadero" al 1. Estas operaciones se definen en principio para cantidades de
un único dígito. Las más importantes de ellas las vemos en la siguiente tabla:

X Y X and Y X or Y X xor Y not X


0 0 0 0 0 1
0 1 0 1 1 1
1 0 0 1 1 0
1 1 1 1 0 0
Tabla 01-01 - Operaciones Lógicas
 X and Y vale 1 sólo cuando X e Y valen 1 los dos, y 0 en caso contrario
 X or Y vale 1 cuando al menos uno de los dos vale 1, y 0 en caso contrario
 X xor Y vale 1 cuando X e Y son distintos y 0 cuando son iguales
 not X es el opuesto de X
1.1.6. El sistema hexadecimal
Uno de los mayores problemas del sistema binario es la cantidad ingente de
cifras que requiere la representación de un número de forma temprana, intenta
por ejemplo representar en binario el número decimal 1000. Otro de sus grandes
problemas es que la conversión de binario a decimal y viceversa no es trivial.
Estos dos problemas se solventan con el sistema hexadecimal que es realmente
compacto y cuya conversión a binario y viceversa es muy sencilla, por lo que
esta notación es comúnmente utilizada en computación, ya que, como ya hemos
dicho, el sistema binario es con el que piensa el ordenador.

Como su nombre indica el sistema hexadecimal tiene por base al número 16.
Aquí tenemos un problema, pues los números que nosotros conocemos van del
0 al 9, que son 10, ¿cómo llegar al 16?; fácil, le añadimos las letras A (10), B
(11), C (12), D (13), E (14), F (15) a partir del 10, con lo cuál ya tenemos 16
dígitos para representar números.

1.1.6.1. Conversión entre hexadecimal y decimal

Vamos a ver cómo se pasa un número hexadecimal a decimal. Supongamos el


12B3 hexadecimal:
12B3 = 1 * 163 + 2 * 162 + 11 * 161 + 3 * 160 = 4096 + 512 +
176 + 3 = 4787
Para pasar un número de decimal a hexadecimal utilizaremos el teorema del
cociente en el anillo Ζ:

Fig. 01-02 - Tma del Cociente

Con lo cuál, 4787d = 12B3h.

Esta es una notación muy común en programación, 4787d significa 4787


decimal y 12B3h, 12B3 hexadecimal. La primera cifra de un número
hexadecimal es siempre un número, poniéndose un cero a la izquierda si fuera
necesario.

El 8086 es un procesador de 16 bits, lo cuál quiere decir que internamente es


capaz de trabajar con cifras binarias de 16 dígitos de longitud. Las direcciones
de memoria con las que trabaja tienen una longitud de 20 bits, lo que le permite
direccionar un total de 1 Megabyte de memoria.

1.1.6.2. Conversión entre hexadecimal y binario

Veamos una tabla con la conversión inmediata de los primeros 16 números


hexadecimales:

Binario Hexadecimal Binario Hexadecimal


0000 0 1000 8
0001 1 1001 9
0010 2 1010 A
0011 3 1011 B
0100 4 1100 C
0101 5 1101 D
0110 6 1110 E
0111 7 1111 F

Tabla 01-02 - Conversión Binario-Hexadecimal

Para convertir un número hexadecimal en binario tan sólo tenemos que utilizar
esta tabla, y a cada dígito hexadecimal sustituirlo por sus cuatro dígitos
decimales. Supongamos que tenemos el número 0ABCDh y lo queremos pasar
a binario:
0 A B C D
0000 1010 1011 1100 1101

Es así de sencillo.

Vamos a convertir ahora un número binario en hexadecimal. Para hacer esto,


tan sólo tenemos que asegurarnos que el número de cifras que tiene nuestro
número es múltiplo de 4, porque vamos a volver a utilizar la tabla anterior.
Supongamos, por ejemplo, el número 1011001010b. Lo primero que tenemos
que hacer es añadirle dos ceros a la izquierda para que tenga un número de
dígitos múltiplo de cuatro, y luego, la operación es justo la inversa de la anterior.

0010 1100 1010


2 C A

1.1.7. Representación de datos


Matemáticamente se puede trabajar con cualquier tipo de número, sin embargo,
las computadoras están diseñadas para trabajar internamente con números de
base binaria, que son, como ya sabemos, sólo ceros y unos. Vamos a ver cómo
están estructurados:

1.1.7.1. Bit

Es la unidad más pequeña de información, puede contener sólo un 0 ó un 1.

1.1.7.2. Nibble

Es una colección de cuatro bits. No es una estructura de especial interés salvo


para dos objetos:

1. El BCD, que es una forma de representar números en formato ASCII en 4 bits


2. Los dígitos hexadecimales, cada uno de ellos tiene una representación de 4 bits.

Con un nibble se pueden representar hasta 24 = 16 valores diferentes.

1.1.7.3. Byte

Es la más importante estructura de datos, pues es el tamaño de dato más


pequeño accesible por un 8086 y está compuesta por 8 bits, o sea, 2 nibbles, y
por tanto son representados por 2 dígitos en hexadecimal.

7 6 5 4 3 2 1 0

Tabla 01-03 - Estructura de un Byte


Un byte puede representar hasta 28 = 256 valores diferentes, por ejemplo,
representa cada uno de los 256 caracteres que hay en el código ASCII.

1.1.7.4. Palabra

Es un grupo de 16 bits, o equivalentemente, 2 bytes ó 4 nibbles; por lo que


puede ser representado por un número hexadecimal de 4 dígitos. Es también
una estructura muy importante porque todos los registros del 8086 son de 16
bits, que es con la unidad con la que fue diseñada para trabajar. Esto quiere
decir que el microprocesador 8086 es de 16 bits.

15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Tabla 01-04 - Estructura de una Palabra

Una palabra puede representar hasta 216 = 65.536 valores diferentes. Si


definimos una variable como palabra, debemos saber que puede representar un
número entero de 0 hasta 65.535 o bien un entero con signo desde -32.768 hasta
32.767. El microprocesador 8086 trabaja con registros de 16 bits.

1.1.7.5. Palabra doble

Es un grupo de dos palabras, es decir, de 32 bits, 4 bytes u 8 nibbles, por lo que


puede ser representado por un número hexadecimal de 8 dígitos.

31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Tabla 01-05 - Estructura de una Palabra Doble

Una de sus funciones más importantes en entornos de 16 bits es la de contener


un segmento:dirección. En entornos de 32 bits contiene una dirección, puesto
que se trabaja con modelos de memoria planos.

Una palabra doble puede representar hasta 232 = 4.294.967.295 valores


diferentes. Por lo que una variable declarada como tal puede tomar valores
desde 0 hasta 4.294.967.295 sin signo o bien desde -2.147.483.648 hasta
2.147.483.647 con signo.

Existen procesadores de 32 bits, como el 386, porque se diseñó para que


trabajase internamente con registros de 32 bits.

1.1.7.6. Cuádruple palabra

Es un grupo de cuatro palabras, es decir, 64 bits, 8 bytes ó 16 nibbles, por lo


que puede ser representado por un número hexadecimal de 16 dígitos.
Una cuádruple palabra puede representar hasta 264 =
18.446.744.073.709.551.616 valores diferentes. Actualmente también existen
microprocesadores de 64 bits profusamente extendidos entre los PCs.

1.1.7.7. Múltiplos del byte

Si para medir la capacidad de los registros internos del procesador se usan las
unidades indicadas anteriormente, cuando se trata de indicar la capacidad de un
bloque de memoria o de ciertos dispositivos como los CD-ROM, discos duros
y similares, se emplean siempre múltiplos del byte. Todos ellos, al operarse con
la base 2 en lugar de la decimal, usan como multiplicador no el millar, sino el
1024, que es el resultado de elevar 2 a 10.

 1 Kb (kilo byte) = 210 bytes = 1.024 bytes


 1 Mb (mega byte) = 220 bytes = 1.024 Kb = 1.048.576 bytes
 1 Gb (giga byte) = 230 bytes = 1.024 Mb = 1.073.741.824 bytes
 1 Tb (tera byte) = 240 bytes = 1.024 Gb = 1.099.511.627.776 bytes
1.1.7.8. Números con y sin signo

Hasta ahora hemos tratado sólo los números binarios positivos, pero ¿qué pasa
con los negativos?.

En el sistema decimal, el signo de un número se expresa anteponiendo a la


cantidad un guión para indicar que se trata de un número negativo o nada en
caso de que sea positivo. Sin embargo, los ordenadores sólo son capaces de
almacenar dígitos binarios, no signos, por lo que hay que recurrir a algún
sistema alternativo para indicar el signo de un número. Además,
matemáticamente podemos conseguir cualquier número, de cualquier longitud,
pero las máquinas tienen una capacidad finita, por lo que debemos restringir el
tipo de número que podemos tomar. Hay que encontrar algún método para
conseguir las mismas propiedades que tiene (Ζ,+), el grupo de los números
enteros.

(Ζ,+) es un grupo conmutativo

Sean a, b, c elementos de (Ζ,+)

 Propiedad asociativa: a + (b + c) = (a + b) + c
 Existe el elemento neutro (0): 0 + a = a + 0 = a
 Existe el elemento simétrico (-a): a + (-a) = (-a) + a = 0
 Propiedad conmutativa : a + b = b + a

Puesto que la capacidad de los ordenadores es limitada, establecemos unas


magnitudes de dimensiones:

 Byte, que tiene una capacidad de 8 bits.


 Palabra, con una capacidad de 16 bits.
 Doble palabra, con capacidad para 32 bits.
 Cuadruple palabra, de 64 bits.

Existen más, pero estos son los más usados. Todos ellos están pensados
naturalmente para albergar números enteros positivos hasta su límite, pero
¿cómo podemos almacenar números negativos?. Realmente tenemos que usar
un truco que es establecer la mitad de su capacidad para los números positivos
y la otra mitad para los negativos.

El sistema más comúnmente utilizado se denomina "complemento a dos", y se


basa en las siguientes reglas:

 Un número positivo tiene su bit más significativo (el de la izquierda) siempre a


cero, y a uno si es negativo.
 Para convertir un número positivo en negativo (o viceversa) se invierten todos
los bits del número, y luego se le suma uno al resultado.

Por ejemplo:

35d = 00100011b -> (invert) -> 11011100b -> +1 -> 11011101b = -


35d
-35d = 11011101b -> (invert) -> 00100010b -> +1 -> 00100011b =
35d

Supongamos que cogemos un número de magnitud byte, esto significa que sólo
puede contener 8 dígitos, ceros o unos.

Gráficamente el sistema "complemento a dos" quedaría como sigue:

Fig. 01-03 - Compl a 2

Enteros con signo Compl a 2 Enteros sin signo Hexadecimal


-128 1000 0000 128 80
-127 1000 0001 129 81
-126 1000 0010 130 82
[...] [...] [...] [...]
-3 1111 1101 253 FD
-2 1111 1110 254 FE
-1 1111 1111 255 FF
0 0000 0000 0 0
1 0000 0001 1 1
2 0000 0010 2 2
3 0000 0011 3 3
[...] [...] [...] [...]
125 0111 1101 125 7D
126 0111 1110 126 7E
127 0111 1111 127 7F
Tabla 01-06 - Complemento a 2

Vemos en este gráfico que esta disposición que hemos elegido para nuestro
sistema numérico es como el pez que se muerde la cola.

Supongamos que tenemos el número 1111 1111b. ¿qué ocurriría si le sumamos


1?:

1111 1111
1
-------------- +
1 0000 0000

La cifra que obtenemos excede de tamaño byte, puesto que tiene 9 dígitos, por
lo tanto el dígito de la extrema izquierda se pierde, es como si intentaras llenar
una botella con vasos de agua y el último no cupiese, simplemente se
desbordaría y se perdería. De esta forma ¿qué es lo que obtenemos?, el número
0b, con lo que obtenemos este círculo vicioso del que hablábamos antes. Esto
ocurre por la ya mencionada limitación de contención de cualquier número de
las computadoras y a este suceso se le llama desbordamiento. La única solución
para que no ocurran desbordamientos (puesto que obtenemos resultados
imprevisibles) es elegir una botella lo suficientemente grande como para que
quepan todos los vasos de agua que le vamos a echar.

Sean los números:

 a = 1000 0001b = -(0111 1110b + 1d) = -(126d + 1d) = -127d


 b = 0000 0010b = 2d
 c = 0101 0101b = 85d

En el caso de que tratemos con enteros con signo se observa inmediatamente


que el número a es negativo, puesto que su dígito de extrema izquierda (el de
mayor peso) es 1. Para ver su representación decimal hay que invertir sus bits
y sumarle un 1 decimal.

Este subconjunto de Z, en el caso de los bytes de -128 a 127, con esta operación
suma en complemento a 2 que hemos definido es un subgrupo:
 Es cerrado respecto de la suma. Si notamos al subconjunto como B, b y b' dos
elementos suyos, b+b' también es de B por la propiedad del desbordamiento.
1000 0001b
0111 1111b
------------ +
1 0000 0000b
 Contiene al elemento neutro. Claramente es el cero.
 Cada elemento tiene su simétrico. Dado g en G, su simétrico será: NOT(g) + 1.
NOT cambiará cada bit a su contrario, y luego le sumamos 1. Únicamente el
128 se escapa de esta propiedad porque -128 es un byte, pero 128 ya es un word.
Así ([-127,127], suma complemento a 2) sería un subgrupo de (Z,+), pero el -
128 también cabe dentro de un byte.

 -128d = 10000000b -> not(10000000b) + 1b = 01111111b + 1b =


10000000b = -128d. Sería 128d en word
 -127d = 10000001b -> not(10000001b) + 1b = 01111110b + 1b =
01111111b = 127d
 -126d = 10000010b -> not(10000010b) + 1b = 01111101b + 1b =
01111110b = 126d
 ...
 -2d = 11111110b -> not(11111110b) + 1b = 00000001b + 1b =
00000010b = 2d
 -1d = 11111111b -> not(11111111b) + 1b = 00000000b + 1b =
00000001b = 1d
 0d = 00000000b -> not(00000000b) + 1b = 11111111b + 1b =
00000000b = 0d. Desborda un 1
 1d = 00000001b -> not(00000001b) + 1b = 11111110b + 1b =
11111111b = -1d
 2d = 00000010b -> not(00000010b) + 1b = 11111101b + 1b =
11111110b = -2d
 ...
 126d = 01111110b -> not(01111110b) + 1b = 10000001b + 1b =
10000010b = -126d
 127d = 01111111b -> not(01111111b) + 1b = 10000000b + 1b =
10000001b = -127d
 Propiedad conmutativa.
 1000 0001b 0000 0010b
 0000 0010b 1000 0001b
 ----------- + ---------- +
 1000 0011b 1000 0011b
 1000 0011b = -(0111 1100b + 1d) = -(124d + 1d) = -125d = -127d +
2d.

Importante
Antes de realizar operaciones con números con signo es preciso cerciorarse de que todos tengan el mismo
número de bits. Si esto no es así hay que alargar el tamaño de los más cortos al tamaño del mayor. Para ello
se repite la cifra a la izquierda tantas veces como sea necesario, es decir, a los números negativos se les añade
1 repetidas veces a la izquierda y 0 a los positivos. Esto ya se consigue con los operadores CBW, CWD, por
ejemplo.

Otra cosa son los enteros sin signo. Por ejemplo, de tipo byte, que son 8 bits
serían 28=256 elementos, que van desde el 0 hasta el 255. Aquí ya no tenemos
la suma en complemento a 2, sino la suma normal, pues no existen los
negativos, pero el desbordamiento sigue existiendo, lo cuál provoca que el
número resultante de la suma siga siendo de tipo byte, pero el resultado sería
distinto al esperado. Por ejemplo:

244d = F4h = 11110100b


128d = 80h = 10000000b
------------------------- +
372d = 174h = 101110100b

Vemos que el número binario resultante de la suma tiene 9 dígitos, se pierde


entonces el de la extrema izquierda, quedando:

116d = 74h = 01110100b

Obsérvese que un byte sólo puede tener dos dígitos hexadecimales, pues FFh =
255d.

1.1.7.9. Números reales

En computación son llamados también números "en coma flotante", "floating


point" en inglés, debido a que los decimales se separan con una coma en la
notación española y con un punto en la notación inglesa.

Al igual que ocurría con los números negativos, el ordenador no trata


directamente con este tipo de números, sino que se vale de un artificio para ello.
Tampoco existe una capacidad infinita para ellos, lógicamente.

Nosotros vamos a ver sólamente tres tipos de números en coma flotante,


descritos en la siguiente tabla:

Tipo de dato Bits Bytes Dígitos significativos Rango aproximado


Real Corto 32 4 6-7 1.18*10-38 hasta 3.40*1038
Real Largo 64 8 15-16 2.23*10-308 hasta 1.79*10308
Real Extendido 80 10 19 3.37*10-4932 hasta 1.18*104932
Tabla 01-07 - Números en Coma Flotante

El rango de los exponentes para los números reales es:

Formato Rango de exponente


Real Corto -127 a +128
Real Largo -1023 a +1024
Real Extendido -16383 a 16384
Tabla 01-08 - Rango de Exponentes

¿Cómo pasamos un número real decimal a binario?

Es bien sencillo, tan sólo hay que fijarse que estar en base 2 significa que todos
los números son combinaciones lineales de potencias de 2 (igual que si estamos
en base decimal serían combinaciones lineales de potencias de 10).

Por ejemplo, cojamos el número decimal 39.5625 (es conveniente habituarse a


la notación inglesa del punto para designar los decimales porque los lenguajes
de programación están diseñados en inglés y esta es la notación que utilizan).

En base decimal tenemos:

39.5625 = 3x101+9x100+5x10-1+6x10-2+2x10-3+5x10-4

En binario tenemos:

39.5625 = 32+4+2+1+0.5+0.0625 =
1x25+0x24+0x23+1x22+1x21+1x20+1x2-1+0x2-2+0x2-3+1x2-4

Con lo que ya tenemos la representación binaria del número:

39.5625d = 100111.1001b

Vamos a ver un algoritmo para hacer esto:

La primera parte consiste en convertir la parte entera a binario. Esto ya se hace


como hemos visto anteriormente, utilizando el Teorema del Resto:

39d = 100111b

La segunda parte consiste en convertir la parte decimal a binario. Para ello


utilizamos un procedimiento semejante al anterior, sólo que ahora en lugar de
dividir por 2, multiplicamos por 2 y vamos cogiendo la parte entera que nos va
saliendo hasta que nos quedemos sin decimales:

Parte decimal *2 Parte entera


0,5625 1,1250 1
0,1250 0,2500 0
0,2500 0,5000 0
0,5000 1,0000 1
Tabla 01-09 - Conversión parte decimal a binario
Tomando de arriba a abajo tenemos que 0.5625b = 1001d

Obsérvese, asimismo que este algoritmo es válido para cualquier base a la que
queramos convertir, utilizando su base para multiplicar.

¿Cómo guardamos en memoria un número binario decimal?

Vamos a seguir las especificaciones dadas por el IEEE-754. Para guardar algo
tendremos que reservar espacio y esto influirá decisivamente en nuestra forma
de guardar el número binario decimal, si bien el método estructural es el mismo.

El formato con el que vamos a guardar un número real binario es este:

Ax2B

Donde A es un número binario decimal con 1 como parte entera y cualquier


combinación de unos y ceros para la parte decimal. Por ejemplo: 1.0101001

B es el exponente del número binario decimal, para aceptar negativos y


positivos, éste se va a guardar de forma discriminada.

Es interesante recordar que la notación científica que se utiliza muy a menudo


para un número decimal real es la siguiente:

aEb

Donde a es un número real cualquiera, Eb significa por 10 elevado a b. Entonces


a es la mantisa y b es el exponente.

Veamos ahora cuál es la estructura para cada uno de los tipos de números reales:

 El bit más significativo tanto de los 32 de real corto, como de los 64 del real
largo, como de los 80 del Real extendido está reservado para almacenar el signo
del número: 0 para positivo, 1 para negativo.
 Los siguientes 8 bits del real corto, 11 para el real largo y 15 para el Real
extendido, están reservados para guardar el exponente del número, y para
trabajar con exponentes positivos y negativos, éste está discriminado por una
suma de 127 (27-1) en el caso de reales cortos, 1023 (210-1) para los reales largos
y 16383 (214-1) para los Reales extendidos.
 El resto de los bits se reservan para la mantisa del número exponencial. Es
importante decir que el 1 de la parte real de la mantisa no se guarda puesto que
se asume que siempre es así. En caso del número real 0, toda la estructura que
comentamos tendrá todos sus bits a cero.

Veamos esto mismo gráficamente:


S EXP MANTISA
31 30 23 22 0
Tabla 01-10 - Real corto

S EXP MANTISA
63 62 52 51 0
Tabla 01-10 - Real largo

S EXP MANTISA
79 78 64 63 0
Tabla 01-10 - Real extendido

Examinamos, como ejemplo, cómo se guardaría el número anterior en un real


corto:

Hemos dicho que

39.5625d = 100111.1001b = 1.001111001x25b

El signo es positivo

El exponente 5d+127d = 132d = 10000100b

La mantisa 001111001

Con lo cuál, se guardaría en un espacio de memoria de 32 bits así:

0 10000100 00111100100000000000000b = 421E4000h

Si estamos trabajando en un entorno de 16 bits, esto se recogería en DX:AX


así: DX = 421E, AX = 4000 y se guardaría en formato Little Endian así: 00 40
1E 42.

Algunos ejemplos:

0 00000000 00000000000000000000000b = Cero Positivo


1 00000000 00000000000000000000000b = Cero Negativo
0 11111111 00000000000000000000000b = Infinito positivo
1 11111111 00000000000000000000000b = Infinito negativo

1 10000111 00110001101000000000000b = C3 98 D0 00h


-1 * 2(135-127) * 1.00110001101 = -100110001.101 = -305.625

0 10000000 00000000000000000000000b = 40 00 00 00h


+1 * 2(128-127) * 1.0 = 10.0 = 2
0 10000101 01101101101001110000000b = 42 B6 D3 80h
+1 * 2(133-127) * 1.0110110110100111 = 1011011.0110100111 =
91.4130859375

0 10000001 01011000000000000000000b = 40 AC 00 00h


+1 * 2(129-127) * 1.01011 = 101.011 = 5.375

0 10000000 11000000000000000000000b = 40 60 00 00h


+1 * 2(128-127) * 1.11 = 11.1 = 3 + 0.5 = 3.5

Obsérvese que multiplicar un número X en binario por 2 n equivale a correr la


coma decimal de X n veces a la derecha si n es positivo, y a la izquierda
si n fuera negativo

Por ejemplo, veamos cómo calculamos la representación decimal del binario


101.011:

La parte entera:

101b=5d.

La parte decimal:

0*2-1+1*2-2+1*2-3 = 1/4d+1/8d=3/8d=0,375d
RealCM01 RealCJ01 RealCN01 RealCF01
Código Código Código Código
[bin] [bin] [bin] [bin]
Resultado en pantalla
>RealCF01
.0004998

Source 01-01 - Imprimir números reales en


pantalla

Lo que hacemos en este ejemplo es ir desmenuzando cada parte del número real
guardado según las especificaciones anteriormente comentadas. Una vez que
tenemos la parte entera y la decimal, hay que convertirlas a ASCII para
imprimirlas.

1.1.7.9.1. Inexactitudes en la representación de los números reales en


binario

Ejercicio: Convertir el número decimal 0,1d a binario.

Pronto comprobaremos que este proceso se repite indefinidamente y nunca


llegaremos al valor exacto de 0,1d en binario. Lo único que podremos hacer
será aproximarlo tanto como podamos, para lo cuál trabajaremos con las cotas
superior e inferior binarias de éste número correspondientes a su truncamiento
(23 bits en caso de los reales simples) y su inmediato superior. Esto implica que
cada vez que operemos con este número, estaremos perdiendo más y más
precisión.

Parte decimal *2 Parte entera


0,1 0,2 0
0,2 0,4 0
0,4 0,8 0
0,8 1,6 1
0,6 1,2 1
0,2 0,4 0
Tabla 01-11 - Representación 1.0 en binario

Ya hemos encontrado el ciclo (00011). Por tanto, la representación binaria es:

0,1 = 0,00011 = 1,10011001100110011001100 * 2-4

cuya representación en IEEE-754 es:

x = 0 01111011 10011001100110011001100...
x' = 0 01111011 10011001100110011001100
x''= 0 01111011 10011001100110011001101 = 3DCCCCCD

x'' está más próximo a x que x', por lo que la representación de x será x''.

Vamos a ver ahora mediante un ejemplo cómo la representación de un número


en coma flotante pierde precisión frente a los números enteros:

Para empezar, el espacio utilizado para representar un número en coma flotante


hay que dividirlo para guardar el exponente y la mantisa, mientras que el
número entero usa todo el espacio para la mantisa. Esto significa que el formato
para representar números en coma flotante tendrá menos precisión que el
formato entero.

Supongamos que tenemos el siguiente formato en 8 bits para representar


números en coma flotante:

EEE MMMMM
3 5

El exponente no va a estar biselado, con lo que podrá representar exponentes


desde el 0 hasta el 7 y la mantisa usa el bit 1 por defecto de la parte entera
numérica.
Si el exponente es 0 y la mantisa es 0, el número más pequeño que podremos
representar será 1,0x20 = 1.

Si el exponente y la mantisa tienen todos sus bits a 1, el número más grande que
podremos representar será 1,11111x27 = 11111100b = 252d.

Por otra parte usando el formato de 8 bits para guardar un entero, tenemos que
el número más pequeño que puede representar es 0 y el mayor es 28-1 = 255.

Para empezar, ya estamos viendo que el número de enteros que abarca es mayor
que el de coma flotante, si bien éste puede representar números decimales que
el otro no puede, claro.

Vamos a ver ahora una curiosa pérdida de datos mediante este método de
representar números en coma flotante. Si el mayor número representable es 252,
¿cuál es su inmediato anterior?. Sería 1,11110 x 27 = 11111000b = 248d.

Es decir que antes del 252 va el 248, ¡nos hemos comido el 249, 250, 251!, que
la representación entera sí posee.

Asímismo, el siguiente en la lista sería

1,11101 x 27 = 11110100b = 244d.

Nos hemos vuelto a saltar 4 valores.

El montante del error es mayor para los valores más altos, puesto que la pérdida
de precisión en la mantisa es amplificada por el exponente. Cuanto más pequeño
es el valor, menor es el error en la precisión.

Por ejemplo, si el valor más pequeño representable es el 1, ¿cuál es el siguiente?

Sería 1,00001 x 20 = 1.03125d

Sin pérdida de precisión comparado con un entero. Y podemos representar


exactamente el número 2:

1,00000 x 21 = 2d

En definitiva, la representación de los números en coma flotante sufren pérdida


de precisión tanto más cuanto mayor sea el valor, mientras que para los valores
pequeños es bastante aproximado. Así que cuidado cuando hagamos
conversiones entre números.

1.1.7.10. Little Endian vs Big Endian


Dependiendo de la computadora que estemos usando, los bytes de una
estructura multibyte se pueden almacenar de dos formas diferentes, que son
llamados "Big Endian" y "Little Endian".

Little Endian significa que el byte más bajo de la estructura multibyte es


almacenado en la dirección de memoria más pequeña, y el byte más alto en la
dirección más grande. Por ejemplo, supongamos que tenemos una estructura
multibyte de tipo LongInt (4 bytes): Byte3 Byte2 Byte1 Byte0, se almacenarían
en memoria de la siguiente manera:

Base Dirección+0 Byte0


Base Dirección+1 Byte1
Base Dirección+2 Byte2
Base Dirección+3 Byte3

Big Endian actúa justo al revés, es decir, el byte más alto de la estructura
multibyte se almacena en la dirección de memoria más pequeña y el byte más
bajo en la dirección más grande. La estructura multibyte anterior, se
almacenaría así:

Base Dirección+0 Byte3


Base Dirección+1 Byte2
Base Dirección+2 Byte1
Base Dirección+3 Byte0

Comentario
La distribución Little Endian la utilizan microprocesadores tales como el de Intel, y la del Big Endian
microprocesadores como los de Motorola, como los que usan los ordenadores "Apple". Obsérvese que con la
primera estructura, que va a ser la que nosotros utilizamos, vamos a ver que los pares de dígitos
hexadecimales que forman un byte se guardan en "orden inverso" al que posee la estructura multibyte. Esta
característica quizás nos desconcierte un poco al principio, pero enseguida se le coge el manejo.

No vamos a entrar aquí en el agrio debate que suscitan los pros y contras de
cada uno de ellos, en cambio vamos a ver un ejemplo ilustrativo de cómo se
guarda un entero de tipo LongInt (4 bytes) usando la distribución Little Endian.
Para ello vamos a utilizar el depurador AFD, que es sencillo de utilizar y
gratuito.

El código fuente del programa LEndian.asm es el siguiente:


; --------------------------------------------------------------
--------------
; - TITULO : Ejemplo de Little Endian
-
; -----
-----
; - AUTOR : Alfonso Víctor Caballero Hurtado
-
; -----
-----
; - VERSION : 1.0
-
; --------------------------------------------------------------
--------------

codigo segment 'code' ; Abre el segmento de


código
assume cs:codigo, ds:codigo, es:codigo, ss:codigo
org 100h ; COM -> comienza en
100h
Inicio: jmp entrada ; Comienzo de la
ejecución
; Este es el espacio reservado para los datos
miLongInt dd 0ABCDEF01h ; Definimos el LongInt
; Aquí podemos definir más prodecimientos
entrada proc ; Abre el
procedimiento de entrada
; Aquí pondremos lo que queramos que haga el programa
mov ax, word ptr miLongInt
mov dx, word ptr miLongInt + 2

; Con lo de aquí abajo terminamos el programa y emitimos una


señal
; de que todo ha ido bien
mov ax, 4c00h ; Servicio 4Ch,
mensaje 0
int 21h ; volvemos al DOS
entrada endp ; cierra el
procedimiento
codigo ends
; Y/O también macros
end Inicio ; le decimos al ensamblador que aquí termina el
programa

Hemos definido una variable de tipo LongInt de 4 bytes llamada "miLongInt"


con el valor 0ABCDEF01h. Le ponemos un cero delante porque sino el
ensamblador supondría que se trata del nombre de una etiqueta. Vamos ahora a
ver cómo se guarda esta variable en memoria. Para ello utilizamos el programa
AFD como hemos dicho. Sabemos que este valor se guarda a partir de la
dirección de memoria 102h porque en el lugar correspondiente a la instrucción
mov dx, word ptr miLongInt aparece en el volcado hexadecimal MOV AX,
[0102]. Nos desplazamos entonces a la parte de abajo correspondiente al
segmento de datos DS mediante la tecla "F8" y avanzamos con la tecla "Av.
Pag." hasta que llegamos a este lugar de memoria. Tras ejecutar las
instrucciones hasta llegar a mov ax, 4c00h para meter en DX y AX el valor alto
y bajo de miLongInt respectivamente, que podemos ver en Fig. 01-04.
Fig. 01-04 - Depurando LENDIAN.COM con AFD

Vemos, pues, que el valor que toman los registros DX:AX = AB CD : EF 01,
que es justamente el valor de la variable miLongInt, además, vemos que su valor
almacenado en memoria es 01 EF CD AB, siguiendo el método Little Endian,
por tanto.

1.2. Registros
Dentro del procesador 8086 existen 14 registros de 16 bits, su misión es
almacenar datos o direcciones de memoria de forma temporal a lo largo del
programa. Hay que utilizarlos siempre que se pueda porque los accesos a
memoria son mucho más lentos que los accesos a los registros. Además, hay
ciertas operaciones que sólo se pueden realizar sobre los registros. Hay registros
que tienen características especiales, los hay que están especializados en una
determinada acción y no todos sirven para realizar algún proceso en concreto.
Por ejemplo:

AX, BX, CX, DX pueden utilizarse como registros de 16 bits o el byte superior
e inferior por separado como sendos registros de 8 bits. No hay ningún otro
registro con esta característica.

1.2.1. Registros de propósito general


Son los registros más utilizados durante el programa.

 Registro AX. Es el acumulador principal, utilizado para operaciones que


implican entrada/salida y la mayor parte de la aritmética.
 Registro BX. Es el registro base. Es el único de proposito general que puede
ser un índice para direccionamiento indexado, así como empleado para
cálculos.
 Registro CX. Es el registro contador. Es muy utilizado con la orden loop para
repetir un ciclo. También es usado para el corrimiento de bits y para muchos
cálculos.
 Registro DX. Es el registro de datos. Es utilizado en operaciones de
entrada/salida, así como en la multiplicación y división con cifras grandes
trabajando con el AX.
1.2.2. Registros de segmento
Definen áreas de 64 Kb dentro del espacio de direcciones de 1 Mb del 8086.
Estas áreas pueden solaparse total o parcialmente. No es posible acceder a una
posición de memoria no definida por algún segmento: si es preciso, habrá de
moverse alguno.

 Registro CS (CODE SEGMENT). El DOS almacena la dirección inicial del


segmento de código de un programa en el registro CS. Esta dirección de
segmento, más un valor de desplazamiento en el registro de apuntador de
instrucción (IP), indica la dirección de una instrucción que es buscada para su
ejecución. Para propósitos de programación normal, no se necesita referenciar
el registro CS.
 Registro DS (DATA SEGMENT). La dirección inicial de un segmento de
datos de programa es almacenada en el registro DS. En términos sencillos, esta
dirección, más un valor de desplazamiento en una instrucción, genera una
referencia a la localidad de un byte específico en el segmento de datos. Suele
ser necesario inicializarlo.
 Registro SS (STACK SEGMENT). Permite la colocación en memoria de una
pila, para almacenamiento temporal de direcciones y datos. El DOS almacena
la dirección de inicio del segmento de pila de un programa en el registro SS.
Esta dirección de segmento, más un valor de desplazamiento en el registro del
apuntador de la pila (SP), indica la palabra actual en la pila que está siendo
direccionada. Para propósitos de programación normal, no se necesita
referenciar el registro SS.
 Registro ES (EXTRA SEGMENT). Algunas operaciones con cadenas de
caracteres (datos de caracteres) utilizan el registro extra de segmento para
manejar el direccionamiento de memoria. En este contexto, el registro ES está
asociado con el registro DI (índice). Un programa que requiere el uso del
registro ES puede inicializarlo con una dirección de segmento apropiada.
1.2.3. Registros punteros de pila
 Registro SP (STACK POINTER). Apunta a la cabeza de la pila. Utilizado en
las instrucciones de manejo de la pila. El MS-DOS suele colocar la pila al final
del espacio de memoria ocupada por el programa. El SP lo coloca justo en la
cima de la pila. Todos los datos que se almacenan en la pila son de longitud
palabra y cada vez que se introduce algo en ella por medio de manejo de pila
(PUSH y POP), el puntero se decrementa en dos (puesto que es una palabra: 2
bytes).
 Registro BP (BASE POINTER). Apunta a una zona de la pila dedicada al
almacenamiento de datos (variables locales y parámetros de las funciones en
los programas compilados).
1.2.4. Registros índices
 Registro SI (SOURCE INDEX). Utilizado como registro de índice en ciertos
modos de direccionamiento indirecto, también se emplea para trabajar con
operaciones de cadenas.
 Registro DI (DESTINATION INDEX). Utilizado en determinados modos de
direccionamiento indirecto y para trabajar con operaciones de cadenas.
1.2.5. Puntero de instrucciones o contador de programa
 Registro IP (INSTRUCTION POINTER). Marca el desplazamiento de la
instrucción en curso dentro del segmento de código. Es automáticamente
modificado con la lectura de una instrucción.
1.2.6. Registro de estado o de indicadores (flags)
Es un registro de 16 bits de los cuáles 9 son utilizados para indicar diversas
situaciones durante la ejecución de un programa. Los bits 0, 2, 4, 6, 7 y 11 son
indicadores de condición, que reflejan los resultados de operaciones del
programa; los bits del 8 al 10 son indicadores de control y el resto no se utilizan.
Estos indicadores pueden ser comprobados por las instrucciones de salto
condicional, lo que permite variar el flujo secuencial del programa según el
resultado de las operaciones.

15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
OF DF IF TF SF ZF AF PF CF
Tabla 01-12 - Estructura de la Palabra de Banderas

La descripción de las banderas es la siguiente:

 CF (Carry Flag). Indicador de acarreo. Su valor más habitual es lo que nos


llevamos en una suma o resta.
 OF (Overflow Flag). Indicador de desbordamiento. Indica que el resultado de
una operación no cabe en el tamaño del operando destino.
 ZF (Zero Flag). Indicador de resultado 0 o comparación igual.
 SF (Sign Flag). Indicador de resultado o comparación negativa.
 PF (Parity Flag). Indicador de paridad. Se activa tras algunas operaciones
aritmeticológicas para indicar que el número de bits a uno resultante es par.
 AF (Auxiliary Flag). Para ajuste con operaciones BCD.
 DF (Direction Flag). Indicador de dirección. Manipulando bloques de memoria,
indica el sentido de avance (ascendente /descendente).
 IF (Interrupt Flag). Indicador de interrupciones: puesto a 1 están permitidas.
 TF (Trap Flag). Indicador de atrape (ejecución paso a paso).
1.2.7 Resumen gráfico del conjunto de registros del
8086:
1.2.7.1. Registros de propósito general
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Nibble 3 Nibble 2 Nibble 1 Nibble 0


AH AL
Tabla 01-13 - Registro AX
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Nibble 3 Nibble 2 Nibble 1 Nibble 0


BH BL
Tabla 01-14 - Registro BX
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Nibble 3 Nibble 2 Nibble 1 Nibble 0


CH CL
Tabla 01-15 - Registro CX
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Nibble 3 Nibble 2 Nibble 1 Nibble 0


DH DL
Tabla 01-16 - Registro DX
1.2.7.2. Registros Punteros de Pila
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Tabla 01-17 - Registro SP


15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Tabla 01-18 - Registro BP


1.2.7.3. Registro Puntero de Instrucciones
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Tabla 01-19 - Registro IP


1.2.7.4. Registros de Segmento
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Tabla 01-20 - Registro CS


15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Tabla 01-21 - Registro DS


15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Tabla 01-22 - Registro ES


15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Tabla 01-23 - Registro SS


1.2.7.5. Registros de índice
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Tabla 01-24 - Registro SI


15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

Tabla 01-25 - Registro DI


1.2.7.6. Registro de banderas
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
OF DF IF TF SF ZF AF PF CF
Tabla 01-26 - Registro de banderas

1.3. Puertos de entrada/salida


Dada la limitación de memoria del 8086, se le añadió un sistema de acceso a
periféricos denominado "puertos de entrada/salida". Las tarjetas colocan su
memoria en un espacio de direcciones especial, separado de la RAM principal
del ordenador, con direcciones en el intervalo 0h-3FFh, es decir 1.024 puertos
en total. El acceso a estas posiciones se lleva a cabo mediante instrucciones
especiales de entrada/salida, que se encuentran bastante limitadas en sus
posibilidades, lo que provoca que las tarjetas que utilizan este sistema ocupen
muy pocas direcciones de E/S. Este es el sistema más utilizado, y no existe
ninguna tarjeta del PC que no lo utilice. Se utilizan las instrucciones IN y OUT
para manejar E/S directamente a nivel de puerto.

IN transfiere información desde un puerto de entrada al AL si es un byte y al


AX si es una palabra. El formato general es:

IN reg-acum, puerto

OUT transfiere información desde un registro a un puerto, si es un byte y desde


AX si es una palabra. El formato general es:

OUT puerto, reg-acum

Podemos especificar una dirección de puerto estática o dinámicamente:

Estáticamente: Podemos utilizar un operando desde 0 hasta 255 directamente


como:

Input IN AL, puerto ; Entrada de un byte


Output OUT puerto, AX ; Salida de una palabra
Dinámicamente: Podemos utilizar el contenido del registro DX de 0 a 65.535
indirectamente, puede ser adecuado para ir incrementando DX a la vez que se
van procesando las direcciones de los puertos. Veamos el siguiente ejemplo:

MOV DX, 60h ; Puerto 60h (teclado)


IN AL, DX ; Obtiene un byte

Podemos ver algunos de los principales puertos en el apéndice E.

Aunque la práctica recomendada es utilizar las interrupciones del DOS y del


BIOS, se pueden pasar por alto con seguridad para acceder directamente a los
puertos 21h, 40-42h, 60h, 61h y 201h.

Por ejemplo, para mandar un carácter a la impresora mediante la interrupción


17h de la BIOS, debemos insertar el número de puerto de la impresora en el
registro DX:

MOV AH, 00h ; Petición para imprimir


MOV AL, char ; Carácter que se va a imprimir
MOV DX, 0 ; Puerto de la impresora 0 = LPT1
INT 21h ; Llama al DOS

1.4. Organización de direcciones:


Segmentación
El diseño del 8086 es de 20 bits para el bus de direcciones y 16 para el de datos.
Con 20 bits para el bus de direcciones el número máximo de posiciones de
memoria es de 220 bytes = 1.048.576 bytes = 1 Mb (20 es el exponente del 2);
sin embargo nuestros registros son de 16 bits que pueden direccionar a un
máximo de 64 Kb (64 Kb es 216 bytes = 65.536 bytes). ¿Cómo acceder entonces
al megabyte de memoria?. Una posible solución consistía en utilizar dos de
nuestros registros de 16 bits, lo cuál equivaldría a utilizar un registro de 32 bits,
pero por aquél entonces probablemente pensaron que nadie habría de necesitar
nunca tanto, por lo cuál idearon un sistema denominado segmentación que
consiste en dividir la memoria en grupos de 64 Kb. Cada grupo se asocia con
un registro de segmento de 16 bits; el desplazamiento dentro de cada segmento
lo proporciona otro registro de 16 bits. Para alcanzar los 20 bits multiplicamos
por 16 el valor del registro de segmento y sumamos el desplazamiento.
Obsérvese que multiplicar por 24=16 equivale a correr el número cuatro
posiciones a la izquierda y los cuatro dígitos de la derecha se rellenan con ceros,
mientras que dividir por 16 equivaldría a correr el número 4 dígitos de la
derecha y los cuatro de la izquierda se rellenan con cero.

OFFSET = SEGMENT * 16
SEGMENT = OFFSET / 16 (perdemos los 4 bits inferiores)
dirección = segmento * 16 + desplazamiento

SEGMENT 0010010000010000----
OFFSET ----0100100000100010
Dirección de 20 bits 00101000100100100010
Por ejemplo, con DS:SI
====== DS ======
====== SI ======

Esta es la forma DS:SI con la que accedemos a 20 bits con dos registros de 16
bits. El segmento en DS y el desplazamiento en SI. Obsérvese que DS y SI se
solapan, esto implica que existe más de una forma de acceder a la misma
dirección de memoria, por ejemplo, 3D00h:0300h es equivalente a
3D30:0000h. Veamos por qué:

Téngase en cuenta que 16 decimal equivale a 10 hexadecimal. Y como


sabemos, multiplicar por 10 equivale a añadir un cero a la derecha. Pues bien,
vamos a encontrar la dirección de memoria accedida por estas segmentaciones:

3D00h:0300h => dirección = 3D00h * 10h + 0300h = 3D000 + 0300h =


3D300h
3D30h:0000h => dirección = 3D30h * 10h + 0000h = =
3D300h

1.4.1. Modos de direccionamiento


Son los distintos modos de acceder a los datos en memoria por parte del
procesador. Antes de nada, la sintaxis general de las instrucciones suele ser la
siguiente:

instrucción DESTINO, FUENTE

Que indica que el contenido de fuente se deja en destino.

 Direccionamiento inmediato. El operando es una constante situada detrás del


código de la instrucción. Sin embargo, como registro destino no se puede
indicar uno de segmento (habrá que utilizar uno de datos como paso
intermedio).
 ADD AX, 0FFFh ; 0FFFh es una constante numérica
 dato EQU 0FFFh ; Declaramos un símbolo constante
 MOV AX, dato
 DATO dw 0FFFh ; Dato es una variable
 MOV AX, OFFSET dato ; AX = dirección de memoria de dato
 Direccionamiento de registro. Los operandos, necesariamente de igual
tamaño, están contenidos en los registros indicados en la instrucción.
 MOV AX, [57D1h]
 MOV AX, ES:[429Ch]
 Direccionamiento directo o absoluto. El operando está situado en la dirección
indicada en la instrucción, relativa al segmento que se trate.
 MOV AX, [57D1h]
 MOV AX, ES:[429Ch]
 Direccionamiento indirecto. El operando se encuentra en una dirección
señalada por un registro de segmento*16 más un registro base (BX/BP) o índice
(SI/DI) (Nota: BP actúa por defecto con SS).
 MOV AX, [BP] ; AX = [SS*16+BP]
 MOV ES:[DI], AX ; [ES*16+DI] = AX
 Indirecto con índice o indexado. El operando se encuentra en una dirección
determinada por la suma de un registro de segmento*16, un registro de índice
Si o DI y un desplazamiento de 8 ó 16 bits.
 MOV AX, [DI+desp] ; AX = desp[DI]
 MOV [SI+desp], BX ; desp[SI] = BX
 Indirecto con base e índice o indexado a base. El operando se encuentra en
una dirección especificada por la suma de un registro de segmento*16, uno de
base, uno de índice y opcionalmente un desplazamiento de 8 ó 16 bits.
 MOV AX, ES:[BX+DI+desp] ; AX = ES:desp[BX][DI]
 MOV CS:[BX+SI+desp], CX ; CS:desp[BX][SI] = CX

1.4.2. Tipos de direccionamiento


 Corto. Si es alcanzada por medio de un desplazamiento y está limitada a una
distancia de -128 a 127 bytes.(8 bits)
 Cercano. Si es alcanzada por medio de un desplazamiento y está limitada a una
distancia de -32.768 a 32.767 bytes dentro del mismo segmento.
 Lejano. Si está en otro segmento y es alcanzada por medio de una dirección de
segmento y un desplazamiento.
1.4.3. La pila
Es un bloque de memoria de estructura LIFO ("Last Input First Output": último
en entrar, primero en salir), gráficamente podría decirse que tiene una estructura
de una pila de platos: el primero se coloca sobre la mesa, el segundo sobre el
primero, el tercero sobre el segundo y así sucesivamente; y cuando vamos a
coger un plato, el primero que cogemos es el último que pusimos, el de más
arriba.

El tamaño de la pila hay que definirlo en un archivo EXE, si es un archivo COM


el DOS establece la pila al final del segmento de 64K y carga el registro SP con
FFFEh, la parte superior de la pila (el tope de la pila) si el segmento de 64K es
suficientemente grande. Se incrementa en dirección a los datos.

La pila se direcciona mediante desplazamientos desde el registro SS (stack


segment). Las posiciones individuales dentro de la pila se calculan sumando al
contenido del segmento de pia SS un desplazamiento contenido en el registro
puntero de pila SP. Todos los datos que se almacenan en la pila son de longitud
palabra, y cada vez que se introduce algo en ella por medio de las instrucciones
de manejo de pila (PUSH y POP), el puntero se decrementa en dos; es decir, la
pila avanza hacia direccines decrecientes. El registro BP suele utilizarse
normalmente para apuntar a una cierta posición de la pila y acceder
indexadamente a sus elementos, generalmente en el caso de variables, sin
necesidad de desapilarlos para consultarlos.

La pila es utilizada para preservar el valor de un registro o de una variable de


16 bits (1 palabra) metiéndola en la pila con la orden PUSH que salvamos para
recuperarla posteriormente con la orden POP que cogemos. Las órdenes POP
deben hacerse justo en orden inverso a las PUSH puesto que la pila tiene
estructura LIFO. Su empleo es la única forma de definir variables locales.
También es utilizada internamente para cargar el IP cuando llamamos a una
subrutina y luego se recoge éste. También se puede emplear para pasar
parámetros a los procedimientos.

Puesto que la pila va a ser muy utilizada, tanto por el programador como por
operaciones internas, es muy necesario ir desapilando todo lo apilado para
evitar una pérdida de control sobre el ordenador por desbordamiento de ésta.

Fig. 01-05 - Funcionamiento de la Pila

1.4.4. Memoria Interna


La microcomputadora posee dos tipos de memoria interna:

 RAM. Memoria de acceso aleatorio.


 ROM. Memoria de sólo lectura.

Los bytes en memoria se numeran en forma consecutiva, iniciando con 00, de


modo que cada localidad tiene un número de dirección único.
Aquí abajo vemos un mapa físico de la memoria del PC tipo 8086. Del primer
megabyte de memoria, los primeros 640K los ocupa la RAM, la mayor parte de
la cuál está disponible para su uso.

Inicio Dirección Uso


Dec 960K Hex F0000 64K sistema base de ROM
768K C0000 192K área de expansión de memoria (ROM) Memoria Superior
640K A0000 128K área de despliegue de vídeo (RAM)
0K 0 640K memoria (RAM) Memoria Convencional
Tabla 01-28 - Mapa de la Memoria Física
 ROM. Es un chip especial de memoria que sólo puede ser leída. Debido a que
las instrucciones y los datos están "grabados" permanentemente en un chip de
ROM, no pueden ser alterados. El Sistema Básico de Entrada/Salida (BIOS) de
ROM inicia en la dirección 768K y maneja los dispositivos de entrada/salida,
como un controlador de disco duro. La ROM que inicia en 960K controla las
funciones básicas de la computadora, como la autoprueba al encender, patrones
de puntos para los gráficos y autocargador de disco. Cuando se enciende la
computadora, la ROM realiza ciertas verificaciones.
 RAM. Un programador está preocupado principalmente con la RAM, que sería
mejor llamada memoria de lectura-escritura. La RAM se dispone como una
"hoja de trabajo" para almacenamiento temporal y ejecución de programas. Ya
que el contenido de la RAM se pierde cuando se apaga la computadora, debe
reservar almacenamiento externo para guardar programas y datos.
1.4.5. Memoria Convencional

Fig. 01-06 - Mapa de la Memoria Convencional

Encender la computadora provoca una "inicialización" (a veces llamado


"arranque en frío"). El procesador introduce un estado de restauración, limpia
todas las localidades de memoria (es decir) coloca cero en todas ellas), realiza
una verificación de paridad de la memoria y asigna al registro CX la dirección
del segmento FFFF[0]h y al registro IP el desplazamiento cero. Por tanto, la
primera instrucción a ejecutarse está en la dirección formada por la pareja
CS:IP, que es FFF0h, la cuál es el punto de entrada al BIOS en ROM.

La rutina de BIOS que se inicia en FFFF0h verifica los diferentes puertos para
identificarlos e inicializa los dispositivos que están conectados a la
computadora. Después el BIOS establece dos áreas de datos. En la Fig. 01-06
vemos un mapa de la memoria convencional.

 La tabla de servicios de interrupciones se inicia en memoria baja en la


localidad 0 y contiene las direcciones de las interrupciones que ocurren.
 área de datos de BIOS. Se inicia en la localidad 0040:0000, que está
estrechamente relacionada con los dispositivos conectados. A continuación el
BIOS determina si está presente un disco que contenga los archivos de sistema
del DOS y, en caso de que así sea, accede al cargador de arranque desde ese
disco, cargando los archivos de sistema IO.SYS y MSDOS.SYS desde el disco
hacia la memoria y transfiere el control al punto de entrada del IO.SYS.
1.4.6. Segmentos de Ensamblador
Tanto el código como los datos se agrupan en bloques conocidos como
segmentos. A pesar de que tienen el mismo nombre, los segmentos de un
programa ensamblador no son equivalentes a los segmentos en que divide la
memoria el procesador 8086. Una diferencia notable es el hecho de que un
segmento ensamblador puede comenzar en cualquier posición de memoria
mientras que, como ya se vio, los segmentos de memoria comienzan siempre
en posiciones múltiplos de 16. Si bien también tienen similitudes:

Un segmento no puede ocupar, entre código y datos, más de 64 kilobytes de


memoria.

El nombre de un segmento puede utilizarse como valor simbólico del segmento


de memoria en que se encuentra éste.

Codigo SEGMENT
MOV AX, Codigo ; Coloca en AX el valor del
segmento
Codigo ENDS ; donde se encuentra el código

Sin embargo, esta forma de utilizar el nombre del segmento de código sólo es
posible en programas de tipo .EXE, en programas de tipo .COM ni si quiera es
necesario, puesto que sólo cuenta con un segmento.

La forma de definir un segmento mediante la sintaxis clásica de MASM es


mediante una pareja de directivas que marcan su comienzo y final. Estas
directivas son SEGMENT para el comienzo y ENDS para el final. Ambas van
precedidas por el nombre que se le da al segmento y, en el caso de la primera,
va seguida de una serie de palabras que indican algunas características del
segmento. Obsérvese que con la sintaxis de NASM no es necesario definir el
final del segmento, pues se considera implícito al inicio del siguiente.

Las características o atributos que se aplican a la directiva SEGMENT son la


alineación, la combinación y la clase, las cuales pueden darse en cualquier
orden. En caso de que se omitan algunos de estos cuatro tipos de atributos, el
ensamblador tomará para ellos sus valores por defecto.

1.4.6.1. Alineación
Se utiliza para que el segmento comience en una dirección de memoria que sea
múltiplo de un valor determinado. Sus posibles valores son los siguientes:

Alineación Múltiplo de
BYTE 1
WORD 2
DWORD 4
PARA 16
PAGE 256
MEMPAGE 4096
Tabla 01-29 - Alineación

El valor por defecto es PARA, el cuál es utilizado en la mayoría de las


ocasiones, puesto que, al utilizar una dirección múltiplo de 16 como comienzo
hace que el segmento ensamblador coincida exactamente sobre un segmento de
memoria, formándose así una equivalencia entre ambos.

Obsérvese que el 8086 y 80286 tienen un bus de datos de 16 bits (una palabra),
por lo que trabajan más rápido si acceden a datos con tamaño de palabra.
Supongamos el valor 8A9Bh que se encuentra en la dirección de memoria 912-
913, el procesador los podrá recuperar en una sola pasada metiéndolos, por
ejemplo en el registro AX; sin embargo, si este mismo valor se encontrase en
911-912 tendría que recuperarlo en dos pasos: primero leería 910-911,
metiendo 9B en AL y luego leería 912-913 metiendo 8A en AH. Veámoslo
gráficamente:

912 913
9B 8A
AL AH
Tabla 01-30 - Primer caso
910 911 912 913
?? 8A 9B ??
AH AL
Tabla 01-31 - Segundo caso

Contamos con la directiva ALIGN para informar al ensamblador que nos alinee
elementos en límites. Por ejemplo "ALIGN 2" alinea en un límite de palabra.
De forma que en un programa con muchos datos resulta interesante su uso para
acelerar el proceso de carga. Se emplea inmediatamente antes del dato que
queramos alinear. Tenemos el código de ejemplo en "AlignC?1.asm".
Evidentemente con esta directiva se ocupa más espacio, compruébese
eliminándolo del código y volviendo a compilarlo; sin embargo, para una
colección de varias palabras, sólo necesitamos usarlo para la primera.

FASM MASM NASM Resultado en pantalla


Código Código Código
; Salida con DEBUG:
; Sin "ALIGN 2"
; C:\Trabajo\AOE\Codigos\Cap01>debug ALIGNCN1.com
; -d 100, 10F
; 0CC8:0100 B8 09 01 B8 00 4C CD 21-3F 9B 8A 61 20 70
61 72 .....L.!?..a par
;
; Con "ALIGN 2"
[bin] [bin] [bin]
; C:\Trabajo\AOE\Codigos\Cap01>debug ALIGNCN1.com
; -d 100, 10F
; 0CC8:0100 B8 0A 01 B8 00 4C CD 21-3F 90 9B 8A 20 70
61 72 .....L.!?... par

; Sin "ALIGN 2" 8A9Bh está en 109-10A, con "ALIGN 2"


está en 10A-10B

Source 01-01 - AlignC?1.asm


1.4.6.2. Combinación

Especifica cómo debe combinarse el segmento con otros que tengan el mismo
nombre y que se encuentren en otros módulos del programa.

Combinación Forma de combinación


PRIVATE No se combina con otros segmentos del mismo nombre
PUBLIC Se combina con los segmentos del mismo nombre en un único segmento
STACK El segmento se combina con otros del mismo nombre para formar la pila del programa
COMMON Coloca el segmento en la mismas posiciones de memoria que otros con el mismo nombre
El segmento hace referencia al segmento físico indicado por el valor numérico xxxx. Ese tipo
AT xxx de segmentos se utilizan para acceder a memoria fuera del programa, como vectores de
interrupción o datos de la BIOS
El segmento se colocará al final del programa, permitiendo utilizar la memoria que hay más
MEMORY allá del mismo. Sólo puede haber un segmento de este tipo y, si hay varios, el resto se tratan
como COMMON
VIRTUAL Define un tipo de segmento común a varios módulos, pero que se define dentro de otro
Tabla 01-31 - Combinación

El valor que se toma por defecto es PRIVATE.

1.4.6.3. Clase

Especifica un nombre de clase entre comillas para el segmento que el enlazador


utilizará a la hora de ordenar los segmentos. Esta ordenación provocará que
segmentos con el mismo atributo de clase se coloquen agrupados unos tras
otros. Esta característica se utiliza para conseguir que todos los segmentos de
un mismo tipo (datos, código, pila) se coloquen juntos en memoria cuando se
cargue el programa.

Ejemplos:
 datos SEGMENT PARA PUBLIC 'data' ; Abre el segmento de
datos
 msg db 'Hola, mundo!$' ; Mensaje a imprimir
 datos ENDS
 ...
 ASSUME CS: Codigo, DS: Datos
 MOV AX, SEG datos
 MOV DS, AX

 datos1 SEGMENT PARA PUBLIC 'data' ; Abre el segmento
de datos
 msg1 DB '¡Hola, $' ; Mensaje a imprimir
 datos1 ENDS
 datos2 SEGMENT PARA PUBLIC 'data' ; Abre el segmento
de datos
 msg2 DB 'mundo!$' ; Mensaje a imprimir
 datos2 ENDS
 ...
 datos GROUP datos1, datos2
 ASSUME CS: codigo, DS: datos

Los dos códigos fuente completos se pueden ver en el directorio Cap2 en los
archivos MundoXM1.asm y SegtosM1.asm. Ambos ejemplos tienen, a efectos
prácticos, el mismo resultado, lo veremos en el siguiente capítulo.

 pila SEGMENT PARA STACK 'stack' ; Define la pila del


programa
 DW 30h DUP (?) ; 30h palabras de pila
 pila ENDS

 video SEGMENT AT 0B800h ; Define un segmento para
direccionar
 video ENDS ; la memoria de vídeo en
modo texto

1.4.6.4. Acceder a datos de Segmento

Para acceder a los datos de segmento es preciso meter su dirección dentro de un


registro de segmento. Si por ejemplo hemos definido un registro de segmento
llamado "datos" como en el primer ejemplo anterior, podemos indicarle al
ensamblador dónde se encuentra de la siguiente forma:

MOV AX, [SEG] datos


MOV DS, AX
ASSUME DS:datos
Es muy conveniente recordar que no se le puede dar un valor directamente a un
registro de segmento, sino que hay que utilizar un registro de intermediario,
como AX en este caso. Con la sintaxis NASM y FASM no podremos usar
ASSUME, puesto que no nos permite "asumir" nada y basta con la primera
parte. Mediante las nuevas directivas de segmento de la sintaxis MASM
podemos, además hacerlo de otra manera.

.data ; Abre el segmento de datos


msg db 'Hola, mundo!$' ; Mensaje a imprimir
...
.CODE
MOV AX, @DATA
MOV DS, AX

Para acceder a una dirección de memoria, se puede poner explícitamente el


valor del segmento donde está aunque ya lo tenga el segmento DS. Por ejemplo:

MOV AX, DS:msg

La directiva ASSUME puede ir seguida de uno o varios nombres de registros


de segmento seguidos de dos puntos y el nombre del segmento hacia el que
están apuntando. Un ejemplo sería:

ASSUME CS:Codigo, DS:Datos, SS:Pila

En caso de que se desee anular la asociación entre un registro de segmento y un


segmento, se puede utilizar el nombre de segmento NOTHING para indicar este
hecho.

ASSUME DS:Datos ; Informa que DS apunta al


segmento Datos
ASSUME DS:NOTHING ; Informa que DS ya no a
punta a Datos

Un detalle importante es que ASSUME es una directiva y, como tal, no genera


código que cargue en los registros de segmento el valor que indica. Es, por
tanto, labor del programador el iniciar los registros de segmento de forma
adecuada. Es además imprescindible colocar al principio de todos los
segmentos del código la directiva ASSUME, asociando el registro CS al
segmento actual.

Codigo SEGMENT
ASSUME CS:Codigo

En el caso de programas EXE se añadirá otra directiva opcional ASSUME


asociando el registro SS al segmento de pila, puesto que el propio MSDOS se
encarga de inicializar los registros SS y SP al segmento de pila definido por el
programa, por lo que no habrá que preocuparse por cambiarlos.

Los segmentos, además, se pueden agrupar entre sí para formar segmentos


mayores mediante la directiva GROUP que va precedida por el nombre del
nuevo segmento formado por la combinación de los segmentos cuyos nombres
se indican separados por comas a continuación de la directiva GROUP. El
tamaño conjunto de los segmentos de un grupo no debe superar los 64Kb. El
nombre de este grupo puede utilizarse como si fuese un nombre de segmento.
Ya hemos visto un ejemplo anteriormente.

Recuérdese que muchas de estas acciones se pueden obviar utilizando directivas


simplificadas de segmento, que
son: .MODEL, .CODE, .CONST, .DATA, .DATA?, .FARDATA, .FARDA
TA?, .STACK, .STARTUP, y .EXIT. La estructura de un programa en
ensamblador con estas directivas vienen más adelante.

1.4.6.5. Tipos de Modelos de Memoria

La sintaxis de MASM soporta modelos de memoria estándar usados por


lenguajes de alto nivel.

Modelo de Código por Datos por Sistema Datos y Código


Memoria Defecto Defecto Operativo combinados
Tiny Cercano Cercano MS-DOS Sí
MS-DOS,
Small Cercano Cercano No
Windows
MS-DOS,
Medium Lejano Cercano No
Windows
MS-DOS,
Compact Cercano Lejano No
Windows
MS-DOS,
Large Lejano Lejano No
Windows
MS-DOS,
Huge Lejano Lejano No
Windows
Flat Cercano Cercano Windows NT Sí
Tabla 01-32 - Modelos de Memoria

Los modelos Small, Medium, Compact, Large y Huge son los modelos de
memoria tradicionalmente reconocidos por la mayoría de lenguajes.

 El modelo SMALL soporta un segmento de datos y un segmento de código.


Todos los datos y códigos son cercanos por defecto.
 El modelo LARGE soporta múltiples segmentos de datos y múltiples
segmentos de código. Todos los datos y códigos son lejanos por defecto.
 El modelo MEDIUM soporta múltiples segmentos de código y un solo
segmento de datos.
 El modelo COMPACT soporta un solo segmento de código y múltiples
segmentos de datos.
 El modelo HUGE soporta objetos de datos mayores que un solo segmento, pero
la implementación de este modelo debe ser cosa del programador. Puesto que
el ensamblador no porporciona soporte directo para esta característica, este
modelo es equivalente al LARGE.
 El modelo TINY sólo es admitido por el MS-DOS. Aloja todos los datos y
código en el mismo segmento que no puede sobrepasar los 64Kb de longitud,
al igual que el tamaño del programa completo.
 El modelo FLAT funciona exactamente igual que el modelo TINY, sólo que en
32 bits, alojándose código y datos en el mismo y único segmento de 2 32 = 4
gigas de tamaño. Para poder utilizarlo, hay que anteponerle al menos la
directiva .386. Las direcciones y punteros son todos cercanos de 32 bits.
1.4.6.6. Símbolos Predefinidos

La sintaxis MASM incluye algunos símbolos predefinidos (también llamados


equivalencias predefinidas). Podemos usar estos símbolos en cualquier punto
de nuestro código para recoger su valor. Sólo hay que tener en cuenta las
mayúsculas y minúsculas en caso de que se especifique la opción /Cp.

Símbolos predefinidos con información de segmentos:

Símbolo Descripción
@code Devuelve el nombre del segmento de código
@CodeSize Devuelve un entero que representa la distancia por defecto al código
@CurSeg Devuelve el nombre del segmento actual
@data Expande hasta el DGROUP
@DataSize Devuelve un entero que representa la distancia por defecto a los datos
@fardata Devuelve el nombre del segmento definido por .FARDATA
@fardata? Devuelve el nombre del segmento definido por .FARDATA?
@Model Devuelve el modelo de memoria seleccionado
@stack Expande al DGROUP para pilas cercanas o al STACK para pilas lejanas
@WordSize Proporciona el atributo de tamaño del segmento actual
Tabla 01-33 - Símbolos de Segmento

Símbolos predefinidos con información de ámbito:

Símbolo Descripción
@Cpu Contiene una máscara de bits especificando el modo de procesador
@Environ Devuelve valores de las variables de entorno durante el ensamblaje
@Interface
Contiene información acerca de los parámetros del lenguaje
Representa el texto equivalente del número de versión del MASM. P.e. para MASM 6.1., lo
@Version
expande a 610.
Tabla 01-34 - Símbolos de ámbito

Símbolos predefinidos con información de fecha y hora:


Símbolo Descripción
@Date Proporciona la fecha actual del sistema durante el ensamblaje
@Time Proporciona la hora actual del sistema durante el ensamblaje
Tabla 01-35 - Símbolos de ámbito

Símbolos predefinidos con información del fichero:

Símbolo Descripción
@FileCur Nombre y extensión del fichero actual
@FileName Da el nombre del fichero principal que está siendo ensamblado
@Line Da el nº de línea del código fuente del fichero actual
Tabla 01-36 - Símbolos de Información de Fichero

Símbolos de Manipulación de Macros:

Símbolo Descripción
@CatStr Devuelve la concatenación de dos cadenas
@InStr Devuelve la posición inicial de una cadena dentro de otra
@SizeStr Devuelve la longitud de una cadena
@SubStr Devuelve una subcadena dentro de otra
Tabla 01-37 - Símbolos de Manipulación de Macros

1.5. Programas ejecutables sobre MS-


DOS

Fig. 01-07 - Estructura en Memoria de los programas COM

Aparte de los ficheros batch, sólo son los archivos .COM y .EXE, llamados así
porque esta es su extensión. La diferencia básica entre los dos es que los .COM
son una imagen exacta del programa cargado en memoria mientras que los
EXE, su estructura final en memoria se la da el DOS, y que los COM no pueden
superar los 64K de tamaño, mientras que los EXE no tienen esta restricción.
Los programas COM son más rápidamente cargados en memoria porque no
necesitan que el DOS les de su estructura y son más pequeños, pero esto es
inapreciable en los ordenadores actuales. Los ficheros COM son una reliquia
del pasado, cuando la memoria era escasa y los programas no pasaban de los 64
Kb y se han ido manteniendo por compatibilidad; si bien, para crear programas
residentes en memoria son muy adecuados y su modelo de memoria es parecido
al flat de windows.

Fig. 01-08 - Estructura en Memoria de los Programas EXE

En los 64 Kb que pueden tener los programas COM como máximo, se han de
incluir el código de programa, los datos y el stack. Esto tiene como
consecuencia que los registros de segmento durante el inicio del programa y
durante la ejecución del mismo apunten al inicio del semgneto de memoria de
64K que el DOS ha reservado antes de cargar el programa. En los programas
EXE, el código, datos y stack se guardan en segmentos diferentes que pueden
estar repartidos en diferentes segmentos dependiendo de su tamaño.

Independientemente de si se ejecuta un programa COM o EXE, el DOS crea


antes de la ejecución del programa una estructura de datos en la memoria, con
el nombre de PSP (Program Segment Prefix), que se compone de 256 bytes. Se
antepone inmediatamente al programa en la memoria, como muestran las Fig.
01-07 y Fig. 01-08:

1.5.1. El PSP
Realmente es una reliquia del DOS, mantenido por compatibilidad. Contiene
numerosos datos que el DOS necesita para la gestión de un programa [bin],
aunque para un programador, la mayoría de esta información, carece de interés,
aunque sí hay algo de mucha utilidad con la que podremos gestionar los
parámetros de entrada en la línea de comandos. Veamos esquemáticamente la
estructura del PSP:

Dirección Contenido
00h-01h Llamada de la interrupción 20h
02h-03h Dirección final de la memoria ocupada por el programa. Por ejemplo A0000 se pone como 00A0h
04h Reservado
05h-09h FAR-CALL a la interrupción 21h
0Ah-0Dh Dirección de terminación (dirección del segmento para INT 22h)
0Eh-11h Dirección de salida de Ctrl+Break (dirección de segmento para INT 23h)
12h-15h Dirección de salida de error crítico (dirección de segmento para INT 24h)
16h-17h Segmento del PSP del proceso padre
18-2Bh Tabla de manejadores de archivo por omisión
2Ch-2Dh Dirección de segmento de las variables de entorno
2Eh-31h Reservado
32h-33h Longitud de la tabla de manejadores de archivo
34h-37h Apuntador lejano a la tabla de manejadores
38h-4Fh Reservado
50h-51h Llama a la función DOS (INT 21h y RETF)
52h-5Bh Reservado
5Ch-6Bh FCB #1
6Ch-7Fh FCB #2
80h Número de caracteres en la línea de comandos
81h-FFh Línea de comandos
Tabla 01-38 - Esquema del PSP

El contenido "reservado" indica que son áreas no documentadas oficialmente


por Microsoft, pero no tiene la menor trascendencia.

A continuación describimos algunas de las áreas más relevantes:

 En la dirección de 00h se encuentra un comando de lenguaje máquina que llama


a una función del DOS para terminar el programa. Vuelve a liberar la memoria
del programa y transfiere el control mediante la ejecución del programa al
procesador de comandos o al programa que ha iniciado la ejecución del
programa a terminar. Antes se necesitaba este comando al final de un programa
COM o EXE para terminar su ejecución, pero actualmente se utiliza la función
4Ch de la interrupción 21h del DOS.
 En la dirección 02h se encuentra un word que marca la dirección final de la
memoria RAM reservada para el programa. Si un programa necesita más
memoria RAM adicional, puede determinar con la ayuda de este valor, si aún
queda espacio entre su final y el fin de la memoria RAM reservada. Si este es
el caso, puede utilizar esta memoria para sus propios fines, de lo contrario ha
de pedir memoria RAM mediante una función DOS.
 En 2Ch tenemos el segmento donde se encuentran las variables de entorno, tales
como COMSPEC, PATH, PROMPT, etc.
 La dirección 80h registra en un byte el número de caracteres que introdujimos
en la línea de comandos excepto el carácter 13 de retorno de carro, con lo que
se deduce que sólo pueden ser entre 0 y 255 caracteres.
 La dirección 81h recoge todos los caracteres que se indicaron después del
nombre del archivo durante la llamada desde la línea de comandos.

La dirección del PSP en los programas COM viene determinada por la de


cualquier registro de segmento (CS=DS=ES=SS) nada más comenzar la
ejecución del mismo. Sin embargo, en los programas de tipo EXE sólo viene
determinada por DS y ES. En cualquir caso, existe una función del DOS para
obtener la dirección del PSP, cuyo uso recomienda el fabricante del sistema en
aras de una mayor compatibilidad con futuras versiones del sistema operativo.
La función es la 62h y está disponible a partir del DOS 3.0.
1.5.2. Programas COM
Cuando se crea un programa COM, hay que dejarle un hueco al inicio de 100h
para que quepa aquí el PSP, por esto se utiliza la orden ORG 100h,
inmediatamente después es necesario encontrar una sentencia ejecutable,
aunque sea una de salto al inicio del programa, para que éste pueda actuar, por
eso solemos poner un JMP Inicio, por ejemplo.

Como ya hemos dicho, un programa COM puede tener una longitud máxima de
64 Kb (65.536 bytes), de los que se ha de descontar la longitud del PSP (256
bytes) y al menos 1 word (2 bytes) para la pila, aunque es recomendable un
mínimo de 256 bytes.

Aunque la longitud de un programa COM nunca puede pasar los 64 Kb, el DOS
siempre reserva la memoria RAM completa para ellos, con lo que no se podría
llamar a otro programa con "EXEC"; esto se puede solucionar liberando en el
programa COM la memoria que no necesita, con ayuda de una función del DOS,
para su utilización.

Cuando se pasa el control a un programa COM, todos los registros de segmento


apuntan al principio del segmento COM en la memoria, con ello, el inicio del
programa COM se encuentra en la dirección de offset 100h. El puntero de stack,
SP contiene el valor FFFEh, y se encuentra al final del segmento COM de 64
Kb y va creciendo "hacia abajo", es decir, en dirección al final del programa, y
debe ser el programador el que cuide que no se solapen, en cuyo caso
probablemente el programa se colgaría.

Un programa COM se puede terminar con un RET ("near return"), que provoca
que el programa continúe en la dirección que está como valor superior en la
pila, donde el DOS colocó un cero antes de iniciar el programa COM, por tanto
saltamos a la dirección de memoria CS:0000; pero aquí se encuentra el inicio
del PSP donde está la interrrupción 20h, que termina la ejecución de un
programa. Sin embargo, lo más recomendable es terminar mediante la función
4Ch de la interrupción 21h, mediante la cuál podemos pasar además un mensaje
a su invocador en el registro AL, estableciendo el valor 0 como terminación de
programa normal.

1.5.2.1. Creación de un programa COM en ensamblador

Ya hemos dicho que un programa COM se guarda en disco como una imagen
directa del código máquina y además, el DOS no lo carga en RAM en una
dirección fija, sino en una divisible por 16, con lo que los segmentos del
programa son desconocidos hasta la propia ejecución del programa. Debido a
todo ello este tipo de programas no puede tener direcciones de segmento
explícitas, es decir, comandos FAR. Sólo se permiten los comandos NEAR, que
sólo contienen una dirección de offset, no de segmento. De esta forma, estos
comandos siempre se refieren al segmento actual en el registro CS, DS, ES o
SS, y no a una dirección de segmento determinada. Esto implica que no están
permitidos los saltos lejanos ni las llamdas lejanas, así como instrucciones LDS
o LES. Si se usa algo de esto, se podrá compilar, pero no se podrá pasar a
archivo COM. Sin embargo no está prohibido cargar valores de segmentos
constantes, como por ejemplo, la dirección de segmento de la RAM de vídeo
en un registro de segmento.

También sabemos que el DOS asigna toda la memoria a cualquier programa


que carga en RAM, lo cuál provoca problemas a un TSR ("Terminate but Stay
Resident"), programa que permanece ejecutándose mientras se puede cargar
otro en RAM, pues el DOS supone que no hay más espacio para cargar un
programa estando ya uno ejecutándose. El mismo problema tenemos cuando
pretendemos ejecutar un programa desde otro.

Esto se puede solucionar de dos formas:

 Podemos liberar la memoria más allá del segmento COM de 64K.


 O bien liberamos la memoria completa que el programa no necesita, donde
también se incluye la memoria no necesitada dentro del segmento COM,
obteniendo así más espacio para otros programas, pero con el inconveniente de
que la pila queda fuera del espacio de memoria protegido para el programa,
pues el DOS lo coloca al final del segmento de 64K del COM. Pero entonces la
pila se ha de desplazar antes de liberar la memoria al final de la memoria
reservada; siendo preciso asignarle un tamaño determinado, en la mayoría de
los casos basta con 256 o 512 bytes.

Atacaremos este asunto en su momento.

Veremos ahora cómo se puede crear un archivo de tipo COM usando la sintaxis
del MASM, NASM y FASM:

Archivo COM usando sintaxis MASM

Existen varios tipos de estructura de archivo COM, veamos una:

FASM MASM NASM Resultado en pantalla


Código Código Código
[bin] [bin] [bin]
Source 01-02 - EsqCom?1.asm
 Versión MASM

En las primeras líneas ponemos un comentario explicando ligeramente de qué


trata el programa, quién es su autor y cuál es su actual versión. Obsérvese que
los comentarios en ensamblador comienzan con un punto y coma.
En la línea 9 definimos un segmento de código que llamaremos "codigo".

En la línea 10 le decimos al compilador que asuma que todos los segmentos


posibles del programa tan sólo están en uno que es el que hemos definido antes
"codigo".

En la línea 11 provocamos que el compilador salte los primeros 100h bytes para
dejar espacio al PSP.

Tenemos que señalar el comianzo del programa. Eso lo hacemos en la línea 12


con la etiqueta "Inicio:" (las etiquetas se marcan con dos puntos). Además con
"jmp entrada" indicamos que se salte las siguientes líneas hasta el punto de
memoria indicado por entrada. Esto es así porque las líneas que hay en medio
no son ejecutables, puesto que son datos.

Las siguientes líneas están reservadas para introducir datos.

En la línea 16 definimos el procedimiento entrada con "entrada proc", es el


principal del programa; similar al "main" del lenguaje c. Podríamos llamarle de
cualquier otra manera, no sólo entrada. En la línea 23 indicamos el fin de este
procedimiento con "entrada endp". El nombre ha de ser el mismo que el que
definimos en la línea 16. Dentro de este procedimiento es donde vamos a definir
todo lo que va a hacer el programa, y al final de éste es donde le indicamos al
compilador que limpie toda la memoria de este programa y vuelva al DOS.

En la línea 21 con la instrucción "mov ax, 4c00h" indicamos que "mueva" al


registro AX el valor 4C00h o mejor dicho que el registro AX tome ese valor.
Pero debería leerse como que el byte superior de AX, AH tome el valor 4Ch y
el byte inferior, AL el valor 00. Esto es así porque en AH indicamos la función
que va a realizar la interrupción 21h del DOS, en este caso finalización del
programa, limpieza de memoria y regreso al DOS y en AL señalamos con qué
tipo de error ha finalizado el programa, siendo un cero por defecto que indica
sin ningún error.

En la línea 22 tenemos la instrucción "int 21h", que pide que se ejecute la


interupción 21h del DOS.

En estas dos líneas tenemos la interrupción que devuelve la ejecución al DOS.


La interrupción 21h del DOS tiene muchas funciones, la que aquí utilizamos la
indicamos en el registro AH, es la número 4Ch, que devuelve la ejecución al
DOS. El valor que se mete en AL es opcional y sirve para indicar si el programa
ha tenido alguna malfunción o no, el valor cero indica que todo ha ido bien.

En la línea 24 indicamos el fin del segmento codigo con "codigo ends".


Antes del procedimiento de entrada podríamos definir más procedimientos o
macros, ya veremos cómo.

En la línea 25 le indicamos al compilador que termina el programa desde donde


le dijimos que empezaba en la línea 5 con la etiqueta Inicio. Para ello utilizamos
"end Inicio".

Muy bien, ya sabemos cómo es la estructura de un programa COM. Ahora


vamos a compilarlo. Para ello abrimos una ventana del emulador del MS-DOS,
a no ser que ya trabajes directamente desde el MS-DOS puro y duro. En el
primer caso está en "Inicio->Todos los programas->Accesorios->Símbolo del
sistema" en Windows XP. Ahora debes cambiarte al directorio donde está
nuestro código fuente. Utiliza para ello la orden "CD nombre_camino". En mi
caso utilizaría "CD c:\Trabajo\AOE\Codigos\Cap01". Una buena idea sería
indicarle al símbolo del sistema que se abra siempre en nuestro directorio. Para
ello sitúate con el cursor del ratón donde ya te he dicho que está y pulsa el botón
derecho, se te abrirá una ventana, donde puedes elegir sus propiedades en la
parte inferior.

Fig. 01-09 - Ruta de Acceso al Símbolo del Sistema

En la pestaña de acceso directo en "Iniciar en", le pondrás tu ruta. En mi caso


sería:
Fig. 01-10 - Propiedades del Símbolo del Sistema

De esta forma, cada vez que abras el símbolo del sistema se abrirá en tu carpeta.

A parte de esto, es necesario que tengas en el PATH la ruta de los compiladores


y enlazadores que vas a utilizar.

Vamos a compilar el programa con MASM:

>ml /AT EsqComM1.asm [INTRO]

ml es el nombre del compilador de Microsoft de la versión 6.13, que además se


encarga de llamar al linkador. "/AT" es una variante que le indica al compilador
que produzca un archivo de tipo COM, pero ¡cuidado!, tiene que ir en
mayúsculas. Y "EsqComM1.asm" es el nombre del programa que se va a
compilar, es necesario indicar también la extensión.

O bien, si tienes el ensamblador de Borland Turbo Assembler, lo puedes


compilar así:
>tasm EsqComM1 [INTRO]
>tlink /t EsqComM1 [INTRO]

Con tasm se ensambla a un archivo objeto. Tlink es el linkador de Borland y


"/t" le indica que produzca un archivo COM.

Ahora ya puedes ejecutar el programa

>EsqComM1 [INTRO]

Verás que no produce ningún resultado porque el programa no hace nada salvo
salir al DOS. Pero nos sirve para saber que lo hemos hecho todo bien, pues el
compilador no ha dado ningún mensaje de error.

Esta es la pantalla de lo que me sale a mí:

Fig. 01-11 - Ejemplo de compilación con ML y EsqComM1.asm

 Versión NASM

En la línea 1 tenemos org 100h , que sería equivalente a resb 100h , que ya
sabemos que deja espacio para el PSP.

Una diferencia importante con la sintaxis de Microsoft es que NASM no


permite "asumir" nada, hay que definirlo explícitamente.

Seguidamente definimos varias secciones:

o section .text para el código.


o Section .data para los datos.
o Section .bss para los datos no inicializados.
El resto es bien conocido.

Para ensamblar dicho código hacemos:

>nasmw -fbin EsqComN1.asm -o EsqComN1.com


 Versión FASM

Con use16 le decimos al compilador que genere código de 16 bits.

org 100h reserva espacio para el PSP.

No necesita ningún tipo de etiqueta, a continuación va el código.

Para ensamblar dicho código usaremos uno de estos dos métodos, el más
sencillo es el primero:

o Compilarlo directamente desde el editor que trae el paquete FASM.


o >fasm EsqComF1.asm EsqComF1.com

En el siguiente capítulo haremos el famoso "Hola mundo", donde ya veremos


un programa que hace algo.

Si vemos este programa con un editor hexadecimal, yo uso el FRHED,


observaríamos algo como:

Fig. 01-12 - ESQCOMM1.COM en hexadecimal

El primer bloque de números es la dirección en memoria y el segundo es el


código de intrucciones de lenguaje máquina en hexadecimal de nuestro
programa. Mientras que el de la derecha es éste traducido a código ASCII. Es
difícil, no obstante entender el sentido de estas instrucciones. Una forma más
sencilla de entenderlo es utilizando la utilidad DEBUG que viene en todas las
versiones de MS-DOS y Windows.

Lanzamos una ventana del "Símbolo del sistema" como ya sabemos y ponemos
lo siguiente:

C:\Trabajo\AOE\Codigos\Cap01>debug ESQCOMM1.COM
-u
0D85:0100 EB00 JMP 0102
0D85:0102 B8004C MOV AX,4C00
0D85:0105 CD21 INT 21
0D85:0107 7365 JNB 016E
0D85:0109 61 DB 61
0D85:010A 206361 AND [BP+DI+61],AH
0D85:010D 6D DB 6D
0D85:010E 62 DB 62
0D85:010F 69 DB 69
0D85:0110 61 DB 61
0D85:0111 7220 JB 0133
0D85:0113 61 DB 61
0D85:0114 6C DB 6C
0D85:0115 206469 AND [SI+69],AH
0D85:0118 7265 JB 017F
0D85:011A 63 DB 63
0D85:011B 746F JZ 018C
0D85:011D 7269 JB 0188
0D85:011F 6F DB 6F
-q

C:\Trabajo\AOE\Codigos\Cap01>

Se puede observar en el bloque hexadecimal del código desensamblado cómo


coincide con lo que nos indicaba el editor hexadecimal.

La utilidad DEBUG viene explicada en el apéndice C. Podemos decir que el


comando "u" es para desensamblar el código del programa
ESQCOMM1.COM. De todas estas líneas las que nos interesan son las tres
primeras, las siguientes son desensamblado de las siguientes posiciones de
memoria al programa. Podemos observar que el desensamblado ha sido
excelente.

El código máquina en hexadecimal EB00 equivale a JMP 0102, que significa


saltar a la posición de memoria 0102, que es la línea siguiente. B8004C equivale
a MOV AX, 4C00 y CD21 a INT 21.

Aquí está todo nuestro programa. Lo único que hace es devolver la ejecución al
sistema operativo.

Vamos a dar a continuación los otros dos esqueletos posibles para construir un
archivo COM con MASM:
EsqCmM1b EsqComM2 EsqComM3 Resultado en pantalla
Código Código Código
[bin] [bin] [bin]
Source 01-03 - EsqCmM??.asm

 EsqCmM1b.asm

Ya hemos dicho que los archivos COM no usan diferentes segmentos puesto
que todo está dentro del mismo y que tampoco se define una pila puesto que es
el propio DOS el que se encarga de definirsela al final del programa. Sin
embargo vemos en esta estructura que se definen diferentes segmentos y
además una pila. Pero todo esto es virtual porque al final todo ello se engloba
dentro del mismo segmento llamado grupo mediante la directiva de la línea 4:

grupo group codigo, datos, pila

Le indicamos al ensamblador que agrupe todos estos segmentos en uno solo:


grupo.

Y luego, en la siguiente línea le decimos que asuma que todos los registros de
segmento apunten a nuestro segmento grupo.

También vemos como diferencia que el título del programa lo especificamos


con la etiqueta "TITLE", esto es exactamente igual a utilizar punto y coma, pero
es más vistoso y legible.

 EsqComM2.asm

Esta estructura es semejante a la primera pero con unas directivas simplificadas


llamadas "de segmento".

.model tiny indica al compilador que el modelo de datos que vamos a usar va
a ser el más pequeño de todos, apropiado para archivos COM.

.code indica que aquí empieza el segmento de código y termina donde empieza
el siguiente.

.STARTUP es una directiva que sirve para inicializar los segmentos.

.EXIT es equivalente a hacer un mov ax, 4C00h; int 21h.

El resto de las sentencias ya las hemos estudiado. Estas estructuras no son


rígidas, se pueden usar una mezcla de ellas. Por ejemplo:

 EsqComM3.asm
Podemos ver la mezcla de sintaxis clásica con simplificada.

1.5.3. Programas EXE


Los programas EXE, al igual que los COM no se cargan en una dirección de
memoria fija, sino en alguna divisible por 16 y, al contrario que los COM, no
tienen la limitación de 64 Kb para el código y poseen segmentos separados para
código, datos y pila que se pueden alinear en orden cualquiera. Dado que los
archivos EXE pueden contener varios segmentos, el empleo de instrucciones
FAR es inevitable si se quiere llamar a un subprograma en el segmento de
código X desde el segmento de código Y. Esto nos crea un problema puesto que
a parte de la dirección del subprograma también necesita la dirección de
segmento (esto no es sencillo puesto que no se puede prever esta dirección de
segmento durante el desarrollo del programa y en cada ejecución del programa
resulta diferente). En los programas EXE este problema se resuelve de la
siguiente manera: el linkador coloca al inicio de cada archivo EXE una
estructura de datos que contiene las direcciones de todas las referencias de
segmento, es decir, la dirección de todos aquellos comandos FAR, en los que
se hace referencia a la dirección de segmento de uno de los diferentes segmentos
de código a datos.

El código máquina de un SALTO LEJANO se compone de cinco bytes:

 El primero contiene el código del comando.


 La siguiente palabra (dos bytes) la dirección de segmento.
 La siguiente palabra (dos bytes) la dirección de segmento a la que ha de saltarse.

No se indica la dirección del código de nuestro comando, sino la dirección del


segmento siguiente.

Evidentemente esto no se hace en un archivo COM puesto que como todo está
dentro de un rango de 64K (el mismo segmento), no es necesario.

Veamos la estructura de una cabecera de un archivo EXE:

Dirección Contenido Tipo


Designación de un programa EXE consistente en las letras ASCII "MZ" de Mark
00h-01h 1 Word
Zbikowski, uno de los mejores programadores para DOS de Microsoft.
02h-03h Resto de la división de la longitud del archivo por 512 1 Word
04h-05h Cociente de la división de la longitud del archivo por 512 1 Word
06h-07h Número de direcciones de segmento a adaptar 1 Word
08h-09h Tamaño de la cabecera en párrafos 1 Word
0Ah-0Bh Número mínimo de párrafos necesitados adicionalmente 1 Word
0Ch-0Dh Número máximo de párrafos necesitados adicionalmente 1 Word
0Eh-0Fh Desplazamiento en párrafos del segmento de pila dentro del módulo cargado. 1 Word
10h-11h Contenido del registro SP durante el inicio del programa. 1 Word
12h-13h Suma de control de la cabecera del archivo EXE 1 Word
14h-15h Contenido del registro IP durante el inicio del programa 1 Word
16h-17h Inicio del segmento de código en el archivo EXE 1 Word
18h-19h Dirección de la Relocation-Table en el archivo EXE 1 Word
1Ah-1Bh Número de overlay 1 Word
1Ch- Memoria de buffer Variable
+?? Direcciones de las direcciones de segmento a adaptar Variable
+?? Segmentos de código de programa, datos y pila Variable
Tabla 01-39 - Estructura de la Cabecera de un Archivo EXE

En un archivo EXE, el PSP se coloca al inicio, al igual que en un COM, y los


registros de segmento DS y ES apuntan al inicio de éste. La dirección de la pila
y el contenido del puntero de la pila se guardan en la cabecera del archivo EXE.

La función EXEC que carga los programas en memoria intenta reservar el


número máximo de párrafos. Si esto no es posible, se conforma con el resto de
la memoria, que en ningún caso puede ser inferior al número mínimo de
párrafos necesitados adicionalmente, que viene en la estructura de cabecera del
archivo EXE. Por tanto, si queremos ejecutar otro programa, tenemos el mismo
problema que con los archivos COM, debiendo liberar la memoria reservada
adicionalmente. Más adelante será cuando veamos cómo se hace esto.

Veremos ahora cómo se puede crear un archivo de tipo EXE con MASM,
NASM y FASM.

EsqExeM1 EsqExeM2 EsqExeM3 EsqExeN1 EsqExeF1 Resultado en pantalla


Código Código Código Código Código
[bin] [bin] [bin] [bin] [bin]
Source 01-04 - EsqExe??.asm
 EsqExeM1.asm

En las primeras líneas tenemos una explicación somera de lo que trata el


programa, del autor y de la versión.

En la línea 9 definimos el segmento de pila. Es necesario poner 'stack' para


compilarlo con el TASM. Definimos una pila de 30h palabras de longitud. Esto
se hace con la instrucción "dw 30h dup (?)". dw indica que estamos reservando
espacio para palabras y 30h dup (?) indica que repetimos 30h veces un valor
indeterminado. La interrogacion indica indeterminado, dup indica repetir y 30h
el número de veces que se repite.

En la línea 13 definimos el segmento de datos

En la línea 17 definimos el segmento de código.


En la línea 18 hacemos corresponder a cada registro de segmento con su valor
apropiado.

En la línea 21 definimos el procedimiento principal del programa,


seguidamente vendrán las instrucciones de éste y luego la salida al DOS. Tanto
después como antes de este procedimiento podemos definir otros
procedimientos y macros.

En las líneas 29 y 30 terminamos con el segmento de código y con el programa,


respectivamente.

Para compilar este archivo utilizando MASM:

> ml EsqExeM1.asm

Para compilarlo utilizando TASM:

> tasm EsqExeM1


> tlink EsqExeM1
 EsqExeM2.asm

Es la versión con directivas de segmento

 EsqExeM3.asm

Mezclamos directivas de segmento con clásicas.

En esta ocasión hemos utilizado la directiva "Comment" para comentar varias


líneas de código. La sintaxis es Comment y un carácter ASCII tras un espacio
en blanco, es conveniente que este carácter sea lo más raro posible para que las
líneas que queramos comentar no lo incluyan. Las líneas comentadas terminan
donde vuelve a aparecer este carácter; es un buen hábito de programación poner
EndComment antes de él porque es más visible e indica muy claramente cuál
es el cometido del segundo carácter, si bien en sí mismo no produce ningún
efecto y además está comentado también. Lo demás está suficientemente claro.

 EsqExeN1.asm

La única diferencia existente aquí con los otros esqueletos vistos es que se
indica dónde empiezan los segmentos pero no donde terminan, pues esto ocurre
donde empieza el siguiente segmento.

En la línea 11 tenemos resb 256 que equivale al código MASM "db 256 dup
(?)".
Después de definir el segmento de pila definimos una etiqueta "InicioPila:" que
está situada en la cima de la pila y nos sirve para indicar donde se va a situar el
registro SP.

Finalmente, la directiva "..start:" le indica al compilador NASM que es aquí


donde empieza el programa y no hace falta indicarle dónde termina puesto que
es al final del segmento de código.

Para compilar el programa haremos lo siguiente:

>nasmw -fobj EsqExeN1.asm


>alink -oEXE EsqExeN1.obj

Alink es un linkador gratuito y necesitamos que esté también dentro del PATH.

 EsqExeF1.asm

Empezamos con FORMAT MZ, que indica al compilador que va a generar un


.EXE.

Para ensamblar dicho código usaremos uno de estos dos métodos, el más
sencillo es el primero:

o Compilarlo directamente desde el editor que trae el paquete FASM.


o >fasm EsqExeF1.asm EsqExeF1.exe

Vamos a desgranar la estructura de uno de estos esqueletos EXE, para lo cuál


vamos a utilizar el compilador NASM, por ejemplo, y cambiaremos
ligeramente nuestra definición de la pila, para que luego podamos ver más
claramente dónde está situada. Para ello hemos creado el archivo EjPilaN1.exe.
Lo compilamos como ya sabemos hacer y vemos la estructura hexadecimal del
fichero [bin] resultante.

Fig. 01-14 - Visualización de la Pila de un fichero EXE en memoria


Observamos que los primeros bytes del archivo son "4d" y "5a" hexadecimales,
que se corresponden con las letras MZ, con lo que tenemos identificado el
archivo como tipo EXE. En el tercer byte tenemos 91h hexadecimal = 145 bytes
de la longitud del programa EsqExN1b.exe, pues según hemos dicho:

145 = 0*512 + 145

También podemos ver claramente las etiquetas con las que definimos la pila y
que ahora nos muestran ésta muy visualmente.

Veamos cómo queda en modo de ejecución con la herramienta debug:

C:\Trabajo\AOE\Codigos\Cap01>debug EjPilaN1.exe
-d 100
0D18:0100 3C 2D 20 41 71 75 69 20-65 6D 70 69 65 7A 61 20 <-
Aqui empieza
0D18:0110 6C 61 20 70 69 6C 61 20-79 20 20 61 71 75 69 20 la
pila y aqui
0D18:0120 74 65 72 6D 69 6E 61 20-00 00 00 00 00 00 00 00
termina ........
0D18:0130 B8 28 0D 8E D0 BC 2A 00-B8 28 0D 8E D8 B4 4C CD
.(....*..(....L.
0D18:0140 21 C4 12 F3 A4 1F 03 E2-5F 5E 5A 59 5B 89 26 C2
!......._^ZY[.&.
0D18:0150 12 2E 8E 16 40 05 8B E0-A1 04 15 C3 59 8B 0E EE
....@.......Y...
0D18:0160 14 8B D1 C1 E1 04 C1 EA-0C E8 61 1F 73 20 A3 D4
..........a.s ..
0D18:0170 14 C1 E9 04 8A C2 C0 E0-04 0A E8 C1 EA 04 74 03
..............t.
-q

Vamos a utilizar aquí el editor hexadecimal gratuíto "HexIT" que posee una
opción que nos va a ofrecer una información muy valiosa respecto a la cabecera
de nuestro archivo "EXE":
Fig. 01-15 - Cabecera de Archivo EXE

Como vemos en el recuadro de fondo azul, tras pulsar la tecla "F6", nos da
información de todo el contenido de la cabecera del programa EXE.

Vamos a ver la estructura del PSP que le ha correspondido a este programa.


Para ello utilizamos la utilidad CodeView de microsoft:

>cv EjPilaN1.exe

Fig. 01-16 - PSP de un archivo EXE


Los primeros 100h bytes son los correspondientes al PSP. Estamos en el
segmento 0BAAh, que corresponde con el valor del registro ES y DS, que ya
hemos dicho que contienen al inicio de un programa EXE la dirección de su
PSP.

En "Run-> Set Runtime Arguments..." del CodeView podemos establecer cuál


va a ser el agumento de línea de comandos para nuestro programa, yo he elegido
"miArgumento"; no es original, pero es muy visible.

Los dos primeros bytes contienen una llamada a la interrupción 20h: "CD 20",
que produce un retorno al DOS.

En el byte 80h nos indica el número de bytes que hemos ingresado por línea de
comandos sin contar el retorno de carro: "0C", es decir, 12 en decimal.

1.6. Diferencias entre las sintaxis


1.6.1. Diferencias entre NASM y MASM
Veamos algunas diferencias:

 NASM es sensible a las letras en mayúsculas y minúsculas, mientras que


MASM sólo lo es usando la opción /Cp. Esto quiere decir que las variables
miVar y MiVAR no son la misma en NASM, pero sí en MASM sin la opción
correspondiente.
 Sección sin inicializar. Nasm permite generar una sección de variables sin
inicializar con la etiqueta .bss, eso significa que reserva ese espacio de memoria,
pero no lo ocupa, con lo que el [bin] resultante es de tamaño inferior. Las
variables sin inicializar en MASM sí ocupan espacio. Mientras MASM usa
"stack db 64 dup (?)", NASM usa "stack resb 64", como intentando que se lea
"reserva 64 bytes de memoria".
 NASM no usa etiquetas especiales para definir procedimientos, sino que
quedan identificados por una etiqueta a la que se salta.
 Las etiquetas definidas por NASM dentro de un procedimiento nunca son
locales a no ser que se indique explícitamente anteponiéndoles un punto,
mientras que MASM siempre las define como locales dentro de un
procedimiento a no ser que indiquemos explícitamente que no lo son. Al no ser
locales en cada procedimiento implica que no podremos usar etiquetas con el
mismo nombre en diferentes procedimientos
 NASM no "ASSUME", por simplicidad, nunca guarda los valores de los
registros de segmento y nunca generará un prefijo dominante de segmento.
 NASM no soporta Modelos de Memoria. Tampoco tiene directivas para
respaldar los diferentes modelos de memoria. El programador debe encargarse
de invocar a una función con una llamada cercana o una lejana y también volver
de ella con un RETN (cercano) o un RETF (lejano), admitiendo RET como
RETN.
 Diferencias con la coma flotante. NASM usa diferentes nombres de registro del
coprocesador que el que usa MASM. NASM usa st0, st1,... frente a st(0),
st(1),... del MASM.
 NASM requite corchetes para todas las referencias de memoria. Por ejemplo el
siguiente código:
 uno equ 1
 dos dw 2

 mov ax, uno ; AX = 1 tanto en NASM como en MASM
 mov ax, dos ; AX = 2 en MASM
 ; AX = la dirección de la variable "dos" en
NASM
 mov ax, OFFSET dos ; AX = la dirección de la variable "dos"
sólo admitido en MASM
 MOV AX, [dos] ; AX = 2 tanto en NASM como en MASM
 En consecuencia, la función OFFSET no existe en NASM, sólo en MASM,
devuelve la posición de memoria de una etiqueta. Es decir, si "dos" es una
etiqueta, las siguientes dos líneas son equivalentes en MASM y NASM:
 mov ax, dos ; AX = la dirección de la variable "dos" en
NASM
 mov ax, OFFSET dos ; AX = la dirección de la variable "dos" en
MASM
 El parámetro PTR sólo existe en MASM, por ejemplo, las siguientes sentencias
son equivalentes en MASM y NASM:
 dos dw 2
 MOV AX, WORD PTR [dos] ; AX = 2 en MASM
 MOV AX, WORD [dos] ; AX = 2 en NASM
 Los segmentos de memoria deben estar dentro del corchete, por ejemplo:
 MOV AX, ES:[BX] ; Correcto en MASM
 MOV AX, [ES:BX] ; Correcto en NASM
 Nasm tiene su propio lenguaje de macros
1.6.1. Diferencias entre NASM y FASM
Estos dos compiladores son, básicamente, muy parecidos, por lo que la mayoría
de las diferencias entre NASM y MASM también aplican a FASM.

Veamos algunas diferencias entre FASM y NASM

 Nasm reserva espacio con la directiva RESB, Fasm lo hace con RB o también
con ?, por ejemplo:
 Var1 DB ?
Var2 RB 1
 En el coprocesador matemático, NASM define el primer registro con ST0,
mientras que FASM lo hace con ST
 Fasm no define espacios especiales con etiquetas como ".bss". En el caso de
variables sin inicializar lo hace como ya hemos indicado anteriormente.
 Fasm tiene su propio lenguaje de macros