《Windows核心编程》学习笔记(4)– 内核对象

什么是内核对象

   内核对象是内核分配的一段空间,如文件对象和进程对象等。可以用Windows提供的函数来创建相应的内核对象。创建成功后返回一个对象句柄,并且对象句柄值是进程相关的。程序不能直接操作内核对象,只能通过Windows提供的函数来控制。

1.1内核对象的使用计数

     内核对象可以被多个进程同时使用,句柄值通常会不一样,但是引用的内容是同一块。内核对象的所有者是操作系统,而非进程,因此内核对象的存在时间通常会比进程的存在时间长。内核对象中有一个值用来保存当前使用该内核对象的进程数,这就是使用计数。这样可以确保在没有进程引用该对象时系统中不保留任何内核对象内核对象由内核控制何时释放,而不是由调用它的进程,每一个内核对象的数据中都有一个进程引用计数,当某进行创建了一个内核对象时,该内核对象中的引用计数被置为1,之后要是有其它进程访问该内核对象时,引用计数加1,当所有访问内核对象的进程都释放,引用计数为0时,该内核对象由内核释放)。

1.2安全性

     内核对象提供了安全描述符来限制操作权限,如文件映射内核对象的函数

     HANDLE CreateFileMapping(

        HANDLE hFile,

        PSECURITY_ATTRIBUTES psa,

        DWORD flProtect,

        DWORD dwMaximumSizeHigh,

        DWORD dwMaximumSizeLow,

        PCTSTR pszName);

   参 数psa就是来控制访问权限的。当访问一个现有的文件映射内核对象时需要设定要对对象执行什么操作,如: HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ, FALSE, "MyFileMapping"),其中FILE_MAP_READ说明是对内核对象执行读操作,"MyFileMapping"是内核对象的名字。

   注:我们可以通过对象的创建函数中是否有安全描述符来区分是内核对象还是一般的用户对象。

2 进程的内核对象句柄表

2.1创建内核对象

   当 进程创建时,它会创建一个句柄表,用于存储内核对象的信息。当生成一个内核对象时,系统会将内核对象的地址以及其他信息存入句柄表,并返回一个句柄。这个 句柄是和内存相关的,即不同的进程访问同一个内核对象时,返回的句柄值是不同的。当访问内核对象时,可以通过句柄到句柄表中查找对应的内核地址

注:当查看创建内核对象的函数返回值时,必须格外小心。特别要注意的是,当调用CreateFile函数失败时,返回值为INVALID_HANDLE_VALUE(值为-1)。其他的内核函数返回值为NULL

2.2关闭内核对象

关 闭内核对象可以调用函数BOOL CloseHandle(HANDLE hobj)。系统会先判断句柄是否有效,如果有效,再判断计数是否为0,如果是的话,就将从内存中撤销该内核对象。不管成功与否,都将从句柄表中删除该对 象信息,该进程将不能再访问。如果忘记调用CloseHandle函数,进程运行时将有可能引起资源泄漏。但是进程终止时,系统浏览句柄表,并释放所有资 源。


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机制。
详述复制对象句柄方法
1。跨进程边界共享内核对象的最后一招是使用DuplicateHandle函数:
BOOL DuplicateHandle( 
  HANDLE hSourceProcessHandle, 
  HANDLE hSourceHandle, 
  HANDLE hTargetProcessHandle, 
  LPHANDLE lpTargetHandle, 
  DWORD dwDesiredAccess, 
  BOOL bInheritHandle, 
  DWORD dwOptions );

简单的说,这个函数获得一个进程的句柄表中的一个记录项,然后在另一个进程的句柄表中创建这个记录项的一个副本.

具体应用如下:
#include <stdio.h>
#include <windows.h>
int main(void)
{
//进程
A
创建一个名为
"MYOBJ"的
互斥量
HANDLE hObjInProcessA = CreateMutex(NULL, FALSE, "MYOBJ");
//判断,如果已经创建了这个互斥量就打印
"run error\n"
if(GetLastError() == ERROR_ALREADY_EXISTS)
{
printf("run error\n");
system("pause");
return 0;
}
//获得进程B的进程句柄(7036为进程B的PID,随便弄了个做了下试验)
HANDLE hProcessB = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 7036);
//
 hObjInProcessB是相对于
进程
B的句柄,他标示的对象就由"进程A中代码引用的
hObjInProcessA"所标示的对象.
HANDLE hObjInProcessB;
DuplicateHandle(GetCurrentProcess(),
hObjInProcessA,
hProcessB,
&hObjInProcessB,
0,
FALSE,
DUPLICATE_SAME_ACCESS);
//收尾工作.
//ps:
hObjInProcessB千万不能关掉
CloseHandle(hProcessB);
CloseHandle(hObjInProcessA);
printf("run \n");
system("pause");
return 0;
}
以上程序第一次运行的时候把互斥量句柄
hObjInProcessA复制到进程B的句柄表中,用变量名
hObjInProcessB来保存.
此时A退出,B不退出,
再运行A,就会打印 run error 的提示,表明互斥量复制成功,已存在于B进程中.
.
2。还可以通过另一种方式来使用
DuplicateHandle:假设一个进程有对一个文件映射对象的读写权限.在程序中的某个位置,我们要调用一个函数,并希望它对文件映像对象进行只读访问.为了使应用程序变得更健壮,可以使用
DuplicateHandle 为现有的对象创建一个新句柄,并确保这个这个新句柄只有只读权限.然后,把这个只读权限的句柄传递给函数.
示例代码:
HANDLE hFileMapRW = CreateFileMapping(INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
10240,
NULL);
HANDLE hFileMapRO;
DuplicateHandle(GetCurrentProcess(), hFileMapRW,
GetCurrentProcess(), &hFileMapRO,
FILE_MAP_READ, FALSE, 0);
//调用自己的函数
ReadFromTheFileMapping(hFileMapRO);
CloseHandle(hFileMapRO);
CloseHandle(hFileMapRW);
posted @ 2011-08-09 09:37  飞翔荷兰人  阅读(1395)  评论(0编辑  收藏  举报

I Love Lina~