Windows核心编程:第三章 内核对象
3.1 何为内核对象
->内核对象包括:访问令牌(access token)对象,事件对象,文件对象,文件映射对象,
I/O完成端口对象,作业对象,邮件槽对象,互斥量对象,管道对象,
进程对象,信号量对象,线程对象,可等待的计时器对象,线程池工厂对象
->每个内核对象都只是一个内存块,由操作系统分配,只能由操作系统内核访问,
这个内存块是一个数据结构,包含了对象的相关信息。少数结构成员(安全描述符和
使用计数)是所有对象都有的,其他成员都是不同对象特有的。
->使用计数是所有内核对象类型都有的一个数据成员,初次创建一个内核对象时它的使用
计数设为1,另一个进程获得对现有内核对象的访问后,使用计数就会增加1,一段
使用计数为0,那么操作系统就会销毁该对象。本进程结束后,另一进程没有结束,
内核对象也不会销毁,所以内核对象的生命周期可能长于创建它的那个进程。
->内核对象可以用一个安全描述符(security description SD)来保护,安全描述符描述
了谁(通常是对象的创建者)拥有对象,那些组和用户被允许访问或使用此对象,哪些
组和用户被拒绝访问此对象。
eg: HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);
typedef struct _SECURITY_ATTRIBUTES{
DWORD nLength;
LPVOID lpSecurityDescription;
BOOL bInheritHandle;
}SECURITY_ATTRIBUTES;
如果psa传入NULL,内核对象就具有默认的安全性,如果想让内核对象的访问限制,
就必须创建一个安全描述符,然后初始化SECURITY_ATTRIBUTES结构
eg: SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = pSD; // 初始化的SD地址
sa.bInheritHandle = FALSE; // 与安全性其实没有关系
3.2 进程内核对象句柄表
->一个进程在初始化时,系统会分配一个只供内核对象使用的句柄表。
句柄表构成:一个内核对象指针,一个访问掩码(access mask)和一些标志。
一个进程首次初始化的时候其句柄表为空
索引 指向内核对象内存块的指针 访问掩码 标志
1 0x???????? 0x???????? 0x????????
2 0x???????? 0x???????? 0x????????
... .... ... ...
提示:句柄值除以4就是索引。
->所有内核对象想要关闭都需要调用CloseHandle向系统表明我们已经结束使用对象
eg:BOOL CloseHandle(HANDLE hobject) // 函数返回之前会清除进程句柄表中的记录项
该函数首先检查主调进程的句柄表,验证“传给函数的句柄表值”标识的是“进程确
实有权访问的一个对象”。如果句柄是有效的,系统就会获得内核对象的数据结构
的地址,并将结构中的“使用计数”成员递减。
->进程终止时,操作系统会将此进程所使用的所有资源都释放,系统会自动扫描该
进程的句柄表,如果这个表中有任何有效的记录项,操作系统都会释放。
3.3 跨进程边界共享内核资源
->作用:①利用文件映射对象,可以在同一台机器上运行的两个不同进程之间共享数据段。
②借助邮件槽和命名管道,在网络中的不同计算机上运行的进程可以相互发送
数据块。
③互斥量、信号量和事件允许不同进程中的线程同步执行。
->使用对象句柄继承
-1->只有在进程之间是父子关系的时候才可以使用对象句柄继承实现跨进程共享内核对象。
eg: SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescription = NULL; // 表明对象用默认安全性来创建
sa.bInheritHandle = TRUE; // 表明对象句柄是可以被子进程继承的
HANDLE hMutex = CreateMutex(&sa,FALSE,NULL);
-------------------------------------------
BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD dwCreationFlags,
PVOID pvEnvironment,
PCTSTR pszCurrentDirectory,
LPSTARTUPINFO pStartupInfo,
PPROCESS_INFORMATION pProcessInformation);
// bInheritHandle为TRUE时,子进程就会继承父进程的“可继承句柄”的值。
子进程创建时它会遍历父进程的句柄表,对它的每一个记录项进行检查,凡是
父进程中有效的"可继承的句柄"项,都会被完整的复制到子进程的句柄表。
而且继承后的句柄在子进程句柄表中的位置和父进程中的位置是完全一致的。
由前面知道句柄值除以4就是索引。可以知道在父进程和子进程中,“可继承句柄”
的句柄值是完全一样的。
注意:对象句柄的继承只会在生成子进程的时候发生,创建完毕后,父进程创建的
其他的内核对象,子进程不能继承。
-2->为了销毁这个共享的内核对象,父进程和子进程都要对这个对象调用CloseHandle。
-3->子进程在继承了父进程的内核句柄后,并不知道自己继承了任何句柄。那么我们就需要
将内核对象的句柄值传给子进程,通常方式有:
①将句柄值作为命令行参数传给子进程,因为句柄值在父进程和子进程中是一样的
②让父进程等待子进程完成初始化(WaitForInputIdle),然后父进程发消息通知子进程。
③让父进程向其环境块添加一个环境变量,子进程会继承父进程的环境变量,用
GetEnvironmentVariable来获得这个句柄值。
-4->改变句柄的标志:父进程创建了一个内核对象,得到一个可继承的句柄,然后生成2个
子进程,父进程只希望其中的一个子进程继承该内核对象。遇到这种情况就可以用
SetHandleInfomation函数来改变内核对象句柄的继承标志。
BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags);
SetHandleInformation(hObj,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT); // 打开继承标志
SetHandleInformation(hObj,HANDLE_FLAG_INHERIT,0); // 关闭内核继承标志
BOOL GetHandleInformation(
HANDLE hObject,
PDWORD pdwFlags); // 可以获取句柄的当前标志,用来判断当前句柄是否是可以继承的。
DWORD dwFlags;
GetHandleInformation(hObj,&dwFlags);
BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));
->为对象命名
HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
PCTSTR pszName); // 跨进程边界共享内核对象的第二个办法就是为对象命名
pszName传入NULL时就会创建一个匿名对象。Microsoft没有提供专门的机制来保证为内核
对象指定的名称是唯一的。所有的内核对象都共享同一个命名空间。
HANDLE hMutex = CreateMutex(NULL,FALSE,_T("JeffObj"));
HANDLE hSem = CreateSemaphore(NULL,1,1_T("JeffObj")); // 此时创建内核对象会失败,因为
// 已经存在一个名为JeffObj的内核对象了。
DWORD dwErrorCode = GetLastError(); // 代码为6(ERROR_INVALID_HANDLE)
注意:通过为对象命名来实现共享时,是否可以继承并非一个必要条件。
假设进程A启动并调用以下函数:
HANDLE hMutexProcessA = CreateMutex(NULL,FALSE,_T("JeffMutex"));
后来某个进程生成了进程B(B不一定是A的子进程),进程B中调用
HANDLE hMutexProcessB = CreateMutex(NULL,FALSE,_T("JeffMutex"));
当进程B调用CreateMutex时,系统会首先查看是否存在一个名为“JeffMutex”的内核对象。
如果存在,就会接着检查对象的类型,而名为"JeffMutex"的对象也是一个互斥量对象,
系统会接着执行一个安全检查,验证调用者是否拥有对该对象的完全访问权限,如果答案
是肯定的,系统就会在进程B的句柄表中查找一个空白记录项,并将其初始化为指向现有的
内核对象。如果对象的类型不匹配,或调用者被拒绝访问,CreateMutex就会失败返回NULL。
进程B中调用CreateMutex成功后并不会实际的创建一个内核对象,在进程B的句柄表中用一个
新的记录项来引用了这个对象,故该对象的使用计数会递增。但由于对象在A,B2个进程中的
记录表的位置不同,故句柄值有可能是不同值的。不过它们可以在自己的进程中用自己的
内核对象的句柄值来引用该内核对象。
注意:可以在进程B中调用GetLastError判断自己是新建的内核对象,还是打开一个已经存在的
HANDLE hMutexProcessB = CreateMutex(NULL,FALSE,_T("JeffMutex"));
if(GetLastError() == ERROR_ALREADY_EXISTS)
// 打开一个现有的内核对象
else
// 创建一个新的内核对象
除了用Create*系列的函数打开一个现有的内核对象,还可以调用Open*系列的函数打开内核对象。
不同之处:如果对象不存在,Create*系列函数会创建它,Open*系列只是返回调用失败。
eg: HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle, // 传入TRUE,返回的句柄就是可继承的
PCTSTR pszName);
说明:pszName不能传入NULL,必须指出内核对象的名称,这类函数会首先在内核对象命名
空间中搜索该名称(没有找到GetLastError返回ERROR_FILE_NOT_FOUND),然后会匹配其内核
对象类型(类型不同ERROR_INVALID_HANDLE),名称类型都相同,系统就会检查请求的访问
(通过dwDesiredAccess来指定),如果允许就会更新主调进程的句柄表,并使对象计数递增。
前面提到Microsoft没有提供任何机制确保内核对象的名称是唯一的,需要怎样确保我们创建的
内核对象名是独一无二的了?(有用到时在仔细看,头大)
->复制对象句柄
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle, // 必须是进程内核对象
HANDLE hSourceHandle, // 该句柄必须与hSourceProcessHandle句柄所标识的进程相关
HANDLE hTargetProcessHandle, // 必须是进程内核对象
PHANDLE phTargetHandle, // 用来接受复制得到的HANDLE值
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions); // 可以是0或者是DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE的组合
该函数获取源进程相关的内核对象的句柄表中的记录项,然后在另一个进程的句柄表中创建
这个记录项的一个副本。
DuplicateHandle复制对象句柄,可以从当前进程复制内核对象句柄到当前进程中。可以达到更改
内核对象的访问权限(修改dwDesiredAccess值)。
GetCurrentProcess会返回一个伪句柄,该句柄始终标识主调进程。
目标进程并不知道它可以访问这个复制的内核对象了,必须使用窗口消息或者其他进程间通讯(IPC)机制。
使用命令行或者更改目标进程的环境变量是行不通的。
3.1 何为内核对象
->内核对象包括:访问令牌(access token)对象,事件对象,文件对象,文件映射对象,
I/O完成端口对象,作业对象,邮件槽对象,互斥量对象,管道对象,
进程对象,信号量对象,线程对象,可等待的计时器对象,线程池工厂对象
->每个内核对象都只是一个内存块,由操作系统分配,只能由操作系统内核访问,
这个内存块是一个数据结构,包含了对象的相关信息。少数结构成员(安全描述符和
使用计数)是所有对象都有的,其他成员都是不同对象特有的。
->使用计数是所有内核对象类型都有的一个数据成员,初次创建一个内核对象时它的使用
计数设为1,另一个进程获得对现有内核对象的访问后,使用计数就会增加1,一段
使用计数为0,那么操作系统就会销毁该对象。本进程结束后,另一进程没有结束,
内核对象也不会销毁,所以内核对象的生命周期可能长于创建它的那个进程。
->内核对象可以用一个安全描述符(security description SD)来保护,安全描述符描述
了谁(通常是对象的创建者)拥有对象,那些组和用户被允许访问或使用此对象,哪些
组和用户被拒绝访问此对象。
eg: HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);
typedef struct _SECURITY_ATTRIBUTES{
DWORD nLength;
LPVOID lpSecurityDescription;
BOOL bInheritHandle;
}SECURITY_ATTRIBUTES;
如果psa传入NULL,内核对象就具有默认的安全性,如果想让内核对象的访问限制,
就必须创建一个安全描述符,然后初始化SECURITY_ATTRIBUTES结构
eg: SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = pSD; // 初始化的SD地址
sa.bInheritHandle = FALSE; // 与安全性其实没有关系
3.2 进程内核对象句柄表
->一个进程在初始化时,系统会分配一个只供内核对象使用的句柄表。
句柄表构成:一个内核对象指针,一个访问掩码(access mask)和一些标志。
一个进程首次初始化的时候其句柄表为空
索引 指向内核对象内存块的指针 访问掩码 标志
1 0x???????? 0x???????? 0x????????
2 0x???????? 0x???????? 0x????????
... .... ... ...
提示:句柄值除以4就是索引。
->所有内核对象想要关闭都需要调用CloseHandle向系统表明我们已经结束使用对象
eg:BOOL CloseHandle(HANDLE hobject) // 函数返回之前会清除进程句柄表中的记录项
该函数首先检查主调进程的句柄表,验证“传给函数的句柄表值”标识的是“进程确
实有权访问的一个对象”。如果句柄是有效的,系统就会获得内核对象的数据结构
的地址,并将结构中的“使用计数”成员递减。
->进程终止时,操作系统会将此进程所使用的所有资源都释放,系统会自动扫描该
进程的句柄表,如果这个表中有任何有效的记录项,操作系统都会释放。
3.3 跨进程边界共享内核资源
->作用:①利用文件映射对象,可以在同一台机器上运行的两个不同进程之间共享数据段。
②借助邮件槽和命名管道,在网络中的不同计算机上运行的进程可以相互发送
数据块。
③互斥量、信号量和事件允许不同进程中的线程同步执行。
->使用对象句柄继承
-1->只有在进程之间是父子关系的时候才可以使用对象句柄继承实现跨进程共享内核对象。
eg: SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescription = NULL; // 表明对象用默认安全性来创建
sa.bInheritHandle = TRUE; // 表明对象句柄是可以被子进程继承的
HANDLE hMutex = CreateMutex(&sa,FALSE,NULL);
-------------------------------------------
BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD dwCreationFlags,
PVOID pvEnvironment,
PCTSTR pszCurrentDirectory,
LPSTARTUPINFO pStartupInfo,
PPROCESS_INFORMATION pProcessInformation);
// bInheritHandle为TRUE时,子进程就会继承父进程的“可继承句柄”的值。
子进程创建时它会遍历父进程的句柄表,对它的每一个记录项进行检查,凡是
父进程中有效的"可继承的句柄"项,都会被完整的复制到子进程的句柄表。
而且继承后的句柄在子进程句柄表中的位置和父进程中的位置是完全一致的。
由前面知道句柄值除以4就是索引。可以知道在父进程和子进程中,“可继承句柄”
的句柄值是完全一样的。
注意:对象句柄的继承只会在生成子进程的时候发生,创建完毕后,父进程创建的
其他的内核对象,子进程不能继承。
-2->为了销毁这个共享的内核对象,父进程和子进程都要对这个对象调用CloseHandle。
-3->子进程在继承了父进程的内核句柄后,并不知道自己继承了任何句柄。那么我们就需要
将内核对象的句柄值传给子进程,通常方式有:
①将句柄值作为命令行参数传给子进程,因为句柄值在父进程和子进程中是一样的
②让父进程等待子进程完成初始化(WaitForInputIdle),然后父进程发消息通知子进程。
③让父进程向其环境块添加一个环境变量,子进程会继承父进程的环境变量,用
GetEnvironmentVariable来获得这个句柄值。
-4->改变句柄的标志:父进程创建了一个内核对象,得到一个可继承的句柄,然后生成2个
子进程,父进程只希望其中的一个子进程继承该内核对象。遇到这种情况就可以用
SetHandleInfomation函数来改变内核对象句柄的继承标志。
BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags);
SetHandleInformation(hObj,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT); // 打开继承标志
SetHandleInformation(hObj,HANDLE_FLAG_INHERIT,0); // 关闭内核继承标志
BOOL GetHandleInformation(
HANDLE hObject,
PDWORD pdwFlags); // 可以获取句柄的当前标志,用来判断当前句柄是否是可以继承的。
DWORD dwFlags;
GetHandleInformation(hObj,&dwFlags);
BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));
->为对象命名
HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
PCTSTR pszName); // 跨进程边界共享内核对象的第二个办法就是为对象命名
pszName传入NULL时就会创建一个匿名对象。Microsoft没有提供专门的机制来保证为内核
对象指定的名称是唯一的。所有的内核对象都共享同一个命名空间。
HANDLE hMutex = CreateMutex(NULL,FALSE,_T("JeffObj"));
HANDLE hSem = CreateSemaphore(NULL,1,1_T("JeffObj")); // 此时创建内核对象会失败,因为
// 已经存在一个名为JeffObj的内核对象了。
DWORD dwErrorCode = GetLastError(); // 代码为6(ERROR_INVALID_HANDLE)
注意:通过为对象命名来实现共享时,是否可以继承并非一个必要条件。
假设进程A启动并调用以下函数:
HANDLE hMutexProcessA = CreateMutex(NULL,FALSE,_T("JeffMutex"));
后来某个进程生成了进程B(B不一定是A的子进程),进程B中调用
HANDLE hMutexProcessB = CreateMutex(NULL,FALSE,_T("JeffMutex"));
当进程B调用CreateMutex时,系统会首先查看是否存在一个名为“JeffMutex”的内核对象。
如果存在,就会接着检查对象的类型,而名为"JeffMutex"的对象也是一个互斥量对象,
系统会接着执行一个安全检查,验证调用者是否拥有对该对象的完全访问权限,如果答案
是肯定的,系统就会在进程B的句柄表中查找一个空白记录项,并将其初始化为指向现有的
内核对象。如果对象的类型不匹配,或调用者被拒绝访问,CreateMutex就会失败返回NULL。
进程B中调用CreateMutex成功后并不会实际的创建一个内核对象,在进程B的句柄表中用一个
新的记录项来引用了这个对象,故该对象的使用计数会递增。但由于对象在A,B2个进程中的
记录表的位置不同,故句柄值有可能是不同值的。不过它们可以在自己的进程中用自己的
内核对象的句柄值来引用该内核对象。
注意:可以在进程B中调用GetLastError判断自己是新建的内核对象,还是打开一个已经存在的
HANDLE hMutexProcessB = CreateMutex(NULL,FALSE,_T("JeffMutex"));
if(GetLastError() == ERROR_ALREADY_EXISTS)
// 打开一个现有的内核对象
else
// 创建一个新的内核对象
除了用Create*系列的函数打开一个现有的内核对象,还可以调用Open*系列的函数打开内核对象。
不同之处:如果对象不存在,Create*系列函数会创建它,Open*系列只是返回调用失败。
eg: HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle, // 传入TRUE,返回的句柄就是可继承的
PCTSTR pszName);
说明:pszName不能传入NULL,必须指出内核对象的名称,这类函数会首先在内核对象命名
空间中搜索该名称(没有找到GetLastError返回ERROR_FILE_NOT_FOUND),然后会匹配其内核
对象类型(类型不同ERROR_INVALID_HANDLE),名称类型都相同,系统就会检查请求的访问
(通过dwDesiredAccess来指定),如果允许就会更新主调进程的句柄表,并使对象计数递增。
前面提到Microsoft没有提供任何机制确保内核对象的名称是唯一的,需要怎样确保我们创建的
内核对象名是独一无二的了?(有用到时在仔细看,头大)
->复制对象句柄
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle, // 必须是进程内核对象
HANDLE hSourceHandle, // 该句柄必须与hSourceProcessHandle句柄所标识的进程相关
HANDLE hTargetProcessHandle, // 必须是进程内核对象
PHANDLE phTargetHandle, // 用来接受复制得到的HANDLE值
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions); // 可以是0或者是DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE的组合
该函数获取源进程相关的内核对象的句柄表中的记录项,然后在另一个进程的句柄表中创建
这个记录项的一个副本。
DuplicateHandle复制对象句柄,可以从当前进程复制内核对象句柄到当前进程中。可以达到更改
内核对象的访问权限(修改dwDesiredAccess值)。
GetCurrentProcess会返回一个伪句柄,该句柄始终标识主调进程。
目标进程并不知道它可以访问这个复制的内核对象了,必须使用窗口消息或者其他进程间通讯(IPC)机制。
使用命令行或者更改目标进程的环境变量是行不通的。