windows COM调试[转]

windows COM调试[转]

Windows程序调试----第三部分 调试技术----第11章 COM调试 - Tiewen的专栏 - 博客频道

    调试COM代码对很多开发人员来说是很件令人沮丧的事情,特别是如果你对Windows平台,尤其是COM编程不熟悉的话。成功地解决一个错误常常需要涉及到很多领域的知识和技巧,包括:

    •对操作系统构造的深刻理解,例如进程、线程和DLL等。

    •对与COM编程有关的规则的深刻理解,例如引用计数,内存分配和线程。

    •对与网络和安全有关的细节及其含义的清醒认识。

    •对于MTS/COM+代理体系结构的了解。

    •了解其他产品,例如IE和IIS,是如何处理你所调试的代码的。

    如果你的开发知识和技巧不够,这些可以使COM代码的调试成为很恐怖的任务。不过解决某一个特定的错误时,你可能不需要掌握上面所列的所有技巧:到底需要哪些技巧和技术依赖于你写的代码的类型和代码运行所在的平台。

11.1 本章基础

先期知识

    本章的焦点是调试技术,而不是COM体系结构和编程,所以我假设你已经有了COM编程的经验,并且了解:COM使用的基于接口的编程思想、进程内与进程外组件的区别、代理和存裉(stub)的含义、基于DLL的和基于EXE的COM服务器的自注册过程。

    如果在MTS或COM+运行时刻环境中开发代码,我们还假设你了解术语“上下文”(context)的含义,以及MTS/COM+如何通过一个代理进程管理你的DLL。

使用的术语

    现在我们再一起看看本章用到的一些术语,这里给出简单的定义。这些术语是大部分新出版的与COM有关的文章和文档中都会使用的标准命名,但是我不能肯定你们看到的资料和我看的完全一样。我使用术语“基COM”来形容在MTS/COM+运行时刻环境之外采用的代码,例如基COM DLL或基COM EXE;用术语“配定组件”(configured component)来形容已经在MTS/COM+环境内部使用的COM DLL。

    考察配定组件,MTS和COM+之间还有一些差别(MTS是Windows NT4.0平台上基于基COM的一个附加技术:COM+是Windows 2000上的一个统一的COM-MTS构架)。其中的一些差别是表面上的(例如,监管代码的代理进程在Windows NT4.0上叫做Mtx.exe,而在Windows 2000上叫做Dllhost.exe),而另外一些差异是深层的。如果我讨论的技术在两个平台上都能应用或者只存在一些表面差异(例如代理EXE的名字),我会直接使用该术语,例如配定组件、代理或者目录(与配置有关)。当存在功能上的不同时,我会明确地使用术语MTS或COM+,分别指示Windows NT4.0和Windows 2000环境。

组织

    本章的内容是这样组织的:如果你还没有开发过配定组件,你可以忽略涉及在这个环境下,调试技术的章节;如果你开发的是配定组件,不要跳过那些讨论基COM技术的部分,因为这些章节中讲述的许多技术也同样适用于你的环境,而我不会在配定组件一节中重复这些内容。

11.2 防御性的COM编程实战经验

    预防错误总是最有效的调试方法,所以先看看COM编程时各种预防错误的办法对大家应该有所帮助。对于一个新手,下面这段代码有什么问题:

        IShape* pShape;

        IColor* pColor;

        IGizmo* pToy;

        HRESULT hr;

        hr = CoCreateInstance(CLSID_Toy, 0, CLSCTX_ALL, IID_IColor, (void**)&pShape);

        hr = pShape->QueryInterface(IID_IColor, (void**)pColor);

        hr = CoCreateInstance(CLSID_Toy, 0, CLSCTX_ALL, IID_IGizmo, (void**)NULL);

    答案是:它能通过编译。其中所有的CoCreateInstance和QueryInterface调用都存在错误,会在运行时刻产生问题,但是编译器不能检查出来。第一个CoCreateInstance调用传递的接口ID错了,应该是IID_ISHAPE而不是IID_ICOLOR。QueryInterface调用应该传给pColor的地址(&pColor),而不是pColor本身。最后,第二个CoCreateInstance调用也很可笑,它把一个空指针传给了一个[out]参数。我在代码中犯了两种类型的错误。第一种错误是给—个返回新接口指针的函数传递了错误的IID;第二个错误是指针的问题,这是大部分返回接口指针的函数使用void**作为[out]参数的一个副作用,这迫使调用者进行类型转换,从而放弃了可以从编译器获得的帮助。只要小小地改变一下书写代码的方式,这两个问题都可以避免。

利用编译器

    Don Box在他的《Essential COM》(注:本社己出版)—书中介绍了一个名为IID_PPV_ARG的宏:

        #define IID_PPV_ARG(Type, Expr) \

            IID_##Type, reinterpret_cast<void**>(static_cast<Type**>(Expr))

    这个宏可以解决上面这段代码的两个问题,而且不影响运行时的开销。我们可以这样重新书写代码:

        IShape* pShape;

        HRESULT hr;

        hr = CoCreateInstance(CLSID_Toy, 0, CLSCTX_ALL, IID_PPV_ARG(IShape, &pShape));

    如果你不介意CoCreateInstance中最后两个参数的奇怪样子,这个方法已经很理想了。首先,static_cast操作使得任何类型名称和指定的指针参数之间的类型不匹配都会产生编译错误。这包括了传递错误的类型名称,例如IID_PPV_ARG(IColor, &pShape);或者指针错误,例如IID_PPV_ARG(IShape, pShape)。这两个失误都会导致编译错误,这对我们来说总比运行时刻的错误好得多。第二个好处是这个技术不导致任何运行时刻的开销,因为所有的事情都在编译时就做完了。

使用_uuidof

    Visual C++5.0引入了_uuid关键字。指定了一个带有uuid属性的对象,这个关键字能给出与这个对象相联系的GUID。例如,表达式_uuid(IShape)计算与接口IShape联系GUID,_uuidof(pShape)也是如此。使用这个关键字可以书写如下的代码:

    IShape* pShape;

    HRESULT hr;

    hr = CoCreatelnstance(CLSID_Toy, 0, CLSCTX_ALL,

                      _uuidof(pShape), (void**)&pShape);

    这样书写代码能保证传递给CoCreateInstance函数的IID一定是正确的,即使后来你决定把pShape变量的类型从IShape*改成IShapeEx。如果你使用IID_PPV_ARG,这时候你就得修改变量声明和使用宏这两个地方。这个例子的唯一缺陷就是CoCreatelnstance的最后一个参数仍有可能发生指针错误。不过,uuidof关键字在一些IID_PPV_ARG不能使用的情况下也很有用。最常见的例子是调用CoCreateInstanceEx:

    IShape* pShape;

    ICoJor* pColor;

    HRESULT hr;

    COSERVERINFO csi = {0};

    MULTI_QI mqi[2] = { {&__uuidof(pShape), 0, 0}, // request IShape

                     {&__uuidof(pColor), 0, 0} };// request IColor

    hr = CoCreateInstdnceEx(CLSID_Toy, 0, CLSCTX_ALL, &csi, 2, mqi);

    使用__uuidof获得与一个接口类型或变量相联系的GUID。

