读Windows核心编程 - 3

来源:互联网 发布:舟山幼儿园数据 编辑:程序博客网 时间:2024/05/01 15:33

        什么是内核对象?比如:事件对象,文件对象,文件映射对象,互斥对象,进程对象,作业对象,线程对象等等都是内核对象。每个内核对象只是内核分配的一个内存块,并且只能 由内核访问。该内存块是一个数据结构,它的成员负责维护该对象的各种信息。当调用一个用于创建内核对象的函数时,该函数就返回一个用于标识该对象的句柄。该句柄可以被视为一个不透明值,进程中的任何线程都可以使用这个值。注意:该句柄是与进程相关的,也就是说,如果将该句柄值传给另外一个进程,那么这个进程使用这个句柄值就会失败。当然,通过一些特殊的机制可以使多个进程能够共享单个内核对象。稍后会介绍。

        内核对象是有内核拥有的,而不是进程。内核对象的存在时间可以比创建该对象的进程长。每个内核对象都包含一个使用计数,用于记录有多少进程正在使用它,当一个新的进程访问它的时候,该计数就增1,反之减1。当减到0的时候,也就是没有进程引用它的时候该内核对象被撤销。

        内核对象能够得到安全描述符的保护。安全描述符用于描述了谁创建了这个对象,谁能够访问这个对象,谁无权访问等等。安全描述符通常在编写服务器应用程序的时候使用,客户端程序可以忽略这个特性。用于创建内核对象的函数几乎都有一个指向SECURITY_ATTRIBUTES结构的指针作为参数。大多数应该程序只是为该参数传递NULL,这样就可以创建带有默认安全性的内核对象,默认安全性意味着对象的管理小组和对象的创建者都拥有全部的访问权。当你需要获得对相应的一个内核对象的访问权时,必须设定要对该对象执行的操作。比如:OpenFileMapping(FILE_MAP_READ, FALSE, "MyFileMapping"); 它在返回一个有效的句柄值之前,先要执行一次安全性检查,如果(已登录用户)被允许访问该对象,则返回一个有效的句柄值。否则,返回NULL。

        除了内核对象外,你的应用程序也可以使用其他类型的对象,如菜单、窗口、图标、字体等。而这些对象属于用户对象或图形设备接口(GDI)对象。要判断一个对象究竟是用户对象还是内核对象,最简单的方法就是看它的参数是否有一个可以用来设置安全性属性的参数。

        当一个进程初始化时,系统要为它分配一个句柄表。该表只用于内核对象,不用于用户对象和GDI对象。表中存放了内核对象的索引、内核对象的地址、以及一些标志。用于创建内核对象的所有函数均返回与进程相关的句柄,这些句柄可以被相同进程中的任何线程使用。该句柄值实现上是放入进程的句柄表中的索引。但是要记住,句柄的含义没有记录文档资料,并且随时可能变更。如果创建内核对象失败,在判断时需要注意,因为有些函数会返回NULL,而有些会返回INVALD_HANDLE_VALUE。无论怎么创建内核对象,都需要调用CloseHandle来结束对该对象的操作。该函数会检查调用进程的句柄表,如果传进来的句柄是有效的,那么就可以通过索引找到该内核对象的地址,并确定该结构中引用计数的值。如果是0,该对象便从内核中撤销。如果忘记调用CloseHandle,就有可能出现内存泄漏。但是当进程终止运行时,系统能够保证该进程使用的任何资源被释放,当然也包括内核对象。

内核对象在许多情况下需要被共享,如:

1. 文件映射对象可以使你能够在同一台机器上运行的两个进程之间共享数据

2. 邮箱和指定的消息管道使得应用程序能够在连网的不同机器上运行的进程之间发送数据块

3. 互斥对象、信标和事件使得不同的进程中的线程能够同步它们的连续运行

跨越进程边界共享内核对象:

1. 对象句柄的可继承性

        只有当进程具有父子关系时,才能使用对象句柄的继承性。要实现继承,首先在父进程创建内核对象的时候要指明它希望对象的句柄是个可继承的句柄。这个工作需要在SECURITY_ATTRIBUTES结构中指定,为成员bInheritHandle设置为TRUE。进程的句柄表中的标志位指定了该对象的可继承性。0表示不可继承,1表示可继承。除此之外,当父进程调用CreateProcess创建子进程的时候也需要将SECURITY_ATTRIBUTES结构中的bInheritHandle设置为TRUE,告诉系统希望子进程继承父进程的句柄表中的可继承句柄。除了拷贝句柄表项目外,系统还要递增内核对象的使用计数等等。父进程和子进程在该内核对象的关系跟普通的进程一样。此时,子进程只要获得对象的索引值就可以访问到该对象。句柄值的传递方法有很多,最常用的方法是将句柄作为一个命令行参数传递给子进程。另外也可以通过进程间通信的方式,方法之一是让父进程等待子进程完成初始化,然后父进程可以将一条消息发送或展示在子进程中的一个线程创建的窗口中。方法之二是让父进程将一个环境变量添加给他的环境程序块。该变量的名字是子进程知道要查找的某种信息,而变量的值则是内核对象要继承的值。

 改变句柄的标志:有时父进程可能需要创建两个子进程,然而只想其中的一个继承内核对象的句柄。可以通过SeHandleInformation函数可以改变内核对象的继承标志。也可以通过GetHandleInformation获得内核对象的标志。

2. 命名对象

        共享跨越进程边界的内核对象的第二种方法是给对象命名。许多(不是全部)内核对象都可以是命名的,在创建这些对象的时候可以指定对象的名字。如果给这个参数传递NULL就创建无名对象。但是Microsoft没有提供为内核对象赋予名字的原则。例如:当我们试图创建一个称为"JeffObj"的对象的时候,不能保证系统中不存在一个名字为"JeffObj"的对象。因此,对象下面这个CreateSemaphore的调用总是失败:

                                          HANDLE hMutex = CreateMutex(NULL, FALSE, "JeffObj");

                                          HANDLE hSem = CreateSemaphore(NULL, 1, 1 , "JeffObj");

那么,如何使用命名对象来共享对象。首先,在进程A中我们调用CreateMutex(NULL, FALSE, "JeffObj")函数,然后在进程B中也调用相同的函数,此时系统首先要查看是否已经存在一个名为"JeffObj"的内核对象,如果确定存在,内核就会检查对象的类型,在这个例子中,由于两次"JeffObj"对象都是互斥对象,因此系统会执行一次安全性检查,以确定调用者是否拥有对该对象的完整访问权,如果有这种访问权,系统就在B进程的句柄表中找出一个空项,指向这个内核对象。这里有一个地方需要注意,就是在第二次调用CreateMutex函数时,它的前两个参数将被忽略。另外还可以在调用进程B中调用OpenMutex来实现对象的共享,于Create*函数的唯一的区别是,如果对象不存在,那么Open*函数就运行失败。

下面的例子可以确保已有一份程序的实例在运行:

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow)
{
        HANDLE h 
= CreateMutex(NULL, FALSE, "{FA531CC1 - 0497 - 11d3 - A180 - 00105A276C3E}");
        
if(GetLastError() == ERROR_ALREADY_EXISTS)
                
return 0;
        
//...
        CloseHandle(h);
        
return 0;
}

3. 复制对象句柄

        共享跨越进程边界的内核对象的最后一个方法是使用DuplicateHandle函数。该函数读取一个进程的句柄表中的项目,并将该项目拷贝到另一个进程的句柄表中。DuplicateHandle可以涉及3个不同进程,比如,我们可以在进程C中调用这个函数,使得进程S句柄表中的一个项目拷贝到进程T中,得到的内核对象的句柄可以通过窗口消息或某种别的IPC机制传给进程T。当然,更常见的是涉及两个进程的情况,代码如下:

// All of the following code is executed by process S

HANDLE hOjbProcessS 
= CreateMutex(NULL, FALSE, NULL);
HANDLE hProcessT 
= OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessIDT);
HANDLE hObjProcessT;
DuplicateHandle(GetCurrentProcess(), hObjProcessS, hProcessT, 
&hObjProcessT, 0, FALSE, DUPLICATE_SAME_ACCESS);
//...
CloseHandle(hProcessT);
CloseHandle(hObjProcessS);

但是上述代码中不能调用CloseHandle(hObjProcessT),结果可能失败,也可能不会失败。可能会错误的关闭某个其他对象,也可能导致错误的访问。这个函数还有另一种使用方法,假设一个进程拥有一个文件映射对象的读写访问权。在某个位置上,一个函数被调用,它仅仅读取这个文件映射对象。为了避免错误的写入,我们可以通过DuplicateHandle在本进程中在复制一个文件映射对象,但是在复制的时候改变其访问权限,使其变成只读。通过这样的方法可以增加代码的健壮性。

原创粉丝点击