12 ноября 2015 г.

Ответ за задачку №19 (или: НЕ используйте LoadLibrary)

Ответ на задачку №19.

Задачка №19 была подготовкой к этой статье. Похоже, что я плохо сформулировал задачку и/или дал мало подсказок. Хотя в ней имеется целых две проблемы.

Давайте разберём проблему номер один, ради которой всё и затевалось, а в конце статьи в P.S. я дам описание второй проблемы.

Итак, предполагалось, что ответ смогли бы назвать многие, кто хоть раз писал эксперт/компонент для среды Delphi или разрабатывал плагины для своей программы.

Проблема

Дело в том, что типичный BPL-пакет для IDE (а BPL - это тоже DLL) как правило является "design-time only", т.е. предназначен для использования только внутри самой IDE. Т.е. этот пакет содержит в себе только регистрацию компонента в IDE (ну и/или эксперт). А чтобы программируемые вами приложения можно было собирать с пакетами, делают ещё "run-time only" пакет, который содержит собственно код компонента. Т.е. вы имеете два пакета: run-time и design-time. В первом лежит компонент, который программа грузит и использует, во втором лежит регистрация компонента в IDE и, возможно, дополнительные плюшки (в виде IDE экспертов).

Ну или если вы разрабатывали более-менее сложную систему плагинов, то наверняка:
  1. Складывали все плагины в отдельную папочку, скажем подпапку \Plugins, чтобы не засорять папку приложения;
  2. Выносили подпапку \Plugins из Program Files в AppData, чтобы иметь возможность устанавливать/удалять плагины без необходимости взятия прав администратора;
  3. Плагины наверняка использовали и другие DLL (например, движок БД, imaging-библиотеку или API к чему-либо), которые нужно было класть вместе с DLL плагинов.

В обоих случаях вы столкнётесь с проблемой загрузки ваших DLL с помощью кода из задачки №19 (загрузки design-time пакета IDE, которая использует похожий код, либо загрузки DLL плагина вашим ядром):


Что происходит? Ведь в условиях задачки явно сказано, что DLL по указанному пути есть и она читается.

Дело в том, что обработка ошибок LoadLibrary основана на кодах ошибок. Это означает, что функция при неудаче возвращает код ошибки - некое число. Если эта функция вызывает другие функции, и какая-либо из вызываемых функций заканчивается неудачей, то LoadLibrary передаст её код вызывающему (вам). Таким образом, у вас нет возможности узнать, кому принадлежит возвращаемый код ошибки - непосредственно ли LoadLibrary или какой-либо из вызываемых ей функций.

Иными словами, ошибка "модуль не найден" ссылается не на Project2.dll. А на кого?

Дело в том, что Project2.dll обязательно импортирует некоторые функции из других DLL. Делать это любая библиотека может двумя способами - либо динамически (через GetProcAddress), либо статически (через external). Если с GetProcAddress всё более-менее понятно (вы указываете полный путь к библиотеке для импорта), то что насчёт статического связывания? Где системный загрузчик будет искать импортируемые библиотеки?

MSDN подсказывает, что, во-первых, статический импорт через external из библиотеки с именем 'name.dll' эквивалентен загрузке библиотеки только по имени (т.е. LoadLibrary('name.dll')), во-вторых, по умолчанию система ищет DLL в следующих папках (и в указанном порядке):
  1. Уже загруженные DLL;
  2. Список "известных" DLL;
  3. Папка приложения (т.е. папка, в которой лежит .exe);
  4. Текущая папка;
  5. Системная папка (т.е. System32);
  6. Папка Windows;
  7. Папки, указанные в переменной окружения PATH.

Обратите внимание, что в этом списке нет "папки, в которой лежит загружаемая DLL".

Иными словами, если вы загружаете DLL Project2.dll из подпапки \DLLs вашего приложения, а Project2.dll статически импортирует функцию из некой другой SomeOther.dll (например, DLL встроенной базы данных или SSL библиотеки) из той же подпапки \DLLs, то система не сможет найти SomeOther.dll при загрузке Project2.dll - что и приведёт к показу сообщения об ошибке "Не найден указанный модуль", т.е. не найден SomeOther.dll.