使用com_cast

    在“利用编译器”小节中所说的问题产生的根源是很多COM函数使用void**参数返回[out]接口指针变量。即使是很久之前就熟悉了指针复杂用法的有经验的C++程序员也免不了偶尔在什么地方漏掉或多加一个&符号。但是,因为我们不得不把表达式转换成vmd**,所以我们不能从编译器那里得到任何帮助。像IID_PPV_ARG这样的宏很有用,但是它们的使用只限于IID参数之后紧跟着一个void**参数的情形。如果你要调用的函数格式不是这样(比如void**和IID参数顺序相反,或中间夹着别的参数),你就需要一个独产的宏,只封装这两个转换操作。虽然这个方法是行的,但我这里要介绍的是另一种方法,可以完成相同的功能,而不需要使用预处理器,看起来就像个C++转换操作符:

        template<typename T>

        void** com_cast(T** ppi) {

            _ASSERTE(ppi != 0);

            return reinterpret_cast<void**>(ppi);

        }

    这个模板函数是在一个头文件中内嵌定义的。用它可以写这样的代码:

    IShape* pShspe;

    HRESULT hr;

    hr = CoCreateInstance(CLSTID_Toy, 0, CLSCTX_ALL,

                      __uuidof(pShape), com_cast<IShape>(&pShape));

    这里CoCreateInstance函数的最后一个参数外观像一个C++转换操作符,可以达到与基于宏的方法相同的效果。编译器隐含地对传递给com_cast的参数执行次satic_cast,模板函数再做一次reinterpret_cast。在调试版本中,使用com_cast会导致额外的运行时刻的开销,因为这里多了一个函数调用,这一点与使用IID_PPV_ARG的解决方案不同。但是,这种方法允许在com_cast中加入断言。在IID_PPV_ARG的使用模式下,不可能使用断言。在发行版本中,com_cast的函数体中实际上不执行任何工作(断言在发行版本中不出现,类型转换是编译时刻做的检査),所以在产生的代码中没有额外的运行时刻开销。

    使用com_cast消除强制转换void**[out]参数的潜在危险。

处理引用计数

    很多C++ COM开发人员可能都听到过这个说法:C++是COM的汇编语言。除了相对简单的指针处理和类型转换,COM程序员必须遵守很多规则和习惯用法。使用高级语言例如Visual Basic的程序员都能从严格自动遵守这些规则的运行时刻环境中获益;但是C++程序员却没有这样幸运。很多C++开发人员遇到的最主要的挫折来自于引用计数。

    当涉及到引用计数的处理时,如果你想用C++写出没有错误的COM程序,最好使用智能指针。调试工具还没有发展到那种程度:C++程序员只需要在调试器中运行他们的程序,就可以很方便地确定是否忘了增加或减少某个特定的接口指针的引用计数。也许有那么一天,C++工具能进化到这种程度,会有一个这样的运行时刻环境,或者我们的调试工具能够检查出引用计数的问题。不过,如果你使用的是一个像ATL这样的基本结构,你可以使用一组智能指针模板类,它们为C++开发人员封装了COM引用计数。

    第4章介绍了ATL对几个函数的跟踪的支持——定义_ATL_DEBUG_QI跟踪QueryInterface,定义_ATL_DEBUG_INTERFACES跟踪AddRef和Release调用。这些预处理符号可以被组件开发人员定义,使得ATL的有关基类在每次调用QueryInterface、AddRef或Release的时候都会在输出窗口的Debug标签下输出消息。如果你是一个组件开发人员,结果信息可以帮助你査明客户程序代码中的引用计数问题。但是,如果你在编写客户端的代码,而且调用的组件不是支持这些特性的调试版本,你就完全不知道发生了些什么。

    这种情况下ATL的CComPtr和CComQiPtr模板类就很有用了,这两个类都封装了C++程序员必须遵守的COM引用计数的规则,在Brent Rector和Chris Sells所著的《ATL Internals》一书的第2章中提供了对CComPtr和CComQiPtr的详细讨论,在此我不再重复了。如果你还没有用过ATL,这两个模板类可以作为开始学习的很好的目标。你可能不能在你己有的代码基础上集成ATL,所以我先在这里写一个智能接口指针类。这个类不仅限于ATL,本节的后面部分将用它来写一些代码。

    一般来说,一个智能接口指针的行为包括以下几点:

    •在初始化和赋值操作时调用被封装的接口指针的AddRef函数;

    •在赋值语句重写指针的时候自动释放被封装接口指针的Release函数;

    •在类析构函数中自动释放被封装的接口指针;

    •看起来和用起来就和普通的指针一样。

    下面这段代码提供了这些要求的部分实现:

        template <typename I>

        class InterfacePtr {

        public:

            InterfacePtr():m_pi(0){} // initialize encapsulated pointer to NULL

            ~InterfacePtr(){

                if(m_pi) {

                    m_pi->Release(); // release encapsulated pointer

                    m_pi = 0;

                }

            }

            I* operator ->() {_ASSERTE(m_pi != 0); return m_pi;}

            I** operator &(){return &m_pi;}

            operator = (const InterfacePtr<I>& rhs) { // assignment

                if(m_pi) m_pi->Release();

                m_pi = rhs.m_pi;

                if(m_pi) m_pi->AddRef();

            }

            operator = (const I* p) { // assignment

                if(m_pi) m_pi->Release();

                m_pi = p;

                if(m_pi) m_pi->AddRef();

            }

        private:

            I *m_pi; // the encapsulated "raw" interface pointer

        private:

            InterfacePtr(const InterfacePtr<I>& init); // not supported

        };

    有了这个类,我就可以这样写我的代码:

        void UseSomeToys(IToy* pToy) {

            InterfacePtr<IToy> pLocalToy;

            HRESULT hr;

            hr = CoCreateInstance(CLSID_Toy, 0, CLSCTX_ALL,

                __uuidof(pLocalToy), com_cast<IToy>(&pLocalToy));

            if(SUCCEEDED(hr)) {

                pLocalToy->DoSomeThing();

                pLocalToy = pToy; // release pLocalToy.m_pi, AddRefs pToy

                pLocalToy->DoSomeThing();

            }

        } // releases pLocalToy.m_pi

    当自动局部变量pLocalToy进入作用域的时候,类的构造函数初始化被封装的接口指针,令它为空。如果我试图在初始化pLocalToy之前释放它,重载的操作符->会通过断言检查出这个问题,并提供比一个未捕捉的异常消息框有用得多的信息。

    通过重载地址运算符(运算符&),我可以在CoCreateInstance调用中使用表达式&pLocalToy。关于这个操作符实现的细节问题和深入讨论,请参考Rector和Sells在1999年出版的著作的第2章。类似地,重载的赋值操作符允许pLocalToy能被安全地修改成指向一个新的接口指针(这个指针是UseSomeToys的调用者传递进来的),同时能够遵守引用计数的使用规则。因为析构函数完成了释放接口指针的功能(即使是在异常发生的情况下),在UseSomeToys函数返回的时候一定能保证接口指针被释放了。

    这个智能接口指针类的例子只是一个部分的实现,因为它没有完成所有引用计数问题的处理,例如,因为操作符->被重载了,返回被封装了的原接口指针,我仍旧可以调用pLocalToy->AddRef(),虽然这显然破坏了InterfacePtr类试图提供的维护引用计数的功能。ATL的智能接口指针类提供了完整的实现,能解决所有这方面的问题。如果你在使用ATL,但还没有用过InterfacePtr类,现在很值得花点时间来研究它们;如果你没有用过ATL,可以在你自己的环境中加入InterfacePtr类的精神,不过要注意参照ATL的CComPtr类的实现。

查明激活失败的原因

    防范性的COM开发技术的最后一点试图解决激活时的问题:当你调用COM的任何一个激活函数时(例如CoCreatelnstance、CoCreateInstanceEx和CoGetClassObject),都有可能发生各种问题。我们来看看在上面的UseSomeToys函数中,CoCreateInstance调用可能产生些什么问题:

    •服务器程序没有正确地注册。

    •如果服务器程序在客户的进程外运行,特定接口的代理/存根可能没有正确地注册。

    •如果服务器程序运行时使用的用户ID与客户程序不同,客户程序可能没有对服务器程序的发射或访问权限。

    •如果服务器程序在另外一台机器上运行,网络身份认证可能失败。

    •ATL代码没有在类映射(就是BEGIN_OBJECT_MAP)中列出CLSID。

    •ATL代码没有在接口映射(就是BEGIN_COM_MAP)中列出接口IID。

    如果你试图调试一个在激活特定服务器时发生的错误,以上这些可以作为一份诊断检查列表。如果你发现自己经常需要调试激活失败,那你最好把这份列表记在脑子笔,或者打印出一份放到你的显示器旁边。更好的做法是,你可以把这份检查列表嵌入到一个激活函数中,在你的代码中所有调用CoCreatelnstance的地方使用。

