- Importación por medio de valores ordinales. Por ejemplo, supongamos que desea invocar esta función DLL:
MiDLL!cierta_funcion ORD:20
. Entonces, el nombre simbólico es cierta_funcion, mientras que el valor ordinal es 20. Por lo general, no se recomienda importar los símbolos por ordinal, ya que los ordinales pueden cambiar entre versiones de la DLL. Sin embargo, si está seguro de que la versión de la DLL no cambiará o el desarrollador de la DLL garantiza la consistencia de los ordinales (por ejemplo, Usted es el desarrollador de la DLL y mantiene los ordinales fijos o simplemente distribuye la misma versión de DLL), entonces puede aprovechar el uso de ordinales. Importar por ordinal reduce significativamente el tiempo de carga del ejecutable y reduce el tamaño en bytes de la sección de importación. Por cierto, si planea usar siempre la misma compilación de DLL, considere vincular sus ejecutables mediante el proceso de binding. Esto reduce aún más el tiempo de carga. - Combinar símbolos de diferentes librerías DLL en una única librería de importación. Por ejemplo, puede crear una única librería que contenga las entradas de KERNEL32, USER32, GDI32 y otras API estándar de Windows, bajo un nombre común como win32api.lib. Puede incluir esta única librería de importación en lugar de especificar kernel32.lib, user32.lib, gdi32.lib y así sucesivamente.
- Al interactuar con DLL que no son de tipo STDCALL desde Visual Basic 6, PureBasic y otros entornos de desarrollo, es posible que la convención de llamado no sea compatible. ImpLib puede generar el código de conversión de CDECL y otras convenciones de llamado a STDCALL con muy poco peso adicional.
- ImpLib puede generar librerías sin saltos conocidos como original thunk. Cuando se utiliza una librería de importación libre de estos saltos,
el ejecutable final tampoco los incluye. Esto significa que se puede reducir el tamaño del ejecutable. El linker polink soporta correctamente
las librerías sin saltos en todos los casos. El linker de MS (el que se usa con Visual Studio) también puede utilizar este tipo de librerías,
pero con una limitación. Al combinar en un solo proyecto librerías con y sin saltos, las tablas de importación en el ejecutable final se pueden corromper.
Por lo tanto, se recomienda que todas las librerías tengan saltos o ninguna los tenga. GNU Linker (LD) parece soportar las librerías sin saltos
correctamente, si no se combinan con las que si tienen saltos. Las versiones más nuevas son más confiables. Para generar una librería de importación
libre de saltos agregue el siguiente comando:
KEEP_ORIGINAL_THUNKS equ 0
justo después deinclude 'implib.inc'
. Nótese que no es posible aplicar binding si se remueven los saltos. Por lo demás el ejecutable es válido en cualquier versión de Windows.
¿Cómo funciona?
El primer paso es generar un listado de los símbolos (es decir, funciones) exportados por la DLL. La sintaxis es muy simple y similar al formato de archivo DEF de Microsoft. Por ejemplo, hagamos una librería de importación con algunas funciones de Entrada/Salida exportadas por KERNEL32:
include 'implib.inc' ; KERNEL32.DLL implib kernel32.dll, CreateFileA implib kernel32.dll, CreateFileW implib kernel32.dll, ReadFile implib kernel32.dll, SetFilePointer implib kernel32.dll, CloseHandle endlib
Todo lo que esté precedido por un ';' es un comentario. Antes de comenzar a describir los símbolos, se debe colocar una referencia a implib.inc. Se requiere indicar la ruta relativa o absoluta, a menos que implib.inc esté en el mismo directorio donde se encuentra el archivo actual. Después de completar la lista, no olvide colocar el comando endlib. Cada definición de símbolo comienza con el comando implib, seguido de una serie de argumentos, siendo los más importantes el nombre de DLL y el nombre simbólico de la función. Puede especificar el valor ordinal en lugar del nombre simbólico, con el prefijo 'ord.':
implib kernel32.dll, ord.5Si desea que el linker indexe un símbolo con un nombre público diferente, agregue otro argumento con el nombre que desee:
implib kernel32.dll, CreateFileA, CreateFile
De esta manera, cuando se hace referencia a CreateFile en el código objeto, el linker lo resolverá como KERNEL32!CreateFileA. Por lo tanto, no es necesario especificar el sufijo 'A'. Esta función es útil para traducir nombres simbólicos. Por ejemplo, las herramientas de MS asumen que la función STDCALL CreateFileA con 7 argumentos debe llamarse _CreateFileA@28. Teniendo esto en cuenta, podemos reescribir nuestro primer ejemplo para que sea compatible con las convenciones de nombres de MS:
include 'sdk\implib.inc' ; KERNEL32.DLL implib kernel32.dll, CreateFileA, _CreateFileA@28 implib kernel32.dll, CreateFileW, _CreateFileW@28 implib kernel32.dll, ReadFile, _ReadFile@20 implib kernel32.dll, SetFilePointer, _SetFilePointer@16 implib kernel32.dll, CloseHandle, _CloseHandle@4 endlib
Como puede ver, los nombres STDCALL tienen prefijo '_' y sufijo '@#', donde # el número de argumentos x4. En realidad, # es el número de bytes que ocupan todos los parámetros de la función cuando son almacenados en la pila. Usualmente este valor equivale al número de argumentos x4.
Guardemos este listado con el nombre kernel32.def. Puede elegir cualquier otro nombre. Estamos listos para compilarlo desde la línea de comando:
bin\fasm kernel32.def kernel32.lib flat assembler version 1.67.23 (533081 kilobytes memory) 3 passes, 4247 bytes.
¡Obtuvimos un archivo llamado kernel32.lib! Por cierto, FASM es realmente un ensamblador, pero su preprocesador es tan avanzado que podemos usarlo para ejecutar toda la lógica de ImpLib :-)
Existen más argumentos opcionales para implib. Puede abrir implib.inc en un editor de texto plano y leer el encabezado con la descripción detallada de todos los argumentos.
DLL2DEF
Es una herramienta de línea de comandos para extraer en formato de texto plano los símbolos de las bibliotecas de vínculos dinámicos.
Las DLL grandes pueden contener muchísimas funciones. Listarlas manualmente en un archivo de texto sería una tarea demasiado tediosa. Esta herramienta,
la cual se incluye dentro del subdirectorio \bin, sirve para automatizar este proceso. Al ejecutar dll2def
, sin argumentos de línea de comandos, obtenemos la siguiente respuesta:
USAGE: dll2def [options] file [output] file - input file name. output - optional output file name. options: /COMPACT - Don't place any comments. /PB - Enable PureBasic mod. /VB - Enable Visual Basic 6 mod. Example: dll2def \windows\system32\kernel32.dll test.def |
Por ejemplo, procedamos a ejecutar:
dll2def c:\windows\system32\kernel32.dll
¡Obtuvimos un archivo llamado kernel32.def con el listado de todas las funciones públicas de KERNEL32.DLL! La referencia de implib.inc contiene la ruta completa (solo si implib.inc se encuentra en el mismo directorio que dll2def.exe). Por lo tanto, puede mover el archivo a cualquier lugar que desee sin tener que actualizar la ubicación de implib.inc. Cada definición de símbolo comienza con un par de comentarios, a saber:
; KERNEL32.AddVectoredExceptionHandler ORD:9 ; -> NTDLL.RtlAddVectoredExceptionHandler implib kernel32.dll, AddVectoredExceptionHandler
Los comentarios incluyen el nombre simbólico completo (si está disponible), el valor ordinal y la referencia de otras DLL, si corresponde. Conocer el ordinal es útil si existe una buena razón para utilizar la importación por ordinal. La referencia de otras DLL significa que el símbolo realmente corresponde a otra DLL. En el ejemplo anterior, el símbolo AddVectoredExceptionHandler de KERNEL32.DLL realmente corresponde a una función de NTDLL. Puede actualizar la definición del símbolo para importar directamente desde la DLL real, pero generalmente no se recomienda hacer esto por razones de compatibilidad. Si no desea ver estos comentarios en el listado, simplemente incluya el argumento /COMPACT en el llamado a DLL2DEF:
sdk\dll2def /COMPACT c:\windows\system32\kernel32.dll
Modificaciones
Actualmente existen 2 modificaciones o mods:
- El mod de Visual Basic sirve para interactuar con DLL de tipo CDECL y otras que no son STDCALL en Visual Basic 6.
- El mod de PureBasic es útil para crear Userlibs de PureBasic que interactúen con DLL externas, especialmente con DLL de tipo CDECL.
Reemplace el comando implib con vbimplib o pbimplib en el listado ".def" para usar las modificaciones de Visual Basic o PureBasic respectivamente. Al llamar a DLL2DEF, agregue el parámetro /VB o /PB en la línea de comandos para que se use el mod correspondiente. Puede consultar los tutoriales a continuación para ver ejemplos más completos.
Tutoriales
Luego de leer la introducción anterior, los siguientes minitutoriales presentan ejemplos de uso real:
- Propósito general: MSVCRT.lib para MASM32 y NASM.
- Mod de PureBasic: OpenAL PureBasic Userlib.
MSVCRT.lib para MASM32 y NASM
La librería de tiempo de ejecución (runtime) de Microsoft Visual C, conocida como MSVCRT.DLL, exporta más de 700 funciones, automatizando muchas tareas de programación comunes, como formateo de Entrada/Salida por consola, administración de memoria, manipulación de cadenas de texto, ordenamiento, etc. Las mismas funcionalidades pueden ser implementadas por medio de la API de Windows. Sin embargo, dado que la librería de C casi siempre está preinstalado en todas las versiones de Windows (excepto en Windows 95), tiene sentido utilizar esta DLL en lugar de crear versiones propias de printf(), malloc(), etc. Entonces, el objetivo de nuestro primer tutorial es crear un par de programas de ejemplo en MASM32 y NASM usando MSVCRT.DLL.
Comencemos con MASM32. Ya existe un archivo de cabecera para MSVCRT, llamado msvcrt.inc, en la última versión de MASM32, pero no se incluye la librería de importación msvcrt.lib necesaria para el linker. Si tiene Visual Studio instalado, puede obtener la versión dinámica de msvcrt.lib desde allí, pero hay un pequeño conflicto de nombres con las funciones div() y fabs(). No se puede usar div o fabs como nombre de una función en MASM32 o NASM, ya que se genera un conflicto con los mnemónicos (conjunto de instrucciones x86) del mismo nombre. El mismo error se presenta cuando se usan nombres con solo caracteres numéricos, o cualquiera de las palabras reservadas por el compilador, como proc, label, etc. Las librerías de importación en Visual Studio a veces introducen dependencias adicionales de OLDNAMES.lib, UUID.lib y otras. Parece que no se permite redistribuir la librería de importación de Visual Studio original. Por eso MASM32 no incluye msvcrt.lib.
Por lo tanto, crearemos nuestro propio MSVCRT.lib sin derechos de autor de terceros, sin generar conflicto con las palabras reservadas de MASM o NASM.
El primer paso es crear el listado con todos los nombres públicos que se encuentran en MSVCRT.DLL:
dll2def /COMPACT \windows\system32\msvcrt.dll
A continuación, necesitamos resolver el conflicto de nombres. Podemos abrir msvcrt.def en el Bloc de notas y buscar la siguiente línea:
implib msvcrt.dll, div
Dado que div es el mnemónico de división de enteros sin signo, necesitamos cambiar el nombre de esta función. Hutch sugirió agregar el prefijo '_crt_' (refiérase al foro oficial de masm32 para mayor detalle). Entonces, este está el reemplazo:
implib msvcrt.dll, div, _crt_div
Hagamos lo mismo con la función de fabs. Se recomienda agregar '_' como prefijo en el alias de salto (thunk) para todos los nombres de funciones que no comiencen con este caracter. Esto permite llamar a estas funciones a través de la macro invoke en MASM32. Por ejemplo:
; MSVCRT.printf ORD:742 implib msvcrt.dll, printf
Se recomienda actualizar la definición de la siguiente manera:
; MSVCRT.printf ORD:742 implib msvcrt.dll, printf, _printf
La misma definición incluyendo el ordinal sería así:
; MSVCRT.printf ORD:742 implib msvcrt.dll, ord.742, _printf, __imp__printf
Dado que el nombre real de la función se pierde cuando se usa su ordinal, se recomienda especificar el alias de salto (_printf) y el nombre público de importación (__imp__printf) explícitamente en este caso. ImpLib no necesita estos nombres, pero hacer el llamado a printf() será complicado sin los nombres ;-)
Compilemos la librería. Dado que ImpLib se ejecuta de modo interpretado por el preprocesador de FASM y el listado contiene más de 700 funciones, puede tardar un tiempo en completar la tarea (hasta un par de minutos en una máquina rápida):
\sdk\fasm msvcrt.def msvcrt.lib flat assembler version 1.67.23 (518320 kilobytes memory) 3 passes, 35.4 seconds, 460585 bytes.
Ahora tenemos nuestro propio msvcrt.lib :-) O una serie de errores de sintaxis, símbolos duplicados u otros mensajes poco alentadores. Hay que corregir los errores y compilar de nuevo. Una versión de msvcrt.def lista para usar y un msvcrt.lib precompilado están disponibles en \src\test.
Para llamar a funciones C en MASM32 usando invoke, necesita especificar los prototipos. Hay un ejemplo de un archivo de cabecera con prototipos C en \src\test\msvcrt.inc. Como alternativa, puede usar PROTO en lugar de externdef, si desea llamar a la librería de C por medio de saltos (thunks). Por ejemplo:
printf PROTO C :DWORD,:VARARG
El llamado a printf() usando invoke con el prototipo anterior generará call <jmp msvcrt!printf>
en lugar de
call msvcrt!printf
. En otras palabras, se utilizará un salto adicional. El uso de estos saltos no favorece el rendimiento. Por eso, en la
mayoría de los casos se recomienda crear los prototipos de funciones externas con externdef, aunque la sintaxis sea menos elegante:
cdecl_dword_vararg typedef PROTO C :DWORD,:VARARG externdef _imp__printf:PTR cdecl_dword_vararg printf equ <_imp__printf>
Esta vez, invoke printf() generará una llamada directa, sin saltos.
A continuación se presenta un ejemplo completo para MASM32 que realiza una llamada a MSVCRT!printf:
.386 .model flat,stdcall ; MSVCRT API include msvcrt.inc includelib msvcrt.lib .CODE format db "Hola mundo!",0 start: invoke printf,OFFSET format ret END start
Compilemos, enlacemos y ejecutemos el ejemplo desde la línea de comando:
\masm32\bin\ml /c /coff test_masm32.asm \masm32\bin\link /SUBSYSTEM:CONSOLE test_masm32.obj test_masm32 Hola mundo!
El mismo ejemplo, pero para NASM:
EXTERN __imp__printf %define printf [__imp__printf] section .text format db "Hola mundo!",0 GLOBAL _start _start: push format call printf add esp,4 ; fix the stack ret
Compilemos, enlacemos y ejecutemos:
\nasm\nasmw -fwin32 test_nasm.asm \nasm\polink /ENTRY:start /SUBSYSTEM:CONSOLE test_nasm.obj msvcrt.lib test_nasm Hola mundo!
Con esto se concluye el tutorial de MSVCRT.LIB.
OpenAL PureBasic Userlib
La intención es generar un módulo Userlib para PureBasic que interactúe con una DLL externa. La DLL de ejemplo es OPENAL32.DLL del paquete de distribución de OpenAL v1.0 / v1.1. Esta DLL usa la convención de llamado CDECL para toda la API. Por lo tanto, no podemos usar la herramienta de importación de DLL de PureBasic. Podríamos usar PureBasic v4 o posterior con la sintaxis de ImportC y el archivo OpenAL32.lib que se incluye en el SDK de OpenAL, o simplemente usar OpenLibrary seguido de CallCFunctionFast cada vez que necesitemos invocar una función OpenAL. Posiblemente existan soluciones alternativas más complejas, como usar una DLL contenedora, etc. En este tutorial crearemos una librería Userlib para PureBasic, que nos dará acceso directo a OpenAL, con una sobrecarga mínima. Las llamadas serán más rápidas en comparación con CallCFunctionFast, contaremos con validación de prototipos, ayuda en línea y cualquier otra ventaja que ofrece Userlib. También funcionará en PureBasic v3.
En primer lugar, necesitamos crear el listado de símbolos de importación:
dll2def /PB /COMPACT \windows\system32\OpenAL32.dll
Esto creará un archivo llamado openal32.def, con la lista de todos los símbolos públicos que se encuentran en OpenAL32.dll. Por ejemplo:
pbimplib OpenAL32.dll, STDCALL, 0, alBuffer3f
La sintaxis completa del comando pbimplib es la siguiente:
pbimplib dllname, convention, numarguments, real_name, PureBasics_name, public_name
El parámetro dllname
contiene el nombre de archivo de la DLL. El parámetro convention
identifica la convención de
llamado para la función especificada. Actualmente se admiten los siguientes valores:
- STDCALL se usa por defecto en la mayoría de las DLL de Windows: KERNEL32.DLL, USER32.DLL, ...
- CDECL se usa principalmente en las DLL de C: MSVCRT.DLL, OPENAL32.DLL, ...
- PBCALL es la convención de llamado de PureBasic. No se espera que esta convención se use en una DLL externa. Se proporciona a manera de ejemplo para mostrar cómo se extiende la funcionalidad de PBIMPLIB. Alguien podría considerar agregar convenciones de llamado más exóticas, como PASCAL, FASTCALL, etc.
numarguments
indica la cantidad de valores de 32 bits que se requieren para almacenar los argumentos de la función en la pila.
Generalmente coincide con la cantidad de argumentos, excepto cuando se trata de parámetros de 64 bits (double, __int64, etc.)
Este valor no se usa realmente en STDCALL. real_name
es el nombre de la función, tal y como aparece en la tabla de exportación de la
DLL. Los 2 últimos parámetros son opcionales. PureBasics_name
permite personalizar el nombre de la función en PureBasic.
Por defecto, cuando no se especifica, se utiliza real_name
. public_name
está reservado para casos muy específicos,
generalmente relacionados con la programación en ensamblador. Este parámetro permite especificar el nombre del símbolo para llamar a una función
directamente, sin utilizar saltos (thunks). Generalmente no es necesario preocuparse por esto. Revisemos el listado generado una vez más:
pbimplib OpenAL32.dll, STDCALL, 0, alBuffer3f
alBuffer3f() no es una función STDCALL y no recibe 0 argumentos. DLL2DEF no puede adivinar la cantidad de argumentos y la convención de llamado porque no puede leer la "Guía del programador de OpenAL" :-) El uso de "STDCALL, 0" evita la reparación automática de la pila después de llamar a la función dada. El prototipo correcto debería ser:
pbimplib OpenAL32.dll, CDECL, 5, alBuffer3f
Puede actualizar las demás definiciones o simplemente usar el listado ya corregido en src\PureBasic\openal32.def. Compilemos:
fasm openal32.def pbopenal.lib
¿Sin errores? - Bien. El siguiente paso es crear una descripción de librería con el mismo nombre que la librería de importación recién creada, pero con extensión .desc. En el ejemplo actual este archivo debería llamarse pbopenal.desc. Consulte en PureBasic\Library SDK\Readme.txt si no conoce la herramienta LibraryMaker.
; Lenguaje de la librería: ASM o C. En este caso debe ser C. C ; Número de DLL de Windows requeridas para esta librería: ninguna en este caso. 0 ; El tipo de librería (puede ser OBJ o LIB). Usaremos LIB, ya que la librería es pbopenal.lib. LIB
El resto del archivo depende del contenido real de la librería. Como ejemplo podemos usar src\PureBasic\pbopenal.desc. Finalmente, usemos LibraryMaker.exe para crear nuestra Userlib:
LibraryMaker.exe pbopenal.desc /COMPRESSED
Simplemente mueva esta Userlib a PureBasic\PureLibraries\UserLibraries e intente compilar una aplicación de prueba simple. Puede encontrar un ejemplo de PureBasic OpenAL en el paquete de uFMOD (o µFMOD).
¡Gracias por leerme! Espero que este material haya sido de utilidad.