Xreferat.com » Рефераты по информатике и программированию » Перехват методов COM интерфейсов

Перехват методов COM интерфейсов

Ivan Andreyev

Введение

В одной из статей RSDN Magazine описывался способ перехвата методов интерфейса IUnknown. Суть этого подхода заключалась в замене указателей на функции QueryInterace, AddRef, Release в VTBL интерфейса и выполнении дополнительной обработки внутри перехватчиков.

В этой статье мы продолжим обсуждение темы перехвата вызовов методов COM-интерфейсов и познакомимся с API-функциями CoGetInterceptor, CoGetInterceptorFromTypeInfo, позволяющими забыть обо всех технических трудностях и проблемах, связанных с передачей вызова от клиента перехватчику, и от перехватчика – исходному компоненту.

Технология “перехвата” вызовов API функций, обработчиков оконных сообщений, методов COM-компонентов имеет много общего с шаблоном проектирования Proxy (Заместитель). Суть этой технологии заключается в том, что вызов клиента перенаправляется (с помощью различных технических ухищрений – замена VTBL, Proxy-объект и т.п.) сначала коду заместителя, который выполняет пред- и постобработку, а затем уже – исходному объекту. Благодаря этому можно добавлять новую функциональность, никак не изменяя ни код клиента, ни код сервера.

Очень широкое распространение технология “перехвата” получила в COM – фундаментальные принципы прозрачности местонахождения компонента (location transparency) и прозрачности типа синхронизации (concurrency transparency) реализуются именно благодаря Proxy-компонентам из инфраструктуры COM, которые имитируют для клиента исходный компонент. С появлением COM+ набор сервисов, которые реализуют перехватчики, расширился еще больше – добавились поддержка транзакций, блокировок для синхронизации доступа к компонентам, поддержка just-in-time активации, ролевая безопасность. За счет того, что эти сервисы реализуются инфраструктурой COM+ прозрачно для клиента и серверных компонентов (хотя серверные COM+-компоненты могут взаимодействовать с инфраструктурой, например, чтобы отменить или подтвердить транзакцию), клиентский код ничего не знает о том, что случится с его вызовом на сервере – будет ли он обслуживаться COM+ или обычным COM-компонентом. Аналогично, один и тот же компонент может использоваться в составе COM+-приложения.

Помимо предоставления различных сервисов перехват вызовов методов COM-компонентов позволяет решить и другие задачи, например:

протоколирование вызовов COM-компонентов;

отладка – проверка значений аргументов, контроль подсчета ссылок;

специальный маршалинг;

использование альтернативных по отношению к RPC видов транспорта для передачи COM-вызовов (MSMQ, SOAP и т.п.);

асинхронные вызовы (заместитель сохраняет информацию о вызове и производит фактический вызов исходного компонента позднее).

Рисунок 1 иллюстрирует принцип перехвата вызовов COM-компонентов, Proxy и Stub – служебные компоненты, один из которых принимает вызовы от клиента, имитируя исходный компонент, а другой – передает эти вызовы компоненту, имитируя логику работы клиента. Именно по такой схеме работает маршалинг в COM, и по такой же схеме COM+ обеспечивает дополнительные сервисы (транзакции, блокировки и т.п.) для сконфигурированных компонентов.

Перехват методов COM интерфейсов 

Рисунок 1. Принцип перехвата COM-вызова.

Как это часто случается, несмотря на простое описание технологии перехвата, ее техническая реализация очень непростое дело, в особенности, когда речь идет об универсальном перехвате.

В первой части статьи мы познакомимся с различными техническими способами перехвата вызовов.

Техника перехвата вызовов

Один из самых простых и эффективных способов перехвата вызовов методов COM-компонента заключается в создании Proxy-компонента, реализующего нужный интерфейс и перенаправляющего вызовы исходному COM-компоненту.

ПРИМЕЧАНИЕ

Для COM-компонентов такой подход используется не только при перехвате вызовов, но еще и как средство повторного использования кода (code reuse), и носит название containment (включение).

В качестве примера рассмотрим стандартную реализацию IStream на основе памяти – CreateStreamOnHGlobal. Предположим, что нам необходимо ассоциировать имя с каждым потоком IStream, созданным с помощью CreateStreamOnHGlobal. Имя потока можно получить с помощью вызова IStream::Stat, но реализация IStream на основе памяти HGlobal всегда возвращает пустое имя. Мы можем поступить следующим образом:

создать компонент-обертку, поддерживающий IStream;

перенаправлять все вызовы IStream в стандартную реализацию CreateStreamOnHGlobal;

в методе IStream::Stat указывать имя потока.

class StreamOnMemory : public CComObjectRoot,

 public IStream

