Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009
.pdf630 Часть IV. Динамически подключаемые библиотеки
что идентификатор, на который мы ссылаемся, импортируется из LIB-файла DLLмодуля. Вот почему я настоятельно рекомендую пользоваться ключевым словом __declspec(dllimport) для импортируемых функций и идентификаторов данных. Именно его подставляет за вас операционная система, когда вы вызываете любую из стандартных Windows-функций.
Разрешая ссылки на импортируемые идентификаторы, компоновщик создает в конечном ЕХЕ-модуле раздел импорта (imports section). В нем перечисляются DLL, необходимые этому модулю, и идентификаторы, на которые есть ссылки из всех используемых DLL.
Воспользовавшись утилитой DumpBin.exe (с ключом -imports), мы можем увидеть содержимое раздела импорта. Ниже показан фрагмент полученной с ее помощью таблицы импорта Calc.exe.
C:\Windows\System32>DUMPBIN -imports Calc.exe
Microsoft (R) COFF/PE Dumper Version 8.00.50727.42
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file calc.exe
File Type: EXECUTABLE IMAGE
Section contains the following imports:
SHELL32.dll
10010CC Import Address Table 1013208 Import Name Table FFFFFFFF time date stamp
FFFFFFFF Index of first forwarder reference
766EA0A5 |
110 ShellAboutW |
|
|
ADVAPI32.dll
1001000 Import Address Table
101313C Import Name Table FFFFFFFF time date stamp
FFFFFFFF Index of first forwarder reference 77CA8229 236 RegCreateKeyW
77CC802D 278 RegSetValueExW
77CD632E 268 RegQueryValueExW 77CD64CC 22A RegCloseKey
...
ntdll.dll
1001250 Import Address Table
101338С Import Name Table FFFFFFFF time date stamp
Глава 19. DLL - основы.docx 631
FFFFFFFF Index of first forwarder reference
77F0850D 548 WinSqmAddToStream KERNEL32.dll
1001030 Import Address Table
101316C Import Name Table FFFFFFFF time date stamp
FFFFFFFF Index of first forwarder reference
77E01890 24F GetSystemTimeAsFileTime 77E47B0D 1AA GetCurrentProcessId 77E2AA46 170 GetCommandLineW 77E0918D 230 GetProfilelntW
…
Header contains the following bound import information: Bound to SHELL32.dll [4549BDB4] Thu Nov 02 10:43:16 2006 Bound to ADVAPI32.dll [4549BCD2] Thu Nov 02 10:39:30 2006 Bound to 0LEAUT32.dll [4549BD95] Thu Nov 02 10:42:45 2006 Bound to ole32.dll [4549BD92] Thu Nov 02 10:42:42 2006 Bound to ntdll.dll [4549BDC9] Thu Nov 02 10:43:37 2006 Bound to KERNEL32.dll [4549BD80] Thu Nov 02 10:42:24 2006 Bound to GDI32.dll [4549BCD3] Thu Nov 02 10:39:31 2006 Bound to USER32.dll [4549BDE0] Thu Nov 02 10:44:00 2006 Bound to msvcrt.dll [4549BD61] Thu Nov 02 10:41:53 2006
Summary
2000 .data
2000 .reloc
16000 .rsrc
13000 .text
Как видите, в разделе есть записи по каждой DLL необходимой Calc.exe: Shell32.dll, AdvAPI32.dll, OleAut32.dll, O1e32.dll, Ntdll.dll, Kernel32.dll, GDI32.dll, User32.dll и MSVCRXdll. Под именем DLL-модуля выводится список идентификаторов, импортируемых программой Саlc.exe. Например, Calc.exe обращается к следующим функциям из Kernel32.dll: GetCurrentProcessId, GetCommandLineW, GetProfileIntW и др.
Число слева от импортируемого идентификатора называется «подсказкой» (hint) и для нас несущественно. Крайнее левое число в строке для идентификатора сообщает адрес, по которому он размещен в адресном пространстве процесса. Такой адрес показывается, только если было проведено связывание (binding) исполняемого модуля, но об этом — в главе 20.
Глава 19. DLL - основы.docx 633
модуле, проверяя наличие указанного идентификатора в соответствующей DLL. He обнаружив его (что происходит крайне редко), загрузчик выводит следующее сообщение:
Если же идентификатор найден, загрузчик отыскивает его RVA и прибавляет к виртуальному адресу, по которому данная DLL размещена в адресном пространстве процесса, а затем сохраняет полученный виртуальный адрес в разделе импорта ЕХЕ-модуля. И с этого момента ссылка в коде на импортируемый идентификатор приводит к выборке его адреса из раздела импорта вызывающего модуля, открывая таким образом доступ к импортируемой переменной, функции или функции-члену С++-класса. Вот и все — динамические связи установлены, первичный поток процесса начал выполняться, и приложение наконец-то работает!
Естественно, загрузка всех этих DLL и настройка ссылок занимает какое-то время. Но, поскольку такие операции выполняются лишь при запуске процесса, на производительности приложения это не сказывается. Тем не менее, для многих программ подобная задержка при инициализации неприемлема. Чтобы сократить время загрузки приложения, вы должны модифицировать базовые адреса своих ЕХЕ- и DLL-модулей и провести их (модулей) связывание. Увы, лишь немногие разработчики знают, как это делается, хотя эти приемы очень важны. Если бы ими пользовались все компании-разработчики, система работала бы куда быстрее. Я даже считаю, что операционную систему нужно поставлять с утилитой, позволяющей автоматически выполнять эти операции. О модификации базовых адресов модулей и о связывании я расскажу в следующей главе.
Оглавление |
|
Г Л А В А 2 0 DLL: более сложные методы программирования................................... |
634 |
Явная загрузка DLL и связывание идентификаторов................................................ |
634 |
Явная загрузка DLL ........................................................................................................... |
635 |
Явная выгрузка DLL.......................................................................................................... |
639 |
Явное подключение экспортируемого идентификатора ...................................... |
642 |
Функция входа/выхода........................................................................................................ |
643 |
Уведомление DLL_PROCESS_ATTACH ....................................................................... |
644 |
Уведомление DLL_PROCESS_DETACH ....................................................................... |
646 |
Уведомление DLL_THREAD_ATTACH .......................................................................... |
648 |
Уведомление DLL_THREAD_DETACH .......................................................................... |
649 |
Как система упорядочивает вызовы DllMain............................................................ |
650 |
Функция DllMain и библиотека С/С++ .......................................................................... |
653 |
Отложенная загрузка DLL ................................................................................................... |
654 |
Программа-пример DelayLoadApp................................................................................ |
659 |
Переадресация вызовов функций................................................................................... |
666 |
Известные DLL ....................................................................................................................... |
667 |
Перенаправление DLL.......................................................................................................... |
669 |
Модификация базовых адресов модулей ..................................................................... |
670 |
Связывание модулей........................................................................................................... |
677 |
Г Л А В А 2 0
DLL: более сложные методы программирования
В предыдущей главе мы говорили в основном о неявном связывании, поскольку это самый популярный метод. Представленной там информации вполне достаточно для создания большинства приложений. Однако DLL открывают нам гораздо больше возможностей, и в этой главе вас ждет целый «букет» новых методов, относящихся к программированию DLL. Во многих приложениях эти методы, скорее всего, не понадобятся, тем не менее они очень полезны, и познакомиться с ними стоит. Я бы посоветовал, как минимум, прочесть разделы «Модификация базовых адресов модулей» и «Связывание модулей»; подходы, изложенные в них, помогут существенно повысить быстродействие всей системы.
Явная загрузка DLL и связывание идентификаторов
Чтобы поток мог вызвать функцию из DLL-модуля, последний надо спроецировать на адресное пространство процесса, которому принадлежит этот поток. Делается это двумя способами. Первый состоит в том, что код вашего приложения просто ссылается на идентификаторы, содержащиеся в DLL, и тем самым заставляет загрузчик неявно загружать (и связывать) нужную DLL при запуске приложения.
Второй способ — явная загрузка и связывание требуемой DLL в период выполнения приложения. Иначе говоря, его поток явно загружает DLL в адресное пространство процесса, получает виртуальный адрес необходимой DLL-функции и вызывает ее по этому адресу. Изящество такого подхода в том, что все происходит в уже выполняемом приложении.
На рис. 20-1 показано, как приложение явно загружает DLL и связывается с ней.
636 Часть IV. Динамически подключаемые библиотеки
HMOOULE LoadLibraryEx(
PCTSTR pszDLLPathName,
HANDLE hFile,
DWORD dwFlags);
Обе функции ищут образ DLL-файла (в каталогах, список которых приведен в предыдущей главе) и пытаются спроецировать его на адресное пространство вызывающего процесса. Значение типа HINSTANCE, возвращаемое этими функциями, сообщает адрес виртуальной памяти, по которому спроецирован образ файла. Если спроецировать DLL на адресное пространство процесса не удалось, функции возвращают NULL. Дополнительную информацию об ошибке можно получить вызовом GetLastError.
Очевидно, вы обратили внимание на два дополнительных параметра функции LoadLibraryEx: hFile и dwFlags. Первый зарезервирован для использования в будущих версиях и должен быть NULL. Во втором можно передать либо 0, либо комбинацию флагов DONT_RESOLVE_DLL_REFERENCES, LOAD_LIBRARY_ AS_DATAFILE, LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE, LOAD_LIBRARY_AS_IMAGE_RESOURCE, LOAD_WITH_ALTERED_SEARCH_PATH и LOAD_IGNORE_CODE_AUTHZ_LEVEL о которых мы сейчас и поговорим.
DONT_RESOLVE_DLL_REFERENCES
Этот флаг указывает системе спроецировать DLL на адресное пространство вызывающего процесса. Проецируя DLL. система обычно вызывает из нее специальную функцию DllMain (о ней — чуть позже) и с ее помощью инициализирует библиотеку. Так вот, данный флаг заставляет систему проецировать DLL, не обращаясь к DllMain.
Кроме того, DLL может импортировать функции из других DLL. При загрузке библиотеки система проверяет, использует ли она другие DLL; если да, то загружает и их. При установке флага DONT_RESOLVE_DLL_REFERENCES дополнительные DLL автоматически не загружаются. Так что вызывать экспортируемые этими DLL функции без риска получить код, использующий неинициализированные внутренние структуры либо еще не загруженную DLL Словом, по возможности лучше избегать использования этого флага. Подробнее см. пост «LoadLibraryEx (DONT_RESOLVE_DLL_REFERENCES) is fundamentally flawed» в блоге Рэймонда Чена (Raymond Chen) по адресу http://blogs.msdn.com/oldnewthing/archive/2005/02/14/372266.aspx.
LOAD_LIBRARY_AS_DATAFILE
Этот флаг очень похож на предыдущий. DLL проецируется на адресное пространство процесса так, будто это файл данных. При этом система не тратит дополнительное время на подготовку к выполнению какого-либо кода из данного файла. Например, когда DLL проецируется на адресное пространство, система считывает информацию из DLL-файла и на ее основе определяяет, какие атрибуты защиты страниц следует присвоить разным частям фай-
Глава 20. DLL - более сложные методы программирования.docx 637
ла. Если флаг LOAD_LIBRARY_AS_DATAFILE не указан, атрибуты защиты устанавливаются такими, чтобы кол из данного файла можно было выполнять. Например, если вызвать функцию GetProcAddress на DLL, загруженной с этим фла-
гом, то GetProcAddress вернет NULL, а GetLastError — ERROR_MOD_NOT_FOUND.
Этот флаг может понадобиться по нескольким причинам. Во-первых, его стоит указать, если DLL содержит только ресурсы и никаких функций. Тогда DLL проецируется па адресное пространство процесса, после чего при вызове функций, загружающих ресурсы, можно использовать значение HMODULE, возвращенное функцией LoadLibraryEx. Во-вторых, он пригодится, если вам нужны ресурсы, содержащиеся в каком-нибудь EXE-файле. Обычно загрузка такого файла приводит к запуску нового процесса, но этого не произойдет, если его загрузить вызовом LoadLibraryEx в адресное пространство вашего процесса. Получив значение HMODULE/HINSTANCE для спроецированного EXE-файла, вы фактически получаете доступ к его ресурсам. Так как в EXE-файле нет DllMain, при вызове LoadLibraryEx для загрузки EXE-файла нужно указать флаг
LOAD_LIBRARY_AS_DATAFILE.
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE
Этот флаг всем похож на LOAD_LIBRARY_AS_DATAFILE, но открывает двоичный файл для монопольного доступа. Ни одно приложение не сможет изменить этот файл, пока ваша программа использует его. Этот флаг более безопасен по сравнению с LOAD_LIBRARY_AS_DATAFILE, поэтому лучше использовать
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE, если только другим программам не требуется возможность записи в ваш файл.
LOAD_LIBRARY_AS_IMAGE_RESOURCE
Этот флаг также похож на LOAD_LIBRARY_AS_DATAFILE, но отличается тем, что при загрузке DLL ее RVA (о RVA см. главу 19) изменяется операционной системой. Это позволяет непосредственно использовать RVA, не преобразуя их с учетом адреса, по которому DLL загружена в память. Это особенно удобно при анализе РЕ-разделов в DLL.
LOAD_WITH.ALTERED.SEARCH.PATH
Этот флаг изменяет алгоритм, используемый LoadLibraryEx при поиске DLLфайла. Обычно поиск осуществляется так, как я рассказывал в главе 19. Однако, если данный флаг установлен, функция ищет файл, в зависимости от значения переданного ей параметра pszDLLPathName, по одному из следующих алгоритмов.
1.Если pszDLLPathName не содержит символа \, поиск идет по алгоритму, описанному в главе 19.
2.Если в pszDLLPathName есть символ \, действия LoadLibraryEx зависят от того, какой путь ей передан: полный или относительный.