Libro ASM
Libro ASM
Libro ASM
Paul A. Carter
9 de agosto de 2007
Copyright
c 2001, 2002, 2003, 2004 by Paul Carter
Observe que esta restricción no está prevista para prohibir el cobro por el
servicio de impresión o copia del documento
A los docentes se les recomienda usar este documento como recurso de clase;
sin embargo el autor apreciarı́a ser notificado en este caso.
Prefacio
Propósito
El propósito de este libro es dar al lector un mejor entendimiento de
cómo trabajan realmente los computadores a un nivel más bajo que los len-
guajes de alto nivel como Pascal. Teniendo un conocimiento profundo de
cómo trabajan los computadores, el lector puede ser más productivo desa-
rrollando software en lenguajes de alto nivel tales como C y C++. Aprender
a programar en lenguaje ensamblador es una manera excelente de lograr este
objetivo. Otros libros de lenguaje ensamblador aún enseñan a programar el
procesador 8086 que usó el PC original en 1981. El procesador 8086 sólo
soporta el modo real. En este modo, cualquier programa puede acceder a
cualquier dirección de memoria o dispositivo en el computador. Este modo
no es apropiado para un sistema operativo multitarea seguro. Este libro, en
su lugar discute cómo programar los procesadores 80386 y posteriores en
modo protegido (el modo en que corren Windows y Linux). Este modo so-
porta las caracterı́sticas que los sistemas operativos modernos esperan, como
memoria virtual y protección de memoria. Hay varias razones para usar el
modo protegido
i
ii PREFACIO
Reconocimientos
El autor quiere agradecer a los muchos programadores alrededor del mun-
do que han contribuido al movimiento de Software Libre. Todos los progra-
me y aún este libro en sı́ mismo fueron producidos usando software libre.
El autor desearı́a agradecerle especialmente a John S. Fine, Simon Tatham,
Julian Hall y otros por desarrollar el ensamblador NASM ya que todos los
ejemplos de este libro están basados en él; a DJ Delorie por desarrollar el
compilador usado de C/C++ DJGPP; la numerosa gente que ha contribuido
al compilador GNU gcc en el cual está basado DJGPP; a Donald Knuth y
otros por desarrollar los lenguajes de composición de textos TEX y LATEX 2ε
que fueron usados para producir este libro; a Richar Stallman (fundador de
la Free Software Fundation), Linus Torvalds (creador del núcleo de Linux) y
a otros que han desarrollado el software que el autor ha usado para producir
este trabajo.
Gracias a las siguientes personas por correcciones:
John S. Fine
Sam Hopkins
Nick D’Imperio
Jeremiah Lawrence
Ed Beroset
Jerry Gembarowski
Ziqiang Peng
Eno Compton
Josh I Cates
Mik Mifflin
Luke Wallis
iii
Gaku Ueda
Brian Heward
Chad Gorshing
F. Gotti
Bob Wilkinson
Markus Koegel
Louis Taber
Dave Kiddell
Eduardo Horowitz
Sébastien Le Ray
Nehal Mistry
Jianyue Wang
Jeremias Kleer
Marc Janicki
Recursos en Internet
Comentarios
El autor agradece cualquier comentario sobre este trabajo.
E-mail: [email protected]
WWW: http://www.drpaulcarter.com/pcasm
iv PREFACIO
Capı́tulo 1
Introducción
1.1.1. Decimal
Los números con base 10 están compuestos de 10 posibles dı́gitos (0-9).
Cada dı́gito de un número tiene una potencia de 10 asociada con él, basada
en su posición en el número. Por ejemplo:
234 = 2 × 102 + 3 × 101 + 4 × 100
1.1.2. Binario
Los números en base dos están compuestos de dos posibles dı́gitos (0 y
1). Cada dı́gito de un número tiene una potencia de 2 asociada con él basada
en su posición en el número. Por ejemplo:
110012 = 1 × 24 + 1 × 23 + 0 × 22 + 0 × 21 + 1 × 20
= 16 + 8 + 1
= 25
Esto muestra cómo los números binarios se pueden convertir a decimal.
El Cuadro 1.1 muestra cómo se representan los primeros números en binario.
1
2 CAPÍTULO 1. INTRODUCCIÓN
110112
+100012
1011002
Si uno considera la siguiente división decimal:
1234 ÷ 10 = 123 r 4
podemos ver que esta división suprime el dı́gito del extremo derecho del
número y desplaza los otros dı́gitos una posición a la derecha. Dividiendo
por dos hacemos una operación similar, pero para los dı́gitos binarios de un
número. Consideremos la siguiente división binaria1 :
Decimal Binario
25 ÷ 2 = 12 r 1 11001 ÷ 10 = 1100 r 1
12 ÷ 2 = 6 r 0 1100 ÷ 10 = 110 r 0
6÷2=3r 0 110 ÷ 10 = 11 r 0
3÷2=1r 1 11 ÷ 10 = 1 r 1
1÷2=0r 1 1 ÷ 10 = 0 r 1
1.1.3. Hexadecimal
Los número hexadecimales tienen base 16. Los hexadecimales (o hex ) se
pueden usar como una representación resumida de los números binarios. Los
números hexadecimales tienen 16 dı́gitos posibles. Esto crea un problema
ya que no hay sı́mbolos para estos dı́gitos adicionales después del nueve.
Por convención se usan letras para estos dı́gitos adicionales. Los 16 dı́gitos
hexadecimales son: 0-9 y luego A, B, C, D, E, F. El dı́gito A equivale a 10
en decimal, B es 11 etc. Cada dı́gito de un número hexadecimal tiene una
potencia de 16 asociada con él. Por ejemplo:
Para convertir de decimal a hex use la misma idea que la empleada para la
conversión binaria excepto que se divide por 16. Vea la Figura 1.3 para un
ejemplo.
La razón por la cual los hexadecimales son útiles es que hay una manera
fácil para convertir entre hex y binario. Los número binarios se tornan lar-
gos y molestos rápidamente. La representación hexadecimal es una manera
mucho más compacta de representar los números binarios.
Para convertir un número hexadecimal a binario simplemente convierta
cada dı́gito hexadecimal a un número binario de 4 bits. Por ejemplo, 24D16
es convertido en 0010 0100 11012 . Observe que ¡los ceros delanteros son im-
portantes! Si los ceros del dı́gito de la mitad de 24D16 no se usan el resultado
es erróneo. Convertir de binario a hex es igual de fácil; uno hace el proceso
4 CAPÍTULO 1. INTRODUCCIÓN
589 ÷ 16 = 36 r 13
36 ÷ 16 = 2 r 4
2 ÷ 16 = 0 r 2
Figura 1.3:
1.2.2. La CPU
La Unidad Central de Procesamiento (CPU) es el dispositivo fı́sico que
ejecuta las instrucciones. Las instrucciones que ejecuta la CPU son por lo
general muy simples. Las instrucciones pueden requerir datos que estén en un
lugar especial de almacenamiento de la CPU en sı́ misma llamados registros.
La CPU puede acceder a los datos en los registros mucho más rápido que
en la memoria. Sin embargo el número de registros en la CPU es limitado,
ası́ el programador debe tener cuidado de dejar en los registros sólo los datos
que esté usando.
Las instrucciones que un tipo de CPU ejecuta las hace en lenguaje de
máquina. Los programas en lenguaje de máquina tienen una estructura mu-
cho más básica que los lenguajes de alto nivel. Las instrucciones en lenguaje
de máquina son codificadas como números, no en formatos de texto ami-
gables. Una CPU debe estar en capacidad de decodificar una instrucción
muy rápidamente para ejecutarse eficientemente. EL lenguaje de máquina
es diseñado con este objetivo en mente, no para ser fácilmente descifrados
3
N del T: En la traducción se usarán los nombres de las unidades de memoria en
español, aunque en la literatura técnica seguramente los encontrarán en inglés
4
De hecho ASCII sólo usa los 7 bits más bajos y sólo tiene 128 valores diferentes
6 CAPÍTULO 1. INTRODUCCIÓN
por humanos. Los programas escritos en otros lenguajes deben ser conver-
tidos en lenguaje de máquina nativo de la CPU para que se ejecute en el
computador. Un compilador es un programa que traduce programas escritos
en un lenguaje de programación al lenguaje de máquina de una arquitectu-
ra en particular de un computador. En general cada tipo de CPU tiene su
propio y único lenguaje de máquina. Esa es una de las razones por las cuales
programas escritos para un Mac no corren en un PC tipo IBM
Los computadores usan un reloj para sincronizar la ejecución de las ins-
GHz significa Gigahertz o trucciones. El reloj pulsa a una frecuencia fija conocida como velocidad del
mil millones de ciclos por reloj. Cuando Ud. compra un computador de 1.5 GHz, la frecuencia de su
segundo. Una CPU de 1.5 reloj es 1.5 GHz. Actualmente, los pulsos del reloj son usados por muchos
GHz tiene mil quinientos componentes de un computador. Con frecuencia, los otros componentes usan
millones de pulsos de reloj
unas velocidades de reloj diferentes que la CPU. El reloj no marca los minu-
por segundo.
tos y los segundos, simplemente toca a una razón constante. La electrónica
de la CPU usa los toques para realizar sus operaciones correctamente, como
los toques de un metrónomo para la interpretación de música al ritmo correc-
to. El número de toques (o como a ellos se les llama comúnmente ciclos) que
una instrucción requiere depende del modelo de la CPU, de la instrucción
anterior y de otros factores.
80286: Esta CPU se usa en los PC tipo AT. Agrega unas instrucciones
nuevas al lenguaje de máquina base del 8080/86. Sin embargo la ca-
racterı́stica principal nueva es el modo protegido de 16 bits. En este
modo puede acceder hasta 16 Mega bytes de memoria y proteger a
los programas del acceso de otros. Sin embargo los programas todavı́a
están divididos en segmentos que no pueden ser más grandes de 64K.
1.2. ORGANIZACIÓN DEL COMPUTADOR 7
AX
AH AL
80386: Esta CPU es una gran ampliación del 80286. Primero extiende mu-
chos de los registros para almacenar 32 bits (EAX, EBX, ECX, EDX,
ESI, EDI, EBP, ESP, EIP) y añade dos nuevos registros de 16 bits FS y
GS. También añade un nuevo modo protegido de 32 bits. En este modo
pueden acceder hasta 4 Gigabyes. Los programas otra vez están divi-
didos en segmentos, pero ahora cada segmento también puede tener
hasta un tamaño de 4 Gigabytes.
80486/Pentium/Pentium Pro: Estos miembros de la familia 80x86 añaden
muy pocas caracterı́sticas nuevas. Ellos principalmente aceleran la eje-
cución de las instrucciones.
Pentium MMX: Este procesador añade instrucciones MMX (eXtensiones
MultiMedia) al Pentium. Estas instrucciones pueden acelerar instruc-
ciones comunes gráficas.
Pentium II: Este es el procesador Pentium Pro con las instrucciones MMX
añadidas (El pentium III es esencialmente sólo un Pentium II rápido.
16 ∗ seleccionador + desplazamiento
1.2.9. Interrupciones
Algunas veces el flujo ordinario de un programa debe ser interrumpido
para procesar eventos que requieren una respuesta rápida. El hardware de
un computador provee un mecanismo llamado interrupción para manipu-
lar estos eventos. Por ejemplo cuando se mueve el ratón la interrupción de
hardware del ratón es el programa actual para manejar el movimiento del
ratón (para mover el cursor del mouse, etc) Las interrupciones hacen que
el control se pase a un manipulador de interrupciones. Los manipuladores
de interrupciones son rutinas que procesan la interrupción. A cada tipo de
interrupción se le asigna un número entero. En el comienzo de la memoria
fı́sica una tabla de vectores de interrupción que contiene la dirección del seg-
mento de los manipuladores de la interrupción. El número de la interrupción
es escencialmente un ı́ndice en esta tabla.
Las interrupciones externas son levantadas desde el exterior de la CPU
(el ratón es un ejemplo de este tipo de interrupción). Muchos dispositivos de
E/S levantan interrupciones (teclado, temporizador, disco duro CD ROM y
tarjetas de sonido) Las interrupciones internas son levantadas desde la CPU,
por una instrucción de error o por una instrucción de interrupción. Las ins-
trucciones de error también se llaman trampas. Las interrupciones generadas
desde la instrucción de interrupción son llamadas interrupciones de sofware.
DOS usa estas interrupciones para implementar su API (Interfaz de progra-
mas de Aplicación). Sistemas operativos más modernos (como Windows y
Linux) usan una interfaz basada en C 5
Muchos manipuladores de interrupción devuelven el control al programa
interrumpido cuando ella culmina. Ella restaura todos los registros con los
mismos valores que tenı́an antes que ocurriera la interrupción. Ası́ el pro-
grama interrumpido se ejecuta como si nada hubiese pasado (excepto que se
perdieron algunos ciclos de CPU) Las trampas generalmente no retornan. A
menudo ellas acaban el programa.
sub bx, 10 ; bx = bx - 10
sub ebx, edi ; ebx = ebx - edi
Las instrucciones INC y DEC incrementan o decrementan valores en uno.
Ya que el uno es un operando implı́cito, el código de de máquina para INC
y el DEC es más pequeño que los de las instrucciones ADD y SUB.
1.3.5. Directivas
Una directiva es un artificio del ensamblador no de la CPU. Ellas se usan
generalmente para decirle al ensamblador que haga alguna cosa o informarle
al ensamblador de algo. Ellas no se traducen en código de máquina. Los usos
comunes de las directivas son:
• Definir constantes
• Definir memoria para almacenar datos en ella
• Definir la memoria para almacenar datos en ella
• Agrupar la memoria en segmentos
• Incluı́r código fuente condicionalmente
• Incluı́r otros archivos
El código de NASM pasa a través de un preprocesador tal como C.
Tiene muchas de las órdenes del preprocesador tal como C. Sin embargo las
directivas del preprocesador de NASM comienzan con un como en C.
directiva equ
La directiva equ se puede usar para definir un sı́mbolo. Los sı́mbolos son
constantes con nombre que se pueden emplear en el programa ensamblador.
El formato es:
sı́mbolo equ valor
Los valores de los sı́mbolos no se pueden redefinir posteriormente.
La directiva %define
Esta directiva es parecida a la #define de C. Se usa normalmente para
definir macros tal como en C.
%define SIZE 100
mov eax, SIZE
1.3. LENGUAJE ENSAMBLADOR 15
Unidad Letra
byte B
palabra W
palabra doble D
palabra cuádruple Q
diez bytes T
El código de arriba define un macro llamado size y muestra su uso en una ins-
trucción MOV. Los macros son más flexibles que los sı́mbolos de dos mane-
ras. Los macros se pueden redefinir y pueden ser más que simples constantes
númericas.
Directivas de datos
Las directivas de datos son usadas en segmentos de datos para definir
espacios de memoria. Hay dos formas en que la memoria puede ser reservada.
La primera es solo definir el espacio para los datos; la segunda manera define
el espacio y el valor inicial. El primer método usa una de las directivas RESX.
La X se reemplaza con una letra que determina el tamaño del objeto (u
objetos) que será almacenados. El Cuadro 1.3 muestra los valores posibles.
El segundo método (que define un valor inicial también) usa una de las
directivas DX. Las X son las mismas que las de la directiva RESX.
Es muy común marcar lugares de memoria con etiquetas. Las etiquetas
le permiten a uno referirse fácilmente a lugares de la memoria en el código.
Abajo hay varios ejemplos.
L9 db 0, 1, 2, 3 ; define 4 bytes
16 CAPÍTULO 1. INTRODUCCIÓN
L10 db "w", "o", "r", ’d’, 0 ; define una cadena tipo C = "word"
L11 db ’word’, 0 ; igual que L10
La directiva DD se puede usar para definir o enteros o constantes de punto
flotante de presición simple.6 Sin embargo DQ solo se puede usar para definir
constantes de punto flotante de doble precisión.
Para secuencias largas la directiva TIMES de NASM es a menudo útil.
Esta directiva repite su operando un número especificado de veces por ejem-
plo:
L12 times 100 db 0 ; equivalente a 100 veces db 0
L13 resw 100 ; reserva lugar para 100 palabras
Recuerde que las etiqueta pueden ser usadas para referirse a datos en el
código. Si se usa una etiqueta ésta es interpretada como la dirección (o
desplazamiento) del dato. Si la etiqueta es colocada dentro de paréntesis
cuadrados ([]), se interpreta como el dato en la dirección. En otras palabras,
uno podrı́a pensar de una etiqueta como un apuntador al dato y los parénte-
sis cuadrados como la des referencia al apuntador tal como el asterisco lo
hace en C (MASM y TASM siguen una convención diferente). En el modo
de 32 bits las direcciones son de 32 bits. A continuación, algunos ejemplos.
1 mov al, [L1] ; copia el byte que está en L1 en AL
2 mov eax, L1 ; EAX = dirección del byte en L1
3 mov [L1], ah ; copia AH en el byte en L1
4 mov eax, [L6] ; copia la palabra doble en L6 en EAX
5 add eax, [L6] ; EAX = EAX + la palabra doble en L6
6 add [L6], eax ; la palabra doble en L6 += EAX
7 mov al, [L6] ; copia el primer byte de la palabra doble en L6 en AL
La lı́nea 7 de los ejemplos muestra una propiedad importante de NASM.
El ensamblador no recuerda el tipo de datos al cual se refiere la etiqueta.
De tal forma que el programador debe estar seguro que usa la etiqueta
correctamente. Luego será común almacenar direcciones de datos en registros
y usar los registros como una variable apuntador en C. Una vez más no se
verifica que el apuntador se use correctamente. De este modo el ensamblador
es mucho más propenso a errores aún que C.
Considere la siguiente instrucción:
mov [L6], 1 ; almacena 1 en L6
Esta instrucción produce un error de tamaño no especificado. ¿Por qué? Por-
que el ensamblador no sabe si almacenar el 1 como byte, palabra o palabra
doble. Para definir esto, se añade un especificador de tamaño .
6
Punto flotante de presición simple es equivalente a la variable float en C.
1.3. LENGUAJE ENSAMBLADOR 17
%include "asm_io.inc"
Para usar una de las rutinas print, uno carga EAX con el valor correcto
y usa la instrucción CALL para invocarla. La instrucción CALL es equivalente
a un llamado de función en un lenguaje de alto nivel. Hace un salto en la
ejecución hacia otra sección de código pero después retorna al origen luego
que la rutina a culminado. El programa muestra varios ejemplos de llamadas
de estas rutinas de E/S.
7
TWORD define un área de memoria de 10 bytes. El coprocesador de punto flotante
usa este tipo de dato.
8
El archivo asm io.inc (y el archivo objeto asm io.o archivo que asi io.inc ne-
cesita) están en los ejemplos que se encuentan en la página web del autor: http:
//www.drpaulcarter.com/pcasm
18 CAPÍTULO 1. INTRODUCCIÓN
1.3.7. Depuración
La biblioteca del autor también contiene algunas rutinas útiles para de-
purar los programas. Estas rutinas de depuración muestran información so-
bre el estado del computador sin modificar su estado. Estas rutinas son en
realidad macros que muestran el estado de la CPU y luego hacen un llamado
a una subrutina. Los macros están definidos en el archivo asm io.inc discu-
tido antes. Los matros se usan como instrucciones normales. Los operandos
de los macros se separan con comas.
Hay cuatro rutinas de depuración llamadas dump regs, dump mem, dump stack
and dump math; Ellas muestran los valores de los registros, memoria, pila y
el coprocesador matemático respctivamente
dump regs Este macro imprime los valores de los registros (en hexadeci-
mal) del computador stdout (la pantalla). También imprime el estado
de los bits del registto FLAGS9 . Por ejemplo si la bandera cero es 1
se muestra ZF. Si es cero no se muestra nada. Torma un solo entero
como parámetro que luego se imprime. Este entero se puede usar para
distinguir la salida de diferentes órdenes dump regs.
dump mem Este macro imprime los valores de una región de memoria (en
hexadecimal) y también como caracteres ASCII. Toma tres argumen-
tos delimitados por comas. El primero es un entero que es usado para
identificar la salida (tal cual como el argumento de dump regs). El
segundo argumento es la dirección a mostrar (ésta puede ser una eti-
queta). El último argumento es un número de párrafos de l6 bytes para
mostrar luego de la direccción. La memoria mostrada comenzará en el
primer lı́mite de párrafo antes de la dirección solicitada.
9
El Capı́tulo 2 discute este registro
1.4. CREANDO UN PROGRAMA 19
dump stack Este macro imprime los valores de la pila de la CPU (la pi-
la se verá en el Capı́tulo 4). La pila está organizada como palabras
dobles y está rutina las mostrará de esta forma. Toma tres paráme-
tros separados por comas. El primero es un identificador entero (como
dump regs). El segundo es el número de palabras dobles para mostrar
antes de la dirección que tenga almacenada el registro EBP, y el ter-
cer argumento es el número de palabras dobles a imprimir luego de la
dirección de EBP.
dump math Este macro imprime los valores de los registros del coprocesa-
dor matemático. Toma un solo parámetro entero como argumento que
se usa para identificar la salida tal como el argumento de dump regs
lo hace.
Los últimos dos puntos demuestran que aprender ensamblador puede ser
útil aún si uno nunca programa en él posteriormente. De hecho, el autor
raramente programa en ensamblador pero usa las ideas aprendidas de él
todos los dı́as.
20 CAPÍTULO 1. INTRODUCCIÓN
int main()
{
int ret status ;
ret status = asm main();
return ret status ;
}
first.asm
1 ; Archivo: first.asm
2 ; Primer programa en ensamblador. Este programa pide dos
3 ; enteros como entrada e imprime su suma
4
10 %include "asm_io.inc"
11 ;
12 ; Los datos iniciados se colocan en el segmento .data
13 ;
14 segment .data
15 ;
16 ; Estas etiquetas se refieren a las cadenas usadas para la salida
17 ;
18 prompt1 db "Digite un número: ", 0 ; no olvide el fin de cadena
19 prompt2 db "Digite otro número: ", 0
1.4. CREANDO UN PROGRAMA 21
24 ;
25 ; Los datos no iniciados se colocan en el segmento .bss
26 ;
27 segment .bss
28 ;
29 ; Estas etiquetas se~
nalan a palabras dobles usadas para almacenar los datos
30 ; de entrada
31 ;
32 input1 resd 1
33 input2 resd 1
34
35 ;
36 ; El código se coloca en el segmento .text
37 ;
38 segment .text
39 global _asm_main
40 _asm_main:
41 enter 0,0 ; setup routine
42 pusha
43
62 ;
63 ; ahora, se imprimen los resultados en una serie de pasos
64 ;
65 mov eax, outmsg1
66 call print_string ; se imprime el primer mensaje
67 mov eax, [input1]
68 call print_int ; se imprime input1
69 mov eax, outmsg2
70 call print_string ; se imprime el segundo mensaje
71 mov eax, [input2]
72 call print_int ; se imprime input2
73 mov eax, outmsg3
74 call print_string ; se imprime el tercer mensaje
75 mov eax, ebx
76 call print_int ; se imprime la suma (ebx)
77 call print_nl ; se imprime una nueva linea
78
79 popa
80 mov eax, 0 ; retorna a C
81 leave
82 ret first.asm
La lı́nea 13 del programa define una sección del programa que especifica
la memoria del segmento de datos (cuyo nombre es .data ). Solo los datos
iniciados se deberı́an definir en este segmento. En las lı́neas 17 a 21 se decla-
ran varias cadenas. Ellas serán impresas con las bibliotecas de C y como tal
deben estar terminadas con el caracter null (el código ASCII 0). Recuerde
que hay una gran diferencia entre 0 y ’0’.
Los datos no iniciados deberı́an declararse en el segmento bss (llamado
.bss en la lı́nea 26). Este segmento toma su nombre de un operador de
ensamblador basado en UNIX que significa “block started by simbol”. Existe
también el segmento de la pila. Será discutido después.
El segmento de código es llamado .text por razones históricas. Acá es
donde se colocan las instrucciones. Observe que la etiqueta de la rutina
principal (lı́nea 38) tiene un prefijo de guión bajo. Esto es parte de las con-
venciones de llamado de C. Esta convención especifica las reglas que usa C
cuando compila el código. Es muy importante conocer esta convención cuan-
do se interfaza C con ensamblador. Luego se presentara toda la convención;
sin embargo por ahora uno solo necesita conocer que todos los sı́mbolos de C
(funciones y variables globales) tienen un guión bajo como prefijo anexado a
ellos por el compilador de C. (Esta regla es especı́fica para DOS/Windows, el
compilador de C de Linux no antepone nada a los nombres de los sı́mbolos).
1.4. CREANDO UN PROGRAMA 23
Donde el formato del objeto es coff ,elf , obj o win32 dependiendo que compi-
lador de C será usado. (Recuerde que también se deben cambiar los archivos
fuente para el caso de Linux y Borland).
gcc -c driver.c
8 segment .bss
9 ;
10 ; Datos no iniciados se colocan en el segmento bss
11 ;
12 segment .text
13 global _asm_main
14 _asm_main:
15 enter 0,0 ; rutina de
16 pusha
17
18 ;
19 ; El código está colocado en el segmento de texto. No modifique el
20 ; código antes o después de este comentario
21 ;
22 popa
23 mov eax, 0 ; retornar a C
24 leave
25 ret skel.asm
Magnitud y signo
29
30 CAPÍTULO 2. LENGUAJE ENSAMBLADOR BÁSICO
Complemento a uno
El segundo método es conocido como complemento a uno. El complemen-
to a uno de un número se encuentra invirtiendo cada bit en el número. (Otra
manera de ver esto es que el nuevo valor del bit es 1 − elvalorantiguodelbit).
Por ejemplo el complemento a uno de 00111000 (+56) es 11000111. En la
notación de complemento a uno calcular el complemento a uno es equiva-
lente a la negación. Ası́ 11000111 es la representación de −56. Observe que
el bit de signo fue cambiado automáticamente por el complemento a uno y
que como se esperarı́a al aplicar el complemento a 1 dos veces produce el
número original. Como el primer método, hay dos representaciones del cero
00000000 (+0) y 11111111 (−0). La aritmética con números en complemento
a uno es complicada.
Hay un truco útil para encontrar el complemento a 1 de un número en
hexadecimal sin convertirlo a binario. El truco es restar el dı́gito hexadecimal
de F (o 15 en decimal). Este método supone que el número de dı́gitos binarios
en el número es un múltiplo de 4. Un ejemplo: +56 se representa por 38
en hex. Para encontrar el complemento a uno reste F de cada dı́gito para
obtener C7 en hexadecimal. Esto es coherente con el resultado anterior.
Complemento a dos
Los dos primeros métodos descritos fueron usados en los primeros compu-
tadores. Los computadores modernos usan un tercer método llamado la re-
presentación en complemento a dos. El complemento a dos de un número se
halla con los dos pasos siguientes:
11000111
+ 1
11001000
00110111
+ 1
00111000
11111111
+ 1
c 00000000
tipos de datos que tienen los lenguajes de alto nivel. Cómo se interpretan
los datos depende de qué instrucción se usa con el dato. Si el valor FF
representa −1 o +255 depende del programador. El lenguaje C define tipos
de entero con y sin signo (signed, unisigned). Esto le permite al compilador
determinar las instrucciones correctas a usar con el dato.
embargo, para extender un número con signo uno debe extender el bit de
signo. Esto significa que los nuevos bits se convierten en copias del bit de
signo. Ya que el bit de signo de FF es 1, los nuevos bits deben ser todos
unos, para producir FFFF. Si el número con signo 5A (90 en decimal) fue
extendido, el resultado serı́a 005A.
Existen varias instrucciones que suministra el 80386 para la extensión de
los números. Recuerde que el computador no conoce si un número está con o
sin signo. Es responsabilidad del programador usar la instrucción adecuada.
Para números sin signo, uno puede simplemente colocar ceros en los bits
superiores usando una instrucción MOV. Por ejemplo, para extender el byte
en AL a una palabra sin signo en AX:
Para números con signo, no hay una manera fácil de usar la instrucción
MOV. EL 8086 suministra varias instrucciones para extender números con
signo. La instrucción CBW (Convert Byte to Word) extiende el registro AL
en AX. Los operandos son implı́citos. La instrucción CWD (Convert Word
to Double Word) extiende AX en DX:AX. La notación DX:AX implica in-
terpretar los registros DX y AX como un registro de 32 bits con los 16 bits
superiores almacenados en DX y los 16 bits inferiores en AX. (Recuerde
que el 8086 no tenı́a ningún registro de 32 bits). El 80386 añadió varias
instrucciones nuevas. La instrucción CWDE (Convert Word to Double word
Extended) extiende AX en EAX. La instrucción CDQ (Convert Double word
to Quad word) extiende EAX en EDX:EAX (¡64 bits!). Finalmente, la ins-
trucción MOVSX trabaja como MOVZX excepto que usa las reglas para números
con signo.
34 CAPÍTULO 2. LENGUAJE ENSAMBLADOR BÁSICO
Figura 2.1:
char ch;
while( (ch = fgetc(fp)) != EOF ) {
/∗ hace algo con ch ∗/
}
Figura 2.2:
Aplicación a la programación en C
ANSI C no define si el ti- Extender enteros con y sin signo también ocurre en C. Las variables en
po char es con signo o no, C se pueden declarar como int signed o unsigned (int es signed). Considere
es responsabilidad de ca- el código de la Figura 2.1. En la lı́nea 3, la variable a se extiende usando las
da compilador decidir esto. reglas para valores sin signo (usando MOVZX), pero en la lı́nea 4 se usan las
Esta es la razón por la cual
reglas con signo para b (usando MOVSX).
el tipo está explı́citamente
definido en la Figura 2.1. Hay un error muy común en la programación en C que tiene que ver
con esto directamente. Considere el código de la Figura 2.2. El prototipo de
fgetc() es:
int fgetc( FILE * );
Uno podrı́a preguntar ¿Por qué la función retorna un int siendo que lee
caracteres? La razón es que normalmente retorna un char (extendido a un
valor entero usando la extensión cero). Sin embargo hay un valor que puede
retornar que no es un carácter, EOF. Este es un macro que normalmente se
define como −1. Ası́ fgetc() o retorna un carácter extendido a entero (que
es como 000000xx en hex) o EOF (que es FFFFFFF en hex).
El problema principal con el programa de la Figura 2.2 es que fgetc()
retorna un entero, pero este valor se almacena en un char. C truncará los
bits de mayor peso para que el entero quepa en el caracter. El único problema
es que los números (en hex) 000000FF y FFFFFFFF ambos se truncarán al
byte FF. Ası́ el ciclo while no puede distinguir entre el byte FF y el fin de
archivo (EOF).
Lo que sucede exactamente en este caso, depende de si el char es con
signo o sin signo ¿por qué? Porque en la lı́nea 2 ch es comparada con EOF.
Ya que EOF es un valor int1 , ch será extendido a un int de modo que los
1
Es un concepto erróneo pensar que los archivos tienen un carácter EOF al final. ¡Esto
2.1. TRABAJANDO CON ENTEROS 35
002C 44
+ FFFF + (−1)
002B 43
mul fuente
div fuente
11 segment .bss
12 input resd 1
13
14 segment .text
15 global _asm_main
16 _asm_main:
17 enter 0,0 ; rutina de inicio
18 pusha
19
23 call read_int
24 mov [input], eax
25
32 call print_nl
33
72 popa
73 mov eax, 0 ; retorna a C
2.2. ESTRUCTURAS DE CONTROL 39
74 leave
75 ret math.asm
2.2.1. Comparaciones
Las estructuras de control deciden que hacer basados en la comparación
de datos. En ensamblador, el resultado de una comparación se almacenan
en el registro FLAGS para usarlas luego. El 80x86 suministra la instrucción
CMP para realizar comparaciones. El registro FLAGS se fija basado en la
diferencia de los dos operandos de la instrucción CMP. Los operandos se
restan y se fija el registro FLAGS basado en el resultado, pero el resultado
no se almacena en ninguna parte. Si necesita el resultado use la instrucción
SUB en lugar de la instrucción CMP.
Para enteros sin signos hay dos banderas (bits en el registro FLAGS)
que son importante: cero (ZF) y carry (CF). La bandera cero se fija (1) si
el resultado de la resta es cero. La bandera carry se usa como bandera de
préstamo para la resta. Considere una comparación como:
SHORT Este salto es de tamaño muy limitado, solo se puede mover arri-
ba o abajo 128 bytes en memoria. La ventaja de este tipo es que usa
menos memoria que otros. Usa un byte con signo para almacenar el
desplazamiento del salto. El desplazamiento es cuántos bytes se mueve
adelante o atrás. (El desplazamiento se añade a EIP). Para especifi-
car un salto corto, use la palabra SHORT inmediatamente antes de la
etiqueta en la instrucción JMP.
FAR Este salto permite moverse a otro segmento de código. Este es una
cosa muy rara para hacerla en el modo protegido del 386.
Las etiquetas de código válidas siguen las mismas reglas que las etique-
tas de datos. Las etiquetas de código están definidas para colocarlas en el
segmento de código al frente de la instrucción sus etiquetas. Dos puntos se
colocan al final de la etiqueta en este punto de la definición. Los dos puntos
no son parte del nombre.
Hay muchas instrucciones de ramificación condicional diferentes. Ellas
también toman una etiqueta como su operando. Las más sencillas solo ven
una bandera en el registro FLAGS para determinar si salta o no. Vea el
Cuadro 2.3 para una lista de estas instrucciones (PF es la bandera de paridad
42 CAPÍTULO 2. LENGUAJE ENSAMBLADOR BÁSICO
if ( EAX == 0 )
EBX = 1;
else
EBX = 2;
if ( EAX >= 5 )
EBX = 1;
else
EBX = 2;
Si EAX es mayor que o igual a 5, ZF debe estar fija o borrada y SF será igual
a OF. A continuación está el código en ensamblador que prueba estas con-
diciones (asumiendo que EAX es con signo):
2.2. ESTRUCTURAS DE CONTROL 43
Signed Unsigned
JE salta si vleft = vright JE salta si vleft = vright
JNE salta si vleft 6= vright JNE salta si vleft 6= vright
JL, JNGE salta si vleft <vright JB, JNAE salta si vleft <vright
JLE, JNG salta si vleft ≤ vright JBE, JNA salta si vleft ≤ vright
JG, JNLE salta si vleft >vright JA, JNBE salta si vleft >vright
JGE, JNL salta si vleft ≥ vright JAE, JNB salta si vleft ≥ vright
1 cmp eax, 5
2 js signon ; salta a signon si SF = 1
3 jo elseblock ; salta a elseblock si OF = 1 y SF = 0
4 jmp thenblock ; salta a thenblock si SF = 0 y OF = 0
5 signon:
6 jo thenblock ; salta a thenblock si SF = 1 y OF = 1
7 elseblock:
8 mov ebx, 2
9 jmp next
10 thenblock:
11 mov ebx, 1
12 next:
1 cmp eax, 5
2 jge thenblock
3 mov ebx, 2
4 jmp next
44 CAPÍTULO 2. LENGUAJE ENSAMBLADOR BÁSICO
5 thenblock:
6 mov ebx, 1
7 next:
2.3.1. instrucciones if
El siguiente pseudocódigo:
if ( condición )
bloque entonces;
else
bloque else ;
2.3. TRADUCIR ESTRUCTURAS DE CONTROL ESTÁNDARES 45
Si no hay else, entonces el salto al else block puede ser reemplazado por
un salto a endif.
1 while:
2 ; código que fija FLAGS basado en la condición
3 jxx endwhile ; selecciona xx tal que salte si es falso
4 ; Cuerpo del bucle
5 jmp while
6 endwhile:
Figura 2.3:
1 do:
2 ; cuerpo del bucle
3 ; código para fijar FLAGS basado en la condición
4 jxx do ; seleccionar xx tal que salte si es verdadero
prime.asm
1 %include "asm_io.inc"
2 segment .data
3
2 es el único número par.
2.4. EJEMPLO: HALLAR NÚMEROS PRIMOS 47
5 segment .bss
6 Limit resd 1 ; halle primos hasta este lı́mite
7 Guess resd 1 ; la conjetura actual para el primo
8
9 segment .text
10 global _asm_main
11 _asm_main:
12 enter 0,0 ; rutina de inicio
13 pusha
14
45
58 popa
59 mov eax, 0 ; retorna a C
60 leave
61 ret prime.asm
Capı́tulo 3
Original 1 1 1 0 1 0 1 0
Desplazado a la izquierda 1 1 0 1 0 1 0 0
Desplazado a la derecha 0 1 1 1 0 1 0 1
Observe que los nuevos bits que entran son siempre cero. Se usan las
instrucciones SHL y SHR para realizar los desplazamientos a la izquierda y
derecha respectivamente. Estas instrucciones le permiten a uno desplazar
cualquier número de posiciones. El número de posiciones puede ser o una
constante o puede estar almacenado en el registro CL. El último bit despla-
zado se almacena en la bandera de carry. A continuación, algunos ejemplos:
1 mov ax, 0C123H
2 shl ax, 1 ; desplaza un bit a la izquierda,
3 ; ax = 8246H, CF = 1
4 shr ax, 1 ; desplaza un bit a la derecha,
49
50 CAPÍTULO 3. OPERACIONES CON BITS
5 ; ax = 4123H, CF = 0
6 shr ax, 1 ; desplaza un bit a la derecha,
7 ; ax = 2091H, CF = 1
8 mov ax, 0C123H
9 shl ax, 2 ; desplaza dos bit a la izquierda,
10 ;ax = 048CH, CF = 1
11 mov cl, 3
12 shr ax, cl ; desplaza tres bit a la derecha,
13 ; ax = 0091H, CF = 1
SAL (Shift aritmetic left). Esta instrucción es solo sinónimo para SHL. Se
ensambla con el mismo código de máquina que SHL. Como el bit de
signo no se cambia por el desplazamiento, el resultado será correcto.
SAR
SAR (Shift Arithmetic Right). Esta es una instrucción nueva que no des-
plaza el bit de signo (el bit más significativo) de su operando. Los
otros bits se desplazan como es normal excepto que los bits nuevos
que entran por la derecha son copias del bit de signo (esto es, si el
3.1. OPERACIONES DE DESPLAZAMIENTOS 51
bit de signo es 1, los nuevos bits son también 1). Ası́, si un byte se
desplaza con esta instrucción, sólo los 7 bits inferiores se desplazan.
Como las otras instrucciones de desplazamiento, el último bit que sale
se almacena en la bandera de carry.
X Y X AND Y
0 0 0
0 1 0
1 0 0
1 1 1
1 0 1 0 1 0 1 0
AND 1 1 0 0 1 0 0 1
1 0 0 0 1 0 0 0
X Y X OR Y
0 0 0
0 1 1
1 0 1
1 1 1
X Y X XOR Y
0 0 0
0 1 1
1 0 1
1 1 0
3.2.2. La operación OR
El O inclusivo entre dos bits es 0 solo si ambos bits son 0, si no el
resultado es 1 como se muestra en el Cuadro 3.2 . A continuación un código
de ejemplo:
X NOT X
0 1
1 0
el número con una máscara igual a 2i −1. Esta máscara contendrá unos desde
el bit 0 hasta el bit i − 1. Son solo estos bits los que contienen el residuo.
El resultado de la AND conservará estos bits y dejará cero los otros. A
continuación un fragmento de código que encuentra el cociente y el residuo
de la división de 100 por 16.
1 mov eax, 100 ; 100 = 64H
2 mov ebx, 0000000FH ; mácara = 16 - 1 = 15 or F
3 and ebx, eax ; ebx = residuo = 4
Usando el registro CL es posible modificar arbitrariamente bits. El siguiente
es un ejemplo que fija (prende) un bit arbitrario en EAX. El número del bit
a prender se almacena en BH.
1 mov cl, bh ;
2 mov ebx, 1
3 shl ebx, cl ; se desplaza a la derecha cl veces
4 or eax, ebx ; prende el bit
Apagar un bit es solo un poco más difı́cil.
1 mov cl, bh ;
2 mov ebx, 1
3 shl ebx, cl ; se desplaza a la derecha cl veces
4 not ebx ; invierte los bits
5 and eax, ebx ; apaga el bit
El código para complementar un bit arbitrario es dejado como ejercicio al
lector.
Es común ver esta instrucción en un programa 80x86.
xor eax, eax ; eax = 0
Un número XOR con sigo mismo, el resultado es siempre cero. Esta instruc-
ción se usa porque su código de máquina es más pequeño que la instrucción
MOV equivalente.
1 ; Archivo: max.asm
2 %include "asm_io.inc"
3 segment .data
4
9 segment .bss
10
13 segment .text
14 global _asm_main
15 _asm_main:
16 enter 0,0 ;
17 pusha
18
44 popa
45 mov eax, 0 ; retorna a C
46 leave
47 ret
58 CAPÍTULO 3. OPERACIONES CON BITS
El truco es crear una máscara de bits que se pueda usar para seleccionar
el valor mayor. La instrucción SETG en la lı́nea 30 fija BL a 1. Si la segunda
entrada es mayor o 0 en otro caso. Esta no es la máscara deseada. Para crear
la máscara de bits requerida la lı́nea 31 usa la instrucción NEG en el registro
EBX. (Observe que se borró EBX primero). Si EBX es 0 no hace nada; sin
embargo si EBX es 1, el resultado es la representación en complemento a dos
de -1 o 0xFFFFFFFF. Esta es la máscara que se necesita. El resto del código
usa esta máscara para seleccionar la entrada correcta como e la mayor.
Un truco alternativo es usar la instrucción DEC. En el código de arriba, si
NEG se reemplaza con un DEC, de nuevo el resultado será 0 o 0xFFFFFFFF.
Sin embargo, los valores son invertidos que cuando se usa la instrucción NEG.
1
¡Este operador es diferente del operador binario && y del unario &!
3.4. MANIPULANDO BITS EN C 59
Macro Meaning
S IRUSR el propietario puede leer
S IWUSR el propietario puede escribir
S IXUSR el propietario puede ejecutar
S IRGRP el grupo de propietario puede leer
S IWGRP el grupo del propietario puede escribir
S IXGRP el grupo del propietario puede ejecutar
S IROTH los otros pueden leer
S IWOTH los otros pueden escribir
S IXOTH los otros pueden ejecutar
if ( p [0] == 0x12 )
printf (”Máquina Big Endian\n”);
else
printf (”Máquina Little Endian\n”);
while( data != 0 ) {
data = data & (data − 1);
cnt++;
}
return cnt ;
}
de este 1 debe ser cero. Ahora, ¿Cuál será la representación de data -1?
Los bits a la izquierda del 1 del extremo derecho serán los mismos que para
data, pero en el punto del 1 del extremo derecho ellos serán el complemento
de los bits originales de data. Por ejemplo:
data = xxxxx10000
data - 1 = xxxxx01111
donde X es igual para ambos números. Cuando se hace data AND data
-1, el resultado será cero el 1 del extremo derecho en data y deja todos los
otros bits sin cambio.
return byte bit count [ byte [0]] + byte bit count [ byte [1]] +
byte bit count [ byte [2]] + byte bit count [ byte [3]];
}
versión del bucle for a la suma explı́cita. Este proceso de reducir o eliminar
iteraciones de bucles es una técnica de optimización conocida como loop
unrolling.
La suma de la derecha muestra los bits sumados. Los bits del byte se
dividen en 4 campos de 2 bits para mostrar que se realizan 4 sumas indepen-
dientes. Ya que la mayorı́a de estas sumas pueden ser dos, no hay posibilidad
de que la suma desborde este campo y dañe otro de los campos de la suma.
Claro está, el número total de bits no se ha calculado aún. Sin embargo
la misma técnica que se usó arriba se puede usar para calcular el total en
una serie de pasos similares. El siguiente paso podrı́a ser:
data = (data & 0x33) + ((data >> 2) & 0x33);
Continuando con el ejemplo de arriba (recuerde que data es ahora 011000102 ):
data & 001100112 0010 0010
+ (data >> 2) & 001100112 or + 0001 0000
0011 0010
Ahora hay 2 campos de 4 bits que se suman independientemente.
El próximo paso es sumar estas dos sumas unidas para conformar el
resultado final:
data = (data & 0x0F) + ((data >> 4) & 0x0F);
Usando el ejemplo de arriba (con data igual a 001100102 ):
data & 000011112 00000010
+ (data >> 4) & 000011112 or + 00000011
00000101
Ahora data es 5 que es el resultado correcto. La Figura 3.8 muestra una
implementación de este método que cuenta los bits en una palabra doble.
Usa un bucle for para calcular la suma. Podrı́a ser más rápido deshacer el
bucle; sin embargo, el bucle clarifica cómo el método generaliza a diferentes
tamaños de datos.
Capı́tulo 4
Subprogramas
Debido a que AX almacena una palabra, la lı́nea 3 lee una palabra comen-
zando en la dirección almacenada en EBX. Si AX fuera reemplazando con
AL, se leerı́a un solo byte. Es importante notar que los registros no tienen
tipos como lo hacen las variables en C. A lo que EBX se asume que señala
está totalmente determinada por qué instrucciones se usan. Si EBX se utiliza
incorrectamente, a menudo no habrá error en el ensamblador; sin embargo,
el programa no trabajará correctamente. Esta es una de las muchas razones
67
68 CAPÍTULO 4. SUBPROGRAMAS
sub1.asm
1 ; file: sub1.asm
2 ; Subprograma programa de ejemplo
3 %include "asm_io.inc"
4
5 segment .data
6 prompt1 db "Ingrese un número: ", 0 ; no olvide el NULL
7 prompt2 db "Ingrese otro número: ", 0
8 outmsg1 db "Ud. ha ingresado ", 0
9 outmsg2 db " y ", 0
10 outmsg3 db ", la suma de ellos es ", 0
11
12 segment .bss
13 input1 resd 1
14 input2 resd 1
15
16 segment .text
17 global _asm_main
18 _asm_main:
19 enter 0,0 ; setup routine
20 pusha
21
4.2. SENCILLO SUBPROGRAMA DE EJEMPLO 69
54 popa
55 mov eax, 0 ; retorno a C
56 leave
57 ret
58 ; subprograma get_int
59 ; Parámetros:
60 ; ebx - dirección de la palabra doble que almacena el entero
61 ; ecx - dirección de la instrucción a donde retornar
62 ; Notes:
63 ; el valor de eax se destruye
70 CAPÍTULO 4. SUBPROGRAMAS
64 get_int:
65 call read_int
66 mov [ebx], eax ; almacena la entrada en memoria
67 jmp ecx ; salta al llamador
sub1.asm
4.3. La pila
Muchas CPU tienen soporte para una pila. Una pila es una lista LIFO
(Last In Firist Out). La pila es un arca de memoria que está organizada de
esta manera. La instrucción PUSH añade datos a la pila y la instrucción POP
quita datos. El dato extraı́do siempre es el último dato insertado (esta es la
razón por la cual es llamado FIFO).
El registro de segmento SS especifica el segmento de datos que contiene
la pila. (Normalmente este es el mismo segmento de datos). El registro ESP
contiene la dirección del dato que serı́a quitado de la pila. Los datos sólo
se pueden añadir en unidades de palabras dobles. Esto es, que no se puede
insertar un solo byte en la pila.
La instrucción PUSH inserta una palabra doble1 en la pila restándole
4 a ESP y entonces almacena la palabra doble en [ESP]. La instrucción
POP lee la palabra doble almacenada en [ESP] y luego añade 4 a ESP. El
código siguiente demuestra cómo trabajan estas instrucciones asumiendo que
el valor inicial de ESP es 1000H.
get_int:
call read_int
mov [ebx], eax
ret
en la pila. Al final del código de red int hay un RET que saca la di-
rección de retorno y que salta de nuevo al código de get int. Cuando
la instrucción RET de get int se ejecuta, saca la dirección de retorno
que salta de nuevo a asm main. Esto trabaja correctamente por la
propiedad LIFO de la pila.
Recuerde es muy importante sacar todos los datos que se han empujado
en la pila. Por ejemplo considere lo siguiente:
1 get_int:
2 call read_int
3 mov [ebx], eax
4 push eax
5 ret ; ¡¡saca el valor de EAX,
6 ; no la dirección de retorno!!
Este código no retornarı́a correctamente.
ESP + 4 Parámetro
ESP Dirección de retorno
Figura 4.1:
ESP + 8 Parámetro
ESP + 4 Dirección de retorno
ESP datos del subprograma
Figura 4.2:
Considere un subprograma al que se le pasa un solo parámetro en la pila. Cuando se usa direcciona-
Cuando el subprograma se invoca, la pila se ve como en la Figura 4.1. Se miento indirecto, el proce-
puede acceder al parámetro usando direccionamiento indirecto ([ESP+4])2 . sador 80x86 accede a seg-
mentos diferentes depen-
Si la pila se usa dentro del subprograma para almacenar datos, el número
diendo de qué registros se
necesario a ser agregado a ESP cambiará. Por ejemplo, la Figura 4.2 muestra usan en la expresión de
cómo se ve la pila si una palabra doble se empuja en ella. Ahora el parámetro direccionamiento indirec-
es ESP + 8 y no ESP + 4. Ası́, esto puede ser muy propenso a errores usar to. ESP (y EBP) usan el
ESP cuando uno se refiere a parámetros. Para resolver este problema, el segmento de la pila mien-
80386 suministra otro registro: EBP. El único propósito de este registro es tras que EAX, EBX, ECX
referenciar datos en la pila. La convención de llamado de C ordena que un y EDX usan el segmento
subprograma primero guarde el valor de EBP en la pila y luego lo haga de datos. Sin embargo, esto
normalmente no tiene im-
igual a ESP. Esto le permite a ESP cambiar cuando los datos se empujen o
portancia para la mayorı́a
se saquen de la pila sin modificar EBP. Al final del subprograma, se debe de los programas en modo
restaurar el valor de EBP (esta es la razón por la cual se guarda el valor al protegido, porque para ellos
principio del subprograma). La Figura 4.3 muestra la forma general de un los segmentos de datos y de
subprograma que sigue estas convenciones. la pila son los mismos.
2
Es válido añadir una constante a un registro cuando se usa direccionamiento indirec-
to. Se pueden construir expresiones más complicadas también. Este tópico se verá en el
capı́tulo siguiente.
74 CAPÍTULO 4. SUBPROGRAMAS
1 subprogram_label:
2 push ebp ; guarda el valor original de EBP en la pila
3 mov ebp, esp ; nuevo EBP = ESP
4 ; subprogram code
5 pop ebp ; restaura el valor original de EBP
6 ret
Figura 4.4:
sub3.asm
1 %include "asm_io.inc"
2
3 segment .data
4 sum dd 0
5
6 segment .bss
7 input resd 1
8
9 ;
10 ; pseudo-código
11 ; i = 1;
12 ; sum = 0;
13 ; while( get_int(i, &input), input != 0 ) {
14 ; sum += input;
15 ; i++;
16 ; }
17 ; print_sum(num);
18 segment .text
76 CAPÍTULO 4. SUBPROGRAMAS
19 global _asm_main
20 _asm_main:
21 enter 0,0 ; setup routine
22 pusha
23
37 inc edx
38 jmp short while_loop
39
40 end_while:
41 push dword [sum] ; empuja el valor de sum en la pila
42 call print_sum
43 pop ecx ; quita [sum] de la pila
44
45 popa
46 leave
47 ret
48
49 ; subprograma get_int
50 ; Paramétros (en el orden que es empujan en la pila)
51 ; número de input (en [ebp + 12])
52 ; dirección de input en [ebp + 8])
53 ; Notas:
54 ; Los valores de eax y ebx se destruyen
55 segment .data
56 prompt db ") Ingrese un entero (0 para salir): ", 0
57
58 segment .text
59 get_int:
60 push ebp
4.5. CONVENCIONES DE LLAMADO 77
69 call read_int
70 mov ebx, [ebp + 8]
71 mov [ebx], eax ; almacena input en memoria
72
73 pop ebp
74 ret ; retorna al llamador
75
76 ; subprograma print_sum
77 ; imprime la suma
78 ; Parameter:
79 ; suma a imprimir (en [ebp+8])
80 ; Nota: destruye el valor de eax
81 ;
82 segment .data
83 result db "La suma es ", 0
84
85 segment .text
86 print_sum:
87 push ebp
88 mov ebp, esp
89
97 pop ebp
98 ret sub3.asm
78 CAPÍTULO 4. SUBPROGRAMAS
1 subprogram_label:
2 push ebp ; guarda el valor original de EBP
3 ; en la pila
4 mov ebp, esp ; nuevo EBP = ESP
5 sub esp, LOCAL_BYTES ; = # de bytes necesitados por las
6 ; variables locales
7 ; subprogram code
8 mov esp, ebp ; libera las variables locales
9 pop ebp ; restaura el valor original de EBP
10 ret
1 cal_sum:
2 push ebp
3 mov ebp, esp
4 sub esp, 4 ; hace espacio para la sum local
5
16 end_for:
17 mov ebx, [ebp+12] ; ebx = sump
18 mov eax, [ebp-4] ; eax = sum
19 mov [ebx], eax ; *sump = sum;
20
Figura 4.9:
1 subprogram_label:
2 enter LOCAL_BYTES, 0 ; = número de bytes necesitados
3 ; por las variables locales
4 ; subprogram code
5 leave
6 ret
main4.asm
1 %include "asm_io.inc"
2
3 segment .data
4 sum dd 0
5
6 segment .bss
7 input resd 1
8
9 segment .text
10 global _asm_main
11 extern get_int, print_sum
12 _asm_main:
13 enter 0,0 ; setup routine
14 pusha
15
29 inc edx
30 jmp short while_loop
31
32 end_while:
33 push dword [sum] ; empuja el valor de sum de la pila
34 call print_sum
35 pop ecx ; quita sum de la pila
36
82 CAPÍTULO 4. SUBPROGRAMAS
37 popa
38 leave
39 ret main4.asm
sub4.asm
1 %include "asm_io.inc"
2
3 segment .data
4 prompt db ") Ingrese un número entero (0 para salir): ", 0
5
6 segment .text
7 global get_int, print_sum
8 get_int:
9 enter 0,0
10
17 call read_int
18 mov ebx, [ebp + 8]
19 mov [ebx], eax ; almacena input en memoria
20
21 leave
22 ret ; retorna
23
24 segment .data
25 result db "La suma es ", 0
26
27 segment .text
28 print_sum:
29 enter 0,0
30
38 leave
4.7. INTERFAZANDO ENSAMBLADOR CON C 83
39 ret sub4.asm
El ejemplo anterior solo tiene etiquetas de código global sin embargo las
etiquetas de datos global trabajan exactamente de la misma manera.
La última razón no es tan válida como una vez lo fue. La tecnologı́a de los
compiladores se ha mejorado con los años y a menudo generan un código muy
eficiente. (Especialmente si se activan las optimizaciones del compilador).
Las desventajas de las rutinas en ensamblador son: portabilidad reducida y
lo poca legibilidad.
La mayorı́a de las convenciones de llamado ya se han especificado. Sin
embargo hay algunas caracterı́sticas adicionales que necesitan ser descritas.
3
GAS es el ensamblador que usan todos los compiladores GNV. Usa la sintaxis AT&T
que es muy diferente de la sintaxis relativamente similares de MASM, TASM y NASM.
84 CAPÍTULO 4. SUBPROGRAMAS
1 segment .data
2 x dd 0
3 format db "x = %d\n", 0
4
5 segment .text
6 ...
7 push dword [x] ; empuja el valor de x
8 push dword format ; empuja la dirección de la cadena
9 ; con formato
10 call _printf ; observe el guión bajo
11 add esp, 8 ; quita los parámetros de la pila
EBP + 12 valor de x
EBP + 8 dirección de la cadena con formato
EBP + 4 Dirección de retorno
EBP EBP guardado
funciones que tomen un número fijo de parámetros (ejm. unas que no sean
como printf y scanf).
GCC también soporta un atributo adicional llamado regparam que le di-
ce al compilador que use los registros para pasar hasta 3 argumentos enteros
a su función en lugar de usar la pila. Este es un tipo común de optimización
que soportan muchos compiladores.
Borland y Microsoft usan una sintaxis común para declarar convencio-
nes de llamado. Ellas añaden a las palabras reservadas cdecl stdcall
a C. Estas palabras reservadas actúan como modificadoras de funciones y
aparecen inmediatamente antes nombre de la función en un prototipo. Por
ejemplo, la función f de arriba se podrı́a definir para Borland y Microsoft
ası́:
void cdecl f ( int );
Hay ventajas y desventajas para cada convención de llamado. La prin-
cipal ventaja de la convención cdecl es que es simple y muy flexible. Se
puede usar para cualquier tipo de función de C y cualquier compilador de
C. Usar otras convenciones puede limitar la portabilidad de la subrutina.
Su principal desventaja es que puede ser más lenta que alguna de las otras
y usa más memoria (ya que cada vez que se invoca de la función requiere
código para quitar los parámetros de la pila).
Las ventajas de la convención stdcall es que usa menos memoria que
cdecl. No se requiere limpiar la pila después de la instrucción CALL. Su
principal desventaja es que no se puede usar con funciones que tengan un
número variable de argumentos.
La ventaja de usar una convención que use registros para pasar enteros es
la velocidad. La principal desventaja es que la convención es más compleja.
Algunos parámetros pueden estar en los registros y otros en la pila.
4.7.7. Ejemplos
El siguiente es un ejemplo que muestra cómo una rutina de ensamblador
se puede interfasar con un programa de C (observe que este programa no
usa el programa esqueleto de ensamblador (Figura 1.7) o el módulo driver.c)
main5.c
#include <stdio.h>
/∗ prototipo para la rutina en ensamblador ∗/
void calc sum( int , int ∗ ) attribute ((cdecl ));
int n, sum;
main5.c
sub5.asm
1 ; subroutinea _calc_sum
2 ; halla la suma de los enteros de 1 hasta n
3 ; Parametros:
4 ; n - hasta dónde sumar (en [ebp + 8])
5 ; sump - apuntador a un int para almacenar sum (en [ebp + 12])
6 ; pseudocódigo en C:
7 ; void calc_sum( int n, int * sump )
8 ; {
9 ; int i, sum = 0;
10 ; for( i=1; i <= n; i++ )
11 ; sum += i;
12 ; *sump = sum;
13 ; }
14
15 segment .text
16 global _calc_sum
17 ;
18 ; local variable:
19 ; sum at [ebp-4]
20 _calc_sum:
21 enter 4,0 ; hace espacio para sum en la pila
22 push ebx ; IMPORTANTE!
23
32 inc ecx
33 jmp short for_loop
34
35 end_for:
36 mov ebx, [ebp+12] ; ebx = sump
37 mov eax, [ebp-4] ; eax = sum
38 mov [ebx], eax
39
La función calc sum podrı́a ser rescrita para devolver la suma como un
valor de retorno en lugar de usar un parámetro apuntador. Ya que la suma
es un valor entero, la suma se podrı́a dejar en el registro EAX. La lı́nea 11
del archivo main5.c deberı́a ser cambiada por:
sum = calc sum(n);
También, el prototipo de calc sum deberı́a ser alterado. A continuación, el
código modificado en ensamblador.
sub6.asm
1 ; subroutina _calc_sum
2 ; halla la suma de los enteros de 1 hasta n
3 ; Parámetros:
4 ; n - hasta dónde se suma (en [ebp + 8])
5 ; Valor de retorno:
6 ; la suma
7 ; pseudocódigo en C:
8 ; int calc_sum( int n )
9 ; {
10 ; int i, sum = 0;
11 ; for( i=1; i <= n; i++ )
12 ; sum += i;
13 ; return sum;
14 ; }
15 segment .text
16 global _calc_sum
17 ;
18 ; local variable:
19 ; sum at [ebp-4]
20 _calc_sum:
21 enter 4,0 ; hace espacio en la pila para sum
22
33 end_for:
4.8. SUBPROGRAMAS REENTRANTES Y RECURSIVOS 91
1 segment .data
2 format db "%d", 0
3
4 segment .text
5 ...
6 lea eax, [ebp-16]
7 push eax
8 push dword format
9 call _scanf
10 add esp, 8
11 ...
36 leave
37 ret sub6.asm
1 ; finds n!
2 segment .text
3 global _fact
4 _fact:
5 enter 0,0
6
n(3)
marco n=3 Dirección de retorno
EBP guardado
n(2)
marco n=2 Dirección de retorno
EBP guardado
n(1)
marco n=1 Dirección de retorno
EBP guardado
void f ( int x )
{
int i ;
for( i =0; i < x; i ++ ) {
printf (” %d\n”, i);
f ( i );
}
}
static Estas son variables locales de una función que son declaradas static
(desafortunadamente, C usa la palabra reservada static para dos
propósitos diferentes). Estas variables se almacenan también en lu-
gares fijos de memoria (en data o bss), pero solo se pueden acceder
directamente en las funciones en donde ellas se definieron.
automatic Este es el tipo por omisión para una variable definida dentro de
una función. Estas variables son colocadas en la pila cuando la función
en la cual están definidas se invocada y quitadas cuando la función
retorna. Ası́, ellas no tienen un lugar fijo en memoria.
4.8. SUBPROGRAMAS REENTRANTES Y RECURSIVOS 95
1 %define i ebp-4
2 %define x ebp+8 ; macros útiles
3 segment .data
4 format db "%d", 10, 0 ; 10 = ’\n’
5 segment .text
6 global _f
7 extern _printf
8 _f:
9 enter 4,0 ; toma espacio en la pila para i
10
Si x pudiera ser alterada por otro hilo, es posible que otro hilo cambie
x entre las lı́neas 1 y 3 ası́ que z podrı́a no ser 10. Sin embargo, si x no
fue declarada volatile, el compilador podrı́a asumir que x no cambia y
fija z a 10.
If x could be altered by another thread, it is possible that the other
thread changes x between lines 1 and 3 so that z would not be 10.
However, if the x was not declared volatile, the compiler might assume
that x is unchanged and set z to 10.
Otro uso de volatile es evitar que el compilador use un registro para
una variable.
Capı́tulo 5
Arreglos
5.1. Introducción
Un arreglo es un bloque contiguo de una lista de datos en la memoria.
Cada elemento de la lista debe ser del mismo tipo y usar exactamente el
mismo número de bytes de memoria para almacenarlo. Por estas propieda-
des, los arreglos permiten un acceso eficiente de los datos por su posición (o
ı́ndice) en el arreglo. La dirección de cualquier elemento se puede calcular
conociendo tres hechos:
97
98 CAPÍTULO 5. ARREGLOS
1 segment .data
2 ; define un arreglo de 10 plabras dobles con valores
3 ; iniciales de 1,2,..,10
4 a1 dd 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
5 ; define un arreglo de 10 palabras inicadas todas con 0
6 a2 dw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
7 ; lo mismo de antes usando TIMES
8 a3 times 10 dw 0
9 ; define un arreglo de bytes con 200 ceros y luego 100 con unos
10 a4 times 200 db 0
11 times 100 db 1
12
13 segment .bss
14 ; define un arreglo de 10 palabras dobles sin valor inicial
15 a5 resd 10
16 ; define un arreglo de 100 palabras dobles sin valor inicial
17 a6 resw 100
No hay una manera directa de definir una arreglo como variable local en
la pila. Como antes, uno calcula el total de bytes necesarios para todas las
variables locales, incluidos los arreglos, y resta esto de ESP (o directamente
usando la instrucción ENTER). Por ejemplo, si una función necesita una va-
riable caracter, dos enteros y 50 elementos de un arreglo de palabras, uno
necesitarı́a 1 + 2 + 50 = 109 bytes. Sin embargo, el número restado de ESP
deberı́a ser un múltiplo de cuatro (112 en este caso) para que ESP esté en el
lı́mite de una palabra doble. Uno podrı́a formar las variables dentro de estos
109 bytes de varias maneras. La Figura 5.2 muestra dos maneras posibles.
La parte sin uso del primer ordenamiento está para dejar las palabras dobles
en los lı́mites de palabras dobles para optimizar la velocidad del acceso a la
memoria.
5.1. INTRODUCCIÓN 99
EBP - 1 char
no usado
EBP - 8 dword 1
EBP - 12 dword 2 word
array
word
array EBP - 100
EBP - 104 dword 1
EBP - 108 dword 2
EBP - 109 char
EBP - 112 no usado
ser del mismo tamaño. Segundo, es probable que al sumar bytes y llegue-
mos a un resultado que no quepa en un byte. Usando DX, se permite una
suma hasta 65.535. Sin embargo es importante ver que se está sumando AH
también. Esta es la razón por la cual se fija AH a cero.1 en la lı́nea 3.
Las Figuras 5.4 y 5.5 muestran dos alternativas para calcular la suma.
Las lı́neas en itálica reemplazan las lı́neas 6 y 7 de la Figura 5.3.
donde:
reg base es uno de los registros EAX, EBX, ECX, EDX, EBP, ESP, ESI o
EDI.
index reg es uno de estos registros EAX, EBX, ECX, EDX, EBP, ESI,
EDI. (observe que ESP no está en la lista.) (o expresión).
5.1.4. Ejemplo
Se presenta un ejemplo que usa un arreglo y se pasa a la función.
Usa el programa array1c.c (listado abajo) como driver y no el programa
driver.c.
array1.asm
1 %define ARRAY_SIZE 100
2 %define NEW_LINE 10
3
4 segment .data
5 FirstMsg db "Primeros 10 elementos del arreglo", 0
6 Prompt db "Ingrese el ı́ndice del elemento a mostrar: ", 0
7 SecondMsg db "Elemento %d es %d", NEW_LINE, 0
8 ThirdMsg db "Elementos 20 hasta 29 del arreglo", 0
9 InputFormat db "%d", 0
10
11 segment .bss
12 array resd ARRAY_SIZE
13
14 segment .text
15 extern _puts, _printf, _scanf, _dump_line
102 CAPÍTULO 5. ARREGLOS
16 global _asm_main
17 _asm_main:
18 enter 4,0 ; variable local en EBP - 4
19 push ebx
20 push esi
21
35 push dword 10
36 push dword array
37 call _print_array ; imprime los 10 primeros
38 ; elementos del arreglo
39 add esp, 8
40
58
59 InputOK:
60 mov esi, [ebp-4]
61 push dword [array + 4*esi]
62 push esi
63 push dword SecondMsg ; imprime el valor del elemento
64 call _printf
65 add esp, 12
66
71 push dword 10
72 push dword array + 20*4 ; dirección de array[20]
73 call _print_array
74 add esp, 8
75
76 pop esi
77 pop ebx
78 mov eax, 0 ; retorna a C
79 leave
80 ret
81
82 ;
83 ; rutina _print_array
84 ; Rutina llamable desde C que imprime elementos de arreglos de palabras
85 ; dobles como enteros con signo
86 ; Prototipo de C:
87 ; void print_array( const int * a, int n);
88 ; Parámetros:
89 ; a - Apuntador al arreglo a imprimir (en ebp+8 en la pila)
90 ; n - número de enteros a imprimir (en ebp+12 en la pila)
91
92 segment .data
93 OutputFormat db "%-5d %5d", NEW_LINE, 0
94
95 segment .text
96 global _print_array
97 _print_array:
98 enter 0,0
99 push esi
104 CAPÍTULO 5. ARREGLOS
array1c.c
#include <stdio.h>
int main()
{
int ret status ;
ret status = asm main();
return ret status ;
}
/∗
∗ función dump line
∗ Vacı́a todos los caracteres de la lı́nea del búfer de entrada
∗/
void dump line()
5.1. INTRODUCCIÓN 105
{
int ch;
array1c.c
Arreglos bidimensionales
No debe sorprender que el arreglo multidimiensional más elemental es
un arreglo bidimensional. Un arreglo bidimensional se presenta a menudo
como una malla de elementos. Cada elemento está identificado por un par
de ı́ndices. Por convención, el primer ı́ndice es identificado con la fila del
elemento y el segundo ı́ndice es la columna.
Considere un arreglo con tres filas y dos columnas definidas como:
int a [3][2];
El compilador de C reservarı́a espacio para un arreglo de 6 (= 2 × 3) enteros
y ordena los elementos como sigue:
Índice 0 1 2 3 4 5
Elemento a[0][0] a[0][1] a[1][0] a[1][1] a[2][0] a[2][1]
La tabla intenta mostrar cómo el elemento referenciado como a[0][0] es
106 CAPÍTULO 5. ARREGLOS
1 segment .data
2 array1 dd 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
3
4 segment .bss
5 array2 resd 10
6
7 segment .text
8 cld ; ¡No olvide esto!
9 mov esi, array1
10 mov edi, array2
11 mov ecx, 10
12 lp:
13 lodsd
14 stosd
15 loop lp
hace que el código trabaje la mayor parte del tiempo (cuando sucede que
la bandera de dirección está en el estado deseado) pero no trabaja todo el
tiempo.
1 segment .bss
2 array resd 10
3
4 segment .text
5 cld ; ¡No olvide esto!
6 mov edi, array
7 mov ecx, 10
8 xor eax, eax
9 rep stosd
en el bucle.
1 segment .bss
2 array resd 100
3
4 segment .text
5 cld
6 mov edi, array ; apuntador al inicio del arreglo
7 mov ecx, 100 ; número de elementos
8 mov eax, 12 ; número a buscar
9 lp:
10 scasd
11 je found
12 loop lp
13 ; código a ejecutar si no se encontró
14 jmp onward
15 found:
16 sub edi, 4 ; edi apunta ahora a 12 en array
17 ; código a ejecutar si se encontró
18 onward:
1 segment .text
2 cld
3 mov esi, block1 ; dirección del primer bloque
4 mov edi, block2 ; dirección del segundo bloque
5 mov ecx, size ; tama~
no del bloque en bytes
6 repe cmpsb ; repita mientras la bandera Z esté fija
7 je equal ; Si Z está fija, los bloque son iguales
8 ; código para ejecutar si los bloques no son igual
9 jmp onward
10 equal:
11 ; código para ejecutar si los bloques son iguales
12 onward:
5.2.5. Ejemplo
Esta sección contiene un archivo fuente en ensamblador con varias fun-
ciones que implementan operaciones con arreglos usando instrucciones de
cadena. Muchas de las funciones duplican las funciones de las bibliotecas
conocidas de C.
memory.asm
1 global _asm_copy, _asm_find, _asm_strlen, _asm_strcpy
2
3 segment .text
4 ; función _asm_copy
5 ; copia un bloque de memoria
114 CAPÍTULO 5. ARREGLOS
6 ; prototipo de C
7 ; void asm_copy( void * dest, const void * src, unsigned sz);
8 ; parámetros:
9 ; dest - apuntador del búfer para copiar a
10 ; src - apuntador del búfer para copiar desde
11 ; sz - número de bytes a copiar
12
23 mov esi, src ; esi = dirección del búfer para copiar desde
24 mov edi, dest ; edi = dirección del búfer para copia a
25 mov ecx, sz ; ecx = número de bytes a copiar
26
30 pop edi
31 pop esi
32 leave
33 ret
34
35
36 ; function _asm_find
37 ; Busca en la memoria un byte dado
38 ; void * asm_find( const void * src, char target, unsigned sz);
39 ; parámetros
40 ; src - apuntador al lugar dónde buscar
41 ; target - valor del byte a buscar
42 ; sz - tama~
no en bytes de dónde se busca
43 ; valor de retorno
44 ; si target se encuentra, apunta a la primera ocurrencia de target
45 ;
46 ; si no
47 ; retorna NULL
5.2. INSTRUCCIONES DE ARREGLOS/CADENAS 115
55 _asm_find:
56 enter 0,0
57 push edi
58
78
79 ; function _asm_strlen
80 ; retorna el tama~no de una cadena
81 ; unsigned asm_strlen( const char * );
82 ; parámetro
83 ; src - apuntador a la cadena
84 ; valor de retorno:
85 ; número de caracteres en la cadena (sin contar el 0 final) (en EAX)
86
90 push edi
91
99 ;
100 ; repnz tendrá un trayecto muy largo, ası́ el tama~
no será
101 ; FFFFFFFE - ECX, y no FFFFFFFF - ECX
102 ;
103 mov eax,0FFFFFFFEh
104 sub eax, ecx ; length = 0FFFFFFFEh - ecx
105
132
memex.c
#include <stdio.h>
void asm copy( void ∗, const void ∗, unsigned ) attribute ((cdecl ));
void ∗ asm find( const void ∗,
char target , unsigned ) attribute ((cdecl ));
unsigned asm strlen( const char ∗ ) attribute ((cdecl ));
void asm strcpy( char ∗, const char ∗ ) attribute ((cdecl ));
int main()
{
char st1 [STR SIZE] = ”test string”;
char st2 [STR SIZE];
char ∗ st ;
char ch;
st1 [0] = 0;
printf (”Digite una Cadena:”);
scanf(” %s”, st1);
printf (”len = %u\n”, asm strlen(st1));
118 CAPÍTULO 5. ARREGLOS
return 0;
}
memex.c
Capı́tulo 6
Punto flotante
Esta idea se puede combinar con los métodos para enteros del Capı́tulo 1
para convertir un número.
0.abcdef . . .
119
120 CAPÍTULO 6. PUNTO FLOTANTE
0,85 × 2 = 1,7
0,7 × 2 = 1,4
0,4 × 2 = 0,8
0,8 × 2 = 1,6
0,6 × 2 = 1,2
0,2 × 2 = 0,4
0,4 × 2 = 0,8
0,8 × 2 = 1,6
Observe que el primer bit está ahora en el lugar del uno. Reemplace a con
0 y obtiene:
0.bcdef . . .
y multiplique por dos otra vez para obtener:
b.cdef . . .
31 30 23 22 0
s e f
63 62 52 51 0
s e f
0 100 0000 0011 0111 1101 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
3
La única diferencia es que para los valores de infinito e indefinido, el exponente sesgado
es 7FF y no FF.
6.2. ARITMÉTICA DE PUNTO FLOTANTE 125
6.2.1. suma
Para sumar dos números de punto flotante, los exponentes deben ser
iguales. Si ellos, no son iguales, entonces se deben hacer iguales, desplazando
la mantisa del número con el exponente más pequeño. Por ejemplo, considere
10,375 + 6,34375 = 16,71875 o en binario:
1,0100110 × 23
+ 1,1001011 × 22
Estos dos números no tienen el mismo exponente ası́ que se desplaza la
mantisa para hacer iguales los exponentes y entonces sumar:
1.0100110 × 23
+ 0.1100110 × 23
10.0001100 × 23
6.2.2. Resta
La resta trabaja muy similar y tiene los mismos problemas que la suma.
Como un ejemplo considere 16,75 − 15,9375 = 0,8125:
Subtraction works very similarly and has the same problems as addition.
As an example, consider 16,75 − 15,9375 = 0,8125:
1,0000110 × 24
− 1,1111111 × 23
1,0100110 × 23
× 1,0100000 × 21
10100110
+ 10100110
1,10011111000000 × 24
Claro está, el resultado real podrı́a ser redondeado a 8 bits para dar:
1,1010000 × 24 = 11010,0002 = 26
4
Una raı́z de una función es un valor x tal que f (x) = 0
6.3. EL COPROCESADOR NUMÉRICO 127
6.3.2. Instrucciones
Para distinguir fácilmente las instrucciones normales de la CPU de las
del coprocesador, todos los nemónicos del coprocesador comienzan por F.
5
Sin embargo el 80486SX no tiene el un coprocesador integrado. Existı́a un chip sepa-
rado para estas máquinas (el 80487SX)
128 CAPÍTULO 6. PUNTO FLOTANTE
Carga y almacenamiento
Hay otras dos instrucciones que pueden mover o quitar datos de la pila
en sı́ misma.
FXCH STn intercambia los valores en ST0 y STn en la pila (donde n es
el número del registro de 1 a 7).
FFREE STn libera un registro en la pila, marcando el registro como no
usado o vacı́o.
6.3. EL COPROCESADOR NUMÉRICO 129
1 segment .bss
2 array resq SIZE
3 sum resq 1
4
5 segment .text
6 mov ecx, SIZE
7 mov esi, array
8 fldz ; ST0 = 0
9 lp:
10 fadd qword [esi] ; ST0 += *(esi)
11 add esi, 8 ; se mueve al próximo dobule
12 loop lp
13 fstp qword sum ; almacena el resultado en sum
Suma y resta
Multiplicación y división
Comparaciones
FCOM fuente compara ST0 y fuente . fuente puede ser un registro del
coprocesador o un float o un double en memoria.
puede ser un FCOMP fuente compara ST0 y fuente , luego sale de la pila. fuente puede ser
un registro del coprocesador o un float o double en memoria.
FCOMPP compara ST0 y ST1, y luego sale de la pila dos veces.
FICOM fuente compara ST0 y (float) fuente . fuente puede ser una pala-
bra o palabra doble en memoria.
FICOMP fuente compara ST0 y (float)fuente , y luego sale de la pila. fuente
puede ser una palabra o palabra doble entera en memoria.
FTST compara ST0 and 0.
Estas instrucciones cambian los bits C0 , C1 , C2 y C3 del registro de
estado del coprocesador. Desafortunadamente no es posible que la CPU ac-
ceda a estos bits directamente. Las instrucciones de salto condicional usan
el registro FLAGS, no el registro de estado del coprocesador. Sin embargo
es relativamente fácil transferir los bits de la palabra de estado a los bits
correspondientes del registro FLAGS usando algunas instrucciones nuevas.
132 CAPÍTULO 6. PUNTO FLOTANTE
1 ; if ( x > y )
2 ;
3 fld qword [x] ; ST0 = x
4 fcomp qword [y] ; compara STO e y
5 fstsw ax ; mueve los bits C a FLAGS
6 sahf
7 jna else_part ; si x no es mayor que y,
8 ; vaya a else_part
9 then_part:
10 ; código para parte de entonces
11 jmp end_if
12 else_part:
13 ; código para parte si no
14 end_if:
Instrucciones miscelaneas
6.3.3. Ejemplos
6.3.4. Fórmula cuadrática
El primer ejemplo muestra cómo se puede programar la fórmula cuadráti-
ca en ensamblador. Recuerde que la fórmula cuadrática calcula la solución
de una ecuación cuadrática:
ax2 + bx + c = 0
quadt.c
#include <stdio.h>
int main()
{
double a,b,c, root1, root2;
quadt.c
quad.asm
1 ; función quadratic
2 ; Halla la solución de la ecuación cuadrática:
3 ; a*x^2 + b*x + c = 0
4 ; Prototipo de C:
5 ; int quadratic( double a, double b, double c,
6 ; double * root1, double *root2 )
7 ; Parámetros:
8 ; a, b, c - Coeficientes de la ecuación cuadrática (ver arriba)
9 ; root1 - Apuntador al double que almacena la primera raı́z
10 ; root2 - Apuntador al double que almacena la segunda raı́z
11 ; Valor de retorno:
12 ; devuelve 1 si las raı́ces son reales si no 0
13
22 segment .data
23 MinusFour dw -4
24
25 segment .text
26 global _quadratic
27 _quadratic:
28 push ebp
29 mov ebp, esp
30 sub esp, 16 ; asigna 2 doubles (disc & one_over_2a)
31 push ebx ; debe guardar el valor original de ebx
32
6.3. EL COPROCESADOR NUMÉRICO 135
69 no_real_solutions:
70 mov eax, 0 ; valor de retorno es 0
71
72 quit:
73 pop ebx
74 mov esp, ebp
136 CAPÍTULO 6. PUNTO FLOTANTE
75 pop ebp
76 ret quad.asm
readt.c
/∗
∗ Este programa prueba el procedimiento en ensamblador de 32 bits read doubles () .
∗ Lee doubles de stdin . (use la redireccióon para leer desde un archivo .)
∗/
#include <stdio.h>
extern int read doubles ( FILE ∗, double ∗, int );
#define MAX 100
int main()
{
int i ,n;
double a[MAX];
readt.c
read.asm
1 segment .data
2 format db "%lf", 0 ; formato para fscanf()
3
4 segment .text
5 global _read_doubles
6 extern _fscanf
7
8 %define SIZEOF_DOUBLE 8
6.3. EL COPROCESADOR NUMÉRICO 137
14 ;
15 ; función _read_doubles
16 ; prototipo de C:
17 ; int read_doubles( FILE * fp, double * arrayp, int array_size );
18 ; Esta función lee dobles de un archivo de texto hasta EOF o hasta
19 ; que se llene el arreglo
20 ; Parámetros:
21 ; fp - apuntador FILE pointer a leer desde (se debe abrir para entrada)
22 ; arrayp - apuntador a un arreglo de doubles para leer en él
23 ; array_size - número de elementos en el arreglo
24 ; Valor de retorno:
25 ; número de doubles almacenado en el arreglo (en EAX)
26
27 _read_doubles:
28 push ebp
29 mov ebp,esp
30 sub esp, SIZEOF_DOUBLE ; define un double en la pila
31
36 while_loop:
37 cmp edx, ARRAY_SIZE ; si edx < ARRAY_SIZE?
38 jnl short quit ; si no, salir del bucle
39 ;
40 ; llama a fscanf() para leer un double en TEMP_DOUBLE
41 ; fscanf() podrı́a cambiar edx y ası́ guardarlo
42 ;
43 push edx ; guarda edx
44 lea eax, TEMP_DOUBLE
45 push eax ; push &TEMP_DOUBLE
46 push dword format ; push &format
47 push FP ; push el apuntador al archivo
48 call _fscanf
49 add esp, 12
50 pop edx ; restaura edx
138 CAPÍTULO 6. PUNTO FLOTANTE
54 ;
55 ; copia TEMP_DOUBLE en ARRAYP[edx]
56 ; (Los ocho bytes del double son copiados por las dos copias de 4 bytes)
57 ;
58 mov eax, [ebp - 8]
59 mov [esi + 8*edx], eax ; primero copia los 4 bytes inferiores
60 mov eax, [ebp - 4]
61 mov [esi + 8*edx + 4], eax ; ahora copia los 4 bytes superiores
62
63 inc edx
64 jmp while_loop
65
66 quit:
67 pop esi ; restaura esi
68
#include <stdio.h>
#include <stdlib.h>
/∗
∗ función find primes
∗ Halla los números primos indicados
∗ Parámetros:
∗ a − arreglo que almacena los primos
∗ n − cuántos primos encontrar
∗/
extern void find primes ( int ∗ a, unsigned n );
int main()
{
int status ;
unsigned i;
unsigned max;
int ∗ a;
if ( a ) {
free (a );
status = 0;
}
else {
fprintf ( stderr , ”No se puede crear el arreglo de %u ints\n”, max);
status = 1;
}
return status ;
}
140 CAPÍTULO 6. PUNTO FLOTANTE
fprime.c
prime2.asm
1 segment .text
2 global _find_primes
3 ;
4 ; function find_primes
5 ; Encuentra el número indicado de primos
6 ; Parámetros:
7 ; array - arreglo que almacena los primos
8 ; n_find - cuántos primos encontrar
9 ; Prototipo de C
10 ;extern void find_primes( int * array, unsigned n_find )
11 ;
12 %define array ebp + 8
13 %define n_find ebp + 12
14 %define n ebp - 4 ; número de primos a encontrar
15 %define isqrt ebp - 8 ; el piso de la raı́z cudrada de guess
16 %define orig_cntl_wd ebp - 10 ; palabra original de control
17 %define new_cntl_wd ebp - 12 ; nueva palabra de control
18
19 _find_primes:
20 enter 12,0 ; Hace espacio para las variables locales
21
44 while_limit:
45 mov eax, [n]
46 cmp eax, [n_find] ; while ( n < n_find )
47 jnb short quit_limit
48
56 ; Este b1ucle interno divide guess por los números primos ya encontrados
57 ; hasta que encuentre un factor primo de guess (que significa que guess
58 ; no es primo) o hasta que número primo a dividir sea más grande que
59 ; floor(sqrt(guess))
60 ;
61 while_factor:
62 mov eax, dword [esi + 4*ecx] ; eax = array[ecx]
63 cmp eax, [isqrt] ; while ( isqrt < array[ecx]
64 jnbe short quit_factor_prime
65 mov eax, ebx
66 xor edx, edx
67 div dword [esi + 4*ecx]
68 or edx, edx ; && guess % array[ecx] != 0 )
69 jz short quit_factor_not_prime
70 inc ecx ; intenta el próximo primo
71 jmp short while_factor
72
73 ;
74 ; found a new prime !
75 ;
76 quit_factor_prime:
77 mov eax, [n]
78 mov dword [esi + 4*eax], ebx ; suma al final de arreglo
79 inc eax
80 mov [n], eax ; inc n
142 CAPÍTULO 6. PUNTO FLOTANTE
81
82 quit_factor_not_prime:
83 add ebx, 2 ; intenta con el impar siguiente
84 jmp short while_limit
85
86 quit_limit:
87
92 leave
93 ret prime2.asm
6.3. EL COPROCESADOR NUMÉRICO 143
1 global _dmax
2
3 segment .text
4 ; función _dmax
5 ; retorna el mayor de dos argumentos double
6 ; Prototipo de C
7 ; double dmax( double d1, double d2 )
8 ; Parámetros:
9 ; d1 - primer double
10 ; d2 - segundo double
11 ; Valor de retorno:
12 ; El mayor de d1 y d2 (en ST0)
13 %define d1 ebp+8
14 %define d2 ebp+16
15 _dmax:
16 enter 0, 0
17
1 segment .data
2 x dq 2.75 ; convertido a formato double
3 five dw 5
4
5 segment .text
6 fild dword [five] ; ST0 = 5
7 fld qword [x] ; ST0 = 2.75, ST1 = 5
8 fscale ; ST0 = 2.75 * 32, ST1 = 5
Estructuras y C++
7.1. Estructuras
7.1.1. Introducción
Las estructuras se usan en C para agrupar datos relacionados en una
variable compuesta. Esta técnica tiene varias ventajas:
145
146 CAPÍTULO 7. ESTRUCTURAS Y C++
Offset Element
0 x
2
y
6
Offset Element
0 x
2 unused
4
y
8
struct S {
short int x; /∗ entero de 2 bytes ∗/
int y; /∗ entero de 4 bytes ∗/
double z; /∗ float de 8 bytes ∗/
} attribute ((packed));
struct S {
short int x; /∗ entero de 2 bytes ∗/
int y; /∗ entero de 4 bytes ∗/
double z; /∗ float de 8 bytes ∗/
};
#pragma pack(1)
La directiva anterior le dice al compilador que embale los elementos de es-
tructuras en los lı́mites de los bytes (sin relleno extra). El uno se puede
reemplazar con dos, cuatro, ocho o dieciséis para especificar el alineamiento
en una palabra, palabra doble, palabra cuádruple y párrafo respectivamen-
te. Esta directiva tiene efecto hasta que se sobreescriba por otra directiva.
Esto puede causar problemas ya que estas directivas se usan a menudo en
los archivos de encabezamiento. Si el archivo de encabezamiento se incluye
antes que otros archivos de encabezamiento con estructuras, esas estructuras
pueden estar diferente que las que tendrı́an por omisión. Esto puede condu-
cir un error muy difı́cil de encontrar. Los módulos de un programa podrı́an
desordenar los elementos de las estructuras de otros módulos.
Hay una manera de evitar este problema. Microsoft y Borland tienen
una manera de guardar el alineamiento y restaurarlo luego. La Figura 7.4
muestra cómo se harı́a esto.
struct S {
unsigned f1 : 3; /∗ campo de 3 bits ∗/
unsigned f2 : 10; /∗ campo de 10 bits ∗/
unsigned f3 : 11; /∗ campo de 11 bits ∗/
unsigned f4 : 8; /∗ campo de 8 bits ∗/
};
Byte \ Bit 7 6 5 4 3 2 1 0
0 Codigo de operación (08h)
1 Unidad Lógica # msb de LBA
2 mitad de la dirección del bloque lógico
3 lsb de dirección del bloque lógico
4 Longitud de transferencia
5 Control
campos de bits es la dirección del bloque lógico (LBA) que abarca 3 bytes
diferentes en la orden. De la Figura 7.6 uno ve que el dato se almacena en
el formato big endian. La Figura 7.7 muestra una definición que intenta tra-
bajar con todos los compiladores. Las dos primeras lı́neas definen un macro
que retorna verdadero si el código se compila con los compiladores de Bor-
land o Microsoft. La parte potencialmente confusa está en las lı́neas 11 a
14. Primero uno podrı́a sorprenderse de ¿Por qué los bits lba mid y lba lsb
están definidos separados y no en un solo campo de 16 bits? La razón es
que el dato está en big endian. Un campo de 16 bits podrı́a ser almacenado
con el orden little endian por el compilador. Ahora, los campos lba msb y
logical unit parecen invertidos; sin embargo esto no es el caso. Ellos tie-
nen que colocarse en este orden. La Figura 7.8 muestra cómo se colocan los
campos en una entidad de 48 bits (los lı́mites del byte están una vez más
marcados con lı́neas dobles). Cuando esto se almacena en memoria en el
orden little endian, los bits se colocan en el orden deseado.(Figura 7.6)
Para complicar las cosas más, la definición para SCSI read cmd no tra-
baja correctamente en el compilador de Microsoft. Si se evalúa la expresión
sizeof (SCSI read cmpl), Microsoft C retornará 8 no 6. Esto es porque el com-
pilador de Microsoft usa el tipo de campo de datos para determinar cómo
colocar los bits. Ya que los bits están definidos como de tipo unsigned, el
compilador rellena dos bytes al final de la estructura para tener un múlti-
plo de palabras dobles. Esto se puede solucionar haciendo todos los campos
unsigned short.
Ahora el compilador de Microsoft no necesita agregar ningún relleno ya
que los 6 bytes son un múltiplo de una palabra4 Los otros compiladores
también trabajarán correctamente con este cambio. La Figura 7.9 muestra
otra definición que trabaja con los tres compiladores. Se evitan todos los
problemas usando campos de tipo unsigned char.
El lector no deberı́a entristecerse si encuentra la división anterior con-
fusa. ¡Es confusa! El autor a menudo encuentra menos confuso evitar los
4
Mezclar diferentes tipos de campos de bits produce un comportamiento muy confuso.
Se invita al lector a experimentar al respecto
7.1. ESTRUCTURAS 151
#if MS OR BORLAND
# pragma pack(push)
# pragma pack(1)
#endif
#if MS OR BORLAND
# pragma pack(pop)
#endif
campos de bits y en su lugar usar las operaciones entre bits para examinar
y modificar los bits manualmente.
1 %define y_offset 4
2 _zero_y:
3 enter 0,0
4 mov eax, [ebp + 8] ; toma s_p (apuntador a la estructura)
5 ; de la pila
6 mov dword [eax + y_offset], 0
7 leave
8 ret
C le permite a uno pasar estructuras por valor a una función; sin embargo
esto la mayorı́a de las veces es mala idea. Cuando se pasa por valor, todos
los datos de la estructura se deben copiar en la pila y luego traı́dos por la
rutina, En lugar de esto es mucho más eficiente pasar un apuntador a una
estructura.
C También permite que un tipo de estructura se use como un valor de
retorno. Obviamente la estructura no se puede retornar en el registro EAX.
Cada compilador maneja la situación diferente. Una solución común es que
el compilador usa esto para internamente reescribir la función para que tome
una apuntador a una estructura como parámetro. El apuntador se usa para
7.2. ENSAMBLADOR Y C++ 153
#include <stdio.h>
void f ( int x )
{
printf (” %d\n”, x);
}
void f ( double x )
{
printf (” %g\n”, x);
}
a función, él busca una función donde emparejen los tipos de los argumentos
pasados a la función.5 Si se empareja entonces crea un CALL a la función
correcta usando las reglas de manipulación del compilador.
Ya que cada compilador usa sus reglas de manipulación de nombres, los
archivos objeto de C++ generados por diferentes compiladores no se pueden
unir, el código de C++ compilado con compiladores diferentes no es posible
unirlo. Este hecho es importante cuando se considera usar una biblioteca
de C++ precompilada. Si uno desea escribir una función en ensamblador
que será usada con código C++, debe conocer las reglas de manipulación
de nombres del compilador (o usar la técnica explicada abajo).
El estudiante astuto puede preguntar si el código de la Figura 7.10 tra-
bajará como se espera. Ya que C++ manipula los nombres de todas las
funciones, entonces la función printf será manipulada y el compilador no
producirá un CALL a la etiqueta printf. Esto es un válido. Si el prototipo
para printf fue colocado simplemente al principio del archivo, esto podrı́a
pasar. El prototipo es:
int printf ( const char ∗, ...);
DJGPP podrı́a manipular esto como printf FPCce (la F es por función, P
por apuntador, C por constante, c por char y e por elipsis). Esto podrı́a no
llamar la función printf de la biblioteca de C. Claro está, debe haber alguna
forma para el código de C++ llamar código de C. Esto es muy importante
ya que hay una gran cantidad de código de C útil. Además permitirle a uno
llamar código de C heredado, C++ también le permite a uno llamar código
de ensamblador usando las convenciones normales de llamado.
C++ extiende la palabra clave extern para permitir especificar que la
función o variable global que modifica usa las convenciones de llamado de
C. En la terminologı́a de C++, las variables y funciones globales usan el
encadenamiento de C. Por ejemplo para declarar que printf tenga el enca-
denamiento de C, use el prototipo:
extern ”C” int printf ( const char ∗, ... );
Esto le dice al compilador que no use las reglas de manipulación de nombres
en esta función, y en su lugar usar las reglas de C. Sin embargo, haciendo
esto, la función printf no se puede sobrecargar. Esto suministra una manera
fácil de interfazar C++ y ensamblador, define la función para que use el
encadenamiento de C y la convención de llamado de C.
Por conveniencia C++ también permite el encadenamiento de un bloque
de funciones y variables globales a ser definidas. El bloque se enmarca por
las tı́picas llaves:
5
El emparejamiento no tiene que ser exacto, el compilador considerará las conversiones
de tipos de los argumentos. Las reglas para este proceso están más allá del alcance de este
libro. Consulte un libro de C++ para los detalles.
156 CAPÍTULO 7. ESTRUCTURAS Y C++
int main()
{
int y = 5;
f (y ); // se pasa la referencia a y, obeserve que
// no hay & acá
printf (” %d\n”, y); // imprime 6!
return 0;
}
extern ”C” {
/∗ encadenamiento tipo C a variables globales y prototipos de funciones ∗/
}
Si uno examina los archivos de encabezado de ANSI C que vienen con
los compiladores de C/C++ de hoy dı́a, en ellos se encontrá al principio de
cada archivo de cabecera:
#ifdef cplusplus
extern ”C” {
#endif
Y una construcción parecida cerca del final conteniendo el fin de la llave.
Los compiladores de C++ definen el macro cplusplus (con dos guiones
bajos adelante). La porción de código anterior encierra todo el archivo de
cabecera en un bloque extern C si el archivo de cabecera es compilado como
C++, pero no hace nada si es compilado como C (ya que el compilador de
C darı́a un error de sintaxis para extern C). Esta misma técnica puede ser
usada por cualquier programador para crear un archivo de cabecera para las
rutinas de ensamblador que pueden ser usadas con C o C++.
7.2.2. Referencias
Las referencias son otra caracterı́stica nueva C++. Ellas le permiten a
uno pasar parámetros a funciones sin usar apuntadores explı́citamente. Por
ejemplo, considere el código de la Figura 7.11. Los parámetros por referencia
son muy simples, ellos realmente son tan solo apuntadores. El compilador
solo le oculta esto al programador (tal como los compiladores de Pascal
implementan los parámetros var como apuntadores). Cuando el compilador
genera el ensamblador para el llamado a la función en la lı́nea 7, pasa la
7.2. ENSAMBLADOR Y C++ 157
int f ( int x )
{ return x∗x; }
int main()
{
int y, x = 5;
y = f(x );
y = inline f (x );
return 0;
}
En este caso hay dos ventajas de las funciones inline. Primero la función
en lı́nea es rápida. No se colocan los parámetros en la pila, no se crea el
marco de la pila ni se destruye, no se hace el salto. Segundo el llamado de
las funciones en lı́nea usa menos código. Este último punto es verdad para
este ejemplo pero no siempre es cierto.
La principal desventaja de las funciones inline es que el código en lı́nea
no se encadena y ası́ el código de una función en lı́nea debe estar disponible
7.2. ENSAMBLADOR Y C++ 159
class Simple {
public:
Simple (); // Construcctor por omisión
˜Simple(); // destructor
int get data () const; // funciones miembro
void set data ( int );
private :
int data; // datos miembro
};
Simple :: Simple()
{ data = 0; }
en todos los archivos que la usen. El ejemplo anterior prueba esto. El llama-
do de una función normal solo requiere el conocimiento de los parámetros,
el tipo de valor de retorno, la convención de llamado y el nombre de la eti-
queta para la función. Toda esta información está disponible en el prototipo
de la función. Sin embargo al usar funciones inline se requiere conocer todo
el código de la función. Esto significa que si cualquier parte de una función
en lı́nea se cambia, todos los archivos fuentes que usen la función deben
ser recompilados. Recuerde que para las funciones no inline, si el prototipo
cambia, a menudo los archivos que usan la función no necesitan ser recompi-
lados. Por todas estas razones, el código de las funciones en lı́nea se colocan
normalmente en los archivos de cabecera. Esta práctica es contraria a la
regla estricta de C las instrucciones de código ejecutable nunca se colocan
en los archivos de cabecera.
160 CAPÍTULO 7. ESTRUCTURAS Y C++
9 leave
10 ret
7.2.4. Clases
Una clase en C++ describe un tipo de objeto. Un objeto tiene como
miembros a datos y funciones8 . En otras palabras, es una estructura con
datos y funciones asociadas a ella. Considere la clase definida en la Figu-
ra 7.13. Una variable de tipo Simple se podrı́a ver tal como una estructura
En efecto, C++ usa la pa- normal en C con un solo int como miembro. Las funciones no son alma-
labra clave this para acce- cenadas en la memoria asignada a la estructura. Sin embargo las funciones
der al apuntador de un ob- miembro son diferentes de las otras funciones . Ellas son pasadas como un
jeto que está dentro de la parámetro oculto. Este parámetro es un apuntador al objeto al cual la fun-
función miembro.
ción pertenece.
Por ejemplo considere el método set data de la clase Simple de la Fi-
gura 7.13. Si se escribió en C se verı́a como una función que fue pasada
explı́citamente a un apuntador al objeto como muestra la Figura 7.14. La
opción -S en el compilador DJGPP (y en los compiladores gcc y Borland
también) le dice al compilador que genere un archivo que contenga el len-
guaje ensamblador equivalente del código producido. Para DJGPP y gcc
el archivo ensamblador finaliza con una extensión .s y desafortunadamen-
te usa la sintaxis del lenguaje ensamblador AT&T que es diferente de la
8
a menudo llamados en C++ funciones miembro o más generalmente métodos.
7.2. ENSAMBLADOR Y C++ 161
Ejemplo
Esta sección usa las ideas del capı́tulo para crear una clase de C++
que representa un entero sin signo de tamaño arbitrario. Ya que el entero
puede ser de cualquier tamaño, será almacenado en un arreglo de enteros
sin signo (palabras dobles). Se puede hacer de cualquier tamaño usando
memoria dinámica. Las palabras dobles se almacenan en orden inverso.11 (la
palabra doble menos significativa es la palabra doble que está en la posición
0). La Figura 7.16 muestra la definición de la clase Big int.12 . El tamaño
de un Big int se mide por el tamaño del arreglo sin signo que es usado
para almacenar este dato. El dato miembro size de la clase se le asigna un
desplazamiento de cero y el miembro number se le asigna un desplazamiento
de 4.
9
El sistema de compilador gcc incluye su propio ensamblador llamado gas. El ensam-
blador gas usa la sintaxis AT&T y ası́ el compilador produce el código en el formato
de gas. Hay varias páginas web que discuten las diferencias entre los formatos INTEL y
AT&T. Hay también un programa mı́nimo llamado a2i (http://www.multimania.com/
placr/a2i.html), que convierte del formato AT&T al formato NASM
10
Como es normal nada está oculto en el código ensamblador
11
¿Por qué? Porque las operaciones de adición siempre se comenzarán a procesar al
inicio del arreglo y se moverá hacia adelante.
12
Vea los archivos example para el código completo de este ejemplo. El texto solo se
referirá a alguna parte del código.
162 CAPÍTULO 7. ESTRUCTURAS Y C++
const Big int & operator = ( const Big int & big int to copy );
friend Big int operator + ( const Big int & op1,
const Big int & op2 );
friend Big int operator − ( const Big int & op1,
const Big int & op2);
friend bool operator == ( const Big int & op1,
const Big int & op2 );
friend bool operator < ( const Big int & op1,
const Big int & op2);
friend ostream & operator << ( ostream & os,
const Big int & op );
private :
size t size ; // tamaño del arreglo sin signo
unsigned ∗ number ; // apuntador al arreglo sin singno que
// almacena el valor
};
inline Big int operator + ( const Big int & op1, const Big int & op2)
{
Big int result (op1. size ());
int res = add big ints ( result , op1, op2);
if ( res == 1)
throw Big int :: Overflow ();
if ( res == 2)
throw Big int :: Size mismatch();
return result ;
}
inline Big int operator − ( const Big int & op1, const Big int & op2)
{
Big int result (op1. size ());
int res = sub big ints ( result , op1, op2);
if ( res == 1)
throw Big int :: Overflow ();
if ( res == 2)
throw Big int :: Size mismatch();
return result ;
}
big math.asm
1 segment .text
2 global add_big_ints, sub_big_ints
3 %define size_offset 0
4 %define number_offset 4
5
6 %define EXIT_OK 0
7 %define EXIT_OVERFLOW 1
8 %define EXIT_SIZE_MISMATCH 2
9
14
15 add_big_ints:
16 push ebp
17 mov ebp, esp
18 push ebx
19 push esi
20 push edi
21 ;
22 ; primero esi se~
nala a op1
23 ; edi se~
nala a op2
24 ; ebx se~
nala a res
25 mov esi, [op1]
26 mov edi, [op2]
27 mov ebx, [res]
28 ;
29 ; Asegurarse que todos los 3 Big_Int tienen el mismo tama~
no
30 ;
31 mov eax, [esi + size_offset]
32 cmp eax, [edi + size_offset]
33 jne sizes_not_equal ; op1.size_ != op2.size_
34 cmp eax, [ebx + size_offset]
35 jne sizes_not_equal ; op1.size_ != res.size_
36
59 jc overflow
60 ok_done:
61 xor eax, eax ; valor de retorno = EXIT_OK
62 jmp done
63 overflow:
64 mov eax, EXIT_OVERFLOW
65 jmp done
66 sizes_not_equal:
67 mov eax, EXIT_SIZE_MISMATCH
68 done:
69 pop edi
70 pop esi
71 pop ebx
72 leave
73 ret big math.asm
Las lı́neas 25 a 27 almacenan los apuntadores a los objetos Big int
pasados a la función en registro. Recuerde que las referencias realmente
son solo apuntadores. Las lı́neas 31 a 35 aseguran que los tamaños de los 3
objetos de los arreglos sean iguales (Observe que el desplazamiento de size
se añade al apuntador para acceder al dato miembro). Las lı́neas 44 a 46
ajustan los registros para apuntar al arreglo usado por los objetos respectivos
en lugar de los objetos en sı́ mismos. (Una vez más, el desplazamiento de
number se añade al apuntador del objeto).
El bucle en las lı́neas 52 a 57 suma los enteros almacenados en los arreglos
sumando primero la palabra doble menos significativa, luego la siguiente
palabra doble y ası́ sucesivamente. La suma debe realizarse en esta secuencia
para la aritmética de precisión extendida (vea la Sección 2.1.5). La lı́nea
59 examina el desborde, si hay desborde la bandera de carry se deberı́a
fijar con la última suma de la palabra doble más significativa. Ya que las
palabras dobles en el arreglo se almacenan en el orden de little endian, el
bucle comienza en el inicio del arreglo y se mueve adelante hasta el final.
La Figura 7.18 muestra un corto ejemplo del uso de la clase Big int. Ob-
serve que las constantes Big int se deben declarar explı́citamente como en
la lı́nea 16. Esto es necesario por dos razones. Primero, no hay un construcc-
tor de conversión que convierta un unsigned int en un Big int. Segundo,
solo se pueden sumar Big Int del mismo tamaño, esto hace la conversión
problemática ya que podrı́a ser difı́cil conocer a qué tamaño convertir. Una
implementación más sofisticada de la clase deberı́a permitir sumar objetos
de cualquier tamaño. El autor no desea complicar este ejemplo implemen-
7.2. ENSAMBLADOR Y C++ 167
int main()
{
try {
Big int b(5,”8000000000000a00b”);
Big int a(5,”80000000000010230”);
Big int c = a + b;
cout << a << ” + ” << b << ” = ” << c << endl;
for( int i =0; i < 2; i ++ ) {
c = c + a;
cout << ”c = ” << c << endl;
}
cout << ”c−1 = ” << c − Big int(5,1) << endl;
Big int d(5, ”12345678”);
cout << ”d = ” << d << endl;
cout << ”c == d ” << (c == d) << endl;
cout << ”c > d ” << (c > d) << endl;
}
catch( const char ∗ str ) {
cerr << ”Caught: ” << str << endl;
}
catch( Big int :: Overflow ) {
cerr << ”Overflow” << endl;
}
catch( Big int :: Size mismatch ) {
cerr << ”Size mismatch” << endl;
}
return 0;
}
Tama~
no de a: 4 Desplazamiento de ad: 0
Tama~
no de b: 8 Desplazamiento de ad: 0 Desplazamiento de bd: 4
A::m()
A::m()
Tama~
no de a: 8 Desplazamiento de ad: 4
Tama~
no de b: 12 Desplazamiento de ad: 4 Desplazamiento de bd: 8
A::m()
B::m()
#include <cstddef>
#include <iostream>
using namespace std;
class A {
public:
void cdecl m() { cout << ”A::m()” << endl; }
int ad;
};
class B : public A {
public:
void cdecl m() { cout << ”B::m()” << endl; }
int bd;
};
void f ( A ∗ p )
{
p−>ad = 5;
p−em();
}
int main()
{
A a;
B b;
cout << ”Tamaño de a: ” << sizeof(a)
<< ” Desplazamiento de ad: ” << offsetof(A,ad) << endl;
cout << ”Tamaño de b: ” << sizeof(b)
<< ” Desplazamiento de ad: ” << offsetof(B,ad)
<< ” Desplazamiento de bd: ” << offsetof(B,bd) << endl;
f(&a);
f(&b);
return 0;
}
class A {
public:
virtual void cdecl m() { cout << ”A::m()” << endl; }
int ad;
};
class B : public A {
public:
virtual void cdecl m() { cout << ”B::m()” << endl; }
int bd;
};
1 ?f@@YAXPAVA@@@Z:
2 push ebp
3 mov ebp, esp
4
15 pop ebp
16 ret
13
Para las clases sin métodos virtuales los compiladores de C++ siempre hacen la clase
compatible con una estructura normal de C con los mismos miembros.
14
Claro está, este valor ya está en el registro ECX él fue colocado en la lı́nea 8 y la lı́nea
10 podı́a ser quitado y la próxima lı́nea cambiada para empujar ECX. El código no es muy
eficiente porque se genera sin tener activadas las optimizaciones del compilador.
172 CAPÍTULO 7. ESTRUCTURAS Y C++
class A {
public:
virtual void cdecl m1() { cout << ”A::m1()” << endl; }
virtual void cdecl m2() { cout << ”A::m2()” << endl; }
int ad;
};
int main()
{
A a; B b1; B b2;
cout << ”a: ” << endl; print vtable (&a);
cout << ”b1: ” << endl; print vtable (&b);
cout << ”b2: ” << endl; print vtable (&b2);
return 0;
}
a:
vtable address = 004120E8
dword 0: 00401320
dword 1: 00401350
A::m1()
A::m2()
b1:
vtable address = 004120F0
dword 0: 004013A0
dword 1: 00401350
B::m1()
A::m2()
b2:
vtable address = 004120F0
dword 0: 004013A0
dword 1: 00401350
B::m1()
A::m2()
R registro general
R8 registro de 8 bits
R16 registro de 16 bits
R32 registro de 32 bits
SR registro de segmento
M memoria
M8 byte
M16 palabra
M32 palabra doble
I valor inmediato
Las abreviaciones se pueden combinar para las instrucciones con varios ope-
randos. Por ejemplo el formato R,R significa que la instrucción toma dos
operandos de registro. Muchas de las instrucciones con dos operandos per-
miten los mismos operandos. La abreviación O2 se usa para representar
los siguientes operandos: R,R R,M R,I M,R M,I. Si se puede usar como
operando un registro de 8 bits o la memoria, se usa la abreviación R/M8
La tabla también muestra cómo afectan las instrucciones varias de las
banderas del registro FLAGS. Si la columna está en vacı́a, el bit correspon-
diente no se afecta. Si el bit siempre cambia a algún valor en particular, se
muestra un 1 o un 0 en la columna. Si el bit cambia a un valor que depende
del operando de la instrucción se coloca una C en la columna. Finalmen-
te, si el bit es modificado de alguna manera no definida se coloca un ? en
175
176 APÉNDICE A. INSTRUCCIONES DEL 80X86
Banderas
Nombre Descripción Formatos O S Z A P C
ADC Suma con carry O2 C C C C C C
ADD Suma enteros O2 C C C C C C
AND AND entre bits O2 0 C C ? C 0
BSWAP Byte Swap R32
CALL Llamado a rutina RMI
CBW Convierta byte a pala-
bra
CDQ Convierte Dword a
Qword
CLC Borra el Carry 0
CLD borra la bandera de di-
rección
CMC Complementa el carry C
CMP Compara enteros O2 C C C C C C
CMPSB Compara Bytes C C C C C C
CMPSW Compara Words C C C C C C
CMPSD Compara Dwords C C C C C C
CWD Convierte Word a
Dword en DX:AX
CWDE Convierte Word a
Dword en EAX
DEC Decrementa entero RM C C C C C
DIV División sin signo RM ? ? ? ? ? ?
ENTER Hace el marco de la pi- I,0
la
IDIV División con signo RM ? ? ? ? ? ?
IMUL Multiplicación con R M C ? ? ? ? C
signo R16,R/M16
R32,R/M32
R16,I
R32,I
R16,R/M16,I
R32,R/M32,I
INC Incremento entero RM C C C C C
INT Genera una interrup- I
ción
JA Salta si está sobre I
A.1. INSTRUCCIONES PARA ENTEROS 177
Banderas
Nombre Descripción Formatos O S Z A P C
JAE Salta si está sobre o es I
igual
JB Salta si está bajo I
JBE Salta si está bajo o es I
igual
JC Salta si hay carry I
JCXZ Salta si CX = 0 I
JE Salta si es igual I
JG Salta si es mayor I
JGE Salta si es mayor o I
igual
JL Salta si es menor I
JLE Salta si es menor o I
igual
JMP Salto incondicional RMI
JNA Salta si no está sobre I
JNAE Salta si no está sobre o I
es igual
JNB Salta si no está bajo i
JNBE Salta si no está bajo o I
es igual
JNC Salta si no hay carry I
JNE Salta si no es igual I
JNG Salta si no es mayor I
JNGE Salta si no es mayor o I
es igual
JNL Salta si no es menor I
JNLE Salta si no es menor o I
igual
JNO Salta si no hay desbor- I
de
JNS Salta si es positivo I
JNZ Salta si no es cero I
JO Salta si hay desborde I
JPE Salta si hay paridad I
par
JPO Salta si hay paridad I
impar
JS Salta si hay signo I
JZ Salta si hay cero I
178 APÉNDICE A. INSTRUCCIONES DEL 80X86
Banderas
Nombre Descripción Formatos O S Z A P C
LAHF Carga FLAGS en AH
LEA Carga dirección efecti- R32,M
va
LEAVE Abandona el marco de
la pila
LODSB Carga byte
LODSW Carga palabra
LODSD Carga palabra doble
LOOP Bucle I
LOOPE/LOOPZ Bucle si es igual I
LOOPNE/LOOPNZ Bucle si no es igual I
MOV Mueve datos O2
SR,R/M16
R/M16,SR
MOVSB Mueve byte
MOVSW Mueve palabra
MOVSD Mueve palabra doble
MOVSX Mueve con signo R16,R/M8
R32,R/M8
R32,R/M16
MOVZX Mueve sin signo R16,R/M8
R32,R/M8
R32,R/M16
MUL Multiplicación sin RM C ? ? ? ? C
signo
NEG Negación RM C C C C C C
NOP No Opera
NOT Complemento a 1 RM
OR OR entre bits O2 0 C C ? C 0
POP Saca de la pila R/M16
R/M32
POPA Saca todo de la pila
POPF Saca FLAGS C C C C C C
PUSH Empuja en la pila R/M16
R/M32 I
PUSHA Empuja todo en la pila
PUSHF Empuja FLAGS
RCL Rota a la izquierda con R/M,I C C
carry R/M,CL
A.1. INSTRUCCIONES PARA ENTEROS 179
Banderas
Nombre Descripción Formatos O S Z A P C
RCR Rota a la derecha con R/M,I C C
carry R/M,CL
REP Repite
REPE/REPZ Repite si es igual
REPNE/REPNZ Repite si no es igual
RET Retorno
ROL Rota a la izquierda R/M,I C C
R/M,CL
ROR Rota a la derecha R/M,I C C
R/M,CL
SAHF Copia AH en FLAGS C C C C C
SAL Dezplazamiento a la R/M,I C
izquierda R/M, CL
SBB Resta con préstamo O2 C C C C C C
SCASB busca un Byte C C C C C C
SCASW Busca una palabra C C C C C C
SCASD Busca una palabra do- C C C C C C
ble
SETA fija sobre R/M8
SETAE fija sobre o igual R/M8
SETB fija bajo R/M8
SETBE fija bajo o igual R/M8
SETC fija el carry R/M8
SETE fija igual R/M8
SETG fija mayor R/M8
SETGE fija mayor o igual R/M8
SETL fija menor R/M8
SETLE fija menor o igual R/M8
SETNA fija no sobre R/M8
SETNAE fija no sobre o igual R/M8
SETNB fija no bajo R/M8
SETNBE fija no bajo o igual R/M8
SETNC fija no carry R/M8
SETNE fija no igual R/M8
SETNG fija no mayor R/M8
SETNGE fija no mayor o igual R/M8
SETNL fija no menor R/M8
SETNLE fijan no menor o igual R/M8
SETNO fija no desborde R/M8
SETNS fija no signo R/M8
180 APÉNDICE A. INSTRUCCIONES DEL 80X86
Banderas
Nombre Descripción Formatos O S Z A P C
SETNZ fija no cero R/M8
SETO fija desborde R/M8
SETPE fija paridad par R/M8
SETPO fija paridad impar R/M8
SETS fija signo R/M8
SETZ fija cero R/M8
SAR Desplazamiento R/M,I C
aritmético a la de- R/M, CL
recha
SHR Desplazamiento lógico R/M,I C
a la derecha R/M, CL
SHL Desplazamiento lógico R/M,I C
a la izquierda R/M, CL
STC fija el carry 1
STD fija la bandera de di-
rección
STOSB almacena byte
STOSW almacena palabra
STOSD almacena palabra do-
ble
SUB resta O2 C C C C C C
TEST comparación lógica R/M,R 0 C C ? C 0
R/M,I
XCHG Intercambio R/M,R
R,R/M
XOR XOR entre bits O2 0 C C ? C 0
A.2. INSTRUCCIONES DE PUNTO FLOTANTE 181
183
184 ÍNDICE ALFABÉTICO
NASM, 12 RCL, 51
NEG, 58 RCR, 51
nemónico, 12 read.asm, 138
nibble, 4 recursión, 91–94
NOT, 54 registro, 5, 7–8
ı́ndice, 7
operaciones con bits 32-bit, 8
AND, 52 apuntador a la pila, 7, 8
C, 58–60 apuntador base, 7, 8
desplazamientos, 49–52 EDI, 109
desplazamiento lógicos, 50 EDX:EAX, 36, 39, 86
desplazamientos aritméticos, 50– EFLAGS, 8
51 EIP, 8
desplazamientos lógicos, 49 ESI, 109
rotaciones, 51 FLAGS, 8, 40
ÍNDICE ALFABÉTICO 187