"Наивные" варианты решения

Как решить эту проблему? Очевидно, что вариантов - тьма:
  1. Вместо статического связывания через external использовать динамическое связывание через GetProcAddress с указанием полного абсолютного пути в LoadLibrary;
  2. Предварительно загрузить все статические связи DLL;
  3. Внести статически связанные DLL в список "известных" DLL;
  4. Вынести статически связанные DLL в папку приложения;
  5. Сменить текущую папку на подпапку \DLLs перед загрузкой;
  6. Хранить статические связанные DLL в папке System32;
  7. Хранить статические связанные DLL в папке Windows;
  8. Включить подпапку \DLLs в переменную окружения PATH;
  9. Использовать средства Windows для разрешения конфликтов DLL: DLL Redirection (Windows 2000+) или сборки/манифесты (Windows XP+);
  10. Кое-что ещё ;)
(Обратите внимание, что в списке выше при упоминании DLL речь идёт не о нашей Project2.dll, а о всех библиотеках, которые Project2.dll статически импортирует через external - т.е. о SomeOther.dll)

Разумеется, варианты эти не равноценны. Давайте посмотрим, что и когда стоит применять, а что применять вообще не нужно.

Использовать GetProcAddress вместо external

Вариант первый (замена external на GetProcAddress) - оптимален, но не всегда возможен (в частности, если вы используете КДЛ - "Код Других Людей"). Также, это не будет ответ на задачку, поскольку в задачке речь идёт именно про проблемную загрузку DLL со статическим связыванием.

Предварительно загрузить все статические связи DLL

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

Внести статически связанные DLL в список "известных" DLL

Вариант третий используют "важные" системные DLL, которые присутствуют в единственном экземпляре. Список "известных" DLL хранится по адресу HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs:


Как вы можете видеть, этот список предназначен для системных DLL, чтобы при загрузке LoadLibrary('kernel32.dll') у вас всегда загружалась бы C:\Windows\System32\kernel32.dll, а не C:\Temp\Hacker\kernel32.dll. Кроме того, система кэширует эти библиотеки для ускоренного доступа. Таким образом, этот вариант не следует использовать сторонним разработчикам (и вам в том числе).

Вынести статически связанные DLL в папку приложения

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

Сменить текущую папку на подпапку \DLLs перед загрузкой

Вариант пять - первый из рабочих:
procedure TForm1.Button1Click(Sender: TObject);
type
  TImportedProc = procedure; safecall; 
var
  DLLFileName: String;
  DLL: HMODULE;
  Test: TImportedProc; 
  OldDir: String;                                       // <- добавлено
begin
  DLLFileName := ExtractFilePath(ParamStr(0)) + 
    'DLLs' + PathDelim + 'Project2.dll';

  OldDir := GetCurrentDirectory;                        // <- добавлено
  try                                                   // <- добавлено
    SetCurrentDirectory(ExtractFilePath(DLLFileName));  // <- добавлено
    DLL := LoadLibrary(PChar(DLLFileName));
  finally                                               // <- добавлено
    SetCurrentDirectory(OldDir);                        // <- добавлено  
  end;                                                  // <- добавлено
  Win32Check(DLL <> 0);
  try
    try
      Test := GetProcAddress(DLL, 'Test');
      Win32Check(Assigned(Test));

      Test;
    except
      Application.HandleException(Sender);
    end;
  finally
    Test := nil;

    FreeLibrary(DLL);
    DLL := 0;
  end;
end;
Да, этот код успешно загрузит Project2.dll и все её статические зависимости (при условии, что библиотеки лежат в той же подпапке \DLLs). Но это решение имеет два недостатка:
  • Никогда не используйте относительные пути - это, во-первых, не потокобезопасно. Более того, поскольку внутри вызова LoadLibrary выполняется DLLMain всех загружаемых DLL (что в Delphi равнозначно выполнению всех секций initialization всех модулей всех DLL), то это также не безопасно и в рамках одного потока;
  • Кроме того, это решение использует всего одну папку, нельзя загружать DLL из разных папок (например, помимо \DLLs использовать \Plugins)
