Suporte de vinculador para DLLs carregadas com atraso

O vinculador MSVC dá suporte ao carregamento atrasado de DLLs. Esse recurso alivia a necessidade de usar as funções LoadLibrary do SDK do Windows e GetProcAddress para implementar o carregamento atrasado da DLL.

Sem carga atrasada, a única maneira de carregar uma DLL em tempo de execução é usando LoadLibrary e GetProcAddress; o sistema operacional carrega a DLL quando o executável ou DLL que o usa é carregado.

Com a carga atrasada, quando você vincula implicitamente uma DLL, o vinculador fornece opções para atrasar a carga de DLL até que o programa chame uma função nessa DLL.

Um aplicativo pode atrasar o carregamento de uma DLL usando a opção /DELAYLOAD do vinculador (Atrasar importação de carga) com uma função auxiliar. (Uma implementação de função auxiliar padrão é fornecida pela Microsoft.) A função auxiliar carrega a DLL sob demanda em runtime chamando LoadLibrary e GetProcAddress para você.

Considere o atraso no carregamento de uma DLL se:

  • Seu programa pode não chamar uma função na DLL.

  • Uma função na DLL pode não ser chamada até o final da execução do programa.

O carregamento atrasado de uma DLL pode ser especificado durante a compilação de um projeto EXE ou DLL. Um projeto de DLL que atrasa o carregamento de uma ou mais DLLs em si não deve chamar um ponto de entrada carregado com atraso em DllMain.

Especifique DLLs para carga com atraso

Você pode especificar quais DLLs atrasar o carregamento usando a opção do vinculador /delayload:dllname. Se você não planeja usar sua própria versão de uma função auxiliar, também deve vincular seu programa delayimp.lib (para aplicativos de área de trabalho) ou dloadhelper.lib (para aplicativos UWP).

Veja um exemplo simples de atraso no carregamento de uma DLL:

// cl t.cpp user32.lib delayimp.lib  /link /DELAYLOAD:user32.dll
#include <windows.h>
// uncomment these lines to remove .libs from command line
// #pragma comment(lib, "delayimp")
// #pragma comment(lib, "user32")

int main() {
   // user32.dll will load at this point
   MessageBox(NULL, "Hello", "Hello", MB_OK);
}

Crie a versão DEBUG do projeto. Percorra o código usando o depurador e você observará que user32.dll ele é carregado somente quando você fizer a chamada para MessageBox.

Descarregando de maneira explícita uma DLL carregada com atraso

A opção do vinculador /delay:unload permite que o código descarregue explicitamente uma DLL que foi carregada por atraso. Por padrão, as importações carregadas com atraso permanecem na tabela de endereços de importação (IAT). No entanto, se você usar /delay:unload na linha de comando do vinculador, a função auxiliar dá suporte ao descarregamento explícito da DLL por uma chamada __FUnloadDelayLoadedDLL2 e redefine o IAT para sua forma original. Os ponteiros agora inválidos são substituídos. O IAT é um campo na estrutura ImgDelayDescr que contém o endereço de uma cópia da IAT original, se existir.

Exemplo de descarregamento de uma DLL carregada com atraso

Este exemplo mostra como descarregar explicitamente uma DLL, MyDll.dllque contém uma função fnMyDll:

// link with /link /DELAYLOAD:MyDLL.dll /DELAY:UNLOAD
#include <windows.h>
#include <delayimp.h>
#include "MyDll.h"
#include <stdio.h>

#pragma comment(lib, "delayimp")
#pragma comment(lib, "MyDll")
int main()
{
    BOOL TestReturn;
    // MyDLL.DLL will load at this point
    fnMyDll();

    //MyDLL.dll will unload at this point
    TestReturn = __FUnloadDelayLoadedDLL2("MyDll.dll");

    if (TestReturn)
        printf_s("\nDLL was unloaded");
    else
        printf_s("\nDLL was not unloaded");
}

Anotações importantes sobre o descarregamento de uma DLL carregada com atraso:

  • Você pode encontrar a implementação da função __FUnloadDelayLoadedDLL2 no arquivo delayhlp.cppno diretório MSVC include. Para obter mais informações, consulte Entenda a função auxiliar de carga de atraso.

  • O parâmetro name da função __FUnloadDelayLoadedDLL2 deve corresponder exatamente (incluindo maiúsculas e minúsculas) ao que a biblioteca de importação contém. (Essa cadeia de caracteres também está na tabela de importação na imagem.) Você pode exibir o conteúdo da biblioteca de importação usando DUMPBIN /DEPENDENTS. Se preferir uma correspondência de cadeia de caracteres que não diferencia maiúsculas de minúsculas, você pode atualizar __FUnloadDelayLoadedDLL2 para usar uma das funções de cadeia de caracteres CRT que não diferenciam maiúsculas de minúsculas ou uma chamada à API do Windows.

Vincular importações carregadas com atraso

O comportamento do vinculador padrão é criar uma tabela de endereço de importação (IAT) associada à DLL carregada com atraso. Se a DLL estiver vinculada, a função auxiliar tentará usar as informações vinculadas em vez de chamar GetProcAddress em cada uma das importações referenciadas. Se o carimbo de data/hora ou o endereço preferencial não corresponder aos da DLL carregada, a função auxiliar assume que a tabela de endereço de importação associada está desatualizada. Ele continua como se a IAT não existisse.

Se você nunca pretende associar as importações carregadas por atraso de uma DLL, especifique /delay:nobind na linha de comando do vinculador. O vinculador não gerará a tabela de endereços de importação associada, o que economiza espaço no arquivo de imagem.

Carregar todas as importações para uma DLL carregada com atraso

