Поделиться через


Инициализация смешанных сборок

Разработчики Windows всегда должны опасаться блокировки загрузчика при выполнении кода во время DllMain. Однако при работе со сборками C++/CLI в смешанном режиме следует учитывать некоторые дополнительные проблемы.

Код в DllMain не должен обращаться к среде CLR .NET. Это означает, что DllMain не должен выполнять вызовы управляемых функций, прямо или косвенно; управляемый код не должен объявляться или реализовываться в DllMain; и сборка мусора или автоматическая загрузка библиотеки не должны выполняться в DllMain.

Причины блокировки загрузчика

С появлением платформы .NET существует два разных механизма загрузки модуля выполнения (EXE или DLL): один для Windows, который используется для неуправляемых модулей, и другой для среды CLR, которая загружает сборки .NET. Проблема загрузки смешанных библиотек DLL касается загрузчика ОС Microsoft Windows.

Когда сборка, содержащая только конструкции .NET, загружается в процесс, загрузчик CLR может самостоятельно выполнять все необходимые задачи загрузки и инициализации. Однако для загрузки смешанных сборок, которые могут содержать машинный код и данные, также необходимо использовать загрузчик Windows.

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

  • Во-первых, если пользователи пытаются выполнить функции, скомпилированные на языке MSIL, при удержании блокировки загрузчика (например, со DllMain статических инициализаторов или в них), это может привести к взаимоблокировке. Рассмотрим случай, когда функция MSIL ссылается на тип в сборке, которая еще не загружена. Среда CLR пытается автоматически загрузить эту сборку, а для этого может потребоваться установить блокировку загрузчика Windows. Возникает взаимоблокировка, так как блокировка загрузчика уже удерживается кодом ранее в последовательности вызовов. Однако выполнение MSIL при блокировке загрузчика не гарантирует, что возникнет взаимоблокировка. Именно это затрудняет диагностику и исправление этого сценария. В некоторых случаях, например если библиотека DLL типа, на которую указывает ссылка, не содержит собственных конструкций, а все ее зависимости не содержат собственных конструкций, загрузчик Windows не требуется для загрузки сборки .NET указанного типа. Кроме того, требуемая сборка или ее зависимые смешанные неуправляемые модули или модули .NET могли быть загружены другим кодом. Следовательно, взаимоблокировку трудно прогнозировать и вероятность ее возникновения зависит от конфигурации целевого компьютера.

  • Во-вторых, при загрузке библиотек DLL в версиях 1.0 и 1.1 платформа .NET Framework среда CLR предположила, что блокировка загрузчика не была удержана, и предприняла несколько действий, которые недопустимы при блокировке загрузчика. Предположение о том, что блокировка загрузчика не удерживается, является допустимым предположением исключительно для библиотек DLL .NET. Но поскольку смешанные библиотеки DLL выполняют собственные подпрограммы инициализации, для них требуется собственный загрузчик Windows и, следовательно, блокировка загрузчика. Таким образом, даже если разработчик не пытался выполнить какие-либо функции MSIL во время инициализации БИБЛИОТЕКи DLL, в платформа .NET Framework версиях 1.0 и 1.1 по-прежнему существует небольшая вероятность недетерминированной взаимоблокировки.

Все недетерминированные ситуации при загрузке смешанных библиотек DLL исключены. Это было сделано с помощью следующих изменений:

  • Среда CLR больше не делает ложных предположений при загрузке смешанных DLL.

  • Неуправляемая и управляемая инициализация выполняется в два отдельных этапа. Сначала выполняется неуправляемая инициализация (с помощью DllMain), а управляемая инициализация — через . Конструкция, поддерживаемая .cctor NET. Последняя полностью прозрачна для пользователя, если не /Zl используется или /NODEFAULTLIB . Дополнительные сведения см. в разделах/NODEFAULTLIB (Игнорировать библиотеки) и /Zl (Опустить имя библиотеки по умолчанию).

