关闭

DCOM列集散集的内部具体实现的研究初探。

1192人阅读 评论(1) 收藏 举报

我也不罗嗦什么理论基础,相信看这篇文章的人肯定对COM有一定的功底了。下面的介绍只是我对COM实现的自己的理解因此肯定有错误的地方,目的是希望大家指出和讨论,以求共同进步。

1.套间的注册

         无论是在服务器端还是在客户端,首先执行的总是CoInitialize来建立一个STA或者MTA套间。每个套间由一个8字节的数字唯一标识称为OXID, 当调用CoInitialize(Ex)时系统将会把套间的信息注册到SCM中,因此每一台机器上的SCM都管理着本机所有注册的套间。那么注册到SCM中的每个套间还应该包括什么信息呢? 据我考虑注册到SCM中的套间信息应该包括如下结构:

typedef struct APARTMENTINFO
{
    LUID  oxid;                         //套间标识符
    DWORD type;                   //套间的类型: STA, MTA
    DWORD ProcessId;        //套间所在的进程ID
    DWORD ThreadId;          //套间所在的线程ID,这只对STA有意义
    HWND  Wnd;                    //STA套间中的隐含窗口句柄,这也只对STA有意义
    本地的IPC端口;                //客户可以通过这个端口连接到进程中来,这个IPC可以参考RPC中IPC的定义
    其他数据;
};

而当系统调用CoUninitialize时则会将套间的注册信息从SCM中删除掉。

为什么要注册这些信息后面将会讨论到。

 


2.EXE服务器端类厂对象的注册


  因为EXE不能提供DllGetObject导出函数,所以必须提供一个方法告诉SCM以便SCM能够找到类厂对象。因此EXE服务器需要调用CoRegisterClassObject来将类厂信息保存到SCM。这个函数格式如下:

STDAPI CoRegisterClassObject(
  REFCLSID rclsid,                     //COM对象的CLSID
  IUnknown * pUnk,                    //类厂接口指针
  DWORD dwClsContext,         //环境上下文
  DWORD flags,                        //属性决定服务器对象的实例的生存
  LPDWORD  lpdwRegister   //[OUT]用来保存注册的标识。
)

而当EXE服务器退出前则需要调用CoRevokeClassObject来将类厂信息从SCM中取消

HRESULT CoRevokeClassObject(
  DWORD dwRegister
);


从上面的注册代码中可以看出,每个类厂对象注册时保存到SCM中的有: 对象CLSID, 类厂接口指针, 上下文, 属性等,但实际还会保存更多的东西比如类厂所输的套间的OXID(也有可能在SCM中的每个OXID上维护着类厂列表,这样每次注册时将根据类厂所属的OXID来查找SCM中的OXID列表并将对应的类厂信息保存到特定的OXID标识的套间信息中去)

1,2注册的信息都为以后服务有用。


3.客户调用CoGetClassObject得到进程外服务器类厂接口的过程.


           当客户通过指定对象的CLSID, 接口IID,对象的生存环境,以及对象所在的服务器名调用CoGetClassObject时, COM将这个调用发向客户端的SCM。 客户端的SCM将根据指定的计算机服务器名跟目标机器上的SCM通信,并传递要求的建立的CLSID,IID等信息(因为指定的计算机名,所以很容易可以跟目标计算机上的SCM通信,具体是如何通信的可以不必研究,但我认为因该是RPC)。而目标服务器机器上的SCM收到请求后查询所有注册的类厂信息(第2步会将类厂的CLSID注册到SCM中)。若没有找到则根据CLSID查注册表并启动组件,而当找到注册的类厂信息后,向类厂注册的套间发送RPC请求以便得到请求接口的列集数据包。然后SCM将得到的数据包返回给客户的SCM,SCM再将数据返回给调用CoGetClassObject的客户(也许客户在调用CoGetClassObject时并不需要本地SCM的介入!!而是直接跟目标计算机上的SCM通信)。客户端根据得到的列集数据包调用CoUnmarsalInterface进行散集,并建立代理对象,并将代理对象的接口返回给客户端。这样就完成了客户到服务器上的第一个连接的建立过程(调用CCoUnmarshalInterface是在CoGetClassObject内部完成的)。

现在的问题是: SCM是如何将请求发送给类厂所在的套间的,以及发送请求时带了些什么参数?
回答(猜测): 若是类厂在STA则根据类厂所在的套间,在SCM中找到套间的类厂接口指针,以及套间的窗口,和线程,和其他需要的信息,然后发送一个窗口消息给套间(记得第1步注册的那些信息吗),STA套间的隐藏窗口在消息处理函数中调用CoMarshalInterface进行数据的列集.并将得到的列集数据返回给SCM;而对于MTA中则是直接发出一个RPC调用,让类厂所在服务器上执行这个调用,而这个调用的实现也是调用CoMarshalInterface进行数据的列集。

 