A função __HrLoadAllImportsForDll, que é definida em delayhlp.cpp, informa ao vinculador para carregar todas as importações de uma DLL especificada com a opção do vinculador /delayload.

Ao carregar todas as importações de uma só vez, você pode centralizar o tratamento de erro em um só lugar. Você pode evitar o tratamento de exceções estruturadas em todas as chamadas reais para as importações. Ele também evita uma situação em que seu aplicativo falha em parte por meio de um processo. Por exemplo, se o código auxiliar não carregar uma importação, depois de carregar outras pessoas com êxito.

A chamada __HrLoadAllImportsForDll não altera o comportamento de ganchos e tratamento de erros. Para obter mais informações, consulte Tratamento e notificação de erros.

__HrLoadAllImportsForDll faz uma comparação que diferencia maiúsculas de minúsculas com o nome armazenado dentro da própria DLL.

Veja um exemplo que usa __HrLoadAllImportsForDll em uma função chamada TryDelayLoadAllImports para tentar carregar uma DLL nomeada. Ele usa uma função CheckDelayException para determinar o comportamento de exceção.

int CheckDelayException(int exception_value)
{
    if (exception_value == VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND) ||
        exception_value == VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND))
    {
        // This example just executes the handler.
        return EXCEPTION_EXECUTE_HANDLER;
    }
    // Don't attempt to handle other errors
    return EXCEPTION_CONTINUE_SEARCH;
}

bool TryDelayLoadAllImports(LPCSTR szDll)
{
    __try
    {
        HRESULT hr = __HrLoadAllImportsForDll(szDll);
        if (FAILED(hr))
        {
            // printf_s("Failed to delay load functions from %s\n", szDll);
            return false;
        }
    }
    __except (CheckDelayException(GetExceptionCode()))
    {
        // printf_s("Delay load exception for %s\n", szDll);
        return false;
    }
    // printf_s("Delay load completed for %s\n", szDll);
    return true;
}

Você pode usar o resultado de TryDelayLoadAllImports para controlar se você chama as funções de importação ou não.

Tratamento de erro e notificação

Se o programa usa DLLs carregadas com atraso, ele deve lidar com erros de forma robusta. Falhas que ocorrem enquanto o programa está em execução que resultarão em exceções sem tratamento. Para obter mais informações sobre o tratamento de erros e notificação em carga de atraso de DLL, consulte Notificação e tratamento de erro.

Despejar importações carregadas com atraso

As importações carregadas com atraso podem ser despejadas usando DUMPBIN /IMPORTS. Essas importações aparecem com informações ligeiramente diferentes das importações padrão. Elas são segregadas em sua própria seção da lista /imports e são explicitamente rotuladas como importações carregadas de atraso. Se houver informações de descarregamento presentes na imagem, será observado. Se houver informações de associação presentes, o carimbo de data e hora da DLL de destino será observado junto com os endereços associados das importações.

Restrições em DLLs de carregamento de atraso

Existem várias restrições no carregamento de atraso de importações de DLL.

  • As importações de dados não têm suporte. Uma solução alternativa é lidar explicitamente com a importação de dados por conta própria usando o LoadLibrary (ou o GetModuleHandle se souber que a ajuda de carregamento atrasado carregou a DLL) e GetProcAddress.

  • Não há suporte para atraso no carregamento Kernel32.dll. Essa DLL deve ser carregada para que funcionem as rotinas auxiliares de carregamento de atraso.

  • Não há suporte para associação de pontos de entrada encaminhados.

  • Um processo pode ter um comportamento diferente se uma DLL for carregada com atraso, em vez de ser carregada na inicialização. Ele pode ser visto se houver inicializações por processo que ocorrem no ponto de entrada da DLL carregada com atraso. Outros casos incluem TLS (armazenamento local de thread) estático, declarado usando __declspec(thread), que não é tratado quando a DLL é carregada por LoadLibrary. TLS dinâmico, usando TlsAlloc, TlsFree, TlsGetValue e TlsSetValue, ainda está disponível para uso em DLLs estáticas ou com carregamento atrasado.

  • Reinicialize os ponteiros de função estática global para as funções importadas após a primeira chamada para cada função. É necessário porque o primeiro uso de um ponteiro de função aponta para a conversão, não para a função carregada.

  • Atualmente, há como atrasar o carregamento apenas de procedimentos específicos de uma DLL ao usar o mecanismo de importação normal.

  • Convenções de chamada personalizadas (como usar códigos e condição em arquiteturas x86) não têm suporte. Além disso, os registros de ponto flutuante não são salvos em nenhuma plataforma. Preste atenção se suas rotinas de ajuda personalizada ou rotinas de gancho utilizam tipos de ponto flutuante. As rotinas devem salvar e restaurar o estado do ponto flutuante completo em máquinas com convenções de chamada de registro com parâmetros de ponto flutuante. Tome cuidado com o carregamento atrasado de CRT DLL caso você chame funções de CRT que consideram parâmetros de ponto flutuante em uma pilha do NDP (processador de dados numéricos) na função de ajuda.

Entender a função auxiliar de atrasar carregamento

A função auxiliar para carregamento atrasado com suporte de vinculador é o que realmente carrega a DLL em tempo de execução. Você pode modificar a função auxiliar para personalizar seu comportamento. Em vez de usar a função auxiliar fornecida em delayimp.lib, escreva sua própria função e vincule-a ao seu programa. Uma função auxiliar atende a todas as DLLs carregadas com atraso. Para obter mais informações, consulte Entenda a função auxiliar de carga de atraso e Desenvolva sua própria função auxiliar.

Confira também

Criar DLLs C /C++ no Visual Studio
Referência de vinculador MSVC