COM в Ассемблере - Часть II — Архив WASM.RU

Все статьи

COM в Ассемблере - Часть II — Архив WASM.RU

В моей предыдущей статье объяснялось, как использовать COM-объекты в ваших программах, написанных на ассемблере. Там говорилось только о том, как вызывать COM-методы, а не как создавать свои COM-объекты. Эта статья расскажет, как это делать.

В этой статье будет затронуто реализация COM-объектов, используя синтаксис MASM. Здесь не будут браться в расчет ассемблеры TASM или NASM, хотя используемые методы легко применить к любому ассемблеру.

В этой статье не будут объсняться продвинутые технологии COM, такие как повтороное использование, трединг, серверы/клиенты и т.д. Об этом будет рассказано в будущих статьях.

Обзор интерфейсов COM

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

 IInterface struct
     lpVtbl  dd  ?
 IInterface ends

 IInterfaceVtbl struct
     ; методы IUnknown
     STDMETHOD       QueryInterface, :DWORD, :DWORD, :DWORD
     STDMETHOD       AddRef, :DWORD
     STDMETHOD       Release, :DWORD
     ; методы IInterface
     STDMETHOD       Method1, :DWORD
     STDMETHOD       Method2, :DWORD
 IInterfaceVtbl ends

STDMETHOD используется для упрощения объявления интерфейса и определяется следующим образом:

 STDMETHOD MACRO name, argl :VARARG
     LOCAL @tmp_a
     LOCAL @tmp_b
     @tmp_a TYPEDEF PROTO argl
     @tmp_b TYPEDEF PTR @tmp_a
     name @tmp_b ?
ENDM

