3跨越进程边界共享内核对象
方法:继承性、命名、复制内核对象三种方法
3.1 对象句柄的继承性
只有当进程具有父子关系时,才能使用对象句柄的继承性。父进程必须执行若干个操作步骤如下:
1)
首先,当父进程创建内核对象时,必须向系统指明,它希望对象的句柄是个可继承的句柄。请记住,虽然内核对象句柄具有继承性,但是内核对象本身不具备继承
性。若要创建能继承的句柄,父进程必须指定一个SECURITY_ATTRIBUTES结构,并将此结构中的成员变量bInheritHandle设为
TRUE。如果返回的句柄是不能继承的,那么句柄表中的标志位是0x00000000。如果将bInheritHandle为
TRUE,那么该标志位将被置为0x00000001。
2)
创建子进程时,需要将子进程设置为可以继承父进程的可继承句柄值。就是函数CreateProcess()中的参数bInheritHandles设置为
TRUE。当bInheritHandles为TRUE时,系统会遍历父进程句柄表,将可继承的句柄拷贝到子进程。
注:
1. 关闭内核对象时,子进程不必首先终止运行,但是父进程也不必首先终止运行,实际上,
CreateProcess函数返回后,父进程可以立即关闭对象的句柄,而不影响子进程对该对象进行操作的能力。
2.当子进程创建后,父进程创建的新的可继承内核对象的句柄不会被拷贝到子进程中。
例如:
#include <windows.h>
#include <stdio.h>
int main(void)
{
SECURITY_ATTRIBUTES
sa;
ZeroMemory(&sa,
sizeof(SECURITY_ATTRIBUTES));
sa.nLength
=sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor
= NULL;
sa.bInheritHandle
= TRUE; //使得创建的句柄可继承
STARTUPINFO
si;
ZeroMemory(&si,
sizeof(si));
si.cb
= sizeof(si);
PROCESS_INFORMATION
pi;
ZeroMemory(&pi,
sizeof(pi));
HANDLE
h = CreateMutex(&sa, FALSE, "fuck");
if(GetLastError()==
ERROR_ALREADY_EXISTS)
{
printf("run
error\n");
system("pause");
return
0;
}
else{
//使得创建的新进程 RunAsDate.EXE 继承父进程中可以继承的内核句柄,如前面创建的互斥句柄 h
CreateProcess(NULL,"RunAsDate",
NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
printf("run\n");
system("pause");
return
0;
}
}
ps:运行上面的程序之后,会创建一个RunAsDate进程,把父进程关闭,再运行父进程会提示run error,表明我们的互斥对象已经被RunAsDate子进程继承,并且关闭父进程,对于互斥对象没有影响.
3)子进程确定它期望的内核对象的句柄值的方法有:
1.将句柄值作为命令行参数传给子进程,子进程的初始化代码将解析命令行(通常是调用_stscanf_s),并提取句柄。子进程获得句柄值之后,就会拥有和父进程一样的内核对象访问权限。
ps:句柄继承之所以能够实现,唯一原因就是“共享的内核对象”的句柄值在父进程和子进程中是完全一样的。这正是父进程能将句柄值作为命令行参数来传递的原因。
2.让父进程等待子进程完成初始化(使用第9章介绍的WaitForInputIdlee函数),然后,父进程可以将一条消息发送或展示在子进程中的一个线程创建的窗口中。
3.让父进程将一个环境变量添加给它的环境程序块。该变量的名字是子进程知道要查找的某种信息,而变量的值则是内核对象要继承的值。这样,当父进程生成子
进程时,子进程就继承父进程的环境变量,并且能够非常容易地调用GetEnvironmentVariable函数,以获取被继承对象的句柄值。如果子进
程要生成另一个子进程,那么使用这种方法是极好的,因为环境变量可以被再次继承。
3.2改变句柄标志
如果创建了一个可继承的内核对象,但又不想某个子进程继承,就可以调用函数
修改标志位,bool SetHandleInformation(HANDLE hObject, DWORD dwMask, DWORD dwFlags);
读取标志位,BOOL GetHandleInformation(HANDLE hObj, PDWORD pdwFlags);
3.3命名对象
1、多进程共享内核对象的第二种方法是使用命名对象,即创建内核对象时为内核对象命名。如函数:
HANDLE CreateMutex(PSECURITY_ATTRBUTES psa, BOOL bInitialOwner,PCTSTR pszName);
当已经创建了同名的内核对象,系统会先判断对象类型是否一样,当前进程是否有访问权,如果类型一样,且有访问权的话,系统将复制已经存在的那个对象的信息到进程的句柄表,并返回一个句柄(不同进程的句柄一般不同)。否则的话调用失败,返回NULL。
既然内核对象创建函数这样设置,我们如何判断是否创建了新的内核对象呢?方法是在调用创建函数后马上调用GetLastError()函数。
代码为:if(GetLastError() == ERROR_ALREADY_EXISTS)
注:用命名对象的方法去创建内核对象时可能会出现不同类型的对象用同一个名字,那么后面创建对象时将失败。例如:
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("MyObj"));
HANDLE hSem = CreateSemaphore(NULL, 1, 1, TEXT("MyObj"));
以上代码CreateSemaphore函数调用肯定会返回NULL,因为已经有一个同名的互斥量对象了,执行上述代码后,如果检查GetLastError函数的返回值会发现错误代码6(ERROR_INVALID_HANDLE).
因为内核对象用同一个内核空间,且内核对象的名字不能相同。
因此我们可以防止运行一个应用程序的多个实例。方法是在Main函数中,用CreateMutex()函数创建一个命名对象,再马上调用GetlastError()来判断是否已经存在这个互斥对象。如果存在,则退出程序。
例如:
#include <Windows.h>
#include <stdio.h>
int main(void)
{
//为了确保名称的唯一性,建议创建一个GUID,并将这个GUID的字符串形式作为自己的对象名称使用.
HANDLE h = CreateMutex(NULL, FALSE, TEXT("{5ACCA58C-8EAB-410E-9263-C5ADAD0D8F9B}"));
if(GetLastError() == ERROR_ALREADY_EXISTS)
{
MessageBox(NULL, "The program is arealy run", "fuck", MB_OK|MB_ICONERROR);
return 0;
}
system("pause");
}
2、我们也可以调用Open函数来打开内核对象。如函数:
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);
Open函数会判断内核中是否有参数pszName所指定的名字的内核对象,如果有,并且对象的类型相同,且当前线程有访问权限,那么系统将拷贝内核信息到进程,且内核计数加一。否则返回0.错误代码为:ERROR_FILE_NOT_FOUND。
3.4终端服务器的命名空间
当有终端服务器时,终端服务器会提供一个全局的命名空间和一个会话的命名空间。这样可以避免相同应用程序的两个或多个会话之间出现互相干扰的情况。指定名字空间的方法如下:
CreateMutex(NULL,FALSE,"Global\\MyName"); \\全局命名空间
CreateMutex(NULL,FALSE,"Local\\MyName"); \\局部命名空间
3.5复制对象句柄
共享内核对象的另一种方法是复制对象句柄,函数是BOOL
DupicateHandle()。此函数主要是将一个线程的句柄信息复制到另一个进程的句柄表中。新生成的句柄值与原句柄可能不同。书中举了两个例子。
一个是将一个进程的句柄复制到另一个句柄。另一个是当一个进程有对某一个内核对象的读写功能时,有一个函数要对这个内核对象进行读操作。那么为了程序的健
壮性,可以先复制生成一个只读的句柄,再将这个句柄传给这个函数。(具体代码见本书本节)。
当一个进程复制了句柄到另一个进程时,如何通知另一个进程,这个内核对象可用呢? 方法是使用窗口消息或某种别的I P C机制。