Tutorial 2: MessageBox

En este tutorial, crearemos un programa de Windows completamente funcional que despliega un cuadro con el mensaje "Win32 assembly is great!".

Baja el ejemplo aquí.

Teoría:

Windows tiene preparado una gran cantidad de recursos para sus programas. En el centro de esta concepción se ubica la API (Application Programming Interface = Interface de Programación de Aplicaciones) de Windows. La API de Windows es una enorme colección de funciones muy útiles que residen en el propio sistema Windows, listas para ser usadas por cualquier programa de Windows. Estas funciones están almacenadas en varias librerías de enlace dinámico [dynamic-linked libraries (DLL)] tales como kernel32.dll, user32.dll y gdi32.dll. Kernel32.dll contiene las funciones de la API que tienen que ver con el manejo de memoria y de los procesos. User32.dll controla los aspectos de la interface de usuario de tu programa. Gdi32.dll es la responsable de las operaciones gráficas. Además de estas "tres funcones principales", hay otras DLLs que nuestros programas pueden emplear, siempre y cuando tengas la suficiente información sobre las funciones de la API que te interesan.

Los programas de Windows se enlazan dinámicamente a estas DLLs, es decir, las rutinas de las funciones de la API no están incluidas en el archivo ejecutable del programa de Windows. Con el fin de que tu programa pueda encontrar en tiempo de ejecución las funciones de la API deseadas, tienes que meter esa información dentro del archivo ejecutable. Esta información se encuentra dentro de archivos .LIB. Debes enlazar tus programas con las librerías de importación correctas o no serán capaces de localizar las funciones de la API.

Cuando un programa de Windows es cargado en la memoria, Windows lee la información almacenada en el programa. Esa información incluye el nombre de las funciones que el programa usa y las DLLs donde residen esas funciones. Cuando Windows encuentra esa información en el programa, cargará las DLLs y ejecutará direcciones de funciones fijadas en el programa de manera que las llamadas transfieran el control a la función correcta.

Hay dos categorías de funciones de la API: Una para ANSI y la otra para Unicode. Los nombres de las funciones de la API para ANSI terminan con "A", por ejemplo, MessageBoxA. Los de Unicode terminan con "W" [para Wide Char (caracter ancho), pienso]. Windows 95 soporta ANSI y Windows NT Unicode.

Generalmente estamos familiarizados con las cadenas ANSI, que son arreglos de caracteres terminados en NULL. Un caracter ANSI tiene un tamaño de 1 byte. Si bien el código ANSI es suficiente para los lenguajes europeos, en cambio no puede manejar algunos lenguajes orientales que tienen millares de caracteres únicos. Esa es la razón por la cual apareció UNICODE. Un caracter UNICODE tiene un tamaño de 2 bytes, haciendo posible tener 65536 caracteres únicos en las cadenas.

Sin embargo, la mayoría de las veces, usarás un archivo include que puede determinar y seleccionar las funciones de la API apropiadas para tu plataforma. Sólo referencia los nombres de las funciones de la API sin su sufijo.

Ejemplo:

Presentaré abajo el esqueleto del programa. Luego lo completaremos.

.386.model flat, stdcall
.data
.code
start:
end start

Lsa ejecución se inicia inmediatamente después de la etiqueta especificada después de la directiva end. En el esqueleto de arriba, la ejecución iniciará en la primera instrucción inmediatamante debajo de la etiqueta start. Le ejecución procederá instrucción por instrucción hasta encontrar algunas instrucciones de control de flujo tales como jmp, jne, je, ret etc. Esas instrucciones redirijen el fujo de ejecución a algunas otras instrucciones. Cuando el programa necesite salir a Windows, deberá llamar a una fucnión de la API, ExitProcess.

ExitProcess proto uExitCode:DWORD

A la línea anterior la llamamos prototipo de la función. Un prototipo de función define los atributos de una función para que el ensamblador/enlazador pueda tipear y chequear para tí. El formato de un prototipo de función es:

FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...

En pocas palabras, el nombre de la función seguido por la palabra clave PROTO y luego por la lista de tipos de datos de los parámetros, separados por comas. En el ejemplo de arriba de ExitProcess, se define ExitProcess como una función que toma sólo un parámetro del tipo DWORD. Los prototipos de funciones son muy útiles cuando usas sintaxis de llamadas de alto nivel, como invoke. Puedes pensar en invoke como una llamada simple con chequeo-tipeo. Por ejemplo, si haces:

call ExitProcess

sin meter en la pila un valor dword, MASM no será capaz de cachar ese error para tí. Sólo lo notarás luego cuando tu programa se guinde o se quiebre. Pero si usas:

invoke ExitProcess