Использование этого макроса значительно упрощает объявления интерфейсов и делает возможным использование команды invoke. (Макрос написан Ewald'ом :) )

 mov     eax, [lpif]                         ; lpif - указатель на интерфейс
 mov     eax, [eax]                          ; получаем адрес vtable
 invoke  (IInterfaceVtbl [eax]).Method1, [lpif] ; косвенный вызов функции
 - or -
 invoke  [eax][IInterfaceVtbl.Method2], [lpif]  ; альтернативная форма
                                                ; записи

Какую форму записи использовать - дело вкуса. В обоих случаях генерируется один и тот же код.

Все интерфейсы должны наследоваться от интерфейса IUnknown. Это означает, что первые 3 метода vtable должны быть QueryInterface, AddRef и Release. Цель и реализация этих методов будет обсуждаться ниже.

GUID'ы

GUID - это глобальный уникальный ID. GUID - это 16-ти байтное число, которое уникально у каждого интерфейса. COM использует GUID'ы, чтобы отличать интерфейсы друг от друга. Использование этого метода предотвращает проблемы с совпадением имен или версий. Чтобы получить GUID, вы можете использовать утилиту, которая включена в большинство пакетов разработки программ под Win32.

GUID можно представить как следующую структуру:

 GUID STRUCT
     Data1   dd ?
     Data2   dw ?
     Data3   dw ?
     Data4   db 8 dup(?)
 GUID ENDS

Затем GUID объявляется в секции данных:

 MyGUID GUID <3F2504E0h, 4f89h, 11D3h, <9Ah, 0C3h, 0h, 0h, 0E8h, 2Ch, 3h,1h>>

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

COM-объекты

COM-объект - это просто реализация интерфейса. Детали реализации не затрагиваются стандартами COM, поэтому мы можем реализовывать объекты как угодно, пока это удовлетворяет всем требованиям определения интерфейса.

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

 Object struct
     interface   IInterface  <?>     ; указатель на IInterface
     nRefCount   dd          ?       ; счетчик ссылок
     nValue      dd          ?       ; приватные данные объекта
 Object ends

Также мы должны определить vtable'ы, которые будут использоваться. Эти таблицы должны быть статическими и не могут меняться во время выполнения программы. Каждый член vtable - это указатель на метод. Далее показывается, как определить vtable.

 @@IInterface segment dword
 vtblIInterface:
     dd      offset IInterface@QueryInterface
     dd      offset IInterface@AddRef
     dd      offset IInterface@Release
     dd      offset IInterface@GetValue
     dd      offset IInterface@SetValue
 @@IInterface ends

Подсчет ссылок

COM-объект управляет продолжительностью своей жизни с помощью подсчета ссылок. У каждого объекта есть счетчик ссылок, отслеживающий, как много экземпляров указателя на интерфейс было создано. Счетчик объекта должен поддерживать значение до 2^32, то есть он должен быть DWORD.

Когда счетчик ссылок падает до нуля, объект больше не используется и разрушает сам себя. Два метода интерфейса IUnknown AddRef и Release обрабатывают подсчет ссылок для COM-объекта.

QueryInterface

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

  1. Объекты должны быть идентичны - вызов QueryInterface должен всегда возвращать одно и то же значение.
  2. Набор интерфейсов объекта не должен меняться - например, если вызов QueryInterface с неким IID был однажды успешен, то он должен быть успешным всегда. Таким же образом, если вызов однажды не удался, то не должен удасться в другой раз.
  3. Должно быть возможно проверить наличие одного интерфейса объекта из другого интерфейса.

QueryInterface возвращает указатель на указанный интерфейс объекта, указатель на интерфейс которого уже есть у клиента. Эта функция должна вызывать метод AddRef указателя, который она возвращает.

Вот описание аргументов QueryInterface:

     pif  : [in] указатель на вызывающий интерфейс
     riid : [in] указатель на IID интерфейса, который запрашивается
     ppv  : [out] указатель на указатель на интерфейс, который запрашивается.
            Если интерфейс не поддерживается, значение переменной будет
            приравнено 0.

QueryInterface возвращает следующее:

    S_OK, если интерфейс поддерживается
    E_NOINTERFACE, если не подерживается

Вот простая ассемблерная реализация QueryInterface:

 IInterface@QueryInterface proc uses ebx pif:DWORD, riid:DWORD, ppv:DWORD
     ; Следующий код сравнивает затребованный IID с доступными. Так как
     ; интерфейс IInterface наследуется от IUnknown, эти два интерфейса
     ; разделяют один и тот же указатель на интерфейс.
     invoke  IsEqualGUID, [riid], addr IID_IInterface
     or      eax,eax
     jnz     @1
     invoke  IsEqualGUID, [riid], addr IID_IUnknown
     or      eax,eax
     jnz     @1
     jmp     @NoInterface

 @1:
     ; GETOBJECTPOINTER - это макрос, который поместит указатель на объект
     ; в eax, если дано имя объекта, имя интерфейса и указатель на интерфейс.
     GETOBJECTPOINTER    Object, interface, pif

     ; теперь получаем указатель на затребованный интерфейс
     lea     eax, (Object ptr [eax]).interface

     ; заполняем *ppv указателем на интерфейс
     mov     ebx, [ppv]
     mov     dword ptr [ebx], eax

     ; повышаем значение счетчика ссылок, вызывая AddRef
     GETOBJECTPOINTER    Object, interface, pif
     mov     eax, (Object ptr [eax]).interface
     invoke  (IInterfaceVtbl ptr [eax]).AddRef, pif

     ; возвpащаем S_OK
     mov     eax, S_OK
     jmp     return

 @NoInterface:
     ; интерфейс не поддерживается, поэтому заполняем *ppv нулем
     mov     eax, [ppv]
     mov     dword ptr [eax], 0

     ; return E_NOINTERFACE
     mov     eax, E_NOINTERFACE

 return:
     ret
 IInterface@QueryInterface endp

AddRef

Метод AddRef используется для повышения значения счетчика ссылок для интерфейса объекта. Он должен вызываться для каждой новой копии указателя на интерфейс объекта.

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

Далее идет простая реализация метода AddRef:

 IInterface@AddRef proc pif:DWORD
     GETOBJECTPOINTER    Object, interface, pif
     ; увеличиваем значение счетчика ссылок
     inc     [(Object ptr [eax]).nRefCount]
     ; теперь вовращяем значение ссылок
     mov     eax, [(Object ptr [eax]).nRefCount]
     ret
 IInterface@AddRef endp

Release

Release понижает значение счетчика ссылок вызывающего интерфейса объекта. Если значение счетчика объекта снижается до 0, то объект выгружается из памяти. Эта функция должна вызываться, когда в указателе на интерфейс больше нет надобности.

Как и AddRef, Release пpинимает только один аpгумент - указатель на интеpфейс. Он также возвpащает текущее значение счетчика ссылок, котоpый также следует использовать только для тестиpования.

Вот простая реализация Release:

 IInterface@Release proc pif:DWORD
     GETOBJECTPOINTER    Object, interface, pif

     ; decrement the reference count
     ; понижаем значение счетчика ссылок
     dec     [(Object ptr [eax]).nRefCount]

     ; проверяем, равно ли значение счетчика ссылок нулю. Если так, то
     ; выгружаем объект
     mov     eax, [(Object ptr [eax]).nRefCount]
     or      eax, eax
     jnz     @1

     ; освобождаем объект: здесь мы предполагаем, что объект был
     ; зарезервирован функцией LocalAlloc и с опцией LMEM_FIXED
     GETOBJECTPOINTER    Object, interface, pif
     invoke  LocalFree, eax
 @1:
     ret
 IInterface@Release endp

Создание COM-объекта

Создание объекта состоит из резервирования памяти для объекта, а затем инициализации его данных. Как правило, инициализируется указатель на vtable и обнуляется счетчик ссылок. Затем можно вызывать QueryInterface, чтобы получить указатель на интерфейс.

Есть и другие методы для создания объектов, такие как CoCreateInstance и использование фабрик классов. Эти методы не будут обсуждаться в данной статье.

Пример реализации COM-объекта

Здесь приводится простая реализация COM-объекта. Демонстрируется, как создать объект, вызвать его методы, а затем освободить их. Вероятно, будет довольно познавательно скомпилировать данный пример и пройтись по нему отладчиком.

 .386
 .model flat,stdcall

 include windows.inc
 include kernel32.inc
 include user32.inc

 includelib kernel32.lib
 includelib user32.lib
 includelib uuid.lib

 ;--------------------------------------------------------------------------

 ; Borrowed from Ewald, http://here.is/diamond/
 ; Макрос для упрощения объявлений интерфейсов
 STDMETHOD   MACRO   name, argl :VARARG
 LOCAL @tmp_a
 LOCAL @tmp_b
 @tmp_a  TYPEDEF PROTO argl
 @tmp_b  TYPEDEF PTR @tmp_a
 name    @tmp_b      ?
 ENDM

 ; Макрос, который получает указатель на интерфейс и возвращает указатель
 ; на реализацию в eax
 GETOBJECTPOINTER MACRO Object, Interface, pif
     mov     eax, pif
     IF (Object.Interface)
         sub     eax, Object.Interface
     ENDIF
 ENDM

 ;--------------------------------------------------------------------------

 IInterface@QueryInterface   proto :DWORD, :DWORD, :DWORD
 IInterface@AddRef           proto :DWORD
 IInterface@Release          proto :DWORD
 IInterface@Get              proto :DWORD
 IInterface@Set              proto :DWORD, :DWORD

 CreateObject                proto :DWORD
 IsEqualGUID                 proto :DWORD, :DWORD

 externdef                   IID_IUnknown:GUID

 ;--------------------------------------------------------------------------

 ; объявляем прототип интерфейса
 IInterface struct
     lpVtbl  dd  ?
 IInterface ends

 IInterfaceVtbl struct
     ; методы IUnknown
     STDMETHOD       QueryInterface, pif:DWORD, riid:DWORD, ppv:DWORD
     STDMETHOD       AddRef, pif:DWORD
     STDMETHOD       Release, pif:DWORD
     ; методы IInterface
     STDMETHOD       GetValue, pif:DWORD
     STDMETHOD       SetValue, pif:DWORD, val:DWORD
 IInterfaceVtbl ends

 ; объявляем структуру объекта
 Object struct
     ; интерфейс объекта
     interface   IInterface  <?>

     ; данные объекта
     nRefCount   dd          ?
     nValue      dd          ?
 Object ends

 ;--------------------------------------------------------------------------

 .data
 ; define the vtable
 ; определяем vtable
 @@IInterface segment dword
 vtblIInterface:
     dd      offset IInterface@QueryInterface
     dd      offset IInterface@AddRef
     dd      offset IInterface@Release
     dd      offset IInterface@GetValue
     dd      offset IInterface@SetValue
 @@IInterface ends

 ; определяем IID интерфейса
 ; {CF2504E0-4F89-11d3-9AC3-0000E82C0301}
 IID_IInterface GUID <0cf2504e0h, 04f89h, 011d3h, <09ah, 0c3h, 00h, 00h, \
                      0e8h, 02ch, 03h, 01h>>

 ;--------------------------------------------------------------------------

 .code
 start:
 StartProc proc
     LOCAL   pif:DWORD       ; указатель на интерфейс

     ; вызываем метод SetValue
     mov     eax, [pif]
     mov     eax, [eax]
     invoke  (IInterfaceVtbl ptr [eax]).SetValue, [pif], 12345h

     ; вызываем метод GetValue
     mov     eax, [pif]
     mov     eax, [eax]
     invoke  (IInterfaceVtbl ptr [eax]).GetValue, [pif]

     ; освобождаем объект
     mov     eax, [pif]
     mov     eax, [eax]
     invoke  (IInterfaceVtbl ptr [eax]).Release, [pif]

     ret
 StartProc endp

 ;--------------------------------------------------------------------------

 IInterface@QueryInterface proc uses ebx pif:DWORD, riid:DWORD, ppv:DWORD
     invoke  IsEqualGUID, [riid], addr IID_IInterface
     test    eax,eax
     jnz     @F
     invoke  IsEqualGUID, [riid], addr IID_IUnknown
     test    eax,eax
     jnz     @F
     jmp     @Error

 @@:
     GETOBJECTPOINTER    Object, interface, pif
     lea     eax, (Object ptr [eax]).interface

     ; устанавливаем значение *ppv
     mov     ebx, [ppv]
     mov     dword ptr [ebx], eax

     ; увеличиваем значение счетчика ссылок
     GETOBJECTPOINTER    Object, interface, pif
     mov     eax, (Object ptr [eax]).interface
     invoke  (IInterfaceVtbl ptr [eax]).AddRef, [pif]

     ; возвpащаем S_OK
     mov     eax, S_OK
     jmp     return

 @Error:
     ; ошибка, интерфейс не поддерживается
     mov     eax, [ppv]
     mov     dword ptr [eax], 0
     mov     eax, E_NOINTERFACE

 return:
     ret
 IInterface@QueryInterface endp


 IInterface@AddRef proc pif:DWORD
     GETOBJECTPOINTER    Object, interface, pif
     inc     [(Object ptr [eax]).nRefCount]
     mov     eax, [(Object ptr [eax]).nRefCount]
     ret
 IInterface@AddRef endp


 IInterface@Release proc pif:DWORD
     GETOBJECTPOINTER    Object, interface, pif
     dec     [(Object ptr [eax]).nRefCount]
     mov     eax, [(Object ptr [eax]).nRefCount]
     or      eax, eax
     jnz     @1
     ; free object
     mov     eax, [pif]
     mov     eax, [eax]
     invoke  LocalFree, eax
 @1:
     ret
 IInterface@Release endp


 IInterface@GetValue proc pif:DWORD
     GETOBJECTPOINTER    Object, interface, pif
     mov     eax, (Object ptr [eax]).nValue
     ret
 IInterface@GetValue endp


 IInterface@SetValue proc uses ebx pif:DWORD, val:DWORD
     GETOBJECTPOINTER    Object, interface, pif
     mov     ebx, eax
     mov     eax, [val]
     mov     (Object ptr [ebx]).nValue, eax
     ret
 IInterface@SetValue endp

 ;--------------------------------------------------------------------------

 CreateObject proc uses ebx ecx pobj:DWORD
     ; set *ppv to 0
     mov     eax, pobj
     mov     dword ptr [eax], 0

     ; pезеpвиpуем объект
     invoke  LocalAlloc, LMEM_FIXED, sizeof Object
     or      eax, eax
     jnz     @1
     ; alloc failed, so return
     mov     eax, E_OUTOFMEMORY
     jmp     return
 @1:

     mov     ebx, eax
     mov     (Object ptr [ebx]).interface.lpVtbl, offset vtblIInterface
     mov     (Object ptr [ebx]).nRefCount, 0
     mov     (Object ptr [ebx]).nValue, 0

     ; Запpашиваем интеpфейс
     lea     ecx, (Object ptr [ebx]).interface
     mov     eax, (Object ptr [ebx]).interface.lpVtbl
     invoke  (IInterfaceVtbl ptr [eax]).QueryInterface,
             ecx,
             addr IID_IInterface,
             [pobj]
     cmp     eax, S_OK
     je      return

     ; ошибка в QueryInterface, поэтому освобождаем память
     push    eax
     invoke  LocalFree, ebx
     pop     eax

 return:
     ret
 CreateObject endp

 ;--------------------------------------------------------------------------

 IsEqualGUID proc rguid1:DWORD, rguid2:DWORD
     cld
     mov     esi, [rguid1]
     mov     edi, [rguid2]
     mov     ecx, sizeof GUID / 4
     repe    cmpsd
     xor     eax, eax
     or      ecx, ecx
     setz    eax
     ret
 IsEqualGUID endp

 end start

Заключение

Мы увидели (надеюсь), как реализовать COM-объект. Мы можем видеть, что это связанно с определенными трудностями и увеличивает избыточность кода нашей программы. Тем не менее, это может добавить гибкость и силу в наши программы. Подробности по данной теме вы можете найти на моей маленькой странице, посвященной COM: http://lordlucifer.cjb.net.

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

2002-2013 (c) wasm.ru