45825 (Перехват методов COM интерфейсов), страница 2
Описание файла
Документ из архива "Перехват методов COM интерфейсов", который расположен в категории "". Всё это находится в предмете "информатика" из , которые можно найти в файловом архиве . Не смотря на прямую связь этого архива с , его также можно найти и в других разделах. Архив можно найти в разделе "рефераты, доклады и презентации", в предмете "информатика, программирование" в общих файлах.
Онлайн просмотр документа "45825"
Текст 2 страницы из документа "45825"
На рисунке 2 приведен пример дизассемблированного кода вызова метода COM-интерфейса (ссылка на который находится в pUnk) с передачей двух параметров, arg1 и arg2.
Отключить генерирование стандартного пролога и эпилога можно с помощью директивы _declspec(naked) перед определением функции. Проблема, связанная с нормальным завершением путем вызова ret, решается за счет использования другой инструкции процессора – jmp. Вместо того, чтобы вызывать исходный метод с помощью инструкции call (мы не можем подготовить стек параметров для call, так как не знаем их количество) и затем выполнить “ret n” (нам неизвестно n – количество параметров * 4) – перехватчик определяет адрес исходного метода, заменяет в стеке указатель на объект (который внутри вызова будет рассматриваться как this), к методу которого производится вызов, а затем просто “перепрыгивает” по нужному адресу с помощью jmp. После вызова jmp в стеке не остается ничего, что напоминало бы о перехватчике – настоящая функция получает нетронутый стек параметров и после ее завершения мы попадем в клиентский код, минуя перехватчик. Ниже приведен код перехватчика, реализованный с помощью ATL:
mov eax, [esp+4] // первый параметр в стеке - this cmp dword ptr [eax+8], 0 // проверяем счетчик ссылок QIThunk::m_dwRef jg goodref call atlBadThunkCall goodref: mov eax, [esp+4] // первый параметр в стеке - this mov eax, dword ptr [eax+4] // получаем переменную-член QIThunk::m_pUnk mov [esp+4], eax // заменяем this-перехватчика в стеке на m_pUnk mov eax, dword ptr [eax] // получаем vptr (указатель на vtbl) // n – порядковый номер метода в vtbl mov eax, dword ptr [eax+4*n] // получаем адрес нужного виртуального метода jmp eax // переходим в нужный метод (обратно не вернемся) |
Необходимо отметить, что подобная техника позволяет выполнить предварительную обработку в перехватчике (в случае ATL – проверка счетчика ссылок перед вызовом), но не пост-обработку. После инструкции “jmp eax” мы больше не вернемся в код перехватчика (в стеке лежит адрес возврата в клиентский код, и после ret мы попадем именно туда).
Например, мы могли бы попытаться расширить код перехватчика так, чтобы писать отладочные сообщения, если вызов метода завершился с ошибкой. Чтобы решить эту задачу, нам пришлось бы заменить адрес возврата в стеке на код перехватчика (вместо адреса возврата в клиентский код), но тогда между пред- и пост-обработкой нужно было бы где-то хранить исходный адрес возврата. Стек не подходит в качестве такого хранилища, так как он будет использоваться вызываемым методом. Один из возможных вариантов – использование TLS или динамической памяти, кроме того, доступ к этому хранилищу должен синхронизироваться для многопоточных приложений.
ПРИМЕЧАНИЕ Количество слотов TLS ограничено, а вызовы к перехватчику в одном потоке могут быть вложенными. Поэтому для хранения адресов возврата пришлось бы использовать связанный список или аналогичную структуру данных, а также обеспечить быстрые выделения/освобождения памяти для элементов списка, чтобы уменьшить влияние перехватчика на скорость выполнения приложения. |
Подход, используемый ATL для перехвата вызовов COM-объектов, сводится к следующему:
Указатель на интерфейс заменяется на перехватчик в методе CComObjectRootBase::InternalQueryInterface при вызове QueryInterface. Поэтому перехватываются только вызовы COM-объектов, разработанных с помощью ATL.
vtbl перехватчика создается путем ручного объявления большого количества (1024) виртуальных методов, имеющих одинаковую реализацию.
ПРИМЕЧАНИЕ Такое решение нельзя назвать изящным – исходные тексты QIThunk получаются большими, но, с другой стороны это наиболее эффективный способ генерации vtbl. Альтернативный способ мог бы заключаться в заполнении vtbl во время выполнения приложения: |
HRESULT __stdcall thunk(void* pthis) { return S_OK; } typedef HRESULT (__stdcall * pthunk)(void* pthis); pthunk vtbl[1024]; for(int i = 0 ; i < sizeof(vtbl) / sizeof(vtbl[0]); ++i) vtbl[i] = &thunk; pthunk* vptr = vtbl; IUnknown* pUnk = reinterpret_cast(&vptr); pUnk->AddRef(); |
Метод-перехватчик, используя ассемблер, выполняет пред-обработку (проверку счетчика ссылок) и после этого передает управление исходному методу с помощью инструкции безусловного перехода jmp.
Замена указателей в vtbl
Для отладочных целей в приведенном выше примере нам было бы достаточно перехватывать только вызовы AddRef, Release и QueryInterface. Но для перехвата всех остальных методов интерфейса, сигнатура которых неизвестна на этапе компиляции, требуется более универсальный код.
Альтернативный способ перехвата вызовов методов интерфейса заключается в том, чтобы заменить в исходной vtbl интерфейса указатели на те методы, которые мы собираемся перехватывать. Эта технология была замечательно описана в статье “ Перехват методов интерфейса IUnknown”.
Если нам известны сигнатуры перехватываемых методов (как в случае с методами IUnknown), нам не потребуется универсальный перехватчик, так как вызовы всех остальных методов будут осуществляться напрямую. Такой способ имеет следующие особенности по сравнению с рассмотренным выше:
Перехватываются все вызовы через любой указатель на интерфейс, так как мы меняем исходную vtbl интерфейса.
Vtbl обычно размещается в R/O секции памяти, поэтому код установки перехватчика должен менять настройки защиты этой секции.
Нет необходимости в генерации vtbl нужного размера (мы используем исходную vtbl), в некоторых случаях нет необходимости в универсальном коде перехвата методов с неизвестной сигнатурой.
В многопоточном приложении после установки перехватчика часть вызовов может выполниться напрямую, так как некоторые потоки могли уже успеть получить адрес метода из vtbl, но еще не выполнить вызов call.
Эта технология не подходит, когда в приложении нужно перехватывать вызовы через конкретный указатель на интерфейс, а не через любые указатели на этот интерфейс, и когда нужно контролировать время жизни перехватчика.
ПРИМЕЧАНИЕ Посмотрим, например, что произойдет при выгрузке модуля перехватчика. При этом он должен восстановить исходные адреса методов в vtbl, после чего выгрузиться. В многопоточном приложении один из потоков мог успеть получить адрес метода из vtbl (который все еще указывал на перехватчик), но не успеть сделать вызов по этому адресу. Если модуль перехватчика не будет предпринимать специальных мер по синхронизации, вызов по адресу выгруженного модуля закончится AV (access violation – ошибка доступа к памяти). |
Перехватчик с постобработкой
Вернемся снова к методу перехвата, используемому в ATL. Код перехватчика позволяет с легкостью выполнить подготовку к вызову – предобработку, но затем он выполняет безусловный переход jmp в исходную функцию. Попробуем дополнить его код так, чтобы позволить выполнить постобработку после вызова.
Первая задача, которую необходимо решить – генерация vtbl перехватчика. ATL использует с этой целью макросы ATL_IMPL_THUNK, явно объявляя 1024 метода в теле класса. Рассмотрим альтернативный подход, заключающийся в динамическом создании vtbl нужного вида в runtime.
Код перехватчика должен знать порядковый номер n метода интерфейса, чтобы выполнить его вызов. Мы можем разделить весь код универсального перехватчика на 2 части – первая будет зависеть от порядкового номера перехватываемого метода (n) и будет передавать управление второй, передавая n через стек, а вторая часть будет одинаковой для всех методов.
Код первой части тривиален – мы опускаем в стек n и затем выполняем переход на тело универсального перехватчика. Мы будем использовать технику ATL (которая используется для создания оконных процедур обработки сообщений, смысл этого будет описан ниже) – создадим структуру, содержащую нужные инструкции:
#pragma pack(push, 1) struct vthunk { BYTE m_push; DWORD m_n; BYTE m_jmp; DWORD m_offset; void init(DWORD_PTR proc, int n) { m_push = 0x68; m_n = n; m_jmp = 0xE9; m_offset = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(vthunk))); FlushInstructionCache(GetCurrentProcess(), this, sizeof(vthunk)); } }; #pragma pack(pop) |
Структуру vtbl можно имитировать с помощью массива указателей на vthunk:
struct ThunkVtbl { ThunkVtbl(DWORD_PTR pthunk) { for(int i = 0; i < thunk_n; ++ i) { code[i].init(pthunk, i); vtbl[i] = reinterpret_cast(&code[i]); } } static const int thunk_n = 1024; DWORD_PTR vtbl[thunk_n]; vthunk code[thunk_n]; }; |
В конструкторе ThunkVtbl мы инициализируем каждый из перехватчиков vthunk порядковым номером n и адресом универсального перехватчика pthunk. Теперь массив vtbl содержит 1024 указателя на структуры vthunk, каждая из которых содержит код для вызова перехватчика:
push n jmp pthunk |
Для постобработки нам потребуется хранить адрес возврата в клиентский код. С этой целью мы будем использовать TLS и контейнер std::deque (так как в одном потоке вызовы могут быть вложенными, нам нужен именно стек).
ПРИМЕЧАНИЕ Автор говорит о стеке, но по каким-то причинам использует двунаправленную очередь. С точки зрения функциональности это, в общем, безразлично, но несколько сбивает с толку. – прим. ред. |
Внутри перехватчика указатель на нужный std::deque берется из TLS, но так как поток создается не нами, мы не можем получить уведомление о его завершении. Значит, у нас нет точки в программе, где можно было бы безопасно уничтожить объект std::deque, ассоциированный с конкретным потоком. Во избежание потери ресурсов нужно дополнительно хранить список всех созданных объектов std::deque и уничтожать их перед завершением приложения.
Ниже приведена реализация специального класса-обертки, автоматизирующего выполнение всех этих действий. Список созданных std::deque в этом классе хранится в динамическом массиве (std::vector), добавление элементов в который происходит в конкурентном режиме и требует синхронизации. Для синхронизации доступа к нему используется критическая секция.
template struct TlsStorage { TlsStorage() { m_slot = TlsAlloc(); } ~TlsStorage() { std::vector for( ; it != m_stacks.end(); ++it) delete *it; TlsFree(m_slot); } void push(T t) { std::deque* p = reinterpret_cast if(!p) { p = new std::deque; m_sec.Lock(); m_stacks.push_back(p); m_sec.Unlock(); TlsSetValue(m_slot, p); } p->push_back(t); } T pop() { std::deque* p = reinterpret_cast T t = p->back(); p->pop_back(); return t; } std::vector CComAutoCriticalSection m_sec; DWORD m_slot; }; |
Теперь у нас есть все необходимые составляющие. Класс ItfThunk собирает их вместе:
class ItfThunk { public: ItfThunk(void* p) : m_p(p) { vptr = &vtbl; } void __stdcall preprocess(int n) { std::cout << "method " << n << " preprocess" << std::endl; } HRESULT __stdcall postprocess(int n, HRESULT hr) { std::cout << "method " << n << " postrocess, result " << std::hex << hr << std::endl; return hr; } private: #pragma pack(push,1) struct CallInfo { void* p; int n; HRESULT hr; DWORD_PTR ret_addr; }; #pragma pack(pop) private: static void __cdecl store(int n, DWORD_PTR ret_addr, void* p) { CallInfo i = { p, n, 0, ret_addr }; storage.push(i); } static void __cdecl restore(HRESULT hr, CallInfo* pi) { *pi = storage.pop(); pi->hr = hr; } static void thunk(); private: ThunkVtbl* vptr; void* m_p; static TlsStorage storage; static ThunkVtbl vtbl; }; __declspec(selectany) ThunkVtbl ItfThunk::vtbl(reinterpret_cast(ItfThunk::thunk)); __declspec(selectany) TlsStorage ItfThunk::storage; |
Переменная-член ThunkVtbl* vptr имитирует указатель vptr на таблицу виртуальных функций “обычного” C++-класса, структура CallInfo хранит информацию, необходимую для постобработки вызова. Нам осталось рассмотреть лишь реализацию статического метода void thunk(), выполняющего универсальный перехват. Перед вызовом этого перехватчика в стеке находятся параметры для исходного метода, указатель на this, адрес возврата в клиентский код и n – порядковый номер метода (который положил в стек vthunk):