Так что - да, это решение, но "на троечку".

Хранить статические связанные DLL в папке System32
Хранить статические связанные DLL в папке Windows

Варианты шесть и семь, как несложно сообразить, предназначены в первую очередь - для системных DLL, во вторую - для "общих компонентов" ("компонентов" - не в смысле Delphi). Современные рекомендации Microsoft состоят в том, что сегодня место на диске - дёшево, поэтому приложениям следует по возможности хранить частные копии DLL у себя в папках, а не расшаривать их через папку System32 - дабы уменьшить вероятность DLL Hell. Поэтому это решение (тупо скидывать DLL в System32, чтобы избавиться от ошибки) - я бы сказал, "на двоечку". И да, это не решение задачи, где мы говорим про библиотеки в подпапке \DLLs.

Включить подпапку \DLLs в переменную окружения PATH

Вариант восемь также работает:
procedure TForm1.Button1Click(Sender: TObject);
type
  TImportedProc = procedure; safecall; 
var
  DLLFileName: String;
  DLL: HMODULE;
  Test: TImportedProc; 
  OldPATH: String;                                        // <- добавлено
  PATH: String;                                           // <- добавлено
begin
  DLLFileName := ExtractFilePath(ParamStr(0)) + 
    'DLLs' + PathDelim + 'Project2.dll';

  OldPATH := GetEnvironmentVariable('PATH');              // <- добавлено 
  try                                                     // <- добавлено
    PATH := ExtractFilePath(DLLFileName) + ';' + OldPATH; // <- добавлено
    SetEnvironmentVariable('PATH', PChar(PATH));          // <- добавлено
    DLL := LoadLibrary(PChar(DLLFileName));
  finally                                                 // <- добавлено
    SetEnvironmentVariable('PATH', PChar(OldPATH));       // <- добавлено
  end;                                                    // <- добавлено
  Win32Check(DLL <> 0);
  try
    try
      Test := GetProcAddress(DLL, 'Test');
      Win32Check(Assigned(Test));

      Test;
    except
      Application.HandleException(Sender);
    end;
  finally
    Test := nil;

    FreeLibrary(DLL);
    DLL := 0;
  end;
end;
Заметьте, что нам нет нужды менять системные переменные окружения, мы можем изменить только свою локальную копию. Также заметьте, что мы не меняем никаких настроек в реестре, мы просто динамически меняем блок переменных нашего процесса.

Что ж, по сравнению с SetCurrentDirectory, у этого решения есть два больших плюса:
  • Мы можем указать несколько папок;
  • Откуда следует, что это решение, хотя и не потокобезопасно, но значительно лучше в плане повторной входимости, поскольку последующий код не затрёт нашу папку, а добавит к ней.
Тем не менее, с этим решением всё ещё есть одна проблема: любая переменная окружения процесса ограничена по размеру, причём размер этот - 32 Кб. Т.е. это небольшое значение, которое вполне реально исчерпать. Более того, это ограничение на старых системах (Windows XP и младше) включает в себя весь блок переменных окружения. Таким образом, если в PATH уже содержится слишком много путей, то попытка добавить к нему ещё одну папку провалится. Если честно, я не знаю, завершится ли вызов SetEnvironmentVariable неудачей или же добавит обрезанную переменную. В любом случае, это не самый вероятный сценарий на практике, так что это решение - "на четыре".

А вот решение, которое использует сама среда Delphi (статически включать папки в PATH, прописывая их в профиле пользователя) - это явно не выше "двойки". Несколько установленных IDE, кучка разных компонентов, плюс сторонние программы - вот место в PATH и закончилось. Неоднократно сталкивался с этим, плююсь каждый раз. Причём у проблемы нормального решения нет. Понятно, что можно почистить PATH от мусора, но это временное решение. А так - либо переустановка софта в папки с более короткими именами, либо создание ссылок на папки. Как вариант, если в PATH много однотипных путей, то можно вынести общую часть путей в отдельные переменные окружения.

Использовать средства Windows для разрешения конфликтов DLL

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

Что нам остаётся ещё?

Решение

