回炉重造之重读Windows核心编程-003-内核对象
内核对象是个比较难理解的概念,问题的根源就在于即使是《核心编程》书中也没有说清楚它的定义,只是不停地举例和描述它的性质,还有如何使用。
盲人摸象,难见全貌。只能尽可能列举它的性质,注意使用了。
引用计数(书中的说法是使用计数)就是内核对象的一个很关键的性质。由于内核对象的拥有者是内核而不是进程,所以只能由内核来做撤销内核对象的操作。而通常一个内核对象不一定只被一个进程使用的,创建或者撤销内核对象,就要看引用计数了。引用计数在内核对象被创建的时候被置为1,被进程访问一次引用计数就递增1。当引用计数降为0,内核就撤销这个内核对象。(至于何时引用计数递减1,书中没有明确说明,不过有编程经验的你肯定知道是什么时候了)
安全性也是内核对象的一个重要的性质,它用于描述内核对象的访问者。由于创建内核对象的时候几乎都会有一个参数,指向security_attributes结构体的指针,例如createfilemapping(定义不详)。如果这个指针是null值,那就是默认的安全性,只有管理对象的小组成员和创建者可以访问;不过,只要你初始化并操作lpsecuritydescriptor这个成员,就可以设置它的安全性了。这样,内核对象被访问的时候(例如openfilemapping),在返回一个有效的句柄之前会执行一次安全检查,如果不通过检查返回的就不是一个有效句柄,而是null了,它的lasterror是5(error_access_denied)。
进程的内核对象句柄表,在进程被创建的时候被分配。当线程共有内核对象产生的时候,内核会在句柄表中找出一个空项,把内核对象的内存块指针写进去。
如果一个线程中调用函数返回一个句柄,这个句柄可以也只可以被线程中的所有线程使用。这些句柄的值实际上是句柄在当前进程句柄表中的索引,但不是固定的,如在win2k中返回的句柄是用于标识句柄表的该对象的字节数。如果给句柄表中传入一个无效值,getlasterror则会返回6(error_invalid_handle)。
如果调用函数创建内核对象失败了,那么句柄的值通常是0(null),也有些函数返回的是invalid_handle_value。但是无论用什么方式创建内核对象,都要通过closehandle来结束对对象的操作。这个函数会先检查句柄表,确定传入的索引是否有效,并查看引用计数,如果是0则撤销这个对象。
一个无效的句柄传给closehandle的话,getlasterror会返回error_invalid_handle。如果进程正在被调试,则通知调试器,以确定这个错误。
加入忘记调用closehandle,有可能会产生资源泄漏。因为进程终止的时候,系统会扫描它的句柄表中的无效项目,然后关闭这些对象句柄。这时句柄的引用计数就有机会降为0而被撤销了。
当进程间有父子关系的时候,父进程才有机会使用一个或多个内核对象句柄,并且父进程还可以产生一个可以访问这个内核对象的子进程。步骤如下:
- 父进程创建内核对象时,必须指明这个句柄可以被继承。(不是内核对象本身可以被继承)
- 指定一个security_attributes结构并对它进行初始化,然后把结构的地址传给create函数。
- sa.binherithandle = true;
每个句柄表项中都有一个标志位,用以指明这个句柄是否有继承性。如果是binherithandle属性是true,标志位被置1,否则置0。
此后只要调用createprocess中的参数binherithandle是true,那么被创建的进程就在创建空的句柄表的同时,遍历父进程的句柄表找到有继承属性的项目,并拷贝到新的句柄表中完全相同的位置、递增引用计数。这样即使父进程关闭了这个句柄,由于引用计数还没到0,也要等子进程终止的时候才能置零。这样子进程只要知道句柄的值就可以使用了。
改变句柄的标志,可以使用sethandleinformation,第一个参数是句柄,第二个参数就确定修改哪些标志了。和每个句柄相关的就是handle_flag_inherit和handle_flag_project_from_close了。第三个参数dwflags,可以用于指明内核对象的继承标志:
- 打开继承标志:sethandleinformation(hobj, handle_flag_inherit, handle_flag_inherit);
- 关闭继承标志:sethandleinformation(hobj, handle_flag_inherit, 0);
使用sethandleinformation(hobj, handle_flag_project_from_close, handle_flag_project_from_close),会告诉系统这个句柄不应该被关闭,如果关闭则会产生一个异常。只有一种特殊的情况,就是子进程又产生了子进程,而有继承属性的句柄也有可能会被传递到新的子进程。但是旧的子进程有可能在新的子进程生成之前关闭这个句柄,那么父进程就不能和新的子进程通信了,因为没有继承这个内核对象。这种情况下,告诉系统句柄不应该关闭才有意义。
跨进程共享内核对象的第二种方法是给对象命名。
- 进程a创建了一个名字为“jeffmutex”的互斥内核对象。
- 进程b也创建一个名字为“jeffmutex”的互斥内核对象,系统会有以下操作:
- 查看是否已经有同名的内核对象
- 如果同名,就检查同名内核对象的类型。
- 如果类型也相同,系统会查看调用者的访问权限。
- 如果有就找个空项目,初始化再指向现有的内核对象
- 没有权限则返回null。
- 如果类型不匹配,返回null。
- 如果类型也相同,系统会查看调用者的访问权限。
- 不同名就创建新的内核对象。
- 如果同名,就检查同名内核对象的类型。
- 没有就创建新的内核对象。
- 查看是否已经有同名的内核对象
进程b调用函数成功后,不是返回一个内核对象,而是返回一个和进程相关的句柄值。
按名字共享的另一种方法是不使用create*函数,而是open*函数。原型相同。最后一个参数必须是0结尾的地址,不能传递null。如果不存在,getlasterror返回值是2(error_fiel_not_found)。还得检查访问权限,如果有就把引用计数递增1。
为了保证对象的唯一性,建议创建guid用来当作对象的名字。这种方法也多用于检查你的应用程序有另一个进程正在运行。
跨进程共享内核对象的最后一个方法是使用duplicatehandle函数。简单地说,这个函数只是取出这个进程的句柄表中的一项,拷贝到另一个进程的句柄表中。
duplicatehandle的参数虽然多,但不复杂。既然是复制句柄,自然少不了源进程和源句柄,以及目标进程和目标句柄了,设计句柄,就得有它的屏蔽值与继承性,这里给了三个。前四个参数不难理解,只是后面的参数要注意:
- dwoption参数可以是0,也可以是duplicate_same_access和duplicate_close_source。
- 如果设定了duplicate_same_access,则目标进程的句柄拥有相同的访问屏蔽,并让函数忽略dwdesiredaccess参数。
- 如果设定duplicate_close_source,则可以关闭源进程中的句柄。使用该标志时,内核对象的引用计数不会受到影响。
最后书中的例子提到,使用duplicatehandle函数的时候,dwdesiredaccess参数应该设置为只读(file_map_read),这样就可以不影响源进程的句柄,提高健壮性。
duplicate_close_source