[ English | Español | Pyccκuú ]

ImpLib SDK

Дата публикации:
Последнее обновление: 2021-06-25
Автор:

ImpLib SDK - это набор утилит для создания библиотек импорта в формате MS-COFF, предоставляющий продвинутые возможности, среди которых можно выделить следующие:

Как это использовать?

Прежде всего нужно написать скрипт с определениями экспортируемых символов (т.е. функций) из заданной 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

Так как в больших библиотеках может быть очень много полезных функций, описывать их вручную может оказаться весьма нудным занятием. Лучше воспользоваться утилитой DLL2DEF в каталоге \bin. Итак, давайте её запустим:

   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:

Вместо команды implib в скрипте укажите команду vbimplib или pbimplib для включения модов Visual Basic или PureBasic соответственно. DLL2DEF поддерживает ключи /VB и /PB, с помощью которых можно задать режим совместимости с тем или иным модом. Обратите внимание на следующие туториалы, чтобы скорее понять что к чему.

Туториалы

Если Вы уже прочитали введение, выберите какой-либо из следующих кратких туториалов для рассмотрения примеров применения:

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 определяет соглашение о вызове данной функции. Поддерживаются следующие соглашения:

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.

Вот и всё. Надеюсь, данный минитуториал оказался Вам полезен.
 

Проект ImpLib SDK стартовал более 15 лет назад. Более 8 тысяч загрузок сделали эту утилиту более популярной, чем можно было представить. Я все еще занимаюсь низкоуровневым программированием, в основном для встраиваемых систем. Для связи на моем текущем веб-сайте есть контактная форма: CelerSMS.

 
© 2006-2021 Владимир Каменяр.
Все права защищены.