Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6269
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

668 Часть IV. Динамически подключаемые библиотеки

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

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

Имя параметра: SomeLib

Значение параметра: SomeOtherLib.dll

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

LoadLibrary(TEXT("SomeLib"));

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

.dll).

LoadLibrary(TEXT("SomeLib.dir));

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

но он. Нет — LoadLibrary(Ex) вернет NULL, а GetLastError — ERROR_FILE__NOT_FOUND (2).

Глава 20. DLL - более сложные методы программирования.docx 669

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

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

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

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

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

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

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

Учтите, что по соображениям безопасности (во избежание загрузки поддельных системных DLL из каталога приложения вместо системных каталогов) в Windows Vista эта функция по умолчанию отключена. Чтобы включить ее снова, необходимо создать DWORD-значение DevOverrideEnable в разделе

HKLM\Software\Microsoft\WindowsNT\CurrentVersion\Image File Execution Options

и присвоить ему значение 1.

670 Часть IV. Динамически подключаемые библиотеки

Примечание. Со времени выхода Windows XP и расцвета приложений для платформы Microsoft .NET автономные приложения и сборки, пригодные для одновременного развертывания (side-by-side assemblies), могут содержать не только управляемый, но и неуправляемый код (см. статью «Isolated

Applications and Side-by-side Assemblies» at http://msdn2.microsoft.com/enus/library/aa375193.aspx).

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

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

C:\>DUMPBIN /headers dumpbin.exe

Microsoft (R) C0FF/PE Dumper Version 8.00.50727.42

Copyright (C) Microsoft Corporation. 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

4333ABD8 time date stamp Fri Sep 23 09:16:40 2005 0 file pointer to symbol table

0 number of symbols

E0 size of optional header

123 characteristics Relocations stripped Executable

Application can handle large (>26B) addresses 32 bit word machine

OPTIONAL HEADER VALUES

10B magic # (PE32)

8.00 linker version

1200 size of code

800 size of initialized data

0 size of uninitialized data

Глава 20. DLL - более сложные методы программирования.docx 671

170C entry point (0040170C)

1000 base of code

3000 base of data

400000 image base (00400000 to 00404FFF) <- Module's preferred base address

1000 section alignment

200 file alignment

5.00 operating system version

8.00 image version

4.00 subsystem version

0 Win32 version

5000 size of image

400 size of headers

1306D checksum

3 subsystem (Windows CUI)

8000 DLL characteristics Terminal Server Aware

100000 size of stack reserve

2000 size of stack commit

100000 size of heap reserve

1000 size of heap commit

0 loader flags

10 number of directories

...

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

int g_x;

 

void Func() {

 

g.x = 5;

// нас интересует эта строка

}

 

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

M0V

[0x00414540], 5

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

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

672 Часть IV. Динамически подключаемые библиотеки

NOV

[0x10014540], 5

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

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

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

M0V

[0x20014540], 5

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

M0V

[0x10014540], 5

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

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

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

M0V

[0x20014540], 5

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

дает две крупные проблемы.

Глава 20. DLL - более сложные методы программирования.docx 673

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

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

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

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

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

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

/SUBSYSTEM:WINDOWS, 5.0 или /SUBSYSTEM:CONSOLE, 5.0; ключ /FIXED

при этом не нужен. Если компоновщик определяет, что модификация адресов в модуле не понадобится, он опускает раздел переадресации и сбрасывает в заго-

ловке специальный флаг IMAGEJFILE_RELOCS_ STRIPPED. Тогда Windows

2000 увидит, что данный модуль можно загружать по базовому адресу, отличному от предпочтительного, и что ему не требуется модификация адресов. Но все, о чем я только что рассказал, поддерживается начиная с Windows 2000 (вот почему в ключе /SUBSYSTEM указывается значение 5.0).

Теперь вы понимаете, насколько важен предпочтительный базовый адрес. Загружая несколько модулей в одно адресное пространство, для каждого из них приходится выбирать свои базовые адреса. Диалоговое окно Project

674 Часть IV. Динамически подключаемые библиотеки

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

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

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

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

В Visual Studio есть утилита Rebase.exe. Запустив ее без ключей в командной строке, вы получите информацию о том, как ею пользоваться. Вот что представляет собой эта функция:

usage: REBASE [switches]

[-R image-root [-G filename] [-0 filename] [-N filename]] image-names...

Глава 20. DLL - более сложные методы программирования.docx 675

One of -b and -i switches are mandatory,

[-a] Does nothing

[-b InitialBase] specify initial base address [-c coffbase_filename] generate coffbase.txt

-C includes filename extensions, -c does not [-d] top down rebase

[-e .SizeAdjustment] specify extra size to allow for image growth [-f] Strip relocs after rebasing the image

[-i coffbase_filename] get base addresses from coffbase_filename [-l logFilePath] write image bases to log file.

[-p] Does nothing [-q] minimal output

[-s] just sum image range

[-u symbol_dir] Update debug info in .DBQ along this path [-v] verbose output

[-x symbol_dir] Same as –u [-z] allow system file rebasing [-?] display this message

[-R image_root] set image root for use by -G, -0f –N [-G filename] group images together in address space [-0 filename] overlay images in address space

[-N filename] leave images at their original address

-G, -0, -N, may occur multiple times. File "filename" contains a list of files (relative to "image-root")

'image-names' can be either a file (foo.dll) or files (*.dll) or a file that lists other files (dfiles.txt).

If you want to rebase to a fixed address (ala OFE) use the eefiles.txt format where files.txt contains address/size combos in addition to the filename

Она описана в документации Platform SDK, и я не буду ее здесь детально рассматривать. Добавлю лишь, что в ней нет ничего сверхъестественного: она просто вызывает функцию ReBaseImage из ImageHlp API для каждого указанного файла.

BOOL ReBaseImage(

 

PCSTR CurrentImageName,

// полное имя обрабатываемого файла

PCSTR SymbolPath,

// символьный путь к файлу (необходим для

 

// корректности отладочной информации)

BOOL bRebase,

// TRUE = выполнить реальную модификацию

 

// адреса; FALSE = имитировать такую

BOOL bRebaseSysFileOk,

// модификацию FALSE = не модифицировать

 

// адреса системных файлов

BOOL bGoingDown,

// TRUE = модифицировать адрес модуля,

676 Часть IV. Динамически подключаемые библиотеки

 

// продвигаясь в сторону уменьшения адресов

UL0NG CheckImageSize,

// ограничение на размер получаемого в итоге

 

// модуля (0 8 размер не ограничен)

ULONG* pOldImageSize,

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

ULONG* pOldImageBase,

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

ULONG* pNewImageSize,

// новый размер модуля

ULONG* pNewImageBase,

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

ULONG TimeStamp);

// новая временная метка модуля

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

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

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

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

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

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

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

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

Я, кстати, добавил специальный инструмент в свою программу ProcessInfo. exe (см. главу 4). Он показывает список всех модулей, находящихся в адресном пространстве процесса. В колонке BaseAddr сообщается виртуальный адрес, по которому загружен модуль. Справа от BaseAdd расположена колонка ImagAddr. Обычно она пуста, указывая, что соответствующий модуль загружен по его предпочтительному базовому адресу. Так и должно быть для всех модулей. Однако, если в этой колонке присутствует адрес в скобках, значит, модуль загружен не по предпочтительному базовому адресу, и в колонке ImagAddr показывается базовый адрес, взятый из заголовка его файла на диске.

Ниже приведена информация о процессе devenv.exe, предоставленная моей программой ProcessInfo. Обратите внимание, что часть модулей загружена по предпочтительным базовым адресам, а часть — нет. Для последних сообщается один и тот же базовый адрес, 0x00400000; значит, автор этих DLL не подумал о проблемах модификации базовых адресов — пусть ему будет стыдно.

Глава 20. DLL - более сложные методы программирования.docx 677

Связывание модулей

Модификация базовых адресов действительно очень важна и позволяет существенно повысить производительность всей системы. Но вы можете сделать еще больше. Допустим, вы должным образом модифицировали базовые адреса всех модулей своего приложения. Вспомните из главы 19, как загрузчик определяет адреса импортируемых идентификаторов: он записывает виртуальные адреса идентификаторов в раздел импорта EXE-модуля. Это позволяет, ссылаясь на импортируемые идентификаторы, адресоваться к нужным участкам в памяти.

Давайте поразмыслим. Сохраняя виртуальные адреса импортируемых идентификаторов в разделе импорта EXE-модуля, загрузчик записывает их на те страницы памяти, где содержится этот раздел. Здесь включается в работу механизм копирования при записи, и их копии попадают в страничный файл. И у нас опять та же проблема, что и при модификации базовых адресов: отдельные части проекции модуля периодически сбрасываются в страничный файл и вновь подгружаются из него. Кроме того, загрузчику приходится преобразовывать адреса всех импортируемых идентификаторов (для каждого модуля), на что может понадобиться немалое время.

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

В Visual Studio есть еще одна утилита, Bind.exe. Информацию о том, как ею пользоваться, вы получите, запустив Bind.exe без ключей в командной строке. Вот что представляет собой эта функция:

usage: BIND [switches] image-names...

[-?] display this message [-c] no caching of import dlls

[-o] disable new import descriptors [-p dll search path]

Соседние файлы в предмете Программирование на C++