Существует много способов подключения вызовов функций в программу. Довольно сложный способ заключается в следующем: нужно отыскать все CALL-инструкции вызовов и заменить указанный в них адрес одним из своих собственных. Этот подход сложен и чреват ошибками. К счастью, в случае DeadlockDetection функции, которые надо подключать, являются импортированными и их намного легче обрабатывать, чем CALL-инструкции.
Импортированной называют функцию, которая приходит из DLL. Например, когда программа вызывает функцию outputoebugstring, то она обращается к функции, которая постоянно находится в KERNEL32.DLL. Только начав заниматься программированием для Microsoft Win32, я думал, что вызов импортированной функции ведет себя точно так же, как вызов любой другой функции: инструкция CALL или инструкция ветвления передает управление на нужный адрес и начинает выполнять импортированную функцию. Единственным различием могло бы быть то, что в случае импортированной функции программный загрузчик операционной системы должен был бы пробежаться через выполняемый файл и подправить адреса вызовов так, чтобы учесть адрес загруженной в память DLL. Когда же я посмотрел, как на самом деле организовано обращение к импортированной функции, то был поражен простотой и красотой ее реализации.
Проблемы станут очевидными, если вспомнить, сколько существует API-функций, и как легко можно вызывать одни и те же функции много раз и в любой точке программы. Если бы загрузчик должен был находить и заменять каждое обращение к OutputDebugstring, например, то загрузка программы длилась бы вечно. Даже если бы компоновщик генерировал полную таблицу, указывающую, где каждое обращение к OutputDebugstring имело место в коде, огромное количество циклов и записей в памяти сделали бы время загрузки мучительно медленным.
Как же загрузчик сообщает вашему приложению, где можно найти импортированную функцию? Решение дьявольски остроумно. Подумав, куда направлены обращения к OutputDebugstring, вы скоро поймете, что каждое обращение должно направляться по одному и тому же адресу — адресу загрузки OutputDebugstring в памяти.
Конечно, ваше приложение не может знать этот адрес заранее, поэтому все вызовы OutputDebugstring направляются через единственный косвенный адрес. Когда программный загрузчик загружает выполняемый файл и связанные с ним DLL-файлы в память, загрузчик устанавливает этот косвенный адрес так, чтобы он соответствовал окончательному адресу загрузки OutputDebugstring. Компилятор заставляет эту косвенную адресацию работать, генерируя переход к косвенному адресу каждый раз, когда код вызывает импортированную функцию. Этот косвенный адрес сохраняется в секции импорта (.idata1) выполняемого файла. Если импорт выполняется через объявление _declspec(dllimport), то вместо косвенного перехода код выполняет косвенное обращение, экономя, таким образом, пару инструкций на вызове функции.
Idata — первый символ в этом имени (i) от англ, import (импорт). — Пер.
При подключении импортированной функции выполняются следующие операции: поиск секции импорта выполняемого файла, поиск адреса конкретной функции, которую вы хотите подключить, и затем запись адреса подключаемой функции на свое место (в коде). Хотя поиск и замена адресов функций могут показаться довольно трудоемким занятием, но тут уж ничего не поделать — так организован РЕ2-формат файлов в Win32.
РЕ - Portable Executable. - Пер.
В главе 10 своей превосходной книги "Секреты системного программирования Windows 95" (Matt Pietrek. System Programming Secrets, — IDG Books, 1995) Мэт Пьетрек описывает метод подключения импортированных функций. Код Мэта просто отыскивает секцию импорта модуля и, используя значение, возвращаемое из вызова функции GetProcAddress, организует циклический просмотр списка импортированных функций. Обнаружив нужную функцию, он перезаписывает адрес подключаемой функции на исходный адрес импортированной функции.
После выхода книги Мэта в этой методике произошло два небольших изменения. Во-первых, когда Мэт писал книгу, большинство разработчиков не объединяло секцию импорта с другими РЕ-секциями. Поэтому, если секция импорта находится в памяти, защищенной от записи (с атрибутом доступа "read-only"), то запись адреса подключения вызывает нарушение доступа.
Я решил эту проблему простой переустановкой значения атрибута доступа к виртуальной памяти на "read-write" (перед записью адреса подключаемой функции). Вторая проблема, справиться с которой немного труднее, появляется из-за того, что при работе под Windows 98 иногда не удается подключить импортированные функции.
При использовании DeadlockDetection желательно иметь возможность переадресовать поточные функции во время выполнения приложения, даже когда оно выполняется под отладчиком. Хотя можно предположить, что подключение функций при работе под отладчиком не должно вызывать особых проблем, это не так. При выполнении программы под Windows 2000 или под Windows 98 вне отладчика, когда вы вызываете GetProcAddress, чтобы найти адрес функции, и затем просматриваете секцию импорта, отыскивая этот адрес в ее списке, вы всегда будете находить данный адрес. Если же программа выполняется под отладчиком в Windows 98, то вызов GetProcAddress возвращает другой адрес — не тот, что при выполнении без отладчика. В этом случае GetProcAddress возвращает адрес "отладочного переходника" (debug thunk1) — специальной оболочки вокруг реального вызова.
Thunk — "переходник" (небольшая секция кода, выполняющая преобразование (напр., типов) или обеспечивающая вызов 32-разрядного кода из 16-разрядного и наоборот). Здесь речь идет о специальном отладочном переходнике, который используется при отладке приложения. — Пер.
Как сказано в главе 4, в операционной системе Windows 98 не реализовано "копирование-при-записи". Отладочный переходник, возвращаемый при выполнении под отладчиком, — это то средство, с помощью которого Windows 98 предохраняет отладчики от попытки входа в системные функции, расположенные выше 2 Гбайтной отметки памяти. В целом, отсутствие "копирования-при-записи" — небольшая проблема для большинства разработчиков. Она важна только для тех, кто пишет отладчики или хочет правильно подключать функции независимо от того, выполняются они под отладчиком или нет.
К счастью, получение реального адреса для импортированной функции не слишком сложная задача — требуется только немного больше работы, и нужно избегать вызовов функции GetProcAddress. Структура IMAGE_IMPORT_
DESCRIPTOR РЕ-файла, которая содержит всю информацию о функциях, импортированных из конкретной DLL, имеет указатели на два массива в выполняемом файле. Эти массивы называются таблицами адресов импорта (Import Address Tables — IAT) или, иногда, массивами данных переходников (thunk data arrays). Первый указатель ссылается на реальную IAT, которую программный загрузчик устанавливает, когда загружает выполняемый файл.
Второй указатель ссылается на исходную IAT, которая не затрагивается загрузчиком, и перечисляет импортированные функции. Чтобы найти реальный адрес импортированной функции, нужно просмотреть исходную IAT, пока не будет найдено название функции, которую нужно подключить. Затем следует записать адрес подключения в соответствующий вход реальной IAT, которую использует программа. При выполнении этого дополнительного шага код подключения будет работать всегда — независимо от того, где он вызывается.
Листинг 12-2 показывает функцию HookimportedFunctionsByName, предназначенную для организации подключения импорта. В табл. 12.3 показаны и описаны все параметры этой функции. Желая сделать подключение максимально обобщенным, я побеспокоился о том, чтобы разрешить подключение множества функций, одновременно импортируемых из одной и той же DLL. Как видно из названия этой функции (HookimportedFunctionsByName), она подключает только функции, импортируемые по имени. В главе 14 обсуждается подключение функций, импортируемых по порядковому номеру (которое используется в утилите LIMODS).
Таблица 12.3. Описания параметров функции HookimportedFunctionsByName
|
Параметр
|
Описание
|
|
hModule |
Модуль, в котором будет подключен импорт |
|
szImportMod |
Имя модуля, чьи функции импортируются |
|
Count |
Количество подключаемых функций. Этот параметр указывает размер массивов paHookArray И paOrigFuncs |
|
paHookArray |
Массив структур дескрипторов функций, который перечисляет, какие функции нужно подключать. Массив не должен быть упорядочен по szFunc-именам (хотя разумно хранить массив отсортированным в порядке имен функций, потому что в будущем можно было бы лучше организовать поиск). Кроме того, если конкретный рРгос-указатель равен NULL (пустой), то HookimportedFunctionsByName пропускает этот элемент. Структура каждого элемента в paHookArray проста: имя подключаемой функции и указатель на процедуру нового подключения. Поскольку у вас может появиться желание подключать или не подключать функции, HookimportedFunctionsByName возвращает все исходные адреса импортируемых функций |
|
paOrigFuncs |
Массив исходных адресов, подключаемых с помощью HookimportedFunctionsByName. Если функция не была подключена, то индекс соответствующего элемента будет иметь значение NULL |
|
pdwHooked |
Возвращает число подключенных функций (в массиве paHookArray) |