_CoActivateServer

    _CoActivateServer的主要目的是将激活检査列表嵌入到代码中,可以生成调试输出消息,帮助我们确定问题可能出在什么地方。这个函数还有另外一个表面上的好处,就是为那些CoCreatelnstance中不那么经常使用的参数提供了默认的参数。下面是调试版本中这个函数的实现(Keith Brown在讲授Programming NT Security课程(我为Developmentor讲授该果程)中,受到了某些编程练习的启示,于是该实现产生了。如果你是ATL开发员,把所有对InterfacePtr类的引用改成CComPtr吧)(在发行版本中的实现我们稍后再讨论):

        template<typename I>

        HRESULT _CoActivateServer(CLSID clsid, I** ppi, DWORD locality = CLSCTX_ALL,

            const OLECHAR* pszServer = 0,

            char* pszFile = 0, int nLine = 0) {

            *ppi = 0;

            // Step 1: try to activate server using class loader

            InterfacePtr<IUnknown> pUnkClassObject;

            COSERVERINFO csi = {0, const_cast<wchar_t*>(pszServer), 0, 0};

            HRESULT hr = CoGetClassObject(clsid, locality, &csi,

                __uuidof(pUnkClassObject),

                com_cast<IUnknown>(&pUnkClassObject));

            if(FAILED(hr)) {

                TraceActivateError(

                    "CoGetClassObject", hr, pszFile, nLine,

                    ">> Is the server registered?\n"

                    ">> Is the CLSID listed in the OBJECT_MAP?\n"

                    ">> Is there a problem with authentication?\n"

                    ">> Do you have launch permission?\n"

                    ">> Do you have access permissions?\n" );

                return hr;

            }

            // Step 2: acquire class loader's instantiation interface

            InterfacePtr<IClassFactory> pcf;

            hr = pUnkClassObject->QueryInterface(

                __uuidof(pcf),

                com_cast<IClassFactory>(&pcf));

            if(FAILED(hr)) {

                TraceActivateError(

                    "QI for IClassFactory", hr, pszFile, nLine,

                    ">> Is there a problem with authentication?\n" );

                return hr;

            }

            // Step 3: instantiate requested class, asking for IUnknown

            InterfacePtr<IUnknown> pUnkServer;

            hr = pcf->CreateInstance(0, __uuidof(pUnkServer),

                com_cast<IUnknown>(&pUnkServer));

            if(FAILED(hr)) {

                TraceActivateError(

                    "IClassFactory::CreateInstance", hr, pszFile, nLine,

                    ">> Hmm...might have a corruption problem in"

                    "one or more constructors.\n" );

                return hr;

            }

            // Step 4: finally, ask for the requested interface

            hr = pUnkServer->QueryInterface(__uuidof(I), com_cast<I>(ppi));

            if(FAILED(hr)) {

                TraceActivateError(

                    "QI for interface", hr, pszFile, nLine,

                    ">> Is the requested interface supported by the object?\n"

                    ">> Is the IID listed in the COM_MAP?\n"

                    ">> Did you register the proxy/stup DLL?\n"

                    ">> Do you have access permission to the interface?\n" );

            }

            return hr;

        }

    通过激活的逐歩实现,这个函数能够给出关于失败原因的详细反馈,错误信息以跟踪消息的形式,使用辅助函数TraceActiveteError传递给调试器(这个函数会在本节的后面部分介绍)。这个方法的缺点是如果要激活的服务器程序在另一台机器上,该函数会导致更多的网络往返开销。_CoActivateServer的发行版本消除了额外的通信往返开销。下面这个例子演示了如何调用_CoActiveteServer:

        IShape* pShape;

        HRESULT hr;

        hr = _CoActivateServer(CLSID_Toy, &pShape);

    在客户端代码中,_CoActiveteServer详细记录了COM服务器的激浩失败,并提供诊断问题的建议。

    既然_CoActiveteServer是一个模板函数,并被第二个参数的类型隐含地参数化(本例中是pShape),编译器仍然可以检测出任何指针错误,并且在调用Queryhterface的时候自动使用正确的IID。

    在第一步中,_CoActivateServer使用CoGetClassObject试图激活被请求的服务器,先请求类启动者的IUnknown接口指针。如果这步失败了,一般是因为服务器没有注册(因此CLSID不能被COM识别),或者ATL代码没有在类映射表中列出请求的CLSID。如果服务器程序是在客户进程外运行的,而且还没有被启动,这一步也要求客户进行身份认证,并对服务器程序有足够的发射权限。不管是服务器程序己经在运行,还是这个激活请求导致服务器被发射,客户程序都必须有足够的访问权限。这里我们先请求IUnknown接口,这样可以避免任何由未注册的代理/存根引起的潜在问题。

    第二步,其实是为捕捉身份认证错误而设计的一个检査,很多程序员都会把这一步合并到第一步中去。如果第二步失败了,请检查客户程序是否能通过服务器程序的身份认证。

    第三歩应该永远不会失败。如果你成功地通过了第一歩和第二歩,调用IClassFactory:::CreateInstance失败通常意味着服务器发生了什么灾难性的错误。首先检查网线是不是插好了,然后检查实现类的构造步骤。因为CreateInstance的实现一般最终归结到调用new初始化C++实现类的一个新的实例,这时候发生错误通常意味着构造函数或构造函数调用的代码中存在内存错误。

    第四步向新产生的服务器对象实例请求接口指针,_CoActivateServer将把这个指针返回调用者。这一步可以独立出任何QueryInstance引起的错误。检査ATL代码的接口映射表中所列的请求IID,同时检査所需的针对特定接口的代理/存根是否己经注册了。如果被激活的对象位于一个配定组件中,确定客户程序对被请求的接口有足够的访问权限。

    下面是另一个函数TraceActiveteError的实现。这个函数比起_CoActivateServer略为逊色,但也是一个很有用的辅助函数:

        void TraceActivateError(char* pszFunc, HRESULT hr,

            char* pszFile, int nLine, char* pszHint) {

            BOOL fOkay;

            char* pszMsg = new char[512];

            char* pszError = 0;

            fOkay = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM |

                FORMAT_MESSAGE_ALLOCATE_BUFFER,

                0, hr, 0, (LPTSTR)&pszError, 0, 0);

            wsprintfA(pszMsg, "%s failed:\n:%s(%d): 0x%08x, %s%s",

                pszFunc, (pszFile?pszFile:"unknown file"),

                nLine, hr,

                (pszError?pszError:"unrecognized error\n"),

                pszHint);

            OutputDebugStringA(pszMsg);

            if(pszError) LocalFree((HLOCAL)pszError);

            delete []pszMsg;

        }

    这个函数检查指定的HRESULT,并且在输出窗口的Debug标签下打印一条消息,消息的内容包括HRESULT的数字值、这个代码的文本解释、发生错误的调用者代码的源文件和行数。例如,如果_CoActivateServer中最后对QueryInstance的调用失败了,会产生一个这样的跟踪消息:

        QI for interface failed:

        unknown file(0): 0x80004002, No such interface supported

        >> Is the requested interface supported by the object?

        >> Is the IID listed in the COM_MAP?

        >> Did you register the proxy/stup DLL?

        >> Do you have access permission to the interface?

    如果调用_CoActivateServer失败,这个跟踪消息能提供一些线索,告诉我们可能是什么原因导致失败,注意这个例子中调用者的源文件和行数并不知道。这是因为调用_CoActivateServer时要传递源文件名和行数太麻烦了,所以这里我调用_CoActivateServer时只显式地指定前两个参数,让编译器为其余参数指定默认值。不过,宏提供了一种简单的方法可以传递源文件名和行数给_CoActivateServer。

