COM 学习
偏实用,简要介绍 COM,并给出使用 COM 的例子
概述
概述部分,介绍 COM 的一些基础知识和相关的 C/C++ 实现,实用部分可从Windows 中的 COM 看起。
COM(Component Object Model,组件对象模型),是一种软件组件之间交互的规范,不依赖于特定的语言,但受 C++ 影响较大;也不依赖于特定平台,但目前仅在 Windows 中应用比较广泛。COM 是二进制级别的规范。
COM 中引入了面向对象的思想,COM 对象通过COM 接口提供服务,COM 组件为 COM 对象提供活动空间(以下简称为组件、对象、接口)。三者关系为:一个组件可以包含多个对象,一个对象包含多个接口。多数情况下,一个组件只包含一个对象。
COM 使用 C/S 模型,客户端调用组件功能的顺序如下:
- 创建或获取一个对象
- 通过该对象实现的接口调用服务
- 不再需要该对象时释放对象
组件、对象与接口
组件在 Windows 中是一个 EXE 或者 DLL 文件。COM 组件对于用户来说是透明的。
类似于 C++,COM 对象实际上是某个 class 的实例,class 则是一组相关的数据和功能的定义。每个对象由一个 128 位的 GUID(Globally Unique Identifier)标识,称为 CLSID。客户端通过 CLSID 创建对象。
C++ 中没有接口的概念,接口是一组逻辑上相关的函数,这些函数通常被称为接口成员函数。每个接口也由 GUID 标识,称为 IID。
COM 库
COM 库是 COM 规范的实现,在 Windows 下以 dll 文件形式存在,包括:
- 部分 API 函数,实现了客户和服务器端 COM 应用的创建过程。包括客户端的创建函数和服务器端对对象的访问支持。
- COM 通过注册表查找本地服务器(EXE 程序),以及程序名与 CLSID 之间的转换等。
- 提供标准的内存控制方法,使应用控制进程中的内存分配。
接口定义
客户端使用指向接口数据结构的指针来调用接口的成员函数,而这个接口指针实际指向另一个指针(pVtable),第二个指针(pVtable)指向一组函数,被称为接口函数表(也成为虚函数表 vtable)。对于一个接口来说,它的虚函数表 vtable 是确定的,因此成员函数的个数和先后顺序是不变的;成员函数的参数和返回值也是不变的。以上所有信息都需要在二进制级别确定,即使使用其他编程语言,只要能够支持这样的内存结构描述,就可以定义接口。
例如一个 IDictionary 字典接口,它的 C 语言描述如下:
1 | struct IDictionaryVtbl; |
几点说明:
- 接口成员函数第一个参数必须为指向 IDictonary 的指针,因为接口必定存在于某个 COM 对象上,该指针提供了对象实例的信息,从而得知是对哪个 COM 对象操作。在此例子中,就是可能存在多本字典,this 指针指明了对哪个字典对象进行操作。
- 接口成员函数中,字符串变量必须使用 Unicode 字符指针,这是 COM 规范的要求。
- 接口成员函数应该使用相同的调用规范。Windows 平台常用的调用规范为
_cdecl
和_stdcall
(也称为pascal
)。由于大多数语言和系统 API 都使用_stdcall
,所以 COM 规范和 COM API 都采用了_stdcall
。 - 以上定义中没有给出具体实现,对于客户端来说只需要使用这样的定义即可,对于组件程序来说,则必须提供具体实现。
接口需要具有不变性,如果需要修改现有功能或提供新的功能,则应创建新的接口。
如果客户端需要使用一个 COM 对象的某个接口,则必须知道接口的 IID 和接口能够提供的方法(接口成员函数)。
前面提到 COM 受 C++ 影响较大,使用 C++ 描述接口十分容易:
1 | class IDictionary { |
这与 C 语言描述效果一致。这里使用了纯虚函数,因为接口知识一种描述,并不提供具体实现。
客户端调用时,当获得了某个接口指针后,就可以调用接口成员函数,如:
1 | // C++ |
接口描述语言(IDL)是一种不依赖于具体语言的接口描述方法,此处略过。
由此可知,
对象实现
对象往往有自己的属性数据,这些数据表示了对象的状态,能够区分对象。比如两本字典之间的不同可以通过字典名和字典数据不同,使用 C++ 的类定义字典对象:
1 | class CDictionary: public IDictionary { |
如果一个客户端使用了两个字典对象,则两个字典对象公用了成员函数,但是数据是没有公用的,此时内存结构如下:
如果第二个字典组件对象没有使用 CDictionary 类实现字典功能,但也实现了 IDictionary 接口,则内存结构如下:
以上模型中,每个接口成员函数都包含一个 this 指针参数,通过该参数可以访问到对应对象的数据。
IUnkown 接口
所有接口必须继承 IUnkown 接口,它提供了两个重要特性:生存期管理和接口查询。C++ 定义如下:
1 | class IUnknown { |
引用计数
AddRef 和 Release 用于对引用计数操作,实现引用计数可以在三个级别上:
- 组件级别,使用全局变量
- 对象级别,使用类的成员变量
- 接口级别,为每一个成员函数使用一个成员变量
下面是引入了 IUnknown 和对象界别的引用计数的例子(C++):
1 | class IDictionary: public IUnkown { |
接口查询
上面提到,一个 COM 对象可以实现多个接口,客户程序可以对 COM 对象的接口进行询问,是否可以提供某个接口对应的一系列服务,这就是 QueryInterface 实现的功能。
1 | HRESULT QueryInterface([in] REFIID iid, [out] void ** ppv); |
iid 为希望得到的接口 IID,ppv 为查询得到的结果接口指针,失败则为 NULL。
当客户创建 COM 对象后,创建函数总会为我们返回一个接口指针。由于接口都继承自 IUnkown ,所以所有接口都有 QueryInterface 函数,所以可以通过这个最初的接口指针获取到该对象支持的任意接口指针。
Windows 中的 COM
之前提到,COM 在 Windows 中有广泛应该用,讨论 Windows 上的 COM 其实是在讨论 Windows 对于 COM 规范的实现,其中使用了许多 Windows 平台的特性,比如注册表和 DLL 等。
组件可以以 DLL 或者 EXE 的形式实现,当以 DLL 形式实现时,由于客户程序调用组件服务时,会把组件程序加载到自己的内存空间中,所以称为进程内组件;以 EXE 形式实现时,则成为进程外组件。实现方式对于用户程序来说是透明的,此处不展开。
Windows 系统上,COM 库所需的注册信息都存放在注册表中,路径为HKEY_CLASSES_ROOT\CLSID\[CLSID]
,进程内组件内部包含InprocServe32 子健,包含组件程序的全路径;进程外则是 LocalServer32 子健。除此之外,还可能包含其他信息。除了使用 CLSID 可以表示 COM 对象以外,还可以使用字符串为对象命名,这样的名字信息称为 ProgID,直接存放在HKEY_CLASSES_ROOT
键下,可以查找CLSID
子键获取 CLSID。
注册表中存在的 COM 对象可以直接通过 CLSID 来查找和使用,
实际应用
枚举
通过以下 Powershell 遍历所有 COM 组件和导出的方法
1 | New-PSDrive -PSProvider registry -Root HKEY_CLASSES_ROOT -Name HKCR |
利用
例如,Windows 中存在一个 CLSID 为 E430E93D-09A9-4DC5-80E3-CBB2FB9AF28E
的类,其注册表路径为HKEY_CLASSES_ROOT\CLSID\{E430E93D-09A9-4DC5-80E3-CBB2FB9AF28E}
,它的组件位置为C:\Program Files (x86)\Windows Kits\10\App Certification Kit\prchauto.dll
,其中包含名为ProcessChain
的类,包含一个名为CommandLine
的属性和一个Start
方法。
使用 Powershell 调用比较简单:
1 | $handle = [activator]::CreateInstance([type]::GetTypeFromCLSID("E430E93D-09A9-4DC5-80E3-CBB2FB9AF28E")) |
其中最后一行使用[ref$false]
也可以成功执行。
接下来使用 C++ 实现,首先使用 OleViewer 打开 prchauto.dll,选择 Save as,保存prchauto.IDL。打开 Visual Studio Developer Command Prompt,执行:
1 | midl /h processchain.h ./prchauto.IDL |
会在执行目录生成prchauto.tlb
,prchauto_i.c
,和processchain.h
文件,把processchain.h
文件加入工程,此时可以使用 IProcessChain 的 put_CommandLine
设置 CommandLine,使用Start
启动,以下是启动计算器的例子(来自COM组件相关的武器化开发技术 - idiotc4t’s blog)
1 |
|
成功弹出计算器。