COM в Ассемблере

ОГЛАВЛЕНИЕ

О COM

Это краткое введение в основы COM.

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

По терминологии COM, интерфейс - это "контракт", состоящий из группы связанных друг с другом прототипов функций, чье использование определено, а реализация - нет. Определение интерфейса задает функции интерфейса, называемые методами, типы возвращаемых ими значений, количество и типы их параметров, и что они должны делать. С интерфейсом не ассоциируется какая-то конкретная его реализация. Реализация интерфейса - это код, который предоставляет программист для выполнения действий, заданных определением интерфейса.

Экземпляр реализации интерфейса - это указатель на массив указателей на методы (таблица указателей, ссылающиеся на реализацию всех методов, указанных в интерфейсе). Любой код, у которого есть подобный указатель, может вызывать методы этого интерфейса.


 

Использование COM-объекта в Ассемблере

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

Здесь приводится пример интерфейса на C++, и как называются его методы:

    interface IInterface
    {
              HRESULT QueryInterface( REFIID iid, void ** ppvObject );
              ULONG AddRef();
              ULONG Release();
              Function1( INT param1, INT param2);
              Function2( INT param1 );
    }

    // вызываем метод Function1
    pObject->Function1( 0, 0);

То же самое можно реализовать на ассемблере следующим образом:

    ; определяем интерфейс
         ; каждое из этих значенией - это смещение в vtable
         QueryInterface          equ             0h
         AddRef                  equ             4h
    Release          equ   8h
    Function1    equ    0Ch
    Function2    equ    10h

    ; вызываем метод Function1 на ассемблере
         ; вызываем метод, получая адрес таблицы виртуальных функций,
         ; а затем вызываем функцию через указатель, находящийся по
         ; нужному смещению в таблице
    push   param2
    push   param1
         mov     eax, pObject
    push   eax
         mov     eax, [eax]
    call   [eax + Function1]

Вы можете видеть, что это отличается от вызова обычной функции. Здесь pObject указывает на таблицу интерфейса. По смещению Function1 (0Ch) в этой таблице находится указатель на саму функцию, которую мы хотим вызвать.

 
 

Использование HRESULT

Возвращаемое значение функциями OLE API и методами является HRESULT. Это не хэндл чег-нибудь, а просто 32-х битное значение с несколькими полями. Части HRESULT показаны ниже.

HRESULT - это 32-х битное значение со следующей структурой.

  3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
  1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
 +-+-+-+-+-+---------------------+-------------------------------+
 |S|R|C|N|r|    Facility         |               Code            |
 +-+-+-+-+-+---------------------+-------------------------------+

  S - Severity Bit
      Используется для того, чтобы сообщить, была ли функция выполнена
      успешно или нет.

      0 - Успех
      1 - Провал

      Так как этот бит фактически является битом знака 32-х битного значения,
      проверить, успешно была выполнена функция или нет, можно просто
      проверив его знак:

      call       ComFunction        ; вызываем функцию
      test       eax,eax            ; теперь проверяем возвращенное значение
      js         error              ; делаем переход, если установлен бит
                                    ; знака (произошла ошибка)
      ; успех, продолжаем выполнение программы

  R - зарезервированная часть кода facility.

  C - зарезервированная часть кода facility.

  N - зарезервированная часть кода facility.

  r - зарезервированная часть кода facility

  Facility - это код facility

      FACILITY_WINDOWS    = 8
      FACILITY_STORAGE    = 3
      FACILITY_RPC        = 1
      FACILITY_WIN32      = 7
      FACILITY_CONTROL    = 10
      FACILITY_NULL       = 0
      FACILITY_ITF        = 4
      FACILITY_DISPATCH   = 2

      Чтобы получить этот код:

      call       ComFunction    ; вызываем функцию
      shr        eax, 16        ; сдвигаем HRESULT вправо на 16 бит
      and        eax, 1FFFh     ; маскируем биты так, что остается только
                                ; код facility
      ; теперь eax содержит HRESULT'овский код facility

  Code - код статуса facility

      Чтобы получить код статуса facility
      call       ComFunction             ; вызываем функцию
      and  eax, 0000FFFFh     ; обнуляем верхние 16 бит
      ; теперь eax содержит the HRESULT'овский код статуса facility


 

Использование COM в MASM

Если вы используете MASM для ассемблирования ваших программ, вы можете использовать некоторые из его возможностей, чтобы сделать вызов COM-функций очень простым. Используя invoke, вы можете сделать COM-вызовы почти такими же понятными как вызовы обычных функций, плюс вы можете добавить проверку типов для каждой функции.

Определение интерфейса:

      IInterface_Function1Proto     typedef proto :DWORD
      IInterface_Function2Proto     typedef proto :DWORD, :DWORD

      IInterface_Function1          typedef ptr IInterface_Function1Proto
      IInterface_Function2          typedef ptr IInterface_Function2Proto

      IInterface struct DWORD
            QueryInterface          IUnknown_QueryInterface         ?
            AddRef                  IUnknown_AddRef                 ?
            Release                 IUnknown_Release                ?
            Function1               IInterface_Function1            ?
            Function2               Interface_Function2             ?
      IInterface ends

Использование интерфейса для вызова COM-функций:

      mov     eax, pObject
      mov     eax, [eax]
      invoke  (IInterface [eax]).Function1, 0, 0

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


 

Программа-пример с использованием COM

Вот исходник, который написан так, чтобы быть максимально совместимым с любым ассемблером, который вы предпочтете (по крайней мере, чтобы вам не пришлось делать глобальных изменений).