Блокировка загрузчика все равно может произойти, но теперь эти случаи можно воспроизвести и обнаружить. Если DllMain содержит инструкции MSIL, компилятор создает предупреждение Предупреждение компилятора (уровень 1) C4747. Кроме того, либо библиотека CRT, либо среда CLR попытаются определить случаи выполнения функций MSIL во время блокировки загрузчика и сообщат о них, если такие были. Если библиотека CRT обнаруживает попытку, выдается ошибка во время выполнения C R6033

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

Ситуации и способы решения проблем

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

DllMain

Функция DllMain — это определяемая пользователем точка входа для библиотеки DLL. Если пользователь не указывает иное, функция DllMain вызывается каждый раз, когда процесс или поток присоединяется к библиотеке DLL или отсоединяется от нее. Поскольку подобный вызов может произойти во время блокировки загрузчика, запрещается компилировать пользовательские функции DllMain в код MSIL. Кроме того, в MSIL нельзя скомпилировать никакую функцию в дереве вызовов с корнем DllMain . Чтобы устранить проблемы, блок кода, который определяет DllMain , следует изменить с помощью #pragma unmanaged. То же необходимо сделать для каждой функции, которая вызывается в DllMain .

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

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

При DllMain попытке выполнить MSIL напрямую возникает предупреждение компилятора (уровень 1) C4747 . Однако компилятор не может обнаружить случаи, когда DllMain вызывает функцию в другом модуле, который, в свою очередь, пытается выполнить MSIL.

Дополнительные сведения об этом сценарии см. в разделе Препятствия для диагностики.

Инициализация статических объектов

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

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

// dynamic initializer function generated
int a = init();
CObject o(arg1, arg2);
CObject* op = new CObject(arg1, arg2);

