Как получить доступ к COM-объекту, используя ассемблер — Архив WASM.RU

Все статьи

Как получить доступ к COM-объекту, используя ассемблер — Архив WASM.RU

Предисловие:

COM (Component Object Model) используется операционной системой Windows различным образом. Например, shell.dll использует COM для доступа к некоторым из своих API методов. Интерфейсы IShellLink и IPersistFile shell32.dll будут продемонстрированы путем создания ярлыка. Предполагается базовое знание COM. Использованный код выдержан в стиле MASM.

Введение:

COM может показаться весьма сложным из-за своих многочисленных особенностей, но в реальности многие из этих сложностей вырождаются в простой вызов функций. Самой сложной частью является понимание вовлеченных в процесс структур данных, которые необходимо использовать при определении интерфейсов. Я извиняюсь за всю эту С++'нутую терминологию, использованную в этом туториале. Хотя COM является языконезависимой технологией, большая часть терминологии для его определения была заимствована из C++.

Чтобы использовать COM-методы какого-либо объекта, сначала вы должны инстанцировать или создать этот объект из соответствующего com-класса, затем получить от него указатель на его интерфейс. Этот процесс производится с помощью API-функции CoCreateInstance. После того, как вы выполните все, что нужно, вам необходимо вызвать его метод Release, и com-класс удалит объект, а COM выгрузит com-класс.

COM-объект называется сервером, а программа, которая его вызывает, называется клиентом.

Доступ к COM-методам

Чтобы использовать COM-методы, вам необходимо знать структуру интерфейса. Даже если вы используете "позднее связывание" через IDispatch, вам все равно нужно знать структуру IDispatch. COM-интерфейс - это всего лишь таблица указателей на функции. Давайте начнем с интерфейса IUnknown. Если бы вы создавали компонент, который просто экспортирует интерфейс IUnknown, то получили бы полностью рабочий COM-объект (хотя и на уровне "Hello, world"). IUnknown имеет три основных метода, которые есть в любом интерфейсе, потому что любой интерфейс наследуется от IUnknown. Держите в уме, что все интерфейсы являются структурой указателей на функции. Для IUnknown это будет выглядеть так:

IUnknown STRUCT DWORD
; IUnknown methods
IUnknown_QueryInterface         QueryInterface_Pointer ?
IUnknown_AddRef                 AddRef_Pointer ?
IUnknown_Release                Release_Pointer ?
IUnknown ENDS

Именно так, всего лишь 12 байтов длиной. Он состоит из трех 4-х байтных указателей на процедуры, которые фактически и являются методами. Это и есть печально известная "vtable", о которой вы уже могли слышать. Указатели определены так, чтобы MASM мог делать для нас кое-какую проверку типов при компиляции наших вызовов. Так как виртуальная таблица содержит адреса функций (указатели), эти указатели определены в нашем описании интерфейса через typedef.

QueryInterface_Pointer  typedef ptr QueryInterface_Proto
AddRef_Pointer          typedef ptr AddRef_Proto
Release_Pointer                 typedef ptr Release_Proto

Наконец мы определяем прототипы функций следующим образом:

QueryInterface_Proto    typedef PROTO :DWORD, :DWORD, :DWORD
AddRef_Proto            typedef PROTO :DWORD
Release_Proto                   typedef PROTO :DWORD

Подобное определение интерфейса позволяет выявлять множество ошибок в процессе компиляции. На практике мы определять эти интерфейсы в заголовочных файлах, чтобы они не засоряли исходный код.

Одно довольно большое замечание относительно определения интерфейсов: MASM не может использовать подобные косвенные ссылки, поэтому мы определяем заранее прототипы функций через typedef'ы, а потом виртуальную таблицу интерфейса. В заголовочных файлах программы-примера интерфейсы определяются именно таким образом.

Чтобы использовать интерфейс, вам нужно получить на него указатель.

Функция CoCreateInstance может быть использована для получения этого косвенного указателя на структуру интерфейса. Таким образом, один уровень "виртуальности" убран и у нас есть указатель на "объект", который содержит структуру. Окончательно все это выглядит так:

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

Когда клиент делает вызов на COM-библиотеку, чтобы создать COM-объект, он передает адрес переменной, в которую будет помещен указатель на объект. Этот изначальный указатель часто называют "ppv", от С++'нутого "pointer to pointer to (void)", где (void) значит неопределенный тип. Ppv содержит адрес другого указателя ("pv"), который, в свою очередь ссылается на всю таблицу указателей. В ней на каждую функцию интерфейса приходится один элемент таблицы.

Например, предположим, что мы использует CoCreateInstance и успешно получили интерфейсный указатель ppv и узнать, поддерживает ли он другие интерфейсы. Мы можем вызвать его метод QueryInterface и запросить новый ppv (ppv2, указатель на интерфейс) на другой интерфейс (pIID, указатель на Interface Indentifying GUID), который нам нужен. В C++ QueryInterface имеет следующий прототип:

(HRESULT) SomeObject::QueryInterface (this:pObject, IID:pGUID, ppv2:pInterface)

Подобный вызов будет выглядеть примерно так:

; получаем указатель на объект
mov eax, ppv
; и используем его, чтобы найти структуру интерфейса
mov edx, [eax]

; загоняем параметры функции в стек
push OFFSET ppv2
push OFFSET IID_ISomeOtherInterface
push dword ppv

; а затем вызываем этот метод
call dword ptr [eax + 0]

Это может быть выполнено с помощью встроенного в MASM макроса 'invoke' следующим образом:

; получаем указатель на объект
mov eax, ppv
; а затем используем его, чтобы найти структуру интерфейса
mov edx, [eax]
; а затем вызываем этот метод
invoke (IUnknown PTR [edx]).IUnknown_QueryInterface, ppv,
    ADDR IID_SomeOtherInterface, ADDR ppv_new

Я надеюсь, что это для будет настолько же просто, как и для меня.

Обратите внимание, что мы должны передать функции использованный нами указатель, так как это позволяет знать интерфейсу какой объект (в C++ этот указатель обозначается как "this") мы используем.

Также заметьте, что необходимо делать привод регистра к соответсвующему типу. Это позоволяет компилятору знать, какую структуру использовать, чтобы получить правильное смещение в виртуальной таблице для функции .QueryInterface (в данном случае это означает нулевое смещение от [edx].

Еще один момент, который может быть не совсем ясен. Обратите внимание, что я изменил имя функции с "QueryInterface" на "IUnknown_QueryInterface". Я считаю подобное изменение имени функций необходимым, так как когда вы перейдете к COM-проектам со множеством похожих интерфейсов, в которых будут встречаться методы с одинаковыми именами. В этом нет ничего неправильного, фактически это и есть то, что называется полиморфизмом, но это может немного смутить компилятор.

Макрос coinvoke

-------------------------------------------------------------------------
С помощью этого макроса мы можем упростить вызов COM-методов. Этот макрос
является частью файла oaidl.inc.
;------------------------------------------------------------------------
; coinvoke MACRO
;
; invokes an arbitrary COM interface
; Запускает метод из заданного COM-интерфейса
;
; revised 12/29/00 to check for edx as a param and force compilation error
;                   (thanks to Andy Car for a how-to suggestion)
; revised 7/18/00 to pass pointer in edx (not eax) to avoid confusion with
;   parmas passed with ADDR  (Jeremy Collake's excellent suggestion)
; revised 5/4/00 for member function name decoration
; see http://ourworld.compuserve.com/homepages/ernies_world/coinvoke.htm
;
; pInterface    pointer to a specific interface instance
; pInterface
; Interface     the Interface's struct typedef
; Function      which function or method of the interface to perform
; args          all required arguments
;                   (type, kind and count determined by the function)
;
coinvoke MACRO pInterface:REQ, Interface:REQ, Function:REQ, args:VARARG
    LOCAL istatement, arg
    FOR arg,      ;; run thru args to see if edx is lurking in there
        IFIDNI <&arg>, 
            .ERR 
        ENDIF
    ENDM
    istatement CATSTR ,<_>,<&Function, pInterface>
    IFNB      ;; add the list of parameter arguments if any
        istatement CATSTR istatement, <, >, <&args>
    ENDIF
    mov edx, pInterface
    mov edx, [edx]
    istatement
ENDM
;---------------------------------------------------------------------

Таким образом, тот же метод QueryInterface теперь можно вызвать одной строчкой кода:

coinvoke ppv ,IUnknown, QueryInterface, ADDR IID_SomeOtherInterface, ADDR ppnew

Обратите внимание, что теперь преобразованием имени делает макрос.

Единственный 'подводный камень' (хорошо, наиболее очевидный) - это то, что параметры COM-функции не должны передаваться через edx, так как он используется для хранения ссылки на 'this'. Использование edx'а в качестве параметра сгенерирует ошибку компиляции.

Использование IShellFile и IPersistFile из shell32.dll

Shell32.dll предоставляет простой, легкий путь для создания ярлыков. Тем не менее, она использует COM-интерфейс для этого. Приведенный ниже пример основан на секции MSDN "Shell Links" из "Internet Tools and Technologies".

Эту статью вы можете найти по адресу http://msdn.microsoft.com/library/psdk/shellcc/shell/Shortcut.htm.

Мы получим доступ к нескольким членам интерфейсов IShellLink и IPersistFile. Обратите внимание, что каждый интерфейс включает параметр "ppi", это интерфейс, который мы вызываем (это параметр THIS). (Следующая информация является копией той, что предоставленна Микрософтом).

  • IShellLink::QueryInterface, ppi, ADDR riid, ADDR ppv
    • riid: идентификатор требуемого интерфейса.
    • ppv: указатель на переменную, которая получает интерфейс.
    • Описание: проверяет, поддерживает ли объект требуемый интерфейс. Если это так, то заполняет ppv указателем на интерфейс.

  • IShellLink::Release, ppi
    • Описание: понижает количество ссылкок на интерфейс IShellLink на один.

  • IShellLink::SetPath, ppi, ADDR szFile
    • pszFile: указатель на текстовый буфер, содержащий новый путь на объект-ярлык.
    • Описание: задает путь к файлу, на который указывает ярлык.

  • IShellLink::SetIconLocation, ppi, ADDR szIconPath, iIcon
    • pszIconPath: указатель на текстовый буфер, содержащий новый путь к иконке.
    • iIcon: индекс иконки. Отсчет идет с нуля.
    • Description: устанавливает какую иконку будет использовать ярлык.

  • IPersistFile::Save, ppi, ADDR szFileName, fRemember
    • pszFileName: указывает на ASCIIZ-строку, содержащую абсолютный путь к файлу, в который должен быть сохранен объект.
    • fRemember: указывает, должен ли файл, заданный в pszFileName, стать текущим рабочим файлом. Если TRUE, то pszFileName становится текущим файлом и объект должен очистить свой флаг занятости после сохранения. Если FALSE, то это сохраняет операцию как "Save A Copy As ...". В этом случае текущий файл не изменяется, а объект не должен очищаеть свой флаг занятости. Если pszFileName равен NULL, флаг fRemember должен игнорироваться.
    • Описание: выполняет операцию сохранения для объекта-ярлыка или сохраняет созданный ярлык.

  • IPersistFile::Release, ppi
    • Description: уменьшает счетчик ссылок на интерфейс IPersistFile на один. Эти интерфейсы содержат гораздо больше методов (смотри полное определение интерфейса в прилагающемся к туториалу коде), но мы сконцентрируемся только на тех, которые реально используем.

В файле ярлыка (.lnk) содержится следующая информацию:

  • Путь к файлу и имя программы
  • Откуда берется иконка, используемая при отображении ярлыка (обычно берется из самого экзешника), и какая конкретна иконка берется из этого файла. Мы будем использовать первую иконку из этого файла.
  • Путь к файлу, в котором должен быть сохранен ярлык.

Использование этих интерфейсов достаточно просто и прямолинейно. Это выглядит примерно следующим образом:

  • Вызываем CoCreateInstance CLSID_ShellLink, чтобы получить интерфейс IID_IShellLink, Queryinterface IShellLink, чтобы получить интерфейс IID_IPersistFile.
  • Вызываем IShellLik.SetPath, чтобы указать, где находится файл, на который будет ссылаться ярлык.
  • Вызываем IShellLink.SetIconLocation, чтобы указать, какую иконку использовать.
  • Вызываем IPersistFile.Save, чтобы сохранить наш ярлык.
  • Вызываем IPersistFile.Release.
  • Вызываем IShellLink.Release.

Вам следуется знать о нескольких полезных утилитах, входящий в MSVC: консольная утилита "FindGUID.exe", которая просматривает регистр в поисках определенного интерфейса или com-класса или выведет список всех классов и интерфейсов с ассоциированными с ними GUID'ами, а также приложение OLEView, которамя позволит вам получать информацию из различных библиотек.

Будьте внимательны во время определения интерфейса. Пропуск методы в виртуальной таблице может повлечь странные результаты. Выкиньте метод из определения таблице вы получите неверный интерфейс. В оригинальное определении интерфейса IShellLink, которое я использовал, была пропущена одна функция. Вызовы, которые я осуществлял, возвращали в hResult "SUCCEDED", но в некоторых случаях стек очищался неправильно (поскольку не совпадало количество параметров, а это, в свою очередь, приводил к GPF, как только я выходил из процедуры. Держите это в уме, если у вас начинают происходить "странные" вещи.

MakeLink.asm, демонстрация COM

Эта программа делает очень немного, как и полагается хорошей программе-туториалу. Как только вы ее запустите, она создаст ярлык на саму себя в той же директории. Довольно необычно запускать ее из файлового менеджера и смотреть, как появляется ярлык. Затем вы можете испытать ярлык и увидеть, как изменится время его создания.

Исходник программы находится в masm32\COM\examples\shortcut. Он начинается с "хакерского" кода, с помощью которого программа получает полный путь к программе и формирует необходимые строки, которые затем передаются интерфейсу IShellLink, после чего ярлык сохраняется.

Процедура CoCreateLink используется для запуска соответствующих COM-методов и создания ярлыка так, чтобы эта процедура могла использоваться в других программах.

2002-2013 (c) wasm.ru