coinitialize
之一:
大家都知道程序中若要使用COM组件则必须要先调用CoInitialize,该函数主要是用来初始化COM运行环境。但这个函数的作用域是以线程为单位还是以进程为单位呢?也许大家已经通过测试程序摸索出答案,没错,是以线程为单位。今天我们就稍微再深入一下,通过分析CoInitialize的具体实现来印证我们的想法。
我们先来看看CoInitialize的汇编
769B2A24 mov edi, edi
769B2A26 push ebp
769B2A27 mov ebp, esp
769B2A29 push 2 ; dwCoInit
769B2A2B push [ebp+8] ; pvReserved
769B2A2E call _CoInitializeEx@8 ; CoInitializeEx(x,x)
769B2A33 pop ebp
769B2A34 retn 4
可以看到,其中的实现还是比较简单的,它只是简单地调用了CoInitializeEx,将第二个参数设置为2,即COINIT_APARTMENTTHREADED。我们再来看看CoInitializeEx的实现
769AEF5B mov edi, edi
769AEF5D push ebp
769AEF5E mov ebp, esp
769AEF60 push ecx
769AEF61 push ebx
769AEF62 mov ebx, [ebp+0C]
769AEF65 mov eax, ebx
769AEF67 and eax, 0Eh ; 检查参数是否正确,目前第二个参数只用了一个字节
769AEF6A cmp eax, ebx
769AEF6C jnz loc_76A0B8C7
769AEF72 push edi
769AEF73 xor edi, edi
769AEF75 cmp [ebp+8], edi ; 判断第一个参数是否为NULL
769AEF78 jnz loc_76A0B8D1
769AEF7E
769AEF7Eloc_769AEF7E:
769AEF7E call ?IsRunningInRPCSS@@YGHXZ ;IsRunningInRPCSS(void)
769AEF83 test eax, eax ;判断当前进程是否是RPCSS
769AEF85 jnz loc_76A0B8ED ;如果是(即返回非0)则返回“灾难性故障”的错误
769AEF8B mov eax, large fs:18h
769AEF91 mov eax, [eax+0F80h]
769AEF97 cmp eax, edi
769AEF99 mov [ebp+8], eax
769AEF9C jz loc_769ADF26 ; 判断当前线程中的struct tagSOleTlsData结构体是否分配,若未分配则进行分配
769AEFA2
769AEFA2loc_769AEFA2:
769AEFA2 push esi
769AEFA3 push edi ; __int32
769AEFA4 push ebx ; unsigned __int32
769AEFA5 xor esi, esi
769AEFA7 inc esi
769AEFA8 push esi ; int
769AEFA9 push esi ; int
769AEFAA call ?NotifyInitializeSpies@@YGJHHKJ@Z ;NotifyInitializeSpies(int,int,ulong,long)
769AEFAF call ?IsThreadInNTA@@YGHXZ ; IsThreadInNTA(void)
769AEFB4 test eax, eax
769AEFB6 jnz loc_769DAFAD ; 如果是 则返回“无法在设置线程模式后对其加以更改。”的错误
769AEFBC mov eax, [ebp+8]
769AEFBF mov ecx, [eax+0Ch]
769AEFC2 test ch, 10h ;判断标识第4位(从第0位开始)是否置位
769AEFC5 jnz loc_769D9D20 ; 服务器出现意外情况。
769AEFCB mov edx, ebx
769AEFCD and edx, 2
769AEFD0 mov [ebp-4], edx
769AEFD3 jz short loc_769AEFDE ; 非COINIT_APARTMENTTHREADED模式
769AEFD5 test ch, 1 ;判断标识第0位是否置位
769AEFD8 jnz loc_769DAFAD ; 返回“无法在设置线程模式后对其加以更改。”的错误
769AEFDE
769AEFDEloc_769AEFDE:
769AEFDE cmp edx, edi
769AEFE0 jz loc_769DAFA5 ; 非COINIT_APARTMENTTHREADED模式
769AEFE6
769AEFE6loc_769AEFE6:
769AEFE6 test bl, 8
769AEFE9 jnz loc_76A0B901 ;第二个参数中COINIT_SPEED_OVER_MEMORY标识位被设置,即为单线程套件
769AEFEF
769AEFEFloc_769AEFEF:
769AEFEF add eax, 18h
769AEFF2 inc dword ptr [eax] ; tagSOleTlsData.dwReserved1[0]++;
769AEFF4 cmp [eax], esi
769AEFF6 jnz loc_769ADBF9 ; 判断tagSOleTlsData.dwReserved1[0]==1?
769AEFFC test edx, edx
769AEFFE mov ebx, offset?gMTainitLock@@3VCOleStaticmutexSem@@A ; COleStaticMutexSem gMTAInitLock
769AF003 jz loc_769DAFF2 ; 第二个参数未设置COINIT_APARTMENTTHREADED标识,即为多线程套件
769AF009
769AF009loc_769AF009:
769AF009 mov esi, offset?g_mxsSingleThreadOle@@3VCOleStaticMutexSem@@A ; COleStaticMutexSemg_mxsSingleThreadOle
769AF00E mov ecx, esi
769AF010 call ?request@COleStaticMutexSem@@QAEXXZ ;COleStaticMutexSem::Request(void)
769AF015 push [ebp+0C]
769AF018 lea eax, [ebp+8]
769AF01B push eax
769AF01C call ?wCoInitializeEx@@YGJAAVCOleTls@@K@Z ;wCoInitializeEx(COleTls &,ulong) 调用wCoInitializeEx
769AF021 mov ecx, esi
769AF023 mov edi, eax
769AF025 call ?Release@COleStaticMutexSem@@QAEXXZ ;COleStaticMutexSem::Release(void)
769AF02A test edi, edi
769AF02C jl loc_76A0B90C
769AF032
769AF032loc_769AF032:
769AF032 cmp [ebp-4], 0
769AF036 jz loc_769DB004 ; 第二个参数未设置COINIT_APARTMENTTHREADED标识,即为多线程套件
769AF03C
769AF03C loc_769AF03C: ; CODE XREF:CoInitializeEx(x,x)+2C0B6j
769AF03C xor esi, esi
769AF03E inc esi
769AF03F
769AF03F loc_769AF03F:
769AF03F push edi ; __int32
769AF040 push [ebp+0C] ; unsigned__int32
769AF043 push 0 ; int
769AF045 push esi ; int
769AF046 call ?NotifyInitializeSpies@@YGJHHKJ@Z ;NotifyInitializeSpies(int,int,ulong,long)
769AF04B pop esi
769AF04C
769AF04C loc_769AF04C:
769AF04C pop edi
769AF04D
769AF04Dloc_769AF04D:
769AF04D pop ebx
769AF04E leave
769AF04F retn 8
其中有几点请注意:
1、在第一个参数为非空时,该函数会判断当前进程是否为excel;
2、该函数也会判断当前进程是否为RPCSS,该进程的用途请大家另行查阅;检查进程是否为RPCSS的方法主要是:先判断当前进程是否有载入windows目录下\\system32\\rpcss.dll,如果未载入则当前进程不是RPCSS;若载入了,则获取该DLL中名为WhichService的导出函数,如果未找到该函数也认为当前进程是RPCSS;若找到,并该函数的返回值大于等于0,且作为该函数参数的指针所指向的值为2则当前进程不是RPCSS,否则当前进程即为RPCSS。
3、每个线程的TEB结构向后偏移0x0F80的地方存放struct tagSOleTlsData的指针,该结构的声明如下:
typedef structtagSOleTlsData
{
void *pvReserved0[2];
dword dwReserved0[3];
void *pvReserved1[1];
DWORD dwReserved1[3];
void *pvReserved2[4];
DWORD dwReserved2[1];
void *pCurrentCtx;
} SOleTlsData;
该结构中存放了当前线程有关COM的环境信息,这个结构体中各个域的定义微软貌似没有公开。线程启动后,在没有该线程调用CoInitialize或CoInitializeEx之前,该指针为空。第一次调用上述函数后,为该线程从堆上分配该结构的内存并将其指针保存至TEB+0x0F80处。
4、我们注意到,所有对struct tagSOleTlsData内容的修改都未进行互斥保护,这是因为所有对该结构的修改操作都在当前线程内部进行,因此也就不存在多线程同步的问题;而对于一些全局信息的修改则都进行了保护。
之二:
最近工作比较忙,在粗略分析了CoInitialize之后我们一直没有再深入研究,下面言归正传。前面我们初步了解到了CoInitialize其实是通过调用CoInitializeEx来实现功能的,而后者最终调用了wCoInitializeEx函数,如果能进一步了解这个函数的内部实现,那么我们对COM环境的初始化过程就比较清晰了。好,我们下面继续看wCoInitializeEx的汇编代码,这次我们分段来看。
769AF092 arg_0 = dword ptr 8
769AF092 arg_4 = byte ptr 0Ch
769AF092 mov edi, edi
769AF094 push ebp
769AF095 mov ebp, esp
769AF097 push ebx
769AF098 push esi
769AF099 push edi
769AF09A mov edi, ds:__imp__InterlockedIncrement@4 ; InterlockedIncrement(x)
769AF0A0 push offset ?g_cProcessInits@@3KA ; lpAddend
769AF0A5 call edi ; InterlockedIncrement(x) ; InterlockedIncrement(x)
769AF0A7 cmp eax, 1
769AF0AA mov esi, [ebp+arg_0]
769AF0AD jz loc_769CA020
函数一开始先将全局变量g_cProcessInits加1,后面还比较了加1后变量的值是否为1,因此这应该是一个计数器,并且在第一次执行函数时会进行一些额外的操作,具体如下:
769CA020 loc_769CA020:
769CA020 call ?ProcessInitialize@@YGJXZ ; ProcessInitialize(void)
769CA025 test eax, eax
769CA027 mov [ebp+arg_0], eax
769CA02A jge loc_769AF0B3
769CA030 jmp loc_76A0B96F
果然,在g_cProcessInits为1的时候,也就是第一次执行wCoInitializeEx的时候,会调用ProcessInitialize,在这里执行了进程范围内全局性的初始化操作,具体是什么操作我们暂时不关心,初始化成功则返回值大于等于0,否则小于0,我们沿着正确的路径往下看。
769AF0B3 loc_769AF0B3:
769AF0B3 test [ebp+arg_4], 2
769AF0B7 mov eax, [esi]
769AF0B9 jz loc_769DAFB7
判断第二个参数,即传入的dwCoInit是否设置了COINIT_APARTMENTTHREADED标识,这里应该是整个函数的一个分水岭,单线程套间和多线程套间的执行从此开始走向不同的方向,本文只研究单线程套间的后续执行情况。
769AF0BF or byte ptr [eax+0Ch], 80h
769AF0C3 test [ebp+arg_4], 4
769AF0C7 jz short loc_769AF0CF
将在CoInitializeEx中分配的SOleTlsData的dwReserved0[1]域的第7位(由低到高从第0位开始)置1后,检查dwCoInit参数是否设置了COINIT_disable_OLE1DDE标识,该标识的意义是为支持Ole1而禁用DDE,如果设置了,我们会看到。
769AF0C9 mov eax, [esi]
769AF0CB or dword ptr [eax+0Ch], 40h
上面只是简单地将在CoInitializeEx中分配的SOleTlsData的dwReserved0[1]域的第2位(由低到高从第0位开始)置1,因此我认为SOleTlsData中dwReserved0[1]域的第7位标识了COM套间是单线程套间还是多线程套间,第2位标识了DDE是否禁用,上述操作只有在dwCoInit参数的COINIT_DISABLE_OLE1DDE标识被设置时才执行。
769AF0CF loc_769AF0CF:
769AF0CF mov eax, [esi]
769AF0D1 push 4
769AF0D3 push 1
769AF0D5 push esi
769AF0D6 lea ecx, [eax+50h]
769AF0D9 push ecx
769AF0DA add eax, 3Ch
769AF0DD push eax
769AF0DE call ?InitThreadCtx@@YGJAAPAVCObjectcontext@@AAPAVCComApartment@@AAVCOleTls@@HW4tagAPTKIND@@@Z ; InitThreadCtx(CObjectContext * &,CComApartment * &,COleTls &,int,tagAPTKIND)
769AF0E3 test eax, eax
769AF0E5 mov [ebp+arg_0], eax
769AF0E8 jl loc_76A0B96F
我们可以看到后面又调用了InitThreadCtx,这个线程中分配了CObjectContext对象实例以及CComApartment对象实例并将其指针分别传入的前两个参数中。从这里我们大概可以猜测到SOleTlsData向后偏移60个字节和80个字节处分别存放了CObjectContext指针和CComApartment指针。不过SOleTlsData结构自身的大小只有60字节,因此在SOleTlsData结构后面肯定还有其他数据结构存在,这部分内存在什么地方分配的呢?再回到CoInitializeEx函数中调用的COleTls::TLSAllocData观察,发现其中分配了288个字节,根据前面的分析,这里分配的应该是COleTls对象,而SOleTlsData是其中第一个成员,其第二个成员应该就是CObjectContext,而CComApartment指针在CObjectContext指针之后16个字节。
769AF0EE mov ebx, offset ?g_cSTAInits@@3KA ; ulong g_cSTAInits
769AF0F3 push ebx ; lpAddend
769AF0F4 call edi ; InterlockedIncrement(x) ; InterlockedIncrement(x)
769AF0F6 cmp eax, 1
769AF0F9 jz loc_769CA351
线程上下文初始化成功后,g_cSTAInits加1并判断其加1后的值是否等于1,这里应该又是判断是否第一个执行,我们继续看。
769CA351 loc_769CA351:
769CA351 call ?RegisterOleWndClass@@YGJXZ ; RegisterOleWndClass(void)
769CA356 test eax, eax
769CA358 mov [ebp+arg_0], eax
769CA35B jge loc_769AF0FF
769CA361 jmp loc_76A0B956
这里注册了一个名为“OleMainThreadWndClass”的窗口类并将返回的窗口类的唯一标识存放在全局变量中供后续程序使用。注册成功后来到这里:
769AF0FF loc_769AF0FF:
769AF0FF cmp ?gdwMainThreadId@@3KA, 0 ; ulong gdwMainThreadId
769AF106 jz loc_769CA3CD
可以看到判断保存线程ID的全局变量是否为0,如果为0则:
769CA3CD loc_769CA3CD:
769CA3CD call ?InitMainThreadWnd@@YGJXZ ; InitMainThreadWnd(void)
769CA3D2 test eax, eax
769CA3D4 mov [ebp+arg_0], eax
769CA3D7 jge loc_769AF10C
769CA3DD jmp loc_76A0B95F
又执行了一个函数,这个函数中先检查当前线程是否某个特殊线程,即满足某个全局变量为非空且当前线程的SOleTlsData.dwReserved0[1]的第4位被置1。然后就创建一个之前注册的窗口类的隐藏窗口并保存该窗口的句柄,接着保存当前线程ID到全局变量后函数执行结束。创建窗口调用的是createwindowEx函数,第9个参数即父窗口的句柄为HWND_message,也就是说创建了一个message-only的窗口。这种窗口可以用来发送和接收消息,不可见、没有Z-order属性、不能被枚举、不能接收广播消息,Windows仅仅是简单地向其派发消息。由此,我们可以看出对于单线程套间,第一个调用CoInitialize函数的线程中会创建一个隐藏窗口,后续对COM组件的调用都是以消息的形式将请求发送至该隐藏窗口,在其消息处理函数中进行处理,也就是说Windows利用消息机制将对COM组件的调用请求进行了串行化,这就降低了对COM组件的多线程同步要求,但反过来也影响了效率。后续我们会进一步分析来验证单线程套间中,COM请求是否最终通过消息机制得以实现的。
769AF10C loc_769AF10C:
769AF10C cmp ?gpNTAApartment@@3PAVCComApartment@@A, 0 ; CComApartment * gpNTAApartment
769AF113 jz loc_769CA22A
这里比较某个全局指针是否为空,如果为空也就是说在第一次调用CoInitialize时要对其进行初始化,若不为空则wCoInitializeEx就直接返回0结束了。我们继续看看该指针为空的情况:
769CA22A loc_769CA22A:
769CA22A call ?InitializeNTA@@YGJXZ ; InitializeNTA(void)
769CA22F test eax, eax
769CA231 mov [ebp+arg_0], eax
769CA234 jge loc_769AF119
这里主要是调用了一个InitializeNTA函数,这个函数中只是重新分配了一个CObjectContext对象和CComApartment对象,并将CObjectContext指针保存到SOleTlsData的pCurrentCtx中,随后还调用了其某个成员函数;将CComApartment指针保存到全局变量中;这个函数的最终用意还不是非常了解。
后续的工作就比较简单了,主要是释放之前分配的对象,置一些标志位等操作,这里就不再一一细究了。