现在的问题是集中到了CoMarshalInterface和CoUnmarshalInterface这两个函数了,这两个函数到底做了什么?他们又是如何建立代理对象和存根管理器的等等。


4.CoMarshalInterface的实现逻辑


  CoMarshalInterface的调用总是在对象所在的套间之中, 而且这个函数还负责实现存根管理器的建立,接口存根的建立等等。函数的定义如下:

STDAPI CoMarshalInterface(
  IStream * pStm,               //接收列集产生结果的流,最终这个流会进行传递
  REFIID riid,                      //预列集的接口IID
  IUnknown * pUnk,          //预列集的对象的指针(riid必须是pUnk所支持的接口)
  DWORD dwDestContext, //列集包括的信息,也就是决定列集的数据可以被谁来散集
  void * pvDestContext,  //为NULL
  DWORD mshlflags  //指定列集的属性, 常规,强表格,弱表格,不执行PIN
);

这个函数的内部实现逻辑大概如下:

(1).当调用这个函数时pUnk首先查询是否支持riid接口,若不支持则返回错误.
(2).查询pUnk对象是否支持IMarshal接口,若支持则表示对象将使用自定义列集,而若不支持则表示使用标准列集器
(3).标准列集器的建立是通过调用CoGetStandarMarshal来完成的,这个函数内部将同时会建立对象的存根管理器对象, 接口存根对象等。函数的定义如下:

STDAPI CoGetStandardMarshal(
  REFIID riid,                            //需要列集的接口IID
  IUnknown * pUnk,               //预列集的对象的指针
  DWORD dwDestContext,
  LPVOID pvDestContext,
  DWORD mshlflags,
  LPMARSHAL * ppMarshal    //[OUT] 输出一个IMarshal接口指针(对象的存根管理器对象实现的接口!!!)
);

那么这个函数是如何来实现的呢?

(3.1). COM运行库维护着套间(或者是本进程)所建立的所有存根管理器,每个存根管理器都维护着对象的一个引用。当函数调用时根据传递进来的pUnk查找是否有存根管理器与此对象有关联(通过查询维护的所有存根管理器来实现,因为每个存根管理器维护着对象的引用,因此这步很容易实现),若无则建COM建立一个存根管理器,并为存根管理器分配一个8字节的标识符称为OID来唯一标识这个存根管理器,同时存根管理器将保存这个对象的一个引用。COM建立的这个存根管理器将实现IMarshal接口。


(3.2). 每个存根管理器将维护着一个接口存根列表。当找到存根管理器后,存根管理器再根据riid查找其所维护的接口存根对象。所谓接口存根对象也是一个COM对象,这个对象负责将接口函数的RPC调用数据包进行散集并构件出物理栈(这些都有原代码可以看到的,在IDL生成的文件中),然后再调用真实的接口函数。因此每个接口存根需要跟这个接口的指针进行关联。当存根管理器没有查到riid对应的接口存根时, COM将会根据riid这个接口信息在注册表的HKEY_CLASSES_ROOT/Interface下查找子键riid的ProxyStubClsid32子健下的默认值,这个默认值是一个对象的CLSID。然后COM根据CLSID调用CoGetClassObject函数请求代理类厂接口IPSFactoryBuffer,并调用接口的CreateStub建立一个接口存根对象。IPSFactoryBuffer的定义如下:

[
    local,
    object,
    uuid(D5F569D0-593B-101A-B569-08002B2DBF7A)
]
interface IPSFactoryBuffer : IUnknown
{

    HRESULT CreateProxy    //建立一个接口代理对象
    (
        [in] IUnknown *pUnkOuter,
        [in] REFIID riid,
        [out] IRpcProxyBuffer **ppProxy,
        [out] void **ppv
    );

    HRESULT CreateStub   //建立一个接口存根对象
    (
        [in] REFIID riid,               //欲建立接口存根的IID
        [in, unique] IUnknown *pUnkServer,  //指定外部对象,这个参数就是外部函数传递的pUnk
        [out] IRpcStubBuffer **ppStub     //接口存根必须实现的接口
    );
}

当建立一个接口存根后得到的是一个IRpcStubBuffer接口指针,所有的接口存根必须实现这个指针。这个接口的定义如下:

[
    local,
    object,
    uuid(D5F56AFC-593B-101A-B569-08002B2DBF7A)
]
interface IRpcStubBuffer : IUnknown
{

    HRESULT Connect   //用于跟对象进行连接,这样接口存根就可以将散集的结果调用对应的接口成员函数了
    (
        [in] IUnknown *pUnkServer
    );

    void Disconnect();  //取消连接

    HRESULT Invoke     //这是一个核心函数他可能在MTA中的某个RPC线程中调用,也可能在窗口的消息处理函数中调用,实现具体的参数散集并执行具体的方法调用也就是接口存根调用这个函数来将函数调用分发到各具体的函数中去
    (
        [in] RPCOLEMESSAGE *_prpcmsg,
        [in] IRpcChannelBuffer *_pRpcChannelBuffer
    );

    IRpcStubBuffer *IsIIDSupported
    (
        [in] REFIID riid
    );

    ULONG CountRefs
    (
        void
    );

    HRESULT DebugServerQueryInterface
    (
        void **ppv
    );

    void DebugServerRelease
    (
        void *pv
    );

};

当COM建立起接口存根后,会为这个接口存根分配一个16位的唯一标识符叫IPID。并将接口存根接口和IPID保存到由对象存根管理器所维护的接口存根列表中去
(现在不确定的是一个接口的接口存根是唯一建立一次还是每一个存根管理器都将建立不同的接口存根,我更偏向于前者).


(3.3).COM将查询对象pUnk是否实现了IExternalConnection接口,若实现了则表明由对象来控制存根管理器的生存周期,而若没有实现则由系统来管理存根管理器的生存周期.

(3.4).存根管理器也建立起来了,接口存根也建立起来的,CoGetStandardMarshal函数将完成,这样就将得到的存根管理器IMarshal接口指针返回给CoMarshalInterface函数。


(4).当CoMarshalInterface得到了一个IMarshal接口指针后,他将会调用IMarshal的接口成员函数,先看看IMarshal的定义:

[
    local,
    object,
    uuid(00000003-0000-0000-C000-000000000046)
]

interface IMarshal : IUnknown
{

    typedef [unique] IMarshal *LPMARSHAL;

    HRESULT GetUnmarshalClass
    (
        [in] REFIID riid,         //欲列集的接口IID
        [in, unique] void *pv,  //指定哪个对象想进行列集散集操作,若为NULL则表示为本对象
        [in] DWORD dwDestContext,
        [in, unique] void *pvDestContext,
        [in] DWORD mshlflags,
        [out] CLSID *pCid    //得到代理对象的CLSID,一般是用在散集端
    );

    HRESULT GetMarshalSizeMax
    (
        [in] REFIID riid,
        [in, unique] void *pv,
        [in] DWORD dwDestContext,
        [in, unique] void *pvDestContext,
        [in] DWORD mshlflags,
        [out] DWORD *pSize    //指定构造列集数据包时,可能需要的SIZE,以便预先分配
    );

    HRESULT MarshalInterface
    (
        [in, unique] IStream *pStm,
        [in] REFIID riid,
        [in, unique] void *pv,
        [in] DWORD dwDestContext,  //指定是列集到本机,还是本进程,还是不同的机器上
        [in, unique] void *pvDestContext,
        [in] DWORD mshlflags    //将接口列集为数据包,并写到流中去
    );

    HRESULT UnmarshalInterface
    (
        [in, unique] IStream *pStm,
        [in] REFIID riid,
        [out] void **ppv  //从流中散集数据,并建立代理对象
    );

    HRESULT ReleaseMarshalData
    (
        [in, unique] IStream *pStm   //释放流中可能包含的
    );

    HRESULT DisconnectObject
    (
        [in] DWORD dwReserved   //销毁存根管理器对象
    );
}


具我观察,当调用CoMarshalInterface时只会调用:
(4.1)GetUnmarshalClass函数得到在客户端建立的代理对象的CLSID值,并写入到流中
(4.2)调用MarshalInterface列集数据。那么列集出来的数据结构是怎么样的呢?。COM公开了结构定义如下:

struct MARSHALINFO
{
 DWORD meow;      //结构标志,用"MEOW"开头的四个字符
 DWORD flag;      //表明采用的列集技术可以为:
                                                //OBJREF_STANDARD  :标准列集
                                                //OBJREF_HANDLER   : ??
                                               //OBJREF_CUSTOM :自定义列集,
 IID   iid;       //要列集的接口IID

  //后面的结构由列集属性决定接。下面是采用标准列集的数据结构:

//上面三个参数是固定的
 DWORD stdflag;    //标准列集下的属性mshlflags参数的内容
 DWORD refs;         //指定存根管理器的引用计数,而不是对象的引用计数,当接口被列集时存根管理器
                                   //此时的外部引用数就是这个值,这个值将根据mshlflags的选择而不同.

