Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Создание эффективных приложений для Windows Джеффри Рихтер 2004 (Книга).pdf
Скачиваний:
377
Добавлен:
15.06.2014
Размер:
8.44 Mб
Скачать

Рис. 20-5. DelayLoadApp сообщает, что модуль "20 DelayLoadLib» загружен

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

Далее вызывается __FUnloadDelayLoadedDLL, и модуль «20 DelayLoadLib» выгружается из памяти. После очередного вьиова IsModuleLoaded на экране появляется окно, показанное на рис. 20-4. Наконец, вновь вызывается импортируемая функция, что приводит к повторной загрузке модуля «20 DelayLoadLib», a IsModuleLoaded открывает окно, как на рис. 20-5.

Если все нормально, то программа будет работать, как я только что рассказал. Однако, если перед запуском программы Вы удалите модуль «20 DelayLoadLib» или если в этом модуле не окажется одной из импортируемых функций, будет возбуждено исключение. Из моего кода видно, как корректно выйти из такой ситуации.

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

Переадресация вызовов функций

Запись о переадресации вызова функции (function forwarder) — это строка в разделе экспорта DLL, которая перенаправляет вызов к другой функции, находящейся в другой

DLL Например, запустив утилиту DumpBin из Visual С++ для Kcrncl32dll в Windows 2000,

Вы среди прочей информации увидите и следующее

С \winnt\system32>DumpBin -Exports Kernel32.dll (часть вывода опущена)

360 167 HeapAlloc (forwarded to NTDLL RtlAllocateHeap)

361 168 HeapCompact (000128D9)

362 1Ь9 HeapCreate (000126EF)

363 16A HeapCreateTagsW (0001279E)

364 16B HpapDpstroy (0001?750)

365 16C HeapExtend (00012773)

366 16D HeapFree (forwarded to NTDLL RtlFreeHeap)

367 16E HeapLock (000128ED)

368 16F HeapQueryTagW (000127B8)

369 170 HeapReAlloc (forwarded to NTDLL RtlReAllocateHeap)

370 171 HeapSize (forwarded to NTDLL RtlSizeHeap) (остальное тоже опущено)

Здесь есть четыре переадресованные функции Всякий раз, когда Ваше приложение вызывает HeapAlloc, HeapFree, HeapReAlloc или HeapSize, его ЕХЕ-модуль динамически связывается с Kernel32.dll При запуске ЕХЕ-модуля загрузчик загружает Kernel32dll и, обнаружив, что переадресуемые функции на самом деле находятся в NTDLLdll, загружаег и эту DLL Обращаясь к HeapAlloc, программа фактически вызы васт функцию Rltоса1еНеар из NTULL.dll А функции HeapAlloc вообще нет1

При вызове НеарАllос (см ниже) функция GetProcAddress просмотрит раздел экспорта Kernel32dll и, выяснив, чю НеарАllос — переадресуемая функция, рекурсивно вызовет сама себя для поиска RtlAllocateHeap в разделе экспорта NTDLL.dll.

GetProcAddress(GetModuleHandle("Kernel32"), "НеарАllос" );

Вы тоже можете применять переадресацию вызовов функций в своих DLL. Самый простой способ — воспользоваться директивой pragma:

// переадресация к функции из DllWork

#pragma comment(linker, "/export:SomeFunc=DllWork.SomeOtherFunc")

Эта директива сообщает компоновщику, что DLL должна экспортировать функцию SomeFunc, которая на самом деле реализована как функция SomeOtherFunc в модуле DlIWork dll Такая запись нужна для каждой переадресуемой функции

Известные DLL

Некоторые DLL, поставляемые с операционной системой, обрабатываются по-особому. Они называются известными DLL (known DLLs) и ведут себя точно так же, кяк и любые другие DLL с тем исключением, что система всегда ищет их в одном и том же каталоге. D реестре есть раздел:

HKEY_LOCAL_MACHTNE\SYSTEM\CurrentControlSet\Control\Session

Manager\KnownDLLs

Содержимое этого раздела может выглядеть примерно так, как показано ниже (при просмотре реестра с помощью утилиты RegEdit.exe).

Как видите, здесь содержится набор параметров, имена которых совпадают с именами известных DLL. Значения этих параметров представляют собой строки, идентичные именам параметров, но дополненные расширением .dll. (Впрочем, это не всегда так, и Вы сами убедитесь в этом на следующем примере) Когда Вы вызываете LoadLibrary или LoadLibraryEx, каждая из них сначала проверяет, указано ли имя DLL вместе с расширением .dll. Если нет, поиск DLL ведется по обычным правилам.