Эта программа использует интерфейсы Windows Shell, чтобы отобразить содержиом Рабочего стола. Программа не закончена, но она показывает, как инициализровать библиотеку COM, деинициализировать и использовать. Я также покажу, как использовать shell-библиотека, чтобы получить папки и объекты, и выполнять над ними различные действия

 .386
 .model flat, stdcall

 include windows.inc    ; подключает стандартных заголовочный файл
 include shlobj.inc     ; этот заголовочный файл содержит константы и
                        ; определения shell'а

 ;----------------------------------------------------------
 .data
         wMsg                    MSG     <?>
         g_hInstance             dd      ?
    g_pShellMalloc      dd ?

         pshf                    dd      ?       ; объект папки shell'а
         peidl                   dd      ?       ; объект списка id

         lvi                     LV_ITEM <?>
         iCount                  dd      ?
         strret                  STRRET  <?gt;
         shfi                    SHFILEINFO <?>
    ...

 ;----------------------------------------------------------
 .code
 ; Entry Point
 start:
     push    0h
     call    GetModuleHandle
     mov     g_hInstance,eax

     call    InitCommonControls

 ; Инициализируем библиотеку Component Object Model (COM)
     push    0
     call    CoInitialize
     test    eax,eax             ; ошибка, если MSB = 1
                                 ; (MSB = бит знака)
     js      exit                ; js = переход, если установлен бит знака

 ; Получаем указатель на объект shell'а IMalloc и сохраняем его в глобальную
 ; переменную
     push    offset g_pShellMalloc
     call    SHGetMalloc
     cmp     eax, E_FAIL
     jz      shutdown
 ; Здесь мы должны создать окна, list view, цикл обработки сообщений и так
 ; далее...
 ; ....

 ; Очищение
 ; Освобождаем указатель на объект IMalloc
     mov     eax, g_pShellMalloc
     push    eax
     mov     eax, [eax]
     call    [eax + Release]         ; g_pShellMalloc->Release();

 shutdown:
 ; закрываем библиотеку COM
     call    CoUninitialize

 exit:
     push    wMsg.wParam
     call    ExitProcess
 ; Здесь программа прекращает свое выполнение


 ;----------------------------------------------------------
 FillListView proc

 ; получаем папку Рабочего стола, сохраняем в pshf
     push    offset pshf
     call    SHGetDesktopFolder

 ; получаем объекты в папке Рабочего стола, используя метода EnumObjects
 ; объекта папки Рабочего стола
     push    offset peidl
     push    SHCONTF_NONFOLDERS
     push    0
     mov     eax, pshf
     push    eax
     mov     eax, [eax]
     call    [eax + EnumObjects]

     xor     ebx, ebx ; используем ebx в качестве счетчика

 ; перебираем элементы списка id
 idlist_loop:
 ; Получаем следующий элемент списка
     push    0
     push    offset pidl
     push    1
     mov     eax, peidl
     push    eax
     mov     eax, [eax]
     call    [eax + Next]
     test    eax,eax
     jnz     idlist_endloop

     mov     lvi.imask, LVIF_TEXT or LVIF_IMAGE
     mov     lvi.iItem, ebx

 ; Получаем имя элемента, используя метод GetDisplayNameOf
     push    offset strret
     push    SHGDN_NORMAL
     push    offset pidl
     mov     eax, pshf
     push    eax
     mov     eax, [eax]
     call    [eax + GetDisplayNameOf]
 ; GetDisplayNameOf возвращает имя в одной из трех форм, поэтому выберите
 ; правильную форму и поступайте соответствующе
     cmp     strret.uType, STRRET_CSTR
     je      strret_cstr
     cmp     strret.uType, STRRET_OFFSET
     je      strret_offset

 strret_olestr:
     ; здесь вы можете использовать WideCharToMultiByte, чтобы получить
     ; строку, я оставляю это на вас, так как я ленив
     jmp     strret_end

 strret_cstr:
     lea     eax, strret.cStr
     jmp     strret_end

 strret_offset:
     mov     eax, pidl
     add     eax, strret.uOffset

 strret_end:
     mov     lvi.pszText, eax

 ; Получаем иконки элементов
     push    SHGFI_PIDL or SHGFI_SYSICONINDEX or SHGFI_SMALLICON or
SHGFI_ICON
     push    sizeof SHFILEINFO
     push    offset shfi
     push    0
     push    pidl
     call    SHGetFileInfo
     mov     eax, shfi.iIcon
     mov     lvi.iImage, eax

 ; теперь добавляем элементы в список
     push    offset lvi
     push    0
     push    LVM_INSERTITEM
     push    hWndListView
     call    SendMessage

 ; увеличиваем значение счетчика ebx и делаем еще один повтор цикла
     inc     ebx, ebx
     jmp     idlist_loop

 idlist_endloop:

 ; теперь освобождаем список id
 ; Помните, что все зарезервированные объекты должны быть освобождены
     mov     eax, peidl
     push    eax
     mov     eax,[eax]
     call    [eax + Release]

 ; освобождаем объект папки Рабочего стола
     mov     eax, pshf
     push    eax
     mov     eax,[eax]
     call    [eax + Release]

     ret
 FillListView endp

 END start 

Заключение

Хорошо, вот и все об использовании COM при программировании на ассемблере. Вероятно, что моя следующая статья расскажет о том, как определить собственные интерфейсы. Как вы можете видеть, использование COM вовсе не сложно, и с его помощью вы можете добавить очень мощные возможности в вашу программу, написанную на ассемблере.