Заметим, что можно изменить сам порядок поиска DLL.
Во-первых, можно включить "безопасный" режим поиска - по сути, это тот же стандартный список, только он просто выносит вперёд системные каталоги и выносит текущую папку в конец. Смысл телодвижений в сужении площади атаки для хакеров. Если хакеру откроется доступ к текущей папке, то хакер сможет положить в текущую папку какую-нибудь security.dll (да, есть такая системная DLL). Так что если потом ваше приложение загружает security.dll (или любую другую DLL, которая статически связана с security.dll), то система загрузит DLL хакера, а не системную - поскольку security.dll отсутствует в списке "известных" DLL. В "безопасном" режиме система сначала проверит System32, а текущую - в конце, так что будет загружена системная security.dll.

Если вам кажется, что получить доступ к текущей папке не так-то просто, то это - заблуждение. Более того, если вы "оппортунистски" пытаетесь загрузить (несуществующую) DLL, то это будет вектором атаки даже с "безопасным" порядком поиска DLL.

В любом случае, "безопасный" порядок включен по умолчанию в новых ОС, выключен в старых, меняется в реестре (HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode) и не вносит в список новые пути, он просто переупорядочивает уже имеющиеся.
Далее, у нас есть расширенная версия LoadLibrary - LoadLibraryEx, которая дополнительно принимает некоторые флаги, меняющие поведение.

Подсказка: LoadLibrary реализована так:
function LoadLibrary(lpLibFileName: PChar): HMODULE; stdcall;
begin
  Result := LoadLibraryEx(lpLibFileName, 0, 0);
end;

В частности, в LoadLibraryEx есть флаг, специально предназначенный для нашего случая: LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR - этот флаг заменяет пути поиска DLL на "папку DLL" (и только её). А если вы хотите использовать и другие пути поиска, то можете добавить флаги LOAD_LIBRARY_SEARCH_APPLICATION_DIR (для папки приложения), LOAD_LIBRARY_SEARCH_SYSTEM32 (для системной папки) и LOAD_LIBRARY_SEARCH_USER_DIRS (для поиска по "пользовательским" папкам - см. ниже). Комбинацию флагов LOAD_LIBRARY_SEARCH_APPLICATION_DIR, LOAD_LIBRARY_SEARCH_SYSTEM32 и LOAD_LIBRARY_SEARCH_USER_DIRS можно заменить флагом LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, но LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR туда не входит. Разумеется, чтобы флаг LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR работал, имя DLL, передаваемое в LoadLibraryEx должно быть полным и абсолютным.

Заметьте, что эти флаги полностью заменяют список путей поиска, поэтому при указании этих флагов в путях поиска не будут присутствовать текущий каталог и папки из PATH.

Также заметьте, что эти флаги доступны только начиная с Windows Vista/Windows 7 с установленным KB2533623 и/или KB2758857 (эти обновления не включены в последние сервис-паки для Vista и 7, поэтому должны быть установлены отдельно - что обычно выполняется автоматически Windows Update). На Windows 8 и выше особых требований нет. Как узнать, что нужные обновления стоят и можно использовать этот метод? Ну, указанное обновление добавляет в систему, например, функцию AddDllDirectory, поэтому:
function IsKB2533623Installed: Boolean;
begin
  Result := Assigned(GetProcAddress(GetModuleHandle(kernel32), 'AddDllDirectory'));
end;

Таким образом, наш код становится таким:
procedure TForm1.Button1Click(Sender: TObject);
type
  TImportedProc = procedure; safecall; 
var
  DLLFileName: String;
  DLL: HMODULE;
  Test: TImportedProc; 
begin
  DLLFileName := ExtractFilePath(ParamStr(0)) + 
    'DLLs' + PathDelim + 'Project2.dll';

  DLL := LoadLibraryEx(PChar(DLLFileName), 0, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR or LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); // <- изменено
  Win32Check(DLL <> 0);
  try
    try
      Test := GetProcAddress(DLL, 'Test');
      Win32Check(Assigned(Test));

      Test;
    except
      Application.HandleException(Sender);
    end;
  finally
    Test := nil;

    FreeLibrary(DLL);
    DLL := 0;
  end;
end;
А если вам лень каждый раз вызывать LoadLibraryEx вместе со всеми флагами, то вы можете вызвать SetDefaultDllDirectories, указав набор флагов - и все дальнейшие вызовы LoadLibraryEx без флагов (а, следовательно, и вызовы LoadLibrary) будут автоматически использовать указанный порядок. Например:
procedure TForm1.Button1Click(Sender: TObject);
type
  TImportedProc = procedure; safecall; 
var
  DLLFileName: String;
  DLL: HMODULE;
  Test: TImportedProc; 
begin
  DLLFileName := ExtractFilePath(ParamStr(0)) + 
    'DLLs' + PathDelim + 'Project2.dll';

  SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR or LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); // <- добавлено
  DLL := LoadLibrary(PChar(DLLFileName));
  Win32Check(DLL <> 0);
  try
    try
      Test := GetProcAddress(DLL, 'Test');
      Win32Check(Assigned(Test));

      Test;
    except
      Application.HandleException(Sender);
    end;
  finally
    Test := nil;

    FreeLibrary(DLL);
    DLL := 0;
  end;
end;
Почти аналогичного эффекта можно добиться с использованием флага LOAD_WITH_ALTERED_SEARCH_PATH - этот флаг добавляет "папку DLL" к стандартному списку поиска DLL (а не заменяет его) и ставит её на третье место (после уже загруженных DLL и после "известных DLL") в этом списке (в обоих вариантах - стандартном и "безопасном"). Разумеется, флаг LOAD_WITH_ALTERED_SEARCH_PATH взаимно исключает флаги LOAD_LIBRARY_SEARCH_APPLICATION_DIR, LOAD_LIBRARY_SEARCH_SYSTEM32, LOAD_LIBRARY_SEARCH_USER_DIRS, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS и LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR (и, следовательно, LOAD_WITH_ALTERED_SEARCH_PATH нельзя передать в SetDefaultDllDirectories).

Получаем:
procedure TForm1.Button1Click(Sender: TObject);
type
  TImportedProc = procedure; safecall; 
var
  DLLFileName: String;
  DLL: HMODULE;
  Test: TImportedProc; 
begin
  DLLFileName := ExtractFilePath(ParamStr(0)) + 
    'DLLs' + PathDelim + 'Project2.dll';

  DLL := LoadLibraryEx(PChar(DLLFileName), 0, LOAD_WITH_ALTERED_SEARCH_PATH); // <- изменено
  Win32Check(DLL <> 0);
  try
    try
      Test := GetProcAddress(DLL, 'Test');
      Win32Check(Assigned(Test));

      Test;
    except
      Application.HandleException(Sender);
    end;
  finally
    Test := nil;

    FreeLibrary(DLL);
    DLL := 0;
  end;
end;
Второй вариант предпочтительнее по соображениям совместимости, поскольку также включает в себя все обычные места поиска, включая текущий каталог и пути из PATH, в то время как первый вариант их не включает. Зато первый вариант более защищён (безопасен) и позволяет установить умолчания, что позволяет не менять по коду вызовы LoadLibrary.

Оба решения полностью безопасны (в плане многопоточности и повторной входимости), но позволяют использовать только специально выбранные папки, а не произвольные. Существенным минусом также является работоспособность только на Windows Vista и Windows 7, обновлённых до уровня 12 июля 2011 года, и на Windows 8 и выше. Если бы не ограничение на версию системы, то это было бы решение на "пятёрочку", а так - "четыре с минусом".

Но это ещё не все варианты. Начиная с Windows XP SP 1 в системе есть функция SetDllDirectory. Она работает аналогично флагу LOAD_WITH_ALTERED_SEARCH_PATH - добавляя произвольную папку в стандартные пути поиска DLL, но с четырьмя отличиями:
  • Папка, добавляемая к путям поиска, указывается явно - в параметре функции (т.е. может быть любой, а не только "папкой DLL");
  • Папка, добавляемая к путям поиска, вставляется на четвёртое место (после уже загруженных DLL, после "известных DLL" и после папки приложения), а не на третье;
  • Текущий каталог исключается из путей поиска;
  • Настройки "безопасного" списка игнорируются, поиск всегда производится по списку, эквивалентному списку "безопасного" поиска, за исключением двух отличий, указанных в п1 и п3 этого списка (добавили папку и удалили текущий каталог).
SetDllDirectory позволяет указать только одну папку. Последующий вызов просто заменит предыдущую папку. Чтобы удалить папку и вернуть стандартные пути поиска - передавайте в функцию nil.

Т.е. в некотором смысле SetDllDirectory просто заменяет текущий каталог в списке путей поиска на определённую папку, не трогая сам текущий каталог.

Итого:
procedure TForm1.Button1Click(Sender: TObject);
type
  TImportedProc = procedure; safecall; 
var
  DLLFileName: String;
  DLL: HMODULE;
  Test: TImportedProc; 
begin
  DLLFileName := ExtractFilePath(ParamStr(0)) + 
    'DLLs' + PathDelim + 'Project2.dll';

  SetDllDirectory(PChar(ExtractFilePath(DLLFileName))); // <- добавлено
  try                                                   // <- добавлено
    DLL := LoadLibrary(PChar(DLLFileName)); 
  finally                                               // <- добавлено
    SetDllDirectory(nil);                               // <- добавлено
  end;                                                  // <- добавлено
  Win32Check(DLL <> 0);
  try
    try
      Test := GetProcAddress(DLL, 'Test');
      Win32Check(Assigned(Test));

      Test;
    except
      Application.HandleException(Sender);
    end;
  finally
    Test := nil;

    FreeLibrary(DLL);
    DLL := 0;
  end;
end;
Что-ж, у этого варианта лучше поддержка среди систем, также в плюсе - возможность указывать произвольную папку, но существенным минусом является использование глобального состояния для решения локальной проблемы. Итого - только "четыре".

"Улучшенный" вариант SetDllDirectory появляется в системе всё в том же KB2533623 - это функция AddDllDirectory. Аналогично SetDllDirectory, AddDllDirectory добавит указанную папку в пути поиска DLL, только:
  1. Можно указать не одну, а несколько папок;
  2. Папки, переданные в AddDllDirectory по умолчанию не применяются к вызовам LoadLibrary и LoadLibraryEx без параметров: вам нужно или использовать LoadLibraryEx с флагом LOAD_LIBRARY_SEARCH_USER_DIRS (или LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) или вызывать SetDefaultDllDirectories с этими же флагами;
Пути от AddDllDirectory вставляются в список поиска после "папки DLL" (если указан LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR) и после папки приложения (если указан LOAD_LIBRARY_SEARCH_APPLICATION_DIR), но до системной папки (если указан LOAD_LIBRARY_SEARCH_SYSTEM32).

Функция AddDllDirectory возвращает cookie, которое можно передать в RemoveDllDirectory для удаления папки из путей поиска. Эта особенность делает это решение безопасным и в плане многопоточности и в плане повторной входимости.

Код:
procedure TForm1.Button1Click(Sender: TObject);
type
  TImportedProc = procedure; safecall; 
var
  DLLFileName: String;
  DLL: HMODULE;
  Test: TImportedProc; 
begin
  DLLFileName := ExtractFilePath(ParamStr(0)) + 
    'DLLs' + PathDelim + 'Project2.dll';

  AddDllDirectory(PChar(ExtractFilePath(DLLFileName)));                          // <- добавлено 
  DLL := LoadLibraryEx(PChar(DLLFileName), 0, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); // <- изменено
  Win32Check(DLL <> 0);
  try
    try
      Test := GetProcAddress(DLL, 'Test');
      Win32Check(Assigned(Test));

      Test;
    except
      Application.HandleException(Sender);
    end;
  finally
    Test := nil;

    FreeLibrary(DLL);
    DLL := 0;
  end;
end;
или:
procedure TForm1.Button1Click(Sender: TObject);
type
  TImportedProc = procedure; safecall; 
var
  DLLFileName: String;
  DLL: HMODULE;
  Test: TImportedProc; 
begin
  DLLFileName := ExtractFilePath(ParamStr(0)) + 
    'DLLs' + PathDelim + 'Project2.dll';

  SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); // <- добавлено 
  AddDllDirectory(PChar(ExtractFilePath(DLLFileName)));       // <- добавлено 

  DLL := LoadLibrary(PChar(DLLFileName));
  Win32Check(DLL <> 0);
  try
    try
      Test := GetProcAddress(DLL, 'Test');
      Win32Check(Assigned(Test));

      Test;
    except
      Application.HandleException(Sender);
    end;
  finally
    Test := nil;

    FreeLibrary(DLL);
    DLL := 0;
  end;
end;

Конечно, для практического применения я бы рекомендовал также добавлять флаг LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR в оба примера.

В целом применять AddDllDirectory не имеет большого смысла если вам нужно добавить в пути поиска только "папку DLL", поскольку полностью аналогичный эффект достигается либо LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, либо LOAD_WITH_ALTERED_SEARCH_PATH. AddDllDirectory имеет смысл применять только если вам нужно добавить несколько папок, либо если нужно добавить одну папку, но она не является "папкой DLL".

Вот, собственно, и все наши варианты. Всего их получилось четырнадцать штук (если не считать список "известных DLL")! Из них на практике я бы порекомендовал использовать несколько вариантов в комплексе:
  1. Если IsKB2533623Installed вернула True, то используйте LoadLibraryEx с указанием полного пути к DLL и флагом LOAD_WITH_ALTERED_SEARCH_PATH (как альтернативный вариант: вызовите в начале программы SetDefaultDllDirectories и далее используйте как обычно LoadLibrary - этот вариант даже более актуален, если вы используете пакеты и стандартную LoadPackage, где у вас нет возможности заменить вызов LoadLibrary на LoadLibraryEx с флагами внутри LoadPackage);
  2. В противном случае - проверьте наличие SetDllDirectory и "заворачивайте" в неё каждый вызов LoadLibrary (и/или LoadPackage);
  3. Если нет и SetDllDirectory, то используйте либо решение с локальным изменением PATH, либо SetCurrentDirectory, либо и то и то.
Если же вам нужно указывать не одну папку, а несколько папок для поиска, то:
  1. Если IsKB2533623Installed вернула True, то используйте AddDllDirectory;
  2. В противном случае - используйте решение с локальным изменением PATH.
Вот, собственно, и всё, что я хотел сказать.


P.S. Да, а что это за вторая проблема задачки, которую я упомянул в начале статьи?

Хм, мне было странно, что эту проблему никто не назвал, ведь она довольно известна - настолько, что в Delphi есть специальная функция для обхода этой проблемы. Дело в том, что в строчке DLL := LoadLibrary(PChar(DLLFileName)) будет выполнена DllMain(DLL_PROCESS_ATTACH) указанной DLL - что приведёт к инициализации всех её модулей (вызове секций initialization). Т.е. будет выполняться пользовательский код DLL. Этот пользовательский код DLL может быть не слишком вежлив и поменять глобальное состояние процесса, а именно - управляющее слово FPU (CWR).

Что-ж, если загружаемая DLL написана на Delphi и она ссылается только на системные DLL и на другие DLL, тоже написанные на Delphi, то эта проблема не столь актуальна, т.к. управляющее слово будет заменено на то же самое значение, что и установлено (и ожидается) вашей программой. Но если или сама DLL или любая DLL, с которой наша DLL статически связана, будет написана на чём-либо ещё - то у такой DLL может быть своё представление о "правильном" управляющем слове. После этого дальнейшая работа вашей программы может стать непредсказуемой, даже если вы лично не используете типы с плавающей точкой. Например, простейший Move в новых версиях Delphi реализован через MMX команды процессора, которые являются расширением математического сопроцессора. Что, собственно, может приводить к тому, что тривиальный вызов Move с заведомо корректными аргументами будет вылетать.

Чтобы обойти эту проблему, нужно восстановить управляющее слово FPU на "ваше" значение. Чтобы не делать это вручную, в Delphi есть специальная функция - SafeLoadLibrary.

Замечу только, что LoadPackage использует внутри себя именно SafeLoadLibrary, а 64-битный код вообще не восстанавливает управляющее слово FPU, поскольку Delphi не использует математический сопроцессор в 64-битном коде (вместо этого используется SSE).