El enlazador te informará que olvidaste meter a la pila un valor dword y gracias a esto evitarás el error. Recomiendo que uses invoke en vez de call. La sintaxis de invoke es como sigue:

INVOKE  expresión [,argumentos]

expresión puede ser el nombre de una función o el nombre de un puntero de función. Los parámetros de la función están separados por comas.

Muchos de los prototipos de funciones para las funciones de la API se conservan en archivos include. Si usas el MASM32 de hutch, la encontrarás en la carpeta MASM32/include. Los archivos include tienen extensión .inc y los prototipos de función para las funciones en una DLL se almacenan en archivos .inc con el mismo nombre que la DLL. Por ejemplo, ExitProcess es exportado por kernel32.lib de manera que el prototipo de función para ExitProcess está almacenado en kernel32.inc.

También puedes crear prototipos para tus propias funciones.

A través de mis ejemplos, usaré windows.inc de hutch que puedes bajar desde http://win32asm.cjb.net

Ahora regresemos a ExitProcess, el parámetro uExitCode es el valor que quieres que el programa regrese a Windows después de que el programa termina. Puedes llamara ExitProcess de esta manera:

invoke ExitProcess, 0

Pon esa línea inmediatamente abajo de la etiqueta de inicio, y obtendrás un programa win32 que saldrá inmediatamente a Windows, pero no obstante será un programa válido.

.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
.code
invoke ExitProcess,0
start:
end start

la opción casemap:none dice a MASM que haga a las etiquetas sensibles a mayusculas-minusculas, así que ExitProcess y exitprocess son diferentes.

Nota una nueva directiva, include. Esta directiva es seguida por el nombre de un archivo que quieres insertar en el lugar donde se encuentra la directiva. En el ejemplo de arriba, cuando MASM procese la línea include \masm32\include\windows.inc, abrirá windows.inc que está en el directorio \MASM32\include y procesará el contenido de windows.inc como si pegaras ahí el contenido de windows.inc.

windows.inc de hutch contiene definiciones de constantes y estructuras que necesitas en la programación de win32. No contiene ningún prototipo de función. windows.inc es totalmente incomprensible. hutch y yo tratamos de poner tantas constantes y estructuras como sea posible pero quedan todavía muchas por incluir. Será constantemente actualizada. Chequea el homepage de hutch o el mío por actualizaciones.

De windows.inc, tus programas obtendrán las definiciones de constantes y estructuras. Pero para los prototipos de la funciones, necesitarás incluir otros archivos include. Puedes generar a partir de librerías de importación los archivos include que contienen sólo prototipos de funciones. Esbozaré ahora los pasos para generar los archivos include:

  1. Baja las librerías para el paquete MASM32 del homepage de hutch o del mío. Contiene una colección de librerías de importación que necesitas para programar para win32. también baja la utilidad l2inc.

  2. Desempaca (unzip) ambos paquetes dentro del mismo archivo. Si instalaste MASM32, desempácalos dentro del directorio MASM32\Lib

  3. Corre l2inca.exe con los siguientes conmutadores (switches):

    l2inca /M *.lib
  4. l2inca.exe extraerá información de las librerías de importación y creará archivos include llenos de prototipos de funciones.

  5. Mueve tus archivos include a tu carpeta de archivos de este tipo. Los usuarios de MASM32 deberán moverlos a la carpeta MASM32\include.

En nuestro ejemplo, llamamos la función exportada por kernel32.dll, así que necesitamos incluir los prototipos de las funciones de kernel32.dll. Ese archivo es kernel32.inc. Si lo abres con un editor de texto, verás que está lleno de prototipos de funciones de kernel32.dll. Si no incluyes kernel32.inc, puedes llamar todavía a call ExitProcess pero sólo con una sintaxis simple de llamada. No podrás usar invoke para "invocar" la función. El punto aquí es: para invocar una función, tienes que poner el prototipo de la función en alguna parte del código fuente. En el ejemplo anterior, si incluyes kernel32.inc, puedes definir los prototipos de funciones para ExitProcess en cualquier parte del código fuente antes del comando invoke para que trabaje. Los archivos include están ahí para liberarte del trabajo que significa escribirlas a cada momento que necesites llamar funciones con el comando invoke.

Ahora encontramos una nueva directiva, includelib. includelib no trabaja como include. Es la única menera que tiene tu programa de decirle al ensamblador que importe las librerías que necesita. Cuando el ensamblador ve la directiva includelib, pone un comando del enlazador dentro del mismo archivo objeto que genera, de manera que el enlazador sepa cuáles librerías necesita importar tu programa que deben ser enlazadas. Sin embargo, no estás obligado a usar includelib. Puedes especificar los nombres de las librerías de importación en la línea de comando del enlazador pero, créeme, es tedioso y la línea de comando sólo soporta 128 caracteres.

