Издание четвертое windows ® для профессионалов создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows


Явное подключение экспортируемого идентификатора



Pdf просмотр
страница50/68
Дата28.11.2016
Размер3.57 Mb.
Просмотров12455
Скачиваний0
1   ...   46   47   48   49   50   51   52   53   ...   68
Явное подключение экспортируемого идентификатора
Поток получает адрес экспортируемого идентификатора из явно загруженной вызовом
GetProcAddress:
FARPROC GetProcAddress(
HINSTANCE hinstDll,
PCSTR Параметр
hinstDll — описатель, возвращенный LoadLibrary(Ex) или GetModule
Handle и относящийся к DLL, которая содержит нужный идентификатор. Параметр
pszSymbolName разрешается указывать в двух формах. Во первых, как адрес строки с нулевым символом в конце, содержащей имя интересующей Вас функции pfn = GetProcAddress(hinstDll, "Заметьте тип параметра
pszSymbolName — PCSTR, а не PCTSTR. Это значит, что функция
GetProcAddress принимает только ANSI строки — ей нельзя передать Unicode строку. А причина в том, что идентификаторы функций и переменных в разделе экспорта всегда хранятся как ANSI строки.
Вторая форма параметра
pszSymbolName позволяет указывать порядковый номер нужной функции pfn = GetProcAddress(hinstDll, Здесь подразумевается, что Вам известен порядковый номер (2) искомого идентификатора, присвоенный ему автором данной DLL. И вновь повторю, что Microsoft

497
Г ЛАВА более сложные методы программирования настоятельно не рекомендует пользоваться порядковыми номерами поэтому Вы редко встретите второй вариант вызова
GetProcAddress.
При любом способе Вы получаете адрес содержащегося в DLL идентификатора.
Если идентификатор не найден,
GetProcAddress возвращает Учтите, что первый способ медленнее, так как системе приходится проводить поиски сравнение строк. При втором способе, если Вы передаете порядковый номер,
не присвоенный ни одной из экспортируемых функций,
GetProcAddress может вернуть значение, отличное от NULL. В итоге Ваша программа, ничего не подозревая, получит неправильный адрес. Попытка вызова функции поэтому адресу почти наверняка приведет к нарушению доступа. Я и сам — когда только начинал программировать подине очень четко понимал эти вещи — несколько раз попадал в эту ловушку.
Так что будьте внимательны. (Вот Вам, кстати, и еще одна причина, почему от использования порядковых номеров следует отказаться в пользу символьных имен — иден тификаторов.)
Функция входа/выхода
В DLL может быть лишь одна функция входа/выхода. Система вызывает ее в некоторых ситуациях (о чем речь еще впереди) сугубо в информационных целях, и обычно она используется DLL для инициализации и очистки ресурсов в конкретных процессах или потоках. Если Вашей DLL подобные уведомления ненужны, Вы не обязаны реализовывать эту функцию. Пример — DLL, содержащая только ресурсы. Но если же уведомления необходимы, функция должна выглядеть так WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad) {
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// DLL проецируется на адресное пространство процесса break;
case DLL_THREAD_ATTACH:
// создается поток break;
case DLL_THREAD_DETACH:
// поток корректно завершается break;
case DLL_PROCESS_DETACH:
// DLL отключается от адресного пространства процесса break;
}
return(TRUE); // используется только для При вызове
DllMain надо учитывать регистр букв. Многие случайно вызывают
DLLMain, и это вполне объяснимо термин DLL обычно пишется заглавными буквами. Если Вы назовете функцию входа/выхода не
DllMain, а как то иначе
(пусть даже только один символ будет набран в другом регистре, компиляция и компоновка Вашего кода пройдет без проблемно система проигнорирует такую функцию входа/выхода, и Ваша DLL никогда не будет инициализирована.

498
Ч АС Т Ь I V
ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ
Параметр
hinstDll содержит описатель экземпляра DLL. Как и hinstExe функции
(w)WinMain, это значение — виртуальный адрес проекции файла DLL на адресное пространство процесса. Обычно последнее значение сохраняется в глобальной переменной, чтобы его можно было использовать и при вызовах функций, загружающих ресурсы (типа
DialogBox или LoadString). Последний параметр, fImpLoad, отличен от 0, если DLL загружена неявно, и равен 0, если она загружена явно.
Параметр
fdwReason сообщает о причине, по которой система вызвала эту функцию. Он принимает одно из четырех значений DLL_PROCESS_ATTACH, DLL_PRO
CESS_DETACH, DLL_THREAD_ATTACH или DLL_THREAD_DETACH. Мы рассмотрим их в следующих разделах.
Не забывайте, что DLL инициализируют себя, используя функции
DllMain.
К моменту выполнения Вашей
DllMain другие DLL в том же адресном пространстве могут не успеть выполнить свои функции
DllMain, те. они окажутся не инициализированными. Поэтому Вы должны избегать обращений из
DllMain
к функциям, импортируемым из других DLL. Кроме того, не вызывайте из
DllMain функции LoadLibrary(Ex) итак как это может привести к взаимной блокировке.
В документации Platform SDK утверждается, что
DllMain должна выполнять лишь простые виды инициализации — настройку локальной памяти потока
(см. главу 21), создание объектов ядра, открытие файлов и т. д. Избегайте обращений к функциям, связанным си сокетами (а также к функциям, которые их вызывают, потому что соответствующие могут быть еще не инициализированы. Кроме того, подобные функции могут вызывать
LoadLibrary(Ex) и тем самым приводить к взаимной блокировке.
Аналогичные проблемы возможны и при создании глобальных или статических+ объектов, поскольку их конструктор или деструктор вызывается в тоже время, что и Ваша
DllMain.
Уведомление DLL_PROCESS_ATTACH
Система вызывает
DllMain с этим значением параметра fdwReason сразу после того,
как DLL спроецирована на адресное пространство процесса. А это происходит, только когда образ DLL файла проецируется в первый раз. Если затем поток вызовет
LoadLibrary(Ex) для уже спроецированной DLL, система просто увеличит счетчик числа пользователей этой DLL; так что
DllMain вызывается со значением DLL_PROCESS_AT
TACH лишь раз.
Обрабатывая DLL_PROCESS_ATTACH, библиотека должна выполнить в процессе инициализацию, необходимую ее функциям. Например, в DLL могут быть функции,
которым нужна своя куча (создаваемая в адресном пространстве процесса. В этом случае
DllMain могла бы создать такую кучу, вызвав HeapCreate при обработке уведомления, а описатель созданной кучи сохранить в глобальной переменной, доступной функциям При обработке уведомления DLL_PROCESS_ATTACH значение, возвращаемое функцией, указывает, корректно ли прошла инициализация DLL. Например, если вызов
HeapCreate закончился благополучно, следует вернуть TRUE. А если кучу создать не удалось — FALSE. Для любых других значений
fdwReason — DLL_PROCESS_DETACH,
DLL_THREAD_ATTACH или DLL_THREAD_DETACH — значение, возвращаемое
DllMain,
системой игнорируется.

499
Г ЛАВА более сложные методы программирования
Конечно, где тов системе должен быть поток, отвечающий за выполнение кода
DllMain. При создании нового процесса система выделяет для него адресное пространство, куда проецируется EXE файл и все необходимые ему DLL модули. Далее создается первичный поток процесса, используемый системой для вызова
DllMain из каждой со значением DLL_PROCESS_ATTACH. Когда все спроецированные DLL ответят на это уведомление, система заставит первичный поток процесса выполнить стартовый код из библиотеки C/C++, а потом — входную функцию EXE файла (
main,
wmain, WinMain или wWinMain). Если DllMain хотя бы одной из DLL вернет FALSE, сообщая об ошибке при инициализации, система завершит процесс, удалив из его адресного пространства образы всех файлов после этого пользователь увидит окно с сообщением о том, что процесс запустить не удалось. Ниже показаны соответствующие окна для Windows 2000 и Windows Теперь посмотрим, что происходит при явной загрузке DLL. Когда поток вызывает, система отыскивает указанную DLL и проецирует ее на адресное пространство процесса. Затем вызывает
DllMain со значением используя поток, вызвавший
LoadLibrary(Ex). Как только DllMain обработает уведомление, произойдет возврат из
LoadLibrary(Ex), и поток продолжит работу в обычном режиме. Если же
DllMain вернет FALSE (неудачная инициализация, система автоматически отключит образ файла DLL от адресного пространства процесса, а вызов
LoadLibrary(Ex) даст NULL.
Уведомление DLL_PROCESS_DETACH
При отключении DLL от адресного пространства процесса вызывается ее функция
DllMain со значением DLL_PROCESS_DETACH в параметре fdwReason. Обрабатывая это значение, DLL должна провести очистку в данном процессе. Например, вызвать
Heap
Destroy, чтобы разрушить кучу, созданную ею при обработке уведомления DLL_PRO
CESS_ATTACH. Обратите внимание если функция
DllMain вернула FALSE, получив уведомление, то ее нельзя вызывать с уведомлением DLL_PRO
CESS_DETACH. Если DLL отключается из за завершения процесса, то за выполнение кода
DllMain отвечает поток, вызвавший ExitProcess (обычно это первичный поток приложения. Когда Ваша входная функция возвращает управление стартовому коду из библиотеки C/C++, тот явно вызывает
ExitProcess и завершает процесс.
Если DLL отключается в результате вызова
FreeLibrary или FreeLibraryAndExitThread,
код
DllMain выполняется потоком, вызвавшим одну из этих функций. В случае обращения к
FreeLibrary управление не возвращается, пока DllMain не закончит обработку уведомления DLL_PROCESS_DETACH.

500
Ч АС Т Ь I V
ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ
Учтите также, что DLL может помешать завершению процесса, если, например, ее
DllMain входит в бесконечный цикл, получив уведомление Операционная система уничтожает процесс только после того, как все DLL модули обработают уведомление Счетчик числа пользователей DLL уменьшается на 1, и DLL отключается от адресного пространства процесса
Счетчик числа пользователей DLL увеличивается на Возвращается hinstDll
(адрес загрузки) библиотеки
Возвращается
NULL
DLL проецируется на адресное пространство процесса
ДА
ДА
ДА
ДА
Нашла ли система указанный
DLL-файл?
Спроецирована ли DLL на адресное пространство процесса?
Поток вызывает
LoadLibrary
Равен ли счетчик Вызывается функция
DllMain библиотеки со значением
DLL_PROCESS_ATTACH
НЕТ
Вернула ли
DllMain TRUE?
НЕТ
НЕТ
НЕТ
Рис. 20-2.
Операции, выполняемые системой при вызове потоком функции LoadLibrary

501
Г ЛАВА более сложные методы программирования
Если процесс завершается в результате вызова
TerminateProcess, система не
вызывает
DllMain со значением DLL_PROCESS_DETACH. А значит, ни одна DLL,
спроецированная на адресное пространство процесса, не получит шанса на очистку до завершения процесса. Последствия могут быть плачевны — вплоть до потери данных. Вызывайте
TerminateProcess только в самом крайнем случае!
На рис. 20 2 показаны операции, выполняемые при вызове
LoadLibrary, а на рис. 20 3 — при вызове
FreeLibrary.
Возвращается Возвращается
TRUE
НЕТ
ДА
ДА
Поток вызывает
FreeLibrary
Вызывается функция
DllMain библиотеки со значением
DLL_PROCESS_DETACH
DLL отключается от адресного пространства процесса
Равен ли счетчик Счетчик числа пользователей DLL уменьшается на Допустимо ли значение параметра
hinstDll?
НЕТ
Рис. 20-3.
Операции, выполняемые системой при вызове потоком функции FreeLibrary
Уведомление DLL_THREAD_ATTACH
Когда в процессе создается новый поток, система просматривает все DLL, спроеци рованные в данный момент на адресное пространство этого процесса, ив каждой из таких DLL вызывает
DllMain со значением DLL_THREAD_ATTACH. Тем самым она уведомляет модули о необходимости инициализации, связанной сданным потоком.
Только что созданный поток отвечает за выполнение кода в функциях
DllMain всех

502
Ч АС Т Ь I V
ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ. Работа его собственной (стартовой) функции начинается лишь после того, как все DLL модули обработают уведомление Если в момент проецирования DLL на адресное пространство процесса в нем выполняется несколько потоков, система
не вызывает DllMain со значением DLL_
THREAD_ATTACH ни для одного из существующих потоков. Вызов
DllMain с этим значением осуществляется, только если DLL проецируется на адресное пространство процесса в момент создания потока.
Обратите также внимание, что система не вызывает функции
DllMain со значением и для первичного потока процесса. Любая DLL, проецируемая на адресное пространство процесса в момент его создания, получает уведомление, а не DLL_THREAD_ATTACH.
Уведомление DLL_THREAD_DETACH
Лучший способ завершить поток — дождаться возврата из его стартовой функции,
после чего система вызовет
ExitThread и закроет поток. Эта функция лишь сообщает системе о том, что поток хочет завершиться, но система не уничтожает его немедленно. Сначала она просматривает все проекции DLL, находящиеся в данный момент в адресном пространстве процесса, и заставляет завершаемый поток вызвать
DllMain
в каждой из этих DLL со значением DLL_THREAD_DETACH. Тем самым она уведомляет модули о необходимости очистки, связанной сданным потоком. Например, версия библиотеки C/C++ освобождает блок данных, используемый для управления многопоточными приложениями.
Заметьте, что DLL может не дать потоку завершиться. Например, такое возможно,
когда функция
DllMain, получив уведомление DLL_THREAD_DETACH, входит в бесконечный цикл. А операционная система закрывает поток только после того, как все заканчивают обработку этого уведомления.
Если поток завершается из за того, что другой поток вызвал для него
Terminate
Thread, система не вызывает DllMain со значением DLL_THREAD_DETACH. Следовательно, ни одна DLL, спроецированная на адресное пространство процесса, не получит шанса на выполнение очистки до завершения потока, что может привести к потере данных. Поэтому
TerminateThread, как и TerminateProcess,
можно использовать лишь в самом крайнем случае!
Если при отключении DLL еще выполняются какие то потоки, то для них
DllMain
не вызывается со значением DLL_THREAD_DETACH. Вы можете проверить это при обработке DLL_PROCESS_DETACH и провести необходимую очистку.
Ввиду упомянутых выше правил не исключена такая ситуация поток вызывает
LoadLibrary для загрузки DLL, в результате чего система вызывает из этой библиотеки
DllMain со значением DLL_PROCESS_ATTACH. (В этом случае уведомление DLL_
THREAD_ATTACH не посылается) Затем поток, загрузивший DLL, завершается, что приводит к новому вызову
DllMain — на этот раз со значением Библиотека уведомляется о завершении потока, хотя она не получала DLL_
THREAD_ATTACH, уведомляющего о его подключении. Поэтому будьте крайне осторожны при выполнении любой очистки, связанной с конкретным потоком. К счастью, большинство программ пишется так, что
LoadLibrary и FreeLibrary вызываются одним потоком.

503
Г ЛАВА более сложные методы программирования
Как система упорядочивает вызовы DllMain
Система упорядочивает вызовы функции
DllMain. Чтобы понять, что я имею в виду,
рассмотрим следующий сценарий. Процесс А имеет два потока Аи В. На его адресное пространство проецируется DLL модуль SomeDLL.dll. Оба потока собираются вызвать
CreateThread, чтобы создать еще два потока Си Когда поток А вызывает для создания потока С функцию
CreateThread, система обращается к
DllMain из SomeDLL.dll со значением DLL_THREAD_ATTACH. Пока поток С
исполняет код
DllMain, поток В вызывает CreateThread для создания потока D. Системе нужно вновь обратиться к
DllMain со значением DLL_THREAD_ATTACH, и на этот раз код функции должен выполнять поток D. Но система упорядочивает вызовы
DllMain, и поэтому приостановит выполнение потока D, пока поток Сне завершит обработку кода
DllMain и не выйдет из этой функции.
Закончив выполнение
DllMain, поток Сможет начать выполнение своей функции потока. Теперь система возобновляет потоки позволяет ему выполнить код
DllMain,
при возврате из которой он начнет обработку собственной функции потока.
Обычно никто и не задумывается над тем, что вызовы
DllMain упорядочиваются.
Но я завел об этом разговор потому, что один мой коллега как то раз написал код, в котором была ошибка, связанная именно с упорядочиванием вызовов
DllMain. Его код выглядел примерно так WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad) {
HANDLE hThread;
DWORD dwThreadId;
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// DLL проецируется на адресное пространство процесса создаем поток для выполнения какой то работы hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId);
// задерживаем наш поток до завершения нового потока, INFINITE);
// доступ к новому потоку больше ненужен создается еще один поток break;
case DLL_THREAD_DETACH:
// поток завершается корректно break;
case DLL_PROCESS_DETACH:
// DLL выгружается из адресного пространства процесса break;
}
return(TRUE);
}

504
Ч АС Т Ь I V
ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ
Нашли жучка Мыто его искали несколько часов. Когда
DllMain получает уведомление, создается новый поток. Системе нужно вновь вызвать эту же
DllMain со значением DLL_THREAD_ATTACH. Но выполнение нового потока приостанавливается — ведь поток, из за которого в
DllMain было отправлено уведомление, свою работу еще не закончил. Проблема кроется в вызове. Она приостанавливает выполнение текущего потока до тех пор, пока не завершится новый. Однако у нового потока нет ни единого шанса не только на завершение, но и на выполнение хоть какого нибудь кода — он приостановлен в ожидании того, когда текущий поток выйдет из
DllMain. Вот Вами взаимная блокировка — выполнение обоих потоков задержано навеки!
Впервые начав размышлять над этой проблемой, я обнаружил функцию
Disable
ThreadLibraryCalls:
BOOL DisableThreadLibraryCalls(HINSTANCE Вызывая ее, Вы сообщаете системе, что уведомления DLL_THREAD_ATTACH и DLL_
THREAD_DETACH не должны посылаться
DllMain той библиотеки, которая указана в вызове. Мне показалось логичным, что взаимной блокировки не будет, если система не станет посылать DLL уведомления. Но, проверив свое решение (см. ниже, я убедился, что это невыход проецируется на адресное пространство процесса предотвращаем вызов DllMain при создании или завершении потока создаем поток для выполнения какой то работы hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId);
// задерживаем наш поток до завершения нового потока, INFINITE);
// доступ к новому потоку больше ненужен создается еще один поток break;
case DLL_THREAD_DETACH:
// поток завершается корректно break;
case DLL_PROCESS_DETACH:
// DLL выгружается из адресного пространства процесса

505
Г ЛАВА более сложные методы программирования Потом я понял, в чем дело. Создавая процесс, система создает и объект мьютекс.
У каждого процесса свой объект мьютекс — он не разделяется между несколькими процессами. Его назначение — синхронизация всех потоков процесса при вызове ими функций
DllMain из DLL, спроецированных на адресное пространство данного про цесса.
Когда вызывается
CreateThread, система создает сначала объект ядра потоки стек потока, затем обращается к
WaitForSingleObject, передавая ей описатель объекта мью текса данного процесса. Как только поток захватит этот мьютекс, система заставит его вызвать
DllMain из каждой DLL со значением DLL_THREAD_ATTACH. И лишь тогда система вызовет
ReleaseMutex, чтобы освободить объект мьютекс. Вот из за того, что система работает именно так, дополнительный вызови не предотвращает взаимной блокировки потоков. Единственное, что я смог придумать, переделать эту часть исходного кода так, чтобы ни одна
DllMain не вызывала Wait
ForSingleObject.
Функция DllMain и библиотека C/C++
Рассматривая функцию
DllMain в предыдущих разделах, я подразумевал, что для сборки Вы используете компилятор Microsoft Visual C++. Весьма вероятно, что при написании DLL Вам понадобится поддержка со стороны стартового кода из библиотеки. Например, весть глобальная переменная — экземпляр какого то С++
класса. Прежде чем DLL сможет безопасно ее использовать, для переменной нужно вызвать ее конструктора это работа стартового кода.
При сборке DLL компоновщик встраивает в конечный файл адрес DLL функции входа/выхода. Вы задаете этот адрес компоновщику ключом /ENTRY. Если у Вас компоновщики Вы указали ключ /DLL, то по умолчанию он считает, что функция входа/выхода называется
_DllMainCRTStartup. Эта функция содержится в библиотеке+ и при компоновке статически подключается к Вашей DLL — даже если Вы используете DLL версию библиотеки Когда DLL проецируется на адресное пространство процесса, система на самом деле вызывает именно
_DllMainCRTStartup, а не Вашу функцию DllMain. Получив уведомление, функция
_DllMainCRTStartup инициализирует библиотеку+ и конструирует все глобальные и статические С+ объекты. Закончив,
_
DllMainCRTStartup вызывает Вашу DllMain.
Как только DLL получает уведомление DLL_PROCESS_DETACH, система вновь вызывает, которая теперь обращается к Вашей функции DllMain, и,
когда та вернет управление,
_DllMainCRTStartup вызовет деструкторы для всех глобальных и статических С+ объектов. Получив уведомление DLL_THREAD_ATTACH, функция не делает ничего особенного. Нов случае уведомления, она освобождает в потоке блок памяти
tiddata, если он к тому времени еще не удален. Обычно в корректно написанной функции потока этот блок отсутствует, потому что она возвращает управление
в _threadstartex из библиотеки+ (см. главу 6). Функция
_threadstartex сама вызывает _endthreadex, которая освобождает блок
tiddata до того, как поток обращается к ExitThread.
Но представьте, что приложение, написанное на Паскале, вызывает функции из, написанной на C/C++. В этом случае оно создаст поток, не прибегая к
_begin

506
Ч АС Т Ь I V
ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ
threadex, и такой поток никогда не узнает о библиотеке C/C++. Далее поток вызовет функцию из DLL, которая в свою очередь обратится к библиотечной C функции. Как
Вы помните, подобные функции налету создают блоки сопоставляют его с вызывающим потоком. Получается, что приложение, написанное на Паскале, может создавать потоки, способные без проблем обращаться к функциям из библиотеки Когда его функция потока возвращает управление, вызывается
ExitThread, а библиотека+ получает уведомление DLL_THREAD_DETACH и освобождает блок памяти
tiddata, так что никакой утечки памяти не происходит. Здорово придумано, да?
Я уже говорил, что реализовать в коде Вашей DLL функцию
DllMain необязательно. Если у Вас нет этой функции, библиотека C/C++ использует свою реализацию
DllMain, которая выглядит примерно так (если Вы связываете DLL со статической библиотекой C/C++):
BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad) {
if (fdwReason == При сборке DLL компоновщик, не найдя в Ваших OBJ файлах функцию
DllMain,
подключит
DllMain из библиотеки C/C++. Если Вы не предоставили свою версию функции, библиотека C/C++ вполне справедливо будет считать, что Вас не интересуют уведомления DLL_THREAD_ATTACH и DLL_THREAD_DETACH. Функция
Disable
ThreadLibraryCalls вызывается для ускорения создания и разрушения потоков.



Поделитесь с Вашими друзьями:
1   ...   46   47   48   49   50   51   52   53   ...   68


База данных защищена авторским правом ©nethash.ru 2019
обратиться к администрации

войти | регистрация
    Главная страница


загрузить материал