Загрузка dll из dll? - PullRequest
       39

Загрузка dll из dll?

5 голосов
/ 20 апреля 2010

Каков наилучший способ загрузки DLL из DLL?

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

Ответы [ 4 ]

79 голосов
/ 21 апреля 2010

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

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

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

(выделение добавлено)

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

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

Как я уже повторял несколько раз, вы думаете, что это DllMain, а не real DllMain of 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 для тех же целей.

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

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

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

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

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

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

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

(опять же, акцент добавлен)

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

Есть еще некоторые эквиваленты DllMain (функции _init и _fini ), которые - какое совпадение! - автоматически принимается CRT, который, в свою очередь, из _init вызывает все конструкторы для глобальных объектов и функции, отмеченные конструктором __ attribute__ (которые в некотором роде эквивалентны " подделка "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 для создания библиотек DLL от Microsoft, в котором фактически говорится почти только о загрузчике, DllMain, блокировке загрузчика и их взаимодействиях; взгляните на него для получения дополнительной информации по теме.

<ч />

Добавление

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

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

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

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

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

Нет, моя дорогая, загрузчик делает свою работу правильно, потому что после LoadLibrary вернул, все зависимости загружены и все готово для использования. Загрузчик пытается вызвать DllMain в порядке зависимости (чтобы избежать проблем со сломанными библиотеками, которые зависят от других библиотек в 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 потребуется для компоновщика в старых операционных системах?

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

<ч />

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

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

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

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

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

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

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

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

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

13 голосов
/ 20 апреля 2010

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

5 голосов
/ 21 апреля 2010

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

0 голосов
/ 20 апреля 2010

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

...