聊一聊被 .NET程序员 遗忘的 COM 组件

  • 聊一聊被 .NET程序员 遗忘的 COM 组件已关闭评论
  • 159 次浏览
  • A+
所属分类:.NET技术
摘要

最近遇到了好几起和 COM 相关的Dump,由于对 COM 整体运作不是很了解,所以分析此类dump还是比较头疼的,比如下面这个经典的 COM 调用栈。


一:背景

1.讲故事

最近遇到了好几起和 COM 相关的Dump,由于对 COM 整体运作不是很了解,所以分析此类dump还是比较头疼的,比如下面这个经典的 COM 调用栈。

 0:044> ~~[138c]s win32u!NtUserMessageCall+0x14: 00007ffc`5c891184 c3              ret 0:061> k  # Child-SP          RetAddr               Call Site 00 0000008c`00ffec68 00007ffc`5f21bfbe     win32u!NtUserMessageCall+0x14 01 0000008c`00ffec70 00007ffc`5f21be38     user32!SendMessageWorker+0x11e 02 0000008c`00ffed10 00007ffc`124fd4af     user32!SendMessageW+0xf8 03 0000008c`00ffed70 00007ffc`125e943b     xxx!DllUnregisterServer+0x3029f 04 0000008c`00ffeda0 00007ffc`125e9685     xxx!DllUnregisterServer+0x11c22b 05 0000008c`00ffede0 00007ffc`600b50e7     xxx!DllUnregisterServer+0x11c475 06 0000008c`00ffee20 00007ffc`60093ccd     ntdll!LdrpCallInitRoutine+0x6f 07 0000008c`00ffee90 00007ffc`60092eef     ntdll!LdrpProcessDetachNode+0xf5 08 0000008c`00ffef60 00007ffc`600ae319     ntdll!LdrpUnloadNode+0x3f 09 0000008c`00ffefb0 00007ffc`600ae293     ntdll!LdrpDecrementModuleLoadCountEx+0x71 0a 0000008c`00ffefe0 00007ffc`5cd7c00e     ntdll!LdrUnloadDll+0x93 0b 0000008c`00fff010 00007ffc`5d47cf78     KERNELBASE!FreeLibrary+0x1e 0c 0000008c`00fff040 00007ffc`5d447aa3     combase!CClassCache::CDllPathEntry::CFinishObject::Finish+0x28 [onecorecomcombaseobjactdllcache.cxx @ 3420]  0d 0000008c`00fff070 00007ffc`5d4471a9     combase!CClassCache::CFinishComposite::Finish+0x4b [onecorecomcombaseobjactdllcache.cxx @ 3530]  0e 0000008c`00fff0a0 00007ffc`5d3f1499     combase!CClassCache::FreeUnused+0xdd [onecorecomcombaseobjactdllcache.cxx @ 6547]  0f 0000008c`00fff650 00007ffc`5d3f13c7     combase!CoFreeUnusedLibrariesEx+0x89 [onecorecomcombaseobjactdllapi.cxx @ 117]  10 (Inline Function) --------`--------     combase!CoFreeUnusedLibraries+0xa [onecorecomcombaseobjactdllapi.cxx @ 74]  11 0000008c`00fff690 00007ffc`6008a019     combase!CDllHost::MTADllUnloadCallback+0x17 [onecorecomcombaseobjactdllhost.cxx @ 929]  12 0000008c`00fff6c0 00007ffc`6008bec4     ntdll!TppTimerpExecuteCallback+0xa9 13 0000008c`00fff710 00007ffc`5f167e94     ntdll!TppWorkerThread+0x644 14 0000008c`00fffa00 00007ffc`600d7ad1     kernel32!BaseThreadInitThunk+0x14 15 0000008c`00fffa30 00000000`00000000     ntdll!RtlUserThreadStart+0x21  

为了做一个简单的梳理,我们搭建一个简单的多语言 COM 互操作。

二:COM 多语言互操作

1. 背景

可能很多新生代的程序员都不知道 COM ,最多也只听过这个名词,其实在 Windows 上有海量的 COM 组件,这些组件信息都是注册在 HKEY_CLASSES_ROOTCLSID 节点目录,截图如下:

聊一聊被 .NET程序员 遗忘的 COM 组件

这个和微服务中的 注册中心 是一个道理,这一篇我们用 C# 写一个COM组件,用 C++ 去调用。

2. C# 写一个 COM 组件

