一、背景
1.讲故事最近遇到了好几起和 COM 相关的Dump,由于对 COM 整体运作不是很了解,所以分析此类dump还是比较头疼的,比如下面这个经典的 COM 调用栈。
0 : 044 > ~~ [ 138 c ] s win32u ! NtUserMessageCall + 0x14 : 00007 ffc`5c891184 c3 ret 0 : 061 > k # Child - SP RetAddr Call Site 00 0000008 c`00ffec68 00007 ffc`5f21bfbe win32u ! NtUserMessageCall + 0x14 01 0000008 c`00ffec70 00007 ffc`5f21be38 user32 ! SendMessageWorker + 0x11e 02 0000008 c`00ffed10 00007 ffc`124fd4af user32 ! SendMessageW + 0xf8 03 0000008 c`00ffed70 00007 ffc`125e943b xxx ! DllUnregisterServer + 0x3029f 04 0000008 c`00ffeda0 00007 ffc`125e9685 xxx ! DllUnregisterServer + 0x11c22b 05 0000008 c`00ffede0 00007 ffc`600b50e7 xxx ! DllUnregisterServer + 0x11c475 06 0000008 c`00ffee20 00007 ffc`60093ccd ntdll ! LdrpCallInitRoutine + 0x6f 07 0000008 c`00ffee90 00007 ffc`60092eef ntdll ! LdrpProcessDetachNode + 0xf5 08 0000008 c`00ffef60 00007 ffc`600ae319 ntdll ! LdrpUnloadNode + 0x3f 09 0000008 c`00ffefb0 00007 ffc`600ae293 ntdll ! LdrpDecrementModuleLoadCountEx + 0x71 0 a 0000008 c`00ffefe0 00007 ffc`5cd7c00e ntdll ! LdrUnloadDll + 0x93 0 b 0000008 c`00fff010 00007 ffc`5d47cf78 KERNELBASE ! FreeLibrary + 0x1e 0 c 0000008 c`00fff040 00007 ffc`5d447aa3 combase ! CClassCache :: CDllPathEntry :: CFinishObject :: Finish + 0x28 [ onecore\com\combase\objact\dllcache .cxx @ 3420 ] 0 d 0000008 c`00fff070 00007 ffc`5d4471a9 combase ! CClassCache :: CFinishComposite :: Finish + 0x4b [ onecore\com\combase\objact\dllcache .cxx @ 3530 ] 0 e 0000008 c`00fff0a0 00007 ffc`5d3f1499 combase ! CClassCache :: FreeUnused + 0xdd [ onecore\com\combase\objact\dllcache .cxx @ 6547 ] 0 f 0000008 c`00fff650 00007 ffc`5d3f13c7 combase ! CoFreeUnusedLibrariesEx + 0x89 [ onecore\com\combase\objact\dllapi .cxx @ 117 ] 10 ( Inline Function ) --------`-------- combase!CoFreeUnusedLibraries+0xa [onecore\com\combase\objact\dllapi.cxx @ 74] 11 0000008 c`00fff690 00007 ffc`6008a019 combase ! CDllHost :: MTADllUnloadCallback + 0x17 [ onecore\com\combase\objact\dllhost .cxx @ 929 ] 12 0000008 c`00fff6c0 00007 ffc`6008bec4 ntdll ! TppTimerpExecuteCallback + 0xa9 13 0000008 c`00fff710 00007 ffc`5f167e94 ntdll ! TppWorkerThread + 0x644 14 0000008 c`00fffa00 00007 ffc`600d7ad1 kernel32 ! BaseThreadInitThunk + 0x14 15 0000008 c`00fffa30 00000000 `00000000 ntdll ! RtlUserThreadStart + 0x21
为了做一个简单的梳理,我们搭建一个简单的多语言 COM 互操作。
二、COM 多语言互操作
1. 背景可能很多新生代的程序员都不知道 COM ,最多也只听过这个名词,其实在 Windows 上有海量的 COM 组件,这些组件信息都是注册在 HKEY_CLASSES_ROOT\CLSID 节点目录,截图如下:
这个和微服务中的 注册中心 是一个道理,这一篇我们用 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}" ; } } }
这里简单说一下:
Guid一个是接口(BaseFly) 的唯一码,即 IID 信息, 一个是 COM组件的 唯一码,叫做 CLSID。
ProgId因为 GUID 不方便记忆,所以给这个 COM组件 取一个别名叫 FlyCom.Show 。
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 放到 注册表,最好都生成一个强签名,否则会有警告提示。
注册 com 互操作
在属性面板中,选择 Build 选项卡,选中 Register for COM interop 选项即可。
3. 注册 COM 到注册表
要将 com组件 放到注册表,需要使用注册表编辑工具 regasm。
Microsoft Windows [ 版本 10.0 .19042 .746 ] ( c ) 2020 Microsoft Corporation. 保留所有权利。 C : \Users\Administrator > cd / d C : \Program Files ( x86 ) \Microsoft SDKs\Windows\v10 .0A \bin\NETFX 4.8 Tools\x64 C : \Program Files ( x86 ) \Microsoft SDKs\Windows\v10 .0A \bin\NETFX 4.8 Tools\x64 > C : \Windows\Microsoft .NET \Framework\v4 .0 .30319 \regasm .exe D : \net6\ConsoleApp1\FlyCom\bin\Debug\FlyCom .dll / tlb : FlyCom .tlb / CodeBase Microsoft .NET Framework 程序集注册实用工具版本 4.8 .4084 .0 ( 适用于 Microsoft .NET Framework 版本 4.8 .4084 .0 ) 版权所有 ( C ) Microsoft Corporation。保留所有权利。 成功注册了类型 成功注册了导出到[D : \net6\ConsoleApp1\FlyCom\bin\Debug\FlyCom .tlb ]的程序集和类型库 C : \Program Files ( x86 ) \Microsoft SDKs\Windows\v10 .0A \bin\NETFX 4.8 Tools\x64 >
从输出中可以看到已成功注册,并且生成了一个 FlyCom.tlb 代理文件,接下来可以到注册表中验证一下 GUID=270C3ED3-053D-4324-9176-9C3FA2BE58A7 注册项以及别名为 FlyCom.Show 的注册项。
4. 使用 C++ 调用
要想 C++ 调用 C# 写的 COM 组件,就像 RPC 调用一样,直接自动生成的代理文件即可,将 FlyCom.tlb 复制到 根目录,并且将程序改成 Win32 位,截图如下:
接下来就是完整的 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 ( ) ; }
将程序跑起来后,真的很完美。
从 C++ 调用 COM 的流程图可以很清楚的看到,这是面向接口编程的方式,非常完美。
三:COM 多语言互通原理
1. 架构图千言万语不及一张图。
这就是 COM 能够实现多语言互通的规范,熟悉 C++ 的朋友肯定知道 vtable ,C++ 能够实现多态,全靠这玩意,COM 也是用了 vtable 这套模式,所以诸如 JAVA,C#,VBS 必须在二进制层面将代码组织成上图这种形式,才能实现 COM 的互通。
所以在 C# 中你看到的 DispId 特性就是为了按照 vtable 方式进行组织,对于 ole32 和 combase 这些 COM 运行环境的基石,我们后续用 windbg 来解读一下,这一篇就先到这里,希望对你有帮助。
原文地址:https://mp.weixin.qq.com/s/pvuw02OjR7mX2EyzVz0vTw
查看更多关于聊一聊被 .NET程序员 遗忘的 COM 组件的详细内容...