好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

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

一、背景

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 组件的详细内容...

  阅读:49次