Windows内核对象

来源:互联网 发布:网络超市加盟 编辑:程序博客网 时间:2024/05/19 15:39

内核对象

一、内核对象

1、基于下图来理解内核的访问,下图是Linux的系统结构

 

2、每个内核对象只是内核分配的一个内存块,而且这个内存块只能由该内核访问,该内存块是一种数据结构。

3、因为内核对象只能由内存访问,故Users App是无法直接在内存中找到内核对象的数据,这样也保证了内核对象的结构状态一致,且微软去修改内核对象的结构不会影响到User App。

4、Win提供了一组函数给User App操作内核对象,这些内核对象始终可以通过这些函数进行访问。当调用一个用于创建内核对象的函数时,该函数返回用于标识该内核对象的句柄。该句柄可以被视为一个不透明的值,进程的任何线程都可以去是使用这个值,将这个句柄值传给Win各个函数,这样OS就知道你想要操作哪个内核对象。

5、基于OS的健壮性考虑,句柄值与进程是密切相关的。因此,若将该句柄值传递给另一个进程的线程使用,那另一个进程使用该句柄值的调用就会失败。

二、内核对象的使用计数

1、内核对象是内核所有,而不是进程所有。即当进程调用创建内核对象的函数,进程终止运行时,内核对象不一定会撤消,因为该内核对象可能被其他的进程访问。

2、内核知道有多少个进程在访问内核对象,因为,每个内核对象都有一个使用计数,使用计数是所有类型的内核对象常用的数据成员之一。

3、当一个进程调用创建内核对象的函数时,被创建的内核对象的使用计数置为1,当另一个进程访问该内核对象时,使用计数递增1。当进程终止时,内核自动确定该进程打开的所有内核对象使用计数,消除该进程对所有内核对象的引用。

当内核对象的引用计数为0时,就可以撤消该内核对象

三、内核对象的安全性

1、安全描述符用于保护内核对象,它描述谁创建了该内核对象,谁具有该内核对象的访问权。

2、用于创建内核对象的函数几乎都有一个指向SECURITY_ATTRIBUTES结构的指针

CreateFileMappingA(    __in     HANDLE hFile,    __in_opt LPSECURITY_ATTRIBUTES lpFileMappingAttributes,    __in     DWORD flProtect,    __in     DWORD dwMaximumSizeHigh,    __in     DWORD dwMaximumSizeLow,    __in_opt LPCSTR lpName);


大多数应用程序都为LPSECURITY_ATTRIBUTES类型的参数传NULL,这样是默认的安全性。但是也可以给LPSECURITY_ATTRIBUTES进行初始化,并将它的地址作为参数传递。

typedef struct _SECURITY_ATTRIBUTES {    DWORD nLength;    LPVOID lpSecurityDescriptor;    BOOL bInheritHandle;} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;


其中真正跟安全性相关的只有LPVOID lpSecurityDescriptor.

若要限制其他进程去访问自己进程创建的内核对象,须定义一个安全描述符,然后对

_SECURITY_ATTRIBUTES结构进行初始化。

3、区别一个对象是否是内核对象,就是看创建它的函数是否有一个指向SECURITY_ATTRIBUTES结构的指针。用于创建用户对象和GDI对象的函数都没有PSECURITY_ATTRIBUTES参数

 

四、进程的句柄表

1、当一个进程被初始化时,系统会为它分配一个句柄表,该表只用于内核对象,不用于GDI对象和用户对象。下表4-1 为句柄表的结构:

表4-1 进程的句柄表结构

 

2、上表可知,进程句柄表是一个数据结构的数组,每个结构都包含一个指向内核对象的指针,一个访问屏蔽,一个标志位。

3、当进程初始化时,若按照表4-1中,则是内核找到索引为1的位置上的结构并初始化,该位置的指针将被初始化为内核对象的数据结构的地址,访问屏蔽设置为全部访问权,并设置一些标志。

4、下面列出一些创建内核对象的函数:

HANDLE CreateThread(      PSECURITY_ATTRIBUTES psa,      DWORD dwStackSize,      LPTHREAD_START_ROUTINE pfnStartAddr,      PVOID pvParam,      DWORD dwCreationFlags,      PDWORD pdwThreadId… … ); HANDLE CreateFile(PCTSTR pszFileName,DWORD dwDesiredAccess,DWORD dwShareMode,PSECURITY_ATTRIBUTES psa,DWORD dwCreateDistribution,DWORD dwFlagsAndAttributesHANDLE hTemplateFile……); HANDLE CreateFileMapping(   HANDLE hFile,   PSECURITY_ATTRIBUTES psa,   DWORD flProtect,   DWORD dwMaximumSizeHigh   DWORD dwMaximumSizeLow,   PCTSTR pszName   ……); HANDLE CreateSemaphore(      PSECURITY_ATTRIBUTES psa,      LONG lInitialCount,      LONG lMaximumCount,      PCTSTR pszName);<span style="font-family:Microsoft YaHei;font-size:12px;"> </span>

这些用于创建内核对象的所有函数均返回与进程相关的句柄,这些句柄可以被相同进程的运行的任何线程成功的使用。该句柄的值实际上是放入进程的句柄表中的索引,用于表示内核对象的信息存放的位置。

但是这个句柄的值的含义是可能随时变更的,WIN 2000中,句柄的值是用于标识放入进程的句柄表的该对象的字节数,而不是索引号。

 

5、调用一个将内核对象句柄作为参数的函数时,需传递由一个Create* &函数返回的值,该函数要查看进程的句柄表,以获取要生成的内核对象的地址。

6、关闭内核对象:BOOL CloseHandle(HANDLE  hObj);

该函数检查进程的句柄表,找到内核对象,确定该结构的使用计数成员,若为0,则撤消。无论内核对象是否已撤消,都会发生清除操作,即会清除当前进程的句柄表中的对象,该句柄对该进程已经无效(这里可以再想想内核对象对于进程是一个什么样的概念,内核对象只属于内核,进程只是去访问,即可以有N个进程去访问同一个内核对象

 

7、假如忘记调用CloseHandle,是可能发生内存泄漏的。进程在运行时,它有可能发生内存泄漏,但,当进程终止时,OS能确保该进程使用的所有资源都被释放。

 

对于内核对象来说,OS将进行的操作是:当进程终止时,系统自动扫描进程的句柄表,若该表有“无效项目”(即在进程终止时没有撤消的内核对象,调用CloseHandle而没有撤消的内核对象也属于无效项目),系统将关闭这些对象句柄。若这些对象的任何对象使用计数为0,则撤消。

 

五、跨进程共享内核对象:3中机制,使多个进程共享单个内核对象

1、共享内核对象对原因:

(1)文件映射对象能够使在同一台机器上运行的两个进程间共享数据块。

(2)邮箱和指定管道使User App能在连网的不同机器上运行的进程间发送数据块。

(3)互斥对象、信标和事件使得不同进程中的线程能够同步它们的连续运行(可理解为同步线程的代码执行状态),这与在一个User App在完成某任务时需要通知另一个User App的情况相同。

2、内核对象句柄的继承性:只有当进程具备父子关系时,才能使用继承性。

具体实现步骤:

首先,当父进程创建内核对象时,必须向系统指明,它希望内核对象的句柄是个可继承的句柄。虽句柄具有继承性,但是内核对象是不具备继承性的。看一段示例:

 SECURITY_ATTRIBUTES sa; sa.nLength =   sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE;    // 将该句柄设置为可继承(句柄的标志位) HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);


每个句柄表对象都有一个标志位,用来指明该句柄是否具有继承性。当创建一个内核对象时,将NULL传递给PSECURITY_ATTRIBUTES的参数,则返回的句柄是不能继承的,并将标志位置为0,若将bInheritHandle成员置为TRUE,则将标志位置为1。

表 5-1 包含两个有效项目的句柄表

由表5-1得出,该进程拥有两个内核对象1和3的访问权,句柄1是不可继承的,句柄3是可继承的。

其次,下一步是父进程要生成子进程:

BOOL CreateProcess(PCTSTR pszApplicationName,PTSTR pszCommandLine,PSECURITY_ATTRIBUTES psaProcess,PSECURITY_ATTRIBUTES pszThread,BOOL bInheritHandles,DWORD dwCreateionFlags,PVOID pvEnvironment,PCTSTR pszCurrentDirectory,LPSTARTUPINFO pStartupInfo,PPROCESS_INFORMATION pProcessInfomation  );


其中bInheritHandle,传FALSE,为不希望子进程继承父进程的可继承句柄。当传递TRUE,则继承,OS就创建该子进程,系统去遍历父进程的句柄表,对于父进程中有效的可继承句柄的每个项目,系统都会将该项目准确的拷贝到子进程句柄表中。而且该项目拷贝到子进程的句柄表中的位置与父进程的句柄表中的位置相同。即表示父进程与子进程中,标识内核对象的所用句柄值是相同的,而且该内核对象的引用计数递增+1

若要撤消内核对象,父进程和子进程都要调用该对象上的CloseHandle,也可以终止进程。也可以子进程、父进程都不必先终止运行,而是在CreateProcess函数返回后,父进程可以立即关闭对象的句柄,从而不影响子进程对该内核对象的操作。对比下面表5-2和表5-3就很清晰的明白句柄的继承。

表 5-2父进程的句柄表

5-3子进程的句柄表

由表5-2和表5-3得出,句柄继承在进程句柄表里面所有值都相同。

因为父进程和子进程的共享的内核对象的句柄值是相同的,故父进程可以将句柄值作为命令行参数来传递。

当然还有其他的方式的进程间通信,将已继承的内核对象句柄值从父进程传递给子进程。

(1) 父进程等待子进程完成初始化,然后,父进程可将一条消息发送或展示到子进程的一个线程创建的窗口中。

(2) 父进程将一个环境变量添加给它的环境程序块,该变量的名字就是子进程要查找的某种信息,而变量的值是内核对象要继承的值,这样,父进程在生成子进程时,子进程就继承父进程的环境变量,并调用GetEnvironmentVariable,以获取被继承对象的句柄值(GetEnvironmentVariable的作用)。若子进程要生成下一个子进程,则可以再次被继承。

3、父进程创建一个内核对象,然后生成两个子进程,但是需要控制指定的子进程来继承父进程内核对象的句柄,这时候就需要改变父进程的内核对象的句柄继承标志。

其实应该这样来理解改变句柄继承标志的作用:更改句柄继承标志只是去修改该句柄能否被自己的后代进程继承,这样可以达到一些不一样的效果。

对于上面的“控制指定的子进程来继承父进程内核对象的句柄”,确切的应该是可以通过修改句柄继承标志来达到父进程希望后代进程谁能有继承,并不包含阻止后代进程继承的意思。

SetHandleInformation函数:

BOOL SetHandleInformation(     HANDLE hObject,     DWORD dwMask,      //改变哪个标志     DWORD dwFlags       //标志的值);//打开一个内核对象hObj的继承标志SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);//关闭一个内核对象hObj的继承标志SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0);// HANDLE_FLAG_PROTECT_FROM_CLOSE告诉系统,该句柄不应该被关闭SetHandleInformation(     hObj,     HANDLE_FLAG_PROTECT_FROM_CLOSE,     HANDLE_FLAG_PROTECT_FROM_CLOSE);CloseHandle(hObj);       //若再调用CloseHandle则异常


其中使用HANDLE_FLAG_PROTECT_FROM_CLOSE的情况是,父进程希望孙进程继承或者是两个子进程A,B中的B继承,若不用HANDLE_FLAG_PROTECT_FROM_CLOSE,子进程A就可能在生成孙进程前关闭该句柄,这样,父进程就无法与孙进程通信,因为这是孙进程还没有继承该内核对象,还没有将父进程的该内核对象的句柄值拷贝到孙进程自己的句柄表中,即父进程与孙进程还没有共享同一个内核对象。

其实子进程想关闭句柄的话多加一句就可以了:

SetHandleInformation(hObj, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);CloseHandle(hObj);


要知道句柄是否可继承:

BOOL GetHandleInformation( HANDLE hObj,PDWORD pdwFlags);DWORD dwFlags;GetHandleInformation(hObj, &dwFlags);BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT)); 

4、跨进程共享内核对象的第二种方法是命名内核对象。许多内核对象是可以命名的,下面的函数都可以创建命名的内核对象:

HANDLE CreateMutex( PSECURITY_ATTRIBUTES psa, BOOL bInitialOwner, PCTSTR pszName); HANDLE CreateEvent( PSECURITY_ATTRIBTUES psa, BOOL bManualReset, BOOL bInitialState, PCTSTR pszName); HANDLE CreateSemaphore( PSECURITY_ATTRIBUTES psa, LONG lInitialCount, LONG lMaximumCount, PCTSTR pszName); HANDLE CreateWaitableTimer(  PSECURITY_ATTRIBUTES psa, BOOL bManualReset, PCTSTR pszName); HANDLE CreateFileMapping( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName); HANDLE CreateJobObject( PSECURITY_ATTRIBUTES psa, PCTSTR pszName);


所有函数的最后一个参数都是PCTSTR pszName用于命名内核对象,当为参数传递NULL时,就创建一个匿名内核对象。此时,可以通过使用内核对象句柄的继承性或者使用DuplicateHandle共享跨进程的内核对象。

 

HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, “ITcomboxMutex”);HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, “ITcomboxMutex”);


上面两行代码表示:

ProcessA创建了一个新的名为”ITcomboxMutex”的互斥对象,hMutexProcessA是默认安全性和不可继承的句柄,且若是命名对象时,它不必是可继承的句柄。

ProcessB也会去创建一个名为”ITcomboxMutex”的互斥对象,其中ProcessB不必是ProcessA的子进程,这样也说明了使用命名对象不必需要句柄的继承性而是实现跨进程共享内核对象。ProcessB调用CreateMutex时,系统检查是否已有名为“ITcombxoMutex”的互斥内核对象。若有,则检查该已有内核对象的类型,并进行安全检查,确定ProcessB是否具有访问该内核对象的权限。若有,系统则找到ProcessB的句柄表的一个空项目,并对该项目初始化,使该项目指向现有的内核对象。

如果该已有的内核对象类型不匹配,或者ProcessB没访问权限,则CreateMutex返回NULL

ProcessB调用CreateMutex成功,则ProcessB的句柄表中的一个新项目将引用该内核对象,该互斥内核对象的引用计数就会递增。需要注意的是,命名对象的这种拷贝,并不像内核对象的句柄继承那样,需要将父子进程的句柄值设置一致。故ProcessAProcessB的句柄值很可能是不同的。两个进程都使用各自的句柄值来操作同一个互斥内核对象。

5Create*Open*的相同点和不同点:

简单的讲,Create*就是比Open*多了一种在内核对象不存在的情况下去创建一个内核对象。

若内核对象已经存在,则执行的操作是一样的。通过上面一点的跨进程共享内核对象的第二种方法,命名内核对象,可以知道,当ProcessB调用CreateMutex要创建的内核对象存在,则是更新ProcessB的句柄表,并递增互斥内核对象的使用计数;而Open*的操作也是更新ProcessB的句柄表,并递增互斥内核对象的使用计数。只是ProcessB调用CreateMutex时,应用程序会立即调用GetLastError,看要创建的互斥内核对象是否存在,存在则执行Open*操作,不存在则Create*

 

下面介绍一下,命名内核对象方法中另一种共享内核对象的方法。就是显示Open*函数。列出一些(参数都一样)

HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle,  PCTSTR pszName);HANDLE OpenEvent(…);HANDLE OpenSemaphore(…);HANDLE OpenWaitableTimer(…);HANDLE OpenFileMapping(…);HANDLE OpenJobObject(…);


6、原先是使用GUID去保证内核对象的唯一性命名。用来防止运行一个程序的多个实例。这时候需要调用WinMain中的Create*函数,在Create*后面调用GetLastError,以确保只运行一个程序实例。

int WINAPI WinMain(HINSTANCE hinstExe,HINSTANCE,PSTR pszCmdLine, int nCmdShow){HANDLE hMutex = CreateMutex(NULL, FALSE, “{FA531CC1-0497-….}”);if(GetLastError() == ERROR_ALREADY_EXISTS){…}CloseHandle(hMutex);return(0);}


 

7、终端服务器的命名空间

终端服务器拥有内核对象的多个命名空间,每个客户端程序会话都有自己的命名空间,一个会话无法访问另一个会话对象。

设置全局命名空间和局部命名空间:

HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, “Global\\MyName”);HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, “Local\\MyName”);


8、跨进程共享内核对象的第三种方法:复制对象句柄

BOOL DuplicateHandle(     HANDLE hSourceProcessHandle,  //源内核对象的进程句柄     HANDLE hSourceHandle,       //源内核对象句柄     HANDLE hTargetProcessHandle,  //目标内核对象的进程句柄     PHANDLE phTargetHandle,         //目标内核对象句柄     DWORD dwDesiredAccess,         //访问屏蔽     BOOL bInheritHandle,                //是否可继承     DWORD dwOptions            );


记住,内核对象属于内核,不属于进程,进程只是引用内核对象,将内核对象的值初始化到进程的句柄表的空项中(这样有利于理解下面的解释)

这里得着重解释一下最后一个参数,dwOptions可以理解成与dwDesiredAccess一起控制目标内核对象句柄是否和源内核对象句柄具有相同的访问屏蔽。这里可能是用两个参数可以满足一些访问屏蔽的情况而设定的。

dwOptions0DUPLICATE_SAME_ACCESSDUPLICATE_CLOSE_SOURCE.

0:不控制访问屏蔽,只由dwDesiredAccess控制。

DUPLICATE_SAME_ACCESS:目标内核对象句柄的访问屏蔽与源内核对象句柄的值一样。这时dwDesiredAccess可以为0,应该说DuplicateHandle忽略dwDesiredAccess的值。

DUPLICATE_CLOSE_SOURCE:当传递完源内核对象后,关闭源内核对象的句柄,这样使得内核对象的使用计数不变。其实,当拷贝源内核对象到目标内核对象完成后,要操作目标进程的句柄还得看是否有足够的权限。将调用Open*函数:

OpenProcess(     DWORD dwDesiredAccess,     BOOL bInheritHandle,     DWORD dwProcessId           //进程的ID);


还有一点需要注意:

不要在SourceProcess中去CloseHandle(phTargetHandle)关闭TargetProcess的内核对象句柄,因为这个句柄值不属于SourceProcess,但是又由于SourceProcessTargetProcess共享同一个内核对象,即SourceProcess具有该内核对象的访问权,而且可能SourceProcess中的源内核对象的句柄值与TargetProcess中的目标内核对象的句柄值相同,这样就会调用CloseHandle成功,会产生无法预料的结果。


 

0 0