TRACE_ACTIVATE

    如果_CoActivateServer函数中Locality和pszServer参数的默认值你觉得可以接受,那么最好使用一个我称之为TRACE_ACTIVATE的宏来调用_CoActivateServer。这个宏的定义如下(你可以自己定义这个宏,指定不同的上下文标志、服务器程序主机名等):

        #define TRACE_ACTIVATE(_clsid, _ppi) \

            _CoActivateServer(_clsid, _ppi, CLSCTX_ALL, 0, __FILE__, __LINE__)

    这个宏把文件名和行数传给_CoActivateServer,使得最后的跟踪消息能指出代码中发生错误的精确位置。所以,如果我现在修改原来的_CoActivateServer调用语句,使用这个宏:

        IShape* pShape;

        HRESULT hr;

        hr = TRACE_ACTIVATE(CLSID_Toy, &pShape);

    结果的跟踪消息形式如下:

        QI for interface failed:

        c:\dcv\vc\FooClient\FooClient.cpp(32): 0x80004002, No such interface supported

        >> Is the requested interface supported by the object?

        >> Is the IID listed in the COM_MAP?

        >> Did you register the proxy/stup DLL?

        >> Do you have access permission to the interface?

    在跟踪消息中包含这个额外的信息有两个好处。第一,错误的精确位置在输出窗口中记录,即使你没有单步跟踪代码,你也不会为到底是哪条语句调用了_CoActivateServer并引起失败而困惑。第二,跟踪消息被格式化了,如果你双击跟踪消息中显示源文件名和行数的那一行,Visual C++编译器就会自动打开指定文件,并定位在出错的这一行上。

_CoActivateServer及其发行版本

    因为_CoActivateServer是在头文件中内嵌定义的,我们很容易提供一个更有效的版本,使它不会导致调试版本中存在的额外通信开销问题。下面是_CoActivateServer和TRACE_ACTIVATE宏的发行版本:

        template<typename I>

        HRESULT _CoActivateServer(CLSID clsid, I** ppi, DWORD locality = CLSCTX_ALL,

            const OLECHAR* pszServer = 0, char* pszFile = 0, int nLine = 0) {

            MULTI_QI mqi = {&__uuidof(I), 0, 0};

            COSERVERINFO csi = {0, const_cast<wchar_t*>(pszServer), 0, 0};

            HRESULT hr;

            hr = CoCreateInstanceEx(clsid, 0, locality, &csi, 1, &mqi);

            if(SUCCEEDED(hr)) {

                *ppi = reinterpret_cast<I*>(mqi.pItf);

            }

            else

            {

                *ppi = 0;

            }

            return hr;

        }

        #define TRACE_ACTIVATE(_clsid, _ppi) _CoActivateServer(_clsid, _ppi)

11.3 调试基COM DLL

    调试基COM DLL最终通常归结为两个活动;①验证是否有配置上的问题:②有效地使用调试器。

配置问题

    如果一个客户程序想成功地激活一个进程内的COM服务,必须先在客户程序所在的机器上注册这个服务器程序。不幸的是,基COM不提供—个安装 API(installation API)供服务器程序调用以通知COM服务控制管理器(SCM)它们的存在(像作者一样,该语句假定你不会用注册APL来修改别人注册正确安装的APL的区域)。代替方法是,COM要求服务器程序实现并输出众所周知的自注册函数DllRegisterServer和DllUnregisterServer。如果你在激活COM服务器程序时遇到麻烦,可以快速检查下面这几项:

    1.服务器是否已经在客户程序所在的机器上注册了?

    2.该目录下的DLL是否还在最初注册时的位置?你使用的DLL是否是你想用那个版本?

    3.其他客户程序能否激活这个服务器?

    4.如果DLL在一个多线程的客户程序中使用,是否注册了合适的代理/存根?

服务器程序是否已经注册

    服务器程序必须负责提供自注册函数,但是如果这些函数永远不被执行,那也一点用处都没有。典型的安装脚本会自动调用自注册函数;不过如果你手工安装一个COM DLL,运行regsvr32注册服务器程序就是你的责任了。如果你是被调试的COM服务器程序的作者,或者你手工把一个COM DLL拷贝到了一台新的机器上,一定要确保在开始调试之前运行regsvr32。因为ATL应用向导会产生一个makefile,在编连的最后一步注册COM DLL和EXE,很多开发人员总是忘记注册是每一台新机器上安装DLL时必须明确完成的一步,而不仅仅是编连该DLL的那台机器。

DLL是否在最初注册时所在的目录

    COM SCM使用一个指定对象的CLSlD来确定服务器DLL的位置。如果你想知道SCM认为DLL位于何处,最简单的办法就是使用Visual C++的OleView工具。要运行OleView,请在Tools菜单中选择OLE/COM Object Viewer命令:在OleView中展开All Objects条目,找到你想激活的服务器程序,如果服务器注册时用一个基于文本的名字注释了自己的CLSID项,那么你会在列表中看到这个文本名字;否则,列出的是CLSID。当你选中这个服务器的项时,OleView会在右侧的属性表的Registry标签中显示有关的注册项目。LocaIServer32项会告诉你SCM认为对应被选中CLSID的DLL在什么地方。验证这个DLL是否的确在文件系统中的指定位置。如果DLL被移到了另一个目录,注册表中的这一项可能就指到了错误的位置。如果遇到这种情况,就应该重新注册这个DLL。注意,你也可以使用注册表编辑器来诊断注册问题,但我发现使用OleView更能提供信息,并且不容易出错。

    这个问题的一个变体是你的DLL从一个版本变成另一个版本。例如从调试版本到发行版本。如果你的工程中没有包括在编连之后自动运行Regsvr32注册这一步,COM SC就不知道你希望客户程序使用DLL的一个新的版本。这个问题也可以通过重新注册DLL解决。

    如果你将COM DLL拷贝到一台新的机器、移动到一个新的目录或者使用一个新的版本,不要忘记注册你的COM DLL。,

其他客户程序能否激活这个服务器

    如果看起来一切正常,但你的客户仍然不能激活服务器,在OleView中右键单击Server条目,并选择Create Instance。这个动作将使得OleView在服务器的CLSlD上调用CoCreateInstance,并请求一个IUnknown接口指针。如果OleView成功地激活了服务器程序,它会在树视图中用粗体字显示该服务器条目,并展开该项,列举被选中服务器支持的所有接口。如果激活成功,说明问题出在客户程序的代码中;否则,问题出在DLL自身。

是否注册了合适的代理/存根

    如果是在一个多线程的环境中使用DLL,你要保证客户用到的所有接口都注册了合适的代理/存根。只要一个方法调用跨越了套间边界(apartment boundary),就必须使用代理和存根(事实上,你每次跨越环境边界都需要理/存根。跨越套间访问不过是个例子、随着配置部件的出现,套间陂分成了环境,当你跨越任何一个时都需要代理/存根对。如果你已经开发了配置部件,可能已经注意到这一点了)。要验证是否注册了合适的代理/存根,运行OleView,展开树视图中的Interfaces项,确认服务器程序实现的所有接口都列出来了。如果少了一个接口,COM SCM就无法知道到哪里去为这个接口找合适的代理/存根。如果发生了这种情况,你必须找到或者重新编连与该服务器DLL相联系的代理/存根DLL,并用Regsvr32注册之。

    对OleView中列出的每个接口,査看属性表中的Registry标签。这里列出其代理/存根实现的CLSID,还有一个LocalServer32值,对于标记为[dual]或[oleautomation]的接口,这个值指向一个系统DLL;或者它指向一个自定义的代理/存根DLL——你不仅要注册服务器DLL自身,还要编连并注册这个DLL。如果使用自定义代理/存根,请确认这个代理/存根DLL可以在指定的位置上找到,如果不能,重新注册这个代理/存根DLL。如果代理/存根DLL确实在OleView希望它所在的位置,那就有可能是你修改过这个接口的IDL文件,但是忘了重新注册代理/存根DLL。如果是这种情况,重新编连并重新注册这个代理/存根DLL。

    如果要访同的接口指针跨越了套间边界,而且这个接口没有被标记为[dual]或[oleautomation],你必须为这个接口重新编连并注册代理/存根DLL。

