关键标识符
CLSID (Class ID)
CLSID是一个128位GUID,唯一标识一个COM对象类
|
1 2 3 4 5 6 7 8 |
// 比如 Shell.Application 的 CLSID: CLSID_ShellApplication = {13709620-C279-11CE-A49E-444553540000} // GUID // CLSID 通常存在注册表: HKEY_CLASSES_ROOT\CLSID\{13709620-C279-11CE-A49E-444553540000} └─ InprocServer32 = "shell32.dll" (哪个 DLL 实现的) └─ ProgID = "Shell.Application" (人类可读的名字) |
ProgID (Programmatic ID)
ProgID是一个字符串,人类可读的类标识
|
1 2 3 4 5 6 7 8 |
// 比如 "Shell.Application" // ProgID 也存在注册表: HKEY_CLASSES_ROOT\Shell.Application └─ CLSID = {13709620-C279-11CE-A49E-444553540000} // ProgID 和 CLSID 是相互映射的: ProgID ──→ CLSID ──→ DLL 路径 ──→ 加载 DLL ──→ 创建对象 |
IID (Interface ID)
IID也是128位GUID,唯一标识一个接口
|
1 2 3 4 5 6 |
// 比如: IID_IDispatch = {00020400-0000-0000-C000-000000000046} IID_IShellDispatch = {D8F015C0-C278-11CE-A49E-444553540000} // 一个对象可以实现多个接口 // 一个对象的虚表有多个,每个接口对应一个虚表 |
关系图
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
注册表 ├─ HKCR\Shell.Application (ProgID) │ └─ CLSID = {13709620...} │ ├─ HKCR\CLSID\{13709620...} (CLSID) │ └─ InprocServer32 = shell32.dll │ └─ ProgID = Shell.Application │ └─ HKCR\Interface\{D8F015C0...} (IID) └─ ProxyStubClsid = ... 运行时: CLSIDFromProgID("Shell.Application") → {13709620...} CoCreateInstance({13709620...}, nullptr, CLSCTX_INPROC_SERVER, IID_IDispatch, &pDisp) → 加载 shell32.dll → 创建 CShellDispatch 对象 → 返回指向 IDispatch 虚表的指针 |
COM 对象在内存中的布局
对象实例的物理布局
- 内存地址空间中一个
COM对象的真实样子
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
┌─────────────────────────────────────┐ │ 第一个虚表指针 (IUnknown vtable) │ ← 对象地址指向这里 │ (8 字节,x64) │ ├─────────────────────────────────────┤ │ 第二个虚表指针 (IDispatch vtable) │ ← 如果对象支持多接口 │ (8 字节,x64) │ ├─────────────────────────────────────┤ │ 第三个虚表指针 (自定义接口 vtable) │ ← 如果需要 │ (8 字节,x64) │ ├─────────────────────────────────────┤ │ 对象数据(成员变量): │ │ - 引用计数(ULONG) │ │ - 初始化状态(BOOL) │ │ - 其他内部数据... │ ├─────────────────────────────────────┤ │ 可能的隐藏数据: │ │ - 类型库信息 │ │ - 事件处理器列表 │ │ - ... │ └─────────────────────────────────────┘ |
虚表的物理布局
- 虚表是一个函数指针数组
- 每一项都是一个函数指针(
8字节,x64)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
虚表基址(比如 0x140050000): ┌───────────────────────────┐ │ [0x00] 0x140001000 │ ← QueryInterface 函数地址 ├───────────────────────────┤ │ [0x08] 0x140001200 │ ← AddRef 函数地址 ├───────────────────────────┤ │ [0x10] 0x140001300 │ ← Release 函数地址 ├───────────────────────────┤ │ [0x18] 0x140002000 │ ← GetTypeInfoCount 函数地址 ├───────────────────────────┤ │ [0x20] 0x140002100 │ ← GetTypeInfo 函数地址 ├───────────────────────────┤ │ [0x28] 0x140002200 │ ← GetIDsOfNames 函数地址 ├───────────────────────────┤ │ [0x30] 0x140002300 │ ← Invoke 函数地址 ├───────────────────────────┤ │ [0x38] 0x140003000 │ ← 接口特有方法 #1 ├───────────────────────────┤ │ [0x40] 0x140003100 │ ← 接口特有方法 #2 ├───────────────────────────┤ │ ... │ └───────────────────────────┘ |
一个具体的例子
- 假设
Shell.Application对象在0x7fff0000
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
0x7fff0000: 0x140050000 ← IDispatch 虚表指针 0x7fff0008: 0x140051000 ← IShellDispatch 虚表指针 0x7fff0010: 0x12345678 ← 引用计数 0x7fff0018: 0xdeadbeef ← 其他数据 ... 虚表 0x140050000 (IDispatch): 0x140050000: 0x140010100 ← QueryInterface 0x140050008: 0x140010200 ← AddRef 0x140050010: 0x140010300 ← Release 0x140050018: 0x140010400 ← GetTypeInfoCount 0x140050020: 0x140010500 ← GetTypeInfo 0x140050028: 0x140010600 ← GetIDsOfNames 0x140050030: 0x140010700 ← Invoke 虚表 0x140051000 (IShellDispatch): 0x140051000: 0x140010100 ← QueryInterface (同一个) 0x140051008: 0x140010200 ← AddRef (同一个) 0x140051010: 0x140010300 ← Release (同一个) 0x140051018: 0x140011000 ← NameSpace (Shell 特有) 0x140051020: 0x140011100 ← CreateShortcut (Shell 特有) ... |
关键点
- 对象地址 = 第一个虚表指针的地址
- 多接口对象有多个虚表指针,按顺序排列
IUnknown虚表指针总是第一个
理解
- 如果对象不支持多接口,怎么实现
- 如果对象支持多接口,怎么实现
- 如果对象支持多接口,多接口背后的原理
- 见下文
com编程相关
- 见下文
COM编程相关
概述
COM编程里有一个关键点要先说清楚:- 每个
COM对象都必须实现IUnknown(包含QueryInterface、AddRef、Release三个方法)
- 每个
- 所谓"支持单接口"还是"支持多接口",真正的区别就体现在
QueryInterface的实现上- 它决定了客户端能"问"出哪些接口
只支持单个接口
- 这种情况其实是支持两个
IID:IID_IUnknown和你自己定义的那一个接口 - 因为只有单继承,
static_cast<IHello*>(this)不存在任何歧义,this指针、IUnknown*、IHello*在内存上是同一个起始地址
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#include <unknwn.h> #include <cstdio> // 自定义接口 struct IHello : public IUnknown { virtual HRESULT STDMETHODCALLTYPE SayHello() = 0; }; // 假设已经用 DEFINE_GUID / __declspec(uuid) 定义好 IID_IHello class CHello : public IHello { public: CHello() : m_cRef(1) {} // ---- IUnknown ---- STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override { if (riid == IID_IUnknown || riid == __uuidof(IHello)) { *ppv = static_cast<IHello*>(this); // 只有一个分支需要转换 } else { *ppv = nullptr; return E_NOINTERFACE; } AddRef(); // 成功返回接口时必须 AddRef return S_OK; } STDMETHODIMP_(ULONG) AddRef() override { return InterlockedIncrement(&m_cRef); } STDMETHODIMP_(ULONG) Release() override { ULONG c = InterlockedDecrement(&m_cRef); if (c == 0) delete this; return c; } // ---- IHello ---- STDMETHODIMP SayHello() override { printf("Hello!\n"); return S_OK; } private: LONG m_cRef; }; |
支持多个接口(多重继承方式)
- 最常见的做法是让实现类多重继承多个接口
- 这里有几个单接口时不会遇到的坑,得注意
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
struct IHello : public IUnknown { virtual HRESULT STDMETHODCALLTYPE SayHello() = 0; }; struct IGoodbye : public IUnknown { virtual HRESULT STDMETHODCALLTYPE SayGoodbye() = 0; }; class CGreeter : public IHello, public IGoodbye { public: CGreeter() : m_cRef(1) {} STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override { if (riid == IID_IUnknown) { // 关键:不能直接 static_cast<IUnknown*>(this),会因二义性编译失败。 // 必须先转到某一个具体接口,固定地返回这一个指针。 *ppv = static_cast<IHello*>(this); } else if (riid == __uuidof(IHello)) { *ppv = static_cast<IHello*>(this); } else if (riid == __uuidof(IGoodbye)) { *ppv = static_cast<IGoodbye*>(this); // 注意:这是不同的指针地址 } else { *ppv = nullptr; return E_NOINTERFACE; } AddRef(); return S_OK; } // AddRef / Release 全对象共享一份引用计数 STDMETHODIMP_(ULONG) AddRef() override { return InterlockedIncrement(&m_cRef); } STDMETHODIMP_(ULONG) Release() override { ULONG c = InterlockedDecrement(&m_cRef); if (c == 0) delete this; return c; } STDMETHODIMP SayHello() override { printf("Hello!\n"); return S_OK; } STDMETHODIMP SayGoodbye() override { printf("Goodbye!\n"); return S_OK; } private: LONG m_cRef; }; |
- 多接口实现里必须记住的三条规则:
IID_IUnknown的二义性COM标识规则(identity)- 引用计数是全对象共享的
IID_IUnknown的二义性- 多重继承下,
IHello和IGoodbye各自带了一份IUnknown,直接static_cast<IUnknown*>(this)编译器无法决定走哪条路径 - 所以要先转到任意一个具体接口(上例固定选
IHello),再隐式当作IUnknown*返回
- 多重继承下,
COM标识规则(identity)- 对同一个对象,无论从哪个接口去查询
IID_IUnknown,返回的指针值必须完全相同 - 这就是为什么上面把
IID_IUnknown固定指向static_cast<IHello*>(this),绝不能一会儿用IHello*、一会儿用IGoodbye* - 客户端正是靠这个规则判断两个接口指针是不是同一个对象
- 对同一个对象,无论从哪个接口去查询
- 引用计数是全对象共享的
- 只有一个
m_cRef,不管客户端拿到的是IHello*还是IGoodbye*,AddRef/Release操作的都是同一个计数
- 只有一个
用 ATL 简化
- 如果不想手写
QueryInterface,可以用ATL的COM map,它会自动生成上面这套逻辑ATL内部会处理好IUnknown二义性、标识规则和引用计数,你只需要在map里列出支持哪些接口即可
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class CGreeter : public CComObjectRootEx<CComSingleThreadModel>, public IHello, public IGoodbye { public: BEGIN_COM_MAP(CGreeter) COM_INTERFACE_ENTRY(IHello) COM_INTERFACE_ENTRY(IGoodbye) END_COM_MAP() STDMETHODIMP SayHello() { printf("Hello!\n"); return S_OK; } STDMETHODIMP SayGoodbye() { printf("Goodbye!\n"); return S_OK; } }; |
嵌套类(Nested Class)多接口
- 核心思路
- 每个接口由一个独立的嵌套类实现,外层对象把它们作为成员持有
- 每个嵌套类保存一个指向外层对象的反向指针,自己的
IUnknown三方法全部委托(delegate)给外层对象 - 这样可以让每个接口的实现代码彼此隔离,避免外层类因多重继承而变得臃肿,也不会有
IUnknown二义性问题
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
#include <unknwn.h> #include <cstdio> struct IHello : public IUnknown { virtual HRESULT STDMETHODCALLTYPE SayHello() = 0; }; struct IGoodbye : public IUnknown { virtual HRESULT STDMETHODCALLTYPE SayGoodbye() = 0; }; class CGreeter { public: CGreeter() : m_cRef(1), m_hello(this), m_goodbye(this) {} // 外层对象的 IUnknown —— 真正持有引用计数和 QueryInterface 逻辑 STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { if (riid == IID_IUnknown) *ppv = &m_hello; // 固定返回一个,保证标识唯一 else if (riid == __uuidof(IHello)) *ppv = static_cast<IHello*>(&m_hello); else if (riid == __uuidof(IGoodbye)) *ppv = static_cast<IGoodbye*>(&m_goodbye); else { *ppv = nullptr; return E_NOINTERFACE; } AddRef(); return S_OK; } STDMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&m_cRef); } STDMETHODIMP_(ULONG) Release() { ULONG c = InterlockedDecrement(&m_cRef); if (c == 0) delete this; return c; } private: LONG m_cRef; // ---- 嵌套类:实现 IHello,IUnknown 委托给外层 ---- class CHelloImpl : public IHello { public: CHelloImpl(CGreeter* p) : m_pOuter(p) {} STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { return m_pOuter->QueryInterface(riid, ppv); } // 委托 STDMETHODIMP_(ULONG) AddRef() { return m_pOuter->AddRef(); } // 委托 STDMETHODIMP_(ULONG) Release() { return m_pOuter->Release(); } // 委托 STDMETHODIMP SayHello() { printf("Hello!\n"); return S_OK; } private: CGreeter* m_pOuter; } m_hello; // ---- 嵌套类:实现 IGoodbye ---- class CGoodbyeImpl : public IGoodbye { public: CGoodbyeImpl(CGreeter* p) : m_pOuter(p) {} STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { return m_pOuter->QueryInterface(riid, ppv); } STDMETHODIMP_(ULONG) AddRef() { return m_pOuter->AddRef(); } STDMETHODIMP_(ULONG) Release() { return m_pOuter->Release(); } STDMETHODIMP SayGoodbye() { printf("Goodbye!\n"); return S_OK; } private: CGreeter* m_pOuter; } m_goodbye; friend class CHelloImpl; friend class CGoodbyeImpl; }; |
分离式多接口(Tear-off Interface)
- 适用场景
- 某个接口很少被用到,如果让主对象一直背着它(多一份
vtable指针、多一组成员)很浪费
- 某个接口很少被用到,如果让主对象一直背着它(多一份
Tear-off的做法是- 主对象平时根本不实现这个接口,只有当客户端
QueryInterface来要的时候,才临时new一个小对象出来 - 当这个接口被
Release到0时,小对象自己销毁
- 主对象平时根本不实现这个接口,只有当客户端
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
struct IHello : public IUnknown { virtual HRESULT STDMETHODCALLTYPE SayHello() = 0; }; struct IRareFeature : public IUnknown { virtual HRESULT STDMETHODCALLTYPE DoRareThing() = 0; }; // 前置声明 class CGreeter; // ---- Tear-off 对象:按需创建,独立生命周期 ---- class CRareTearOff : public IRareFeature { public: CRareTearOff(CGreeter* pOuter); // 见下方实现 ~CRareTearOff(); STDMETHODIMP QueryInterface(REFIID riid, void** ppv); // 仍要能查回主对象 STDMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&m_cRef); } STDMETHODIMP_(ULONG) Release() { ULONG c = InterlockedDecrement(&m_cRef); if (c == 0) delete this; // 自己销毁,主对象不受影响 return c; } STDMETHODIMP DoRareThing() { printf("Rare thing done.\n"); return S_OK; } private: LONG m_cRef; CGreeter* m_pOuter; // 持有主对象引用,保证 tear-off 存活期间主对象不死 }; class CGreeter : public IHello { public: CGreeter() : m_cRef(1) {} STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { if (riid == IID_IUnknown || riid == __uuidof(IHello)) { *ppv = static_cast<IHello*>(this); AddRef(); return S_OK; } if (riid == __uuidof(IRareFeature)) { // 按需创建 tear-off。构造函数里它会 AddRef 主对象, // new 出来后引用计数为 1,直接返回给客户端。 CRareTearOff* p = new (std::nothrow) CRareTearOff(this); if (!p) return E_OUTOFMEMORY; *ppv = static_cast<IRareFeature*>(p); return S_OK; // 注意:这里不调用 this->AddRef,计数已在构造里处理 } *ppv = nullptr; return E_NOINTERFACE; } STDMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&m_cRef); } STDMETHODIMP_(ULONG) Release() { ULONG c = InterlockedDecrement(&m_cRef); if (c == 0) delete this; return c; } STDMETHODIMP SayHello() { printf("Hello!\n"); return S_OK; } private: LONG m_cRef; }; // ---- tear-off 的实现 ---- CRareTearOff::CRareTearOff(CGreeter* pOuter) : m_cRef(1), m_pOuter(pOuter) { m_pOuter->AddRef(); // 钉住主对象,防止它先于 tear-off 死亡 } CRareTearOff::~CRareTearOff() { m_pOuter->Release(); // 释放对主对象的引用 } STDMETHODIMP CRareTearOff::QueryInterface(REFIID riid, void** ppv) { // 从 tear-off 还能查回主对象的其它接口,委托给主对象即可 return m_pOuter->QueryInterface(riid, ppv); } |
虚表调用的底层机制
调用方式 1:C++ 方式
|
1 2 3 4 5 6 7 8 |
IDispatch* pDisp = ...; pDisp->Invoke(...); // 编译后的机器码(伪代码): mov rax, [pDisp] // RAX = 虚表指针 mov rcx, pDisp // RCX = this(对象地址) mov rdx, [rax + 0x30] // RDX = Invoke 函数地址 call [rax + 0x30] // 调用 Invoke |
调用方式 2:直接虚表偏移访问
|
1 2 3 4 5 6 7 8 9 |
// 等价代码: typedef HRESULT (*InvokeFunc)(void*, DISPID, REFIID, LCID, WORD, DISPPARAMS*, VARIANT*, EXCEPINFO*, UINT*); void** ppVtable = (void**)pDisp; // 获得虚表指针 void* pVtable = *ppVtable; // 解引用,得到虚表地址 InvokeFunc pfnInvoke = (InvokeFunc)((void**)pVtable)[6]; // 虚表[6] pfnInvoke(pDisp, dispId, &IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &dispParams, &varResult, nullptr, nullptr); |
为什么是这样设计
- 性能
- 虚表调用只需一次内存读取(获得虚表)
- 然后一次函数指针跳转
- 比动态查表快
- 兼容性
- 所有
COM对象都遵循虚表约定 - 接口可以被各种语言使用
(C++, VB, C#, JavaScript)
- 所有
- 扩展性
- 新增方法只需在虚表末尾添加新条目
- 现有代码不需要改动
ABI兼容性
IDispatch 双接口详解
什么是双接口
- "双接口" =
Dual Interface= 同时支持两种调用方式
|
1 2 3 4 5 6 7 8 |
一个对象实现 IDispatch: ├─ 方式 1:通过 IDispatch::Invoke (动态,慢) │ 优点:支持脚本语言、运行时反射 │ 缺点:性能差、类型不安全 │ └─ 方式 2:通过强类型接口虚表 (静态,快) 优点:快速、编译时类型检查 缺点:需要编译时知道接口定义 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// "双接口" = IDispatch + 强类型接口 // 方式 1:使用 IDispatch(动态,但慢) IDispatch* pDisp = ...; DISPID dispId = ...; // 方法或属性的 ID VARIANT varResult; pDisp->Invoke(dispId, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, ¶ms, &varResult, nullptr, nullptr); // 缺点:性能差,类型不安全 // 方式 2:使用强类型接口(静态,快) IShellDispatch* pShell = ...; IShellFolderViewDual* pFolder = nullptr; pShell->BrowseForFolder(hwnd, &pFolder, ...); // 优点:快速,编译时类型检查 // "双接口" = 同一个对象既支持 IDispatch(动态), // 也支持强类型接口(静态) // 好处:动态语言(VB、JavaScript)用 IDispatch, // 静态语言(C++)用强类型接口 |
IDispatch::Invoke 的完整签名和含义
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
HRESULT Invoke( DISPID dispIdMember, // [in] 方法或属性的逻辑 ID REFIID riid, // [in] 保留,通常是 IID_NULL LCID lcid, // [in] 地区 ID(LOCALE_USER_DEFAULT = 0x400) WORD wFlags, // [in] 调用标志 // DISPATCH_METHOD (1) = 调用方法 // DISPATCH_PROPERTYGET (2) = 获得属性 // DISPATCH_PROPERTYPUT (4) = 设置属性 // DISPATCH_PROPERTYPUTREF (8) = 设置对象引用 DISPPARAMS* pDispParams, // [in] 参数列表 VARIANT* pVarResult, // [out] 返回值(可选,nullptr) EXCEPINFO* pExcepInfo, // [out] 异常信息(可选,nullptr) UINT* puArgErr // [out] 出错参数索引(可选,nullptr) ); // 返回值: // S_OK = 成功 // DISP_E_MEMBERNOTFOUND = 方法不存在 // DISP_E_BADPARAMCOUNT = 参数个数错误 // DISP_E_TYPEMISMATCH = 参数类型错误 |
DISPID 和方法名的映射
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
应用代码: "Pin To Taskbar" (字符串) ↓ GetIDsOfNames DISPID = 0x1234 (唯一整数 ID) ↓ Invoke 执行实际方法 ↓ 返回结果 GetIDsOfNames 的作用: 输入:方法名字符串数组 输出:对应的 DISPID 数组 // 伪代码 HRESULT GetIDsOfNames( REFIID riid, OLECHAR** rgszNames, // ["Pin To Taskbar", "Name", ...] UINT cNames, LCID lcid, DISPID* rgDispId // [0x1234, 0x5678, ...] ); |
Invoke 调用流程
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
调用者: char* szMethodName = "Pin To Taskbar"; VARIANT varArg1, varArg2; // 参数 ... ┌─────────────────────────────────────────┐ │ 步骤 1:GetIDsOfNames │ │ pDisp->GetIDsOfNames(IID_NULL, │ │ &szMethodName, 1, │ │ LOCALE_USER_DEFAULT, │ │ &dispId); │ │ 结果:dispId = 0x1234 │ └────────────────┬────────────────────────┘ │ ┌────────────────┴────────────────────────┐ │ 步骤 2:Invoke │ │ DISPPARAMS dispParams; │ │ dispParams.rgvarg = &varArg1; │ │ dispParams.cArgs = 1; │ │ │ │ pDisp->Invoke(dispId, │ │ IID_NULL, │ │ LOCALE_USER_DEFAULT, │ │ DISPATCH_METHOD, │ │ &dispParams, │ │ &varResult, │ │ nullptr, │ │ nullptr); │ │ 结果:varResult 包含返回值 │ └────────────────┬────────────────────────┘ │ 返回调用者 |
CoCreateInstance 的流程
代码流程
CoCreateInstance内部做了什么?- 查注册表
HKCR\CLSID\{clsid}\InprocServer32,得到DLL路径(比如"C:\Windows\System32\shell32.dll") LoadLibraryA("shell32.dll")加载DLLGetProcAddress(hDll, "DllGetClassObject")获得工厂函数DllGetClassObject(clsid, IID_IClassFactory, &pFactory)创建工厂pFactory->CreateInstance(nullptr, IID_IDispatch, &pDisp)用工厂创建对象- 返回
pDisp(指向虚表的指针)
- 查注册表
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 步骤 1:ProgID → CLSID(查注册表) CLSID clsid; CLSIDFromProgID(L"Shell.Application", &clsid); // 内部:打开注册表 HKCR\Shell.Application,读 CLSID 值 // 步骤 2:CLSID → 加载 DLL,创建对象 IDispatch* pDisp = nullptr; CoCreateInstance( clsid, // 要创建的类的 CLSID nullptr, // 聚集(通常为 null) CLSCTX_INPROC_SERVER, // 在进程内加载 IID_IDispatch, // 要获得的接口的 IID (void**)&pDisp // 输出:指向接口的指针 ); // CoCreateInstance 内部做了什么? // 1. 查注册表 HKCR\CLSID\{clsid}\InprocServer32 // 得到 DLL 路径(比如 "C:\Windows\System32\shell32.dll") // 2. LoadLibraryA("shell32.dll") 加载 DLL // 3. GetProcAddress(hDll, "DllGetClassObject") 获得工厂函数 // 4. DllGetClassObject(clsid, IID_IClassFactory, &pFactory) 创建工厂 // 5. pFactory->CreateInstance(nullptr, IID_IDispatch, &pDisp) 用工厂创建对象 // 6. 返回 pDisp(指向虚表的指针) |
返回值
pDisp不是指向对象本身,而是指向"虚表指针"
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// 对象布局在内存中: IDispatch* pDisp 指向这里: ┌─────────────────────────────┐ │ 虚表指针(vtable pointer) │ ← pDisp 的值就是这个地址 ├─────────────────────────────┤ │ 对象数据(成员变量) │ │ - 引用计数 │ │ - 其他数据 │ └─────────────────────────────┘ 虚表(vtable): ┌─────────────────────────────┐ │ [0] QueryInterface │ ← 虚表偏移 0 ├─────────────────────────────┤ │ [1] AddRef │ ← 虚表偏移 8(x64) 或 4(x86) ├─────────────────────────────┤ │ [2] Release │ ← 虚表偏移 16(x64) 或 8(x86) ├─────────────────────────────┤ │ [3] GetTypeInfoCount │ ← IDispatch 接口开始 ├─────────────────────────────┤ │ [4] GetTypeInfo │ ├─────────────────────────────┤ │ [5] GetIDsOfNames │ ├─────────────────────────────┤ │ [6] Invoke │ ← 最重要!所有方法调用都通过这个 └─────────────────────────────┘ |
QueryInterface:IID 如何对应接口
原理:一个对象,多个虚表
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// Shell.Application 对象实现多个接口: // - IUnknown (基础接口) // - IDispatch (动态调用) // - IShellDispatch (Shell.Application 特有的接口) // CoCreateInstance 返回的是其中一个接口的虚表指针 IDispatch* pDisp = nullptr; CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, IID_IDispatch, (void**)&pDisp); // 这里获得的是 IDispatch 接口 // 但你也许想要 IShellDispatch 接口(功能更多) // 用 QueryInterface 转换: IShellDispatch* pShellDisp = nullptr; pDisp->QueryInterface(IID_IShellDispatch, (void**)&pShellDisp); // QueryInterface 返回指向另一个虚表的指针 // 对象在内存的布局: 对象实例 ├─ IDispatch 虚表指针 ─→ IDispatch vtable │ ├─ QueryInterface │ ├─ AddRef │ ├─ Release │ ├─ GetTypeInfoCount │ ├─ GetTypeInfo │ ├─ GetIDsOfNames │ └─ Invoke │ ├─ IShellDispatch 虚表指针 ─→ IShellDispatch vtable │ ├─ QueryInterface │ ├─ AddRef │ ├─ Release │ └─ NameSpace │ └─ CreateShortcut │ └─ ... (Shell 特有的方法) │ └─ 对象数据(内部状态) ├─ 引用计数 ├─ ... |
理解
- 一个接口对应一个虚函数表吗?
- 每个接口对应一个
vtable指针(即一个vptr),客户端拿到的"接口指针"实际指向的就是一个vptr - 注意:"一个接口对应一张
vtable"指的是类型层面
同一个类的所有实例共享同一张vtable(vtable是每个类一份,存在只读数据段里)
- 每个接口对应一个
- 为什么可以通过
QueryInterface获取另一个接口的虚表指针?QueryInterface并没有"获取"或"创建"任何虚表,它只是把this指针加上一个偏移量,让返回的指针正好落在对象内部另一个vptr的位置上- 所有虚表在对象构造时就已经全部存在了,
QueryInterface做的只是"指路"
QueryInterface 的工作原理
|
1 2 3 4 5 6 7 8 9 |
// 调用: HRESULT hr = pDisp->QueryInterface(IID_IShellDispatch, (void**)&pShellDisp); // 等价于(虚表调用): HRESULT hr = (*(HRESULT (__stdcall**)(void*, REFIID, void**))(*(void**)pDisp))( pDisp, // this 指针(对象地址) IID_IShellDispatch, // 要查询的接口 IID (void**)&pShellDisp // 输出:指向虚表的指针 ); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
IDispatch* pDisp = ...; IShellDispatch* pShell = nullptr; HRESULT hr = pDisp->QueryInterface(IID_IShellDispatch, (void**)&pShell); // 底层: // 1. 获得虚表[0] = QueryInterface // 2. 调用 QueryInterface(pDisp, IID_IShellDispatch, &pShell) // 3. 返回值: // - S_OK: pShell 现在指向 IShellDispatch 虚表 // - E_NOINTERFACE: 对象不支持该接口,pShell = nullptr // 对象内部处理: class CShellApplication { public: HRESULT QueryInterface(REFIID riid, void** ppv) { if (riid == IID_IUnknown || riid == IID_IDispatch) { // 返回第一个虚表的地址 *ppv = (void*)(((void**)this)[0]); // IDispatch vtable this->AddRef(); return S_OK; } else if (riid == IID_IShellDispatch) { // 返回第二个虚表的地址 *ppv = (void*)(((void**)this)[1]); // IShellDispatch vtable this->AddRef(); return S_OK; } else { *ppv = nullptr; return E_NOINTERFACE; } } }; |
多接口对象的虚表转换
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
原始对象地址:0x7fff0000 IDispatch* pDisp = (IDispatch*)0x7fff0000; ↓ QueryInterface(IID_IShellDispatch) IShellDispatch* pShell = ??? 对象内存布局: 0x7fff0000: &IDispatch_vtable ← pDisp 指向这里 0x7fff0008: &IShellDispatch_vtable ← QueryInterface 返回这个地址 所以 pShell = 0x7fff0008 注意:两个接口指针指向同一个对象的不同虚表, 而不是两个不同的对象! |
为什么要 QueryInterface?
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
场景 1:已有 IDispatch,想要 IShellDispatch pDisp->QueryInterface(IID_IShellDispatch, (void**)&pShell); // 现在可以调用 IShellDispatch 特有的方法,比如: pShell->NameSpace(...); // 直接调用,不用 Invoke 场景 2:检查对象是否支持某接口 IUnknown* pUnknown = ...; IDispatch* pDisp = nullptr; if (SUCCEEDED(pUnknown->QueryInterface(IID_IDispatch, (void**)&pDisp))) { // 支持 IDispatch,可以用 Invoke 动态调用 pDisp->Release(); } else { // 不支持 IDispatch,可能是原始 COM 对象 } 场景 3:安全的类型转换 // 这是错的(指针强制转换,不安全): IShellDispatch* pShell = (IShellDispatch*)pDisp; // ❌ // 这是对的(通过 QueryInterface): IShellDispatch* pShell = nullptr; pDisp->QueryInterface(IID_IShellDispatch, (void**)&pShell); // ✓ |
虚表布局和反编译识别
典型的虚表布局(x64)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 假设虚表基址在 0x140050000 偏移(字节) 偏移(虚表项) 函数指针值 函数名 0x00 [0] 0x140001000 QueryInterface 0x08 [1] 0x140001200 AddRef 0x10 [2] 0x140001300 Release 0x18 [3] 0x140002000 GetTypeInfoCount 0x20 [4] 0x140002100 GetTypeInfo 0x28 [5] 0x140002200 GetIDsOfNames 0x30 [6] 0x140002300 Invoke 0x38 [7] 0x140003000 NameSpace (Shell 特有) 0x40 [8] 0x140003100 CreateShortcut ... |
IDA 反编译中的虚表调用
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// IDA 反编译看到的伪代码: (*(__int64 (__fastcall **)(void *, int, void *))(*(_QWORD *)pObj + 0x30))( pObj, pVar, v4 ); // 解析这个: // (*(...)(obj+0x30))(...) // └─ obj+0x30 = 虚表基址 + 0x30 // └─ 0x30 / 8 = 6 (虚表索引) // └─ 虚表[6] 通常是 Invoke (对于 IDispatch) // 识别规则: 虚表偏移 0x00 → QueryInterface 虚表偏移 0x08 → AddRef 虚表偏移 0x10 → Release (以上三个是 IUnknown,所有接口都有) 虚表偏移 0x18 → GetTypeInfoCount (IDispatch) 虚表偏移 0x20 → GetTypeInfo (IDispatch) 虚表偏移 0x28 → GetIDsOfNames (IDispatch) 虚表偏移 0x30 → Invoke (IDispatch) ← 最常见的调用 |
IDA 中的虚表识别技巧
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 在 IDA 中看到: mov rax, [rbx] // 获得虚表指针 call [rax + 0x30] // 调用虚表[6] // IDA 会自动识别(如果有符号): call [pObj->IDispatch::Invoke] // 如果没有符号,你可以: 1. 右键点击 [rax + 0x30] 2. 选 "Structure offset" 3. 输入虚表大小、成员列表 4. IDA 会自动识别后续所有虚表调用 // 或者手工注释: mov rax, [rbx] // 获得虚表 call [rax + 0x30] // 偏移 0x30 = Invoke (IDispatch) |
VARIANT 的重要性
为什么需要 VARIANT
IDispatch的参数和返回值都用VARIANT,因为:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
typedef struct tagVARIANT { VARTYPE vt; // 类型标记:VT_I4, VT_BSTR, VT_BOOL 等 WORD wReserved1; WORD wReserved2; WORD wReserved3; union { LONGLONG llVal; LONG lVal; BYTE bVal; SHORT iVal; FLOAT fltVal; DOUBLE dblVal; VARIANT_BOOL boolVal; BSTR bstrVal; IDispatch* pdispVal; // ... }; } VARIANT; // VARIANT 允许在单一结构中表达任何类型的值 // 通过 vt 字段区分实际类型 |
用法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// 创建 VARIANT VARIANT varString; varString.vt = VT_BSTR; varString.bstrVal = SysAllocString(L"Hello"); VARIANT varInt; varInt.vt = VT_I4; varInt.lVal = 42; // 通过 Invoke 传参 DISPPARAMS dispParams; dispParams.rgvarg = &varString; // 可以是任何类型的 VARIANT dispParams.cArgs = 1; // 返回值也是 VARIANT VARIANT varResult; VariantInit(&varResult); pDisp->Invoke(dispId, ..., &varResult, ...); // 处理返回值 switch (varResult.vt) { case VT_I4: printf("结果是整数:%d\n", varResult.lVal); break; case VT_BSTR: printf("结果是字符串:%ls\n", varResult.bstrVal); break; case VT_DISPATCH: printf("结果是 COM 对象:%p\n", varResult.pdispVal); // 可以继续调用该对象的方法 varResult.pdispVal->Invoke(...); break; } // 释放资源 VariantClear(&varResult); // 自动释放 BSTR、COM 对象等 |
示例
样本代码研究
- 注入
dll,使得explorer将快捷方式固定到了任务栏
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
1. CLSIDFromProgID("Shell.Application", &clsid) → 查注册表,得到 CLSID 2. CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, IID_IDispatch, &pDisp) → 创建 Shell.Application 对象 → 返回 IDispatch 接口指针 pDisp 3. pDisp->GetIDsOfNames(IID_NULL, &"NameSpace", 1, LOCALE_USER_DEFAULT, &dispIdNameSpace) → 获得 NameSpace 方法的 DISPID 4. pDisp->Invoke(dispIdNameSpace, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &dispParams, &varFolder, nullptr, nullptr) → 调用 NameSpace(folder),返回 Folder 对象 → varFolder.pdispVal = 新的 IDispatch* 指向 Folder 对象 5. 重复步骤 3-4,在 Folder 对象上调用: - ParseName(name) 获得 FolderItem - Verbs() 获得 动词集合 6. 遍历动词,找到 "Pin To Taskbar",调用其 DoIt() 方法 → 快捷方式被固定到任务栏 核心:整个过程都是通过 IDispatch::Invoke 动态调用, 没有使用强类型接口(IShellDispatch, IShellFolder) |
枚举右键动词
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
#include <atlbase.h> // CComPtr,自动管理 Release #include <shlobj.h> // IShellFolder, IContextMenu, SHBindToParent 等 #include <shlwapi.h> #include <windows.h> #include <cstdio> #include <io.h> #include <fcntl.h> #pragma comment(lib, "shell32.lib") #pragma comment(lib, "ole32.lib") void EnumShortcutVerbs(LPCWSTR pszPath) { // 1. 把文件路径解析成绝对 PIDL(条目标识列表,Shell 命名空间里的"地址") LPITEMIDLIST pidlFull = nullptr; HRESULT hr = SHParseDisplayName(pszPath, nullptr, &pidlFull, 0, nullptr); if (FAILED(hr)) { wprintf(L"SHParseDisplayName 失败: 0x%08X\n", hr); return; } // 2. 绑定到该项目的「父文件夹」,并取得它在父文件夹内的「子 PIDL」 CComPtr<IShellFolder> spParent; LPCITEMIDLIST pidlChild = nullptr; // 注意是 const,指向 pidlFull 内部,不要单独释放 hr = SHBindToParent(pidlFull, IID_PPV_ARGS(&spParent), &pidlChild); if (FAILED(hr)) { wprintf(L"SHBindToParent 失败\n"); CoTaskMemFree(pidlFull); return; } // 3. 向父文件夹「索要」该子项的 IContextMenu 接口 // GetUIObjectOf 内部就是一次 QueryInterface 式的接口请求 CComPtr<IContextMenu> spMenu; hr = spParent->GetUIObjectOf(nullptr, 1, &pidlChild, IID_IContextMenu, nullptr, reinterpret_cast<void**>(&spMenu)); if (FAILED(hr)) { wprintf(L"GetUIObjectOf 失败\n"); CoTaskMemFree(pidlFull); return; } // 4. 创建一个空菜单,让 IContextMenu 把右键项填进去 HMENU hMenu = CreatePopupMenu(); const UINT idCmdFirst = 1; // 命令 ID 的起始值,自己定 const UINT idCmdLast = 0x7FFF; // CMF_NORMAL 是普通菜单;若想包含 Shift+右键 才出现的「扩展动词」,用 // CMF_EXTENDEDVERBS hr = spMenu->QueryContextMenu(hMenu, 0, idCmdFirst, idCmdLast, CMF_NORMAL | CMF_EXTENDEDVERBS ); if (FAILED(hr)) { wprintf(L"QueryContextMenu 失败\n"); } // 5. 遍历菜单项,逐个取出「动词字符串」和「显示文本」 int count = GetMenuItemCount(hMenu); wprintf(L"共 %d 个菜单项:\n", count); for (int i = 0; i < count; ++i) { MENUITEMINFOW mii = {sizeof(mii)}; mii.fMask = MIIM_ID | MIIM_FTYPE | MIIM_STRING; WCHAR szText[256] = L""; mii.dwTypeData = szText; mii.cch = _countof(szText); if (!GetMenuItemInfoW(hMenu, i, TRUE, &mii)) continue; // 分隔符没有命令,跳过 if (mii.fType & MFT_SEPARATOR) { wprintf(L" [---- 分隔符 ----]\n"); continue; } if (mii.wID < idCmdFirst || mii.wID > idCmdLast) continue; // 把菜单项 ID 还原成「命令偏移」(QueryContextMenu 时是以 idCmdFirst // 为基准的) UINT idCmd = mii.wID - idCmdFirst; // GCS_VERBW: 取动词(如 "open" "edit" "properties");有些项没有动词会失败 WCHAR szVerb[128] = L""; HRESULT hrVerb = spMenu->GetCommandString(idCmd, GCS_VERBW, nullptr, reinterpret_cast<LPSTR>(szVerb), _countof(szVerb)); wprintf(L" 显示文本=\"%s\" 动词=\"%s\"\n", szText, SUCCEEDED(hrVerb) && szVerb[0] ? szVerb : L"(无)"); } // 6. 清理 DestroyMenu(hMenu); CoTaskMemFree(pidlFull); // spMenu / spParent 由 CComPtr 在析构时自动 Release } int wmain() { _setmode(_fileno(stdout), _O_U16TEXT); HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); if (FAILED(hr)) return 1; // 换成你自己的 .lnk 路径 EnumShortcutVerbs(L"c:\\Users\\wangx\\Desktop\\EasyRecycle_360ad.lnk"); CoUninitialize(); return 0; } |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Windows开发相关简记一08/15
- ♥ Windows_进程地址空间相关03/29
- ♥ Spy++相关08/18
- ♥ Bkwin一12/01
- ♥ Soui应用 动画二06/27
- ♥ Windows动态库(DLL)详细学习:一05/30