Как я могу "запустить" DLL как исполняемый файл во время выполнения?

Я хочу написать очень, очень маленькую программу, которая анализирует аргументы запуска и выбирает одну из нескольких DLL для загрузки.

Я уже написал приложение, которое я хотел бы "запустить" как DLL, написав его как приложение, а затем изменив свойства проекта Visual Studio, чтобы вместо этого построить его как DLL. Я знаю, что мне нужно совместно использовать LoadLibrary и GetProcAddress, чтобы получить желаемую функциональность, но у меня возникают проблемы с нахождением четкой и исчерпывающей документации по этому вопросу, поскольку многие варианты использования на самом деле не такого рода. Кроме того, я должен идти по этому пути в зависимости от ограничений проекта и платформы.

Я нашел эту страницу, на которой есть некоторая информация, но она не достаточно ясна для меня, чтобы приспособиться к моим целям.

Редактировать: Вот где я сейчас нахожусь.

У меня есть проект DLL, чья сигнатура основной функции выглядит примерно так:

__declspec(dllexport) int cdecl main(int argc, char *argv[])

У меня также есть проект приложения, попытка загрузки DLL и запуска вышеуказанной функции выглядит следующим образом:

typedef int (CALLBACK* LPFNDLLFUNC1)(int, char *);

...

        HMODULE dllHandle = NULL;
        BOOL freeResult, runTimeLinkSuccess = FALSE;
        LPFNDLLFUNC1 lpfnDllFunc1;    // Function pointer  
        if (args->IsEmpty())
        {
            dllHandle = LoadLibrary(L"TrueApplication.dll");
            if (NULL != dllHandle)
            {
                lpfnDllFunc1 = (LPFNDLLFUNC1)GetProcAddress(dllHandle, "main");
                if (lpfnDllFunc1)
                {
                    int retVal = lpfnDllFunc1(0, "1");
                }

В настоящее время вызов LoadLibrary работает, но не GetProcAddress.

3 ответа

Решение

Прежде всего, изменение типа проекта с исполняемого на DLL недостаточно для создания DLL. Вам также нужно экспортировать некоторые символы, чтобы создать свой API. По крайней мере, вам нужно украсить функции, которые вы экспортируете __declspec(dllexport), Тем не менее, я рекомендую вам экспортировать C API, то есть extern "C" функции с C-совместимые аргументы. Итак, функции, которые вы экспортируете, должны начинаться с extern "C" __declspec(dllexport),

Как только вы это сделаете, вы можете динамически загрузить вашу DLL следующим образом:

   const char* dllname = "myfile.dll";
   h = LoadLibrary(dllname);
   if( h == nullptr )
   {
       /*handle error*/
   }

   using myfunc_type = bool (*)(int x, double y); //example
   auto myfunc = reinterpret_cast<myfunc_type>(GetProcAddress(h, "myfunc"));       
   //......
   myfunc(x,y); //call the imported function

Это решение требует больше работы, чем статическая загрузка с /delayload показано Джерри Коффином, но у него есть преимущество: если DLL требуется, но не найдена, вы можете дать пользователям свое собственное сообщение об ошибке вместо того, чтобы полагаться на сообщение, поступающее из Windows (что часто неприемлемо для людей, не являющихся техническими специалистами). Вы также можете включить проверку версии API в собственное сообщение об ошибке в API.

Изменить: пример кода будет работать, если вы измените его следующим образом

extern "C" __declspec(dllexport) int main(int argc, char *argv[]){...}
typedef int (* LPFNDLLFUNC1)(int, char **);

Вам не нужно GetProcAddress (...) сделать это, хотя этот подход (вариант № 2) проще, когда вы понимаете, как компилятор генерирует имена символов.


Опция 1

DllMain порождает основную тему

Никогда не делайте ничего сложного внутри DllMain, вы можете заблокировать свое программное обеспечение.

У DLL есть своя собственная точка входа (и точка выхода, и точка подключения потока... это очень занятая функция). Просто звоню LoadLibrary (...) на вашей DLL вызывает как минимум один вызов DllMain (...) для прикрепления процесса.

BOOL
APIENTRY
DllMain ( HMODULE hModule,
          DWORD   ul_reason_for_call,
          LPVOID  lpReserved )

Вы можете на самом деле лечить ul_reason_for_call == DLL_PROCESS_ATTACH как мандат на выполнение DllMain, как если бы это была основная функция вашей программы.

Теперь вы ни в коем случае не должны запускать цикл программы здесь... в любое время DllMain запускает он содержит очень важную блокировку операционной системы (DLL Loader), и вам нужно снять ее, вернувшись для нормальной работы программы.

Это означает, что если вы хотите использовать DllMain как точка входа вашей программы, она должна порождать поток и ваш оригинал main метод не должен возвращаться, пока этот поток не завершится...


Вариант № 2

DLL экспортирует main Функция.

Примите во внимание соглашение о вызовах, компилятор переименует символы для вас и сделает поиск функций в DLL с GetProcAddress менее чем интуитивно.

В вашей DLL, экспорт main :

__declspec (dllexport)
int
__cdecl main (int argc, char *argv [])
{
  printf ("foobar");
  return 0;
}

В вашей программе импортируйте main из DLL:

// Need a typedef for the function you are going to get from the DLL
typedef int (__cdecl *main_pfn)(int argc, char *argv[]);

int main (int argc, char *argv[])
{
  HMODULE hModMyDLL = LoadLibraryA ("MyDll.dll");

  if (hModMyDLL != 0) {
    //
    // The preceding underscore deals with automatic decorations
    //   the compiler added to the __cdecl function name.
    //
    //  It is possible to do away with this completely if you use a .def
    //    file to assign export names and ordinals manually, but then you
    //      lose the ability to tell a function's calling convention by its
    //        name alone.
    //
    main_pfn MyMain = (main_pfn)
      GetProcAddress (hModMyDLL, "_main");

    // Call the main function in your DLL and return when it does
    if (MyMain != nullptr)
      return MyMain (argc, argv);
  }

  return -1;
}

Оба подхода имеют свои достоинства.

Нерест из DllMain избегает вообще ничего знать о том, как реализована DLL, которую вы хотите загрузить, но это также требует от вас main функцию никогда не вернуть - DLL вызовет ExitProcess (...),

Экспорт функций и последующий импорт их по названию позволяет избежать пушистой стрельбы вокруг загрузчика Windows DLL. Однако, если вы не используете .def файл для явного имени экспортируемых символов, компилятор собирается добавить украшения, такие как _... (__cdecl) или ...@n (__stdcall) к именам, и вы должны изучить эти соглашения, чтобы сделать что-нибудь полезное с GetProcAddress,

Вам не нужно использовать LoadLibrary а также GetProcAddress чтобы вызвать функциональность в DLL.

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

void DLL_a();
void DLL_b();
void DLL_c();

int main(int argc, char **argv) { 
    // we'll assume DLL_a is the default:
    if (argc < 2) 
        DLL_a();

    // For now, we'll do a *really* trivial version of parsing the command line
    // to choose the right DLL:
    if (argv[1][0] == 'A')
        DLL_a();
    else if (argv[1]][0] == 'B')
        DLL_b();
    else if (argv[1][0] == 'C')
        DLL_c();
    else {
        std::cerr << "Unrecognized argument\n";
        return 1;
    }
}

Когда вы связываете свой основной, вы будете указывать .lib соответствующие каждой DLL, и вы, вероятно, захотите указать /delayload флаг для компоновщика. Это означает, что DLL не будет загружена, пока функция в DLL не будет фактически вызвана. Если, например, вы хотите распространить версию вашей программы с ограниченной функциональностью, которая включает в себя только DLL A, она все равно сможет работать (без DLL B или C, присутствующей в системе пользователя), пока не будет функции из DLL B или C когда-либо называется. Если вы не укажете /delayloadзагрузчик попытается сопоставить все библиотеки DLL с оперативной памятью при запуске программы, запустить их DllMain чтобы инициализировать их для использования, сделайте то же самое рекурсивно для всех библиотек DLL, от которых они зависят, и т. д.

/delayload имеет еще одно преимущество: он вообще не отображает другие библиотеки на адреса, если они никогда не используются. Похоже, что любой вызов будет использовать только одну DLL, так что это, вероятно, победа в вашем случае.

Другие вопросы по тегам