- 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 escierta_funcion
, mientras que el valor ordinal es 20. Por lo general, no se recomienda realizar importación 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. La importación 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 enlazar 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 al utilizar librerías de importación sin saltos.
Para generar una librería de importación sin dichos saltos agregue el siguiente parámetro de configuración:
KEEP_ORIGINAL_THUNK equ 0
justo antes de la primera declaraciónimplib
. 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.
Las librerías de importación generadas por ImpLib SDK son compatibles con cualquier linker que soporte el formato MS COFF. Los siguientes linker se han probado exhaustivamente en materia de compatibilidad:
- Pelle's Linker, también llamado polink, es un linker gratuito y confiable creado por Pelle Orinius, el desarrollador de Pelle's C. No se han detectado problemas de compatibilidad al utilizar librerías de importación con y sin saltos, de 32 y 64 bits.
- El linker de MS forma parte de Visual C++ y el SDK de Windows. Todas las versiones son compatibles. La versión de 32 bits puede producir el error LNK1102 al utilizar librerías de importación que excedan cierto tamaño. ImpLib SDK emite una advertencia de compatibilidad cuando la librería de importación exceda dicho tamaño máximo. Normalmente, esto puede suceder cuando la DLL exporta más de 1500 símbolos. El tamaño de cada símbolo también cuenta. La solución es utilizar el linker de 64 bits. Al utilizar el linker de MS, no se recomienda combinar librerías de importación con y sin saltos. Es más seguro utilizar solamente librerías con saltos o sólo sin saltos, pero no ambos tipos de librerías al tiempo. De lo contrario, las tablas de importación en el ejecutable final podrían corromperse.
- El linker de GNU, también conocido como LD, se puede encontrar en las instalaciones de MinGW. Se admiten DLL de 32 y 64 bits. Este linker se puede utilizar para realizar una compilación cruzada para Win32 o Win64 desde Linux u otro sistema operativo. Soporta librerías de importación con y sin saltos.
Instalación y uso
Desempaque ImpLib SDK en cualquier directorio. Luego ejecute build_libs.bat
.
Este archivo por lotes crea o recrea todas las librerías de importación incluidas de ejemplo, a saber: MSVCRT, API del sistema para Win32 y Win64.
El proceso de compilación puede tardar varios minutos en completarse debido al gran tamaño de las librerías de ejemplo.
Los archivos de salida se almacenan bajo el subdirectorio lib
. Se crean dos librerías de importación por cada archivo de entrada: una librería de importación normal que incluye los saltos (original thunk) y una versión sin los saltos (sin original thunk).
Esta última se almacena en el subdirectorio stripped
.
Si agrega nuevas definiciones para librerías de importación o modifica los archivos de ejemplo, simplemente vuelva a ejecutar build_libs.bat
.
Sólamente se recompilarán los archivos nuevos o modificados.
¿Cómo funciona?
El primer paso es generar un listado de los símbolos (es decir, funciones) exportados por la DLL. El listado se almacena en formato de texto plano.
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 disponibles en KERNEL32.DLL
:
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 ';' es un comentario. Antes de comenzar a declarar los símbolos, se debe agregar una referencia ya sea a implib.inc
(si la DLL es de 32 bits), ya sea a implib64.inc
(si la DLL es de 64 bits).
Se requiere indicar la ruta relativa o absoluta, a menos que implib.inc
o implib64.inc
esté en el mismo directorio donde se encuentra el archivo actual.
Después de completar la lista, no olvide agregar la macro endlib
. Cada definición de símbolo comienza con la macro implib
, seguida por
el nombre de DLL y el nombre simbólico de la función. Puede especificar el valor ordinal en lugar del nombre simbólico, colocando el prefijo 'ord.':
implib kernel32.dll, ord.5
Si 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 de 32 bits CreateFileA
con 7 argumentos debe llamarse _CreateFileA@28
.
Teniendo esto en cuenta, si estamos enlazando con la DLL de 32 bits KERNEL32.DLL, podemos reescribir nuestro primer ejemplo para que sea compatible con las convenciones de nombres de MS:
include 'src\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 en modo de 32 bits tienen como prefijo el signo de subrayado (_
) y el sufijo es el signo de arroba seguido de un valor decimal (@#
).
Ese valor decimal es el tamaño en bytes de la lista de argumentos. Generalmente, este valor es el número de argumentos x4, ya que la mayoría de los argumentos son de 32 bits o se encuentran alineados a 32 bits (32 bits = 4 bytes).
Esta convención de nombres no se aplica en modo de 64 bits. Por lo tanto, si pretendemos enlazar con la versión de 64 bits de KERNEL32.DLL, el listado sería como sigue:
include 'src\implib64.inc'
; KERNEL32.DLL
implib kernel32.dll, CreateFileA
implib kernel32.dll, CreateFileW
implib kernel32.dll, ReadFile
implib kernel32.dll, SetFilePointer
implib kernel32.dll, CloseHandle
endlib
Como habrá notado, la DLL se llama KERNEL32, aunque la versión sea de 64 bits (Win64). Microsoft mantiene intencionalmente los mismos nombres de DLL.
Guardemos el listado, ya sea la versión de 32 o 64 bits, con el nombre kernel32.def
. Puede elegir cualquier otro nombre. Estamos listos para compilarlo desde la línea de comandos:
bin\fasm kernel32.def kernel32.lib flat assembler version 1.67.23 (533081 kilobytes memory) DLL symbols indexed: 5 3 passes, 4313 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 el motor de ImpLib.
Existen más argumentos opcionales para la macro implib
. Puede abrir implib.inc
o implib64.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. DLL2DEF soporta DLL tanto de 32 como de 64 bits.
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:
bin\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
, la cual corresponde a DLL de 32 bits, o su equivalente implib64.inc
para las DLL de 64 bits contiene
la ruta completa si el archivo inc
se encuentra en el mismo directorio que dll2def.exe
.
Por lo tanto, puede mover el archivo def
a cualquier ubicación sin tener que actualizar la ruta del archivo inc
.
Cada definición de símbolo comienza con unos 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 hacia otra DLL significa que el símbolo exportado
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 agregue /COMPACT
justo antes del nombre de archivo de entrada en el llamado a DLL2DEF:
bin\dll2def /COMPACT c:\windows\system32\kernel32.dll
Modificaciones
Actualmente existen 2 modificaciones o mods:
- El mod de Visual Basic sirve para utilizar las 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 la macro implib
con vbimplib
o pbimplib
en el archivo 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.
Los siguientes minitutoriales presentan ejemplos de uso reales:
- 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 1300 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á preinstalada en todas las versiones de Windows (excepto en Windows 95), podría ser útil hacer uso de 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, allí puede obtener la versión dinámica de
msvcrt.lib
, 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 sólo 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 nuestra propia 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:
bin\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.
Steve Hutchesson (Hutch), el creador del SDK de MASM32, sugirió agregar el prefijo '_crt_
' (refiérase al foro oficial de masm32 para mayor detalle). Entonces, quedaría como sigue:
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 en modo interpretado por el preprocesador de FASM y el archivo def
contiene muchísimas funciones,
puede tardar un tiempo en completar la tarea:
\bin\fasm msvcrt.def msvcrt.lib flat assembler version 1.67.23 (518320 kilobytes memory) DLL symbols indexed: 1372 3 passes, 21.4 seconds, 845227 bytes.
Ahora tenemos nuestra propia 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 está disponibles en \src\MSVCRT32
.
Para llamar las funciones de C en MASM32 usando invoke
, necesita especificar los prototipos. Hay un ejemplo de un archivo de cabecera con prototipos
C en \src\MSVCRT32\test_masm\msvcrt.inc
. Como alternativa, puede usar PROTO
en lugar de externdef
, si desea llamar a la librería de C por medio de saltos
(thunk). Por ejemplo:
printf PROTO C :DWORD,:VARARG
El llamado a printf()
usando invoke
con el prototipo anterior generará call <jmp msvcrt!printf>
en lugar del llamado directo
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
:
cdecl_dword_vararg typedef PROTO C :DWORD,:VARARG externdef _imp__printf:PTR cdecl_dword_vararg printf equ <_imp__printf>
Ahora la invocación de printf()
generará un llamado directo, sin saltos.
A continuación se presenta un ejemplo completo para MASM32 que utiliza 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.asm \masm32\bin\link /SUBSYSTEM:CONSOLE test.obj test 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\nasm -fwin32 test.asm \nasm\polink /ENTRY:start /SUBSYSTEM:CONSOLE test.obj msvcrt.lib test Hola mundo!
Con esto se concluye el tutorial de MSVCRT.LIB.
OpenAL PureBasic Userlib
Vamos a generar un módulo Userlib para PureBasic que interactúe con una DLL externa. La DLL de ejemplo es OPENAL32.DLL, la cual ofrece la API
de OpenAL v1.0 / v1.1. Esta DLL usa la convención de llamado CDECL para sus funciones de API.
Podríamos usar 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 de OpenAL. En este tutorial tomaremos un camino alterno: crearemos una librería Userlib para
PureBasic, la cual nos dará acceso directo a OpenAL, con muy poco código adicional. Las llamadas hacia la API de OpenAL serán más rápidas en comparación con CallCFunctionFast,
contaremos con validación de prototipos, ayuda en línea, además de otras ventajas que ofrece Userlib.
En primer lugar, necesitamos crear el listado de símbolos de importación:
bin\dll2def /PB /COMPACT \windows\system32\OpenAL32.dll
Esto creará un archivo llamado openal32.def
, con la lista de los símbolos públicos que se encuentran en OpenAL32.dll. Por ejemplo:
pbimplib OpenAL32.dll, STDCALL, 0, alBuffer3f
La sintaxis completa de la macro pbimplib
es la siguiente:
pbimplib dllname, convention, numarguments, real_name, PureBasics_name, public_name
El parámetro dllname
contiene el nombre del archivo 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. 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 este valor coincide con la cantidad real de argumentos, excepto cuando se trata de parámetros de 64 bits (double
, __int64
, etc.)
Este valor no se usa 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 (sin thunk). 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. El uso de "STDCALL, 0" evita la restauración automática de la pila después del llamado a la función. 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:
bin\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 este ejemplo el archivo debería llamarse Pbopenal.desc
. Consulte en [PureBasic]\SDK\Readme.txt
para mayor información
acerca de la herramienta LibraryMaker.
; Lenguaje de la librería: ASM o C. En este caso debe ser C. C ; Cuántas DLL de Windows requiere 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 ; Cuántas librerías de PureBasic requiere esta librería: ninguna en este caso. 0 ; El nombre del directorio con la guía de ayuda en línea para la API de esta librería. Help
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 .\Pbopenal.desc
Nota: Asegúrese de que la ruta donde se encuentra LibraryMaker no tenga espacios en blanco.
Si la compilación fue exitosa, simplemente mueva la Userlib a [PureBasic]\PureLibraries\UserLibraries
e intente compilar una aplicación de prueba.
Puede encontrar varios ejemplos de PureBasic con OpenAL en PureBasic OpenAL SDK y en uFMOD (o µFMOD).
¡Gracias por leerme! Espero que este material haya sido de utilidad.