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.
**Atualizado: **
- 17 de maio de 2006
APIs importantes
- DllMain
- LoadLibraryEx
- CreateProcess
A criação de DLLs apresenta uma série de desafios para os desenvolvedores. As DLLs não têm controle de versão imposto pelo sistema. Quando existem várias versões de uma DLL em um sistema, a facilidade de ser substituída, juntamente com a falta de um esquema de controle de versão, cria conflitos de dependência e API. A complexidade no ambiente de desenvolvimento, na implementação do carregador e nas dependências da DLL criou fragilidade na ordem de carga e no comportamento do aplicativo. Por fim, muitos aplicativos dependem de DLLs e têm conjuntos complexos de dependências que devem ser honrados para que os aplicativos funcionem corretamente. Este documento fornece diretrizes para desenvolvedores de DLL para ajudar na criação de DLLs mais robustas, portáteis e extensíveis.
Sincronização incorreta dentro de DllMain pode fazer com que um aplicativo entre em deadlock ou aceda a dados ou código numa DLL não inicializada. Chamar certas funções de dentro DllMain causa esses problemas.
Melhores Práticas Gerais
DllMain é chamado enquanto o bloqueio do carregador é mantido. Portanto, restrições significativas são impostas sobre as funções que podem ser chamadas dentro DllMain. Como tal, DllMain é projetado para executar tarefas mínimas de inicialização, usando um pequeno subconjunto da API do Microsoft® Windows®. Você não pode chamar qualquer função em DllMain que direta ou indiretamente tenta adquirir o bloqueio do carregador. Caso contrário, você introduzirá a possibilidade de que seu aplicativo trave ou falhe. Um erro em um implementação de DllMain pode comprometer todo o processo e todos os seus threads.
O ideal DllMain seria apenas um esboço vazio. No entanto, dada a complexidade de muitas aplicações, isto é geralmente demasiado restritivo. Uma boa regra geral para DllMain é adiar o máximo de inicialização possível. A inicialização lenta aumenta a robustez do aplicativo porque essa inicialização não é executada enquanto o bloqueio do carregador é mantido. Além disso, a inicialização lenta permite que você use com segurança muito mais da API do Windows.
Algumas tarefas de inicialização não podem ser adiadas. Por exemplo, uma DLL que depende de um arquivo de configuração deve falhar ao carregar se o arquivo estiver malformado ou contiver lixo. Para esse tipo de inicialização, a DLL deve tentar a ação e falhar rapidamente em vez de desperdiçar recursos concluindo outro trabalho.
Você nunca deve executar as seguintes tarefas de dentro DllMain:
- Chame LoadLibrary ou LoadLibraryEx (direta ou indiretamente). Isso pode causar um impasse ou uma falha.
- Chame GetStringTypeA, GetStringTypeExou GetStringTypeW (direta ou indiretamente). Isso pode causar um impasse ou uma falha.
- Sincronize com outros threads. Isso pode causar um impasse.
- Adquira um objeto de sincronização que pertence ao código que está aguardando para adquirir o bloqueio do carregador. Isso pode causar um impasse.
- Inicialize threads COM usando CoInitializeEx. Sob certas condições, essa função pode chamar LoadLibraryEx.
- Chame as funções do registo.
- Chamada CreateProcess. Criar um processo pode carregar outra DLL.
- Chame ExitThread. Sair de um thread durante a desanexação da DLL pode fazer com que o bloqueio do carregador seja adquirido novamente, causando um deadlock ou uma falha.
- Chamado CreateThread. Criar um thread pode funcionar se você não sincronizar com outros threads, mas é arriscado.
- Ligue ShGetFolderPathW. Chamar APIs da shell/de pasta conhecida pode levar à sincronização de threads e, assim, causar deadlocks.
- Crie um pipe nomeado ou outro objeto nomeado (somente Windows 2000). No Windows 2000, objetos nomeados são fornecidos pela DLL de serviços de terminal. Se essa DLL não for inicializada, chamadas para a DLL podem fazer com que o processo falhe.
- Use a função de gerenciamento de memória do C dinâmico Run-Time (CRT). Se a DLL CRT não for inicializada, chamadas para essas funções podem fazer com que o processo falhe.
- Chame funções em User32.dll ou Gdi32.dll. Algumas funções carregam outra DLL, que pode não ser inicializada.
- Use código gerenciado.
As seguintes tarefas são seguras para executar dentro DllMain:
- Inicialize estruturas de dados estáticos e membros em tempo de compilação.
- Crie e inicialize objetos de sincronização.
- Aloque memória e inicialize estruturas de dados dinâmicas (evitando as funções listadas acima).
- Configure o armazenamento local de threads (TLS).
- Abra, leia e grave em arquivos.
- Chame funções no Kernel32.dll (exceto as funções listadas acima).
- Defina ponteiros globais como NULL, adiando a inicialização de membros dinâmicos. No Microsoft Windows Vista™, você pode usar as funções de inicialização única para garantir que um bloco de código seja executado apenas uma vez em um ambiente multithreaded.
Interbloqueios causados pela inversão da ordem de bloqueio
Quando você está implementando um código que usa vários objetos de sincronização, como bloqueios, é vital respeitar a ordem de bloqueio. Quando for necessário adquirir mais de um bloqueio de cada vez, deve-se definir uma precedência explícita que é chamada de hierarquia de bloqueio ou ordem de bloqueio. Por exemplo, se o bloqueio A é adquirido antes do bloqueio B em algum lugar do código, e o bloqueio B é adquirido antes do bloqueio C em outro lugar do código, então a ordem de bloqueio é A, B, C e essa ordem deve ser seguida em todo o código. A inversão da ordem de bloqueio ocorre quando a ordem de bloqueio não é seguida — por exemplo, se o bloqueio B for adquirido antes do bloqueio A. A inversão da ordem de bloqueio pode causar bloqueios difíceis de depurar. Para evitar tais problemas, todos os threads devem adquirir bloqueios na mesma ordem.
É importante notar que o carregador chama DllMain com o bloqueio do carregador já adquirido, uma vez que o bloqueio do carregador deve ter a maior precedência na hierarquia de bloqueio. Observe também que o código só precisa adquirir os bloqueios necessários para a sincronização adequada; ele não precisa adquirir todos os bloqueios definidos na hierarquia. Por exemplo, se uma seção de código requer apenas bloqueios A e C para sincronização adequada, então o código deve adquirir o bloqueio A antes de adquirir o bloqueio C; não é necessário que o código adquira também o cadeado B. Além disso, o código DLL não pode adquirir explicitamente o bloqueio do carregador. Se o código deve chamar uma API como GetModuleFileName que pode adquirir indiretamente o bloqueio do carregador e o código também deve adquirir um bloqueio privado, então o código deve chamar GetModuleFileName antes de adquirir o bloqueio P, garantindo assim que a ordem de carregamento seja respeitada.
A Figura 2 é um exemplo que ilustra a inversão da ordem de bloqueio. Considere uma DLL cujo thread principal contém DllMain. O carregador de biblioteca adquire o bloqueio do carregador L e, em seguida, chama DllMain. O thread principal cria objetos de sincronização A, B e G para serializar o acesso às suas estruturas de dados e, em seguida, tenta adquirir o bloqueio G. Um thread de trabalho que já adquiriu com êxito o bloqueio G chama uma função como GetModuleHandle que tenta adquirir o bloqueio L do carregador. Assim, o thread de trabalho é bloqueado em L e o thread principal é bloqueado em G, resultando em um deadlock.
Para evitar deadlocks causados pela inversão da ordem de bloqueio, todas as threads devem tentar adquirir objetos de sincronização na ordem de carregamento definida em todos os momentos.
Práticas recomendadas para sincronização
Considere uma DLL que cria threads de trabalho como parte da sua inicialização. Após a limpeza da DLL, é necessário sincronizar com todos os threads de trabalho para garantir que as estruturas de dados estejam em um estado consistente e, em seguida, encerrar os threads de trabalho. Hoje, não há uma maneira simples de resolver de forma completa o problema de sincronizar e desligar limpamente DLLs em um ambiente multithread. Esta seção descreve as práticas recomendadas atuais para sincronização de threads durante o desligamento da DLL.
Sincronização de thread no DllMain durante a saída do processo
- No momento em que DllMain é chamado na saída do processo, todos os threads do processo foram limpos à força e há uma chance de que o espaço de endereço seja inconsistente. A sincronização não é necessária neste caso. Em outras palavras, o manipulador de DLL_PROCESS_DETACH ideal está vazio.
- O Windows Vista garante que as estruturas de dados principais (variáveis de ambiente, diretório atual, heap de processo e assim por diante) estejam em um estado consistente. No entanto, outras estruturas de dados podem ser corrompidas, portanto, limpar a memória não é seguro.
- O estado persistente que precisa ser salvo deve ser liberado para armazenamento permanente.
Sincronização de thread em DllMain para DLL_THREAD_DETACH durante o descarregamento de DLL
- Quando a DLL é descarregada, o espaço de endereço não é jogado fora. Portanto, espera-se que a DLL execute um desligamento limpo. Isso inclui sincronização de threads, identificadores abertos, estado persistente e recursos alocados.
- A sincronização de threads é complicada porque aguardar pela saída das threads no DllMain pode causar um deadlock. Por exemplo, a DLL A mantém o bloqueio do carregador. Ele sinaliza o thread T para sair e espera que o thread saia. O thread T sai e o carregador tenta adquirir o bloqueio do carregador para chamar o da DLL A DllMain com DLL_THREAD_DETACH. Isso causa um impasse. Para minimizar o risco de um impasse:
- DLL A recebe uma mensagem DLL_THREAD_DETACH em seu DllMain e define um evento para thread T, sinalizando-o para sair.
- Thread T termina sua tarefa atual, traz-se a um estado consistente, sinaliza DLL A, e espera infinitamente. Observe que as rotinas de verificação de consistência devem seguir as mesmas restrições que DllMain para evitar uma situação de deadlock.
- DLL A termina T, sabendo que está num estado consistente.
Se uma DLL for descarregada depois que todos os seus threads tiverem sido criados, mas antes de começarem a ser executados, os threads poderão falhar. Se a DLL criou threads no seu DllMain como parte da sua inicialização, alguns dos threads podem não ter completado a sua inicialização e a mensagem DLL_THREAD_ATTACH ainda está à espera para ser entregue à DLL. Nessa situação, se a DLL for descarregada, ela começará a encerrar threads. No entanto, alguns threads podem ser bloqueados atrás do bloqueio do carregador. Suas mensagens DLL_THREAD_ATTACH são processadas depois que a DLL foi desmapeada, fazendo com que o processo falhe.
Recomendações
As seguintes diretrizes são recomendadas:
- Use o Application Verifier para detetar os erros mais comuns no DllMain.
- Se estiver usando um bloqueio privado dentro DllMain, defina uma hierarquia de bloqueio e use-a de forma consistente. O bloqueio do carregador deve estar na parte inferior desta hierarquia.
- Verifique se nenhuma chamada depende de outra DLL que pode não ter sido totalmente carregada ainda.
- Execute inicializações simples estaticamente em tempo de compilação, em vez de em DllMain.
- Adie todas as chamadas em DllMain que podem esperar até mais tarde.
- Adie tarefas de inicialização que podem esperar até mais tarde. Certas condições de erro devem ser detetadas antecipadamente para que o aplicativo possa lidar com erros normalmente. No entanto, existem compensações entre esta deteção precoce e a perda de robustez que pode resultar da mesma. Adiar a inicialização geralmente é melhor.