Загрузка dll от dll?

Каков лучший способ для загрузки dll от dll?

Моя проблема, я не могу загрузить dll на process_attach, и я не могу загрузить dll из основной программы, потому что я не управляю основным источником программы. И поэтому я не могу вызвать non-dllmain функцию, также.

6
задан Stefan Steiger 20 April 2010 в 11:44
поделиться

4 ответа

После всех дебатов, которые велись в комментариях, я думаю, что лучше резюмировать свои позиции в «реальном» ответе.

Прежде всего, все еще не ясно , почему вам нужно загружать dll в DllMain с помощью LoadLibrary. Это определенно плохая идея, поскольку ваш DllMain выполняет внутри другой вызов LoadLibrary, который удерживает блокировку загрузчика, как объясняется в документации DllMain :

Во время начального процесса при запуске или после вызова LoadLibrary, система просматривает список загруженных библиотек DLL для поиска процесса. Для каждой DLL, которая еще не была вызвана со значением DLL_PROCESS_ATTACH, система вызывает функцию точки входа DLL. Этот вызов выполняется в контексте потока, который вызвал изменение адресного пространства процесса, такого как основной поток процесса или поток, вызвавший LoadLibrary. Доступ к точке входа сериализуется системой для всего процесса. Потоки в DllMain удерживают блокировку загрузчика, поэтому никакие дополнительные библиотеки DLL не могут быть динамически загружены или инициализированы.
Функция точки входа должна выполнять только простые задачи инициализации или завершения . Он не должен вызывать функцию LoadLibrary или LoadLibraryEx (или функцию, которая вызывает эти функции) , потому что это может создать петли зависимости в порядке загрузки DLL. Это может привести к использованию DLL до того, как система выполнит свой код инициализации.Точно так же функция точки входа не должна вызывать функцию FreeLibrary (или функцию, которая вызывает FreeLibrary) во время завершения процесса, потому что это может привести к использованию библиотеки DLL после того, как система выполнила свой код завершения.

(курсив добавлен)

Итак, вот почему это запрещено; для более четкого и более подробного объяснения см. this и this , для некоторых других примеров того, что может случиться, если вы не будете придерживаться этих правил в DllMain, см. также некоторые сообщения в блоге Раймонда Чена .

Теперь об ответе Ракиса.

Как я уже повторял несколько раз, то, что вы думаете, это DllMain, не является реальным DllMain библиотеки dll; вместо этого это просто функция, которая вызывается реальной точкой входа в dll. Этот, в свою очередь, автоматически используется CRT для выполнения дополнительных задач инициализации / очистки, среди которых есть создание глобальных объектов и статических полей классов (на самом деле все они с точки зрения компилятора почти одинаковы вещь). После (или до, для очистки) он выполняет такие задачи, он вызывает ваш DllMain.

Это происходит как-то так (очевидно, я не писал всю логику проверки ошибок, просто чтобы показать, как это работает):

/* This is actually the function that the linker marks as entrypoint for the dll */
BOOL WINAPI CRTDllMain(
  __in  HINSTANCE hinstDLL,
  __in  DWORD fdwReason,
  __in  LPVOID lpvReserved
)
{
    BOOL ret=FALSE;
    switch(fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            /* Init the global CRT structures */
            init_CRT();
            /* Construct global objects and static fields */
            construct_globals();
            /* Call user-supplied DllMain and get from it the return code */
            ret = DllMain(hinstDLL, fdwReason, lpvReserved);
            break;
        case DLL_PROCESS_DETACH:
            /* Call user-supplied DllMain and get from it the return code */
            ret = DllMain(hinstDLL, fdwReason, lpvReserved);
            /* Destruct global objects and static fields */
            destruct_globals();
            /* Destruct the global CRT structures */
            cleanup_CRT();
            break;
        case DLL_THREAD_ATTACH:
            /* Init the CRT thread-local structures */
            init_TLS_CRT();
            /* The same as before, but for thread-local objects */
            construct_TLS_globals();
            /* Call user-supplied DllMain and get from it the return code */
            ret = DllMain(hinstDLL, fdwReason, lpvReserved);
            break;
        case DLL_THREAD_DETACH:
            /* Call user-supplied DllMain and get from it the return code */
            ret = DllMain(hinstDLL, fdwReason, lpvReserved);
            /* Destruct thread-local objects and static fields */
            destruct_TLS_globals();
            /* Destruct the thread-local CRT structures */
            cleanup_TLS_CRT();
            break;
        default:
            /* ?!? */
            /* Call user-supplied DllMain and get from it the return code */
            ret = DllMain(hinstDLL, fdwReason, lpvReserved);
    }
    return ret;
}

