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


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

Разработчики 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 использует глобальный критический раздел процесса (часто называемый блокировкой загрузчика), который предотвращает небезопасный доступ во время инициализации модуля. В результате процесс загрузки уязвим для множества классических сценариев взаимоблокировки. Для смешанных сборок следующие две ситуации повышают риск взаимоблокировки.

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

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

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

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

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

Блокировка загрузчика все равно может произойти, но теперь эти случаи можно воспроизвести и обнаружить. Если 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 с нативным кодом и .NET.

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

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

  • Вызов встроенной функции осуществляется через глобальный указатель статической функции. Этот сценарий неосуществим, поскольку виртуальные функции вызываются через глобальные указатели функций. Например,
#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-файлам в список поиска. Эти PDB-файлы устанавливаются в каталог %VSINSTALLDIR%\SDK\v2.0\symbols. Выберите OK.

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

  2. Установите режим отладчика на только нативный.

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

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

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

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

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

  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.

См. также

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