• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-06-10 16:28 Aet 隐藏边栏 |   抢沙发  1 
文章评分 1 次,平均分 5.0

关键标识符

CLSID (Class ID)

  1. CLSID 是一个 128GUID,唯一标识一个 COM 对象类

ProgID (Programmatic ID)

  1. ProgID 是一个字符串,人类可读的类标识

IID (Interface ID)

  1. IID 也是 128GUID,唯一标识一个接口

关系图

COM 对象在内存中的布局

对象实例的物理布局

  1. 内存地址空间中一个 COM 对象的真实样子

虚表的物理布局

  1. 虚表是一个函数指针数组
  2. 每一项都是一个函数指针(8 字节,x64)

一个具体的例子

  1. 假设 Shell.Application 对象在 0x7fff0000

关键点

  1. 对象地址 = 第一个虚表指针的地址
  2. 多接口对象有多个虚表指针,按顺序排列
  3. IUnknown 虚表指针总是第一个

理解

  1. 如果对象不支持多接口,怎么实现
  2. 如果对象支持多接口,怎么实现
  3. 如果对象支持多接口,多接口背后的原理
    1. 见下文com编程相关

COM编程相关

概述

  1. COM 编程里有一个关键点要先说清楚:
    1. 每个 COM 对象都必须实现 IUnknown(包含 QueryInterfaceAddRefRelease 三个方法)
  2. 所谓"支持单接口"还是"支持多接口",真正的区别就体现在 QueryInterface 的实现上
    1. 它决定了客户端能"问"出哪些接口

只支持单个接口

  1. 这种情况其实是支持两个 IID:IID_IUnknown 和你自己定义的那一个接口
  2. 因为只有单继承,static_cast<IHello*>(this) 不存在任何歧义,this 指针、IUnknown*IHello* 在内存上是同一个起始地址

支持多个接口(多重继承方式)

  1. 最常见的做法是让实现类多重继承多个接口
    1. 这里有几个单接口时不会遇到的坑,得注意

  1. 多接口实现里必须记住的三条规则:
    1. IID_IUnknown 的二义性
    2. COM 标识规则(identity)
    3. 引用计数是全对象共享的
  2. IID_IUnknown 的二义性
    1. 多重继承下,IHelloIGoodbye 各自带了一份 IUnknown,直接 static_cast<IUnknown*>(this) 编译器无法决定走哪条路径
    2. 所以要先转到任意一个具体接口(上例固定选 IHello),再隐式当作 IUnknown* 返回
  3. COM 标识规则(identity)
    1. 对同一个对象,无论从哪个接口去查询 IID_IUnknown,返回的指针值必须完全相同
    2. 这就是为什么上面把 IID_IUnknown 固定指向 static_cast<IHello*>(this),绝不能一会儿用 IHello*、一会儿用 IGoodbye*
    3. 客户端正是靠这个规则判断两个接口指针是不是同一个对象
  4. 引用计数是全对象共享的
    1. 只有一个 m_cRef,不管客户端拿到的是 IHello* 还是 IGoodbye*,AddRef/Release 操作的都是同一个计数

ATL 简化

  1. 如果不想手写 QueryInterface,可以用 ATLCOM map,它会自动生成上面这套逻辑
    1. ATL 内部会处理好 IUnknown 二义性、标识规则和引用计数,你只需要在 map 里列出支持哪些接口即可

嵌套类(Nested Class)多接口

  1. 核心思路
    1. 每个接口由一个独立的嵌套类实现,外层对象把它们作为成员持有
    2. 每个嵌套类保存一个指向外层对象的反向指针,自己的 IUnknown 三方法全部委托(delegate)给外层对象
    3. 这样可以让每个接口的实现代码彼此隔离,避免外层类因多重继承而变得臃肿,也不会有 IUnknown 二义性问题

分离式多接口(Tear-off Interface)

  1. 适用场景
    1. 某个接口很少被用到,如果让主对象一直背着它(多一份 vtable 指针、多一组成员)很浪费
  2. Tear-off 的做法是
    1. 主对象平时根本不实现这个接口,只有当客户端 QueryInterface 来要的时候,才临时 new 一个小对象出来
    2. 当这个接口被 Release0 时,小对象自己销毁

虚表调用的底层机制

调用方式 1:C++ 方式

调用方式 2:直接虚表偏移访问

为什么是这样设计

  1. 性能
    1. 虚表调用只需一次内存读取(获得虚表)
    2. 然后一次函数指针跳转
    3. 比动态查表快
  2. 兼容性
    1. 所有 COM 对象都遵循虚表约定
    2. 接口可以被各种语言使用(C++, VB, C#, JavaScript)
  3. 扩展性
    1. 新增方法只需在虚表末尾添加新条目
    2. 现有代码不需要改动
    3. ABI 兼容性

IDispatch 双接口详解

什么是双接口

  1. "双接口" = Dual Interface = 同时支持两种调用方式

IDispatch::Invoke 的完整签名和含义

DISPID 和方法名的映射

Invoke 调用流程

CoCreateInstance 的流程

代码流程

  1. 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(指向虚表的指针)

返回值

  1. pDisp 不是指向对象本身,而是指向"虚表指针"

QueryInterface:IID 如何对应接口

原理:一个对象,多个虚表

理解

  1. 一个接口对应一个虚函数表吗?
    1. 每个接口对应一个 vtable 指针(即一个 vptr),客户端拿到的"接口指针"实际指向的就是一个 vptr
    2. 注意:"一个接口对应一张 vtable"指的是类型层面
      同一个类的所有实例共享同一张 vtable(vtable 是每个类一份,存在只读数据段里)
  2. 为什么可以通过QueryInterface获取另一个接口的虚表指针?
    1. QueryInterface 并没有"获取"或"创建"任何虚表,它只是把 this 指针加上一个偏移量,让返回的指针正好落在对象内部另一个 vptr 的位置上
    2. 所有虚表在对象构造时就已经全部存在了,QueryInterface 做的只是"指路"

QueryInterface 的工作原理

多接口对象的虚表转换

为什么要 QueryInterface?

虚表布局和反编译识别

典型的虚表布局(x64)

IDA 反编译中的虚表调用

IDA 中的虚表识别技巧

VARIANT 的重要性

为什么需要 VARIANT

  1. IDispatch 的参数和返回值都用 VARIANT,因为:

用法

示例

样本代码研究

  1. 注入dll,使得explorer将快捷方式固定到了任务栏

枚举右键动词

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享