В этой статье предлагаю рассмотреть создание динамических библиотек, в винде это всем наверное известные DLL.)
И .exe, и .dll файлы считаются исполняемыми файлами, в формате PE, сам PE уже описан много где, нет не времени не желание описывать архитектуру, если интересно всё в сети есть и очень разжованно.)
Что такое DLL?
DLL - это общие библиотеки исполняемых функций или данных, которые могут использоваться несколькими приложениями одновременно. Они используются для экспорта функций, которые будут использоваться процессом. В отличие от EXE файлов, DLL файлы не могут исполнять код самостоятельно. Вместо этого библиотеки DLL необходимо загрузить другими программами для выполнения кода.
Например функция createFile экспортирована из kernel32.dll, поэтому если процесс хочет вызвать эту функцию, ему сначала нужно загрузить kernel32.dll в свое адресное пространство.
Некоторые DLL автоматически загружаются в каждый процесс по умолчанию, так как эти DLL экспортируют функции, необходимые для правильного выполнения процесса.
Несколько примеров таких DLL: ntdll.dll, kernel32.dll и kernelbase.dll.
В PocessExplorer показанно какие dll загружены в процесс explorer.exe:
Системный Базовый Адрес DLL
ОС Windows использует системный базовый адрес DLL, чтобы загружать некоторые DLL по одному и тому же базовому адресу в виртуальном адресном пространстве всех процессов на данной машине для оптимизации использования памяти и улучшения производительности системы.
Следующее изображение показывает, как kernel32.dll загружается по одному и тому же адресу (0x7fff9fad0000) среди нескольких работающих процессов.
В такой ситуации кодовая область у всех процессов будет одна, но данные будут разные, у каждого процесса свои.
Зачем использовать DLL?
Есть несколько причин, почему DLL так часто используются в Windows:
- Модульность кода - вместо одного огромного исполняемого файла, содержащего всю функциональность, код делится на несколько независимых библиотек, каждая из которых фокусируется на конкретной функциональности. Модульность упрощает работу разработчиков во время разработки и отладки.
- Повторное использование кода - DLL способствуют повторному использованию кода, так как библиотеку можно вызывать из нескольких процессов.
- Эффективное использование памяти - когда несколько процессов нуждаются в одной и той же DLL, они могут экономить память, разделяя эту DLL, вместо того чтобы загружать ее в память процесса.
DLL могут по желанию указывать функцию точки входа, которая выполняет код, когда происходит определенная задача, например, когда процесс загружает библиотеку DLL.
Есть 4 возможности для вызова точки входа:
- Загрузка процессом DLL.
- Создание процессом нового потока.
- Нормальный выход потока.
- Выгрузка процессом DLL.
Код ниже показывает типичную структуру кода DLL.
Код:
BOOL APIENTRY D11Main(
HANDLE hModule, // Handle to DLL module
DWORD ul_reason_for_call, // Reason for calling function
LEVOID lpReserwved // Reserved
)
{
switch (ul_reason_for_call)
case DLL_PROCESS_ATTACH: //Load to process
// Do something here
break;
DLL_THREAD_ATTACH: // A a new thread.
// Do socmething here
break;
case DLL_THREAD_DETACH // А thread exits normally.
// Do something here
break;
case DLL_PROCESS_DETACH: // A process unloads the DLL.
// Do something here
break;
return TRUE;
}
Экспорт функции
DLL-файлы могут экспортировать функции, которые могут быть использованы вызывающим приложением или процессом. Чтобы экспортировать функцию, она должна быть определена с использованием ключевых слов extern и __declspec(dllexport). Приведен ниже пример экспортированной функции HelloWorld.
C:
extern _ declspec(dllexport) void HelloWorld() {
// Function code here
}
Динамическое связывание
Можно использовать WinAPI, такие как LoadLibrary, GetModuleHandle и GetProcAddress, чтобы импортировать функцию из DLL. Этот процесс называется динамическим связыванием.
Это метод загрузки и связывания кода (DLL) во время выполнения, а не связывания их на этапе компиляции с помощью компоновщика и таблицы адресов импорта.
Существует несколько преимуществ использования динамического связывания, их подробное описание можно найти в документации Microsoft.
В этом разделе рассматриваются шаги по загрузке DLL, получению дескриптора DLL, извлечению адреса экспортированной функции и последующему вызову этой функции.
Загрузка DLL
Вызов функции, например MessageBoxA, в приложении заставит ОС Windows загрузить DLL, экспортирующую функцию MessageBoxA, в адресное пространство памяти вызывающего процесса, в данном случае это user32.dll. Загрузка user32.dll была выполнена автоматически ОС при запуске процесса, а не кодом.
Однако в некоторых случаях, эта DLL может не быть загружена в память. Чтобы приложение могло вызвать функцию, ему сначала нужно получить дескриптор DLL, экспортирующий эту функцию. Для этого потребуется использовать функцию WinAPI LoadLibrary, как показано ниже.
Код:
HMODULE hModule = LoadLibraryA("sampleDLL.d11"); // hMedule now contain sampleDLL.dll's handle
Получение адреса функции
После того как DLL загружена в память и дескриптор получен, следующий шаг - это получение адреса функции. Это делается с помощью функции WinAPI GetProcAddress, которая принимает дескриптор DLL, экспортирующего функцию, и имя функции.
Код:
PVOID pHelloWorld = GetProcAddress(hModule, "HelloWorld");
Вызов функции
После сохранения адреса HelloWorld в переменной pHelloWorld, следующим шагом является приведение типа этого адреса к указателю функции HelloWorld. Этот указатель на функцию требуется для вызова функции.
Код:
typedef void (WINAPI* HelloWorldFunctionPointer) ();
HelloWorldFunctionPointer HelloWorld = (HelloWorldFunctionPointer)pHelloWorld;
HelloWorld();
Пример динамического связывания
Ниже представлен еще один простой пример динамического связывания, где вызывается MessageBoxA. Код предполагает, что user32.dll экспортирующая эту функцию не загружена в память. Напоминаю, что если DLL не загружена в память, для загрузки этой DLL в адресное пространство процесса требуется использование LoadLibrary.
Код:
typedef int (WINAPI* MessageBoxAFunctionPointer)(HWND, LPCSTR, LPCSTR, UINT);
MessageBoxAFunctionPointer pMessageBoxA = (MessageBoxAFunctionPointer)GetProcAddress(LoadLibrary("user32.dll"), "MessageBoxA");
if (pMessageBoxA) {
pMessageBoxA(NULL, "Текст MessageBox", "Заголовок MessageBox", 0);
}
Указатели на функции
На протяжении оставшегося курса типы данных указателей на функции будут иметь именование, которое использует имя WinAPI с префиксом fp, что означает "указатель на функцию". Например, вышеуказанный тип данных MessageBoxAFunctionPointer будет представлен как fpMessageBoxA. Это используется для упрощения и повышения ясности на протяжении курса.
Rundll32.exe
Существует несколько способов запуска экспортированных функций без использования программного метода.
Одна из общеизвестных техник - использование бинарного файла rundll32.exe. Rundll32.exe - это встроенный бинарник Windows, который используется для запуска экспортированной функции DLL. Для запуска экспортированной функции используйте следующую команду:
Код:
rundll32.exe <имя_dll>, <экспортированная функция для запуска>
Создание файла DLL с помощью Visual Studio
Чтобы создать файл DLL, запустите Visual Studio и создайте новый проект. Когда вам будут предложены шаблоны проектов, выберите опцию "Dynamic-Link Library (DLL)".
Далее будут сгенерированы шаблоны, в которых уже можно писать код (dllmain.cpp):
Предоставленный шаблон DLL поставляется с framework.h, pch.h и pch.cpp, которые известны как предварительно скомпилированные заголовки. Эти файлы используются для ускорения компиляции проекта для больших проектов. Вряд ли они понадобятся в этой ситуации, поэтому рекомендуется удалить эти файлы. Для этого выделите файл и нажмите клавишу удаления, затем выберите опцию "Удалить".
После удаления предварительно скомпилированных заголовков необходимо изменить настройки компилятора по умолчанию, чтобы подтвердить, что в проекте не следует использовать предварительно скомпилированные заголовки.
Перейдите к вкладке C/C++.
Измените параметр "Предварительно скомпилированный заголовок" на "Не использовать предварительно скомпилированные заголовки"и нажмите "Применить".
Наконец, можно изменить файл dllmain.cpp на dllmain.c. Это необходимо, если вы используете C вместо C++. Для компиляции программы выберите Сборка > Собрать решение, и DLL будет создан в папке Release или Debug, в зависимости от конфигурации компиляции.
Последнее редактирование: