windows核心编程
3.3 跨越进程边界共享内核对象
3.3.1 对象句柄的继承性
3.3.2 改变句柄的标志
3.3.3命名对象
3.3.4终端服务器的名字空间
3.3.5 复制对象句柄
文章目录
3.3 跨越进程边界共享内核对象
许多情况下,在不同进程中运行的线程需要共享内核对象。下面是为何需要共享的原因:·文件映射对象使你能够在同一台机器上运行的两个进程之间共享数据块。·邮箱和指定的管道使得应用程序能够在连网的不同机器上运行的进程之间发送数据块。·互斥对象、信标和事件使得不同进程中的线程能够同步它们的连续运行,这与一个应用程序在完成某项任务时需要将情况通知另一个应用程序的情况相同。
由于内核对象句柄与进程相关,因此这些任务的执行情况是不同的。不过,Microsoft公司有若干很好的理由将句柄设计成与进程相关的句柄。最重要的理由是要实现它的健壮性。如果内核对象句柄是系统范围的值,那么一个进程就能很容易获得另一个进程使用的对象的句柄,从而对该进程造成很大的破坏。另一个理由是安全性。内核对象是受安全性保护的,进程在试图操作一个对象之前,首先必须申请获得操作该对象的许可权。对象的创建人只需要拒绝向用户赋予许可权,就能防止未经授权的用户接触该对象。
在下面的各节中,将要介绍允许进程共享内核对象的 3个不同的机制。
3.3.1 对象句柄的继承性
只有当进程具有父子关系时,才能使用对象句柄的继承性。在这种情况下,父进程可以使用一个或多个内核对象句柄,并且该父进程可以决定生成一个子进程,为子进程赋予对父进程的内核对象的访问权。若要使这种类型的继承性能够实现,父进程必须执行若干个操作步骤。首先,当父进程创建内核对象时,必须向系统指明,它希望对象的句柄是个可继承的句柄
请记住,虽然内核对象句柄具有继承性,但是内核对象本身不具备继承性。若要创建能继承的句柄,父进程必须指定一个 SECURITY_ATTRIBUTES结构并对它进行初始化,然后将该结构的地址传递给特定的 Create函数。下面的代码用于创建一个互斥对象并将一个可继承的句柄返回给它:
SECURITY_ATTRIBUTES Sa;
sa.nLength=sizeof(sa);
sa.1pSecurityDescriptor=ULL:
sa.bInheritHandle =TRUE;// Make the returned handle inheritable.
HANDLE hMutex=CreateMutex(&Sa,FALSE, NULL);
该代码对一个SECURITY_ATTRIBUTES结构进行初始化,指明该对象应该使用默认安全性(在Windows98中该安全性被忽略)来创建,并且返回的句柄应该是可继承的。
Windows 98 尽管Windows 98不拥有完整的对安全性的支持,但是它却支持继承性,因此,Windows98能够正确地使用bInheritHandle成员的值。
现在介绍存放在进程句柄表项目中的标志。每个句柄表项目都有一个标志位,用来指明该句柄是否具有继承性。当创建一个内核对象时,如果传递NULL作为PSECURITY ATTRIBUTES的参数,那么返回的句柄是不能继承的,并且该标志位是0。如果将bInheritHandle成员置为TRUE,那么该标志位将被置为1。
表3-2显示了一个进程的句柄表。
表3-2表示该进程拥有对两个内核对象(句柄1和3)的访问权。句柄1是不可继承的,而句
柄3是可继承的。使用对象句柄继承性时要执行的下一个步骤是让父进程生成子进程。这要使用CreateProcess函数来完成:
B00L CreateProcess(
PCTSTR pszApplicationName.
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess
PSECURITY_ATTRIBUTES pszThread,
B00L bInheritHandles.
DWORD dwCreationFiags.
PYOID DvEnvironment.
PCTSTR pszCurrentDirectory
LPSTARTUPINFO pStartupInfo.
PPROCESS_INFORMATION pProcessInformation);
下一章将详细介绍这个函数的用法,不过现在我想要让你注意 bInheritHande这个参数。般来说,当生成一个进程时,将为该参数传递 FALSE。该值告诉系统,不希望子进程继承父进程的句柄表中的可继承句柄。
但是,如果为该参数传递 TRUE,那么子进程就可以继承父进程的可继承句柄值。当传递TRUE时,操作系统就创建该新子进程,但是不允许子进程立即开始执行它的代码。当然,系统为子进程创建一个新的和空的句柄表,就像它为任何新进程创建句柄表那样。不过,由于将TRUE传递给了CreateProcess的bInheritHandles参数,因此系统要进行另一项操作,即它要遍历父进程的句柄表,对于它找到的包含有效的可继承句柄的每个项目,系统会将该项目准确地拷贝到子进程的句柄表中。该项目拷贝到子进程的句柄表中的位置将与父进程的句柄表中的位置完全相同。这个情况非常重要,因为它意味着在父进程与子进程中,标识内核对象所用的句柄值是相同的。
除了拷贝句柄表项目外,系统还要递增内核对象的使用计数,因为现在两个进程都使用该对象。如果要撤消内核对象,那么父进程和子进程必须调用该对象上的 CloseHandle函数,也可以终止进程的运行。子进程不必首先终止运行,但是父进程也不必首先终止运行。实际上,CreateProcess函数返回后,父进程可以立即关闭对象的句柄,而不影响子进程对该对象进行操
作的能力。表3-3显示了子进程被允许运行前该进程的句柄表。可以看到,项目1和项目2尚未初始化因此是个无效句柄,子进程是无法使用的。但是,项目 3确实标识了一个内核对象。实际上它标识的内核对象的地址是0xF0000010,这与父进程的句柄表中的对象地址相同。访问屏蔽与父进程中的屏蔽相同,两者的标志也相同。这意味着如果该子进程要生成它自己的子进程(即父进程的孙进程),该孙进程也将继承与该内核对象句柄相同的句柄值、同样的访问权和相同的标志,同时,对象的使用计数再次被递增。
应该知道,对象句柄的继承性只有在生成子进程的时候才能使用。如果父进程准备创建带有可继承句柄的新内核对象,那么已经在运行的子进程将无法继承这些新句柄。对象句柄的继承性有一个非常奇怪的特征,那就是当使用它时,子进程不知道它已经继承了任何句柄。只有在另一个进程生成子进程时记录了这样一个情况,即它希望被赋予对内核对象的访问权时,才能使用内核对象句柄的继承权。通常,父应用程序和子应用程序都是由同一个公司编写的,但是,如果另一个公司记录了子应用程序期望的对象,那么该公司也能够编写
子应用程序。子进程为了确定它期望的内核对象的句柄值,最常用的方法是将句柄值作为一个命令行参数传递给子进程,该子进程的初始化代码对命令行进行分析(通常通过调用 sscanf函数来进行分析),并取出句柄值。一旦子进程拥有该句柄值,它就具有对该对象的无限访问权。请注意句柄继承权起作用的唯一原因是,父进程和子进程中的共享内核对象的句柄值是相同的,这就是为什么父进程能够将句柄值作为命令行参数来传递的原因。
当然,可以使用其他形式的进程间通信,将已继承的内核对象句柄值从父进程传送给子进程方法之一是让父进程等待子进程完成初始化(使用第 9章介绍的WaitForInpuldle函数),然
后,父进程可以将一条消息发送或展示在子进程中的一个线程创建的窗口中。另一个方法是让父进程将一个环境变量添加给它的环境程序块。该变量的名字是子进程知道要查找的某种信息,而变量的值则是内核对象要继承的值。这样,当父进程生成子进程时子进程就继承父进程的环境变量,并且能够非常容易地调用 GetEnvironmentVariable函数,以获取被继承对象的句柄值。如果子进程要生成另一个子进程,那么使用这种方法是极好的,因为环境变量可以被再次继承。
3.3.2 改变句柄的标志
有时会遇到这样一种情况,父进程创建一个内核对象,以便检索可继承的句柄,然后生成两个子进程。父进程只想要一个子进程来继承内核对象的句柄。换句话说,有时可能想要控制哪个子进程来继承内核对象的句柄。若要改变内核对象句柄的继承标志,可以调用SetHandleInformation函数:
B00L SetHandleInformation(
HANDLE hobject,
DWORD dwMask,
DWORD dwFlags):
可以看到,该函数拥有 3个参数。第一个参数hObject用于标识一个有效的句柄。第二个参数dwMask告诉该函数想要改变哪个或那几个标志。目前有两个标志与每个句柄相关联:
#define HANDLE_FLAG INHERIT 0x0000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE x0000002
如果想同时改变该对象的两个标志,可以逐位用0R将这些标志连接起来。SetHandleInformation函数的第三个参数是dwFlags,用于指明想将该标志设置成什么值。例如,若要打开一个内核对象句柄的继承标志,请创建下面的代码:
SetHandleInformation(hObJ, HANDLE FLAG INHERIT, HANDLE_FLAG_INHERIT):
若要关闭该标志,请创建下面的代码:
SetHandleInformation(hobj,HANDLE_FLAG INHERIT, 0);
HANDLE_FLAG_PROTECT_FROM_CLOSE标志用于告诉系统,该句柄不应该被关闭:
SetHand1eInformation(hobj,
HANDLE_FLAG_PROTECT_FROM_CLOSE,
HANDLE_FLAG_PROTECT_FROM_CLOSE);
CloseHandle(hobj);//Exception is raised
如果一个线程试图关闭一个受保护的句柄, CloseHandle就会产生一个异常条件。很少想要将句柄保护起来,使他人无法将它关闭。但是如果一个进程生成了子进程,而子进程又生成了孙进程,那么该标志可能有用。父进程可能希望孙进程继承赋予子进程的对象句柄。不过子进程有可能在生成孙进程之前关闭该句柄。如果出现这种情况,父进程就无法与孙进程进行通信,因为孙进程没有继承该内核对象。通过将句柄标明为“受保护不能关闭”,那么孙进程就能继承该对象。
但是这种处理方法有一个问题。子进程可以调用下面的代码来关闭HANDLE_FLAGPROTECT FROM CLOSE标志,然后关闭句柄。
SetHandleInformation(hobJ,HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);
C1oseHandle(hobj):
父进程可能打赌说,子进程将不执行该代码。当然,父进程也可能打赌说,子进程将生成孙进程。因此这种打赌没有太大的风险。
为了完整地说明问题,也要讲一下GetHandleInformation函数的情况:
B00L GetHandleInformation(HANDLE hObj,
PDWORD pdwFlags);
该函数返回pdwFlags指向的DWORD中特定句柄的当前标志的设置值。若要了解句柄是否是可继承的,请使用下面的代码:
DWORD dwFlags:
GetHandleInformation(hobj, &dwFlags);
B00L fHandleIsInheritable=(0!=(dWFagS &HANDLE_FLAG_INHERIT));
3.3.3命名对象
共享跨越进程边界的内核对象的第二种方法是给对象命名。许多(虽然不是全部)内核对象都是可以命名的。例如,下面的所有函数都可以创建命名的内核对象:
HANDLE CreateMutex(PSECURITY_ATTRIBUTES Sa,B00L bInitia10wner,PCTSTR pszName):
HANDLE CreateEvent(
PSECURITY_ATTRIBUTES pSa,
B00L bManualReset.
B00L bInitialState,
PCTSTR pszName):
HANDLE CreateSemaphore(
PSECURITY ATTRIBUTES pSa.
LONG lInitialCount.
LONG MaximumCount.
PCTSTR pszName):
HANDLE CreateWaitableTimer(PSECURITY ATTRIBUTES pSaB00lL bManualReset.
PCTSTR pszName);
HANDLE CreateFileMapping(
HANDLE hFile.
PSECURITY_ATTRIBUTES pSa.
DWORD fiProtect.
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);
HANDLE CreateJobObject(
PSECURITY_ATTRIBUTES pSa.
PCTSTR pszName):
所有这些函数都有一个共同的最后参数 pszName。:当为该参数传递 NULL时,就向系统指明了想创建一个未命名的(匿名)内核对象。当创建一个未命名的对象时,可以通过使用继承调用Create函数与调用0pen函数之间的主要差别是,如果对象并不存在,那么 Create函数将创建该对象,而Open函数则运行失败。如前所述,Microsof没有提供创建唯一对象名的指导原则。换句话说,如果用户试图运行来自不同公司的两个程序,而每个程序都试图创建一个称为“MyObjec”的对象,那么这就是个问题。为了保证对象的唯一性,建议创建一个GUID,并将GUID的字符串表达式用作对象名。命名对象常常用来防止运行一个应用程序的多个实例。若要做到这一点,只需要调用 main或WinMain函数中Create函数,以便创建一个命名对象(创建的是什么对象则是无所谓的)。当Create函数返回时,调用GetLastError函数。如果GetLastError函数返回ERROR_ALREADY_EXISTS,那么你的应用程序的另一个实例正在运行,新实例可以退出。下面是说明这种情况的部分代码:
int WINAPI WinMain(HINSTANCE hinStExe, HINSTANCE, PSTR pszCmdLine.
int nCmdShow){HANDLE h=CreateMutex(NULL, FALSE.
"[FA531CC1-0497-11d3-A180-00105A276C3E}");
if(GetLastError()==ERRORALREADY_EXISTS){
// There is already an instance of this application running.
return(0);
}
// This is the first instance of this application running.
...
// Before exiting,close the object.
C1oseHandle(h);
return(0):
3.3.4终端服务器的名字空间
注意,终端服务器能够稍稍改变上面所说的情况。终端服务器拥有内核对象的多个名字空间。如果存在一个可供内核对象使用的全局名字空间,就意味着它可以供所有的客户程序会话访问。该名字空间主要供服务程序使用。此外,每个客户程序会话都有它自己的名字空间。它能防止运行相同应用程序的两个或多个会话之间出现互相干扰的情况,也就是说一个会话无法访问另一个会话的对象,尽管该对象拥有相同的名字。在没有终端服务器的机器上,服务程序和应用程序拥有上面所说的相同的内核对象名字空间,而在拥有终端服务器的机器上
却不是这样。服务程序的名字空间对象总是放在全局名字空间中。按照默认设置,在终端服务器中,应用程序的命名内核对象将放入会话的名字空间中。但是,如果像下面这样将“Globa1\”置于对象名的前面,就可以使命名对象进入全局名字空间:
HANDLE h=CreateEvent(NULL, FALSE, FALSE, “Globa]\MyName”);
也可以显式说明想让内核对象进入会话的名字空间,方法是将”Loca1\”置于对象名的前面:
HANDLE h= CreateEvent(NULL, FALSE, FALSE, “Local\MyName”);
Microsoft将Global和Local视为保留关键字,除非要强制使用特定的名字空间,否则不应该使用这两个关键字。Microsof还将Session视为保留关键字,虽然目前它没有任何意义。请注意所有这些保留关键字是区分大小写字母的。如果主机不运行终端服务器,这些关键字将被忽略。
3.3.5 复制对象句柄
共享跨越进程边界的内核对象的最后一个方法是使用 DuplicateHandle函数:
B00L DupiicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
PHANDLE phTargetHandle,
DWORD dwDesiredAccess,
B00L bInheritHandle,
DWORD dwptions);
简单说来,该函数取出一个进程的句柄表中的项目,并将该项目拷贝到另一个进程的句柄表中。DuplicateHandle函数配有若干个参数,但是实际上它是非常简单的。DuplicateHandle函
数最普通的用法要涉及系统中运行的3个不同进程。当调用 DuplicateHandle函数时,第一和第三个参数 hSourceProcessHandle和hTargetProcessHandle是内核对象句柄。这些句柄本身必须与调用 DuplicateHandle函数的进程相关。此外,这两个参数必须标识进程的内核对象。如果将句柄传递给任何其他类型的内核对象,那么该函数运行就会失败。第 4章将详细介绍进程的内核对象,而现在只需要知道,每当系统中启动一个新进程时都会创建一个进程内核对象。
第二个参数hSourceHandle是任何类型的内核对象的句柄。但是该句柄值与调用 DuplicateHandle的进程并无关系。相反,该句柄必须与 hSourceProcessHandle句柄标识的进程相关。第四个参数phTargetHande是HANDLE变量的地址,它将接收获取源进程句柄信息拷贝的项目索引。返回的句柄值与hTargetProcessHandle标识的进程相关。
DuplicateHandle的最后3个参数用于指明该目标进程的内核对象句柄表项目中使用的访问屏蔽值和继承性标志。 dwOptions参数可以是0(零),也可以是下面两个标志的任何组合:DUPLICATE SAME ACCESS和DUPLICATE CLOSE SOURCE.如果设定了DUPLICATE_SAME_ACCESS标志,则告诉DuplicateHandle函数,你希望目标进程的句柄拥有与源进程句柄相同的访问屏蔽。使用该标志将使DuplicateHandle忽略它的dwDesiredAccess参数。
如果设定了DUPLICATE CLOSE SOURCE标志,则可以关闭源进程中的句柄。该标志使得一个进程能够很容易地将内核对象传递给另一个进程。当使用该标志时,内核对象的使用计
数不会受到影响。下面用一个例子来说明DuplicateHandle函数是如何运行的。在这个例子中,Process S是目前可以访问某个内核对象的源进程,Process T是将要获取对该内核对象的访问权的目标进程。ProcessC是执行对DuplicateHandle调用的催化进程。ProcessC的句柄表(表3-4)包含两个句柄值,即1和2。句柄值1用于标识Process S的进程内核对象,句柄值2则用于标识ProcessT的进程内核对象。
表3-5是Process S的句柄表,它包含句柄值为2的单个项目。该句柄可以标识任何类型的内核对象,就是说它不必是进程的内核对象。
表3-6显示了Process C调用DuplicateHandle函数之前Process T的句柄表包含的项目。如你所见,Process T的句柄表只包含句柄值为2的单个项目,句柄项目1目前未用。
如果Process C现在使用下面的代码来调用DuplicateHandle,那么只有Process T的句柄表改变更,如表3-7所示。
Process S的句柄表中的第二项已经被拷贝到 Process T的句柄表中的第一项。DuplicateHandle也已经将值1填入Process C的hObj变量中。值1是Process T的句柄表中的索引,新项目将被放入该索引。
由于DUPLICATE_SAME_ACCESS标志被传递给了DuplicateHandle,因此Process T的句柄表中该句柄的访问屏蔽与 Processs的句柄表项目中的访问屏蔽是相同的。另外,传递DUPLICATE SAME ACCESS标志将使DuplicateHandle忽略它的DesiredAccess参数。最后请注意,继承位标志已经被打开,因为给 DuplicateHandle的bInheritHandle参数传递的是TRUE。显然,你永远不会像在这个例子中所做的那样,调用传递硬编码数字值的 DuplicateHandle函数。这里使用硬编码数字,只是为了展示函数是如何运行的。在实际应用程序中,变量可能拥有各种不同的句柄值,可以传递该变量,作为函数的参数。与继承性一样,DuplicateHandle函数存在的奇怪现象之一是,目标进程没有得到关于新内核对象现在可以访问它的通知。因此,ProcessC必须以某种方式来通知ProcessT,它现在拥有对内核对象的访问权,并且必须使用某种形式的进程间通信方式,以便将 hObi中的句柄值传递给Process T。显然,使用命令行参数或者改变Process T的环境变量是不行的,因为该进程已经启动运行。因此必须使用窗口消息或某种别的IPC机制。上面是DuplicateHandle的最普通的用法。如你所见,它是个非常灵活的函数。不过,它很少在涉及3个不同进程的情况下被使用(因为Processc不可能知道对象的句柄值正在被ProcessS使用)。通常,当只涉及两个进程时,才调用DuplicateHandle函数。比如一个进程拥有对另一个进程想要访问的对象的访问权,或者一个进程想要将内核对象的访问权赋予另一个进程。例如,Process S拥有对一个内核对象的访问权,并且想要让Process T能够访问该对象。若要做到这一点,可以像下面这样调用DuplicateHandle:
// All of the following code is executed by Process S.
//Create amutexobject accessible by Process S.
HANDLE hObiProcessS=CreateMutex(NULL,FALSE,NULL);
//0pen a handle to Process T's kernel object.
HANDLE hProcessT=OpenProceSS(PROCESS_ALL_ACCESS, FALSE.dwProcessIdT);
HANDLE hObjProcessT; // An uninitialized handle relative to Process T.
//Give Process Taccess to our mutex
object.
DuplicateHandle(GetCurrentProcess(),hObiProcessS, hProcessT.&hObjProceSST,0,FALSE,DUPLICATE_SAME_ACCESS);
//Use some IPC mechanism to get the handle
//value in hObjProcessS into Process T.
...
//We no longer need to communicate with process T.
CloseHandle(hProcessT);
...
// When Process S no longer needs to use the mutex, it should close it.
CloseHandle(hObiProcessS);
在这个例子中,对GetCurrentProcess的调用将返回一个伪句柄,该句柄总是用来标识调用端的进程Process S。一旦DuplicateHandle返回,hObjProcessT就是与Process T相关的句柄,它所标识的对象与引用Process S中的代码时hObiProcessS的句柄标识的对象相同。Process S决不应该执行下面的代码:
// Process Sshould never attempt to close the
// duplicated handle.
C1oseHandle(hObjProcessT):
如果Process s要执行该代码,那么对代码的调用可能失败,也可能不会失败。如果 ProcessS恰好拥有对内核对象的访问权,其句柄值与 hObiProcessT的值相同,那么调用就会成功。该代码的调用将会关闭某个对象,这样Process S就不再拥有对它的访问权,这当然会导致应用程序产生不希望有的行为特性。
下面是使用DuplicateHandle函数的另一种方法。假设一个进程拥有对一个文件映射对象的读和写访问权。在某个位置上,一个函数被调用,它通过读取文件映射对象来访问它。为了使应用程序更加健壮,可以使用DuplicateHandle为现有的对象创建一个新句柄,并确保这个新句柄拥有对该对象的只读访问权。然后将只读句柄传递给该函数,这样,该函数中的代码就永远不会偶然对该文件映射对象执行写入操作。下面这个代码说明了这个例子:
int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE.LPSTR szCmdLine,int nCmdShow){//Create a file-mapping object; the handle has read/write access.HANDLE
hFi1eMapRW=CreateFileMapping(INVALID_HANDLE_VALUE.NULL,PAGE_READWRITE,0,10240,NULL);
//Create another handle to the file-mapping object:
// the handle has read-only access.
HANDLE hFiTeMapRO;
DuplicateHandle(GetCurrentProcess(),hFileMapRW, GetCurrentProcess().&hFi1eMapRO,FILEMAP READ,FALSE,8);
// Ca1l the function that should only read from the file mapping.
ReadFromTheFileMapping(hFi1eMapRO);
// Close the read-only file-mapping object.
C1oseHandle(hFileMapR0):
// We can still read/write the file-mapping object using hFileMapRW.
...
// When the main code doesn't access the file mapping anymore.
// close it.
C1oseHandle(hFileMapRW);
标签:HANDLE,终端服务器,对象,句柄,Process,内核,进程
From: https://blog.csdn.net/zhyjhacker/article/details/141170397