Práticas recomendadas da biblioteca de vínculo dinâmico
**Atualizado: **
- 17 de maio de 2006
APIs importantes
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 substituição juntamente com a falta de um esquema de controle de versão cria conflitos de dependência e de API. A complexidade no ambiente de desenvolvimento, a implementação do carregador e as dependências da DLL criaram fragilidade na ordem de carregamento e no comportamento do aplicativo. Por fim, muitos aplicativos dependem de DLLs e têm conjuntos complexos de dependências que precisam ser respeitados 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.
A sincronização incorreta dentro de DllMain pode causar um deadlock de aplicativo ou acessar dados ou código em uma DLL não inicializada. Chamar determinadas funções de dentro de DllMain causa esses problemas.
Melhores práticas gerais
DllMain é chamado enquanto o loader-lock é mantido. Portanto, restrições significativas são impostas às funções que podem ser chamadas dentro de DllMain. Como tal, DllMain é projetado para executar tarefas de inicialização mínimas, usando um pequeno subconjunto da API do Microsoft® Windows®. Você não pode chamar nenhuma função em DllMain que tente direta ou indiretamente adquirir o bloqueio do carregador. Caso contrário, você introduzirá a possibilidade de que seu aplicativo falhe ou entre em deadlock. Um erro em uma implementação de DllMain pode comprometer todo o processo e todos os respectivos threads.
O DllMain ideal seria apenas um stub vazio. No entanto, dada a complexidade de muitos aplicativos, isso geralmente é restritivo demais. Uma boa regra prática para o 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 realizar a ação e falhar rapidamente em vez de desperdiçar recursos concluindo outro trabalho.
Você nunca deve executar as seguintes tarefas de dentro do DllMain:
- Chame LoadLibrary ou LoadLibraryEx (direta ou indiretamente). Isso pode causar um deadlock ou um travamento.
- Chame GetStringTypeA, GetStringTypeEx ou GetStringTypeW (direta ou indiretamente). Isso pode causar um deadlock ou um travamento.
- Sincronize com outros threads. Isso pode causar um deadlock.
- Adquira um objeto de sincronização que pertence ao código que está aguardando para adquirir o bloqueio do carregador. Isso pode causar um deadlock.
- Inicialize threads COM usando CoInitializeEx. Sob certas condições, essa função pode chamar LoadLibraryEx.
- Chame as funções do Registro.
- Chame 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.
- Chame CreateThread. Criar um thread pode funcionar se você não sincronizar com outros threads, mas é arriscado.
- Chame ShGetFolderPathW. Chamar APIs de shell/pasta conhecida pode resultar em sincronização de thread e, portanto, pode 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 CRT (Tempo de Execução C) dinâmico. Se a DLL CRT não for inicializada, as chamadas para essas funções podem fazer com que o processo falhe.
- Funções de chamada em User32.dll ou Gdi32.dll. Algumas funções carregam outra DLL, que pode não ser inicializada.
- Use o código gerenciado.
É seguro executar as seguintes tarefas dentro do DllMain:
- Inicializar estruturas de dados estáticos e membros em tempo de compilação.
- Criar e inicializar objetos de sincronização.
- Alocar memória e inicializar estruturas de dados dinâmicas (evitando as funções listadas acima).
- Configurar o TLS (armazenamento local de threads).
- Abrir, ler e escrever em arquivos.
- Chamar funções no Kernel32.dll (exceto as funções listadas acima).
- Definir 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.
Deadlocks causados pela inversão da ordem de bloqueio
Quando você está implementando código que usa vários objetos de sincronização, como bloqueios, é crucial respeitar a ordem de bloqueio. Quando for necessário adquirir mais de um bloqueio ao mesmo tempo, você precisa 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 é adquirido antes do bloqueio A. A inversão da ordem de bloqueio pode causar bloqueios difíceis de depurar. Para evitar esses problemas, todos os threads precisam adquirir bloqueios na mesma ordem.
É importante notar que o carregador chama DllMain com o bloqueio do carregador já adquirido, portanto, o bloqueio do carregador deve ter a maior precedência na hierarquia de bloqueios. 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 bloqueio B. Além disso, o código DLL não pode adquirir explicitamente o bloqueio do carregador. Se o código precisa chamar uma API como GetModuleFileName, que pode adquirir indiretamente o bloqueio do carregador, e o código também precisa adquirir um bloqueio privado, 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 estruturas de dados dele 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 de carregador L. 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, todos os threads devem sempre tentar adquirir objetos de sincronização na ordem de carregamento definida.
Práticas recomendadas para sincronização
Considere uma DLL que cria threads de trabalho como parte da própria 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 completamente o problema de sincronizar e desligar DLLs de maneira limpa em um ambiente multithreaded. Esta seção descreve as práticas recomendadas atuais para sincronização de threads durante o desligamento da DLL.
Sincronização de threads 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 nesse caso. Em outras palavras, o manipulador de DLL_PROCESS_DETACH ideal é 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 precisa ser liberado para armazenamento permanente.
Sincronização de thread no DllMain para DLL_THREAD_DETACH durante o descarregamento da DLL
- Quando a DLL é descarregada, o espaço de endereço não é descartado. 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 a espera pela saída dos threads no DllMain pode causar um deadlock. Por exemplo, a DLL A contém o bloqueio do carregador. Ela sinaliza o thread T para sair e aguarda o thread sair. O thread T sai e o carregador tenta adquirir o bloqueio do carregador para chamar o DllMain da DLL A com DLL_THREAD_DETACH. Isso causa um deadlock. Para minimizar o risco de um deadlock:
- A DLL A obtém uma mensagem DLL_THREAD_DETACH no DllMain dela e define um evento para o thread T, sinalizando-o para sair.
- O thread T termina a tarefa atual dele, chega a um estado consistente, sinaliza para a DLL A e aguarda infinitamente. Observe que as rotinas de verificação de consistência devem seguir as mesmas restrições que DllMain para evitar deadlocks.
- A DLL A encerra T, sabendo que ele está em um estado consistente.
Se uma DLL for descarregada depois que todos os threads dela tiverem sido criados, mas antes de começarem a ser executados, os threads poderão falhar. Se a DLL criou threads em seu DllMain como parte de sua inicialização, alguns threads podem não ter concluído a inicialização e sua mensagem de DLL_THREAD_ATTACH ainda está aguardando 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 por trás do bloqueio do carregador. As mensagens DLL_THREAD_ATTACH desses threads 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 capturar os erros mais comuns no DllMain.
- Se estiver usando um bloqueio privado dentro do DllMain, defina uma hierarquia de bloqueios e use-a de maneira consistente. O bloqueio do carregador precisa estar em último lugar nessa hierarquia.
- Verifique se nenhuma chamada depende de outra DLL que pode ainda não ter sido totalmente carregada.
- Execute inicializações simples estaticamente em tempo de compilação, em vez de em DllMain.
- Adie todas as chamadas no DllMain que podem esperar até mais tarde.
- Adie tarefas de inicialização que podem esperar até mais tarde. Certas condições de erro precisam ser detectadas antecipadamente para que o aplicativo possa lidar com erros normalmente. No entanto, existem compensações entre essa detecção precoce e a perda de robustez que pode resultar dela. Adiar a inicialização geralmente é melhor.