{

public:

 BEGIN_COM_MAP(StreamOnMemory)

 COM_INTERFACE_ENTRY(IStream)

 END_COM_MAP()

public:

 // реализация IStream

 STDMETHOD(Seek)(_LARGE_INTEGER dlibMove, ULONG dwOrigin,

 _ULARGE_INTEGER * plibNewPosition)

 {

 return m_spStm->Seek(dlibMove, dwOrigin, plibNewPosition);

 }

 // остальные методы реализованы аналогично Seek

...

 STDMETHOD(Stat)(tagSTATSTG * pstatstg, ULONG grfStatFlag)

 {

 HRESULT hr = m_spStm->Stat(pstatstg, grfStatFlag);

 if( SUCCEEDED(hr) && (grfStatFlag & STATFLAG_NONAME) == 0)

 {

 pstatstg->pwcsName = AtlAllocTaskWideString(m_name);

 }

 return hr;

 }

private:

 friend HRESULT CreateStreamOnHGlobal2(HGLOBAL ,BOOL ,LPOLESTR, LPSTREAM*);

 HRESULT init(HGLOBAL hGlobal,BOOL fDeleteOnRelease, LPOLESTR name)

 {

 m_spStm.Release();

 HRESULT hr = CreateStreamOnHGlobal(hGlobal, fDeleteOnRelease, &m_spStm);

 if(SUCCEEDED(hr))

 {

 m_name = name;

 }

 return hr;

 }

private:

 CComPtr<IStream> m_spStm;

 CComBSTR m_name;

};

HRESULT CreateStreamOnHGlobal2(HGLOBAL hGlobal,BOOL fDeleteOnRelease,

 LPOLESTR name, LPSTREAM* ppstm)

{

 CComObject<StreamOnMemory>* p = NULL;

 HRESULT hr = CComObject<StreamOnMemory>::CreateInstance(&p);

 if(SUCCEEDED(hr))

 {

 CComPtr<IStream> spStm = p;

 hr = p->init(hGlobal, fDeleteOnRelease, name);

 if(SUCCEEDED(hr))

 {

 *ppstm = spStm.Detach();

 }

 }

 return hr;

}

При таком подходе нет необходимости вносить какие-либо изменения в клиентский код, работающий с указателями на интерфейс IStream.

ПРИМЕЧАНИЕ

За исключением кода, создающего поток с помощью вызова CreateStreamOnHGlobal.

Такой “частный” подход неприменим, когда количество перехватываемых интерфейсов велико, или если информация об интерфейсах и сигнатурах их методов недоступна во время компиляции и станет известна только во время выполнения программы. Например, typelib-маршалинг в COM предоставляет клиенту Proxy-компонент, поддерживающий интерфейс серверного компонента, но обеспечить реализацию этого интерфейса инфраструктура COM может только во время выполнения – на этапе компиляции неизвестно, какие интерфейсы будут использоваться для typelib-маршалинга.

Разумеется, лучше было бы реализовать универсальный перехват вызовов COM-методов. Но при этом мы столкнемся с несколькими проблемами:

заранее неизвестно количество методов в произвольном интерфейсе, т.е. структура vtbl;

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

Решить указанные проблемы, используя только средства языков высокого уровня, не удастся. Мы могли бы попытаться обойти отсутствие информации о сигнатурах методов путем объявления функции с переменным количеством параметров:

void f(int a, ...);

Но такие функции используют соглашение о вызове cdecl, а методы COM-интерфейсов – stdcall.

ПРИМЕЧАНИЕ

Эти соглашения о вызовах в первую очередь различаются тем, кто ответственен за удаление параметров из стека после вызова. stdcall-функции очищают стек сами, а для cdecl-функций стек очищает вызывающая функция.

Подход ATL

В библиотеке ATL перехват вызовов используется для отладки COM-серверов. Если до включения заголовочного файла <atlbase.h> объявить символ препроцессора _ATL_DEBUG_INTERFACES (или _ATL_DEBUG_REFCOUNT), то в окне “Output” отладчика VS во время выполнения приложения будут появляться сообщения, описывающие вызовы AddRef и Release для COM-объектов, созданных с помощью ATL, текущий счетчик ссылок или IID запрашиваемого интерфейса. Ниже приведен пример таких сообщений:

QIThunk-1 AddRef:Object=0x00da4c50 Refcount = 1 CComClassFactory - IUnknown

QIThunk-2 AddRef:Object=0x00da4c50 Refcount = 1 CComClassFactory - IClassFactory

QIThunk-3 AddRef:Object=0x00da4e20 Refcount = 1 CFoo - IFoo

QIThunk-3 AddRef:Object=0x00da4e20 Refcount = 2 CFoo - IFoo

QIThunk-3 Release:Object=0x00da4e20 Refcount = 1 CFoo - IFoo

QIThunk-2 Release:Object=0x00da4c50 Refcount = 0 CComClassFactory - IClassFactory

QIThunk-4 AddRef:Object=0x00da4e20 Refcount = 1 CFoo - IFoo

QIThunk-3 Release:Object=0x00da4e20 Refcount = 0 CFoo - IFoo

QIThunk-1 Release:Object=0x00da4c50 Refcount = 0 CComClassFactory - IUnknown

ATL: QIThunk-4 LEAK:Object = 0x00da4e20 Refcount = 1 MaxRefCount = 1 CFoo - IFoo

Во время выгрузки ATL COM-сервера в окне “Output” появятся сведения об указателях на интерфейс, для которых счетчик ссылок не достиг значения 0, т.е. об утечках COM объектов.

“Магия” ATL работает благодаря перехвату вызовов методов COM-интерфейсов, в частности, AddRef, Release и QueryInterface.

Когда клиент запрашивает интерфейс у объекта с помощью QueryInterface, класс CComObject делегирует вызов базовому классу CComObjectRootBase::InternalQueryInterface, который при определенном макросе _ATL_DEBUG_INTERFACES обращается к экземпляру класса CAtlDebugInterfacesModule и вызывает у него метод AddThunk.

HRESULT AddThunk(IUnknown** pp, LPCTSTR lpsz, REFIID iid) throw()

Результатом вызова CComObjectRootBase::InternalQueryInterface становится специальный объект-посредник QIThunk, который перехватывает AddRef, Release и QueryInterface, а все остальные вызовы делегирует исходному компоненту.

Класс CAtlDebugInterfacesModule хранит список всех активных объектов-заместителей QIThunk и в своем деструкторе выполняет отладочную печать всех объектов, чей счетчик ссылок не достиг нулевого значения.

Когда клиент отпускает последнюю ссылку на компонент, QIThunk удаляет себя из списка активных посредников в CAtlDebugInterfacesModule.

Таким образом, клиенты имеют дело не с прямым указателем на интерфейс COM-объекта, а с указателем на QIThunk, который и печатает отладочные сообщения о текущем значении счетчика ссылок и IID запрашиваемого интерфейса.

Указатель на QIThunk ведет себя в точности так же, как и указатель на обычный интерфейс. Это достигается за счет того, что vtbl класса QIThunk содержит адреса методов-перехватчиков, вызывающих исходные методы. Поскольку все интерфейсы унаследованы от IUnknown, первые три адреса vtbl содержат QueryInterface, AddRef и Release. Их реализация в QIThunk тривиальна – сигнатура методов в точности известна на этапе компиляции.

Но как быть с остальными методами интерфейса, количество и сигнатуры которых неизвестны? Для решения этой проблемы QIThunk использует универсальную функцию-перехватчик, адресом которой заполняется vtbl. Виртуальные методы объявляются в QIThunk так:

STDMETHOD(f3)();

STDMETHOD(f4)();

...

STDMETHOD(f1023)();

Vtbl QIThunk содержит 1024 адреса. Интерфейсы, объявляющие большее количество методов, встречаются нечасто.

Реализация этих методов задается с помощью макроса:

ATL_IMPL_THUNK(3)

ATL_IMPL_THUNK(4)

...

ATL_IMPL_THUNK(1023)

Метод-перехватчик будет вызываться клиентом с заранее неизвестным количеством параметров, поэтому написать такую функцию на языке высокого уровня невозможно – не подходят ни стандартные пролог/эпилог, генерируемые компилятором C++, ни “нормальное” завершение функции вызовом инструкции ret, так как stdcall-функции должны очищать стек сами, передавая размер стека параметров в ret.

Перехват методов COM интерфейсов 

Рисунок 2. Вызов COM метода.

На рисунке 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) виртуальных методов, имеющих одинаковую реализацию.

Если Вам нужна помощь с академической работой (курсовая, контрольная, диплом, реферат и т.д.), обратитесь к нашим специалистам. Более 90000 специалистов готовы Вам помочь.
Бесплатные корректировки и доработки. Бесплатная оценка стоимости работы.

Поможем написать работу на аналогичную тему

Получить выполненную работу или консультацию специалиста по вашему учебному проекту
Нужна помощь в написании работы?
Мы - биржа профессиональных авторов (преподавателей и доцентов вузов). Пишем статьи РИНЦ, ВАК, Scopus. Помогаем в публикации. Правки вносим бесплатно.

Похожие рефераты:

ПРИМЕЧАНИЕ

Такое решение нельзя назвать изящным – исходные тексты QIThunk получаются большими, но, с другой стороны это наиболее эффективный способ генерации vtbl. Альтернативный способ мог бы заключаться в заполнении vtbl во время выполнения приложения: