Отладка приложений

       

Подключение импортированных функций


Существует много способов подключения вызовов функций в программу. Довольно сложный способ заключается в следующем: нужно отыскать все 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)






ЛИСТИНГ 12-2. Функция HookImportedFunctionsByNaine из файла 

                                 HOOKIMPORTEDFUNCTIONSBYNAME.CPP . 

BOOL BUGSUTIL_DLLINTERFACE _stdcall

HooklmportedFunctionsByName ( HMODULE hModule ,

LPCSTR szImportMod,

 UINT uiCount 

LPHOOKFUNCDESCA paHookArray, 

PROC * paOrigFuncs, 

LPDWORD pdwHooked )

 {

// Проверить параметры.

ASSERT ( FALSE == IsBadReadPtr ( hModule

sizeof ( IMAGE_DOS_HEADER) ));

ASSERT ( FALSE == IsBadStringPtr ( szImportMod, MAX_PATH)); 

ASSERT ( 0 != uiCount); 

ASSERT ( NULL != paHookArray);

 ASSERT ( FALSE == IsBadReadPtr ( paHookArray,

sizeof (HOOKFUNCDESC) * uiCount));

// В отладочных построениях выполнить тщательную проверку paHookArray.

 #ifdef _DEBUG

if ( NULL != paOrigFuncs)

 {

ASSERT ( FALSE == IsBadWritePtr ( paOrigFuncs,

sizeof ( PROC) * uiCount)); 

}

if ( NULL != pdwHooked)

 {

ASSERT ( FALSE == IsBadWritePtr ( pdwHooked, sizeof ( UINT)));

}

// Проверить каждый элемент массива подключения.

 {

for ( UINT i = 0; i < uiCount; i++) 

{

ASSERT ( NULL != paHookArray[ i ].szFunc );

ASSERT ( '\0' != *paHookArray[ i ].szFunc);

// If the function address isn't NULL, it is validated.

if ( NULL != paHookArray[ i ].pProc)

{

ASSERT ( FALSE == IsBadCodePtr ( paHookArray[i].pProc)); 

}

 } 



#endif

// Выполнить проверку ошибок параметров.  

if ( ( 0 == uiCount ) | | 

( NULL == szIinportMod ) | | 

( TRUE == IsBadReadPtr ( paHookArray,

sizeof ( HOOKFUNCDESC) * uiCount))) 

{

SetLastErrorEx ( ERROR_INVALID_PARAMETER, SLE_ERROR);

return ( FALSE);

 }

if ( ( NULL != paOrigFuncs) && 

( TRUE == IsBadWritePtr ( paOrigFuncs,

sizeof ( PROC) * uiCount)) ) 

{

SetLastErrorEx ( ERROR_INVALID_PARAMETER, SLE_ERROR);



return ( FALSE); 

}

 if ( ( NULL != pdwHooked) &&

( TRUE == IsBadWritePtr ( pdwHooked, sizeof ( UINT))) )

 {

SetLastErrorEx ( ERROR_INVALID_PARAMETER, SLE_ERROR);

return ( FALSE); 

}

// Это системная DLL, которую Windows 98 не

// разрешает загружать (из-за того, что адрес загрузки >2 Гбайт)? 

if ( ( FALSE == IsNT .()) && ( (DWORD)hModule >= 0x80000000)) 

{

SetLastErrorEx ( ERROR_INVALID_HANDLE, SLE_ERROR);

return ( FALSE); 

}

// Должен ли каждьй элемент массива подключений проверяться .

 //в выпускных построениях?

 if ( NULL != paOrigFuncs) 



// Установить все значения paOrigFuncs в NULL.

memset ( paOrigFuncs, NULL, sizeof ( PROC) * uiCount);

 }

if ( NULL != pdwHooked) 

{

// Установить число функций, подключенных к 0.

*pdwHooked = 0; 

}

// Получить специальный дескриптор импорта.

PIMAGE_IMPORT_DESCRIPTOR plmportDesc =

GetNamedlmportDescriptor ( hModule, szImportMod); 

if ( NULL == plmportDesc)

{

// Затребованный модуль не был импортирован.

 // Ошибку не возвращать.

 return ( TRUE); 

}

// Получить информацию об исходных переходниках для этого DLL. 

// Невозможно использовать информацию переходников, хранящуюся в 

// p!mportDesc->FirstThunk, т. к. загрузчик уже изменил этот массив

 // при установке всех импортов. Исходный переходник обеспечивает

 // доступ к именам функций.

 PIMAGE_THUNK_DATA pOrigThunk =

MakePtr ( PIMAGE_THUNK_DATA

hModule , 

plmportDesc-XDriginalFirstThunk );

// Получить массив p!mportDesc->FirstThunk, в котором я буду

// выполнять подключения и всю черную работу.

 PIMAGE_THUNK_DATA pRealThunk = MakePtr { PIMAGE_THUNK_DATA

hModule , pImportDesc->FirstThunk ); 

// Цикл поиска подключаемых функций,

 while ( NULL != pOrigThunk->ul.Function) 

{

// Искать только функции, которые импортируются по имени, 

// а не те, что импортируются по порядковому номеру.



 if ( IMAGE_ORDINAL_FLAG !=

( pOrigThunk->ul.Ordinal & IMAGE_ORDINAL_FLAG)) 

{

// Поиск имени данной импортируемой функции.

PIMAGE_IMPORT_BY_NAME pByName;

pByName = MakePtr ( PIMAGE_IMPORT_BY_NAME,

hModule ,

 pOrigThunk->ul.AddressOfData );

// Если имя начинается с NULL, то пропустить элемент,

 if ( '\0' == pByName->Name[ 0 ]) 

{

continue; 

}

// Определить, подключилась ли функция

 BOOL bDoHook = FALSE;

// Здесь можно рассмотреть возможность двоичного поиска. 

// Посмотреть, есть ли имя импортируемой функции в массиве

 // подключения. Обдумайте требования к paHookArray для 

// сортировки по именам функций, что позволило бы использо-

// вать двоичный поиск, который увеличил бы скорость поиска.

 // Однако размер параметра uiCount, получаемого этой

// функцией, должен быть скорее небольшим для успешного

 // поиска по всему массиву paHookArray для каждой функции,

 // импортированной модулем szImportMod.

 for ( DINT i = 0; i < uiCount; i++) 

{

if ( ( paHookArray[i].szFunc[0] ==

pByName->Name [0]) & & 

( 0 == strcmpi ( paHookArray[i].szFunc,

(char*)pByName->Name ) ) )

 {

// Если адрес функции есть NULL, то — немедленный

 // выход; иначе приступить к подключению функции.

 if ( NULL != paHookArray[ i ].pProc) 

{

bDoHook = TRUE; 

}

break; 

}

 }

if ( TRUE == bDoHook}

 {

// Функция для подключения найдена. Теперь нужно

 // изменить защиту памяти на "read-write" (для записи), 

// прежде чем перезаписывать указатели функций. Заметьте,

 // что в реальную область переходников запись 

//не производится!

MEMORY_BASIC_INFORMATION mbi_thunk;

 VirtualQuery ( pRealThunk , 

&mbi_thunk  ,

 sizeof ( MEMORY_BASIC_INFORMATION));

if ( FALSE — VirtualProtect ( mbi_thunk.BaseAddress,

mbi_thunk.RegionSize , 

PAGE_READWRITE , 



&mbi_thunk.Protect )) 

{

ASSERT ( !"VirtualProtect failed!");

 SetLastErrorEx ( ERROR_INVALID_HANDLE, SLE_ERROR);

 return ( FALSE); 

}

// Сохранить исходный адрес, если потребуется.

 if ( NULL != paOrigFuncs)

 {

paOrigFuncs[i] = (PROC)pRealThunk->ul.Function; 

}

// Microsoft имеет два различных определения

 // РIМАСЕ_ТНЦМК_ОАТА-полей для будущей поддержки Win64.

 // Используем самый последний набор заголовков

 // из W2K RC2 Platform SDK, и заставим иметь с ними дело

 // заголовки из Visual C++ 6 Service Pack 3. 

// Подключить функцию.

DWORD * pTemp = (DWORD*)&pRealThunk->ul.Function;

 *pTemp = (DWORD)(paHookArray[i].pProc); 

DWORD dwOldProtect;

// Изменить защиту обратно к тому состоянию, которое 

// было перед переписыванием указателя функции. 

VERIFY ( VirtualProtect ( mbi_thunk.BaseAddress,

mbi_thunk.RegionSize , mbi_thunk.Protect , SdwOldProtect )) ;

 if ( NULL != pdwHooked)

 {

// Инкремент общего количества подключенных функций.

 *pdwHooked += 1; 

}

 }

}

// Инкремент обеих таблиц. pOrigThunk++; pRealThunk++; 

}

// Все OK, Jumpmaster! 

SetLastError ( ERROR_SUCCESS);

 return ( TRUE); 

}

Уточнить принципы работы HooklmportedFunctionsByName не так  уж трудно. После выполнения обычной для практики профилактической отладки полной проверки (макросами утверждений) каждого параметра вызывается вспомогательная функция GetNamedimportDescriptor, чтобы найти структуру IMAGE_IMPORT_DESCRIPTOR для требуемого модуля. После получения указателей на исходную и реальную IAT-таблицы выполняется циклический просмотр исходной IAT-таблицы, проверяется каждая импортированная по имени функция, присутствует ли она в списке подключения paHookArray. Если функция находится в этом списке, то устанавливается атрибут доступа PAGE_READWRITE для памяти реальной  IAT-таблицы, чтобы благополучно записать в нее адрес подключения, затем этот адрес заносится в ячейку указателя соответствующей реальной функции и защита памяти восстанавливается в ее исходное состояние.Если вы не совсем разобрались в том, что происходит, воспользуйтесь функцией блочного тестирования для пошагового выполнения HookimportedFunctionsByName (эта функция включена в исходный код BUGSLAYERUTIL.DLL на сопровождающем компакт-диске).

Теперь, рассмотрев основные идеи подключения импортированных функций, перейдем к реализации остальной части DeadlockDetection.



Содержание раздела