第三章 内核对象
1、 内核对象用来管理进程、线程、和文件等许多类的大量资源。
用到内核对象的地方:
访问令牌环(acess token)对象、
事件对象、
文件对象、
文件映射对象、
I/O完成端口对象、
作业对象、
邮件槽(mailslot)对象、
互斥量(mutex)对象、
管道(pipe)对象、
进程对象、
线程对象、
信号量(semaphore)对象、
可等待的计时器(waitable timer)对象、
线程池工厂(thread pool worker factory)对象等。
每个内核对象都是一个内存块,它由操作系统内核分配,并只能由操作系统内核访问。这个内存块是一个数据结构,其成员维护着内核对象的相关信息,少数成员,比如安全描述符和引用计数是都有的,不同的内核对象还有一些个个性的东西。比如进程有进程ID、一个基本的优先级和一个退出代码;而文件内核对象有一个字节偏移量、一个共享模式和一个打开模式。
内核对象的数据结构只能由操作系统访问,应用程序只能通过windows提供的一组API来访问,这组API提供了恰当的访问权限。
调用创建内核对象的函数后,如果成功会返回一个标识内核对象的句柄,为了增加可靠性,内核对象句柄是和进程相关的,也就是说:句柄的值并不是全局唯一的,而是在某个进程内唯一的。
操作系统会通过“引用计数”来维护内核对象,当另一个进程获得对现有内核对象的访问后,该内核对象的引用计数将加1,当某个内核对象的引用计数减为0时系统将销毁该内核对象。
2、 内核对象的安全性
内核对象可用一个安全描述符(security descriptor, SD)来保护。安全描述符描述了谁(通常是对象的创建者)拥有对象;哪些组和用户被允许访问内核对象;那些组和用户被拒绝访问内核对象。
用于创建内核对象的所有函数几乎都有一个指向SECURITY_ATTRIBUTES结构的指针作为参数。大多数函数会为这个参数传入NULL,即默认的安全属性(具体要取决于当前进程的安全令牌)。
Typedef struct _SECURITY_ATTRIBUTES{ DWORD nLenght; //该结构体大小, sizeof(SECURITY_ATTRIBUTES)
LPVOID LPSecurityDescriptor; //安全描述符
BOOL bInherithandle; //该内核对象句柄是否可被继承
}SECURITY_ATTRIBUTES;
创建内核对象的函数一般都会有一个名字参数,用来指定该内核对象的名字。然而需要注意到的是:windows并没有一种机制来防止内核对象重名的问题,所以当我们创建一个内核对象的时候如果函数返回不成功,最好用GetLastError()判断一下是否返回ERROR_ALREADY_EXIST, 如果返回此值则说明系统中已经有一个此名字的内核对象,可以根据实际情况来看是否可以用OpenXXX函数来打开这个内核对象访问。
创建/访问内核对象的函数一般都会有指定flags的参数,即以什么方式访问该内核对象,这一点很关键!
3、 详细说内核对象
一个进程在初始化时,系统将为它分配一个句柄表,这个句柄表仅提供内核对象使用,不适用于用户对象或GDI对象。
句柄表的结构大体如下:
----------------------------------------------------------------------------------
索引----指向内核对象内存块的指针---访问掩码---标志
----------------------------------------------------------------------------------
一个进程初始化时其句柄表为空,当进程内的某个线程调用函数来产生一个内核对象的时候,操作系统内核将为这个对象分配并初始化一个内存块。然后内核开始扫描句柄表,从中找一个空闲的记录项(empty entry),并对其进行初始化。
系统用索引来表示内核对象的信息保存在进程句柄表中的具体位置,
索引值 == 句柄值 / 4;
索引值为0的不可用,所以最小的索引值为1,从而最小的句柄值为4.
再次强调:内核对象句柄和进程相关,即内核对象句柄不是全局唯一的,而是进程内唯一的!
创建内核对象的函数失败时一般返回NULL 或INVALID_HANDLE_VALUE,具体看MSDN。
当创建了一个内核对象并不想再使用内核对象句柄访问该对象时,一定要关闭该句柄,关闭句柄将使得该内核对象的引用计数减1,如果减1后引用计数为0则销毁该对象。
BOOL CloseHandle(HANDLE hObject);
当给定的句柄参数不正确时函数将ERROR_INVALID_HANDLE;如果给定了一个可用的句柄值,但并不是期望关闭的句柄,则函数将返回TRUE,但是如果用户再视图访问已经被不小心关闭的句柄时将发生错误。
一旦一个句柄被关闭了,就不能再通过句柄访问该内核对象,关闭句柄只是说我们不再对这个内核对象句柄关心。 好的做法是当关闭了一个内核对象句柄后把该句柄值置为NULL。
4、 进程间内核对象的共享
4.1 句柄继承
用于父子进城之间,父进程创建子进程的时候如果指定了子进程可以继承自己【可被继承的句柄】,那么子进程就会继承,需要注意的是:子进程继承到的只能是此时父进程中有的、可被继承的句柄,当子进程创建之后如果父进程又有了可被继承的其他句柄则不会让子进程再次继承,此所谓跨进程边界共享内核对象。
BOOL CreateProcess( PCTSTR pszApplicationName, //一般为NULL
PTSTR pszCommandLine, //命令行,一般为”XX.exe 参数s”,不能为PCTSTR!
PSECURITY_ATTRIBUTES psaProcess, //进程安全属性(包括该子进程句柄是否可被继承)
PSECURITY_ATTRIBUTES psaThread, //线程安全属性,同上
BOOL bInheritHandles, //该子进程是否可继承父进程可被继承的句柄
DWORD dwCreationFlags, // 一系列标志,第四章详细说
PVOID pvEnvironment, //环境变量,父进程向子进程传递可被继承的句柄时可以使用这个参数
PCTSTR pszCurrentDirectory, //进程当前驱动器/目录
LPSTARTUPINFO pStartupInfo, //初始化信息,第四章详细说
PPROCESS_INFORMATION pProcessInformation //进程信息,包括子进程和子进程的主线程的句柄和ID,当创建完该子进程后需要如果不再关心这个内核对象则要关闭该子进程句柄和主线程句柄(CloseHandle)
);
当参数bInheritHandles为TRUE时,子进程可以继承父进程可被继承的句柄。
在子进程中,从父进程继承来的句柄和父进程中的句柄值是一样的,继承后系统还会增加该内核的引用计数。
(这里本来想到了一个问题,结果一想就通了,那就是:当子进程继承的父进程句柄值和自己当前拥有的内核对象句柄值一样时怎么办? 一想这是个伪命题,因此这是不可能的,因此继承据句柄只发生在进程边界上,也就是创建进程时,此时子进程是不可能是拥有其他据句柄的,因此进程初始时句柄表为空,此所谓跨进程边界共享内核对象!)
奇怪的是:子进程虽然在法律上继承了父进程的句柄,但是它却不知道自己继承了任何句柄,所以需要通过某种方式把继承到句柄传给子进程,这里的方法可有如下:
① 命令行参数
② 进程间通信机制
③ 让父进程向其环境块添加一个环境变量,环境变量的名字应该是让子进程知道的,句柄的值是该变量的名字。当父进程创建子进程的时候该子进程会继承父进程的环境变量。
4.2、改变句柄的标志
可以控制句柄是否可被继承或者句柄是否可以被关闭。
BOOL SetHandleInformation( HANDLE hObject, // 对象句柄
DWORD dwMaks, //掩码,要更改哪个或哪些标志
DWROD dwFlags //把标志设置为什么
);
dwMask和dwFlags可用的值为:
#define HANDLE_FLAG_INHERIT 0x00000001 //是否可被继承
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002 //是否可被关闭
eg.
1 BOOL bRet = SetHandleInformation(hObject, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); //允许被继承
2
3 bRet = SetHandleInformation(hObject, HANDLE_FLAG_INHERIT, 0); //不可被继承
4
5
6
7 bRet = SetHandleInformation(hObject, HANDLE_FLAG_PROTECT_FROM_CLOSE, 8
9 HANDLE_FLAG_PROTECT_CLOSE); //防止句柄被关闭
10
11 bRet = SetHandleInformation(hObject, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);//允许句柄被关闭。
12
13
14
15 bRet = SetHandleInformation(hObject, HANDLE_FLAG_INHERIT | HANDLE_FLAG_PROTECT_FROM_CLOSE, 0 | 0); //不允许继承并且 允许被关闭。
5、 通过为内核对象命名来共享
需要注意的是:通过名字来共享内核对象与该对象句柄是否可被继承没有关系!
一般创建内核对象的函数都会有一个内核对象名字的参数,例如:
1 HANDLE CreateMutex( 2
3 PSECURITY_ATTRIBUTES psz, 4
5 BOOL bInitialOwner, //创建者是否为拥有者
6
7 PCTSTR pszName //内核对象名字
8
9 ); 10
11
如果不想为内核对象命名,则最后一个参数可为NULL。
这个名字的长度最大可为MAX_PATH,对于内核对象的名字不能重复需要程序员自己控制。
如果创建一个内核对象的时候返回错误,用GetLastError()函数来查找错误代码是ERROR_ALREADY_EXIST,则说明系统中已经存在了同的内核对象(类型是否相同则不一定),可以用内核对象的这一特性来实现一个程序只能启动一个实例。 如果返回的错误代码为ERROR_ALREADY_EXIST,则该内核对象的引用计数要加1.
如果相对内核对象有更多的控制,则可以使用创建内核对象函数的Ex版本。
可以同Open*函数来打开一个命名的内核对象,同时返回一个新的内核对象句柄,该内核对象句柄可被指定为可继承的,而至于原来的该句柄是否为可继承的则没有关系。
Create*和Open*的区别:Open*只是去打开一个命名的内核对象,如果不存在则返回失败返回NULL,GetLastError返回ERROR_FILE_NOT_FOUND,如果存在该命名内核对象但是类型不同函数失败返回NULL,GetLastError将返回ERROR_INVALID_HANDLE。
如果名字相同并且类型也相同将返回该句柄的值。
而Create*函数如果发现创建的命名内核对象已经存在,则打开。
Open*函数只有在打开的内核对象为同名且同类型时才返回成功,如果同名不同类型返回ERROR_INVALID_HANDLE,如果不同命且不同类型返回ERROR_FILE_NOT_FOUND。
Create*函数成功有两种情况:①成功创建了一个内核对象,无重名(不管类型)内核对象
②创建的内核对象指定的名字和类型存在一个相同的,也成功,只不过此时是打开了这个内核对象,函数返回该打开的已经存在的内核对象句柄。
6、 通过内核对象句柄的复制来共享内核对象
1 BOOL DuplicateHandle( 2
3 HANDLE hSourceProcessHandle, //源进程句柄
4
5 HANDLE hSourceHandle, //需要复制的内核对象句柄
6
7 HANDLE hTargetProcessHandle, //目标进程句柄
8
9 PHANDLE phTargetHandle, //[out],接收复制的源句柄的句柄
10
11 DWORD dwDesiredAccess, //期望的访问方式
12
13 BOOL bInheritHandle,//复制到的该目标句柄是否可被子进程继承
14
15 DWORD dwOptions//如下
16
17 );
复制后,在目标进程的句柄表中,该内核对象句柄和源进程中的内核对象句柄在句柄表中的位置(索引)不一定相同,也就是说复制后的句柄值也不一定相同。
dwOptions:
可以为这两个的任意组合
DUPLICATE_SAME_ACCESS:复制到的该句柄和源句柄有一样的访问掩码,如果指定了该选项,则忽略dwDesiredAccess。
DUPLICATE_CLOSE_SOURCE:会比源进程句柄(如果父进程将父进程的该句柄设置不可关闭的,则将不会被关闭)
SetHandleInformation(HANDLE hObject, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE); //防止句柄被关闭
需要注意的是:句柄的复制同样会遇到句柄继承的问题,那就是得到句柄的进程并不知道自己已经可以访问这些句柄了,而是需要通过某种方式来告诉这个进程,不同于句柄继承可以通过命令行参数或者改变目标进程的环境变量的方式,这个时候目标进程早已启动,所以需要通过窗口消息或者其他进程间通信的方式来通知目标进程。
1 void CtestDuplicateDlg::OnBnClickedButton1()
3 { 4
5 // TODO: 在此添加控件通知处理程序代码;
9 HANDLE hEvent = CreateEvent(NULL, FALSE, TRUE, _T("myevent")); 10
11 if (NULL == hEvent)
13 { 14
15 AfxMessageBox(_T("创建失败"));
17 return;
19 } 20
21 else
23 {
25 DWORD dwError = GetLastError(); 26
27 if (ERROR_ALREADY_EXISTS == dwError)
29 { 30
31 AfxMessageBox(_T("已经存在同名事件内核对象"));
34
35 HANDLE hDest = NULL; 36
37 BOOL bRet = DuplicateHandle(GetCurrentProcess(), hEvent, GetCurrentProcess(), &hDest, 0, FALSE, DUPLICATE_SAME_ACCESS); 38
39 if (bRet != FALSE) 40
41 { 42
43 AfxMessageBox(_T("复制成功")); 44
45 } 46
47 else
48
49 { 50
51 AfxMessageBox(_T("复制失败")); 52
53 } 54
55 }
57 } 58
61 } 62
63 ////////////////////////////////////////////////////////////////
64
65
66
67 void CtestDuplicateDlg::OnBnClickedButton2()
69 { 70
71 // TODO: 在此添加控件通知处理程序代码;
72
73 HANDLE hMutex = CreateMutex(NULL, TRUE, _T("myevent"));
76
77 if (NULL == hMutex)
79 { 80
81 DWORD dwError = GetLastError(); 82
83 if (ERROR_ALREADY_EXISTS == dwError)
85 { 86
87 AfxMessageBox(_T("已经存在同名事件内核对象")); 88
89 } 90
91 else if (ERROR_INVALID_HANDLE == dwError)
93 { 94
95 AfxMessageBox(_T("不可用的句柄")); 96
97 } 98
99 } 100
101 }
7、 终端服务命名空间
终端服务(Terminal Service)的情况和前面的描述有所区别,在正在运行终端服务的计算机中,有多个用于内核对象的命名空间:
① 全局命名空间,所有客户端都能访问的内核对象要放在这个命名空间中
② 私有命名空间,每个客户端会话都有的
如果想知道当前进程在哪个终端服务(Terminal service)会话中运行,可以借助于ProcessIdToSessionId函数(由kernel32.dll导出)在winbase.h中声明.。
1 int _tmain(int argc, _TCHAR* argv[])
3 { 4
5 DWORD dwProcssID = GetCurrentProcessId(); 6
7 DWORD dwSessionID = 0;
10
11 if (FALSE != ProcessIdToSessionId(dwProcssID, &dwSessionID))
13 { 14
15 cout<<"dwSessionID="<<dwSessionID<<endl;
17 } 18
19 return 0;
21 } 22
23
关于命名空间,有两个关键字,一个是表示全局命名空间的Global,另一个是表示当前会话的命名空间的的Local。默认没情况下:应用程序的命名内核对象在会话的命名空间中,但是可以通过指定Global来限定其在全局命名空间中。
当创建命名内核对象时,可以在内核对象名字前加上这两个中的任何一个来限定其命名空间,如下:
1 HANDLE hEvnet = CreateEvent(NULL, FALSE, FALSE, _T(“Global\\MyName)); 2
3 HANDLE hEvent2 = CreateEvent(NULL, FALSE, FALSE, _T(“Local\\MyName2”));
上面的代码创建了一个 在全局命名空间中的名为MyName的内核对象和一个在会话的命名空间中的名为MyName2的内核对象。
MS认为:Global、Local、Session都是保留关键字,而所有的保留关键字都是大小写敏感的。
8、 常见自己的专用命名空间
HANDLE WINAPI CreateBoundaryDescriptor( //创建边界描述符
__in LPCTSTR Name, //边界描述符的名字
__in ULONG Flags //传0即可
);
CreateBoundaryDesriptor返回的类型为HANDLE,但他并不是一个内核对象句柄,而是一个伪句柄,所以不能用CloseHandle来关闭,应该用DeleteBoundaryDescriptor来关闭。
BOOL WINAPI CreateWellKnownSid( //创建一个SID(安全描述符)
__in WELL_KNOWN_SID_TYPE WellKnownSidType, //类型
__in_opt PSID DomainSid, //指向创建了SID的域的指针,为NULL时表示使用本地计算机
__out_opt PSID pSid, //指向存储SID的地址
__inout DWORD *cbSid //指向存储pSid的大小的地址
);
BOOLWINAPI AddSIDToBoundaryDescriptor(//增加一个SID到特定的边界描述符
__inout HANDLE *BoundaryDescriptor,//边界描述符指针
__in PSID RequiredSid //SID指针
);
//函数可以将一个按安全描述符格式的字符串转换成一个有效的安全描述符结构。本函数和ConvertSecurityDescriptorToStringSecurityDescriptor函数的功能相反。
BOOL WINAPI ConvertStringSecurityDescriptorToSecurityDescriptor( __in LPCTSTR StringSecurityDescriptor, //一个字符串,以'\0'为结束符,是一个符合安全描述符的字符串,是本函数用来转换的源字符串。
__in DWORD StringSDRevision,// 指定StringSecurityDescriptor的修订级别,目前的版本必须是SDDL_REVISION_1
__out PSECURITY_DESCRIPTOR *SecurityDescriptor,// 一个指向用来存储转换后的SID变量。返回的安全描述符是自相关的。要释放返回的缓冲区,需要调用LocalFree函数。为了将安全描述符转换成一个绝对安全描述符,需要调用MakeAbsoluteSD函数。
__out PULONG SecurityDescriptorSize //一个指向存储返回的缓冲区大小的变量。如果不需要,可以将这个参数传入NULL。
);
释放:
LocalFree(sa.lpSecurityDescriptor);
1 //创建私有命名空间
2
3 HANDLE WINAPI CreatePrivateNamespace( 4
5 _In_opt_ LPSECURITY_ATTRIBUTES lpPrivateNamespaceAttributes, //Conver…函数中用到的安全属性变量
6
7 _In_ LPVOID lpBoundaryDescriptor, //边界描述符
8
9 _In_ LPCTSTR lpAliasPrefix //命名空间前缀;
10
11 ); 12
13
14
15 //打开私有命名空间
16
17 HANDLE WINAPI OpenPrivateNamespace( 18 _In_ LPVOID lpBoundaryDescriptor, //边界描述符
19 _In_ LPCTSTR lpAliasPrefix //命名空间前缀
20 ); 21
22
23 CreatePrivateNamespace和OpenPrivateNamespace返回的HANDLE类型也不是内核对象句柄,而是伪句柄,可以用下面函数关闭: 24
25 BOOL ClosePrivateNamespace( 26
27 HANDLE hNamespace, // 上面两个函数的返回值
28
29 DWORD dwFlags // 30
31 );
如果不希望私有命名空间句柄在关闭后仍然可见,应该把PRIVATE_NAMESPACE_FLAG_DESTROY作为第二个参数传给上述函数,反之则传入0。
边界描述符将在下面两种情况下关闭:
① 进程终止运行
② 调用DeleteBoundaryDescriptor并将边界句柄作为它唯一的参数传给它。
注意:如果还有其他内核对象正在使用,命名空间一定不能关闭,如果在内部还有内核对象时关闭一个命名空间,其他人就可以在同一个边界描述符中重新创建一个相同的命名空间,并在其中创建一个同名的内核对象。
终于写完了这一章!