В этом нет ничего особенного: это также происходит с обычными исполняемыми файлами, с ваш основной вызов из реальной точки входа, которая зарезервирована CRT для тех же целей.

Теперь из этого становится ясно, почему решение Ракиса не работает: конструкторы для глобальных объектов вызываются реальным DllMain (т.е.фактическая точка входа в dll, о которой говорится на странице MSDN на DllMain), поэтому вызов LoadLibrary оттуда имеет точно такой же эффект, как и ее вызов из вашего поддельного DllMain.

Таким образом, следуя этому совету, вы получите те же отрицательные эффекты от прямого вызова LoadLibrary в DllMain, и вы также скроете проблему в, казалось бы, несвязанном положении, что заставит следующего сопровождающего усердно работать, чтобы найти, где эта ошибка находится.

Что касается задержки загрузки: это может быть идеей, но вы должны быть очень осторожны, чтобы не вызвать какую-либо функцию указанной dll в вашем DllMain: фактически, если бы вы сделали это, вы бы запустили скрытый вызов LoadLibrary, который имеют те же отрицательные эффекты, когда вы вызываете его напрямую.

В любом случае, на мой взгляд, если вам нужно обратиться к некоторым функциям в dll, лучшим вариантом будет статическая компоновка с ее библиотекой импорта, чтобы загрузчик автоматически загрузил ее, не создавая никаких проблем, и она автоматически разрешится. любая странная цепочка зависимостей, которая может возникнуть.

Даже в этом случае вы не должны вызывать какие-либо функции этой dll в DllMain, поскольку не гарантируется, что она уже загружена; на самом деле, в DllMain вы можете полагаться только на загружаемое ядро ​​32, и, возможно, на dll вы абсолютно уверены , что ваш вызывающий абонент уже загрузился до того, как была выпущена LoadLibrary, которая загружает вашу dll (но все же вы не должны » Я полагаюсь на это, потому что ваша dll также может быть загружена приложениями, которые не соответствуют этим предположениям, и просто хотят, например, загрузить ресурс вашей dll без вызова вашего кода ).

Как указано в статье, на которую я ссылался ранее,

Дело в том, что что касается вашего двоичного файла, DllMain вызывается в действительно уникальный момент. К тому времени загрузчик ОС нашел, сопоставил и привязал файл с диска, но - в зависимости от обстоятельств -в некотором смысле ваш двоичный код мог не быть «полностью рожденным». Вещи могут быть непростыми.
Короче говоря, когда вызывается DllMain, загрузчик ОС находится в довольно хрупком состоянии. Во-первых, он применил блокировку к своим структурам, чтобы предотвратить внутреннее повреждение внутри этого вызова, а во-вторых, некоторые из ваших зависимостей могут быть не в полностью загруженном состоянии . Перед загрузкой двоичного файла загрузчик ОС проверяет его статические зависимости. Если для этого требуются дополнительные зависимости, он также учитывает их. В результате этого анализа выясняется последовательность, в которой необходимо вызывать DllMains этих двоичных файлов. Это довольно умно, и в большинстве случаев вы можете даже избежать соблюдения большинства правил, описанных в MSDN - , но не всегда .
Дело в том, порядок загрузки вам неизвестен , но, что более важно, он основан на статической информации импорта. Если в вашем DllMain во время DLL_PROCESS_ATTACH происходит динамическая загрузка и вы совершаете исходящий вызов, все ставки отключены . Нет никакой гарантии, что DllMain этого двоичного файла будет вызван , и поэтому, если вы затем попытаетесь использовать GetProcAddress в функции внутри этого двоичного файла, результаты будут совершенно непредсказуемыми, поскольку глобальные переменные могут не быть инициализированы. Скорее всего получится AV.

(снова выделено автором)

Кстати, по вопросу о Linux и Windows: я не эксперт по системному программированию Linux, но я не думаю, что в этом отношении все обстоит так иначе.

Есть еще несколько эквивалентов DllMain (функции _init и _fini ), которые - какое совпадение! - автоматически берется CRT, который, в свою очередь, из _init , вызывает все конструкторы для глобальных объектов и функции, отмеченные __ attribute__ constructor (которые в некотором роде эквивалентны " поддельный "DllMain, предоставленный программисту в Win32). Аналогичный процесс происходит с деструкторами в _fini .