Если же расширение .dll указано, функция его отбрасывает и ищет в разделе реестра KnownDLLs параметр, имя которого совпадает с именем DLL. Если его нет, вновь применяются обычные правила поиска А если он есть, система считывает значение этого параметра и пытается загрузить заданную в нем DLL. При этом система ищет

DLL в каталоге, на который указывает значение, связанное с параметром реестра DllDirectory. По умолчанию в Windows 2000 параметру DllDirectory присваивается значение %SystemRoot%\System32

А теперь допустим, что мы добавили в раздел реестра KnownDLLs такой параметр

Имя параметра; SomeLib Значение параметра SomeOtherLib.dll

Когда мы вызовем следующую функцию, система будет искать файл по обычным правилам,

LoadLibrary("SomeLib");

Но если мы вызовем ее так, как показано ниже, система увидит, что в реестре есть параметр с идентичным именем (не забудьте она отбрасывает расширение .dll).

LoadLibrary("SomeLib dll");

Таким образом, система попытается загрузить SomeOtherLib.dll вместо SomcLib dll При этом она будет сначала искать SomeOtherLib.dll в каталоге %SystemRoot%\System32. Если нужный файл в этом каталоге есть, будет загружен именно он. Нет — LoadLibrary(Ex)

вернет NULL, a GetLastError - ERROR_FILE_NOT_FOUND (2).

Перенаправление DLL

WINDOWS 98

Windows 98 не поддерживает перенаправление DLL.

Когда разрабатывались первые версии Windows, оперативная намять и дисковое пространство были крайне дефицитным ресурсом, так что Windows была рассчитана на предельно экономное их использование — с максимальным разделением между потребителями. В связи с этим Microsoft рекомендовала размещать все модули, используемые многими приложениями (например, библиотеку С/С++ и DLL, относящиеся к MFC) в системном каталоге Windows, где их можно было легко найти.

Однако со временем это вылилось в серьезную проблему: программы установки приложений то и дело перезаписывали новые системные файлы старыми или не полностью совместимыми. Из-за этого уже установленные приложения переставали работать. Но сегодня жесткие диски стали очень емкими и недорогими, оперативная память тоже значительно подешевела. Поэтому Microsoft сменила свою позицию на прямо противоположную; теперь она настоятельно рекомендует размещать все фаЙлы приложения в своем каталоге и ничего не трогать в системном каталоге Windows. Тогда Вашс приложение не нарушит работу других программ, и наоборот.

С той же целью Microsoft ввела в Windows 2000 поддержку перенаправления DLL (DLL redirection). Она заставляет загрузчик операционной системы загружав модули сначала из каталога Вашего приложения и, только если их там нет, искать в других каталогах

Чтобы загрузчик всегда проверял сначала каталог приложения, нужно всего лишь поместить туда специальный файл Его содержимое не имеет значения и игнорируется — вяжно только его имя: оно должно быть в виде AppName.local. Так, если исполняемый файл Вашего приложения — SupcrAppexe, присвойте перенаправляющему файлу имя

SuperApp cxc local

Функция LoadLibrary(Ex) проверяет наличие этого файла и, ссли он есть, загружает молуль из каталога приложения; в ином случае LoadLibrary(Ex) работает так же, как и раньше.

Перенаправление DLL исключительно полезно для работы с зарегистрированными СОМобъектами. Оно позволяет приложению размещать DLL с СОМ-объектами в своем каталоге, и другие программы, регистрирующие те же объекты, не будут мешать его нормальной работе.

Модификация базовых адресов модулей

У каждого EXE и DLL-модуля есть предпочтительный базовый адрес (preferred base address) — идеальный адрес, по которому он должен проецироваться на адресное пространство процесса. Для ЕХЕ-модуля компоновщик выбирает в качестве такого адреса значение 0x00400000, а для DLL-модуля — 0x10000000. Выяснить этот адрес позволяет

утилита DumpBin с ключом /Headers. Вот какую информацию сообщает DumpBin о самой себе:

С \>DUMPBIN /headers dumpbin.exe

Microsoft (R} COFF Binary File Dumper Version 6 00.8168 Copyright (C) Microsoft Corp 1992-1998. All rights reserved

Dump of file dumpbin.exe

PE signature found

File Type: EXECUTABLE_IMAGE

File HEADER VALUES

14C machine (i386)

