0-ensamblador y c
DESCRIPTION
ProgramaciónTRANSCRIPT
Lenguajes: Ensamblador y CUn enfoque conjunto
CAPITULO ICONCEPTOS
Conceptos relacionados con la arquitectura Intel
IntroducciónAunque Intel ha realizado cambios substanciales en la arquitectura de sus
procesadores desde que apareció la primera PC, la forma de programar se ha
mantenido con el paso del tiempo. En la actualidad, las actuales computadoras
incluyen en su repertorio, instrucciones que se usaban en los procesadores de
antaño. El objetivo de realizar tal acción era mantener su base establecida de
clientes programadores. Con las necesidades cada vez más demandantes de
velocidad de sus clientes, Intel decidió construir arquitecturas más sofisticadas,
aunque tenía un objetivo en mente: mantener el repertorio de instrucciones
originales. Con el paso del tiempo, esta forma de escalar sus productos le
proporcionó un beneficio económico (a expensas de construir un conjunto de
instrucciones que resultaban cada vez más difíciles de programar).
Analizar algunos componentes de la arquitectura de los procesadores Intel permitirá
introducir los conceptos básicos utilizados en la programación de bajo nivel.
El Modelo de programación en los microprocesadores IntelLos primeros microprocesadores Intel consistían de arquitecturas basadas en 16-
bits. A partir del microprocesador 80386 las arquitecturas fueron expandidas a 32
bits conservando una parte de la antigua estructura de 16 bits. Esto se puede
apreciar mejor si analizamos los registros de propósito general del microprocesador
Representación interna en los
registros de propósito general
en los microprocesadores
Intel
Los registros de propósito general (y los cuales serán tratados a mayor detalle
posteriormente), son de gran interés debido a que son las piezas de programación
más valiosa para el desarrollo de software de bajo nivel. En ellos se muestra la
evolución de las arquitecturas de 16 bits a 32 bits.
La Jerarquía de memoriaExisten tres hechos que limitan el uso de memoria en una computadora:
a) Velocidad
b) Costo
c) Cantidad
Si pudiéramos construir una computadora con únicamente registros, el coste de esta
resultaría prohibitivo. Por otro lado, si requiriéramos almacenar una gran cantidad
de información, una memoria veloz y volátil no serviría a tal propósito.
Un factor conveniente es que la memoria más veloz con la que contamos realice sus
operaciones a la misma velocidad que el procesador. Por todas estas cuestiones, es
necesario clasificar las memorias existentes en un equipo de cómputo.
La jerarquía de memoria propone la forma de su uso. En esta forma de uso de la
memoria, se tiene acceso a diferentes niveles y el nivel 1 son los registros internos
del procesador. Si los registros no contienen la información necesaria que el
procesador necesita, se recurre a los recursos de la memoria cache. Por otro lado, si
la memoria cache no contiene la información que el procesador necesita, se extrae la
información de la memoria principal.
Como ejemplo, si se tiene un procesador el cual puede acceder a dos niveles de
memoria y aunque el primer nivel de memoria es mucho más veloz que el segundo,
se cuenta en menor cantidad. Si la información deseada se encuentra en el primer
nivel, esta será accedida de manera directa, pero si se encuentra únicamente en el
segundo nivel, esta información debe ser trasladada al primer nivel antes de poder
ser usada.
El principio de cercanía de referencias establece que se logra un mejor rendimiento
del procesador, si se disminuye la frecuencia de acceso a la memoria [Denn68].
Para explotar de una manera adecuada el principio de cercanía, se debe tener
cuidado que tanto los datos como las instrucciones se encuentren agrupadas. La
buena estructuración de programas genera código eficiente. El uso de ciclos
optimiza la búsqueda de instrucciones en memoria. De igual forma los vectores y
arreglos optimizan la búsqueda de datos. Esto es cierto debido al principio de
cercanía de referencias. Por tal motivo, es necesario mencionar que un buen estilo
de programación propicia la creación de programas más eficientes y veloces. El
objetivo es lograr que en promedio, el mayor número de referencias de
instrucciones y datos se realice a nivel 1.
Los registros internos, la memoria cache y la memoria principal definen lo que se
denomina memoria volátil. Los discos tanto magnéticos como ópticos se encuentran
dentro de la clasificación de lo que se denomina memoria secundaria o memoria
masiva. Algunos Sistemas Operativos hacen uso del principio de cercanía pero
también hacen uso de la memoria secundaria para extender su funcionalidad. Al uso
de memoria secundaria para almacenar la funcionalidad de un sistema operativo se
le denomina memoria virtual.
El uso de principio de cercanía será tratado con mayor profundidad cuando se
estudie la eficiencia de un programa.
Los Registros InternosEn la Figura 1 se ilustran los registros de propósito general, los cuales son visibles
para el programador. Estos registros pueden ser manipulados y usados por los
desarrolladores de software. Aunque pueden ser utilizados para realizar cualquier
tarea, también existen tareas predefinidas para cada uno de estos registros y que
merecen ser mencionadas:
Eax: Se le puede utilizar como un registro de 8 bits (al o ah), como un
registro de 16 (ax), o bien como un registro completo de 32 bits. El nombre
que común mente recibe es acumulador y su funcionalidad se encuentra
predefinida para algunas operaciones de tipo aritmético.
Ebx: También llamado registro índice base, algunas veces es usado para
contener el desplazamiento de una localidad.
Ecx: Registro contador, utilizado en la construcción de ciclos en
instrucciones tales como REP, REPE o REPNE.
Edx: registro de datos. Aplicado para el direccionamiento de datos.
Ebp: Registro apuntador base utilizado para construir los registros de
activación (variables locales y parámetros en una función). Analizaremos
más sobre registros de activación en secciones posteriores.
Esp: Registro apuntador de pila. Es el otro registro interno que permite la
creación de los registros de activación.
Esi y Edi: Índice fuente e índice destino respectivamente. Utilizados para
transferencia y uso de cadenas y arreglos.
Aparte de estos registros existen algunos otros los cuales no pueden ser
manipulados de manera directa (no pueden ser accedidos de manera directa en
modo protegido).
Registros de SegmentoExisten registros de segmento que permiten almacenar código, variables y datos en
general. Los registros de segmento en los procesadores Intel son de 16 bits y actúan
de acuerdo al modo de funcionamiento del procesador. Los registros de segmento
son:
CS Segmento de Código. Porción de memoria que contiene el código del
programa. El segmento de código usado para los procesadores 286 y
anteriores tiene una longitud de 64 KB y a partir del 386 el tamaño de este
segmento es de 4 GB
DS Segmento de datos. Contiene los datos con los cuales funciona un
programa. De igual forma que CS, DS tiene el tamaño de 64 KB en
procesadores 286 o menores y a partir del 386 el tamaño de este segmento es
de 4GB
SS Segmento de Pila. Los registros de propósito general que se utilizan para
acceder al segmento de pila son ESP y EBP. Este registro es usado
primordialmente para almacenar los registros de activación (variables
locales y parámetros de funciones).
ES Extra segmento. Utilizado de manera especial para manipular cadenas.
FS y GS Segmentos de memoria adicional.
Registro EFLAG (registro de banderas)
Banderas y su significadoC: Bandera de acarreo. Utilizada para marcar lo que se lleva en una suma o
lo que se presta en una resta
P: Bandera de Paridad. Usada para determinar si la cantidad de bits
encendidos es un número es par (valor de 1), o impar (valor de 0)
A: Acarreo auxiliar. Acarreo entre los bits 3 y 4
Z: Bandera de cero. Cuando el resultado de una operación es cero este bit se
enciende, en otro caso el bit se apaga
S: Bandera de signo. Si S=1 el número que resulta de una operación es
negativo, 0 en caso contrario
T: Bandera de Trampa. Permiten verificar el estado de error. Utilizado en la
construcción de depuradores.
I: Bandera de interrupción. Este bit puede ser controlado mediante las
instrucciones CLI y STI
D: Bandera de dirección. Utilizada en modo decremento D=1 o en forma de
incremento D=0 al ser aplicada en operaciones con cadenas. Se puede
utilizar las instrucciones STD y CLD para activar y desactivar este bit
O: Bandera de desborde. Utilizado para señalar que una operación a
desbordado la capacidad de memoria.
IOPL: Bandera de privilegio para entrada o salida
NT: Bandera de anidamiento. Utilizada para señalar cuando una tarea se
encuentra anidada dentro de otra
RF: Bandera de Reanudación. Se aplica cuando se requiere reanudar la tarea
después de realizar una depuración
VM: Bandera de Modo virtual.
AC: Bandera de comprobación de alineación
VIF: Interrupción Virtual
VIP: Interrupción pendiente
ID: Bandera de identificación
Registro apuntador a instrucciónEl registro apuntador a instrucción EIP, siempre apunta a la siguiente instrucción a
será desarrollada. El registro de código CS, en conjunción con el registro apuntador
a instrucción EIP contienen la dirección para la siguiente instrucción (CS:EIP).
Referencias Bibliográficas
[Denning. P.] “The Working Set Model for program Behavior.” Communications of the
ACM, mayo de 1968
CAPITULO IIHERRAMIENTAS DE PROGRAMACIÓN
Herramientas en general: Compiladores, Cargadores, Enlazadores, Ensambladores y Depuradores
Introducción
Todas estas herramientas se pueden clasificar dentro de la categoría de software de
base. Entendemos como software de base, aquel software que nos permite a su vez
generar software. Ellas varían en tamaño y complejidad. Se les puede conseguir
como software comercial o de iniciativa libre.
Como menciona [Aho], un compilador es un programa que lee un programa escrito
en un lenguaje -el lenguaje fuente- lo traduce a un programa equivalente en otro
lenguaje objeto. Como una parte importante del proceso de traducción, el
compilador reporta al usuario la presencia de errores en el código fuente.
Figura 3
Un compilador es entonces, un programa que traduce de un lenguaje que por
tradición se le denomina programa fuente, a un lenguaje terminal el cual se le
Programa objetoPrograma fuenteCompilador
Errores
denomina programa objeto. Esta traducción es realizada mediante una serie de pasos
los cuales no analizaremos de manera detallada aquí, pero que sí mencionaremos de
manera breve.
Fases de un compilador
Aho, propone las siguientes fases de acción de un compilador
Descompone la entrada en símbolos separados
Con los símbolos de entrada proporcionados poe
el analizador léxico y utilizando una gramática se
produce el análisis jerárquico
Funciones de Tipificación
Genera código intermedio sin optimizar
Código intermedio optimizado
Código Objeto
Figura 4
Analizador Léxico
Analizador Sintáctico
Analizador Semántico
Generador de código intermedio
Optimizador de Código
Generador de código
Código IntermedioCuando se diseña un compilador, el código intermedio se define para una máquina
hipotética. En un compilador real, el código intermedio es algún lenguaje
ensamblador y específicamente, un lenguaje ensamblador simbólico.
Un ensamblador es un lenguaje muy cercano al lenguaje que utilizan las
computadoras (lenguaje máquina). Como mencionamos anteriormente, existen
ensambladores simbólicos y de direccionamiento real. Los ensambladores
simbólicos utilizan 'etiquetas' en lugar de usar direcciones reales.
EnlazadoresUna herramienta de enlace o 'enlazador' toma como entrada varios archivos en
lenguaje objeto y los combina para formar un código único. Las tareas de un
enlazador son
1. Resolver referencias de símbolos externos.
2. Asignar direcciones finales tanto a funciones como a variables.
3. Revisa el código y los datos para que estén acordes con las nuevas
direcciones. Este proceso también se le llama 're-localización'
CargadoresUn cargador es la parte de un sistema operativo que se encarga de enviar a memoria
un archivo que se encuentra en disco. De manera particular realiza las siguientes
tareas
a) Lee la cabecera del archivo ejecutable y calcula el tamaño necesario para
almacenar los datos y el código.
b) Crea un nuevo espacio de direcciones para el programa
c) Copia datos y código en este espacio generado
d) Copia los argumentos del programa a la pila
e) Inicia los registros
f) Salta a una rutina que copia los argumentos del programa en la pila a los
registros y accede al punto de entrada principal del programa
DepuradoresDesde los procesadores 80386, existen los registros dr0-dr3 los cuales ayudan a
controlar la depuración (dr0 a dr6). Estos registros pueden ser accedidos únicamente
si se tiene un nivel de privilegio cero. Los depuradores actuales pueden utilizar un
diferente número de puntos de ruptura. Estos puntos de ruptura se utilizan para
frenar la funcionalidad del procesador. Los puntos de ruptura permiten analizar el
código que se está ejecutando en el acto.
El ambiente de Microsoft Visual C++ 2010 Express
®
Visual C++ Express Edition®, el cual puede ser obtenido de manera gratuita del
sitio oficial de Microsoft®. Para mantener la funcional del producto se debe obtener
un registro del sitio antes mencionado.
Aunque este producto soporta diferentes formas de desarrollo, utilizaremos el modo
consola. Comenzaremos la acción con esta herramienta la cual nos proporciona una
caratula inicial similar a la mostrada en la Figura 5
Figura 5
Seleccionamos desarrollar un proyecto mediante los menús Archivo → Nuevo →
Proyecto...
Debemos utilizar la plantilla “Aplicación de Consola Win32”, como se puede
apreciar en la figura 6
Figura 6
Seleccionando nombre y ubicación del proyecto se procede a realizar la
configuración de la aplicación y marcando el casillero de Proyecto vacío como se
puede observar en la figura 7.
En la ventana de Explorador de Soluciones y sobre el ícono Archivos de código
fuente con el botón derecho del mouse agregar un Nuevo elemento. Seleccionar la
plantilla Archivo C++ (.cpp) e indicar un nombre. En este momento se puede
escribir el código de prueba. Escribir el código mostrado en la tabla 1.
1 #include<stdio.h>
2
3 void main(){
4 printf(“Hello World\n”);
5 }
TABLA 1
Figura 7
En la ventana de Ámbito global se debe colocar un punto de ruptura en la última
llave haciendo ‘click’ sobre el margen izquierdo como se puede apreciar en la figura
8.
Figura 8
Ahora iniciamos la depuración presionando la tecla <F5>. Aparecerá la flecha
amarilla de depuración sobre el punto de ruptura. Esto indica que el programa se
encuentra en modo depuración. El menú Depurar → Ventanas es el que ocupará
nuestra atención en esta sección.
Para utilizar todas las posibilidades de desensamblado que ofrece el Visual C++ se
debe acceder al menú Herramientas → Configuración → Configuración para
expertos. Como lo muestra la figura 9
Figura 9
Las opciones de depuración más importantes son
a) Depurar → Ventanas → Inspección; tal menú nos permite llevar un
seguimiento de las variables existentes en el programa pudiendo observar su
dirección y contenido
b) Depurar → Ventanas → Memoria; proporciona mapas de memoria que
permiten analizar diferentes tipos de contenidos
c) Depurar → Ventanas → Desensamblado; permite revisar el código
ensamblador del programa
d) Depurar → Ventanas → Registros; con esta opción se puede analizar el
contenido de los registros internos del procesador
Para continuar con la depuración presionar de nueva cuenta la tecla <F5>
Ejemplo
Crear un nuevo proyecto y escribir el código de la tabla 2
1 #include<stdio.h>
2
3 void main(){
4 int x;
5 int y;
6 }
TABLA 2
Colocar un punto de ruptura sobre la llave de cierre del programa e iniciar la
depuración. En Depurar → Ventanas → Inspección seleccionar Inspección 1, lo que
nos proporciona una ventana de inspección como se muestra en la figura 10.
Solicitar el contenido y la dirección (&) de las variables x y y como se muestra en la
figura 11.
Analizando la imagen anterior podemos apreciar el contenido y dirección de las
variables x y y (direcciones en formato hexadecimal y contenidos en formato
decimal).
Utilizando el mismo programa y procedimientos similares podemos analizar el
contenido de memoria mediante la opción Depurar → Ventanas → Memoria y el
código ensamblador generado por C++ mediante la opción Depurar → Ventanas →
Desensamblado.
Figura 10
Figura 11
Ensambladores tipo Intel® y AT&T®Aunque existe una gran variedad de ensambladores, una clasificación más o menos
genérica puede ayudar a construir programas de manera estándar. La clasificación a
la que nos referimos es
a) Ensambladores con sintaxis AT&T
b) Ensambladores con sintaxis INTEL
Ensambladores con sintaxis AT&TLos ensambladores que reconocen esta sintaxis observan las siguientes reglas
1. Todos los registros internos del procesador tienen un prefijo %. Ejemplo
%eax
2. La transferencia de información se realiza de izquierda a derecha. Ejemplo
movl %eax, %ebx, mueve el contenido del registro eax al registro ebx
3. A las literales numéricas se les agrega el prefijo $
4. Los direccionamientos a memoria utilizan paréntesis circular y su sintaxis
es:
segmento:offset(base,índice,escala).
Ejemplo %es:100(%eax,%ebx,2)
5. Los operandos pueden tener tamaño b=byte, w=word, l=double Word
Ejemplos movl %eax, %ebx
movb $10, %es:(%eax)
pushl %eax
popw %ax
Ensambladores con sintaxis INTELLos ensambladores que reconocen esta sintaxis INTEL observan las siguientes
reglas
1. Los registros se les denomina de manera directa sin prefijos ni sufijos
Ejemplos eax, bx, etc.
2. La transferencia de información ocurre de derecha a izquierda
Ejemplos mov eax, ebx ebx → eax
mov eax, outmsg2 otmsg2 → eax
3. Se utilizan paréntesis rectangulares en lugar de circulares
Ejemplos mov eax, [input2]
mov [input1], eax
4. Se utilizan las palabras byte, word y dword para especificar el tamaño de
transferencia
Ejemplos mov dword [L6], 1
push dword 1
mov dword [ebp - 4], 0
Son ejemplos de ensambladores que utilizan sintaxis at&t, GAS y AS. Ejemplos de
ensambladores que usan la sintaxis INTEL son NASM y MASM.
MINGW y el código ensambladorMINGW es un compilador que se puede obtener de manera gratuita y con él se
puede desarrollar código para diferentes plataformas. Su instalación es muy simple
y únicamente se requiere desempacar el paquete y notificar en la variable de
ambiente PATH la localidad de los directorios /bin, /lib y /include.Las opciones de
compilación son similares a las usadas en el compilador gcc. El compilador gcc
tiene opciones de compilación que permiten generar código ensamblador. La opción
-S genera código ensamblador. El uso de la opción masm=dialecto, en donde
'dialecto' puede ser 'intel' o 'att', también ocupará nuestro interés.
SciTEScite es un editor de texto basado en SCIntilla. Es software de iniciativa libre y se le
puede conseguir de manera gratuita. Tiene soporte para verificación de sintaxis de
varios lenguajes. Junto con el compilador MINGW y el ensamblador NASM se
puede integrar un excelente ambiente de programación a bajo nivel. Se pueden
conseguir las versiones para Windows y Linux. Su instalación requiere únicamente
desempacar los archivos y declarar la localidad en donde se encuentra SciTE en la
variable de ambiente PATH.
NASMNASM es un ensamblador con sintaxis INTEL, iniciativa de software libre. Se le
puede conseguir para programar en 16 bits o en 32 bits. Su riqueza en el uso de
directivas permite programar de manera cómoda
Directivas de NASM
Directiva equ. Relaciona algún símbolo con un valor. Los símbolos así
definidos deben ser considerados constantes
Directiva %define. Similar a la directiva equ es utilizada para establecer
valores constantes.
Ejemplo %define x 16
Directivas db, dw, dd, declaran components tipo byte, word y double word
respectivamente
Ejemplos
X1 db 0 ; define un byte en cero
X2 dw 7000 ; define un word en 7000
Y1 db “Hello World”,0 ; define una cadena de
caracteres
Y3 resb 1 ; define un byte sin valor inicial
Y4 resw 100 ; define lugar para cien palabras
Y5 times 100 db 0 ; define lugar para 100 bytes
Directiva %include, utilizada para inclusión de archivos
Ejemplo
%include “asm_io.inc”
Analizaremos de manera más detenida el ensamblador NASM en otros temas y en
posteriores ejemplos.
OBJDUMP
Es una utilería que permite desplegar información de archivos objeto. Como se
menciona en el manual, esta herramienta proporciona información la cual es usada
por programadores quienes trabajan en herramientas de compilación lo cual es lo
opuesto a los programadores que lo único que desean es que su programa sea
compilado y trabaje. Esta herramienta será utilizada en capítulos posteriores cuando
se analice el código máquina.
Misceláneos
Otras herramientas que debemos mencionar y que serán de ayuda en posteriores
temas son el emulador DosBox y el ensamblador NASM para 16 bits. Ambas
herramientas permiten desarrollar programas que emulan el comportamiento de una
máquina Intel de 16 bits. También utilizaremos editores hexadecimales para la
modificación de código máquina.
Referencias Bibliográficas
[Aho, Alfred V] Compilers: Principles, Techniques, and Tools
CAPITULO IIICONVENCIÓN DE LLAMADAS
Llamadas a funciones usando el estándar de convención de llamadas¿Es posible programar en ensamblador de manera estructurada? El uso de un
estándar para realizar las llamadas a funciones, la disciplina al aplicar las
instrucciones de transferencia y una programación bien documentada permite
contestar de manera afirmativa la anterior pregunta.
En ensamblador existe un estándar que indica la forma correcta en la cual se deben
realizar las llamadas a funciones, también indica la manera en la cual se deben pasar
los parámetros a las funciones, la declaración de variables locales y el conjunto de
instrucciones que se deben utilizar para regresar de una función. El estándar al que
hacemos mención se le llama convención de llamadas.
Tanto las variables locales, como los parámetros usados en una función (los cuales
en conjunto reciben el nombre de registros de activación) son almacenados en el
segmento de pila (SS). Existen varios enfoques relacionados con la convención de
llamadas, pero los más importantes son:
a) Convención de llamada en C. Los parámetros de las funciones son colocadas
por la función llamadora de derecha a izquierda. La función llamadora debe
de limpiar el área de pila utilizada por los parámetros después de regresar de
la llamada
Ejemplo
1 push dword 1 Envía el parámetro 1 a la función f
2 call f Llama a la función f
3add esp, 4
Limpia el área de pila utilizada por el
parámetro 1
TABLA 3
En el ejemplo anterior se empuja el parámetro 1 como dword a la pila, se
llama a la función f y se limpia el espacio de pila utilizado por el parámetro
realizando una suma de 4 bytes
b) La convención de llamadas en Pascal. Los parámetros son colocados en el
segmento de pila (SS) de izquierda a derecha y la función que se llama es la
responsable de limpiar la pila.
En la actualidad es más usada la convención de llamadas a C, debido a que
en Pascal no se puede implementar las llamadas a funciones con un número
variable de parámetros.
Como se puede apreciar en el texto anterior, la pila en la arquitectura INTEL crece
de arriba a abajo. Es por esto que se debe sumar y no restar el número de bytes
necesarios para recuperar el espacio que antes usaban los parámetros (la instrucción
add esp, 4 explica el ejemplo anterior). Un efecto similar se puede lograr con la
instrucción pop.
Registros de Activación para parámetros
Tanto las variables locales como los parámetros de una función son lo que se
denomina registros de activación. El segmento de pila es utilizado para almacenar
tanto parámetros como variables locales. Las variables locales son extraídas de la
pila hasta que se sale de la función.
Cuando se llama una función con un parámetro, la forma en que se ve la pila es la
siguiente
Figura 12
Mediante la instrucción push se guarda el parámetro 1 en la pila y se recorre el
apuntador de pila esp. La instrucción call f, guarda la dirección de retorno en la pila
y salta a la dirección de la función. En la figura 12 se aprecia que el apuntador de
pila esp se desplaza hacia abajo en la pila (una resta es un incremento en el espacio
de pila y una suma es una disminución).
Y si el programa realiza una resta para crear un espacio que contendrá una variable
local, esta variable empuja el parámetro y la dirección de retorno en la pila (ver
figura 13)
Figura 13
La resta sub esp, 4 reserva un espacio de 4 bytes en el espacio de pila. La
disposición de registros de activación (parámetros y variables locales) en la pila se
le llama marco de pila. Ver figura 14.
Figura 14
Debido a este desplazamiento no es conveniente utilizar el registro esp para calcular
la dirección de variables locales y parámetros. Utilizar el registro esp para calcular
direcciones puede ocasionar errores y es por ello que debe ser fijada la cima de la
pila al momento de iniciar una función. Para fijar la cima de la pila se usa el registro
ebp y de esta forma esp puede ser desplazado cuando se necesite. Ver figura 15
Figura 15
El estándar de convención de llamadas en C, indica que se debe guardar el valor de
ebp en la pila y posteriormente fijar ebp. Ejemplo
1 f: Etiqueta de la función
2 push ebp Guardando el valor del apuntador base
ebp en la pila
3 mov ebp, esp Fijando la cima de la pila
TABLA 4
Esqueleto de una función que utiliza convención de llamadas usando la sintaxis INTEL1 f: Etiqueta de la función
2 push ebp Guardando el valor del apuntador base ebp en la pila
3 mov ebp, esp Fijando la cima de la pila
:
; El código debe ser colocado aquí
4 pop ebp Restablece el antiguo valor del apuntador base
5 ret Carga la dirección de retorno y salta a ella
TABLA 5
Esqueleto de una función que utiliza convención de llamadas usando la sintaxis AT&T1 f: Etiqueta de la función2 pushl % ebp Guardando el valor del apuntador base
ebp en la pila3 movl %esp, %ebp Fijando la cima de la pila
: ; El código debe ser colocado aquí
4 popl %ebp Restablece el antiguo valor del apuntador base
5 ret Carga la dirección de retorno y salta a ella
TABLA 6
Sintaxis INTEL Sintaxis AT&T Descripción
push ebp
mov ebp, esp
sub esp, inmed_bytes
pushl %ebp
movl %esp, %ebp
subl $inmed_bytes, %esp
Prólogo de una función. Acciones realizadas: (a) guardar el valor anterior del apuntador base, (b) establecer la cima de la pila, (c) reservar el espacio para variables locales
mov esp, ebp
pop ebp
ret
movl %ebp, %esp
popl %ebp
ret
Epílogo de la función. Acciones que realiza: (a) restablece el valor anterior de la cima de la pila, (b) extrae el valor anterior de ebp de la pila, (c) regresa a la dirección del código que llama
TABLA 7
Cuando se programa con funciones en lenguaje ensamblador, es recomendable
utilizar alguna convención de llamadas.
Al inicio de una función y como primera instrucción se debe almacenar el valor del
apuntador base (ebp) en la pila. La segunda instrucción debe fijar la cima de la pila
copiando el valor de esp en el apuntador base ebp. Dependiendo si se requiere del
uso de variables locales, se debe restar un inmediato en bytes (valor constante) de la
cima de la pila y mediante esta acción se reserva el espacio requerido para las
variables. Estas tres instrucciones son llamadas prólogo de la función.
Al final de una función se debe restablecer la cima de la pila a su valor inicial
copiando el valor del apuntador base ebp al apuntador de pila esp. La instrucción
siguiente debe extraer de la pila el valor anterior del apuntador base. Como
instrucción final, se debe retornar a la instrucción de llamado de la función mediante
la instrucción ret. La instrucción ret extrae la dirección de retorno de la pila y salta a
ella. Estas instrucciones son llamadas epílogo de la función.
Registros de activación para variables locales
Las variables locales son almacenadas en el segmento de pila. Para asignar espacio
en la pila para almacenar registros de activación de variables locales se resta la
cantidad en bytes para las variables locales.
Ejemplo
sub esp, 4 ; reserva espacio para almacenar 4 bytes
El código completo en el prólogo utilizando ensamblador INTEL sería el siguiente
f:
push ebp
mov ebp, esp
sub esp, 4 ; reserva espacio para variables locales
Como mencionamos anteriormente, Las variables locales junto con los parámetros
de una función reciben el nombre de Marco de Pila.
Inmediatamente después de fijar el valor del registro ebp, si se envía un parámetro a
la función, el estado de la pila se encuentra como lo indica la figura 16
Figura 16
Después de asignar espacio para variables locales, los parámetros de la función se
encuentran colocados en las localidades de memoria [ebp+8], [ebp+12], [ebp+16],
etc, para los parámetro_1, parámetro_2, parámetro_3, etc. respectivamente. Y
[ebp-4], [ebp-8], [ebp-12], etc, para las variables locales var1, var2, var3, etc
respectivamente. (como se encuentra mencionado en la información contenida en
los manuales de MINGW y NASM).
La tabla 8 muestra un programa desarrollado utilizando NASM y MINGW. En la
líneas 12 y 13 se puede revisar el prólogo de la función _main. La línea 14 le envía
un parámetro (el valor de 20) a la función imprime. La línea 15 llama a la función
imprime. La línea 16 limpia el espacio de pila ocupado por el parámetro que le fue
enviado en la línea 14. De las líneas 17 a 19 se puede revisar el epílogo de la
función _main. En las líneas 21 y 22 se tiene el prólogo de la función imprime. La
línea 23 envía el único argumento que recibe la función imprime a la función
externa _printf. La línea 24 envía la dirección de Letrero a la función externa
_printf. La línea 25 llama a la función externa _printf. La línea 26 limpia el espacio
de pila ocupado por los parámetros asignados en las líneas 23 y 24. En las líneas 27
a 29 se puede ver el epílogo de la función imprime.
Código Observación1 ; --------------------------------------------------- El punto y coma es usado para
comentarios en NASM2 ; nasm -fwin32 e01.asm3 ; gcc e01.obj -o e014 ; ---------------------------------------------------5 section .data Inicia sección de datos6 Letrero: Etiqueta letrero7 db 'Valor a imprimir=%d', 10, 0 db = define bytes, 10 es salto de
línea y las cadenas de caracteres finalizan con un cero
8 section .text Inicia sección de código9 global _main La función _main es global10 extern _printf La función _printf es externa11 _main: Etiqueta de la función _main12 push ebp Gurda el valor de ebp en la pila13 mov ebp, esp Fija la cima de la pila14 push 20 Envía 20 como parámetro a la
función imprime15 call imprime Llama a la función imprime16 add esp, 4 Limpia el espacio asignado en
la pila por la instrucción de la línea 14
17 mov esp, ebp Restablece el valor anterior de la cima de la pila
18 pop ebp Extrae el valor anterior del apuntador base
19 ret Regresa de la función main20 imprime: Etiqueta de la función imprime21 push ebp Gurda el valor de ebp en la pila22 mov ebp, esp Fija la cima de la pila23 push dword [ebp+8] Guarda el valor del parámetro
en la pila para llamar a la función externa _printf
24 push Letrero Guarda la dirección del letrero en la pila para llamar a la función externa _printf
25 call _printf Llama a la función externa _printf
26 add esp, 8 Elimina el espacio que ocupan los parámetros enviados a la función _printf en las líneas 23 y 24
27 mov esp, ebp Restablece el valor anterior de
la cima de la pila28 pop ebp Extrae el valor anterior del
apuntador base29 ret Regresa de la función imprime
TABLA 8
El ensamblador resultante del proceso de depuración en Visual C++
En este último tema se analizará el código ensamblador generado por VC++. Para
realizar esto, se crea un nuevo proyecto con el siguiente código:
Ejemplo
#include<stdio.h>void main(){ int x,y; x=2; y=3;}
Entrando a la fase de depuración podemos ver el siguiente código
Figura 17
Si analizamos la figura 17 apreciamos que VC++ utiliza sintaxis INTEL. Las
funciones creadas por la fase de desensamblado tienen prólogo. La pila crece hacia
abajo. El espacio que ocupan las variables locales es creado mediante una resta de
bytes (la instrucción sub esp, 0D8h para este caso). Los nombres de las variables
son simbólicos.
CAPITULO IVENSAMBLADOR EN LÍNEA
Debido a que la sintaxis del ensamblador en línea que debe ser usado en cada
herramienta es diferente, estudiaremos la forma de programar en línea analizando
cada uno de los ambientes de programación mencionados en el capítulo II.
Ensamblador en línea de Visual C++
Las funciones naked en Visual C++ permiten utilizar código ensamblador
empotrado (en línea) en código C. Para programar funciones naked, debemos
escribir el prólogo y el epílogo de la función de manera similar como se escribe en
código ensamblador normal. Se debe utilizar la macro Naked como se muestra a
continuación
#define<cstdio>
#define Naked __declspec( naked )
Ejemplo
1 #include<cstdio>2 #define Naked __declspec( naked )345 Naked int func(int x, int y[] )6 {78 _asm {9 push ebp10 mov ebp, esp11 }12 int a, b;13 _asm {14 mov eax, DWORD PTR[a]15 mov ebx, DWORD PTR [b]
16 mov ecx, 017 lazo:18 cmp ecx,eax19 jge siguiente20 mov esi, DWORD PTR [ebx+4*ecx]21 mov DWORD PTR[b], esi22 push eax23 push ebx24 push ecx25 push esi26 }27 printf("-->%d\n",b);28 _asm{29 pop esi30 pop ecx31 pop ebx32 pop eax33 inc ecx34 jmp lazo35 siguiente:36 move esp, ebp37 pop ebp38 ret39 }40 }4142 void main(void){43 int arreglo[]={10,20,30,40,50,60,70,80,90,100};44 func(10,arreglo);4546 }
TABLA 9
En el programa de la tabla 8 en la función func existe un prólogo de la función entre
las líneas 9-10 y un epílogo entre las líneas 36-38. Otro punto interesante que debe
ser notado es el código ensamblador incrustado entre líneas de lenguaje C utilizando
la instrucción _asm, en las líneas 13 y 28. A este código ensamblador incrustado
sobre código C se le denomina ensamblador en línea.
El ensamblador en línea de MINGW
MINGW proporciona una forma cómoda de construir programas C utilizando
código ensamblador incrustado. Se puede utilizar la directiva “asm” para incrustar
código tal como se muestra en el siguiente listado
1 int f(){2 asm("movl $5,%eax");3 }4 main(){5 printf("%d\n",f()); // Regresa el valor de 56 }
TABLA 10
En el ejemplo anterior se utiliza la directiva “asm” aplicada a una sola línea. En la
línea (2) se guarda el valor cinco en el acumulador (similar a realizar la instrucción
return 5). La salida impresa es 5 como lo muestra la siguiente figura
Figura 18
El código ensamblador incrustado en MINGW se puede extender a múltiples líneas
utilizando los paréntesis, tal como se aprecia en el ejemplo de la tabla 11. En este
listado, en la línea 3 se utilizó la directiva de ensamblador __asm__(, que inicia el
código ensamblador. El programa modifica la información contenida en los
parámetros y los retorna con un valor nuevo. La línea (4) carga la dirección efectiva
del primer parámetro (instrucción lea), 8(%ebp) y la amacena en el registro %eax.
La dirección contenida en %eax es almacenada en el registro %ebx en la línea (5).
Se almacena 25 en el primer parámetro en la línea (6). Una operación similar es
desarrollada entre las líneas 7-9 para almacenar el valor de 50 en el segundo
parámetro.
1 f(int *x, int *y){2 printf("Dirección de x=%x, Dirección de y=%x\n",&x,&y);3 __asm__(4 "leal 8(%ebp), %eax\n\t"5 "movl (%eax), %ebx\n\t"6 "movl $25, (%ebx)\n\t"7 "leal 12(%ebp), %ecx\n\t"8 "movl (%ecx), %edx\n\t"9 "movl $50, (%edx)\n\t"10 );11 }12 main( ){13 int x=0, y=0;14 f(&x,&y);15 printf(“%d %d\n”,x,y);16 }
TABLA 11
*Recordar que el primer parámetro es almacenado en 8(%ebp) en sintaxis AT&T lo
cual es equivalente al uso de [ebp+8] en sintaxis INTEL (revisar el capítulo 3 en el
tema registros de activación).
La salida al listado 11 se muestra en la figura 19.
Figura 19
En la penúltima línea de la figura 19 podemos observar los valores 25 y 50 los
cuales son los valores substitutos de los originales (0 para ambos casos).
El formato extendido para ensamblador en línea en MINGWEl formato extendido del ensamblador en línea de MINGW nos permite pasar
inmediatos a registros, utilizar variables y utilizar variables para almacenar el
contenido de los registros.
Su sintaxis es:
asm(plantilla : operandos de salida : operandos de entrada : lista de registros
destruidos)
a) Plantilla: consistente de instrucciones en lenguaje ensamblador
b) Operandos de salida: registros o variables donde se almacena un dato
c) Operandos de entrada: registros, variables o datos utilizados como entradas
d) Registros destruidos: registros modificados en las instrucciones contenidas
en la plantilla
Los operandos de salida, los operandos de entrada y los registros destruidos pueden
o no encontrarse en la instrucción (es opcional)
Aparte de esta sintaxis existen algunos modificadores que deben ser conocidos si se
requiere programa utilizando el formato extendido. La tabla 12 proporciona un
listado de los modificadores utilizados en el formato extendido
Modificador Descripcióna Eaxb Ebxc Ecxd EdxD EdiS Esig Variable o inmediatoq Registro de propósito generalI Inmediato=r Registro de salidar Registro de entrada
TABLA 12
Ejemplo de ensamblador en línea utilizando formato extendido. Tabla 13
1 #define LETRERO "Hello World\n"2 main( ){3 __asm__("pushl %0; call _printf; addl $4, %%esp"::"g" (LETRERO));4 }
TABLA 13
En el listado de la tabla 13 se invoca a la función "printf", pasándole como
argumento el LETRERO (el argumento %0). El modificador “g” indica que
“LETRERO” puede ser una variable o inmediato. No se tienen modificadores de
salida, ni registros destruidos. Los registros de propósito general deben ser
especificados con %% (como ejemplo %%esp).
Ejemplo
Ensamblador en línea utilizando formato extendido en MINGW. Tabla 14
1 f(){2 int x=450,y;3 // =r usa 'y' como registro de salida y x como registro de entrada
__asm__("movl %0, %%eax; movl %%eax, %1":"=r" (y):"r" (x));4 printf("%d\n",y);5 }6 main( ){7 f( );8 }
TABLA 14
En la línea 3 del listado de la tabla 14 se usa el doble “%” para los registros de
propósito general (en %%eax). La variable x es utilizada como registro de entrada
(r) y y como un registro de salida (=r).
Una referencia que puede ser consultada para el ensamblador en línea se encuentra
en los manuales [doc-gcc-inline.pdf].
Como un último ejemplo presentaremos el código de la tabla 15 que cambia de
formato “big-endian” a “little endian”. Como ejemplo se tiene como entrada
0x2AFB5C3A lo que nos debe proporcionar una salida 0x3A5CFB2A.
1 little_big_endian(int x){2 __asm__("movl %0, %%eax; bswap %%eax; movl %%eax, %0":"=r" (x):"r" (x));3 return x;4 }5 main(){6 int x=0x2AFB5C3A;7 printf("%x\n",little_big_endian(x));8 }
TABLA 15
Referencias Bibliográficas
[doc-gcc-inline.pdf] Http://es.tldp.org/Manuales-LuCAS/doc-gcc-inline/doc-gcc-inline.pdf
CAPITULO VPROGRAMACIÓN BÁSICA EN LENGUAJE
ENSAMBLADOR
Introducción
El objetivo del presente capítulo es presentar las instrucciones de mayor uso en
lenguaje ensamblador. En estos párrafos se presentan las instrucciones de
asignación, transferencia y control necesarias para diseñar programas que utilizan
funciones que pueden ser llamadas desde cualquier parte (código re-entrante). En
estas líneas de código se pretende también tener el primer contacto con los códigos
de operación de las instrucciones de ensamblador y su traducción al lenguaje
máquina.
Como primer punto a ser tratado en este capítulo, mencionamos cada una de las
instrucciones (y sus variantes) que serán utilizadas en posteriores ejemplos.
Nomenclatura utilizada
El propósito de una nomenclatura es definir un lenguaje estándar de comunicación
entre el que escribe y el lector. Los términos de la Tabla 16 permitirán avanzar en el
estudio de los códigos de operación utilizados en las instrucciones de lenguaje
ensamblador para traducir a lenguaje máquina.
Símbolo Descripcióni16 Inmediato de 16 bits
i8 Inmediato de 8 bitsi32 Inmediato de 32 bitsr8 Registro de 8 bitsr32 Registro de 32 bits
rm16 Registro o memoria de 16 bitsrm8 Registro o memoria de 8 bitsrm32 Registro o memoria de 32 bitsseg Registro de segmento
TABLA 16
Instrucción MOV
Variantes de la instrucción mov
88 89 8A 8B 8C 8Erm8 , r8 rm32, r32 r8 , rm8 r32, rm32 rm❑ , seg seg , rm
A0 A1 A2 A3al , [i32] eax , [i32] [i32], al [i32], eax
B0 B1 B2 B3 B4 B5al, i8 cl, i8 dl, i8 bl, i8 ah, i8 ch, i8
B6 B7 B8 B9 BAdh, i8 bh, i8 eax, i32 ecx, i32 edx, i32
BB BC BD BE BF
ebx, i32 esp, i32 ebp, i32 esi, i32 edi, i32
C6 C7rm8 , i8 rm32 , i32
La instrucción mov permite realizar asignaciones en los siguientes formatos
a) De registro a memoria
b) De registro a registro
c) De memoria a registro
d) De inmediato a registro (inmediato=constante numérica)
Ejemplos
La tabla 17 demuestra las instrucciones de asignación y sus respectivos códigos de
operación.
Instrucción Código de operaciónmov dh, F8h B6mov ebx, 2AFB5C3Ah BBmov ebp, esp 89mov esp, ebp 89
TABLA 17
El código de operación es un byte que ayuda a traducir a lenguaje máquina como
estudiaremos en un capítulo de más adelante. Pero en estos párrafos mencionaremos
únicamente que el código máquina para la instrucción mov ebp, esp es 89 e5 y el
código máquina para la instrucción mov esp, ebp es 89 ec.
Instrucción PUSH
Variantes de la instrucción push
06 0EES CS
16 1ESS DS
50 51 52 53 54 55 56 57eax ecx edx ebx esp ebp esi edi
68 6Ai32 i8
La instrucción push realiza dos acciones: -almacenar un componente en la memoria
de la pila y desplazar hacia abajo el apuntador de pila esp -. El componente que es
almacenado en la pila puede ser un inmediato de 32 u 8 bits o bien el contenido de
un registro.
Ejemplos
La tabla 18 demuestra las instrucciones push y sus respectivos códigos de operación
Instrucción Código de operaciónpush ebp 55push eax 50
TABLA 18
.
Instrucción POP
Variantes de la instrucción pop
05es
1Fds
58 59 5A 5B 5C 5D 5E 5F
eax ecx edx ebx esp ebp esi edi
8Frm32
La instrucción pop realiza dos acciones: -extrae un componente de la pila y desplaza
el apuntador de la pila esp hacia arriba-. El elemento extraído puede ser enviado a
un registro o una localidad de memoria
Ejemplos
La tabla 19 demuestra las instrucciones pop y sus respectivos códigos de operación
Instrucción Código de operaciónpop ebp 5Dpop eax 58
TABLA 19
Instrucción CALL
Variantes de la instrucción call
E8i32
La instrucción call realiza dos acciones: - guardar la dirección de retorno en la pila y
salta a la dirección indicada.
Instrucción LOOP
Variantes de la instrucción loop
E2
La instrucción loop realiza las siguientes actividades:
Resta 1 al contenido del registro ecx. Si el registro ecx es mayor que cero transfiere
el control a la etiqueta de la misma instrucción loop. Si el registro ecx es cero
continúa a la siguiente instrucción sin realizar la iteración.
Instrucción ADD
Variantes de la instrucción add
00 01 02 03 04 05rm8 , r8 rm32 , r32 r8 , rm8 r32 , rm32 al, i8 eax, i32
La acción de la instrucción add es sumar dos operandos y el resultado es
almacenado en el registro o la memoria destino.
Ejemplos Instrucción add tabla 20
Instrucción Código de Operaciónadd ah, al 00add al, 0xAB 04
TABLA 20
Instrucción SUB
Variantes de la instrucción sub
28 29 2ª 2B 2C 2Drm8 , r8 rm32 , r32 r8 , rm8 r32 , rm32 al, i8 eax, i32
La acción de la instrucción sub resta dos operandos y el resultado lo almacena en el
operando destino. El operando destino puede ser un registro o memoria.
Ejemplos de la instrucción sub, tabla 21
Instrucción Código de operaciónsub bh, bl 28sub ebx, ecx 29sub bl, [0x1B] 2A
TABLA 21
Instrucción INC
Variantes de la instrucción inc
40 41 42 43 44 45 46 47
eax ecx edx ebx esp ebp esi edi
La instrucción inc aumenta el contenido del registro destino en 1.
Ejemplos instrucción inc tabla 22
Instrucción Código de operacióninc eax 40inc ecx 41inc edx 42
TABLA 22
Instrucción DEC
Variantes de la instrucción dec
48 49 4A 4B 4C 4D 4E 4Feax ecx edx ebx esp ebp esi edi
La instrucción dec disminuye el contenido del registro destino en 1
Ejemplos instrucción dec tabla 23
Instrucción Código de operacióndec eax 48dec ecx 49dec edx 4A
TABLA 23
Instrucción PUSHA
Variantes de la instrucción pusha
60
La instrucción pusha almacena los registros de propósito general en la pila
Instrucción POPA
Variantes de la instrucción popa
61
La instrucción popa retira los registros de propósito general de la pila. La tabla 24
muestra las transferencias que utilizan las banderas como una condición de salto
Transferencias basadas en las banderas
Las transferencias que utilizan las banderas para realizar una acción son las
siguientes
Código de Operación
Instrucción Descripción
70 jo Salta si la bandera de desborde está encendida71 jno Salta si la bandera de desborde no está
encendida78 js Salta si la bandera de signo está encendida79 jns Salta si la bandera de signo no está encendida7A jp Salta si la bandera de paridad está encendida7B jnp Salta si la bandera de paridad no está encendida
TABLA 24
Transferencias que utilizan condiciones relacionales para saltar
La tabla 25 muestra las transferencias que utilizan condiciones relacionales
Sin signo Con signoCod. Op.
Instrucción Descripción Cod. Op.
Instrucción Descripción
74 je ¿ 74 je ¿75 jne ≠ 75 jne ≠72 jb < 7C jl <76 jbe <= 7E jle <=77 ja > 7F jg >73 jae >= 7D jge >=
TABLA 25
Todas estas transferencias utilizan inmediato de 8 bits para especificar el número de
bytes que deberán ser brincados.
Transferencias incondicionales
La instrucción de salto incondicional utiliza las siguientes variantes
E9 Salto incondicional de 32 bits
i32
EA Salto incondicional lejano
seg :m❑
EB Salto incondicional de 8 bits
i8
Sección de ejemplos
Debido a que algunos ejemplos de código construido en lenguaje ensamblador
requieren de explicación previa a un más alto nivel de abstracción, se usará el
lenguaje C como un pseudocódigo para demostrar la funcionalidad de los
algoritmos y posteriormente será analizado el código en el formato original (código
ensamblador junto con códigos de operación).
Variables de 8, 16 y 32 bits en lenguaje C
Aunque la definición de tipos en el lenguaje C es tema de otro capítulo, aquí lo
mencionaremos de manera breve. El lenguaje C cuenta entre su definición de tipos
con valores numéricos los cuales en tamaño pueden ser de 8, 16, 32 e incluso 64
bits, dependiendo del compilador. Las variables de tipo char, short, int y long
tendrán un tamaño prefijado en bits.
En la tabla 26 se muestra un listado de código C y en la figura 20 el resultado del
proceso de desensamblado del mismo programa.
1 void main(){
2 char c;
3 short s;
4 int i;
5 long l;
6 c=1;
7 s=2;
8 i=3;
9 l=4;
10 }
TABLA 26
Figura 20
Nota: Para analizar los códigos de operación revisar los párrafos anteriores en este
mismo capítulo.
En la figura 20 se pueden verificar los siguientes aspectos interesantes:
a) La instrucción mov byte ptr [c], 1, indica que las variables tipo char en el
lenguaje C, son traducidas a tipo byte de ensamblador. La instrucción en
código máquina de esa misma instrucción es: C6 45 FB 01, expresa que el
código de operación es C6 lo que se puede traducir a código ensamblador
como: mov rm8 , i8.
b) La instrucción mov eax, 2, es utilizada como preparación para la siguiente
instrucción (ver inciso c). Indica que un inmediato de 32 bits es almacenado
en el registro de 32 bits eax. La instrucción en código máquina para esa
misma instrucción es: B8 02 00 00 00 lo cual indica que el código de
operación es B8 que es una operación que se traduce como mov eax, i32. El
inmediato 2, que aunque se almacena como una variable de tipo short de 16
bits en el lenguaje C, se utiliza un inmediato de 32 bits en ensamblador para
almacenar de manera temporal este valor.
c) La instrucción mov word ptr[s], ax es una asignación de 16 bits (word) a la
variable simbólica s. Se asignan los bits bajos del registro eax contenidos en
el registro ax a la variable s. Debido a que se está utilizando una arquitectura
de 32 bits el uso de los registros de 32 y 8 bits es la forma natural de operar,
pero los registros de 16 bits son penalizados colocando un prefijo de tamaño
(el número 66) el cual antecede al código de operación, como se puede
apreciar en el código máquina completo de la instrucción: 66 89 45 EC. El
código de operación es entonces el número 89, lo que indica que la
instrucción es del tipo mov rm32, r32. Aunque en este caso y debido a que
antecede el prefijo 66 al código de operación 89, los registros y memorias de
32 bits son substituidos por registros y memorias de 16 bits. Por los anterior,
se puede obtener la conclusión que los números tipo short en lenguaje C son
traducidos a ensamblador utilizando palabras (word).
d) La instrucción mov dword ptr[i], 3 como se puede ver en la figura 20 es
traducida a código máquina que tiene la forma C7 45 E0 03 00 00 00. El
código de operación C7 indica que es una instrucción del tipo mov rm32 , i32.
Entonces, para almacenar un entero se debe utilizar una doble palabra
(dword).
e) De manera similar a la mencionada en el inciso (d), la instrucción mov
dword ptr[l], 4 la cual puede ser traducida a código máquina como el
formato: C7 45 D4 04 00 00 00, también es del tipo mov rm32 , i32.
En resumen, podemos indicar que en VC++ se almacena los caracteres en bytes, los
enteros cortos (short) en palabras (word) y para el caso de los enteros y largos en
palabras dobles (dword).
Los prefijos de signo en el lenguaje C
El lenguaje C tiene dentro de su repertorio de palabras reservadas el prefijo de signo
unsigned, que indica que el número en cuestión debe ser considerado sin el uso de
un bit de signo (bit más a la izquierda). La tabla 27 muestra un ejemplo de código
VC++ utilizando el prefijo unsigned y la figura 21 el código desensamblado
correspondiente.
1 void main()
2 {
3 unsigned int x;
4 x=1;
5 if(x < 1) goto termina;
6 x++;
7 termina:
8 ;
9 }
TABLA 27
En la línea 2 de la tabla 27 se encuentra declarada la variable x de tipo entera y sin
signo. En la línea 3 se asigna el valor de 1 a la variable x. En la línea 5 se compara
el valor de la variable x con la literal numérica 1. Si la variable x contiene un valor
que es menor que 1, se transfiere el control a la etiqueta termina. Si la variable x
contiene un valor mayor o igual que 1 se incrementa el contenido de la variable x.
En la línea 6 se indica el incremento del contenido de la variable 1 en una unidad.
La línea 7 es una etiqueta que le permite al goto de la línea 5 transferir el control. La
línea 8 es una instrucción vacía.
El código ensamblador de este listado se encuentra ilustrado en la figura 21. De esta
figura podemos mencionar los siguientes puntos
1. La instrucción mov dword ptr[x], 1, expresa que la literal numérica 1 será
asignada a la variable simbólica x cuyo tipo es dword. El código máquina
para esta instrucción es C7 45 F8 01 00 00 00 y su código de operación es
C7 lo que indica una instrucción similar a mov rm32 , i32.
2. La instrucción cmp dword ptr [x], 1 que es traducida a lenguaje máquina
como 83 7D F8 01, su código de operación es 83. La forma de
determinación de su código de operación lo trataremos en capítulos
posteriores. El objetivo de esta instrucción es realizar la comparación de sus
dos operandos y dejar las banderas listas con el propósito de utilizar alguna
transferencia similar a las descritas en la tabla 25. La comparación que será
realizada se muestra en el inciso (3).
3. La instrucción jae es la transferencia que se utiliza para expresar la relación
mayor o igual cuando se comparan dos operandos sin signo. El código de
operación para jae es 73 como se puede observar en la tabla 25.
4. Las instrucciones mov eax, dword ptr[x] con código de operación 8B, add
eax,1 con código de operación 83 y mov dword ptr[x], eax con código de
operación 89, son utilizadas para aumentar el contenido de la variable x en
1.
5. Las instrucciones de comparación en lenguaje C son traducidas utilizando
lógica inversa. Mientras que se utilizó la relación < en lenguaje C, la
traducción a código ensamblador resultó en una transferencia con relación
>= (jae).
Figura 21
Uso de la instrucción Loop para construcción de ciclos
Existen varias formas de construir ciclos en lenguaje ensamblador. Algunas
instrucciones fueron diseñadas con ese propósito en mente. Como se mencionó en
este mismo capítulo, el código de operación para la instrucción loop es E2. La
función Naked construida utilizando VC++ y mostrada en la tabla 28 explica el uso
de la instrucción loop y la figura 22 contiene el código ensamblador generado por el
proceso de depuración del mismo programa.
1 #include<cstdio>
2 #define Naked __declspec( naked )
3 Naked void contador()
4 {
5 _asm {
6 push ebp
7 mov ebp, esp
8 sub esp, 0CCh
9 }
10 int a;
11 _asm {
12 mov ecx, 5
13 ciclo:
14 mov DWORD PTR[a], ecx
15 push eax
16 push ebx
17 push ecx
18 push esi
19 }
20 printf("Valor=%d\n",a);
21 _asm {
22 pop esi
23 pop ecx
24 pop ebx
25 pop eax
26 loop ciclo
2728 mov esp , ebp
29 pop ebp
30 ret
3132 }
33 }
34 void main(){
35 contador();
36 }
TABLA 28
Figura 22
En el código de la figura 22 podemos apreciar lo siguiente:
a) El código de operación para la instrucción push ebp es 55. Este valor se
puede obtener directamente de una de las variantes de la instrucción push
(ver arriba en este mismo capítulo).
b) La instrucción mov ebp, esp, tiene un código de operación 8B pues respeta
el formato mov r32, rm32.
c) De los incisos anteriores se puede deducir que el código máquina para el
prólogo de una función es: 55 de la instrucción push ebp y 8B EC de la
instrucción mov ebp, esp.
d) La instrucción mov ecx, 5 respeta el formato mov ecx, i32 y su traducción a
código maquina es B9 05 00 00 00, siendo B9 el código de operación y 05
00 00 00 el inmediato de 32 bits 5.
e) La traducción a código máquina de la instrucción loop ciclo es E2 D8. El
valor E2 es el código de operación de esta instrucción y D 816 utilizando
complemento a dos es el número negativo −4010 que indica el número de
bytes que deben ser saltados para regresar a la etiqueta ciclo. Los números
negativos indican salto hacia atrás y los números positivos salto hacia
adelante.
f) Por último, se puede observar que la traducción a lenguaje máquina del
epílogo de una función es: 8B E5 para la instrucción mov esp, ebp, 5D para
pop ebp y C3 para ret.
La instrucción loop utiliza el registro contador ecx para realizar un ciclo, sin
embargo es más común que en la mayoría de los compiladores se utilice la
instrucción cmp junto con alguna instrucción de salto condicionado (ver tabla 25)
para construir ciclos.
El ejemplo de la tabla 29 el cual fue ensamblado utilizando NASM, demuestra el
uso de la instrucción cmp para construir dos ciclos anidados los cuales imprimen las
tablas de multiplicar.
1 ; ----------------------------------------------------------------------------2 ; p2_2.asm3 ;4 ; nasm -fwin32 p2_2.asm5 ; gcc p2_2.obj -o p2_26 ; ----------------------------------------------------------------------------78 section .data9 LC0:10 db '%dX%d=%d', 10, 011
12 section .text1314 global _main15 extern _printf1617 section .text18 _main:19 push ebp20 mov ebp, esp2122 mov eax, 12324 ce:25 cmp eax, 1026 jg terminar2728 mov ebx, 129 ci:30 cmp ebx,1031 jg salircicloi3233 mov ecx, eax34 imul ecx, ebx3536 pusha37 push ecx38 push ebx39 push eax40 push LC041 call _printf42 add esp, 164344 popa4546 inc ebx47 jmp ci4849 salircicloi:5051 inc eax5253 jmp ce545556 terminar:
57 mov esp, ebp58 pop ebp59 ret
TABLA 29
En el listado de la tabla 29, las líneas 19-20 contienen el prólogo de la función
main. En la línea 22 se almacena en el registro eax el valor de 1 y en este caso eax
es el registro utilizado como contador para el ciclo externo. En la línea 24 se tiene la
etiqueta del ciclo externo la cual es usada desde la línea 53 para iterar. En la línea
25 se utiliza la instrucción cmp para comparar el contador del ciclo externo (eax)
con el inmediato 10. La línea 26 indica que si es mayor que 10 entonces finaliza
ciclo externo. En la línea 28 se inicia el contador del ciclo interno (el registro ebx es
usado como contador del ciclo interno). La línea 29 contiene la etiqueta del ciclo
interno que utiliza la instrucción en la línea 47 para iterar. La línea 30 compara el
contador del ciclo interno (ebx) con el inmediato 10. En la línea 31 se indica que si
el valor del contador de ciclo interno (ebx) es mayor que 10 entonces se debe salir
del ciclo interno. El contenido del registro eax es almacenado en el registro ecx. La
instrucción imul realiza la multiplicación y el resultado es almacenado en el registro
ecx (lo que contenía el registro ecx es destruido). En la línea 36 se almacenan todos
los registros debido a que la función _printf puede destruir su contenido. Las líneas
37-42 imprimen la información. En la línea 44 se restablecen los contenidos de los
registros almacenados en la línea 36. La línea 46 incrementa en uno el contador del
ciclo interno. La línea 47 realiza la iteración al ciclo interno. La línea 49 contiene la
etiqueta de salida del ciclo interno. La línea 51 incrementa en uno el contador del
ciclo externo. La línea 53 salta a la etiqueta ciclo externo con propósito de iteración.
Las líneas 57-59 definen el epílogo de la función.
Código Reentrante
Un método es reentrante si permite múltiples invocaciones las cuales no se
interfieren unas con otras. Una función es recursiva si se llama a sí misma. Una
función es reentrante si pueden ser ejecutadas de manera segura concurrentemente.
Las funciones reentrantes no pueden utilizar variables globales o estáticas [Smith,
Adam R. 2009]. Para que una función sea reentrante debe considerar los siguientes
requisitos:
1. No debe contener datos globales
2. No debe regresar la dirección de un dato global.
3. Debe trabajar únicamente con los datos proporcionados por la función
llamadora.
4. Desconfiar del bloqueo de recursos que utilizan el patrón de diseño
singleton.
5. No modificar su propio código
6. No llamar a funciones o programas no reentrantes
Se debe notar que las funciones reentrantes no son únicamente relevantes para
realizar programas multi-hilo, también son importantes para funciones recursivas y
para manejadores de señales.
Código Recursivo
Una función recursiva es aquella función la cual se llama a sí misma. Por lo general
(pero no siempre) las llamadas recursivas también son funciones reentrantes pues no
requieren de variables de tipo estático o global para almacenar los valores de sus
cálculos parciales. En lenguaje ensamblador se pueden utilizar variables locales
para almacenar de manera temporal los resultados de esos cálculos parciales. La
forma estándar de regresar el resultado de una función en lenguaje ensamblador es
la utilización del registro acumulador. Una función recursiva puede volverse no
reentrante si viola alguno de los puntos mencionados en el párrafo anterior.
El listado en lenguaje C de la tabla 30 muestra un ejemplo de código recursivo
reentrante.
1 #include<stdio.h>
2 int factorial(int n)
3 {
4 if(n==0) return 1;
5 return n*factorial(n-1);
6 }
7 void main()
8 {
9 printf("3!=%d\n",factorial(3));
10 }
TABLA 30
La figura 23 muestra los marcos de pila generados por el código mostrado en la
tabla 30 y los resultados parciales.
Figura 23
Si estudiamos de manera detenida la figura 23 podemos deducir los siguientes pasos
internos.
a) La función main() coloca un 3 en la pila el cual será utilizado por la función fac.
b) El uso de la instrucción call empuja la dirección de retorno a la función main en
la pila.
c) Mediante el uso de la instrucción cmp, se compara el parámetro enviado (en la
dirección ebp+8 para el ensamblador NASM) con el inmediato 0.
d) Si es igual el parámetro al valor cero, se retorna el inmediato 1 utilizando el
acumulador eax.
e) Se disminuye el valor del parámetro en 1 utilizando la instrucción dec.
f) Se empuja el valor del parámetro disminuido en 1 en la pila y llama a la función
fac. creando de esta forma un nuevo marco de pila.
g) Se destruye el marco de pila utilizando la instrucción add esp, 4.
h) El resultado de la llamada recursiva almacenada en el registro eax es
multiplicado con el parámetro mediante la instrucción imul eax, [ebp+8].
El código ensamblador resultante se muestra en la tabla 31.
1 ; ----------------------------------------------------------------------------2 ; e05.asm3 ;4 ; nasm -fwin32 e05.asm5 ; gcc e05.obj -o e056 ; ----------------------------------------------------------------------------78 section .data9 LC0:10 db '3!=%d', 10, 01112 section .text1314 global _main
15 extern _printf1617 section .text18 fac:19 push ebp20 mov ebp, esp2122 mov ebx, [ebp+8] ; almacena el prámetro en el registro ebx23 cmp ebx,024 jne continuar25 mov eax, 1 ; return 126 jmp terminar27 continuar:28 dec ebx29 push ebx30 call fac31 add esp, 432 imul eax, [ebp+8]33 terminar:34 mov esp, ebp35 pop ebp36 ret3738 _main:39 push ebp40 mov ebp, esp4142 push 343 call fac44 add esp, 44546 push eax47 push LC048 call _printf49 add esp, 85051 mov esp, ebp52 pop ebp53 ret
TABLA 31
Un último ejemplo (uso de ciclos recursivos)
En el código C mostrado en la figura 24 existen dos funciones reentrantes las cuales
calculan las tablas de multiplicar a manera recursiva. Para la construcción de dicho
programa fue utilizada la herramienta MINGW.
Figura 24
En la función cicloi de la figura 24 se tiene una llamada recursiva en la línea 7.
La función cicloe es recursiva debido a que se considera un ciclo recursivo en la
línea 14.
La traducción de este programa a lenguaje ensamblador resulta en el código de
la tabla 32.
1 ; ----------------------------------------------------------------------------2 ;p2_5.asm3 ; compilar como:4 ; nasm -fwin32 p2_5.asm5 ; gcc p2_5.obj -o p2_56 ; ----------------------------------------------------------------------------7 section .data8 LC0:9 db '%d X %d = %d', 10, 01011 section .text1213 global _main14 extern _printf1516 section .text17 _main:18 push ebp19 mov ebp, esp2021 push 122 push 1023 call cicloe24 add esp,82526 mov esp, ebp27 pop ebp28 ret2930 cicloe:31 push ebp32 mov ebp, esp3334 mov eax, [ebp+8]3536 mov ebx, [ebp+12]3738 cmp ebx,eax3940 jle iterae41 jmp fine42 iterae:43 pusha44 push eax45 push 1
46 push ebx47 call cicloi48 add esp,1249 popa5051 inc ebx5253 push ebx54 push eax55 call cicloe56 add esp,8575859 fine:60 mov esp, ebp61 pop ebp62 ret63646566 cicloi:67 push ebp68 mov ebp, esp6970 mov eax, [ebp+8]7172 mov ebx, [ebp+12]73 mov ecx, [ebp+16]7475 cmp ebx,ecx7677 jle iterai78 jmp fini79 iterai:80 mov edx, ebx81 imul edx, eax82 pusha83 push edx84 push ebx85 push eax86 push LC087 call _printf88 add esp,1689 popa90
91 inc ebx9293 push ecx94 push ebx95 push eax96 call cicloi97 add esp,129899100
fini:
101
mov esp, ebp
102
pop ebp
103
ret
104
TABLA 32
El desensamblado del archivo objeto utilizando la biblioteca objdump del
mismo programa se puede apreciar en la tabla 33
p2_5.obj: file format pe-i386
Disassembly of section .text:
00000000 <_main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 68 01 00 00 00 push $0x1
8: 68 0a 00 00 00 push $0xa
d: e8 0a 00 00 00 call 1c <cicloe>
12: 81 c4 08 00 00 00 add $0x8,%esp
18: 89 ec mov %ebp,%esp
1a: 5d pop %ebp
1b: c3 ret
0000001c <cicloe>:
1c: 55 push %ebp
1d: 89 e5 mov %esp,%ebp
1f: 8b 45 08 mov 0x8(%ebp),%eax
22: 8b 5d 0c mov 0xc(%ebp),%ebx
25: 39 c3 cmp %eax,%ebx
27: 7e 05 jle 2e <iterae>
29: e9 22 00 00 00 jmp 50 <fine>
0000002e <iterae>:
2e: 60 pusha
2f: 50 push %eax
30: 68 01 00 00 00 push $0x1
35: 53 push %ebx
36: e8 19 00 00 00 call 54 <cicloi>
3b: 81 c4 0c 00 00 00 add $0xc,%esp
41: 61 popa
42: 43 inc %ebx
43: 53 push %ebx
44: 50 push %eax
45: e8 d2 ff ff ff call 1c <cicloe>
4a: 81 c4 08 00 00 00 add $0x8,%esp
00000050 <fine>:
50: 89 ec mov %ebp,%esp
52: 5d pop %ebp
53: c3 ret
00000054 <cicloi>:
54: 55 push %ebp
55: 89 e5 mov %esp,%ebp
57: 8b 45 08 mov 0x8(%ebp),%eax
5a: 8b 5d 0c mov 0xc(%ebp),%ebx
5d: 8b 4d 10 mov 0x10(%ebp),%ecx
60: 39 cb cmp %ecx,%ebx
62: 7e 05 jle 69 <iterai>
64: e9 29 00 00 00 jmp 92 <fini>
00000069 <iterai>:
69: 89 da mov %ebx,%edx
6b: 0f af d0 imul %eax,%edx
6e: 60 pusha
6f: 52 push %edx
70: 53 push %ebx
71: 50 push %eax
72: 68 00 00 00 00 push $0x0
77: e8 00 00 00 00 call 7c <iterai+0x13>
7c: 81 c4 10 00 00 00 add $0x10,%esp
82: 61 popa
83: 43 inc %ebx
84: 51 push %ecx
85: 53 push %ebx
86: 50 push %eax
87: e8 c8 ff ff ff call 54 <cicloi>
8c: 81 c4 0c 00 00 00 add $0xc,%esp
00000092 <fini>:
92: 89 ec mov %ebp,%esp
94: 5d pop %ebp
95: c3 retTABLA 33
La descripción del archivo objeto analizado con la utilería objdump muestra tres
columnas: el número de línea, el código máquina y la instrucción en lenguaje
ensamblador. Utilizaremos de manera más extensa esta utilería en capítulos
posteriores.
Referencias bibliográficas
[Smith, Adam R. 2009]. Compiler Transformations to Generate Reentrant C
Programs to Assist Software Parallelization. Submitted to the Department of
Electrical Engineering & Computer Science and the Faculty of the Graduate School
of the University of Kansas in partial fulfillment of the requirements for the degree
of Master´s of Science, 2009/06/16.
CAPITULO VIProgramación a bajo nivel utilizando
lenguaje C
IntroducciónEn el presente capítulo se estudiarán las localidades en donde se almacenan las
variables de tipo global (uso de código no reentrante). Al mismo tiempo, se
analizará el concepto de apuntador, su uso, aplicación y aritmética. También se
revisará el concepto de asignación estática y dinámica de memoria.
Las variables globales
Desde el punto de vista de un programador, la memoria es un lugar donde se pueden
almacenar cosas. En la memoria se almacena tanto el código del programa como los
datos que manipula este mismo código. Tanto en lenguaje C como en ensamblador,
las variables globales y locales son almacenadas en lugares independientes. [Lewis,
Daniel W] menciona que la organización tradicional de memoria es segmentada
como lo muestra la figura 25.
Figura 25
En capítulos previos se mencionó que las variables locales son almacenadas en la
pila. Por otro lado, las variables globales se pueden clasificar en variables globales
iniciadas y variables globales sin iniciar. Algunos ensambladores poseen las
directivas .data y .bss que hacen referencia a las variables iniciadas y sin iniciar
respectivamente como lo muestra la figura 26.
Figura 26
La figura 26 muestra una porción de programa que utiliza .data y .bss para ilustrar el
uso de variables globales iniciadas y no iniciadas respectivamente. La variable
peticion1 es un ejemplo de variable definida como bytes (db) y cuyo terminador es
nulo. La variable entrada1 ejemplifica a una variable definida como doble (resd).
Las variables globales en lenguaje C
Al igual que en lenguaje ensamblador, en el lenguaje C las variables globales sin
iniciar y las variables globales iniciadas son colocadas en dos espacios de
direcciones diferentes como podemos observar la salida del programa de la tabla 34.
1 static int a;2 static int b;3 static int c;4 static int d=1;
5 static int e=2;6 static int f=3;78 main(){9 printf("Dirección de a= %x\n",&a);10 printf("Dirección de b= %x\n",&b);11 printf("Dirección de c= %x\n",&c);12 printf("Dirección de d= %x\n",&d);13 printf("Dirección de e= %x\n",&e);14 printf("Dirección de f= %x\n",&f);15 }
TABLA 34
Figura 27 Salida del listado 34
En la figura 27 podemos ver que las variables no iniciadas son colocadas en el
rango 404010-404030 (con separación de 16 bytes) mientras que las variables
iniciadas se encuentran en el rango 402000-402008 (con una separación de 4 bytes).
Una herramienta que facilita el análisis de la memoria es un mapa o volcado de
memoria. La función mostrada en la figura 28 y la cual utilizaremos de manera
continua durante el desarrollo de este capítulo también permite realizar volcados de
memoria.
Figura 28
Debido a que usará de manera frecuente unsigned char, para representar bytes, es
necesario mencionar que resulta conveniente definir el tipo BYTE como se muestra
en el programa de la figura 28. También debemos indicar en este punto, que los
prefijos de signo en lenguaje C fueron descritos en el Capítulo V.
El concepto de dirección y uso de apuntadores
Como se menciona en [Kernighan, Brian W.], un apuntador es una variable que
contiene la dirección de una variable. La justificación de utilizar apuntadores, es que
por lo regular producen un código más compacto y eficiente. Según menciona
Kernighan Los apuntadores se han puesto junto a la proposición goto como una
forma maravillosa de crear programas ininteligibles. Esto es verdadero cuando se
utilizan en forma descuidada, y es fácil crear apuntadores que señalen a algún
lugar inesperado. Para utilizar apuntadores se debe tener disciplina y saber sin lugar
a dudas su funcionamiento. Sin el uso de esta premisa, los apuntadores pueden
resultar un riesgo (únicamente los buenos programadores pueden obtener provecho
del uso de los apuntadores).
En ANSI C se utiliza void * para denotar un apuntador de tipo genérico. Un
apuntador genérico puede ser adaptado a cualquier otro tipo de apuntador. La forma
de declarar un apuntador se expresa mediante la siguiente sintaxis:
Tipo_base *Nombre_de_la_variable
En donde:
Tipo_base Expresa cualquier tipo válido de la programación C
Nombre_de_la_variable Especifica un identificador
El código de la tabla 35 aclara el punto
1 typedef unsigned char BYTE;2 void volcado (void *dirini, void *dirfin){3 int i,j;4 for(i=(int)dirini; i<(int)dirfin+4;i+=16){5 printf("%008x - ",i);6 for(j=0;j<16 && (i+j)<(int)dirfin+4;++j){7 if(j==8) printf("-- ");
8 printf("%002x ",*((BYTE *)(i+j)));9 }10 printf("\n");11 }12 }1314 int main(){15 int *p;16 int y=0x30;17 int x=0x40;18 p=&x;1920 volcado(&x, &p);2122 return 0;23 }
TABLA 35
Figura 29 Salida del listado de la tabla 35
En la tabla 35 podemos ver en la línea 15 la declaración del apuntador p. En la línea
20 realizamos el volcado de memoria a partir de la dirección más baja (dirección de
la variable x) y escalando direcciones hasta la dirección más alta (dirección del
apuntador p). La dirección más baja de variables locales se encuentra contenida en
la última declaración línea-17 y la dirección más alta en la primera declaración
línea-15. La dirección de la variable x 22FF4C es almacenada en el apuntador p. La
asignación se realiza en la línea 18, como se explica en la ventana derecha de Scite.
Las variables y apuntadores locales son almacenados en el marco de pila iniciando
en la dirección [ebp-4] o bien para ensambladores tipo at&t en -4(%ebp), para el
apuntador p, la variable y es almacenada en [ebp-8], o bien, -8(%ebp) en un
ensamblador at&t y -12(%ebp) para la variable x.
1 int a,b,c;2 typedef unsigned char BYTE;3 void volcado (void *dirini, void *dirfin){4 int i,j;5 for(i=(int)dirini; i<(int)dirfin+4;i+=16){6 printf("%008x - ",i);7 for(j=0;j<16 && (i+j)<(int)dirfin+4;++j){8 if(j==8) printf("-- ");9 printf("%002x ",*((BYTE *)(i+j)));10 }11 printf("\n");12 }13 }14 int little_big_endian(int x){15 __asm__("movl %0, %%eax; bswap %%eax;
movl %%eax, %0":"=r"(x):"r"(x));16 return x;17 }18 int main(){19 int *p;20 int y=0xDEADC0DE;21 int x=0xBABEB0DE;22 p=&x;2324 __asm__("movl -4(%%ebp), %0;movl -8(%%ebp), %1;
movl -12(%%ebp), %2": "=r"(a),"=r"(b),"=r"(c):);25 printf("\t x=%008x y=%008x p=%008x\n"
,c,b,little_big_endian(a));26 volcado(&x, &p);2728 return 0;29 }
TABLA 36
Figura 30 Salida del listado de la tabla 36
El listado de la tabla 36 permite observar el uso del apuntador de marco de pila
(apuntador base %ebp) para verificar el contenido de los registros de activación.
El operador &
El símbolo & es usado para determinar la dirección de una variable. En la línea
p=&x de la tabla 36 se almacena la dirección de la variable x en el apuntador p. De
hecho no es la única instrucción que utiliza el operador &. La función 'volcado'
requiere de la dirección inicial y la dirección final. En la instrucción volcado(&x,
&p), enviamos una dirección inicial (&x) y una dirección final(&p). La dirección de
x (0022FF4C) es almacenada en formato little endian (4CFF2200). Para convertir
de formato little endian podemos utilizar la instrucción “bswap” de ensamblador
(función little_big_endian). La impresión de contenidos mediante el uso de la
instrucción printf, concuerda con el volcado (ver ventana derecha).
Los contenidos de los registros de activación son extraídos utilizando ensamblador
en línea.
Aritmética de apuntadores
El uso del operador & permite determinar la dirección y realizar operaciones. La
suma o resta de cantidades de una dirección se le denomina aritmética de
apuntadores.
Figura 31
La figura 31 explica el uso de direcciones para realizar aritmética de apuntadores. El
entero p (4 bytes) es fragmentado en cuatro partes utilizando la siguiente secuencia
de acciones:
byte0: obtiene la dirección de la variable p (&p), mediante un cast (BYTE
*) se utiliza el primer byte de p. De todo esto obtenemos el contenido
mediante el uso de *.
byte1: obtiene la dirección de la variable p (&p), mediante un cast (BYTE *)
se hace referencia al primer byte y se suma 1 para referirnos al segundo byte
de p. De todo esto obtenemos el contenido mediante el uso de *.
byte2: obtiene la dirección de la variable p (&p), mediante un cast (BYTE *)
se hace referencia al primer byte y se suma 2 para referirnos al tercer byte de
p. De todo esto obtenemos el contenido mediante el uso de *.
byte3: obtiene la dirección de la variable p (&p), mediante un cast (BYTE
*) se hace referencia al primer byte y se suma 3 para referirnos al cuarto
byte de p. De todo esto obtenemos el contenido mediante el uso de *.
El cast (BYTE *) es necesario debido a que la dirección pertenece a un entero de
cuatro bytes y únicamente se requiere de un byte a la vez.
Arreglos y cadenas de caracteres
En el lenguaje C existe una relación entre apuntadores y arreglos. Como menciona
Kernighan, cualquier operación que pueda lograrse por indexación de un arreglo
también puede realizarse con apuntadores. La versión con apuntadores será por lo
general más rápida
Figura 32
En la línea 14 de la figura 32 declaramos arreglo de tamaño 20. El tamaño de
cualquier estructura puede ser calculado mediante la función size. En la línea 20
calculamos el tamaño de arreglo, mediante la determinación del número de bytes de
arreglo 80 bytes, dividido por el tamaño de un entero 4 bytes, esto indica que son
20 componentes.
En la línea 17, arreglo es manipulado como un apuntador. Con &arreglo, se
obtiene la dirección que sumada a la variable i permite acceder al elemento 0, 1, etc.
En la línea 18, usamos la función volcado para conocer el contenido de arreglo.
Mediante (int *)&arreglo+t-1, obtenemos la dirección del último componente del
arreglo.
Arreglos de varias dimensiones
Figura 33
Asignación de memoria dinámica
Puesto que la memoria es el recurso más valioso del que dispone la programación
de sistemas, en el capítulo VII mencionaremos los fallos de memoria en los que se
puede incurrir al utilizar la memoria dinámica, la administración de este recurso y
las posibles estrategias que se pueden utilizar para trazar el consumo de este
recurso. Pero el tema que nos interesa analizar en este apartado, es la utilización de
las funciones contenidas en la biblioteca estándar del lenguaje C y cuyo propósito es
realizar la petición de memoria dinámica al administrador de memoria del sistema
operativo. La asignación de memoria dinámica es lograda en tiempo de ejecución
del programa. Como lo hemos mencionado, el lenguaje C proporciona esta ventaja
mediante el uso de las funciones: calloc, malloc y realloc.
Para utilizar la función malloc, debemos respetar la siguiente sintaxis.
identificador = (tipo_base_identificador *)
malloc(sizeof(tipo_base_identificador)* num_componentes)
En donde
identificador: expresa un apuntador de cualquier tipo
tipo_base_identificador: el tipo de apuntador de identificador
num_componentes: número de elementos requeridos
El listado en la tabla 37 y su salida en la figura 34 explica el uso de la función
malloc para asignar memoria dinámica.
1 #include<stdlib.h>23 typedef unsigned char BYTE;4 void volcado(void *dirini, void *dirfin){5 int i,j;6 for(i=(int)dirini; i<(int)dirfin+4;i+=16){7 printf("%008x - ",i);8 for(j=0;j<16 && (i+j)<(int)dirfin+4;++j){9 if(j==8) printf(" -- ");10 printf("%002x ",*((BYTE *)(i+j)));11 }12 printf("\n");13 }14 }1516 int main(){17 int *p=NULL;18 int NumBloques=5;19 volcado(&NumBloques,&p);20 if((p=(int *)malloc(sizeof(int)*NumBloques))==NULL)21 perror("Error al asignar memoria dinámica");22 volcado(&NumBloques, &p);
23 free(p);24 if((p=(int *)malloc(sizeof(int)*NumBloques))==NULL)25 perror("Error al asignar memoria dinámica");26 volcado(&NumBloques, &p);27 p=NULL;28 return 0;29 }
TABLA 37
Figura 34
En el listado de la tabla 37, existen dos lugares en donde se asigna la memoria de
manera dinámica. Las líneas 20 y 24 asignan espacio de memoria dinámica
mediante el uso de la función malloc. Si el espacio no es asignado, la función
devolverá NULL. La función free en la línea 23 libera el espacio de memoria
dinámica. El volcado de memoria nos muestra que la dirección asignada al
momento de utilizar los dos malloc, es la dirección 6F0FA800. Es buena práctica de
programación utilizar la asignación a NULL después de usar la función free, de otra
forma el apuntador conservará la direccionando al mismo espacio asignado.
Estructuras
Las estructuras en el lenguaje C permiten agrupar información de diferentes tipos de
componentes, también llamados miembros. Una estructura se declara utilizando la
palabra struct.
Ejemplo
struct dato{
unsigned int miembro1;
unsigned int miembro2;
};
El programa del listado de la tabla 36 utiliza estructuras para crear una lista global.
1 #include<stdlib.h>2 #include<stdio.h>3 #include <malloc.h>4 typedef unsigned char BYTE;5 typedef signed long int SIGNED32;6 typedef signed short int SIGNED16;78 typedef unsigned int Direccion;9 typedef void * ApGenerico;10 struct nodo {11 Direccion siguiente;12 ApGenerico dato;13 };14 struct dato{15 unsigned int miembro1;16 unsigned int miembro2;17 };1819 typedef struct dato Dato;20 typedef Dato * ApDato;21 typedef struct nodo Nodo;22 typedef Nodo* ApNodo;2324 ApNodo lista=NULL;2526 ApNodo CreaNodo(ApNodo p, ApDato d){27 if((p=(ApNodo )malloc(sizeof(Nodo)))==NULL)28 perror("Error al asignar memoria dinamica");29 else{30 p->siguiente=0;31 p->dato=d;32 }
33 return p;34 }3536 ApGenerico CreaDato(ApGenerico dato, int m1, int m2){37 if((dato=(ApDato *)malloc(sizeof(Dato)))==NULL)38 perror("No existe espacio de memoria");39 else{40 ((ApDato)dato)->miembro1=m1;41 ((ApDato)dato)->miembro2=m2;42 }43 return dato;4445 }4647 InsertaAlFrente(int m1, int m2){48 ApNodo n;49 ApDato d;50 d=CreaDato(d,m1,m2);51 if(lista==NULL)52 lista=CreaNodo(lista,d);53 else{54 n=CreaNodo(n,d);55 n-> siguiente =(Direccion)lista;56 lista=n;57 }5859 }60 void volcado(void *dirini, void *dirfin){61 int i,j;62 for(i=(int)dirini; i<(int)dirfin+4;i+=16){63 printf("%008X - ",i);64 for(j=0;j<16 && (i+j)<(int)dirfin+4;++j){65 if(j==8) printf(" -- ");66 printf("%002X ",*((BYTE *)(i+j)));67 }68 printf("\n");69 }70 }7172 void ventanaHeap(void *p, SIGNED16 l1, SIGNED16 l2){73 _HEAPINFO informacionHeap;74 int estado;75 informacionHeap._pentry = NULL;76 if(l1>l2) return;77 while( ( estado = _heapwalk( &informacionHeap)) == _HEAPOK){
78 if(informacionHeap._useflag!=_USEDENTRY &&79 informacionHeap._size>0 &&80 (SIGNED32)p<(SIGNED32)informacionHeap._pentry &&81 (SIGNED32)informacionHeap._pentry-82 (SIGNED32)informacionHeap._size<(SIGNED32)p){83 printf("PUNTO ENTRADA BLOQUE: %X TAMAÑO: %08X\n",84 informacionHeap._pentry,informacionHeap._size);85 printf("-->VENTANA: DE <<%d>> A <<%d>> DEL PUNTO DE
ENTRADA\n",l1,l2);86 volcado((void *)informacionHeap._pentry+l1,
(void *)informacionHeap._pentry+l2);87 break;88 }89 }90 }91 int main(){92 InsertaAlFrente(0xDEADBABE,0xC0DEC0DE);93 InsertaAlFrente(0xBABEBABE,0xDEADDEAD);94 ventanaHeap((SIGNED32 *)lista,-184,-36);9596 return 0;97 }
TABLA 36
La figura 38 (salida del listado 36) muestra que los miembros de una misma
estructura son almacenados en espacios contiguos en el área de heap. Utilizando la
función ventanaHeap podemos abrir una ventana en el heap con el propósito de
analizar el contenido.
Figura 38
Algunas veces el compilador no coloca los miembros de una estructura en espacios
contiguos y para que así suceda se debe indicar de manera programática. El
siguiente fragmento de código muestra la instrucción packed para asignar espacios
contiguos de memoria en una estructura.
struct nodo{
Direccion siguiente;
ApGenerico dato;
}__attribute__((packed));
Las estructuras empaquetadas mejoran la sintaxis que se requiere para acceder a
memoria en dispositivos de entrada/salida.
Apuntadores a funciones
Un apuntador a función se distingue de los demás tipos de apuntador en que
contiene direcciones a código. Un apuntador a función se utiliza como un selector
entre funciones. La sintaxis para su declaración es
tipo_de_retorno (* identificador)(tipo_de_param_1, ..., tipo_de_param_n);
En donde
tipo_de_retorno: tipo de retorno que debe devolver la función seleccionada
identificador: nombre del apuntador
tipo_de_para1... : tipos en los parámetros que deben ser enviados
Figura 39
En los recuadros que aparecen en la figura 39 podemos revisar parte de la
traducción del lenguaje máquina a ensamblador. En estos recuadros se puede ver el
prólogo de las funciones f1 (recuadro rojo) y f2 (recuadro azul).
Aunque dedicaremos un capítulo por entero para analizar la traducción del lenguaje
ensamblador a lenguaje máquina, aquí daremos una breve explicación del proceso
de traducción utilizando los apuntadores a funciones.
El código analizado respeta la sintaxis at&t y expresa lo siguiente
La instrucción 55 puede ser traducida a lenguaje ensamblador: pushl %ebp.
La instrucción 89 E5 es traducida a ensamblador: movl %esp, %ebp
La instrucción: 83 EC 18 se traduce como: subl $0x18, %esp
La instrucción: C7 45 F8 FF FF FF FF se traduce como: movl
$0xFFFFFFFF, -8(%ebp).
En la última instrucción el código F8 representa el valor de -8 utilizando
complemento a 2.
Ejercicios1. Revise la salida del siguiente programa y verifique las coincidencias entre las direcciones
obtenidas con la función printf y el resultado de la función volcado.
int main(){char a='a';char *p; /* Declaración de un apuntador a caracteres */int *q; /* Declaración de un apuntador a enteros */
int i=3; q=&i; /* El apuntador q contiene la dirección de la variable i */p=&a; /* El apuntador p contiene la dirección de la variable a */ printf("Dirección de i (%x) Contenido de i (%x)\n",&i,i);printf("Dirección de q (%x) Contenido de q (%x)\n",&q,q);printf("Dirección de p (%x) Contenido de p (%x)\n",&p,p);printf("Dirección de a (%x) Contenido de a (%x)\n",&a,a);volcado (&i,&a);return 0;
}
Desarrolle el programa mostrado anteriormente utilizando para ello VC y MINGW, observe
las respectivas salidas y aprecie las diferencias.
2. Revise la salida del siguiente programa y observe el bloque de variables estáticas
iniciadas y no iniciadas.
static int a,b,c; /* Declaración de estáticas sin iniciar */static int d=0xaaaaaaaa,e=0xbbbbbbbb,f=0xcccccccc; /* Estáticas iniciadas */
int main(){
a=0xa1a1a1a1; /* Asignación tardía de la variables estáticas sin iniciar */b=0xa2a2a2a2;c=0xa3a3a3a3;volcado (&d,&c+1);return 0;
}
Desarrolle el programa mostrado anteriormente utilizando para ello VC y MINGW, observe
las respectivas salidas y aprecie las diferencias.
3. Analice el siguiente programa y verifique diferencias en la forma de ubicar los elementos
de un arreglo entre las herramientas VC y MINGW.int main(){
int *q; /* Declaración de un apuntador a enteros */ int i=3;
int y[] ={0xa1a1a1a1,0xa2a2a2a2,0xa3a3a3a3};q=&i; /* El apuntador q apunta a la misma región que la variable i.*/ volcado (&y,&q);return 0;
}4. Analice el siguiente código y verifique las diferencias existentes en las salidas de VC y
MINGW. Compare la forma de ubicar los miembros de una estructura en ambas salidasstruct nodo {
int *x;int *y;
};int main(){
int y[] ={1,2,3};int a;int b;struct nodo z;a=0x1A;b=0x1B;z.x=&a;z.y=&b;volcado (&z,(int *)&y+2);return 0;
}
5. El inciso (a) del siguiente código fue desarrollado en VC y el inciso (b) fue desarrollado
en MINGW. Explique las diferencias al determinar la dirección de retorno.
(a) int dirret =1;int *pdirret=&dirret;
void f(int *x, int *y){int *p;int *q;p=x;q=y;printf("p => Dirección (%x), contenido (%x), contenido del contenido (%x)\n",&p,p,*p);printf("q => Dirección (%x), contenido (%x), contenido del contenido (%x)\n",&q,q,*q);printf("x => Dirección (%x), contenido (%x), contenido del contenido (%x)\n",&x,x,*x);printf("y => Dirección (%x), contenido (%x), contenido del contenido (%x)\n",&y,y,*y); __asm {
MOV EAX , dword ptr pdirret MOV ebx,DWORD PTR [EBP+4]
MOV [eax], ebx }
printf("dirección de retorno=%x\n",*pdirret);printf("dirección de retorno=%x\n",*(((int *)&q)+6));printf("dirección de retorno=%x\n",*(((int *)&y)-2));volcado(&q, &y);
}int main(){
int a=1,b=2;f(&a,&b);return 0;
}
(b)int dirret =1;
void f(int *x, int *y){
int *p;
int *q;
p=x;
q=y;
printf("p => Dirección (%x), contenido (%x), contenido del contenido (%x)\n",&p,p,*p);
printf("q => Dirección (%x), contenido (%x), contenido del contenido (%x)\n",&q,q,*q);
printf("x => Dirección (%x), contenido (%x), contenido del contenido (%x)\n",&x,x,*x);
printf("y => Dirección (%x), contenido (%x), contenido del contenido (%x)\n",&y,y,*y);
__asm__("movl 4(%%ebp), %0":"=r" (dirret):);
printf("dirección de retorno=%x\n",dirret);
printf("dirección de retorno=%x\n",*(((int *)&q)+3));
printf("dirección de retorno=%x\n",*(((int *)&y)-2));
volcado(&q, &y);
}
int main(){
int a=1,b=2;
f(&a,&b);
return 0;
}
De sus observaciones anteriores, ¿qué puede concluir en relación con los marcos de pila
utilizados en MINGW y VC?
6. Explique el comportamiento del siguiente código utilizando para ello VC y MINGW.
Verifique las diferencias existentes.
int main(){int **p;p=(int **)malloc(sizeof(int *)*3);*(p)=(int *)malloc(sizeof(int)*3);*(p+1)=(int *)malloc(sizeof(int)*3);*(p+2)=(int *)malloc(sizeof(int)*3);*((int *)*(p+0)+0)=0xa1a1a1a1;*((int *)*(p+0)+1)=0xb1b1b1b1;*((int *)*(p+0)+2)=0xc1c1c1c1;*((int *)*(p+1)+0)=0xa2a2a2a2;*((int *)*(p+1)+1)=0xb2b2b2b2;*((int *)*(p+1)+2)=0xc2c2c2c2;*((int *)*(p+2)+0)=0xa3a3a3a3;*((int *)*(p+2)+1)=0xb3b3b3b3;*((int *)*(p+2)+2)=0xc3c3c3c3;printf("Direcciones de Pila\n");volcado(&p,&p);printf("Direcciones de Heap para p\n");volcado(p,p+2);printf("Direcciones de Heap para p[ ]\n");volcado( (int *)*(p),(int *)*(p+2)+2);return 0;
}
7. Analice y compare las salidas del siguiente código el cual debe ser desarrollado tanto en
VC como MINGW. Utilice el programa objdump para comparar el código generado en el
compilador MINGW. Mediante el proceso de desensamblado analice el resultado generado
por VC.int f1(int x, int y){
int a;int b;a=x;b=y;return 0;
}
int f2(int x, int y){int a;int b;b=x;a=y;return 0;
}
int main(){int (*func)(int, int)=0;func=f1;(*func)(0xAAAAAAAA,0xABABABAB);volcado (func, (BYTE *)func+21);func=f2;(*func)(0xBBBBBBBB,0xBCBCBCBC);volcado(func, (BYTE *)func+21);return 0;
}
Referencias bibliográficas
[Kernighan, Brian W.] El lenguaje de programación C, ISBN: 968-880-205-0
[Lewis, Daniel W] Fundamentals of Embedded Software Where C and Assembly
Meet, isbn 0-13-061589-7
CAPITULO VIIAsignación de Memoria de Manera
Dinámica
IntroducciónComo ya lo hemos mencionado en el Capítulo VI, la forma de realizar una petición
al sistema operativo para adquirir de manera programática una porción de memoria
dinámica es el uso de alguna de las funciones construidas para tal objetivo y que se
encuentran contenidas en las bibliotecas estándar de lenguaje C.
Cada administrador de memoria es dependiente de la arquitectura de hardware
subyacente y del sistema operativo que administra los recursos.
Con el continuo avance en el diseño de hardware y el incremento constante en las
velocidades, un término salta a la vista; NUMA (Non Uniform Memory Access),
acceso no uniforme a memoria. Como se menciona en la referencia [Gorman, Mel
2004] ‘En las computadoras de gran escala la memoria puede ser arreglada en
bancos de memoria los cuales incurren en diferentes costos de acceso dependiendo
de la distancia que exista entre estos y el procesador. Por ejemplo, un banco de
memoria puede ser asignado a cada CPU, o un banco de memoria puede ser usado
para acceso directo a memoria y el cual puede estar muy cercano al dispositivo
asignado’.
En los sistemas de Multiprocesamiento simétrico un único controlador de memoria
es compartido por varios CPU generando con ello un cuello de botella. La solución
a este problema es la utilización de la arquitectura NUMA.
Caso de estudio: el Heap del sistema operativo WindowsPuesto que el área de Heap es conflictiva si no se le trata con cuidado, es importante
verificar de manera constante su contenido realizando para ello trazas de memoria.
Tanto en el compilador MINGW como en Visual C++ existe la instrucción
_heapwalk para verificar el contenido del Hep. Como podemos ver en el mismo
archivo cabecera “malloc.h”, la función _heapwalk puede regresar los siguientes
valores:
#define _HEAPEMPTY (-1)
#define _HEAPOK (-2)
#define _HEAPBADBEGIN (-3)
#define _HEAPBADNODE (-4)
#define _HEAPEND (-5)
#define _HEAPBADPTR (-6)
Cuando finalizamos una traza del hep, la función _heapwalk regresará _HEAPOK.
La estructura _heapinfo (figura 35) proporciona información del Heap cuando se le
usa como argumento en la llamada a la función _heapwalk.
Figura 35
Un ciclo similar al mostrado a continuación permite realizar una traza del heap
while( ( estado = _heapwalk( &hinfo ) ) == _HEAPOK )
El listado de la tabla 38 muestra la forma de realizar una traza del heap mediante el
uso de la función _heapwalk().
1 #include <stdio.h>2 #include <malloc.h>34 void bloquesHeap()5 {6 _HEAPINFO informacionHeap;7 int estado;8 informacionHeap._pentry = NULL;9 while( ( estado = _heapwalk( &informacionHeap ) ) == _HEAPOK )10 {11 if(informacionHeap._useflag==_USEDENTRY)12 printf("BLOQUE USADO\t\t");13 else14 printf("BLOQUE LIBRE\t\t");15 printf(" DIRECCIÓN: %Fp\ttamaño: %08X\n",16
informacionHeap._pentry,informacionHeap._size);17 }18 switch( estado )19 {20 case _HEAPEMPTY:21 printf( "Heap no iniciado\n" );22 break;23 case _HEAPEND:24 printf( "Encuentra fin de cabecera\n" );25 break;26 case _HEAPBADPTR:27 printf( "Mal apuntador al Heap\n" );28 break;29 case _HEAPBADBEGIN:30 printf( "Cabecera inválida o no encontrada\n" );31 break;32 case _HEAPBADNODE:33 printf( "Heap dañado o inválido\n" );34 break;
35 }36 }3738 int main( void )39 {40 char *buffer;41 printf("-------Estado del Heap antes de malloc --------------\n");42 bloquesHeap();43 if( (buffer = (char *)malloc( 59 )) == NULL )44 perror("Error al asignar espacio");4546 else47 {48 printf("-------Estado del Heap después de malloc----------\n");49 bloquesHeap();50 free( buffer );51 printf("------- Estado del Heap deapués de free ----------\n");52 bloquesHeap();53 }54 }55
TABLA 34
En la línea 42 se llama a las función bloquesHeap() la cual realiza la traza. En la
línea 43 se asigna espacio de memoria dinámica (59 bytes) a la variable buffer. En
la línea 49 se vuelve a utilizar la función bloquesHeap(). En la línea 50 se libera el
espacio de memoria asignado. En la línea 52 se llama de nueva cuenta a la función
bloquesHeap(). La salida se muestra en la figura 36.
Figura 36
El programa de la tabla 35 utiliza la función _heapwalk() para analizar una ventana
de información en el Hep.
1 #include <stdio.h>2 #include<stdlib.h>3 #include <malloc.h>4 typedef unsigned char BYTE;
5 typedef signed short int SIGNED16;6 typedef signed long int SIGNED32;7 void volcado(void *dirini, void *dirfin){8 int i,j;9 for(i=(int)dirini; i<(int)dirfin+4;i+=16){10 printf("%008x - ",i);11 for(j=0;j<16 && (i+j)<(int)dirfin+4;++j){12 if(j==8) printf(" -- ");13 printf("%002x ",*((BYTE *)(i+j)));14 }15 printf("\n");16 }17 }18 void ventanaHeap(void *p, SIGNED16 l1, SIGNED16 l2){19 _HEAPINFO informacionHeap;20 int estado;21 informacionHeap._pentry = NULL;22 if(l1>l2) return;23 while( ( estado = _heapwalk( &informacionHeap)) == _HEAPOK){24 if(informacionHeap._useflag!=_USEDENTRY&& informacionHeap._size>0 &&25 (SIGNED32)p<(SIGNED32)informacionHeap._pentry &&26 (SIGNED32)informacionHeap._pentry-
(SIGNED32)informacionHeap._size<(SIGNED32)p){27 printf("PUNTO ENTRADA BLOQUE: %X TAMAÑO: %08X\n",
informacionHeap._pentry,informacionHeap._size);28 printf("-->HEAP: DE <<%d>> A <<%d>> DEL PUNTO DE ENTRADA\n",l1,l2);29 volcado((void *)informacionHeap._pentry+l1,
(void *)informacionHeap._pentry+l2);30 break;31 }32 }33 }34 main(){35 int *p;36 if((p=(int *)malloc(sizeof(int)*30))==NULL)37 perror("Error al asignar memoria dinamica");38 *((int *)p)=0xAAAAAAAA;39 *((int *)p+1)=0xBBBBBBBB;40 *((int *)p+2)=0x11111111;41 ventanaHeap((SIGNED32 *)p,-156,-128);42 printf("\n VOLCADO\n");43 volcado ((SIGNED32 *)p,(SIGNED32 *)p+2);44 }
TABLA 35
La línea 41 del listado de la tabla 35 invoca la función ventanaHeap enviándole la
dirección de la variable p. Los parámetros -156 y -128 son los valores que deben ser
sumados a la dirección inicial del bloque en donde se encuentra la variable p. Esa
suma es el límite superior e inferior de la ventana en el bloque de heap que debe ser
mostrada. El rango de la ventana es determinado mediante las líneas de código 25 y
26 de la función ventanaHeap.
La salida del código anterior proporciona una ventana al heap de tamaño -156 a -
128 como se muestra en la figura 37. Como podemos ver, la dirección (a80f3600)
de inicio en donde se almacena el contenido del apuntador p coincide tanto para la
función volcado, como para la función ventanaHeap.
Figura 37
Referencias Bibliográficas
[Gorman, Mel 2004] Understanding the Linux Virtual Memory Manager. Prentice
Hall 2004. ISBN: 0-13-145348-3