Práctica 2 Entorno de programación

Anuncio
Práctica 2
Entorno de programación
1. Proceso de ensamblaje
2. Visual Studio .NET
1. Proceso de ensamblaje
El proceso de ensamblado tiene el siguiente esquema:
Listado
Editor
fuente 1
obj 1
Ensamblador
fuente 2
obj 2
fuente 3
obj 3
Linker
1.1. Ensamblador
A partir de los fuentes obtenidos de un editor, los fuentes se envían al Ensamblador el cual,
traduce los nemotécnicos del lenguaje ensamblador al código objeto de la maquina. Y crea un
fichero especial de listado de variables y procedimientos. Por último el Linker toma todos los
ficheros objeto y con la información sobre las direcciones de memoria para las variables y
procedimientos que contiene el fichero Listado, crea el fichero ejecutable final.
Normalmente en el proceso de ensamblado bastan con 2 pasos para determinar todas las
direcciones, por ejemplo:
Jmp etiq
…
etiq:
En este caso, cuando el ensamblador no puede determinar la dirección de etiq, ya que ésta está
definida más adelante, con lo que debe dejar un espacio y dar una segunda pasada para
determinar exactamente el dirección de memoria a la que debe saltar.
Por lo tanto, en el 1er paso se utiliza una variable denominada “contador de posición” ($). A
medida que se analiza el programa, se va incrementando dicha variable en función de los bytes
que necesita cada sentencia. Por ejemplo:
$
.CODE
comi: mov ax, @data
mov ds, ax
mov cx, cont
(3 bytes)
(2 bytes)
(5 bytes)
0
3
5
1
Práctica 1: Introducción a la programación en ensamblador
repetir: dec cx
…
(1 byte)
9
10
Para cada segmento se inicializa el contador de posición. Ejemplo
$
.DATA
NUM db -29
VECTOR db 100 DUP (?)
CONT dw 5
…
0
1
101
103
Para cada símbolo (variable, constante, etiqueta), se determina su $, su tipo, su nombre de
segmento. Con esta información, ensamblador construye la TABLA DE SIMBOLOS en esta
primera pasada. Luego sólo hay que buscar en esa tabla para determinar la dirección de cada
etiqueta, constante o variable.
El 2º paso comienza cuando se detecta END. Ayudado por las directivas y por los nemotécnicos
de las instrucciones se comienza a general el código máquina. Ejemplo:
MOV CX, CONT
1º comprobar si el tipo de fuente y destino coinciden
2º comprobar si CONT está en el segmento de datos
3º sustituir CONT por su desplazamiento, que en este caso es 101.
No siempre es tan simple:
- Pueden existir expresiones aritméticas (que se calculan en este segundo paso). Ejemplo:
VAR1 EQU VAR2 + 10
En principio el ensamblador usa valores por defecto. Pero en todo caso, si 2 pasadas no fueran
suficientes da un “Phase error”, Puede indicarle al ensamblador que de más pasadas.
- Otra dificultad aparece con las llamas instrucciones “relocalizables”, por ejemplo:
JMP ETIQ
Con ETIQ en otro segmento. Estos valores son se sabe pues no tienen que ver el contador $ (que
equivale a un desplazamiento) sino que hay que tener en cuenta es segmento en el que está. Por
lo tanto no se conocerá el valor correcto hasta que el programa sea colocado en memoria. Para
ello los ejecutables .EXE disponen de una cabecera con “información de relocalización” donde se
indican las instrucciones relocalizables, y poder completar el código una vez situado el programa
en memoria.
1.2. Linker
El problema de la relozalización: suponer un procesador con memoria lineal. El ensamblador
comienza siempre en 0, pero al cargar el programa en memoria no estará en la cero. En el 80x86,
es fácil ya que lo único que hay que hacer es colocar el valor segmento correcto, y seguir los
desplazamientos indicados de $.
El problema de las llamadas “externas”:
2
Departamento de Arquitectura y Tecnología de Computadores: Universidad de Sevilla
CALL proc
MOV AX, SEGMENT
JMP far ETIQ
En todos estos casos se hacen referencias a otros módulos. El ensamblador trata cada modulo
por separado y el linker soluciona estos problemas.
El linker une todos los .obj de forma lineal, asumiendo un solo espacio de direcciones. Añade la
cabecera de “relocalización” (en los ejecutable .exe).
2. Visual Studio .NET
La siguiente figura muestra un esquema simplificado de los principales elementos (programas y
ficheros) que se manejan en el desarrollo de una aplicación, en este caso usando lenguaje C:
Son necesarias una serie de operaciones para crear un programa ejecutable que funcione
correctamente a partir de uno o varios ficheros fuente:
• Editar los ficheros fuente con un programa editor.
• Compilarlos.
• Enlazarlos con el linker para conseguir el fichero ejecutable.
• El ejecutable debe probarse con un programa depurador. Si encontramos errores repetiremos el
ciclo anterior.
Al conjunto de programas que realizan estas o cualquier otra operación destinada a generar y
probar programas se le llama entorno de desarrollo. Tras cada operación se generan ficheros
temporales para comunicar resultados al programa que ejecuta la siguiente operación, y mensajes
para informar al programador del estado del proceso. Necesitamos al menos 4 programas
distintos, cada uno de ellos con sus propios parámetros, ficheros de entrada y salida, y mensajes
de error o información. Para facilitar el manejo de estos programas vamos a utilizar un entorno de
desarrollo integrado (IDE: Integrated Development Environment).
Un IDE no es más que un programa que integra el software necesario para el proceso de
desarrollo. Como mínimo integra funciones del editor, compilador, linker, depurador y manejador
de proyectos (project management). Un proyecto es un fichero o ficheros que contiene la
3
Práctica 1: Introducción a la programación en ensamblador
información necesaria para generar un programa: los ficheros fuente que hay que compilar, las
opciones de compilación/enlace, ficheros de salida, listado, etc.
2.1. El entorno de desarrollo Visual Studio .Net.
Este entorno de desarrollo permite utilizar distintos lenguajes de programación para generar un
programa. En TPBN usaremos solamente las características para programar en lenguaje C/C++ y
ensamblador. Es lo que se conoce como Visual C++ .Net (Visual C++ ó VC++).
Utilizaremos VC++ para crear y analizar programas que nos permitan conocer cómo interaccionan
procesador y compilador.
Crear un proyecto.
Empezaremos creando un proyecto y el programa C que vamos a analizar. Con Visual C++ hay
que seguir los siguientes pasos:
X Ejecutar Visual Studio .Net. Aparecerá la ventana principal del programa, con este aspecto:
Fig. 2. Pantalla principal de Visual C++. Puede haber pequeñas variaciones en el aspecto Debido al
añadido o supresión de paneles durante la última sesión con el IDE. Se recomienda cerrar los
paneles y ventanas que no se vayan a usar.
X Crear un proyecto.
• Pulsar Ctrl-Mayús-N (o seleccionar en el menú principal Archivo ÆNuevo Æ Proyecto).
Aparece la ventana que permite seleccionar el tipo de fichero o proyecto que se quiere
crear:
4
Departamento de Arquitectura y Tecnología de Computadores: Universidad de Sevilla
• La siguiente ventana es la del asistente para aplicaciones, donde hay que activar la opción
de Proyecto vacío y el tipo de aplicación: aplicación de consola.
Fig. 4. Configuración de la aplicación.
• Crear un fichero .c con el programa principal:
Fig. 5.
Aparecerá la ventana Agregar nuevo Elemento, donde se introduce el nombre del archivo fuente
de nuestro programa: Principal.c. Esto crea el fichero Principal.c en disco y lo abre en la ventana
de edición. ES MUY IMPORTANTE ASEGURARSE QUE LA EXTENSIÓN DE LOS FICHEROS C
ES .c Y NO .cpp ó .C.
•
Escribir el código C.
Compilar y depurar el proyecto.
El programa se compila pulsando la tecla F7 (o seleccionando Generar Æ Generar Solución).
La depuración de un programa se suele hacer colocando puntos de interrupción en las líneas de
código que interese. Para activar un punto de interrupción (breakpoint), hay que pinchar con el
ratón en el inicio de la línea del código fuente donde se quiere parar y pulsar F9 (o pulsar
5
Práctica 1: Introducción a la programación en ensamblador
RATON_DERECHA Æ Insertar/Quitar punto de interrupción). El punto aparecerá marcada en la
ventana de edición.
Las teclas más útiles para usar el depurador (debugger) son1:
X Ejecutar depurador (F5), terminar depuración (Mayúscula + F5)
X Mostrar ventana de desensamblado (Alt + 8 ó Depurar Æ Ventanas Æ Desensamblador)
X Mostrar ventana de registros (Alt + 5)
X Mostrar ventana de memoria (Alt + 4)
X Ejecutar paso a paso: Pinchar ventana de desensamblado y pulsar F11
Todas las ventanas del depurador se pueden organizar dentro de la ventana principal de la forma
que resulte más cómoda.
La siguiente figura muestra la ventana de desensamblado, que permite ver las direcciones donde
están almacenados el código y la variable de nuestro programa. La ventana de registros también
permite ver el valor del puntero de pila, ESP, que señala el final de la pila.
Fig. 6. Ventanas de depuración.
Como se ve, la dirección que señala el puntero de pila es menor que las direcciones donde se
almacena código o datos, lo que nos indica que la pila está colocada en las posiciones más bajas
de la memoria.
2.2. Cómo funcionan los depuradores.
Un depurador es un programa capaz de parar la ejecución de otro cuando se dan determinadas
circunstancias:
X Se ha llegado a un punto determinado del código (punto de parada o breakpoint de
código).
X Se ha ejecutado una sola instrucción (ejecución paso a paso).
X Se accede a datos o se intenta ejecutar código de zonas de memoria fuera de las
asignadas al programa (detección de accesos ilegales a memoria).
X Se intenta acceder a una variable determinada (breakpoint de datos).
X etc.
Cuando ocurre algo de esto, se produce una excepción, es decir, el procesador salva su estado
(valor de los registros internos) y salta a una función del sistema operativo que debe manejar la
6
Departamento de Arquitectura y Tecnología de Computadores: Universidad de Sevilla
excepción. El proceso es muy similar al de una interrupción hardware. La función del sistema
operativo normalmente pasa el control al depurador, que informa al programador.
Las circunstancias concretas tratables por un depurador dependen fundamentalmente del
hardware de depuración del procesador. Hay grandes diferencias entre distintos procesadores.
Algunos no disponen de ninguna facilidad, por lo que lo único que pueden hacer los depuradores
son breackpoints de código y ejecución paso a paso4.
Los procesadores con arquitectura IA32 (Pentium y sucesores) disponen de un hardware muy
completo para depuración y análisis de prestaciones. Cuatro registros de depuración, DR0 a DR3
(Debug Registers), permiten especificar las direcciones a analizar, y con dos registros de control,
DR6 y DR5, se indican cuáles son las condiciones que provocan la excepción de depuración:
breackpoints de código o datos, ejecución paso a paso y muchas más.
La variedad de condiciones que es capaz de detectar un procesador IA32 es realmente extensa,
por lo que el hardware de depuración es complicado de manejar. De hecho, muchos depuradores
no utilizan todas las condiciones.
Una vez parada la ejecución del programa que se está depurando, la información a mostrar
depende fundamentalmente del sofware del depurador. En el caso de VC++ la tenemos agrupada
en ventanas:
X Ventana de código. El código se puede mostrar en varios formatos:
•
Desensamblado. Es el más básico: siempre se puede utilizar, tenga o no información de
depuración el ejecutable.
•
Desensamblado + fuente. Muestra el código ensamblador que corresponde a cada
sentencia del código fuente. Para usarlo es necesario que el depurador disponga del
fichero fuente y que el fichero ejecutable tenga información de depuración.
•
Sólo fuente.
Fig. 9. Ventana de código con programa desensamblado y fuente.
•
Ventanas de memoria. Muestran el contenido de cualquier zona de la memoria virtual del
programa en formato hexadecimal de 8 bits (byte), 16 (Short Hex) o 32 (Long hex). El
programador puede modificar el contenido de la memoria que aparece en la ventana.
Fig. 10
7
Práctica 1: Introducción a la programación en ensamblador
•
Ventana de registros. Contiene los registros que se pueden modificar desde una aplicación de
usuario5.
Fig. 11. Ventana con los registros enteros y los indicadores (flags).
•
Ventanas de inspección (watch). Permite seleccionar elementos puntuales para ver y
modificar su contenido: registros, posiciones de memoria, variables, etc.
Fig. 12
•
Ventana de variables. Permite ver y modificar el valor de las variables locales.
•
Ventana de pila de llamadas. Muestra el árbol de llamadas de la instrucción de código donde
nos hemos detenido por última vez.
2.3. Puntos a tener en cuenta.
Las ventanas de la pantalla principal (Fig. 2).
Las funciones de las ventanas más importantes son:
X Ventanas de edición: Sirven para editar los ficheros con código fuente.
X Ventana de proyectos. Permite manejar y organizar los ficheros fuente del proyecto.
X Ventana de mensajes: es donde aparecen los mensajes de error o estado que se generan
al ejecutar un elemento del entorno de desarrollo: compilador, linker, etc.
Tipos de proyectos (Fig. 3).
Cuando seleccionamos el tipo de proyecto estamos escogiendo unas opciones por defecto para el
proceso de desarrollo: las opciones de compilación, las librerías y ficheros objeto con los que se
enlazará, opciones de depuración, etc. Al seleccionar una aplicación de tipo consola le decimos a
VC++ que active las opciones de compilación necesarias para que nuestra aplicación pueda usar
las funciones de manejo de consola estándar de C: printf, getc, putc, etc.
Extensiones de los archivos fuente (Fig. 5).
La extensión del nombre de un fichero fuente determina las opciones por defecto de
compilación/linkado. Algunas extensiones, como .c ó .cpp, ya tienen preasignadas estas opciones.
Esto quiere decir que si llamamos a un programa Principal.c, se compilará de forma ligeramente
distinta que si lo llamamos Principal.cpp. La mayor diferencia es que un fichero .c sólo puede usar
las características de C, y no de C++. En principio, si un programa sólo usa C, podría tener
cualquiera de las dos extensiones. Sin embargo, internamente se compilará de forma distinta, y
algunas características del código generado también serán distintas.
Por tanto, para evitar confusiones a la hora de analizar el código generado por el compilador, hay
que procurar usar la extensión .c en todos los programas de prácticas.
8
Departamento de Arquitectura y Tecnología de Computadores: Universidad de Sevilla
2.4. Compilación externa en Visual C++.
Algunos entornos de desarrollo pueden compilar ficheros de un proyecto con compiladores
externos. El mecanismo concreto para hacerlo dependerá del entorno de desarrollo.
Supongamos, por ejemplo, que inicialmente todos los ficheros fuente del proyecto son programas
C que se compilan con el compilador interno de Visual Studio. El proyecto tendría la siguiente
estructura:
Cuando genera el ejecutable, Visual Studio procesa automáticamente los ficheros fuente que
componen el proyecto. Para los que tengan extensión conocida por Visual Studio (.h, .c, .cpp, ...),
el IDE ejecuta el compilador interno que tiene asociada la extensión3. Una vez obtenido todos los
ficheros objeto, el IDE lanza el linker interno para generar el ejecutable.
Para las extensiones conocidas por Visual Studio el IDE sabe qué compilador utilizar, cómo usarlo
(con qué opciones) y cuál es el resultado de la compilación (el fichero objeto).
Para compilar algún fichero fuente con un compilador “externo” a Visual Studio hay que indicar al
IDE cómo tiene que tratarlo. Esto implica que:
X El fichero fuente debe tener una extensión no conocida para el IDE. En caso contrario,
usará la compilación por defecto para esa extensión.
X Hay que indicarle al IDE que debe hacer una compilación a medida (Custom Build) para
ese fichero.
Vamos a modificar el proyecto, para que uno de los ficheros se compile usando un ensamblador
externo. La estructura del proyecto quedaría:
9
Práctica 1: Introducción a la programación en ensamblador
Para ello se tendría que borrar el fichero Combina3.c y crear uno nuevo llamado Combina3.asm.
Como el IDE no sabe cómo manejarlos, no hace ninguna operación sobre ficheros .asm a menos
se lo indiquemos explícitamente activando la compilación personalizada (Custom Build). La forma
de hacerlo es:
X Seleccionamos el fichero en la ventana Explorador de soluciones y pulsamos el botón de la
derecha del ratón.
•
Seleccionar Propiedades en el menú que aparezca.
X El IDE mostrará la ventana de propiedades asociadas al fichero. Seleccionando la entrada
Paso de generación personalizada (Fig. 3) podremos ajustar los tres campos necesarios para
integrar el compilador externo:
•
Línea de comandos: Es la línea de comandos que ejecutará el IDE para procesar el fichero.
•
Descripción: Es el mensaje que mostrará el IDE cuando procese el fichero fuente.
•
Resultado: Es el resultado de la compilación externa. Le indica al linker (vinculador o
enlazador) interno el fichero que tiene que usar.
10
Departamento de Arquitectura y Tecnología de Computadores: Universidad de Sevilla
Fig. 3. Ventana Paso de generación personalizada.
En nuestro ejemplo, la compilación de combina3.asm tendrá los siguientes parámetros:
X Línea de comandos: \masm32\bin\ml.exe /Zi /Zd /Cp /c /coff /Gd "$(InputPath)". Hay que
asegurarse que el ensamblador o compilador externo esté previamente instalado y que
tiene el path y las opciones correctas en las líneas de comandos.
X Descripción: Ensamblando "$(InputPath)". El IDE sustituye la cadena $(InputPath) por el
nombre completo del fichero de entrada, incluyendo su path.
X Resultado: "$(InputName).obj". El IDE sustituye la cadena $(InputName) por el nombre
completo del fichero, incluyendo path, pero excluyendo la extensión.
Las distintas cadenas de sustitución que pueden utilizarse en los campos de la ventana Paso de
generación personalizada se insertan con el botón Macro de la ventana que aparece cuando se
pulsa “…”.
Ya estaría integrado el fichero ensamblador en el proceso de construcción del ejecutable. El IDE
lanza automáticamente el ensamblador cada vez que se necesite, mostrando los mensajes de
error o estado en la ventana de Resultados:
11
Práctica 1: Introducción a la programación en ensamblador
3. Primer programa en Ensamblador
El siguiente código corresponde a un programa escrito en ensamblador que suma dos vectores de
enteros almacenando el resultado en un tercero.
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
.data
vect1 DD 10 dup (1,2,3,4,5,6,7,8,9,10)
vect2 DD 10 dup (10,9,8,7,6,5,4,3,2,1)
vectres DD 10 DUP (?)
.code
start:
mov ecx, 10
xor edx,edx
inibucle:
mov eax, vect1[edx]
add eax ,vect2[edx]
mov vectres[edx] , eax
add edx,4
loop inibucle
call ExitProcess
end start
12
Descargar