客户端的调试技术

    调试一个基COM DLL和调试一个普通的DLL类似,只不过客户代码让COM SCM调用LoadLibrary和GetProcAddress函数。如果你的COM DLL中有调试符号,第一次在客户地址空间中激活该DLL的时候,Visual C++会自动载入这些符号。一旦你得到了一个指向该DLL的接口指针,单步跟踪这个接口的某个方法调用可以使Visual C++跟踪进入它的源代码。你不必做任何特殊的操作来实现这个目的。

    如果你在DLL源代码中设置了断点,Visual C++会记住这些断点都设在了什么地方,但是,一旦你停止调试并启动一个新的调试会话,Visual C++就会警告你不能设置在DLL中的断点。为了解决这个问题,你必须让Visual C++预先载入被调试的DLL的调试符号。在Project菜单中选择Settings命令。在工程设置对话框中,选择Debug标签,然后在Category列表框中选择Additional DLLs。现在,为模块列表输入DLL的全路径名。这将使得以后你每次在该客户工程中开始一个调试会话的时候,Visual C++都会为指定DLL载入调试符号。它保证你在DLL中设置的断点会在跨调试会话的时候仍然有效。

服务器端的调试技术

    有时候你知道不是客户程序的错误。可能客户程序很久之前就写好并且调试好了,或者可能你根本就没有客户程序的源代码(例如你写的是一个被IE调用的ActiveX控件)。不管是以上的哪种情况,总之你希望在服务器工程里开始一个调试会话。请选择Project菜单中的Settings命令。在工程设置对话框中,选择Debug标签,然后在Category列表框中选择General项。在Executable for Debug session框中输入客户可执行文件的全路径,并在Program argument文本框中输入该客户程序所需的任意命令行参数。现在,当你启动一个调试会话的时候,Visual C++就会发射指定的程序,并传递指定的命令行参数。这个技术可以用来在任何环境下调试你的Visual C++组件。

    在指定要发射的可执行文件名字的时候,如果这个文件不在工作目录中(见Debug标签下的设置),一定要指定该程序的全路径名。调试器会自动载入所选程序的调试符号,如果无法载入会给出警告信息。这个问题不大,因为Visual C++仍然会使用你在DLL工程设置的断点(只要该工程有调试符号)。另外,Visual C++还提供了一些捷径供开发人员选择需要发射的可执行文件,例如,如果你在开发一个ActiveX控件,希望使用ActiveX控件测试包容器调试你的组件,可以直接单击Executable for Debug session框右边的按钮,选择(Active Control Test Container ActiveX控件测试包容器)即可。如果你想要调试的组件在一个Web页面的脚本中被调用,可以选择Default Web Browser指定浏览器可执行文件。

    如果基COM DLL被一个不是你开发的客户程序调用,可以在Project Settings属性页的Debug标签中使用Executable for Debug session选项进行调试。

初始化和激活代码的调试

    如果服务器程序激活失败,你现在在客户工程中调试,你必须使调试器在达到第一个激活请求之前的某个时刻预载入DLL的调试符号,而不是等待直到COM SCM调用LoadLibrary。一旦载入了调试符号,你就可以在服务器DLL中设置断点了。如果在服务器工程中进行调试,可以在开始调试会话之前设置断点。不管是以上两种情况中的哪一种,关键是要在合适的地方设置断点从而揭露出问题。下面是一些常见的出问题的地方:

    1.DllMain中,如果你在这里做了什么处理的话。

    2.DllGetClassObject中,如果你要调试的错误是“class not registered”(类没有注册)或者其他早期的激活错误。如果你使用ATL实现IClassFactory,最可能发生问题的地方是类映射表中缺少条目,所以在使用调试器之前请先检查这个问题。

    3.如果你用ATL实现被请求的接口,注意该C++类的构造函数,尤其注意最有意思的初始化代码都在FinalConstract中。

    4.如果你要诊断的错误是“interface not supported”(接口没有被支持),请注意QueryInterface的实现。如果你使用ATL实现IUnknown,QueryInterface是自动实现的,所以最可能发生的错误是在接口映射表中缺少条目。

    一旦设置好了你的断点,初始化调试会话,并跟踪发生错误的代码段。

    要调试初始化和激活代码,预装载DLL的调试符号,并且在客户调用CoCreateInstance或其他激活函数之前设置断点。

调试ATL自注册代码

    如果你自己实现DllRegisterServer,对自注册代码的调试就和其他普通函数的调试一样:在DllRegisterServer上设置一个断点,然后开始调试。不过,如果你用的是ATL,自注册的工作由一个名叫注册器(registrar)的ATL组件完成。这个注册器组件能够分析驱动自注册进程的注册脚本文件(RGS)。

    如果你没有修改过ATL应用向导生成的RGS文件,一般不可能遇到问题。如果你从来没有修改过RGS文件,而自注册失败了,最著名的个案就是注册组件时登录到Windows 2000上的账号不是本地系统管理员组的成员。如果DllRegisterServer失败了,请先检查你使用的是否是管理员账号。

    如果你是以管理员身份登录的,而DllRegisterServer还是出错,你就得精确地定位在自注册过程中发生错误的位罝。一个比较费事的方法就是审查RGS脚本,直到找出脚本的哪一部分可能在注册器组件处理时会有问题。所幸,ATL提供了一个冗长但是很有用的注册器组件版本,你可以通过以下步骤激活它:

    1.打开工程设置对话框,并选择Win32 Debug build。

    2.单击C/C++标签,并在Preprocessor definition文本框中输入符号_ATL_STATIC_REGISTERY。

    3.单击DEBUG标签,设置调试会话的可执行文件为Regsvr32的全路径名(System文件夹下的Regsrv.exe文件)。

    4.将Regsvr32的程序参数设置为“.\debug\YourDllName.dll”。

    5.单击OK保存这些设置。

    6.重新编连你的DLL。现在,它将使用注册器组件的一个静态链接版本,这个版本的注册器很长,会告诉你RGS脚本的哪一部分它不能识别或处理。

    7.启动调试器。你不需要设置任何断点;你只要让调试器发射Regsvr32,后者又会激活DllRegisterServer函数。让Regsvr32运行完。

    做完这些之后,查看输出窗口的Debug标签,这个冗长的注册器组件在从DLL资源数据库中读入RGS脚本的同时,会使用 API函数OutputDebugString在输出窗口中显示RGS脚本的内容。在输出窗口中,紧跟着RGS脚本的是关于注册器的动作和结果的详尽描述。如果注册器遇到了任何困难,例如非法的RGS语法或者其他运行时刻错误,它会告诉你问题出在什么地方。解决这个问题,再重复上述过程,直到注册器成功地注册了你的服务器程序。

    在你的ATL工程中定义预处理符号_ATL_STATIC_REGISTERY诊断RGS脚本的错误。

11.4 调试基COM EXE

    很多时候,调试进程外的COM服务器最终归结为诊断并改正配置问题。本节的大部分内容介绍对COM程序员有影响的最常见的配置问题。说到调试器的使用,基COM EXE的调试和普通的Win32程序的调试几乎一模一样。在Visual C++中打开服务器工程(或者将Visual C++附着到一个已经开始运行的服务程序上),设置断点,开始调试。除了基本的调试技术,Visual C++还提供了一个非常有用的特性,叫做OLD RPC调试,它将跟踪客户程序的过程简化为一个在服务器程序中实现的方法调用。在介绍配置问题之后,我会解释如何使用这个OLE RPC调试。

配置问题

    如果你要编连一个典型的进程外服务器可执行文件,在使用调试器之前你应该检查的诊断列表是我在前一节(“调试基COM DLL”)中所列项目的一个超集。以下是处理进程外服务器程序时你应该额外注意的一些配置问题:

    1.对服务器程序的发射权限是否正确?

    2.对服务器程序的访问权限是否正确?

    3.服务器进程的标识是否设置正确?

    4.是否存在身份认证的问题?

对服务器程序的发射权限是否正确

    当有人发出一个激活请求时,COM SCM自动发射一个服务器进程。不管做出激活请求的客户是否有发射权限,服务器进程在服务器位置上都是可配置的。要确定一个给定AppID的发射权限,可以运行DCOM配置工具(System文件夹中的Dcomcnfg.exe)。在已安装COM程序的列表中找到对应的AppID,单击Properties并在结果属性页中选择Security标签。如果选中的是Use custom launch permissions,单击Edit按钮,DCOM配置工具会显示一个对话框,列出所有允许发射这个服务器程序的安全角色(包括个人和组)。如果发出激活请求的客户不在列表中,也不是列出的任何一个组的成员,它就不能被允许发射服务器程序,你只有或者把客户账号加入列表,或者换一个被赋予了发射权限的账号运行客户程序。

    如果Use default launch permissions被选中,或者在DCOM配贯工具的ApplD列表中没有列出这个服务器,就会使用默认设置管理这个服务器的发射权限(该默认设置对本计算机上的所有组件适用)。释放应用程序属性表,返回到开始的DCOM配置窗口,选择Default Security标签,然后单击Default launch permissions中的Edit Default按钮。和自定义发射权限一样,确保发生激活请求的客户有发射服务器程序的权限。

对服务器程序的访问权限是否正确

    一旦服务器进程进入运行(不管是服务器原来就已经启动,还是作为调用客户的激活请求的结果被启动),就要用到访问权限。只有那些被明确地赋予了访问权限的客户能够激发特定服务器进程中的函数。访问权限的设置有两种方式:使用注册表和DCOM配置管理工具,或者在服务器程序的代码中指定。SCM确定访问权限的方法遵从如下逻辑(以伪代码形式给出):

        IF server is explicitly call CoInitializeSecurity THEN

            SWITCH(first parameter to CoInitializeSecurity)

                CASE NULL:

                    Everyone is allowed access permissions.

                END CASE

                CASE PointerToSecurityDescriptor:

                    The DACL here governs securiy(registry is ignored).

                END CASE

                CASE PointerToAccessControlImplementation:

                    The COM SCM calls IAccessControl::IsAccessAllowed.

                END CASE

                CASE PointerToAppIdGUID:

                    UsePermissionsFromRegistryForGivenAppId().

                END CASE

            END SWITCH

        ELSE

            IF server filename-to-AppId registry mapping exists THEN

                UsePermissionsFromRegistryForGivenAppid().

            ELSE

                UseMachinedWideDefaultsFromRegirstryForAccessPermissions().

            END ELSE

        END ELSE

    为什么要调用CoInitializeSecurity有很多可能的原因,不过归结起来只有一点,就是访问权限是否由注册表中的信息所决定。如果服务器程序开发者将一个安全描述户(security descriptor)或一个IAccessControI接口指针传给CoInitializeSecurity,开发者就指定了服务器的访问权限。如果这个服务器是你自己开发的,你已经知道了自己是怎么调用CoInitializeSecurity的。如果你使用的是别人开发的服务器,想知道服务器行为的唯一一方法是咨询服务器程序的开发者。在所有其他情况下,COM SCM根据注册表确定谁有权限访问服务器。

    如果访问权限是通过注册表设置的,你可以运行DCOM配置工具査看这些权限。和发射权限一样,查看服务器的安全属性类。如果选中了Use default launch permissions,单击相应的Edit按钮查看谁有权限访问该服务器程序。如果选中的是Use default access permbsions,或者在DCOM配置工具的ApplD列表中没有该服务器,就应用整个机器范围内的默认值,释放应用程序属性表,返回到开始的DCOM配罝窗口,选择Default Security键,然后单击Default Access Permissions中的Edit Default按钮。和自定义访问权限一样,确保发生激活请求的客户有访问服务器程序的权限。

服务器进程的标识是否设置正确

    当COM SCM激活一个服务器的时候,它在给定安全角色的安全上下文中执行程序(调用CreateProcessAsUser而不是CreateProcess)。如果你运行DCOM配置工具,显示指定服务器的属性页并选择Identity标签,就可以看到一个COM服务器进程可以使用以下三种标识之一运行:

    •交互用户(interactive user)

    •发射用户(经常叫做“激活者”(activetor))

    •本用户(经常叫做“指定用户”(distinguished principa))

    在调试期间,服务器程序应该设置成以交互用户方式运行,这个选项允许断言和其他基于窗口的反馈形式,能够被坐在运行服务器程序的计算机前的人观察到。这个人一般就是你,所以交互用户方式一般是你所希望的行为方式。将服务器设置成以激活者身份运行几乎没有意义,这是为了向后兼容而提供的选项(MTS和COM+取消了这个选项,只允许服务器程序以交互用户或者指定用户两种身份运行)。使用这种方式的问题是,SCM会为每个发出激活请求的客户发射一个单独的服务器程序拷贝,当系统有很多客户的时候,可扩展性就会很不好。另外一个问题是服务器进程的每个拷贝都运行在一个独立的,不可见的窗口中,这意味着如果服务器中的一个断言失败了,没有人能看到结果消息框,在每个客户眼中,服务器都像挂起了。在实际应用中,一般将服务器设置成以指定用户身份运行。这种方式的可扩展性最好,因为所有客户都连接到服务器进程的同一个实例。但是,服务器进程仍是在一个不可见的窗口中运行的,断言和其他形式的基于窗口的用户交互仍然不可见。

    除了使断言可见,还有一个原因促使我们选择交互用户作为服务器的配置。当服务器程序调用CoRegisterClassObject的时候,SCM检査调用进程的标识,并与相应CLSID的“run as”配置标识相比较。如果两者不匹配,CoRegisterClassObject返回一个CO_E_WRONG_SERVER_IDENTTTY,从而导致服务器失败。但是,SCM发射服务器进程的情况下这不是问题,因为SCM会使用适当的“run as”标识发射进程。但如果你通过打开服务器工作空间、设置断点、初始化调试会话的方式来调试服务器程序,这就会导致错误。这种情况下,服务器进程的进程标识继承自Visual C++进程,后者是作为交互用户运行的。如果服务器的设置不足以交互用户运行,CoRegisterClassObject就注定会失败。

    当调试服务器程序的时候,应将服务器设置成以交互用户运行。实际应用的时候将服务器设置成以指定用户运行。

是否存在身份认证的问题

    即使前面所说的所有配置问题都已经解决了,如果你的客户程序和服务器程序运行在不同的机器上,还是有最后一个障碍需要克服,那就是网络身份认证。与一个客户程序试图连接服务器的时候,SCM会要求服务器指定的发射和访问权限。为了支持远程激活的同时保证服务器的安全性,SCM在服务器所在的计算机上创建一个本地登录会话。这个登录会话的结果是产生一个代表客户标识的访问令牌,可以用来同意或拒绝对服务器的访问。为了使登录成功,客户必须能够通过服务器的身份认证。这意味着客户必须拥有一个服务器机器上的帐号,或者一个该服务器能够访问的域控制者的账号。如果这两点都不能做到,身份认证就会失败。

    如果想知道你是否遇到了身份认证的问题,可以运行本地安全策略微软管理平台MMC(在Windows 2000上),或者用户管理器(Windows NT4.0上),打开对登录和注销事件的监听选项,包括成功的和失败的。现在,对机器的任何登录企图,无论是交互式的,还是SCM应客户的请求在程序中实现的,都会在服务器的安全事件日志上留下一个记录。你可以重新尝试失败的访问过程,然后检查事件日志来确定问题是否与身份认证有关。

    监听登录和注销事件以帮助诊断身份认证失败。

    关于身份认证的最后一点说明:在典型的情况下,一个客户试图获取服务器程序的一个接口指针,但这个行为可能因为身份认证问题而失败。如果客户和服务器程序之间存在回调(callback)关系,服务器进程又调用客户进程的COM方法,服务器就必须在第一次进行这类调用的时候通过身份认证。这时,你又会有回调问题,可能在客户和服务器机器上都需要监听登录和注销事件。客户能够访问服务器并不意味着服务器一定能访问客户。任何一方都必须授予另一方适当的访问权限。

    当两方存在回调关系时,两方都必须拥有适当的访问权限。