 LUID oxid;    //对象所处的套间的标识符,在套间建立时会为套间建立一个OXID,叫做对象引出标识符
 LUID  oid;    //存根管理器的标识符
 IID   ipid;    //接口存根标识符,用来唯一的标识套间中的一个接口指针,这跟接口的IID是不同的,IID是用来标识
                    //接口的,而IPID则是用来标识接口的指针的,一个是静态的,一个是动态的,这个标识符会放在
                    //代理方法调用时,放到数据包的前面,这样可以用他来找到相应的接口存根指针
 WORD chs;     //主机信息的字符数
 WORD offset;  //安全信息的偏移(也就是安全信息的字符数)
        [主机的信息]   //有没有这部分是根据dwDestContext指定的上下文来决定的.若是列集到不同机器上则这个部分描述本机的IPC端口信息
        [安全包信息]   //??
};


若是一个自定义接口的MEOW则格式如下:

    struct MARSHALINFO
{
       DWORD meow;
       DWORD flag;
       IID iid;
       CLSID clsid;  //代理对象的CLSID
       DWORD size;  //下面自定义结构的数据的SIZE
       BYTE array[1];
};

(4.3)因为一个存根管理器中实现的IMarshal接口,所以很容易得到这些信息.
(4.4)因为mshlflags中指定了列集的属性,包括一次列集, 强表格, 弱表格.所以将会增加存根管理器的外部引用。(具体参考列集的实现)

(5).经过一系列操作后CoMarshalInterface终于返回了,这样SCM就可以通过IStream中的内容转化为数据流返回给客户端了.

 

 

5.CoUnmarshalInterface的实现逻辑

  因为CoGetClassObject会在内部调用CoUnmarshalInterface.
   CoUnmarshalInterface总在客户所在的套间中调用,而且这个函数还负责实现代理对象的建立,接口代理的建立等。这个函数定义如下:

STDAPI CoUnmarshalInterface(
  IStream * pStm,
  REFIID riid,  //[IN], 需要散集的接口IID
  void ** ppv   //[OUT], 散集的结果
);


(1). COM将流信息转化为一个列集数据包结构(前面已经定义),然后判断Flag表示看是使用什么类型的列集,若是自定义的列集,则根据根据CLSID在注册表中查找代理对象并
     建立代理对象,然后查询代理对象的IMarshal接口,并将流对象和riid,ppv传递给IMarshal的UnmarshalInterface函数继续执行散集.因为代理对象实现了
     IMarshal所以当然知道该如何散集了.

(2).若是一个标准列集则处理不同, 每个客户的套间上都维护着很多的代理对象列表, 而代理对象通过(OXID,OID)来唯一标识一个代理对象。这样当列集数据包到来时则搜索匹配的(OXID, OID)所定义的代理对象,若是找到了则对这个代理对象查询riid指定的接口,若是找到这个接口了,则直接返回给客户端。而若是没有找到代理对象呢.因为列集数据包中指定是用标准列集的方法.因此客户端还是调用CoGetStandarMarshal来建立标准的代理对象.标准的代理对象也必须实现IMarshal接口, 调用CoGetStandarMarshal的格式如下:

CoGetStandarMarshal(riid,
                    NULL,       //必须是NULL,因为只有这样才能建立代理对象,否则就是建立存根管理器
                    0, NULL, 0,
                    &pMarshal    //输出代理对象的IMarshal接口指针

 

经过这样代理对象就建立起来了.当建立好了代理对象后COM库会将代理对象以及对应的OXID,OID保存起来以便以后使用.

不管如何我们最终都是要得到代理对象的IMarshal接口.

(3).当得到代理对象的IMarshal接口后,就调用接口成员函数:UnmarshalInterface.以便散集出接口代理指针.那么UnmarshalInterface内部是如何实现的呢.先看这个函数定义:

HRESULT UnmarshalInterface(
  IStream * pStm,
  REFIID riid,
  void ** ppv
);

 这个函数内部同样在注册表:HKEY_CLASSES_ROOT/Interface中的信息(同建立接口存根一样)。通过得到的CLSID建立接口代理类厂得到IPSFactoryBuffer接口指针,然后IPSFactoryBuffer调用CreateProxy建立一个接口代理,这个函数调用如下:

    HRESULT CreateProxy    //建立一个接口代理对象
    (
        [in] IUnknown *pUnkOuter,   //代理对象指针,因为代理对象聚合了接口代理
        [in] REFIID riid,
        [out] IRpcProxyBuffer **ppProxy,
        [out] void **ppv   //得到的接口代理指针,这个指针将最终返回给客户端使用
    );

经过调用这个函数返回给UnmarshalInterface,而UnmarshalInterface又返回给CoUnmarshalInterface,而CoUnmarshalInterface又最终返回给客户端.

 

着就是列集和散集的全过程

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:1513次
    • 积分:28
    • 等级:
    • 排名:千里之外
    • 原创:1篇
    • 转载:0篇
    • 译文:0篇
    • 评论:1条
    文章存档