一:背景 1.講故事 最近遇到了好幾起和 COM 相關的Dump,由於對 COM 整體運作不是很瞭解,所以分析此類dump還是比較頭疼的,比如下麵這個經典的 COM 調用棧。 0:044> ~~[138c]s win32u!NtUserMessageCall+0x14: 00007ffc`5c891 ...
一:背景
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 [onecore\com\combase\objact\dllcache.cxx @ 3420]
0d 0000008c`00fff070 00007ffc`5d4471a9 combase!CClassCache::CFinishComposite::Finish+0x4b [onecore\com\combase\objact\dllcache.cxx @ 3530]
0e 0000008c`00fff0a0 00007ffc`5d3f1499 combase!CClassCache::FreeUnused+0xdd [onecore\com\combase\objact\dllcache.cxx @ 6547]
0f 0000008c`00fff650 00007ffc`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 0000008c`00fff690 00007ffc`6008a019 combase!CDllHost::MTADllUnloadCallback+0x17 [onecore\com\combase\objact\dllhost.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_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 來解讀一下,這一篇就先到這裡,希望對你有幫助。