Поскольку _init тоже вызывается, пока идет загрузка dll ( dlopen еще не возвращался), я думаю, что вы подвержены аналогичным ограничениям в том, что вы можно сделать там. Тем не менее, на мой взгляд, в Linux проблема ощущается меньше, потому что (1) вы должны явно отказаться от функции, подобной DllMain, чтобы сразу не поддаться соблазну злоупотреблять ею и (2) приложениями Linux, насколько я видел, обычно используют менее динамическую загрузку dll.

Вкратце

Ни один «правильный» метод не позволит вам ссылаться на любую dll, кроме kernel32.dll, в DllMain.

Таким образом, не делайте ничего важного из DllMain, ни напрямую (т.е. в «вашем» DllMain, вызываемом CRT), ни косвенно (в конструкторах глобальных классов / статических полей), особенно не загрузка других dll , опять же, ни напрямую (через LoadLibrary), ни косвенно (с вызовами функций в загруженных с задержкой dll, которые запускают вызов LoadLibrary).

Правильный способ загрузить еще одну dll в качестве зависимости - это… ну! - пометить как статическую зависимость.Просто сделайте ссылку на его статическую библиотеку импорта и укажите хотя бы одну из ее функций: компоновщик добавит ее в таблицу зависимостей исполняемого образа, а загрузчик загрузит ее автоматически (инициализируя ее до или после вызова вашего DllMain, вы не нужно знать об этом, потому что вы не должны вызывать его из DllMain).

Если по какой-то причине это невозможно, все еще есть варианты задержки (с ограничениями, о которых я говорил ранее).

Если вы по-прежнему по какой-то неизвестной причине испытываете необъяснимую потребность вызвать LoadLibrary в DllMain, что ж, вперед, стреляйте себе в ногу, это в ваших силах. Но не говорите мне, что я вас не предупреждал.


Я забыл: еще один фундаментальный источник информации по этой теме - это документ Best Practices for Creating DLLs от Microsoft, в котором фактически говорится почти только о загрузчике, DllMain, блокировке загрузчика и их взаимодействии; взгляните на него для получения дополнительной информации по теме.

Addendum

Нет, не совсем ответ на мой вопрос. Все, что там сказано: «Это невозможно с динамическим связыванием, вы должны ссылаться статически» и «вы не должны звонить из dllmain».

Что является ответом на ваш вопрос: в условиях, которые вы наложили, вы не можете делать то, что хотите. Короче говоря, из DllMain вы не можете вызывать ничего, кроме функций kernel32 . Период.

Хотя подробно, но меня не очень интересует, почему это не работает.

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

Фактически, загрузчик неправильно разрешает зависимости, и процесс загрузки неправильно распределяется по потокам со стороны Microsoft.

Нет, милая, грузчик свою работу делает правильно,поскольку после возврата LoadLibrary, все зависимости загружены, и все готово к использованию. Загрузчик пытается вызвать DllMain в порядке зависимости (чтобы избежать проблем с неработающими библиотеками DLL, которые полагаются на другие библиотеки DLL в DllMain), но в некоторых случаях это просто невозможно.

Например, могут быть две библиотеки DLL (скажем, A.dll и B.dll), которые зависят друг от друга: теперь, чей DllMain должен вызывать первым? Если загрузчик сначала инициализировал A.dll, и это в своем DllMain вызвало функцию в B.dll, может произойти что угодно, поскольку B.dll еще не инициализирован (его DllMain еще не был вызван). То же самое применимо, если мы изменим ситуацию вспять.

Могут быть и другие случаи, в которых могут возникнуть подобные проблемы, поэтому простое правило: не вызывайте никаких внешних функций в DllMain, DllMain предназначен только для инициализации внутреннего состояния вашей dll.

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

Это обсуждение происходит так: вы говорите: «Я хочу решить уравнение типа x ^ 2 + 1 = 0 в реальной области». Вам все говорят, что это невозможно; вы говорите, что это не ответ, и обвиняете математику.

Кто-то говорит вам: эй, можно, вот трюк, решение - просто +/- sqrt (-1); все голосуют против этого ответа (потому что это неверно для вашего вопроса, мы выходим за пределы реального домена), а вы обвиняете тех, кто голосует против.Я объясняю вам, почему это решение неверно в соответствии с вашим вопросом и почему эта проблема не может быть решена в реальной области. Вы говорите, что вас не волнует, почему это невозможно сделать, что вы можете делать это только в реальной области, и снова обвиняете математику.