写一个 .NET Framework 4.8 下的 32bit FlyCom 组件,一个接口,一个实现类,具体原理后续再分析,先搭建尝尝鲜, C# 代码如下:

 namespace FlyCom {     [Guid("31A3CED7-B4F1-4D59-881A-EA1D7ABCC4CF")]     public interface BaseFly     {         [DispId(1)]         string Show(string str);     }      [Guid("270C3ED3-053D-4324-9176-9C3FA2BE58A7")]     [ProgId("FlyCom.Show")]     public class Fly : BaseFly     {         public string Show(string str)         {             return $"str={str}, length={str.Length}";         }     } }  

这里简单说一下:

  1. Guid

一个是接口(BaseFly) 的唯一码,即 IID 信息, 一个是 COM组件的 唯一码,叫做 CLSID。

  1. ProgId

因为 GUID 不方便记忆,所以给这个 COM组件 取一个别名叫 FlyCom.Show

  1. DispId

这个是为了遵循 COM多语言互通下的 vtable调用标准,表示第一个接口方法是 Show,后续再聊。

有了代码,接下来还要做三个配置。

  • 对 COM 的可见性

修改 AssemblyInfo.cs 中的 ComVisible = true,参考如下:

 // Setting ComVisible to false makes the types in this assembly not visible // to COM components.  If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(true)]  
  • 生成签名

一般来说,将 com 放到 注册表,最好都生成一个强签名,否则会有警告提示。

聊一聊被 .NET程序员 遗忘的 COM 组件

  • 注册 com 互操作

在属性面板中,选择 Build 选项卡,选中 Register for COM interop 选项即可。

聊一聊被 .NET程序员 遗忘的 COM 组件

3. 注册 COM 到注册表

要将 com组件 放到注册表,需要使用注册表编辑工具 regasm

 Microsoft Windows [版本 10.0.19042.746] (c) 2020 Microsoft Corporation. 保留所有权利。  C:UsersAdministrator>cd /d C:Program Files (x86)Microsoft SDKsWindowsv10.0AbinNETFX 4.8 Toolsx64  C:Program Files (x86)Microsoft SDKsWindowsv10.0AbinNETFX 4.8 Toolsx64>C:WindowsMicrosoft.NETFrameworkv4.0.30319regasm.exe D:net6ConsoleApp1FlyCombinDebugFlyCom.dll /tlb:FlyCom.tlb  /CodeBase Microsoft .NET Framework 程序集注册实用工具版本 4.8.4084.0 (适用于 Microsoft .NET Framework 版本 4.8.4084.0) 版权所有 (C) Microsoft Corporation。保留所有权利。  成功注册了类型 成功注册了导出到“D:net6ConsoleApp1FlyCombinDebugFlyCom.tlb”的程序集和类型库  C:Program Files (x86)Microsoft SDKsWindowsv10.0AbinNETFX 4.8 Toolsx64>  

从输出中可以看到已成功注册,并且生成了一个 FlyCom.tlb 代理文件,接下来可以到注册表中验证一下 GUID=270C3ED3-053D-4324-9176-9C3FA2BE58A7 注册项以及别名为 FlyCom.Show 的注册项。

聊一聊被 .NET程序员 遗忘的 COM 组件
聊一聊被 .NET程序员 遗忘的 COM 组件

4. 使用 C++ 调用

要想 C++ 调用 C# 写的 COM 组件,就像 RPC 调用一样,直接自动生成的代理文件即可,将 FlyCom.tlb 复制到 根目录,并且将程序改成 Win32 位,截图如下:

聊一聊被 .NET程序员 遗忘的 COM 组件

接下来就是完整的 C++ 代码。

 #include <Windows.h> #include <string.h> #include <iostream>  #import "FlyCom.tlb" named_guids raw_interface_only  using namespace std;  int main() { 	CoInitialize(NULL);  	FlyCom::BaseFlyPtr ptr;  	ptr.CreateInstance("FlyCom.Show");  	wchar_t* c = ptr->Show(L"hello world");  	wprintf(L"%s", c);  	getchar(); }  

将程序跑起来后,真的很完美。

聊一聊被 .NET程序员 遗忘的 COM 组件

从 C++ 调用 COM 的流程图可以很清楚的看到,这是面向接口编程的方式,非常完美。

三:COM 多语言互通原理

1. 架构图

千言万语不及一张图。

聊一聊被 .NET程序员 遗忘的 COM 组件

这就是 COM 能够实现多语言互通的规范,熟悉 C++ 的朋友肯定知道 vtable ,C++ 能够实现多态,全靠这玩意,COM 也是用了 vtable 这套模式,所以诸如 JAVA,C#,VBS 必须在二进制层面将代码组织成上图这种形式,才能实现 COM 的互通。

所以在 C# 中你看到的 DispId 特性就是为了按照 vtable 方式进行组织,对于 ole32 和 combase 这些 COM 运行环境的基石,我们后续用 windbg 来解读一下,这一篇就先到这里,希望对你有帮助。