0-ensamblador y c

147
Lenguajes: Ensamblador y C Un enfoque conjunto

Upload: bruno-mau

Post on 16-Feb-2016

10 views

Category:

Documents


1 download

DESCRIPTION

Programación

TRANSCRIPT

Page 1: 0-Ensamblador y C

Lenguajes: Ensamblador y CUn enfoque conjunto

Page 2: 0-Ensamblador y C

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.

Page 3: 0-Ensamblador y C

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

Page 4: 0-Ensamblador y C

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,

Page 5: 0-Ensamblador y C

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.

Page 6: 0-Ensamblador y C

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.

Page 7: 0-Ensamblador y C

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.

Page 8: 0-Ensamblador y C

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

Page 9: 0-Ensamblador y C

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

Page 10: 0-Ensamblador y C

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

Page 11: 0-Ensamblador y C

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

Page 12: 0-Ensamblador y C

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.

Page 13: 0-Ensamblador y C

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

Page 14: 0-Ensamblador y C

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

Page 15: 0-Ensamblador y C

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

Page 16: 0-Ensamblador y C

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

Page 17: 0-Ensamblador y C

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>

Page 18: 0-Ensamblador y C

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.

Page 19: 0-Ensamblador y C

Figura 10

Figura 11

Page 20: 0-Ensamblador y C

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

Page 21: 0-Ensamblador y C

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

Page 22: 0-Ensamblador y C

-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

Page 23: 0-Ensamblador y C

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

Page 24: 0-Ensamblador y C

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

Page 25: 0-Ensamblador y C

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:

Page 26: 0-Ensamblador y C

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.

Page 27: 0-Ensamblador y C

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)

Page 28: 0-Ensamblador y C

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

Page 29: 0-Ensamblador y C

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

Page 30: 0-Ensamblador y C

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

Page 31: 0-Ensamblador y C

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.

Page 32: 0-Ensamblador y C

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

Page 33: 0-Ensamblador y C

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.

Page 34: 0-Ensamblador y C

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

Page 35: 0-Ensamblador y C

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

Page 36: 0-Ensamblador y C

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.

Page 37: 0-Ensamblador y C

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]

Page 38: 0-Ensamblador y C

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

Page 39: 0-Ensamblador y C

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

Page 40: 0-Ensamblador y C

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.

Page 41: 0-Ensamblador y C

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).

Page 42: 0-Ensamblador y C

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

Page 43: 0-Ensamblador y C

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

Page 44: 0-Ensamblador y C

__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

Page 45: 0-Ensamblador y C

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

Page 46: 0-Ensamblador y C

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

Page 47: 0-Ensamblador y C

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

Page 48: 0-Ensamblador y C

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

Page 49: 0-Ensamblador y C

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

Page 50: 0-Ensamblador y C

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

Page 51: 0-Ensamblador y C

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

Page 52: 0-Ensamblador y C

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

Page 53: 0-Ensamblador y C

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

Page 54: 0-Ensamblador y C

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.

Page 55: 0-Ensamblador y C

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

Page 56: 0-Ensamblador y C

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).

Page 57: 0-Ensamblador y C

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.

Page 58: 0-Ensamblador y C

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).

Page 59: 0-Ensamblador y C

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

Page 60: 0-Ensamblador y C

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

Page 61: 0-Ensamblador y C
Page 62: 0-Ensamblador y C

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.

Page 63: 0-Ensamblador y C

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

Page 64: 0-Ensamblador y C

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:

Page 65: 0-Ensamblador y C

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,

Page 66: 0-Ensamblador y C

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.

Page 67: 0-Ensamblador y C

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.

Page 68: 0-Ensamblador y C

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

Page 69: 0-Ensamblador y C

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

Page 70: 0-Ensamblador y C

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.

Page 71: 0-Ensamblador y C

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

Page 72: 0-Ensamblador y C

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

Page 73: 0-Ensamblador y C

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

Page 74: 0-Ensamblador y C

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

Page 75: 0-Ensamblador y C

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

Page 76: 0-Ensamblador y C

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

Page 77: 0-Ensamblador y C

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.

Page 78: 0-Ensamblador y C

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.

Page 79: 0-Ensamblador y C

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.

Page 80: 0-Ensamblador y C

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;

Page 81: 0-Ensamblador y C

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.

Page 82: 0-Ensamblador y C

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

Page 83: 0-Ensamblador y C

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("-- ");

Page 84: 0-Ensamblador y C

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

Page 85: 0-Ensamblador y C

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;

Page 86: 0-Ensamblador y C

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.

Page 87: 0-Ensamblador y C

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.

Page 88: 0-Ensamblador y C

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 *.

Page 89: 0-Ensamblador y C

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.

Page 90: 0-Ensamblador y C

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

Page 91: 0-Ensamblador y C

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);

Page 92: 0-Ensamblador y C

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.

Page 93: 0-Ensamblador y C

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 }

Page 94: 0-Ensamblador y C

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){

Page 95: 0-Ensamblador y C

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

Page 96: 0-Ensamblador y C

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

Page 97: 0-Ensamblador y C

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

Page 98: 0-Ensamblador y C

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.

Page 99: 0-Ensamblador y C

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 }

Page 100: 0-Ensamblador y C

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.

Page 101: 0-Ensamblador y C

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;

Page 102: 0-Ensamblador y C

}

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

Page 103: 0-Ensamblador y C

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.

Page 104: 0-Ensamblador y C

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

Page 105: 0-Ensamblador y C

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;

Page 106: 0-Ensamblador y C

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.

Page 107: 0-Ensamblador y C

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;

Page 108: 0-Ensamblador y C

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

Page 109: 0-Ensamblador y C

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