Ahora salvamos el ejemplo bajo el nombre msgbox.asm. Asumiendo que ml.exe está un tu entorno ("path"), ensamblamos msgbox.asm con:

Después de haber ensamblado satisfactoriamente msgbox.asm, obtendrás msgbox.obj. msgbox.obj es un archivo objeto. Un archivo objeto está a sólo un paso del archivo ejecutable. Contiene las instrucciones/datos en forma binaria. Sólo necesitas que el enlazador fije algunas direcciones en él.

Entonces enlacemos:

/SUBSYSTEM:WINDOWS  informa a Link qué tipo de ejecutable es este programa
/LIBPATH:<path to import library> dice a Link dónde se encuentran las librerías de importación. Si usas MASM32, estarán en el archivo MASM32\lib.

Link lee en el archivo objeto y lo fija con las direcciones de las librerías de importación. Cuando el proceso termina obtienes msgbox.exe.

Obtienes msgbox.exe. Vamos, córrelo. Encontrarás que no hace nada. Bien, todavía no hemos puesto nada interesante en él. Sin embargo, es un programa de Windows. ¡Y mira su tamaño! En mi PC, es de 1,536 bytes.

Vamos a ponerle ahora una caja de mensaje [Dialog Box]. Su prototipo de función es:

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD

hwnd es el manejador de la ventana padre. Puedes pensar en el manejador como un número que representa la ventana a la cual te refieres. Su valor no es tan importante para tí. Sólo recuerda que representa la ventana. Cuando quieres hacer algo con la ventana, debes referirte a ella por su manjador.
lpText es el puntero al texto que quieres desplegar en el área cliente de la caja de mensaje. En realidad, un puntero es la dirección de lago: Puntero a cadena de texto==Dirección de esa cadena.
lpCaption es un puntero al encabezado de la caja de mensaje
uType especifica el icono, el número y el tipo de botones de la caja de mensajes

Modifiquemos msgbox.asm para que incluya la caja de mensaje.
 

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.data
MsgBoxCaption  db "Iczelion Tutorial No.2",0
MsgBoxText       db "Win32 Assembly is Great!",0

.code
start:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start

Ensámblalo y córrelo. Verás un cuadro de mensaje desplegando el texto "Win32 Assembly is Great!".

Veamos de nuevo el código fuente.

Definimos dos cadenas terminadas en cero en la sección .data. Recuerda que toda cadena ANSI en Windows debe terminar en NULL (0 hexadecimal).

Usamos dos constantes, NULL y MB_OK. Esas constantes están documentadas en windows.inc. Así que nos referiremos a ellas por nombres y no por valores. Esto facilita la lectura de nuestro código fuente.

El operador addr es usado para pasar la dirección de una etiqueta a la función. Es válido sólo en el contexto de la directiva invoke. No puedes usarla para asignar la dirección de un registro/variable, por ejemplo. En vez de esto, puedes usar offset en el ejemplo anterior. Sin embargo hay algunas diferencias entre los dos:

  1. addr no puede manejar referencias delante de ella mientras que offset si puede. Por ejemplo, si la etiqueta está definida en una parte más adelante del código fuente que la directiva invoke, entonces addr no trabajará.
  2. invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK
    ......
    MsgBoxCaption  db "Iczelion Tutorial No.2",0
    MsgBoxText       db "Win32 Assembly is Great!",0

    MASM reportará error.Si usas offset en vez de addr en el recorte de código de arriba, MASM lo ensamblará felizmente.

  3. addr puede manejar variables locales mientras que offset no puede. Una variable local es un espacio reservado en algún lugar de la pila. No conocerás su dirección durante el tiempo de ejecución. offset es interpretado por el ensamblador durante el tiempo de ensamblaje. Así que es natural que offset no trabaje para variables locales. addr puede manejar variables locales debido a que el ensamblador chequea primero si la variable referida por addr es global o local. Si es una variable global, pone la dirección de la variable dentro del archivo objeto. En este aspecto, trabaja como offset. Si la variable es local, genera uina secuencia de instrucciones como la siguiente antes de llamar a la función:
  4. lea eax, LocalVar
    push eax


    Puesto que "lea" puede determinar la dirección de una etiqueta en tiempo de ejecución, esto trabaja bien.


Índice

Siguiente

[Iczelion's Win32 Assembly HomePage]

n u M I T_o r's   Programming Page

Este tutorial, original de Iczelion, ha sido traducido por:   n u M I T_o r  [ kUT ]

www.000webhost.com