Динамические библиотеки
В этом туториале мы узнаем о dll, что это такое и как их создавать.
Вы можете скачать пример здесь.
Теория:
Если вы программируете достаточно долго, вы заметите, что программы, которые вы пишете, зачастую используют один и те же общие процедуры. Из-за того, что вам приходиться переписывать их снова и снова, вы теряете время. Во времена DOS'а программисты сохраняли эти общие процедуры в одной или более библиотеках. Когда они хотели использовать эти функции, они всего лишь прилинковывали библиотеку к объектному файлу и линкер извлекал функции прямо из библиотек и вставлял их в финальный файл. Этот процесс называется статической линковкой. Хорошим примером являются стандартные библиотеки в C. У этого метода есть изъян - то, что в каждой программе у вас находятся абсолютно одинаковые копии функций. Впрочем, для ДОСовских программ это не очень большой недостаток, так как только одна программа могла быть активной в памяти, поэтому не происходила трата драгоценной памяти.
Под Windows ситуация стала более критичной, так как у вас может быть несколько программ, выполняющихся одновременно. Память будет быстро пожираться, если ваша программа достаточно велика. У Windows есть решение этой проблемы: динамические библиотеки (dynamic link libraries). Динамическая библиотека - это что-то вроде сборника общих функций. Windows не будет загружать несколько копий DLL в память; даже если одновременно выполняются несколько экземпляров вашей программы, будет только одна копия DLL в памяти. Здесь я должен остановиться и разъяснить чуть поподробнее. В реальности, у всех процессов, использующих одну и ту же dll есть своя копия этой библиотеки, однако Windows делает так, чтобы все процессы разделяли один и тот же код этой dll. Впрочем, секция данных копируется для каждого процесса.
Программа линкуется к DLL во время выполнения в отличии от того, как это осуществлялось в старых статических библиотеках. Вы также можете выгрузить DLL во время выполнения, если она вам больше не нужна. Если программа одна использует эту DLL, тогда та будет выгружена немедленно. Hо если ее еще используют какие-то другие программы, DLL останется в памяти, пока ее не выгрузит последняя из использующих ее программ.
Как бы то ни было, перед линкером стоит сложная задача, когда он проводит фиксирование адресов в конечном исполняемом файле. Так как он не может "извлечь" функции и вставить их в финальный исполняемый файл, он должен каким-то образом сохранить достаточно информации о DLL и используемых функциях в выходном файле, чтобы тот смог найти и загрузить верную DLL во время выполнения.
И тут в дело вступают библиотеки импорта. Библиотека импорта содержит информацию о DLL, которую она представляет. Линкер может получить из нее необходимую информацию и вставить ее в исполняемый файл.
Когда Windows загружает программу в память, она видит, что программа требует DLL, поэтому ищет библиотеку и мэппирует ту в адресное пространство процесса и выполняет фиксацию адресов для вызовов функций в DLL.
Вы можете загрузить DLL самостоятельно, не полагаясь на Windows-загрузчик.
- В этом случае вам не потребуется библиотека импорта, поэтому вы сможете загружать и использовать любую DLL, даже если к ней не прилагается библиотеки импорта. Тем не менее, вам все pавно нужно знать какие функции находятся внутри нее, сколько параметров они принимают и тому подобную информацию.
- Когда вы поручаете Windows загружать DLL, если та отсутствует, Windows выдаст сообщение "Тpебуемый .DLL-файл, xxxxx.dll отсутствует" и все! Ваша программ не может сделать ничего, что изменить это, даже если ваша dll не является необходимой. Если же вы будете загружать DLL самостоятельно и библиотека не будет найдена, ваша программа может выдать пользователю сообщение, уведомляющее об этом, и продолжить работу.
- Вы можете вызывать *недокументированные* функции, которые не включены в библиотеки импорта, главное, чтобы у вас было достаточно информации об этих функциях.
- Если вы используете LoadLibrary, вам придется вызывать GetprocAddress для каждой функции, которую вы заходите вызвать. GetprocAddress получает адрес входной точки функции в определенной DLL. Поэтому ваш код будет чуть-чуть больше и медленнее, но не намного.
Теперь, рассмотрев преимущества и недостатки использования LoadLibrary, мы подробно рассмотрим как создать DLL.
Следующий код является каркасом DLL.
;---------------------------------------------------------------------------- ; DLLSkeleton.asm ;----------------------------------------------------------------------------
.386 .model flat,stdcall
opt*on casemap:none inc*ude \masm32\include\windows.inc inc*ude \masm32\include\user32.inc inc*ude \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
.data .code DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD mov eax,TRUE * ret Dll*ntry Endp
;---------------------------------------------------------------------------- ; Это функция-пустышка - она ничего не делает. Я поместил ее сюда, чтобы ; показать, как вставляют функции в DLL. ;---------------------------------------------------------------------------- TestFunction proc ret TestFunction endp
End DllEntry
;---------------------------------------------------------------------------- ; DLLSkeleton.def ;----------------------------------------------------------------------------
LIBRARY DLLSkeleton EXpORTS TestFunction
Вышеприведенная программа - это каркас DLL. Каждая DLL должна иметь стартовую функцию. Windows вызывает эту функцию каждый pаз, когда:
DLL загружена в первый раз
DLL выгружена
Создается тред в том же процессе
Тред разрушен в том же процессе
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE ret
DllEntry Endp
Вы можете назвать стартовую функцию как пожелаете, главное чтобы был END . Эта функция получает три параметра, только первые два из них важны.
hInstDLL - это хэндл модуля DLL. Это не тоже самое, что хэндл процесса. Вам следует сохранить это значение, так как оно понадобится вам позже. Вы не сможете ее получить в дальнейшем легко.
reason может иметь одно из следующих четырех значений:
- DLL_рROCESS_ATTACH - DLL получает это значение, когда впервые загружается в адресное пространство процесса. Вы можете использовать эту возможность для того, чтобы осуществить инициализацию.
- DLL_рROCESS_DETACK - DLL получает это значение, когда выгружается из адресного пространства процесса. Вы можете использовать эту возможность для того, чтобы "почистить" за собой: освободить память и так далее.
- DLL_THREAD_ATTACK - DLL получает это значение, когда процесс создает новую ветвь.
- DLL_THREAD_DETACK - DLL получает это значение, когда ветвь в процессе уничтожена.
Вы возвращаете TRUE в eax, если вы хотите, чтобы DLL продолжала выполняться Если вы возвратите FALSE, DLL не будет загружена. Hапример, если ваш инициализационный код должен зарезервировать память и он не может это сделать, стартовой функции следует возвратить FALSE, чтобы показать, что DLL не может запуститься.
Вы можете поместить ваши функции в DLL следом за стартовой функцией или до нее. Hо если вы хотите, чтобы их можно было вызвать из других программ, вы должны поместить их имена в списке экспортов в файле установок модуля.
DLL требуется данный файл на стадии разработки. Мы сейчас посмотрим, что это такое.
LIBRARY DLLSkeleton EXpORTS TestFunction
Обычно у вас должна быть первая строка. Ключевое слово LIBRARY определяет внутреннее имя модуля DLL. Желательно, чтобы оно совпадало с именем файла.
EXрORTS говорит линкеру, какие функции в DLL экспортируются, то есть, могут вызываться из других программ. В прилагающемся примере нам нужно, чтобы другие модули могли вызывать TestFunction, поэтому мы указываем здесь ее имя.
Другое отличие заключается в параметрах, передаваемых линкеру. Вы должны указать /DLL и /DEF:.
link/DLL /SUBSYSTEM:WINDOWS/DEF:DLLSkeleton.def/LIBpATH:c:\masm32\lib DLLSkeleton.obj
Параметры ассемблера те же самые, обычно /c /coff /Cp. После компиляции вы получите .dll и .lib. Последний файл - это библиотека импорта, которую вы можете использовать, чтобы прилинковать к другим программам функции из соответствующей .dll.
Далее я покажу вам как использовать LoadLibrary, чтобы загрузить DLL.
;---------------------------------------------------------------------------- ; UseDLL.asm ;----------------------------------------------------------------------------
.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc
include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib
.data LibName db "DLLSkeleton.dll",0 FunctionName db "TestHello",0 DllNotFound db "Cannot load library",0 AppName db "Load Library",0 FunctionNotFound db "TestHello function not found",0
.data? hLib dd ? ; хэндл библиотеки (DLL) TestHelloAddr dd ? ; адрес функции TestHello
.code start: invoke LoadLibrary,addr LibName
;------------------------------------------------------------------------------- ; Вызываем LoadLibrary и передаем имя желаемой DLL. Если вызов проходит успешно, ; будет возвращен хэндл библиотеки (DLL). Если нет, то будет возвращен NULL. ; Вы можете передать хэндл библиотеки функции GetрrocAddress или любой другой ; функции, которая требует его в качестве одного из параметров. ;-------------------------------------------------------------------------------
.if eax==NULL invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK
.else
mov hLib,eax invoke GetprocAddress,hLib,addr FunctionName
;------------------------------------------------------------------------------ ; Когда вы получаете хэндл библиотеки, вы передаете его GetрrocAddress вместе ; с именем функции в этой dll, которую вы хотите вызвать. Она возвратит адрес ; функции, если вызов пройдет успешно. В противном случае, она возвратит NULL. ; Адреса функций не изменятся, пока вы не перезагрузите библиотеку. Поэтому ; их можно поместить в глобальные переменные для будущего использования. ;------------------------------------------------------------------------------
.if eax==NULL invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK .else
mov TestHelloAddr,eax call [TestHelloAddr]
;---------------------------------------------------------------------------- ; Затем мы вызываем функцию с помощью call и переменной, содержащей адрес ; функции в качестве операнда. ;----------------------------------------------------------------------------
.endif
invoke FreeLibrary,hLib
;------------------------------------------------------------------------------ ; Когда вам больше не требуется библиотека, выгрузте ее с помощью FreeLibrary. ;------------------------------------------------------------------------------
.endif invoke Exitprocess,NULL end start
Как вы можете видеть, использование LoadLibrary чуть сложнее, но гораздо гибче.
[C] Iczelion, пер. Aquila.