Так что - да, не используйте LoadLibrary. Используйте SafeLoadLibraryEx (написание SafeLoadLibraryEx остаётся вам в качестве домашнего упражнения).

Читать далее: Разработка API (контракта) для своей DLL.

16 комментариев :

  1. Решение со сменой текущей папки не такое уж и плохое. Особенно если используются плагины сторонних разработчиков, которые любят делать TIniFile.Create('Settings.ini') прямо в секции initialization.

    ОтветитьУдалить
    Ответы
    1. Ну, текущая папка - это средство, которое стоит выполнять дополнительно к основному методу - "на всякий случай".

      Удалить
  2. Я поступал другим способом. Ставил хук на LdrLoadDll (ntdll). И к строковому параметру путей добавлял свой путь. Вот только надо быть осторожным, функция в Win10 ведёт немного себя иначе, чем в других Windows.

    ОтветитьУдалить
  3. Подскажите, какой модуль надо подключить, что бы в Delphi XE10 стала доступной функция AddDllDirectory ?

    ОтветитьУдалить
    Ответы
    1. или вот так ?

      function AddDllDirectory(lpPathName: LPCWSTR): Integer; stdcall; external kernel32 name 'AddDllDirectory';

      Удалить
    2. Delphi не слишком активно обновляет свои заголовочные файлы (читай: почти не обновляет вообще). Поэтому большинство современных функций в ней просто нет (где "современных" = "времён Windows XP"). Поэтому можно использовать JEDI API.

      Или, в данном случае, проще объявить ручками.

      Удалить
    3. Большое спасибо!
      за ответ и вообще за прекрасный блог, он очень многому меня научил

      Удалить
  4. Undeclared identifier: 'LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR' Что надо подключить чтобы видело это ?

    ОтветитьУдалить
    Ответы
    1. Это решается поиском по .pas файлам.

      Ответ на подобные вопросы всегда такой:
      1. Использовать последнюю версию Delphi со свежими заголовочниками (в данном конкретном случае - этой константы нет даже в Delphi 10.3).
      2. Использовать сторонние заголовочники (например, JEDI WinAPI - но там её тоже нет).
      3. Объявить самостоятельно. В частности:
      const
      LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = $100;

      Удалить
    2. Зря ты это конечно не указал сразу, я еле нашёл

      const
      LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = $00000100;
      LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = $00001000;

      Удалить
  5. Доброго времени суток. Загружаю из Delphi 10.2 (Windows 10 x64) dll, написанную на C# внутри с формой на WPF. Испробовал все перечисленные способы, но зависимые dll она видит только возле exe файла, загружающего библиотеку. Текущая директория у dll всегда папка exe, как бы я не пытался её перед запуском поменять. Флаги тоже результата не дают. Может быть Вы сталкивались с подобной ситуацией. DLL тоже наша но разобраться не можем. Зависимости грузятся не при загрузке DLL, а при инициализации расположенной в ней формы. LoadLibrary отрабатывает нормально, но при вызове метода создающего форму падает внутреннее исключение в dll. Если положить рядом с exe файлами все зависимые dll то всё работает. Расположение самой загружаемой DLL роли н играет, гружу по полному пути. Не подгружаются MaterialDesignColors.dll, MaterialDesignThemes.Wpf.dll и bos_games.Commons.dll.

    ОтветитьУдалить
    Ответы
    1. Я вообще не разбираюсь в C#, но подозреваю, что у вас не просто DLL, а сборка. У сборок есть манифест, декларирующий зависимости и способы разрешения.

      Попробуйте использовать sxstrace.

      Подробнее: https://www.gunsmoker.ru/2011/02/dll-dll-hell-dll-side-by-side.html

      Удалить
    2. Посмотрите https://docs.microsoft.com/ru-ru/dotnet/standard/assembly/resolve-loads
      Вот рабочий пример: https://github.com/achechulin/loodsman/blob/master/Plugins/PluginSampleNet/PluginFunctions.cs#L151

      Удалить
    3. Всем спасибо за помощь! Помогло AssemblyResolve.

      Удалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.