- Импорт по ординалу. Например, Вы собираетесь вызвать следующую функцию из DLL:
MYDLL!dummy_function ORD:20
. Символьное имя функции — dummy_function, а ординал — 20. Обычно не советуют импортировать функции по ординалу, т.к. значение ординала может измениться к следующей версии DLL. Но, если есть уверенность, что версия DLL не изменится, или разработчик гарантирует совместимость по ординалам (например, если это ваша DLL и Вы фиксированно назначаете ординалы или всегда используете одну и ту же версию), тогда Вы можете заметно повысить скорость загрузки ваших экзешников при использовании ординалов вместо символьных имён. В добавок к тому, ещё и размер секции импорта получается меньше. Кстати, если Вы всегда используете один и тот же билд DLL, тогда ещё лучше использовать байнд (bind) модулей, чтобы максимально поднять скорость загрузки. - Совмещение символов из разных DLL в одной библиотеке импорта. Например, Вы можете собрать в одной библиотеке, назвав её, скажем, win32api.lib, символы из основных системных DLL, как то: KERNEL32, USER32, GDI32 и других библиотек Windows API. Тогда Вы сможете добавлять в опции линкера эту единственную библиотеку вместо комбинации из kernel32.lib, user32.lib, gdi32.lib и т.д.
- При использовании не-STDCALL DLL в Visual Basic 6, PureBasic и других популярных средах разработки, соглашение о вызове той или иной функции может не поддерживаться компилятором. ImpLib умеет генерировать легковесные переходники для приведения CDECL и других соглашений к стандартному STDCALL.
- ImpLib поддерживает создание библиотек без оригинальных санков (original thunk). После линковки со "стрипнутой" библиотекой, экзешник также будет лишён оригинальных санков. Таким образом, экзешник получается меньшего размера. Линкер polink полностью поддерживает "стрипнутые" библиотеки импорта. Линкер MS (тот, что входит в Visual Studio) также воспринимает без-санковые библиотеки, но с одним немаловажным ограничением. Иногда, при совмещении "стрипнутых" и "нестрипнутых" библиотек импорта в одном и том же проекте, таблицы импорта получаются "бракованными". Поэтому, если Вы используете линкер MS, не рискуйте совмещать полноценные библиотеки импорта со "стрипнутыми": используйте либо те, либо другие, но не оба вида сразу. Линкер GNU (LD) поддерживает "стрипнутые" библиотеки, но тоже не любит совмещение их с обычными. Последние версии в этом плане более надёжны. Чтоб библиотека получилась без оригинальных санков, просто добавьте следующий флаг:
KEEP_ORIGINAL_THUNKS equ 0
сразу послеinclude 'implib.inc'
. "Стрипнутый" импорт нельзя байндить, но во всех остальных смыслах он полноценен и работает во всех Windows.
Как это использовать?
Прежде всего нужно написать скрипт с определениями экспортируемых символов (т.е. функций) из заданной DLL. Синтаксис довольно прост и похож на формат DEF Microsoft. Давайте рассмотрим пример с функциями ввода-вывода из библиотеки 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
Всё, что начинается с ';', является комментарием. Перед списком символов нужно указать ссылку на файл implib.inc. Нужно указывать полный или относительный путь, если только данный файл не расположен в одном со скриптом каталоге. В конце списка не забудьте указать замыкающую команду endlib. Каждое определение символа начинается с команды implib, среди параметров которой можно выделить в первую очередь имя DLL и имя экспортируемого символа. Вместо символьного имени можно указать ординал — для этого предусмотрена приставка 'ord.':
implib kernel32.dll, ord.5
Если Вы хотите, чтобы данная функция была доступна линкеру под каким-либо другим именем, укажите это имя в дополнительном параметре:
implib kernel32.dll, CreateFileA, CreateFile
Таким образом, при обращении к функции CreateFile, линкер на самом деле создаст ссылку на KERNEL32!CreateFileA. Таким образом, можно опустить суффикс 'A' в обьектном файле. Данная фишка полезна в основном для совместимости с различного рода манглингом. Например, компиляторы MS полагают, что функция CreateFileA с 7ю параметрами по соглашению STDCALL должна называться _CreateFileA@28. Давайте исправим наш первоначальный пример для совместимости с соглашением 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
Как видите, имена фукций STDCALL начинаются с приставки '_' и заканчиваются суффиксом '@#', где # — это количество параметров умноженное на 4. На самом деле # — это размер параметров в байтах, занимаемый ими в стеке, но в большинстве случаев это равняется количеству параметров на 4.
Сохраните скрипт. Давайте назовём его kernel32.def и запустим компилятор из командной строки:
bin\fasm kernel32.def kernel32.lib flat assembler version 1.67.23 (533081 kilobytes memory) 3 passes, 4247 bytes.
Получаем файл с именем kernel32.lib! Кстати, FASM на самом деле является ассемблером, но его препроцессор настолько развит, что на нём можно разрабатывать скриптинговые движки вроде ImpLib :-)
Есть дополнительные параметры для более тонкой настройки, о которых можно узнать в заглавной части файла implib.inc.
DLL2DEF
Это утилита для извлечения в текстовом виде экспортируемых символов из динамических библиотек DLL.
Так как в больших библиотеках может быть очень много полезных функций, описывать их вручную может оказаться весьма нудным занятием. Лучше воспользоваться утилитой в каталоге \bin. Для начала, откроем командный интерпретатор и введём dll2def
без параметров, чтобы получить следующее сообщение:
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 |
Итак, давайте запустим:
dll2def c:\windows\system32\kernel32.dll
Получаем файл kernel32.def, в котором уже определены все символы, которые экспортируются данной библиотекой. Ссылка на implib.inc содержит полный путь (только если implib.inc находится в одном каталоге с dll2def.exe), чтобы можно было свободно перемещать скрипт в любой другой каталог, не заботясь о пути к implib.inc. Перед каждым символом есть пара строчек с комментариями:
; KERNEL32.AddVectoredExceptionHandler ORD:9 ; -> NTDLL.RtlAddVectoredExceptionHandler implib kernel32.dll, AddVectoredExceptionHandler
В комментариях указано полное имя символа (если доступно), ординал и ссылка на настоящий символ, если это цепочка форварда (т.н. forwarder chain). Сведения об ординале полезны для замены символьного имени на ординал, если у Вас есть на то веские основания. Цепочка форварда означает, что символ на самом деле находится в другой DLL. В предыдущем примере символ AddVectoredExceptionHandler, который экспортируется в KERNEL32.DLL, в конечном итоге принадлежит NTDLL. Вы можете переписать определение форварда дабы направить импорт на настоящую DLL, но с точки зрения совместимости этого делать не стоит. Если Вам не нужны лишние комментарии в скрипте, просто укажите опцию /COMPACT в командной строке DLL2DEF:
sdk\dll2def /COMPACT c:\windows\system32\kernel32.dll
Моды
На данный момент сыществуют 2 модификации ImpLib:
- Мод для Visual Basic, предназначенный для подключения CDECL и других не-STDCALL DLL к проектам на Visual Basic 6.
- Мод для PureBasic, который предназначен для создания Userlib-ов для подключения внешних DLL, особенно с использованием соглашения CDECL.
Вместо команды implib в скрипте укажите команду vbimplib или pbimplib для включения модов Visual Basic или PureBasic соответственно. DLL2DEF поддерживает ключи /VB и /PB, с помощью которых можно задать режим совместимости с тем или иным модом. Обратите внимание на следующие туториалы, чтобы скорее понять что к чему.
Туториалы
Если Вы уже прочитали введение, выберите какой-либо из следующих кратких туториалов для рассмотрения примеров применения:
- Пример общего назначения: MSVCRT.lib для MASM32 и NASM.
- Мод для PureBasic: OpenAL PureBasic Userlib.
MSVCRT.lib для MASM32 и NASM
Библиотека Microsoft Visual C Run-Time (MSVCRT.DLL) экспортирует более 700 функций, упрощающих многие стандартные задачи, как то: форматирование консольного вывода, работа с динамической памятью, манипуляция текстовых строк, сортировка и т.д. Конечно, всё это можно реализовать вручную через стандартные функции API, но раз уж библиотека C Run-Time практически всегда предустановлена в любой версии Windows (кроме Windows 95), обычно имеет смысл использовать сишные функции вместо изобретения велосипедов printf(), malloc() и др. Итак, целью нашего первого туториала будет написание парочки простых примеров на MASM32 и NASM с использованием MSVCRT.DLL.
Давайте начнём с MASM32. В комплекте последнего релиза MASM32 имеется заголовочный файл msvcrt.inc для импорта из библиотеки MSVCRT, но нет файла msvcrt.lib, которого требует линкер. Если у Вас установлена Visual Studio, можете извлечь динамическую версию msvcrt.lib из неё, но в этом случае возникнет небольшой конфликт связанный с именами символов div() и fabs(). В MASM32 и NASM нельзя использовать div или fabs в качестве имени функции потому, что эти имена зарезервированы для мнемоник x86. Аналогичная ситуация возникает при использовании имён, состоящих из одних цифр, и зарезервированных слов, вроде proc, label и т.д. Библиотеки импорта из комплекта Visual Studio зачастую создают дополнительные зависимости от OLDNAMES.lib, UUID.lib и других вспомогательных библиотек. Есть подозрение, что библиотеки из Visual Studio не подлежат свободному распространению, поэтому msvcrt.lib и не включена в состав MASM32.
Давайте создадим свою собственную MSVCRT.lib, не подпадающую под какие-либо копирайты и с именами символов, полностью совместимыми с MASM и NASM.
Первым делом нужно создать скрипт со всеми именами публичных символов MSVCRT.DLL:
dll2def /COMPACT \windows\system32\msvcrt.dll
Теперь нужно исправить конфликт имён символов. Откройте в блокноте msvcrt.def и задайте поиск следующей строки:
implib msvcrt.dll, div
Так как div является мнемоникой инструкции беззнакового целочисленного деления, нужно переименовать эту функцию. Hutch предлагает использовать приставку '_crt_' (на официальном форуме masm32 есть топик, посвящённый данной теме). Итак, давайте добавим приставку:
implib msvcrt.dll, div, _crt_div
Аналогичным образом исправляем описание функции fabs. Рекомендуется добавить приставку '_' к именам санков всех функций, которые не содержат этой приставки. Таким образом эти функции можно будет вызывать через invoke. Например:
; MSVCRT.printf ORD:742 implib msvcrt.dll, printf
Рекомендуется переписать следующим образом:
; MSVCRT.printf ORD:742 implib msvcrt.dll, printf, _printf
А вот как это будет выглядеть с использованием ординала:
; MSVCRT.printf ORD:742 implib msvcrt.dll, ord.742, _printf, __imp__printf
Так как настоящее имя функции пропадает при использовании ординала, рекомендуется явно указывать имя санка (_printf) и публичное имя импорта (__imp__printf). ImpLib не выдаст ошибку, если Вы не укажите явно эти имена, но в противном случае вызвать эти функции окажется нетривиальной задачей ;-)
Скомпилируйте библиотеку. Так как ImpLib выполняется интерпретатором FASM'а, а скрипт содержит более 700 функций, компиляция может затянуться на несколько минут:
\sdk\fasm msvcrt.def msvcrt.lib flat assembler version 1.67.23 (518320 kilobytes memory) 3 passes, 35.4 seconds, 460585 bytes.
Ну, вот мы и получили нашу собственную версию msvcrt.lib :-) Или список синтаксических ошибок, повторяющихся имён или других невесёлых сообщений. Просто исправьте ошибки и повторите попытку. Готовый к употреблению msvcrt.def и уже откомпилированная msvcrt.lib находятся в \src\test.
Чтобы вызвать функцию CRT в MASM32 с использованием invoke, необходимо предварительно указать прототип. \src\test\msvcrt.inc содержит набор прототипов для CRT. Вместо externdef можно использовать PROTO, если Вы по какой-то причине предпочитаете строить импорт через санки. Например:
printf PROTO C :DWORD,:VARARG
При вызове printf() с использованием предыдущего прототипа, компилятор сгенерирует call <jmp msvcrt!printf>
вместо прямого call msvcrt!printf
. Подобный косвенный вызов и называется вызовом через санк. Использование санков обычно влечёт за собой небольшую потерю производительности, поэтому лучше использовать более эффективный externdef:
cdecl_dword_vararg typedef PROTO C :DWORD,:VARARG externdef _imp__printf:PTR cdecl_dword_vararg printf equ <_imp__printf>
На этот раз вызов будет прямым.
Вот пример под MASM32 содержащий вызов MSVCRT!printf:
.386 .model flat,stdcall ; MSVCRT API include msvcrt.inc includelib msvcrt.lib .CODE format db "Hello, world!",0 start: invoke printf,OFFSET format ret END start
Давайте попробуем откомпилировать, слинковать и запустить этот пример из командной строки:
\masm32\bin\ml /c /coff test_masm32.asm \masm32\bin\link /SUBSYSTEM:CONSOLE test_masm32.obj test_masm32 Hello, world!
Тоже самое, но для NASM:
EXTERN __imp__printf %define printf [__imp__printf] section .text format db "Hello, world!",0 GLOBAL _start _start: push format call printf add esp,4 ; fix the stack ret
Собрать, слинковать, запустить:
\nasm\nasmw -fwin32 test_nasm.asm \nasm\polink /ENTRY:start /SUBSYSTEM:CONSOLE test_nasm.obj msvcrt.lib test_nasm Hello, world!
Туториал по MSVCRT.LIB подошёл к концу.
OpenAL PureBasic Userlib
Задача: создать Userlib для PureBasic с обращением ко внешней DLL. В качестве примера будет использована OPENAL32.DLL из дистрибутива OpenAL v1.0/v1.1. Так как данная DLL активно использует соглашение CDECL, мы не можем просто воспользоваться утилитой DLL Importer. Конечно, можно обновить PureBasic до версии 4 и использовать новую конструкцию ImportC совместно с OpenAL32.lib из комплекта OpenAL SDK, или OpenLibrary и CallCFunctionFast при каждом обращении к какой-либо функции OpenAL. Можно придумать и более сложные варианты, вроде враппера над DLL и т.д. В этом туториале мы создадим Userlib для PureBasic для прямого доступа к OpenAL с минимальными затратами на переходниковый (санковый) код. Переходник будет более быстрым, чем аналогичный вызов через CallCFunctionFast, с проверкой прототипа на стадии компиляции, контекстными подсказками и справкой и любыми дополнительными возможностями, которые только может предоставить отдельный Userlib. Поддержка 3й версии PureBasic гарантируется.
Для начала нужно создать скрипт с описанием всех функций:
dll2def /PB /COMPACT \windows\system32\OpenAL32.dll
Получаем файл с именем openal32.def, в котором находятся все публичные символы из OpenAL32.dll. Например:
pbimplib OpenAL32.dll, STDCALL, 0, alBuffer3f
Синтаксис команды pbimplib:
pbimplib dllname, convention, numarguments, real_name, PureBasics_name, public_name
Параметр dllname
определяет имя файла DLL. Параметр convention
определяет соглашение о вызове данной функции. Поддерживаются следующие соглашения:
- STDCALL используется по умолчанию во всех DLL комплекта API Windows: KERNEL32.DLL, USER32.DLL, ...
- CDECL используется преимущественно в сишных DLL: MSVCRT.DLL, OPENAL32.DLL, ...
- PBCALL — это родное соглашение PureBasic, которое врядли используется в какой-либо DLL. Это соглашение приводится на всякий случай и в качестве примера расширения для PBIMPLIB. Возможно, кто-нибудь напишет расширения для более экзотических соглашений, вроде PASCAL, FASTCALL и др.
numarguments
указывает количество 32-битных ячеек, которые нужно выделить на стеке под аргументы функции. Обычно это значение соответствует количеству параметров функции, за исключением случаев с 64-битными аргументами (double, __int64 и т.д.). Этот параметр не имеет значения для STDCALL. real_name
— это настоящее имя функции, определённое в таблице экспорта DLL. Последние 2 параметра факультативны. PureBasics_name
позволяет назначить данной функции другое имя, под которым она будет доступна в PureBasic. По умолчанию, если не указывать этот параметр, используется real_name
. public_name
зарезервирована для специальных случаев, обычно связанных с низкоуровневым программированием. Этот аргумент позволяет задать имя символа для прямого вызова, не через санк. Обычно нет необходимости заботиться об этом значении. Давайте снова взглянем на наш скрипт:
pbimplib OpenAL32.dll, STDCALL, 0, alBuffer3f
alBuffer3f() не использует соглашение STDCALL, да и принимает несколько аргументов, а не 0. DLL2DEF просто не может угадать количество аргументов и соглашение о вызове, потому что не умеет читать "OpenAL Programmer's Guide" :-) Использование "STDCALL, 0" просто отключает автоматическое восстановление стека после вызова данной функции через санк. Правильный прототип:
pbimplib OpenAL32.dll, CDECL, 5, alBuffer3f
Можете исправить остальные прототипы или просто воспользоваться готовым скриптом в src\PureBasic\openal32.def. Давайте скомпилируем его:
fasm openal32.def pbopenal.lib
Без ошибок? — Вот и отлично. Теперь нужно создать описание для библиотеки и назвать его точно также как библиотеку импорта, но с расширением .desc. В данном случае этот файл нужно назвать pbopenal.desc. Загляните в PureBasic\Library SDK\Readme.txt, если не знакомы с утилитой LibraryMaker.
; ЯП, на котором создана библиотека: ASM или C. В данном случае нужно указать C. C ; Кол-во внешних DLL, от которых зависит данная библиотека: ни одной в данном случае. 0 ; Формат библиотеки (OBJ или LIB). Нужно указать LIB, ведь pbopenal.lib — это LIB и есть. LIB
Далее нужно указать символы, которые эспортируются библиотекой. Полный пример находится в src\PureBasic\pbopenal.desc. Наконец, нужно запустить утилиту LibraryMaker.exe, чтобы получить Userlib:
LibraryMaker.exe pbopenal.desc /COMPRESSED
Готовую Userlib нужно поместить в PureBasic\PureLibraries\UserLibraries. Тестовые примеры с использованием OpenAL Userlib можно найти в пакете uFMOD (или µFMOD).
Благодарю за внимание! Надеюсь, данный материал оказался Вам полезен.