客户端的调试技术

    在客户端代码的调试机制上,服务器是在进程内还是在进程外被调用造成的区别不大。Visual C++支持它称为“OLE RPC调试”的技术。如果这个特性被打开,你单步跟踪进入一个本地EXE服务器的方法的时候,Visual C++自动发射调试器的一个新的拷贝。将它附着在该服务器进程上。这个新的调试器实例在客户程序调用的方法的代码开始处停止,看起来就好像你跟踪进入了一个客户地址空间中的DLL实现的一个方法。实际上,当你离开方法的代码时,起初的Visual C++调试器实例(用来调试客户程序的那个)被带到前台,指令指针指向紧接着最初跟踪进入的方法的调用语句之后的那行代码。

    要打开OLE RPC调试功能,在TooIs菜单中选择Options命令,单击Debug标签,然后选择OLE RPC Debugging。要打开并使用这个特性,运行Visual C++时登录的账号必须是系统管理员组中的成员。

    打开OLE RPC调试,允许从客户程序跨地址空间地单步跟踪进入服务器程序。

服务器端的调试技术

    前面己经说过,对COM可执行文件的调试几乎和Win32程序的调试一模一样;但是,有两种技术在COM服务器程序调试中会有差别:DebugBreak和即时调试。APl函数DebugBreak抛出一个断点异常给调试器,如果没有调试器,Windows会发射Visual C++并将它附着到程序上。这种情况下,DebugBreak的处理就和任何其他异常相同。在一个COM服务器中,存根根据客户的要求处理对方法的调用,它会捕捉在方法中发生的所有未捕捉的异常。这意味着在COM方法中发生的错误(包括DebugBreak产生的异常)不会引起即时调试的发射,相反地,客户会看到一个代表错误的HRESULT。在调用COM方法的上下文之外发生的异常仍然可以被Windows捕捉,并且按标准模式传递给调试器。如果你的服务器己经附着有调试器,那么调试器仍旧可以看到first-chance异常并且作出应有的反应。

    存根会捕捉所有发生在COM方法内部的异常,从而降低了DebugBreak和即时调试技术在进程外(out-of-process)COM服务器调试中的作用。

11.5 调试配定组件

    很多初次涉足MTS/COM+开发的程序员觉得自己不得不重新学习如何调试。我自己就是这样。一个很大的区别是配置信息现在被分成了注册表和MTS/COM+目录两部分。另一个区别是虽然你仍旧是在开发DLL,但是这个DLL不再装载入客户地址空间(我假设你开发的是服务器包;如果开发库包,仍旧运行在客户地址空间)。好消息是配定组件的调试是在已经掌握的技术的一个简单的扩展,只偶尔在一些地方有一些变形。

配置问题

    除了安全属性是个例外,基COM服务器的配罝设罝仍旧在注册表中维护,例如CLSID映射、线程模式、接口代理/存根信息等。这样做是为了保证基COM服务器的向后兼容,但这同时也意味着我们还不能摆脱使用注册表的不幸历史岁月。安全设置(包括标识)和所有配定组件可以使用的新的配置选项现在都存放在MTS或者COM+目录里,这个目录提供一组可以在脚本中调用的COM接口,支持枚举、读、写等操作,安装程序员可以很方便地使用。目录也可以使用MTS或COM+浏览器图形化地进行管理。一般来说,在配定组件中诊断配置问题和基COM EXE服务器的诊断过程一样,区别仅仅是你使用MTS或COM+浏览器,而不是DCOM配置工具。当然,还是有几个方面必须了解。

不要在Windows NT4.0上自注册配定组件

    在Windows NT4.0上,MTS是一个附加技术。其实现不需要对COM基本架构做任何改动。这也带来了一个副作用,就是当你在MTS中设置DLL时,配置进程会调用你的自注册入口点(DllRegisterServer),并临听你对注册API的调用。MTS正是通过这个方法发现你的DLL支持的所有对象的CLSID。对于你支持的每个CLSID,MTS删除相应的InprocServer32注册值,用一个LocalServer32值代替,通知COM SCM发射Mtx.exe,并在命令行参数中标明你的组件所在的MTS包。这个过程使得客户程序察觉不到现在服务器DLL已经是一个运行在另一个地址空间的配定组件了。

    但是,如果之后你再运行RegSvr32注册这个DLL,你的自注册代码会重新添加InprocServer32注册值,从而抵消了MTS配置过程的作用。下一次客户程序试图激活服务器并在激活请求中带有CLTCTX_LOCAL_SERVER标志的时候,COM SCM就会直接把你的DLL装载到客户程序的地址空间里——这并不是你所希望的。ATL应用向导生成的DLL工程中包含了在编连之后自动运行Regsvr32的步骤(即使你选择了Support MTS);因此,在你编连DLL的时候,其实就撤消了MTS注册。

    要解决这个问题有两种方法。第一种,你可以在编连DLL之前在工程中去掉注册这一步。这种方法总是可行的。第二个选择是运行MTS浏览器,在Computers文件夹中右击MyComputer,选择Refresh All Components。这使得MTS重新检査你的自注册代码,修复注册表。你也可以在命令行里执行Mtxregeg.exe工具。具有讽刺意味的是,当你在ATL应用向导中选择支持MTS的时候,向导会添加一个编连后的自定义歩骤,警告你运行Mtxregeg。我猜想微软是为了向后兼容才选择不去掉RegSvr32调用的,在任何情况下,都没有什么限制会不让你手工去掉RegSvr32步骤,这正是我推荐的方法。

    对于配定组件,在ATL上应用向导生成的DLL工程中去掉自动的自注册步骤。

修改配置之前关闭服务器

    当你修改配置的时候,一定要确认服务器程序不在运行中。这个看起来很理所当然,但问题出在MTS和COM+都在最后一个客户结束连接之后仍然保持服务器进程的运行。所以,在进行配置修改之前简单地把客户程序停下来是不够的。当最后一个客户断开连接的时候,服务器进程并不马上退出,直到大概三分钟之内没有新的激活请求才真正结束:这个延迟能减少服务器的关闭-启动颠簸。如果你在服务器退出之前修改配置,然后重新运行客户程序,会发现配置并没有改变。

    所幸的是这个问题的解决方案很简单。在完成了希望的配置修改之后,在MTS成COM+浏览器中右击服务器包,选择Shut down。这个动作强迫MTS/COM+结束服务器进程。之后的任何激活请求都会使得COM SCM重新发射该包的代理进程,反应你所作出的修改。

    对一个配定组件进行配置修改之后,使用MTS或COM+浏览器关闭服务器包,这样下一次服务器被激活时修改就会生效。

客户端的调试技术

    从技术上说,在客户工程中调试一个配定组件和基COM EXE服务器是一样的。OLE RPC调试仍旧被支持,但是你必须完成从客户工作空间切换到服务器的辅助工作。当单步跟踪进入一个配定组件的方法调用时,Visual C++仍会发射一个新的调试器版本,并将它附着到组件所在的代理进程。但是,这个新的调试器拷贝显示的行为和你试图进入的方法的真正实现有点出入,你将会看到代理进程的一些汇编代码。为了跨过这个鸿沟,请打开被调用方法所在的源代码文件,设置断点,然后继续调试。代理进程会继续执行,直到达到断点。如果执行没有达到被调用的方法,说明截取者已经抛弃了该方法调用(典型的情况是为了安全考虑),没有激发组件的代码就返回了客户程序。

