Gostengine dll не загружена в адресное пространство программы
Я прочитал несколько статей Мэтта Питрека о переносимых исполняемых файлах (PE), например:
- Углубленный анализ формата переносимых исполняемых файлов Win32, часть 1 и часть 2
Итак, вот вопросы:
Известно, что при загрузке EXE-файла загрузчик Windows считывает список импортированных DLL из таблицы адресов Importa (IAT) и загружает их в адресное пространство процесса.
Адресное пространство процесса - это виртуальное пространство. DLL, возможно, уже загружена в какое-то физическое пространство. Это происходит с библиотеками DLL, такими как KERNEL32.dll или USER32.dll . Какая связь между физическим и виртуальным адресом? Загрузчик просто выделяет страницы и копирует DLL или делает ссылки?
Если DLL не загружена, загрузчик загружает всю DLL или только необходимые функции? Например, если вы использовали функцию foo() из bar.dll , загрузчик загружает все bar.dll в адресное пространство процесса? Или он просто загружает foo код в адресное пространство процесса?
Предположим, ваш EXE-файл использует функцию MessageBox() from USER32.DLL , которая находится в %WINDIR%\system32\user32.dll . Можете ли вы разработать индивидуальный USER32.DLL файл, поместить его в тот же каталог, что и ваш EXE-файл, и ожидать, что ваш настроенный файл MessageBox будет вызываться вашим приложением, а не системой по умолчанию MessageBox ?
По поводу 1: физические адреса не играют никакой роли, здесь все задействовано в виртуальной памяти. Физический адрес устанавливается только тогда, когда страница виртуальной памяти отображается в ОЗУ, вызванная ошибкой страницы. Многие базовые библиотеки DLL появляются по одному и тому же адресу виртуальной памяти в нескольких процессах, например, kernel32.dll. Процессы просто используют одни и те же страницы кода (не данных).
По поводу 2: фактической «загрузки» не происходит, используется та же функция, которая поддерживает файлы с отображением в память. Основой этих страниц является сам файл DLL, а не файл подкачки. Ничего не загружается, пока сбой страницы не заставит Windows прочитать страницу из файла в ОЗУ. Но да, отображается весь код DLL.
По поводу 3: да, это сработает. Но практически невозможно заставить его работать, так как вам придется писать функции замены для всех экспортов user32, которые использует ваша программа. Включая те, которые используют другие функции Win32, вы не можете знать. Перехват API - это типичный метод, который используется, Detours from Microsoft Labs - хороший метод.
Windows Internals edition 5 - отличная книга, чтобы узнать больше о сантехнике.
Использование DLL в программе на Visual C++
Многие знают, что существует два основных способа подключить DLL к программе - явный и неявный.
При неявном подключении (implicit linking) линкеру передается библиотека импорта (обычно имеет расширение lib), содержащая список переменных и функций DLL, которые могут использовать приложения. Обнаружив, что программа обращается хотя бы к одной из них, линкер добавляет в целевой exe-файл таблицу импорта . Таблица импорта содержит список всех DLL, которые использует программа, с указанием конкретных переменных и функций, к которым она обращается. Позже, когда exe-файл будет запущен, загрузчик проецирует все DLL, перечисленные в таблице импорта, на адресное пространство процесса; в случае неудачи весь процесс немедленно завершается.
При явном подключении (explicit linking) приложение вызывает функцию LoadLibrary, чтобы загрузить DLL, затем использует функцию GetProcAddress, чтобы получить указатели на требуемые функции (или переменные), а по окончании работы с ними вызывает FreeLibrary, чтобы выгрузить библиотеку и освободить занимаемые ею ресурсы.
Каждый из способов имеет свои достоинства и недостатки. В случае неявного подключения все библиотеки, используемые приложением, загружаются в момент его запуска и остаются в памяти до его завершения (даже если другие запущенные приложения их не используют). Это может привести к нерациональному расходу памяти, а также заметно увеличить время загрузки приложения, если оно использует очень много различных библиотек. Кроме того, если хотя бы одна из неявно подключаемых библиотек отсутствует, работа приложения будет немедленно завершена. Явный метод лишен этих недостатков, но делает программирование более неудобным, поскольку требуется следить за своевременными вызовами LoadLibrary и соответствующими им вызовами FreeLibrary, а также получать адрес каждой функции через вызов GetProcAddress.
Теперь рассмотрим, как каждый из перечисленных методов используется на практике. Для этого будем считать, что у нас есть библиотека MyDll.dll, которая экспортирует переменную Var, функцию Function и класс Class. Их объявления содержатся в заголовочном файле MyDll.h, который выглядит следующим образом:
Кроме того, будем считать, что библиотека импорта содержится в файле MyDll.lib.
Неявное подключение
Это наиболее простой метод подключения DLL к нашей программе. Все, что нам нужно - это передать линкеру имя библиотеки импорта, чтобы он использовал ее в процессе сборки. Сделать это можно различными способами.
Теперь можно использовать в программе любые переменные, функции и классы, содержащиеся в DLL, как если бы они находились в статической библиотеке. Например:
Явное подключение
Загрузка DLL
Как уже говорилось ранее, при явном подключении DLL программист должен сам позаботиться о загрузке библиотеки перед ее использованием. Для этого используется функция LoadLibrary, которая получает имя библиотеки и возвращает ее дескриптор. Дескриптор необходимо сохранить в переменной, так как он будет использоваться всеми остальными функциями, предназначенными для работы с DLL.
В нашем примере загрузка DLL выглядит так.
Вызов функций
После того как библиотека загружена, адрес любой из содержащихся в ней функций можно получить с помощью GetProcAddress, которой необходимо передать дескриптор библиотеки и имя функции. Затем функцию из DLL можно вызывать, как обычно. Например:
Обратите внимание на приведение указателя к ссылке на тип FARPROC. FARPROC - это указатель на функцию, которая не принимает параметров и возвращает int. Именно такой указатель возвращает функция GetProcAddress. Приведение типа необходимо, чтобы умиротворить компилятор, который строго следит за соответствием типов параметров оператора присваивания. Альтернативный подход заключается в использовании оператора typedef с последующим приведением значения, возвращаемого GetProcAddress, к указателю на функцию с нужным прототипом.
Доступ к переменным
Хотя это не всегда очевидно из документации, получить указатель на переменную из DLL можно, используя все ту же функцию GetProcAddress. В нашем примере это выглядит так.
Использование классов
Сразу замечу, что в общем случае не рекомендуется размещать классы в библиотеках, подключаемых явно. Приемлемым можно считать только подход, который исповедует COM, при котором объекты класса создаются и разрушаются внутри DLL (для этого используются экспортируемые глобальные функции), а сам класс содержит исключительно виртуальные методы.
Однако предположим, что у нас нет доступа к исходным кодам библиотеки, содержащей класс, а использование других типов подключения DLL по каким-то причинам невозможно. Классом удастся воспользоваться и в этом случае, но для достижения цели придется проделать дополнительную работу.
Сначала задумаемся, почему объекты класса из явно подключаемой библиотеки нельзя использовать, как обычно. Дело в том, что при создании объекта класса компилятор генерирует вызов его конструктора. Но линкер не может разрешить этот вызов, поскольку адрес конструктора будет известен только в процессе выполнения программы. В результате сборка программы закончится неудачно. Такая же проблема возникает при вызове невиртуальных методов класса. С другой стороны, вызов виртуальных методов возможен, так как он осуществляется через таблицу виртуальных функций (vtable). Так, следующий фрагмент откомпилируется и слинкуется нормально (хотя, конечно, вызовет ошибку в процессе выполнения):
Приведенные выше рассуждения подсказывают решение проблемы. Коль скоро неявный вызов конструктра невозможен, мы можем вызвать его вручную, предварительно получив его адрес и выделив память под объект. Затем можно вызывать невиртуальные методы, получая их адреса с помощью GetProcAddress. Виртуальные методы можно вызывать, как обычно. Кроме того, необходимо не забыть вручную вызвать деструктор объекта, прежде чем выделенная для него память будет освобождена.
Продемонстрирую все сказанное на примере. Сначала мы выделяем память для объекта и вызываем для него конструктор. Память можно выделить как на стеке, так и в куче (с помощью оператора new). Рассмотрим оба варианта.
Обратите внимание на использование операторов .* и ->* для вызова функции-члена класса по указателю на нее. Этими операторами мы будем пользоваться и дальше.
ПРИМЕЧАНИЕ
Как правило, имена функций, экспортируемых из DLL, искажаются линкером. Поэтому вместо понятного имени, такого как "Constructor", получается совершенно нечитабельное имя вида "??0Class@@QAE@XZ". В рассматриваемом примере я назначил переменным и функциям нормальные имена при помощи def-файла следующего содержания:
Невиртуальные методы класса вызываются так же, как и конструктор, например:
Виртуальные методы вызываются непосредственно (как это делается для обычных классов). Хотя DLL и экспортирует их, явно получать их адреса с помощью GetProcAddress не требуется. Отсюда следует вывод: если все методы класса являются виртуальными, использование объектов класса из явно подключаемой библиотеки практически ничем не отличается от использования объектов любого другого класса. Разница только в том, что конструктор и деструктор для таких объектов придется вызывать вручную.
В нашем примере виртуальная функция вызывается так.
После того, как работа с объектом завершена, его нужно уничтожить, вызвав для него деструктор. Если объект был создан на стеке, деструктор необходимо вызвать до его выхода из области видимости, иначе возможны неприятные последствия (например, утечки памяти). Если объект был распределен при помощи new, его необходимо уничтожить перед вызовом delete. В нашем примере это выглядит так.
До сих пор я ничего не говорил о статических переменных и функциях класса. Дело в том, что они практически ни чем не отличаются от обычных функций и переменных. Поэтому к ним можно обращаться, используя уже известные нам методы. Например:
Выгрузка библиотеки
После того, как работа с библиотекой закончена, ее можно выгрузить, чтобы она не занимала системные ресурсы. Для этого используется функция FreeLibrary, которой следует передать дескриптор освобождаемой библиотеки. В нашем примере это выглядит так.
Отложенная загрузка
Использование отложенной загрузки
Вот и все. Теперь можно использовать функции и классы DLL прозрачно, как и в случае с неявным подключением. Единственная проблема возникает с переменными: их невозможно использовать напрямую. Дело в том, что при обращении к одной из функций в DLL мы на самом деле вызываем функцию __delayLoadHelper, которая и выполняет загрузку DLL (если она еще не загружена), затем получает адрес функции с помощью GetProcAddress и перенаправляет все последующие вызовы функции по этому адресу. Но при обращении к переменной вызова функции не происходит, а значит использовать __delayLoadHelper не удается.
Проблема решается путем явного использования GetProcAddress при работе с переменными. Если DLL еще не загружена, ее придется загрузить явно с помощью LoadLibrary. Но если мы уже обращались к ее функциям и точно знаем, что она находится в памяти, мы можем получить ее дескриптор с помощью функции GetModuleHandle, которой необходимо передать имя DLL. В нашем примере это выглядит так.
Выгрузка библиотеки
Итак, мы установили, что при использовании отложенной загрузки DLL грузится в память, когда происходит обращение к одной из ее функций. Но в последствии нам может потребоваться выгрузить ее, чтобы не занимать зря системные ресурсы. Специально для этого предназначена функция __FUnloadDelayLoadedDLL, объявленная в файле Delayimp.h. Если вы планируете использовать ее, вам нужно задать еще один ключ линкера - /DELAY:UNLOAD. Например:
Имя, которое вы передаете функции __FUnloadDelayLoadedDLL, должно в точности соответствовать имени, заданному в ключе /DELAYLOAD. Если, к примеру, передать ей "MYLIB.DLL" или "mylib.dll", библиотека останется в памяти.
ПРЕДУПРЕЖДЕНИЕ
Не используйте FreeLibrary, чтобы выгрузить DLL с отложенной загрузкой.
Обработка исключений
Как я уже говорил, в случае ошибки функция __delayLoadHelper возбуждает исключение. Если нужная DLL не обнаружена, возбуждается исключение с кодом VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND). Если в DLL не обнаружена требуемая функция, исключение будет иметь код VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND).
ПРИМЕЧАНИЕ
VcppException - это макрос, который используется для формирования кода ошибки в подсистеме Visual C++. Первый параметр задает "степень серьезности" ошибки, а второй - код исключения.
И то, и другое исключение можно обработать, используя механизм структурной обработки исключений. Для этого нужно написать фильтр , реагирующий на приведенные коды исключения, а также обработчик исключения. В простейшем случае они могут выглядеть так.
Написанный вами фильтр исключений может также получить дополнительную информацию с помощью функции GetExceptionInformation. Эта функция возвращает указатель на структуру EXCEPTION_POINTERS. В ней содержится поле ExceptionRecord - указатель на структуру EXCEPTION_RECORD. А структура EXCEPTION_RECORD в свою очередь содержит поле ExceptionInformation[0], в которое __delayLoadHelper помещает указатель на структуру DelayLoadInfo, содержащую дополнительную информацию. Эта структура объявлена следующим образом (файл Delayimp.h).
В частности, вы можете извлечь из нее имя DLL (поле szDll), а также имя или порядковый номер функции, вызов которой привел к исключению (поле dlp).
Функции-ловушки
Функции-ловушки должны иметь следующий прототип:
Первый параметр функции содержит код уведомления или ошибки, второй - указатель на уже знакомую нам структуру DelayLoadInfo. Все возможные коды уведомления описаны в файле Delayimp.h при помощи следующего перечисления:
В качестве примера приведу текст функции-ловушки, которая подменяет вызов функции SomeFunc на вызов функции YetAnotherFunc.
Ошибка в Delayimp.lib
И последнее замечание. Иногда при попытке слинковать программу с библиотекой Delayimp.lib линкер выдает ошибку Access Violation и аварийно завершается. Это связано с тем, что в некоторых дистрибутивах Visual C++ распространяется поврежденный файл Delayimp.lib. Если у вас возникла такая проблема, загрузите корректную версию файла здесь и скопируйте его в каталог %Visual Studio Folder%\Vc98\Lib\.
Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
Хорошо, я прочитал несколько статей Matt Pietrek о файлах Portable Executable (PE), например:
-
In-Depth Посмотрите в формате исполняемого файла Win32 Portable, часть 1 и Часть 2
статья MSJ о компоновщиках
статья MSJ о формате COFF
Кроме того, я прочитал несколько других источников по этому вопросу. Я либо игнорирую некоторые части, либо вопросы там не отвечают.
Итак, вот вопросы:
Известно, что при загрузке EXE загрузчик Windows считывает список импортированных DLL из таблицы адресов импорта (IAT) и загружает их в адресное пространство процесса.
Адресное пространство процесса - это виртуальное пространство. Возможно, DLL уже загружена в какое-то физическое пространство. Это происходит для DLL, таких как KERNEL32.dll или USER32.dll . Какова связь между физическим и виртуальным адресом? Загружает ли загрузчик только страницы и копирует DLL или делает ссылки?
Если DLL не загружена, загружает ли Loader всю DLL или только необходимые функции? Например, если вы использовали функцию foo() из bar.dll , загружает ли загрузчик весь bar.dll в адресное пространство процесса? Или он просто загружает код foo в адресное пространство процесса?
Предположим, что ваш EXE файл использует функцию MessageBox() от USER32.dll , которая находится в %WINDIR%\system32\user32.dll . Можете ли вы разработать персонализированный USER32.dll , поместить его в тот же каталог, что и ваш EXE файл, и ожидать, что ваш настраиваемый MessageBox будет вызван вашим приложением вместо системного по умолчанию MessageBox ?
Re 1: физические адреса не играют никакой роли, здесь все задействовано в виртуальной памяти. Физический адрес устанавливается только тогда, когда страница виртуальной памяти отображается в ОЗУ, вызванная ошибкой страницы. Многие базовые библиотеки DLL появляются в одном и том же адресе виртуальной памяти в нескольких процессах, таких как kernel32.dll. Процессы просто используют одни и те же страницы кода (а не данные).
Re 2: фактическая "загрузка" не выполняется, используемая функция является той же, которая поддерживает файлы с отображением памяти. Поддержкой этих страниц является сам файл DLL, а не файл подкачки. Ничто не загружается до тех пор, пока ошибка страницы не заставит Windows читать страницу из файла в ОЗУ. Но да, весь раздел кода DLL отображается.
Re 3: да, это сработает. Но практически невозможно заставить его работать на практике, так как вам придется писать функции замены для всех экспортируемых пользователем программ user32. Включая те, которые используют другие функции Win32, вы не можете знать. API-соединение - типичный метод, который используется, Detours из Microsoft Labs является хорошим.
Windows Internals edition 5 - отличная книга, чтобы узнать больше о сантехнике.
Мне любопытно узнать, как DLL загрузочных карт подключается к процессу адресного пространства. Как загрузчик делает эту магию. Пример очень важен.
Хорошо, я предполагаю, что у Windows есть вещи. Что происходит при загрузке PE файла, так это то, что загрузчик (содержащийся в NTDLL) выполнит следующее:
-
Найдите каждую из библиотек DLL, используя семантику поиска DLL (с учетом специфики системы и патча), известные DLL файлы отчасти освобождаются от этого
Сопоставьте файл в памяти (MMF), где страницы копируются на запись (CoW)
Перейдите в каталог импорта и для каждого запуска импорта (рекурсивно) в точке 1.
Устранение переходов, которые в большинстве случаев являются очень ограниченным числом объектов, поскольку сам код является независимым от положения кодом (PIC)
(IIRC) патёт EAT из RVA (относительный виртуальный адрес) в VA (виртуальный адрес в текущем пространстве памяти процесса)
Отметьте IAT (таблицу адресов импорта), чтобы ссылаться на импорт с их фактическим адресом в пространстве памяти процесса.
Для вызова DLL DLLMain() для EXE создайте поток, начальный адрес которого находится в точке входа PE файла (это также упрощено, поскольку фактический начальный адрес находится внутри kernel32.dll для процессов Win32)
Теперь, когда вы компилируете код, он зависит от компоновщика, как ссылается внешняя функция. Некоторые линкеры создают заглушки, так что - теоретически - попытка проверить адрес функции на NULL всегда будет говорить, что это не NULL. Это причуда, которую вы должны знать о том, когда и когда ваш линкер затронут. Другие ссылаются на запись IAT напрямую, и в этом случае адрес unreferenced function (think delay-loaded DLL) может быть NULL, а обработчик SEH затем вызывает помощник с задержкой и (пытается) разрешить адрес функции, прежде чем возобновлять выполнение на point it failed.
В вышеупомянутом процессе много ошибок, которые я упростил.
Суть в том, что вы хотели знать, заключается в том, что отображение в процесс происходит как MMF, хотя вы можете искусственно имитировать поведение с кучей пространства. Однако, если вы помните пункт о CoW, это суть идеи DLL. На самом деле одна и та же копия (большая часть) страниц DLL будет разделяться между процессами, которые загружают отдельную DLL. Не разделяемые страницы - это те, на которые мы писали, например, при разрешении перемещений и подобных вещей. В этом случае каждый процесс имеет теперь измененную копию исходной страницы.
И слово предупреждения относительно EXE-пакетов в DLL. Они проиграли именно этот механизм CoW, который я описал в том, что они выделяют пространство для распакованного содержимого библиотеки DLL в куче процесса, в который загружается DLL. Поэтому, пока фактическое содержимое файла по-прежнему отображается как MMF и совместно используется, распакованное содержимое занимает один и тот же объем памяти для каждого процесса, загружая DLL вместо того, чтобы делиться этим.
Я разобрал DLL и вижу там некоторые функции. Я нашел нужную мне функцию и адрес 0x10001340 .
Будет ли этот адрес оставаться неизменным, если я загружу эту DLL в свое приложение? Так можно ли мне вызвать эту функцию по этому адресу из моего приложения?
Я спрашиваю, потому что я не уверен: что, если при загрузке этой DLL какая-то функция в основном приложении уже имеет тот же адрес? Таким образом, возможно, функции внутри dll могут изменять адреса при загрузке и т.д.
В Windows dll есть предпочтительный адрес загрузки, но загрузчик может изменить все эти ссылки, если он замечает, что такая часть виртуального адресного пространства уже используется. Этот процесс называется "rebasing".
Базовый адрес "по умолчанию" указан во время компоновки ( /BASE с помощью компоновщика Microsoft), и может быть полезно установить его в значение, отличное от значения по умолчанию, если вы планируете использовать dll вместе с другим с помощью один и тот же базовый адрес; это ускоряет процесс загрузки, так как загрузчик не должен перегружать одну из DLL при каждой загрузке. (IIRC есть также инструменты, которые могут пересобирать существующую dll и сохранять результат на диске)
Хорошо иметь в виду, что с Windows Vista и dll, скомпилированные с указанным флагом, всегда загружаются на случайный базовый адрес, чтобы избежать каких-либо эксплойтов.
Очень маловероятно, что вы получите тот же адрес. Аргумент default/BASE для компоновщика для DLL - это 0x10000000, что ваша точка входа оказалась по этому адресу. Но есть много DLL, которые связаны с использованием настройки по умолчанию, только один может фактически загрузиться по этому адресу. Все остальные, которые загружаются позже, должны быть повторно основаны.
Вы можете придумать лучшее значение для /BASE, однако он никогда не гарантирует, что вы получите адрес загрузки, о котором вы просите.
Как сказал Маттео, DLL имеет предпочтительный адрес загрузки (указанный в поле ImageBase структуры IMAGE_OPTIONAL_HEADER). Когда система пытается загрузить DLL, она будет загружать его по этому адресу, если это возможно (если только рандомизация адресного пространства не включена), и не требуется "исправление". Если он не может загружаться по предпочитаемому адресу, DLL перемещается, что потребует каких-либо абсолютных ссылок в DLL для исправления, чтобы компенсировать перемещение.
Итак, чтобы ответить на ваш вопрос:
Нет никакой гарантии, что DLL будет загружена по его привилегированному адресу. После загрузки последующих загрузок не загружается больше копий DLL, поэтому адреса не будут меняться. Однако, как только выгруженные (DLL-ссылки подсчитываются), нет гарантии, что он будет загружен по тому же адресу в следующий раз.
Читайте также: