Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Os desenvolvedores do Windows devem sempre ter cuidado com o bloqueio do carregador ao executar o código durante DllMain. No entanto, há alguns problemas adicionais a serem considerados ao lidar com assemblies de modo misto C++/CLI.
Código dentro DllMain não deve acessar o .NET Common Language Runtime (CLR). Isso significa que DllMain não deve fazer chamadas para funções gerenciadas, direta ou indiretamente, nenhum código gerenciado deve ser declarado ou implementado e DllMainnenhuma coleta de lixo ou carregamento automático de biblioteca deve ocorrer dentro do DllMain.
Causas do bloqueio do carregador
Com a introdução da plataforma .NET, há dois mecanismos distintos para carregar um módulo de execução (EXE ou DLL): um para Windows, que é usado para módulos não gerenciados, e outro para o CLR, que carrega assemblies .NET. O problema de carregamento de DLL mista gira em torno do carregador do sistema operacional Microsoft Windows.
Quando um assembly contendo apenas elementos .NET é carregado num processo, o carregador CLR responsabiliza-se por todas as tarefas de carregamento e inicialização necessárias. No entanto, para carregar assemblies mistos que podem conter código e dados nativos, o carregador do Windows também deve ser usado.
O carregador do Windows garante que nenhum código pode acessar código ou dados nessa DLL antes de ser inicializado. E garante que nenhum código possa carregar redundantemente a DLL enquanto ela é parcialmente inicializada. Para fazer isso, o carregador do Windows usa uma seção crítica global do processo (geralmente chamada de "bloqueio do carregador") que impede o acesso inseguro durante a inicialização do módulo. Como resultado, o processo de carregamento é vulnerável a muitos cenários clássicos de impasse. Para montagens mistas, os dois cenários a seguir aumentam o risco de impasse:
Primeiro, se os usuários tentarem executar funções compiladas para a linguagem intermediária da Microsoft (MSIL) quando o bloqueio do carregador é mantido (de
DllMainou em inicializadores estáticos, por exemplo), isso pode causar deadlock. Considere o caso em que a função MSIL faz uma referência a um tipo numa assemblagem que ainda não está carregada. O CLR tentará carregar automaticamente esse conjunto, o que pode exigir que o carregador do Windows bloqueie o bloqueio do carregador. Ocorre um deadlock, uma vez que o bloqueio do carregador já é mantido por código anteriormente na sequência de chamadas. No entanto, executar o MSIL sob bloqueio do carregador não garante que um impasse ocorrerá. É isso que torna este cenário difícil de diagnosticar e corrigir. Em algumas circunstâncias, como quando a DLL do tipo referenciado não contém construções nativas e todas as suas dependências não contêm construções nativas, o carregador do Windows não é necessário para carregar o assembly .NET do tipo referenciado. Além disso, a biblioteca necessária ou as suas dependências mistas de componentes nativos e .NET podem já ter sido carregadas por outro código. Consequentemente, os impasses podem ser difíceis de prever e podem variar consoante a configuração da máquina de destino.Em segundo lugar, ao carregar DLLs nas versões 1.0 e 1.1 do .NET Framework, o CLR assumiu que o bloqueio do carregador não foi mantido e tomou várias ações que são inválidas sob o bloqueio do carregador. Supor que o bloqueio do carregador não seja mantido é uma suposição válida para DLLs puramente .NET. Mas como as DLLs mistas executam rotinas de inicialização nativas, elas exigem o carregador nativo do Windows e, consequentemente, o bloqueio do carregador. Portanto, mesmo que o desenvolvedor não estivesse tentando executar nenhuma função MSIL durante a inicialização da DLL, ainda havia uma pequena possibilidade de bloqueio não determinístico nas versões 1.0 e 1.1 do .NET Framework.
Todo o não-determinismo foi removido do processo de carregamento de DLL misto. Foi realizado com estas alterações:
O CLR não faz mais suposições falsas ao carregar DLLs mistas.
A inicialização não gerenciada e gerenciada é feita em dois estágios separados e distintos. A inicialização não gerida acontece primeiro (via
DllMain) e a inicialização gerida ocorre depois, através de uma construção suportada por .NET.cctor. Este último é completamente transparente para o utilizador, a menos que/Zlou/NODEFAULTLIBsejam utilizados. Para obter mais informações, consulte/NODEFAULTLIB(Ignorar bibliotecas) e/Zl(Omitir nome da biblioteca padrão).
O bloqueio do carregador ainda pode ocorrer, mas agora ocorre de forma reprodutível e é detetado. Se DllMain contiver instruções MSIL, o compilador gera aviso Aviso do compilador (nível 1) C4747. Além disso, o CRT ou o CLR tentará detetar e relatar tentativas de executar MSIL sob bloqueio do carregador. A deteção de CRT resulta num diagnóstico de erro de execução em tempo real C Run-Time, erro R6033.
O restante deste artigo descreve os cenários restantes em que o MSIL pode ser executado enquanto o bloqueio do carregador está ativo. Ele mostra como resolver o problema em cada um desses cenários e técnicas de depuração.
Cenários e soluções alternativas
Existem várias situações diferentes sob as quais o código do usuário pode executar MSIL sob bloqueio do carregador. O desenvolvedor deve garantir que a implementação do código do usuário não tente executar instruções MSIL em cada uma dessas circunstâncias. As subseções a seguir descrevem todas as possibilidades com uma discussão de como resolver problemas nos casos mais comuns.
DllMain
A DllMain função é um ponto de entrada definido pelo usuário para uma DLL. A menos que o usuário especifique o contrário, DllMain é invocado toda vez que um processo ou thread se anexa ou se desanexa da DLL que contém. Dado que essa invocação pode ocorrer enquanto o bloqueio do carregador é mantido, nenhuma função DllMain fornecida pelo usuário deve ser compilada em MSIL. Além disso, nenhuma função na árvore de chamada enraizada em DllMain pode ser compilada para MSIL. Para resolver problemas aqui, o bloco de código que define DllMain deve ser modificado com #pragma unmanaged. O mesmo deve ser feito para cada função que DllMain chama.
Nos casos em que essas funções devem chamar uma função que requer uma implementação MSIL para outros contextos de chamada, você pode usar uma estratégia de duplicação onde um .NET e uma versão nativa da mesma função são criados.
Como alternativa, se DllMain não for necessário ou se não precisar ser executado sob bloqueio do carregador, você pode remover a implementação fornecida DllMain pelo usuário, o que elimina o problema.
Se DllMain tentar executar MSIL diretamente, o aviso do compilador (nível 1) C4747 resultará. No entanto, o compilador não pode detetar casos em que DllMain chama uma função em outro módulo que, por sua vez, tenta executar MSIL.
Para obter mais informações sobre esse cenário, consulte Impedimentos ao diagnóstico.
Inicializando objetos estáticos
A inicialização de objetos estáticos pode resultar em impasse se um inicializador dinâmico for necessário. Casos simples (como quando você atribui um valor conhecido em tempo de compilação a uma variável estática) não exigem inicialização dinâmica, portanto, não há risco de impasse. No entanto, algumas variáveis estáticas são inicializadas por chamadas de função, invocações de construtor ou expressões que não podem ser avaliadas em tempo de compilação. Todas essas variáveis exigem que o código seja executado durante a inicialização do módulo.
O código abaixo mostra exemplos de inicializadores estáticos que exigem inicialização dinâmica: uma chamada de função, construção de objeto e inicialização de ponteiro. (Esses exemplos não são estáticos, mas supõe-se que tenham definições no âmbito global, o que tem o mesmo efeito.)
// dynamic initializer function generated
int a = init();
CObject o(arg1, arg2);
CObject* op = new CObject(arg1, arg2);
Esse risco de impasse depende se o módulo que contém é compilado com /clr e se o MSIL será executado. Especificamente, se a variável estática é compilada sem /clr (ou está em um #pragma unmanaged bloco), e o inicializador dinâmico necessário para inicializá-la resulta na execução de instruções MSIL, deadlock pode ocorrer. É porque, para módulos compilados sem /clr, a inicialização de variáveis estáticas é realizada por DllMain. Por outro lado, as variáveis estáticas compiladas com /clr são inicializadas pela .cctor após a conclusão do estágio de inicialização não gerida e a liberação do bloqueio do carregador.
Há uma série de soluções para o impasse causado pela inicialização dinâmica de variáveis estáticas. Eles são organizados aqui aproximadamente em ordem de tempo necessária para corrigir o problema:
O arquivo de origem que contém a variável estática pode ser compilado com
/clr.Todas as funções chamadas pela variável estática podem ser compiladas para código nativo usando a
#pragma unmanageddiretiva .Clone manualmente o código do qual a variável estática depende, fornecendo uma versão .NET e uma versão nativa com nomes diferentes. Os desenvolvedores podem então chamar a versão nativa a partir de inicializadores estáticos nativos e chamar a versão .NET noutro local.
User-Supplied Funções que influenciam a inicialização
Existem várias funções fornecidas pelo utilizador das quais as bibliotecas dependem para inicialização durante o arranque. Por exemplo, ao sobrecarregar globalmente operadores em C++, como os operadores new e delete, as versões fornecidas pelo utilizador são usadas em todos os contextos, incluindo na inicialização e destruição da Biblioteca Padrão C++. Como resultado, a Biblioteca Padrão C++ e os inicializadores estáticos fornecidos pelo usuário invocarão quaisquer versões fornecidas pelo usuário desses operadores.
Se as versões fornecidas pelo usuário forem compiladas para MSIL, esses inicializadores tentarão executar instruções MSIL enquanto o bloqueio do carregador é mantido. Um malloc fornecido pelo utilizador tem as mesmas consequências. Para resolver esse problema, qualquer uma dessas sobrecargas ou definições fornecidas pelo usuário deve ser implementada como código nativo usando a #pragma unmanaged diretiva .
Para obter mais informações sobre esse cenário, consulte Impedimentos ao diagnóstico.
Localidades personalizadas
Se o usuário fornecer uma localidade global personalizada, essa localidade será usada para inicializar todos os fluxos de E/S futuros, incluindo fluxos que são inicializados estaticamente. Se este objeto de localidade global for compilado para MSIL, as funções de membro do objeto de localidade que foram compiladas para MSIL poderão ser invocadas enquanto o bloqueio do carregador for mantido.
Existem três opções para resolver este problema:
Os arquivos de origem contendo todas as definições de fluxo de E/S globais podem ser compilados usando a /clr opção. Impede que os seus inicializadores estáticos sejam executados durante o bloqueio de carregamento.
As definições de função de localidade personalizada podem ser compiladas para código nativo usando a #pragma unmanaged diretiva .
Abster-se de definir a localidade personalizada como a localidade global até que o bloqueio do carregador seja liberado. Em seguida, configure explicitamente os fluxos de E/S criados durante a inicialização com a localidade personalizada.
Impedimentos ao diagnóstico
Em alguns casos, é difícil detetar a origem dos impasses. As subseções a seguir discutem esses cenários e maneiras de contornar esses problemas.
Implementação em cabeçalhos
Em casos selecionados, implementações de função dentro de arquivos de cabeçalho podem complicar o diagnóstico. As funções embutidas e o código do modelo exigem que as funções sejam especificadas em um arquivo de cabeçalho. A linguagem C++ especifica a One Definition Rule, que força todas as implementações de funções com o mesmo nome a serem semanticamente equivalentes. Consequentemente, o vinculador C++ não precisa fazer considerações especiais ao mesclar arquivos de objeto que têm implementações duplicadas de uma determinada função.
Nas versões do Visual Studio anteriores ao Visual Studio 2005, o vinculador simplesmente escolhe a maior dessas definições semanticamente equivalentes. Isso é feito para acomodar declarações antecipadas e cenários em que diferentes opções de otimização são usadas para diferentes fontes. Isso cria um problema para DLLs mistas nativas e .NET.
Como o mesmo cabeçalho pode ser incluído por arquivos C++ com /clr ativado ou desativado, ou um #include pode ser encapsulado dentro de um bloco #pragma unmanaged, é possível ter tanto versões MSIL quanto nativas de funções que fornecem implementações em cabeçalhos. A MSIL e as implementações nativas têm semânticas diferentes para a inicialização sob o bloqueio de carregamento, o que efetivamente viola a regra da definição única. Consequentemente, quando o vinculador escolhe a maior implementação, ele pode escolher a versão MSIL de uma função, mesmo que ela tenha sido explicitamente compilada para código nativo em outro lugar usando a #pragma unmanaged diretiva. Para garantir que uma versão MSIL de um modelo ou função em linha nunca seja chamada sob o bloqueio do carregador de código, cada definição de cada função desse tipo chamada sob esse bloqueio deve ser modificada com a diretiva #pragma unmanaged. Se o arquivo de cabeçalho for de terceiros, a maneira mais fácil de fazer essa alteração é enviar e exibir a #pragma unmanaged diretiva em torno da diretiva #include para o arquivo de cabeçalho ofensivo. (Consulte gerenciado, não gerenciado para obter um exemplo.) No entanto, essa estratégia não funciona para cabeçalhos que contêm outro código que deve chamar diretamente APIs .NET.
Como uma conveniência para os usuários que lidam com o bloqueio do carregador, o vinculador escolherá a implementação nativa em vez da gerenciada quando confrontado com ambas as opções. Esse padrão evita os problemas acima. No entanto, há duas exceções a essa regra nesta versão devido a dois problemas não resolvidos com o compilador:
- A chamada para uma função embutida é feita através de um ponteiro de função estática global. Este cenário não é viável porque as funções virtuais são chamadas através de ponteiros de funções globais. Por exemplo
#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();
}
Diagnóstico no Modo de Depuração
Todos os diagnósticos de problemas de bloqueio do carregador devem ser feitos com compilações de depuração. As compilações de versão podem não produzir diagnósticos. As otimizações feitas no modo Release podem mascarar alguns dos cenários de MSIL durante o bloqueio do carregador.
Como depurar problemas de bloqueio do carregador
O diagnóstico que o CLR gera quando uma função MSIL é invocada faz com que o CLR suspenda a execução. Isso, por sua vez, faz com que o depurador de modo misto do Visual C++ também seja suspenso ao executar o programa em depuração no processo. No entanto, ao anexar ao processo, não é possível obter uma pilha de chamadas gerenciada para o depurador usando o depurador misto.
Para identificar a função MSIL específica que foi chamada enquanto o bloqueio do carregador está ativo, os desenvolvedores devem concluir as seguintes etapas:
Certifique-se de que os símbolos para mscoree.dll e mscorwks.dll estão disponíveis.
Você pode disponibilizar os símbolos de duas maneiras. Primeiro, os PDBs para mscoree.dll e mscorwks.dll podem ser adicionados ao caminho de pesquisa de símbolos. Para adicioná-los, abra a caixa de diálogo de opções de caminho de pesquisa de símbolos. (No menu Ferramentas , escolha Opções. No painel esquerdo da caixa de diálogo Opções , abra o nó Depuração e escolha Símbolos.) Adicione o caminho aos arquivos mscoree.dll e mscorwks.dll PDB à lista de pesquisa. Esses PDBs são instalados no %VSINSTALLDIR%\SDK\v2.0\symbols. Escolha OK.
Em segundo lugar, os PDBs para mscoree.dll e mscorwks.dll podem ser baixados do Microsoft Symbol Server. Para configurar o Servidor de Símbolos, abra a caixa de diálogo das opções de caminhos de pesquisa de símbolos. (No menu Ferramentas , escolha Opções. No painel esquerdo da caixa de diálogo Opções , abra o nó Depuração e escolha Símbolos.) Adicione este caminho de pesquisa à lista de pesquisa:
https://msdl.microsoft.com/download/symbols. Adicione um diretório de cache de símbolos à caixa de texto de cache do servidor de símbolos. Escolha OK.Defina o modo de depurador para o modo somente nativo.
Abra a grelha Propriedades do projeto de inicialização na solução. Selecione Propriedades de Configuração>Depuração. Defina a propriedade Debugger Type como Native-Only.
Inicie o depurador (F5).
Quando o
/clrdiagnóstico for gerado, escolha Repetir e, em seguida, escolha Interromper.Abra a janela da pilha de chamadas. (Na barra de menus, escolha Depurar>Windows>Pilha de chamadas.) O inicializador ofensivo ou estático é identificado com uma seta verde. Se a função infratora não for identificada, as etapas a seguir devem ser tomadas para encontrá-la.
Abra a janela Imediato (Na barra de menus, escolha Depurar>Imediato do Windows>.)
Entre
.load sos.dllna janela Imediata para carregar o serviço de depuração SOS.Entre
!dumpstackna janela Imediata para obter uma listagem completa da pilha interna/clr.Procure a primeira instância (mais próxima da parte inferior da pilha) de _CorDllMain (se
DllMaincausar o problema) ou _VTableBootstrapThunkInitHelperStub ou GetTargetForVTableEntry (se um inicializador estático causar o problema). A entrada de pilha logo abaixo desta chamada é a invocação da função implementada em MSIL que tentou ser executada sob o bloqueio do carregador.Vá para o arquivo de origem e o número da linha identificados na etapa anterior e corrija o problema usando os cenários e soluções descritos na seção Cenários.
Exemplo
Descrição
O exemplo a seguir mostra como evitar o bloqueio do carregador movendo o código de DllMain para o construtor de um objeto global.
Neste exemplo, há um objeto gerenciado global cujo construtor contém o objeto gerenciado que estava originalmente em DllMain. A segunda parte deste exemplo faz referência ao assembly, criando uma instância do objeto gerenciado para invocar o construtor do módulo que faz a inicialização.
Código
// 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;
}
Este exemplo demonstra problemas na fase de inicialização de assemblies mistos.
// 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();
}
Este código produz a seguinte saída:
Module ctor initializing based on global instance of class.
Test called so linker doesn't throw away unused object.