服务器端的调试技术

    如果不能在客户工作空间中启动初始化一个调试会话,或者你想调试一个配定组件的初始化化代码,就必须真接面对代理体系结构。配定组件的调试机制还是和基COM EXE的一样,只不过你不再拥有组件宿主进程的源代码,因为你的服务器DLL的宿主是一个COM提供的代理进程,因此对配定组件、重新启动服务器或者将Visual C++附着到服务器的一个运行实例的过程和以前有一些不同。

在调试器中预先启动代理进程——Windows NT4.0

    打开要调试的代码所在的DLL工作空间。打开工程设置对话框,单击Debug标签。将可执行程序设置成Mtx.exe的全路径名(在System文件夹下)。现在,将Mtx.exe的命令行设置为/p:{package guid}。在MTS浏览器中右击包(package)再选择Properties,就可以看到包的GUID。包的GUID位于General标签下。现在,当启动调试会话的时候,调试器会发射Mtx.exe,后者马上装载你的组件DLL,然后为组件支持的每个CLSID调用一次DIlGetClassObject。如果你需要调试DllGelClassObject,在初始化调试会话之前设罝断点即可。

    要预先启动配定组件的MTS代理,在DLL中将调试可执行文件指定为Mtx.exe,命令行参教为/p:{package guid}。

在调试器中预先启动代理进程——Windows 2000

    在Windows 2000中,有两种方法可以将调试器附着到DLL的代理进程上。第一种方法,按照上述在Win NT4.0中的过程,不过在Win 2000有两点区别:①代理进程是DllHost.exe而不是Mtx.exe;②Dllhost.exe的命令行参数是/ProcessId:{package guid},而不是/p:{package guid}。

    在Windows 2000的COM+浏览器中使用包的Launch in the Debugger选项将Visual C++附着到代理进程上。

    第二个方法是否诉COM+在代理进程启动的时候自动为该代理激活调试器。在COM+浏览器中找到你的包,选择Properties,单击Advanced,选择Launch in the debugger选项。当一个客户试图激活你的服务器时,COM SCM就会使用适当的命令行参数发射Visual C++。和第一种技术一样,你可以输入断点,然后通过初始化调试会话启动服务器。如果你希望在任何客户试图激活服务器之前预先启动服务器并设置断点,只需要在COM+浏览器中右击该COM+包,并选择start即可。

附着到已运行的代理进程,或者说“我现在在什么代理进程中?”

    因为毎一个配定组件都在一个可执行代理程序的独立拷贝内部运行,现在要附着调试器到一个已经运行的组件就不像以前那么简单了(只需要在任务管理器右击程序并选择Debug)。现在,你不得不找出来你的组件到底是在哪个Mtx.exe或者Dllhost.exe中。以下步骤可以确定你应该附着的进程:

    1.运行Visual C++的进程观察器工具(Pview.exe)。

    2.在运行进程的列表中选择一个代理进程实例。

    3.单击MemoryDetail按钮。

    4.在内存细节(MemoryDetails)对话框中,展开User Address Space for下拉列表,该列表中包含所有已经装入该代押进程拷贝的DLL。

    5.如果在列表中有你的组件DLL,记录该代理的进程lD,将调试器附着到这个调试器。

    6.如果列表中没有你的组件DLL,重复步骤2到4,直到找到你的组件DLL的宿主代理进程。

    显然,如果这台机器上有多个MTS或COM+包在运行,这个过程就会比较麻烦。为了使配定组件的调试更简便,我开发了一个工具,可以帮助你自动将调试器附着到正确的代理进程实例上。这个工具名叫DbgPak,可以在www.windebug.com网站上找到。

    DbgPak工具在www.windebug.com上可以荻得,它能自动完成将Visual C++附着到你的配定组件所在的Mtx.exe或Dllhost.exe运行实例的过程。

调试自注册代码

    如果你需要调试一个配定组件的自注册代码,最简便的方法是在你将该DLL设置成在MTS或COM+中运行之前就进行调试。请遵照本章中“调试基COM DLL”节中所列的步骤。解决了自注册代码的问题之后,再将组件设置成在MTS/COM+下运行。比起将MMC设置成DLL的调试可执行程序,启动一个调试会话,将MTS/COM+组件快照装入MMC、创建服务器包然后将有问题的DLL添加到这个包里的过程,这个方法要简单得多。

    在MTS或COM+中配置DLL之前调试自注册问题——指定Regsvr32为Visual C++发射的调试可执行程序。

11.6 调试被ASP调用的基COM DLL

    如果你写的基COM DLL要被在一个ASP(活动服务器页)中运行的脚本调用,你的代码将从一个配定组件中被调用。当微软的Inernet信息服务处理一个来自ASP的请求时,它激活一个叫做网络应用管理器(Web Application Manager)的配定组件。这个组件执行该ASP中的脚本发出的激活和方法调用请求。因为网络应用管理器组件是在MTS/COM+配置的,你的基COM DLL会在一个代理进程的环境中执行。

    在这个场景下,将调试器附着到正确的代理进程的工作就变成了确定一个服务器包是你的组件宿主的问题。Internet服务管理器允许你控制独立性的程度,它封装了到Web站点上的指定目录的请求。你的组件可能直接在IIS进程内执行,也对能在另一个地址空间中执行,这取决于你如何配置特定的虚拟目录。

调试被ASP调用的组件——Windows NT4.0

    在指定虚拟目录的属性页中,包含一个叫做Run in a separate address space(isolated) (在一个独立的地址空间中运行(分离的))的选项。如果这个选项没有被选中,你的组件会在IIS的地址空间(Inetinfo.exe)中运行。要调试你的组件,将Visual C++附着到Inetinfo.exe,载入你的DLL的符号,设置断点,然后使用一个网络浏览器打开调用该组件的ASP就行了。如果这个虚拟目录被设置为在一个单独的地址空间中运行,使用MTS浏览器找到一个叫做IIS-{website//virtual directory name}的包,记录下来这个包的GUID。现在,你就可以按照时面“在调试器中预先启动代理进程”中介绍的步骤继续了,注意其中的服务器包GUID就使用刚才你找到的GUID。

调试被ASP调用的组件——Windows 2000

    在Windows 2000中,在指定虚拟目录的属性页中,包含一个叫做Application Profection(应用程序保护)的选项,可以设置成如下几个值之一:

l  Low(低,IID进程);

l  Medium(中等,对象池);

l  High(高,独立的);

    这个设置将决定组件的宿主服务器包,如果应用保护设置成Low,就在COM+浏览器中找到In-Process Application包的GUID。如果应用保护设置成Medium,就在COM+浏览器中找到IIS Out-of-Process Pooled Applications包的GUID。如果应用保护设置成High,在COM+浏览器中找到IIS-{website//virtual directory name}包的GUID。现在,你就可以按照前面“将Visual C++附着到已运行的代理进程”中介绍的步骤继续了。注意其中的服务器包GUID就使用刚才你找到的GUID。

    我希望本章关于COM调试的介绍能够对你有所帮助,并能给你一点指导。熟练运用这些技术,你就能更快地对付系统中出现的问题了,越快地找到问题的点,你就能越快地去掉错误,继续前行。

11.7 推荐阅读

    Box. Don. Essential COM. Reading, MA: Addison-Wesley, l998.

    介绍COM的核心概念和编程模式典型的编程错误以及如何避免这些错误。

    Brown, Keith. Programming Windows Security. Boston. MA: Addison-Wesley. 2000.

    对Windows的安全休系结构和编程模式提供了一个概览,一种一章专门讲述COM的安全机制。

    Kaufman, Charlie, Radia Perlman, and Mike Speciner. Network Security: Private Communication in a Public World. Englewood Cliffs, NJ: Prentice-hall. 1995.

    这本书不是针对Windows的,它介绍了各祌网络认证协议。

    Rector, Brent, and Chris Sells. ATL Internals. Reading, MA: Addison-Wesley. 1999.

    展示了ATL编程的概貌,其中有关于使用ATL智能接口指针类的详细介绍。

posted @ 2013-12-04 10:41 心灵捕手 阅读( ...) 评论( ...)   编辑 收藏
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值