Риск взаимоблокировки зависит от того, компилируется ли содержащий модуль с /clr и будет ли выполняться MSIL. В частности, если статическая переменная компилируется без /clr (или находится в блоке #pragma unmanaged ), а динамический инициализатор, необходимый для ее инициализации, приводит к выполнению инструкций MSIL, может возникнуть взаимоблокировка. Это связано с тем, что для модулей, скомпилированных без /clr, инициализация статических переменных выполняется DllMain. Статические переменные, скомпилированные с /clr , инициализируются с помощью .cctor, после завершения этапа неуправляемой инициализации и снятия блокировки загрузчика.

Существует ряд решений для взаимоблокировки, вызванной динамической инициализацией статических переменных. Они расположены здесь примерно в порядке времени, необходимого для устранения проблемы:

  • Исходный файл, содержащий статическую переменную, можно скомпилировать с /clrпомощью .

  • Все функции, вызываемые статической переменной, можно скомпилировать в машинный код с помощью директивы #pragma unmanaged .

  • Можно вручную клонируйте код, от которого зависит статическая переменная, создав версию .NET и неуправляемую версию с разными именами. Разработчики могут затем вызвать собственную версию из неуправляемых статических инициализаторов и вызывать версию .NET в других случаях.

Пользовательские функции, влияющие на автозагрузку

Существует несколько пользовательских функций, от которых зависит инициализация библиотек при автозагрузке. Например, при глобальной перегрузке операторов в C++, таких как new операторы и delete , предоставленные пользователем версии используются везде, в том числе при инициализации и уничтожении стандартной библиотеки C++. В результате стандартная библиотека C++ и предоставляемые пользователем статические инициализаторы будут вызывать любые предоставленные пользователем версии этих операторов.

Если они скомпилированы в код MSIL, эти инициализаторы пытаются выполнить инструкции MSIL во время блокировки загрузчика. Предоставленный пользователем malloc объект имеет те же последствия. Чтобы устранить эту проблему, любая из этих перегрузок или предоставленных пользователем определений должна быть реализована в виде машинного кода с помощью директивы #pragma unmanaged .

Дополнительные сведения об этом сценарии см. в разделе Препятствия для диагностики.

Пользовательские языковые стандарты

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

Существует три способа решения этой проблемы.

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

Определения функций пользовательского языкового стандарта можно скомпилировать в машинный код с помощью директивы #pragma unmanaged .

Рекомендуется устанавливать пользовательский языковой стандарт как глобальный языковой стандарт только после отключения блокировки загрузчика. Затем следует явно настроить потоки ввода-вывода, созданные во время инициализации с использованием пользовательского языкового стандарта.

Трудности при диагностике

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

Реализация в заголовках

В некоторых случаях реализация функций в файлах заголовка может затруднить диагностику. Для встроенных функций и кода шаблона требуется, чтобы функции были заданы в файле заголовка. Язык C++ указывает "правило одного определения", которое говорит о том, что все реализации функций с одинаковым именем должны быть семантически эквивалентны. Как следствие, компоновщику C++ не требуются особые предосторожности при слиянии объектных файлов с дублированными реализациями определенной функции.

В версиях Visual Studio до Visual Studio 2005 компоновщик просто выбирает наибольшее из этих семантически эквивалентных определений. Это делается для размещения оповещающих объявлений и сценариев, когда разные параметры оптимизации используются для разных исходных файлов. Это создает проблему для смешанных собственных библиотек DLL и библиотек DLL .NET.

Так как один и тот же заголовок может быть включен в файлы C++ с /clr включенными и отключенными, или #include может быть заключен в #pragma unmanaged блок, можно иметь как MSIL, так и собственные версии функций, которые предоставляют реализации в заголовках. MSIL и собственные реализации имеют разную семантику для инициализации под блокировкой загрузчика, что фактически нарушает одно правило определения. Следовательно, когда компоновщик выбирает самую большую реализацию, он может выбрать версию MSIL функции, даже если она была явно скомпилирована в машинный код в другом месте с помощью директивы #pragma unmanaged . Чтобы гарантировать, что версия MSIL шаблона или встроенной функции никогда не вызывается под блокировкой загрузчика, каждое определение каждой такой функции, вызываемой под блокировкой загрузчика, должно быть изменено с помощью директивы #pragma unmanaged . Если файл заголовка от стороннего производителя, самый простой способ внести это изменение — отправить и развернуть директиву #pragma unmanaged #include для файла заголовка, вызывающе ошибку. (Пример см. в разделе Управляемый, неуправляемый .) Однако эта стратегия не работает для заголовков, содержащих другой код, который должен вызывать API.NET напрямую.

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

  • Вызов встроенной функции осуществляется через указатель глобальной статической функции. Этот сценарий не является table, так как виртуальные функции вызываются через глобальные указатели функций. Например,
#include "definesmyObject.h"
#include "definesclassC.h"

typedef void (*function_pointer_t)();

function_pointer_t myObject_p = &myObject;

#pragma unmanaged
void DuringLoaderlock(C & c)
{
    // Either of these calls could resolve to a managed implementation,
    // at link-time, even if a native implementation also exists.
    c.VirtualMember();
    myObject_p();
}

Диагностика в режиме отладки

Вся диагностика проблем блокировки загрузчика должна проходить в отладочных построениях. Сборки выпуска могут не создавать диагностика. Кроме того, оптимизации, выполненные в режиме выпуска, могут маскировать некоторые сценарии msil под блокировкой загрузчика.

Отладка проблем с блокировкой загрузчика

Диагностика, которую создает среда CLR при вызове функции MSIL, приводит к тому, что CLR приостанавливает выполнение. Это, в свою очередь, приводит к приостановке работы отладчика в смешанном режиме Visual C++ при выполнении отладчика внутри процесса. Однако при присоединении к процессу невозможно получить управляемый пакет вызовов для отладчика с помощью смешанного отладчика.

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

  1. Убедитесь, что доступны символы для библиотек mscoree.dll и mscorwks.dll.

    Вы можете сделать символы доступными двумя способами. Первый способ заключается в том, что PDB-файлы для библиотек mscoree.dll и mscorwks.dll можно добавить к пути поиска. Чтобы добавить их, откройте диалоговое окно параметров пути поиска символов. (В меню Сервис выберите Пункты. В левой области диалогового окна Параметры откройте узел Отладка и выберите Символы.) Добавьте путь к mscoree.dll и mscorwks.dll PDB-файлы в список поиска. Эти файлы устанавливаются в каталог % VSINSTALLDIR%\SDK\v2.0\symbols. Нажмите кнопку ОК.

    Второй способ заключается в том, что PDB-файлы для mscoree.dll и mscorwks.dll можно загрузить с сервера символов Майкрософт. Для настройки сервера символов откройте диалоговое окно параметров пути поиска символов. (В меню Сервис выберите Пункты. В левой области диалогового окна Параметры откройте узел Отладка и выберите Символы.) Добавьте следующий путь поиска в список поиска: https://msdl.microsoft.com/download/symbols. Добавьте каталог кэша символов в текстовом поле кэша сервера символов. Нажмите кнопку ОК.

  2. Для отладчика установите режим отладки только машинного кода.

    Откройте сетку Свойства запускаемого проекта в решении. Выберите Свойства конфигурации>Отладка. Присвойте свойству Тип отладчика значение Только машинный код.

  3. Запустите отладчик (F5).

  4. После создания диагностики /clr нажмите кнопку Повторить, а затем — Разорвать.

  5. Откройте окно "Стек вызовов". (В строке меню выберите Отладка>. Windows>Вызовите стек.) Обидный DllMain или статический инициализатор идентифицируется зеленой стрелкой. Если функция, создающая ошибку, не определена, необходимо выполнить следующие действия, чтобы найти ее.

  6. Откройте окно Интерпретация (В строке меню выберите Отладка>windows>Интерпретация.)

  7. Введите .load sos.dll в окне Интерпретация , чтобы загрузить службу отладки SOS.

  8. Введите !dumpstack в окне Интерпретация , чтобы получить полный список внутреннего /clr стека.

  9. Найдите первый экземпляр (ближайший к нижней части стека) _CorDllMain (если DllMain это вызывает проблему) или _VTableBootstrapThunkInitHelperStub или GetTargetForVTableEntry (если проблема вызвана статическим инициализатором). Запись в стеке сразу под этим вызовом является вызовом функции, реализованной в коде MSIL, которая была вызвана во время блокировки загрузчика.

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

Пример

Описание

В следующем примере показано, как избежать блокировки загрузчика, переместив код из DllMain в конструктор глобального объекта.

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

Код

// initializing_mixed_assemblies.cpp
// compile with: /clr /LD
#pragma once
#include <stdio.h>
#include <windows.h>
struct __declspec(dllexport) A {
   A() {
      System::Console::WriteLine("Module ctor initializing based on global instance of class.\n");
   }

   void Test() {
      printf_s("Test called so linker doesn't throw away unused object.\n");
   }
};

#pragma unmanaged
// Global instance of object
A obj;

extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) {
   // Remove all managed code from here and put it in constructor of A.
   return true;
}

В этом примере показаны проблемы при инициализации смешанных сборок:

// initializing_mixed_assemblies_2.cpp
// compile with: /clr initializing_mixed_assemblies.lib
#include <windows.h>
using namespace System;
#include <stdio.h>
#using "initializing_mixed_assemblies.dll"
struct __declspec(dllimport) A {
   void Test();
};

int main() {
   A obj;
   obj.Test();
}

Этот код выводит следующие результаты:

Module ctor initializing based on global instance of class.

Test called so linker doesn't throw away unused object.

См. также раздел

Смешанные (собственные и управляемые) сборки