Теперь, поскольку, как объяснялось и пересказывалось миллион раз, в ваших условиях ваш ответ не имеет решения , не могли бы вы объяснить нам , почему вы «должны» делать такой идиотский такое дело как загрузка dll в DllMain ? Часто «невозможные» проблемы возникают из-за того, что мы выбрали странный путь решения другой проблемы, который приводит нас в тупик. Если бы вы объяснили общую картину, мы могли бы предложить лучшее решение, которое не требует загрузки dll в DllMain.

PS: Если я статически свяжу DLL2 (ole32.dll, Vista x64) с DLL1 (mydll), какая версия DLL потребуется компоновщику в старых операционных системах?

Та, которая присутствует (очевидно, я ' m при условии, что вы компилируете для 32-битной версии); если экспортированная функция, необходимая вашему приложению, отсутствует в найденной dll, ваша dll просто не загружается (сбой LoadLibrary).


Приложение (2)

Положительный результат при внедрении, с CreateRemoteThread, если хотите знать. Только в Linux и Mac dll / разделяемая библиотека загружается загрузчиком.

Добавление dll в качестве статической зависимости (что предлагалось с самого начала) позволяет загружать ее загрузчиком точно так же, как это делают Linux / Mac, но проблема все еще существует, поскольку, как я объяснил, в DllMain вы по-прежнему не может полагаться ни на что, кроме kernel32.dll (даже если загрузчик в целом достаточно умен, чтобы сначала инициализировать зависимости).

Тем не менее, проблема может быть решена. Создайте поток (который фактически вызывает LoadLibrary для загрузки вашей dll) с помощью CreateRemoteThread; в DllMain используйте какой-нибудь метод IPC (например, именованную разделяемую память, дескриптор которой будет где-то сохранен для закрытия в функции инициализации), чтобы передать программе инжектора адрес «настоящей» функции инициализации, которую предоставит ваша dll. После этого DllMain выйдет, ничего не делая. Вместо этого приложение-инжектор будет ждать конца удаленного потока с помощью WaitForSingleObject, используя дескриптор, предоставленный CreateRemoteThread. Затем, после того, как удаленный поток будет завершен (таким образом, LoadLibrary будет завершена, и все зависимости будут инициализированы), инжектор прочитает из именованной общей памяти, созданной DllMain, адрес функции инициализации в удаленном процессе и запустит это с CreateRemoteThread.

Проблема: в Windows 2000 использование именованных объектов из DllMain запрещено, поскольку

В Windows 2000 именованные объекты предоставляются DLL служб терминалов. Если эта DLL не инициализирована, вызовы DLL могут вызвать сбой процесса.

Таким образом, этот адрес, возможно, придется передать другим способом. Достаточно чистым решением было бы создать разделяемый сегмент данных в dll, загрузить его как в приложение-инжектор, так и в целевое, и поместить в такой сегмент данных требуемый адрес.Очевидно, что dll должна быть загружена сначала в инжектор, а затем в целевой, потому что в противном случае «правильный» адрес будет перезаписан.

Другой действительно интересный метод, который можно сделать, - это записать в память другого процесса небольшую функцию (непосредственно в сборке), которая вызывает LoadLibrary и возвращает адрес нашей функции инициализации; так как мы написали это там,мы также можем вызвать его с помощью CreateRemoteThread, потому что мы знаем, где он находится.

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

80
ответ дан 8 December 2019 в 02:00
поделиться

Самый надежный способ - связать первую DLL с импортная библиотека второго. Таким образом, фактическая загрузка второй DLL будет выполняться самой Windows. Звучит очень банально, но не все знают, что библиотеки DLL могут связываться с другими библиотеками DLL. Windows может даже иметь дело с циклическими зависимостями. Если A.DLL загружает B.DLL, которому требуется A.DLL, импорт в B.DLL разрешается без повторной загрузки A.DLL.

13
ответ дан 8 December 2019 в 02:00
поделиться

Один из возможных ответов - использование LoadLibrary и GetProcAddress для доступа к указателям на функции, найденные / расположенные внутри загруженной DLL, но ваши намерения / потребности недостаточно ясны, чтобы определить, является ли это подходящий ответ.

0
ответ дан 8 December 2019 в 02:00
поделиться

Я предлагаю вам использовать механизм отложенной загрузки. DLL будет загружена при первом вызове импортированной функции. Кроме того, вы можете изменить функцию загрузки и обработку ошибок. См. Поддержка компоновщика для DLL с отложенной загрузкой для получения дополнительной информации.

5
ответ дан 8 December 2019 в 02:00
поделиться
Другие вопросы по тегам:

Похожие вопросы: