ПЕРЕЛІК ДИСЦИПЛІН:
  • Адміністративне право
  • Арбітражний процес
  • Архітектура
  • Астрологія
  • Астрономія
  • Банківська справа
  • Безпека життєдіяльності
  • Біографії
  • Біологія
  • Біологія і хімія
  • Ботаніка та сільське гос-во
  • Бухгалтерський облік і аудит
  • Валютні відносини
  • Ветеринарія
  • Військова кафедра
  • Географія
  • Геодезія
  • Геологія
  • Етика
  • Держава і право
  • Цивільне право і процес
  • Діловодство
  • Гроші та кредит
  • Природничі науки
  • Журналістика
  • Екологія
  • Видавнича справа та поліграфія
  • Інвестиції
  • Іноземна мова
  • Інформатика
  • Інформатика, програмування
  • Юрист по наследству
  • Історичні особистості
  • Історія
  • Історія техніки
  • Кибернетика
  • Комунікації і зв'язок
  • Комп'ютерні науки
  • Косметологія
  • Короткий зміст творів
  • Криміналістика
  • Кримінологія
  • Криптология
  • Кулінарія
  • Культура і мистецтво
  • Культурологія
  • Російська література
  • Література і російська мова
  • Логіка
  • Логістика
  • Маркетинг
  • Математика
  • Медицина, здоров'я
  • Медичні науки
  • Міжнародне публічне право
  • Міжнародне приватне право
  • Міжнародні відносини
  • Менеджмент
  • Металургія
  • Москвоведение
  • Мовознавство
  • Музика
  • Муніципальне право
  • Податки, оподаткування
  •  
    Бесплатные рефераты
     

     

     

     

     

     

         
     
    Перехоплення методів 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 + забезпечує додаткові сервіси (транзакції, блокування і т.п.) для сконфігурованих компонентів.

    Малюнок 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 m_spStm;   

    CComBSTR m_name;   

    );      

    HRESULT CreateStreamOnHGlobal2 (HGLOBAL hGlobal, BOOL fDeleteOnRelease,   

    LPOLESTR name, LPSTREAM * ppstm)   

    (   

    CComObject * p = NULL;   

    HRESULT hr =   CComObject :: CreateInstance (& p);   

    if (SUCCEEDED (hr))   

    (   

    CComPtr 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-серверів. Якщо до включення заголовки оголосити символ препроцесора _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.

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

    ПРИМІТКА   

    Таке рішення не можна назвати витонченим --   вихідні тексти 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   

    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   

    (   

    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 *   >:: iterator it = m_stacks.begin ();   

    for (; it! = m_stacks.end ();   + + it)   

    delete * it;      

    TlsFree (m_slot);   

    )   

    void push (T t)   

    (   

    std:: deque * p =   

    reinterpret_cast *   > (TlsGetValue (m_slot ));   

    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 *   > (TlsGetValue (m_slot ));   

    T t = p-> back ();   

    p-> pop_back ();   

    return t;   

    )      

    std:: vector *>   m_stacks;   

    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   "<   

    )      

    HRESULT __stdcall   postprocess (int n, HRESULT hr)   

    (   

    std:: cout << "method   "<   

    <   

    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):

    Малюнок 3. Стек дзвінка        

    __declspec (naked) void ItfThunk:: thunk ()   

    (   

    __asm   

    (   

    push [esp]// кладемо в стек n (параметр методу preprocess)   

    push [esp +0 Ch]// кладемо в стек this для виклику preprocess   

    call preprocess// викликаємо ItfThunk:: preprocess (n)   

    call store// викликаємо ItfThunk:: store   

    mov eax, [esp +8]// замінюємо this у стеку на вихідний   

    mov eax, [eax +4]// із змінної ItfThunk:: m_p   

    mov [esp +8], eax   

    lea eax, post_thunk// замінюємо адреса повернення   на post_thunk   

    mov [esp +4], eax   

    mov eax, [esp +8]// отримуємо vptr з   вихідного покажчика   

    mov eax, [eax]   

    pop ecx// прибираємо з стека зайвий параметр   n   

    mov eax, [eax +4 * ecx]// полчаем адреса методу   з vtbl   

    jmp eax// переходимо у вихідний метод   

    post_thunk:   

    sub esp, 10h// виділяємо в стеку місце для   CallInfo   

    push esp   

    push eax// результат виклику вихідного   методу в eax   

    call restore// відновлюємо инфрмация з   TLS   

    add esp, 8   

    call postprocess// постобробка   

    ret   

    )   

    )     

    Використовувати перехоплювач дуже просто - клієнт передає вказівник на даний інтерфейс конструктору ItfThunk і потім використовує ItfThunk як покажчика:        

    CComPtr   spFoo;   

    HRESULT   hr = spFoo.CoCreateInstance (__uuidof (Foo ));   

    thunks:: ItfThunk   t (spFoo.p);   

    spFoo.p   = Reinterpret_cast (& t);   

    spFoo-> F ();     

    Тепер ми можемо виконувати постобробку виклику, але є ще одне завдання, яку цей перехоплювач не вирішує - припустимо, що в деяких випадках в результаті передобробки ми приймаємо рішення, що виклик вихідного методу має бути заблоковано. Типовий приклад - рольова безпеку. Виклик методу не проходить перевірку рольової безпеки і повинен бути відхилений. Але ми не можемо зробити цього, тому що точну кількість параметрів методу невідомо, і наш перехоплювач делегує очищення стека після виклику самому методу.

    У загальному випадку для COM-інтерфейсів ми не можемо дізнатися сигнатуру їх методів, але для інтерфейсів, які використовують typelib-маршалинга або ітерфейсов, proxy/stub яких згенерований з ключем MIDL/oicf, ця інформація доступна.        

    ПРИМІТКА   

    Ключ/oicf компілятора midl дозволяє   генерувати інтерпретується код для proxy/stub і, як результат, інформація   про сигнатурах методу доступна програмно. Докладніше про це можна прочитати в   статті "Секрети   маршалинга ".     

    Отримавши інформацію про кількість параметрів методу, ми змогли вирішити кілька завдань:

    Заблокувати виклик методу.

    Виконувати відкладений/асинхронний виклик.

    І все це завдяки тому, що перехоплювач зможе очищати стек самостійно, не делегуючи цю роботу вихідного методу.

    Необхідності самостійно розробляти перехоплювач, що спирається на інформацію з бібліотеки типів, ні - починаючи з W2K документований API, що дозволяє використовувати стандартні перехоплювачі з інфраструктури COM/COM + в своїх цілях.

    CoGetInterceptor, CoGetInterceptorFromTypeInfo

    У попередньому розділі статті ми розглянули кілька технологій перехоплення викликів методів інтерфейсів (і могли відчути складність створення універсального перехоплювача). Але жодна з цих технологій не дозволила вирішити завдання перехоплення повністю. Зокрема, не вирішена задача асинхронних/відкладених дзвінків.

    На щастя, тепер документовані API-функції, що дозволяють використовувати в додатках перехоплювачі з інфраструктури COM/COM +.        

    ПРИМІТКА   

    Це ті самі перехоплювачі, за допомогою   яких COM + забезпечує свої сервіси прозоро для компонента і клієнта --   рольову безпека, синхронізацію і т.д.     

    Отримати перехоплювач для довільного інтерфейсу можна за допомогою функції CoGetInterceptor:        

    HRESULT CoGetInterceptor (   

    REFIID iidIntercepted,// IID   перехоплюваних інтерфейсу   

    IUnknown * punkOuter,// IUnknown для   агрегації   

    REFIID iid,// IID інтерфейсу,   запитуваної у перехоплювача   

    void ** ppv// покажчик на інтерфейс   перехоплювача   

    );     

    Перехоплювачі COM + використовують інформацію з бібліотеки типів, щоб визначити сигнатуру методу і кількість/типи параметрів, а також виконати маршалинга. Тому, якщо бути більш точним, як перший параметра (iidInterceptor) годяться не довільні інтерфейси, а тільки ті з них, які сумісні з oleautomation та описані в бібліотеці типів.

    Головне вікно перехоплювача - ICallInterceptor, його ми і будемо запитувати у виклику CoGetInterceptor:        

    # include      

    CComModule _Module;      

    int _tmain (int argc, _TCHAR * argv [])   

    (      

    CoInitialize (0);   

    _Module.Init (0, 0);   

    (   

    CComPtr spFoo;   

    HRESULT hr =   spFoo.CoCreateInstance (__uuidof (Foo ));      

    CComPtr   spInt;   

    hr =   CoGetInterceptor (__uuidof (IFoo), 0, __uuidof (ICallInterceptor),   

    reinterpret_cast (& spInt ));   

    )   

    _Module.Term ();   

    CoUninitialize ();   

    return 0;   

    )     

    Результатом виконання наведеного вище програми буде ... Access Violation в надрах ntdll.dll. Цей неприємний сюрприз викликаний тим, що перехоплювачі використовують розподільник пам'яті RPC, який за умовчанням не ініціалізованим першим. Виправити цю проблему можна або з допомогою виклику CoInitializeSecurity, або викликом будь-яких функцій маршалинга, які проініціалізіруют RPC heap (є ще варіант із прямим викликом функції ініціалізації з rpcrt4.dll, але вона не документована).        

    ПРИМІТКА   

    Проблема з ініціалізацією RPC-купи була   виправлена в Windows 2003 Server.     

    Виправлений код клієнта:        

    HRESULT hr = CoInitializeSecurity (NULL, -1, NULL, NULL,   

    RPC_C_AUTHN_LEVEL_DEFAULT,   RPC_C_IMP_LEVEL_IMPERSONATE,   

    NULL, EOAC_NONE, NULL);      

    CComPtr spFoo;   

    hr = spFoo.CoCreateInstance (__uuidof (Foo ));      

    CComPtr spInt;   

    hr = CoGetInterceptor (__uuidof (IFoo), 0, __uuidof (ICallInterceptor),   

    reinterpret_cast (& spInt ));     

    За допомогою покажчика на інтерфейс ICallInterceptor ми можемо зареєструвати свої власні обробники викликів:        

    Методи ICallInterceptor         

    Опис             

    HRESULT RegisterSink (ICallFrameEvents *   psink);         

    Зареєструвати обробник             

    HRESULT   GetRegisteredSink (ICallFrameEvents ** ppsink);         

    Отримати зареєстрований обробник             

    ПРИМІТКА   

    Інші методи ICallInterceptor описані   в MSDN     

    Оброблювач повинен реалізовувати інтерфейс ICallFrameEvents.        

    Методи ICallFraneEvent         

    Опис             

    HRESULT OnCall (ICallFrame * pFrame);         

    Виклик перехоплюваних   інтерфейсу     

    Після реєстрації обробника ми будемо отримувати подія OnCall кожного разу, коли клієнт буде здійснювати виклик через перехоплюваних інтерфейс.

    Доповнимо код клієнта (див. вище) - тепер ми будемо реєструвати свій оброблювач викликів:        

    class CallHandler: public CComObjectRoot,   

    public ICallFrameEvents   

    (   

    public:      

    BEGIN_COM_MAP (CallHandler)   

    COM_INTERFACE_ENTRY (ICallFrameEvents)   

    END_COM_MAP ()      

    STDMETHOD (OnCall) (ICallFrame *   pFrame)   

    (   

    return S_OK;   

    )   

    );         

    ...   

    CComPtr spInt;   

    hr = CoGetInterceptor (__uuidof (IFoo), 0, __uuidof (ICallInterceptor),   

    reinterpret_cast (& spInt ));      

    CComObject * pHandler = 0;   

    CComObject :: CreateInstance (& pHandler);   

    hr = spInt-> RegisterSink (pHandler);      

    CComPtr spFooInt;   

    hr = spInt.QueryInterface (& spFooInt);   

    hr = spFooInt-> F ();             

    ПРИМІТКА   

    Якщо обробник поверне   HRESULT з помилкою, помилку отримає і клієнт, але її код, на жаль, не   передається користувачеві. Якщо клієнт не зареєструє жодного обробника,   то виклик методу також завершиться з помилкою.       

    Ми запитуємо покажчик на перехоплюваних інтерфейс у перехоплювачі, а потім виконуємо виклик методу IFoo:: F, в результаті ми потрапимо в код обробника ICallFrameEvent:: OnCall.

    Завдання обробника - вирішити, що робити далі з викликом:

    Відхилити його, повернувши помилку.

    Зберегти стек параметрів виклику, щоб виконати його асинхронно.

    Виконати виклик негайно.

    Прямі/синхронні виклики

    Інформацію про виклик оброблювач одержує за допомогою покажчика на інтерфейс ICallFrame, переданий йому як параметр pFrame.

    Інтерфейс ICallFrame дозволяє отримати інформацію про сигнатурі методу, розмір стека параметрів, значення окремих параметрів і результат виклику методу. Крім того, за допомогою ICallFrame можна змінити значення окремих (або всіх) параметрів і доповнити стек параметрів у випадку, якщо клієнт передав не всі необхідні параметри (наприклад, клієнт зробив виклик не через вказівник на перехоплюваних інтерфейс, а за допомогою ICallInterceptor:: CallIndirect, передаючи частковий стек параметрів).        

    ПРИМІТКА   

    Подробнее опис методів інтерфейсу   ICallFrame см. в MSDN     

    Розширимо код нашого обробника CallHandler так, щоб він видавав налагоджувальний повідомлення про виклик і його результати і виконував негайний виклик за допомогою ICallFrame:: Invoke:        

    template   

    class CallHandler: public CComObjectRoot,   

    public ICallFrameEvents   

    (   

    public:   

    BEGIN_COM_MAP (CallHandler)   

    COM_INTERFACE_ENTRY (ICallFrameEvents)   

    END_COM_MAP ()      

    void init (CComPtr   spItf)   

    (   

    m_spItf = spItf;   

    )   

    STDMETHOD (OnCall) (ICallFrame *   pFrame)   

    (   

    LPWSTR itf, method;   

    HRESULT hr =   pFrame-> GetNames (& itf, & method);   

    hr =   pFrame-> Invoke (m_spItf.p);   

    ATLTRACE ( "call% s::% s   % 8xn ", itf, method, hr);   

    CoTaskMemFree (itf);   

    CoTaskMemFree (method);   

    return hr;   

    )   

    private:   

    CComPtr m_spItf;   

    );     

    Викликаючи ICallFrame:: Invoke, ми не передаємо ніяких параметрів - значення для параметрів перехоплюваних методу були передані клієнтом, коли він виконував виклик через перехоплювач.        

    ПРИМІТКА   

    Метод ICallFrame:: Invoke має змінну   кількість параметрів (що рідко зустрічається у COM-інтерфейсів). Якщо стек   параметрів виклику заповнено лише частково, в Invoke можуть передаватися   додаткові параметри виклику (які будуть додані в стек перед   викликом).     

    Непрямі іасинхронні/відкладені виклики

    Ми навчилися виконувати прямі виклики через покажчик на перехоплюваних інтерфейс. Такий перехоплювач може виконувати трасування викликів і їх результатів, полегшувати процес налагодження складних компонентів, відслідковувати значення окремих параметрів (і замінювати їх з метою налагодження).

    За допомогою перехоплювачів COM + можна виконувати непрямі і асинхронні виклики. Замість прямого виклику ICallFrame:: Invoke ми можемо:

    зберегти вміст параметрів, що знаходяться у стеку, в спеціальний буфер (фактично виконати маршалинга параметрів);

    передати їх за допомогою будь-якого доступного транспорту (RPC, MSMQ, SOAP, файли і т.п.) компоненту;

    виконати виклик;

    отримати значення [out] параметрів, виконати зворотний маршалинга;

    передати значення параметрів клієнта за допомогою будь-якого доступного транспорту.

    Для пакування стека виклику, тобто маршалинга призначений метод ICallFrame:: Marshal:        

    HRESULT Marshal (   

    CALLFRAME_MARSHALCONTEXT *   pmshlContext,// контекст (т.e. inproc тощо)   

    MSHLFLAGS * mshlflags,// звичайний   або табличний маршалинга   

    PVOID pBuffer,// буфер   

    ULONG cbBuffer,// розмір буфера   

    ULONG * pcBufferUsed,// спожитий розмір буфера   

    RPCOLEDATAREP * pdataRep,// формат   подання даних   

    ULONG * prpcFlags// RPC-прапори   

    );     

    Розмір буфера, необхідного для маршалинга, можна визначити за допомогою ICallFrame:: GetMarshalSizeMax:        

    HRESULT GetMarshalSizeMax (   

    CALLFRAME_MARSHALCONTEXT   * PmshlContext,// контекст (т.e. inproc тощо)   

    MSHLFLAGS   mshlflags,// звичайний або табличний   маршалинга   

    ULONG   * PcbBufferNeeded// необхідну   розмір буфера   

    );     

    Зворотне перетворення буфера в стек виклику виконується за допомогою спеціального інтерфейсу ICallUnmarshal і його методу ICallUnmarshal:: Unmarshal:        

    HRESULT Unmarshal (   

    ULONG iMethod,// номер методу   

    PVOID pBuffer,// буфер   

    ULONG cbBuffer,// розмір буфера   

    BOOL fForceBufferCopy,// зберегти копію буфера   

    RPCOLEDATAREP dataRep,// формат представлення даних   

    CALLFRAME_MARSHALCONTEXT *   pcontext,// контекст (т.e. inproc тощо)   

    ULONG * pcbUnmarshalled,// розмір використаної частини буфера   

    ICallFrame ** ppFrame// ICallFrame з   стеком виклику   

    );     

    Інтерфейс ICallUnmarshal підтримується перехоплювачем, який ми отримуємо викликом CoGetInterceptor. Таким чином, щоб перетворити буфер в стек виклику, нам необхідно:

    створити перехоплювач в адресному просторі сервера (тобто що викликається компоненту);

    запросити в нього (через QI) покажчик на інтерфейс ICallUnmarshal;

    викликати ICallUnmarshal:: Unmarshal - ми отримаємо вказівник на інтерфейс ICallFrame.

    Після виклику компонента звичайно потрібно передати вихідні (out) параметри назад клієнтові. Зробити це можна кількома викликів:

    ICallFrame:: Marshal на стороні сервера;

    ICallFrame:: Unmarshal на стороні клієнта.        

    HRESULT UnMarshal (   

    PVOID pBuffer,// буфер з out-параметрами   

    ULONG cbBuffer,// розмір буфера   

    RPCOLEDATAREP pdataRep,// формат   подання даних   

    CALLFRAME_MARSHALCONTEXT * pcontext,// контекст (т.e. inproc тощо)   

    ULONG * pcbUnmarshaled// розмір використаної частини буфера   

    );     

    Тип маршалинга параметрів - in чи out - задається прапором структури CALLFRAME_MARSHALCONTEXT.

    Послідовність викликів при маршалинга in-та out-параметрів проілюстрована на р

         
     
         
    Реферат Банк
     
    Рефераты
     
    Бесплатные рефераты
     

     

     

     

     

     

     

     
     
     
      Все права защищены. Reff.net.ua - українські реферати ! DMCA.com Protection Status