3 number of sections

3588004A time date stamp Wed Jun 17 10'43-38 1998 0 file pointer to symbol table 0 number of symbols E0 size of optional header 10F characteristics

Relocations stripped

Executable

Line numbers stripped

Symbols stripped

32 bit word machine OPTIONAL HEADER VALUES 108 magic #

6.00 linker version

1000 size of code

2000 size of initialized data

0 size of uninitialized data

1320 RVA of entry point

1000 base of code

2000 base of data

400000 image base <-- предпочтительный базовый адрес модуля

1000 section alignment

1000 file alignment 4.00 operating system verbion 0.00 image version 4.00 subsystem version

0 Win32 version 4000 size of image 1000 size of headers 127E2 checksum

3 subsystem (Windows CUI)

0 DLL characteristics

100000 size of stack reserve 1000 size of stack commit

При запуске исполняемого модуля загрузчик операционной системы создает виртуальное адресное пространство нового процесса и проецирует этот модуль по адресу 0x00400000, а DLL-модуль — по адресу 0x10000000. Почему так важен предпочтительный базовый адрес? Взгляните на следующий фрагмент кода.

int g_x;

void Func()

{

g_x = 5; // нас интересует эта строка

}

После обработки функции Func компилятором и компоновщиком полученный машинный код будет выглядеть приблизительно так:

MOV [0x00414540], b

Иначе говоря, компилятор и компоновщик "жестко зашили" в машинный код адpеc переменной g_x: в адресном пространстве процесса (0x00414540). Но, конечно, этот адрес корректен, только ссли исполняемый модуль будет загружен по базовому адресу

0x00400000

А что получится, если тот же исходный код будет помещен в DLL? Тогда машинный код будет иметь такой вид

MOV [0x10014b40], 5

Заметьте, что и на этот paз виртуальный адрес переменной g_x "жестко зашит" в машинный код. И опять жс этот адрес будет правилен только при том условии, что DLL загрузится по своему базовому адресу.

О'кэй, а теперь представьте, что Вы создали приложение с двумя DLL. По умолчанию компоновщик установит для ЕХЕ-модуля предпочтительный базовый адрес 0x00400000, а для обеих DLL — 0x10000000. Если Вы затем попытаетесь запустить исполняемый файл, загрузчик создаст виртуальное адресное пространство и спроецирует ЕХЕ-модуль по адресу 0x00400000 Далее первая DLL будет спроецирована по адресу 0x10000000, но загрузить вторую DLL по предпочтительному базовому адресу не удастся — ee придется проецировать по какому-то другому адресу.

Переадресация (relocation) в EXEили DLL-модуле операция просто ужасающая, и Вы должны сделать все, чтобы избежать ее. Почему? Допустим, загрузчик переместил вторую DLL по адресу 0x20000000. Тогда код, который присваивает переменной

g_x значение 5, должен измениться на:

MOV [0x20014540], 5

Но в образе файла код остался прежним:

MOV [0x10014540], 5

Если будет выполнен именно этот кол, он перезапишет какое-то 4-байтовое значение в первой DLL значением 5 Но, по идее, такого не должно случиться. Загрузчик исправит этот код. Дсло в том, что, создавая модуль, компоновщик встраивает в конечный файл раздел переадресации (relocation section) co списком байтовых смещений. Эти смещения идентифицируют адреса памяти, используемые инструкциями машинного кода. Если загрузчику удастся спроецировать модуль по его предпочтительному базовому адресу, раздел переадресации не понадобится Именно этого мы и хотим.

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

В предыдущем примере вторая DLL была спроецирована по адресу 0x20000000, тогда как ее предпочтительный базовый адрес — 0x10000000 Получаем разницу (0х 10000000), добавляем ее к адресу в машинной команде и получаем.

MOV [0x20014540], 5

Теперь и вторая DLL корректно ссылается на переменную g_x. Невозможность загрузить модуль по предпочтительному базовому адресу создает две крупные проблемы

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

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

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

Кстати, Вы можете создать EXEили DLL-модуль без раздела переадресации, указав при сборке ключ /FIXED компоновщика. Тогда у модуля будет меньший размер, но загрузить сго по другому базовому адресу, кроме предпочтительного, уже не удастся. Если загрузчику понадобится модифицировать адреса в модуле, в котором пет раздела переадресации, он уничтожит весь процесс, и пользователь увидит сообщение «Abnormal Process Termination» («аварийное завершение процесса")

Для DLL, содержащей только ресурсы, это тоже проблема. Хотя в ней нет машинного кода, отсутствие раздела переадресации не позволит загрузить ее по базовому

адресу, отличному от предпочтительного Просто нелепо. Но, к счастью, компонов щик может встроить в заголовок модуля информацию о том, что в модуле нет раздела переадресации, так как он вообще не нужен. А загрузчик Windows 2000, обнаружив эту информацию, может пагрупить DLL, которая содержит только ресурсы, без дополнительной нагрузки на страничный файл.

Для создания файла с немодифицируемыми адресами предназначен ключ

/SUBSYSTEM:WINDOWS, 5 0 или /SUBSYSTEM:CONSOLE, 5 0; ключ /FIXED при этом не нужен. Если компоновщик определяет, что модификация адресов в модуле не понадобится, он опускает раздел переадресации и сбрасывает в заголовке специальный флаг IMAGEFILERELOCS_STRIPPED Тогда Windows 2000 увидит, что данный модуль можно загружать по базовому адресу, отличному от предпочтительного, и что ему не требуется модификация адресов. Но все, о чем я только что рассказал, поддерживается лишь в Windows 2000 (вот почему в ключе /SUBSYSTEM указывается значение 50)

Теперь Вы понимаете, насколько важен предпочтительный базовый адрес. Загружая несколько модулей в одно адресное пространство, для каждого из них приходится выбирать свои базовые адреса. Диалоговое окно Project Settings в среде Microsoft Visual Studio значительно упрощает решение этой задачи. Вам нужно лишь открыть вкладку Link, в списке Category указать Output, а в поле Base Address ввести предпочтительный адрес. Например, на следующей иллюстрации для DLL установлен базовый адрес

0x20000000

Кстати, всегда загружайте DLL, начиная со старших адресов; это позволяет уменьшить фрагментацию адресного пространства.

NOTE:

Предпочтительные базовые адреса должны быть кратны гранулярности выделения памяти (64 Кб на всех современных платформах). В будущем эта цифра может измениться Подробнее о гранулярности выделения памяти см. главу 13

О'кэй, все это просто замечательно, но что делать, если понадобится загрузить кучу модулей в одно адресное пространство? Было бы неплохо «одним махом» задать правильные базовые адреса для всех модулей. К счастью, такой способ есть

В Visual Studio есть утилита Rebase.exe. Запустив ее без ключей в командной строке, Вы получите информацию о том, как ею пользоваться. Она описана в документации Platform SDK, и я не буду ее здесь детально рассматривать Добавлю лишь, что в ней нет ничего сверхъестественного: она просто вызывает функцию ReBaselmage для каждого указанного файла. Вот что представляет собой эта функция:

BOOL ReBaseImage(

PSIR CurrentImageName; // полное имя обрабатываемого файла PSTR SymbolPath; // символьный путь к файлу (необходим для корректности отладочной информации)

BOOL fRebase; // TRUE = выполнить реальную модификацию адреса; // FALSE - имитировать такую модификацию

BOOL fRebasoSysFileOk; // FALSE = не модифицировать адреса системных файлов

BOOL fGoingDown; // TRUE = модифицировать адрес модуля, // продвигаясь в сторону уменьшения адресов

ULONG CheckImageSize; // ограничение на размер получаемого в итоге модуля

ULONG* pOldImageSize; // исходный размер модуля

ULONG* pOldImageBase; // исходный базовый адрес модуля

ULONG* pNewIinageSize; // ноеый размер модуля

ULONG* pNfiwImageRase; // новый базовый адрес модуля

ULONG TirneStamp); // новая временная мегка модуля

Когдя Вы запускаете утилиту Rebase, указывая ей несколько файлов, она выполняет следующие операции.

1.Моделирует создание адресного пространства процесса

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

3.Моделирует переадресацию модулей в адресном пространстве, добиваясь того, чтобы модули не перекрывались.

4.В каждом модуле анализирует раздел переадресации и соответственно изменяет код в фяйле модуля на диске.

5.Записывает новый базовый адрес в заголовок файла.

Rebase — отличная утилита, и я настоятельно рекомендую Вам пользоваться ею. Вы должны запускать ее ближе к концу цикла сборки, когда уже созданы все модули приложения. Кроме того, применяя утилиту Rebase, можно проигнорировать настройку базового адреса в диалоговом окне Pro)cct Settings. Она автоматически изменит базовый адрес 0x10000000 для DLL, задаваемый компоновщиком по умолчанию

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