解码GDI对象句柄表:
1、本节中我们先猜想到某个地方存在着一个系统(假设是GDI)管理的GDI对象句柄表。然后经过一系列复杂而又长时间的探索过程(此过程略)终于完成了任务。证明了一个结论:
确实存在着系统范围的GDI对象表,甚至还有没有文档记载的函数GdiQueryTable(),它返回对象表指针,这张表对用户模式程序是只读的。
2、由上面那一系统探索过程,过可以得到一个16字节的GDI对象表。得到如下的表中单个入口的形式。
typedef struct
{
void * pKernel;
unsigned short nProcess;
unsigned short nCount;
unsigned short nUpper;
unsigned short nType;
void * pUser;
}GdiTableCell;
(1) pKernel指向页面池:对每个有效GDI对象,pKernel从不为空,并且值总是唯一的。因此看起来对每个GDI对象有一个相应的数据结构,这个数据结构只能从内核模式代码存取,甚至不能从GDI32.DLL直接存取。对于不同进程的对象,从pKernel的值中看不出明显区分区域来。pKernel指向的对象起始地址是0xE1000000,根据《Inside Windows NT》,起始地址是0xE1000000的区域一般是被称为“页面池”的可分页系统的内存堆。
(2)nCount 是一个部分选择计数器:在Windows 2000下,nCount总是零,就是说未使用。但在Windows NT 4.0中,某些GDI对象用了它。为了理解nCount的意义,我们试着将对象句柄选入和取消一个或多个设备上下文中,观察选入和取消是否能根据nCount值的变化而成功。在这个实验中要做是创建对象,把对象选入、取消两个设备上下文,最后删除该对象。
这个小试验告诉我们:对象创建时,它的nCount是以零开始的,对大多数据、类型的对象来说不变。
如下图
a):对于设备相关位图(DDB),当它被选入DC时,nCount从0变到1。如果试图把它选入加一个DC,调用就会失败。当DDB从第一个DC中取消后,nCount从1变到0。无疑,DDB位图的nCount表示一种限制条件:它不能一次被选入一个以上的设备上下文。
b):另一个使用nCount的GDI对象是字体,对于字体,nCount值只是一个选择计数,并不强加任何限制把选择逻辑字体对象选入第二个设备上下文成功后,nCount加1。
c):一个显然的问题是GDI是否对选入设备上下文的对象是有保护,这样可以保证它们不被删除。对调色板来说,答案是肯定的。可以从图中看出,调色板被选入两个DC后,第一个DeleteObject调用失败,但是当调色板从两个DC中取消后,第二个DeleteObject调用成功了。但nCount不用于实现这种保护机制。
d):对其他的GDI对象比如字体、位图、画刷和画笔,程序员可以随便什么时候删除对象,把无效的对象柄留在了设备上下文。对于Windows为什么不一致地使用nCount,还没有明确的解释,也不能解释为什么用它阻止用户删除已选入的对象。
(3)nProcess使得GDI句柄绑定到进程:如果程序想使用另一个进程的GDI对象句柄,Win32 API调用一般会失败。GdiTableCell中的nProcess字段就是这种现象背后的原因。对于库存(stock)对象,如GetStockObject(BLACK_PEN),nProcess被置为零。对于用户进程创建的其他 GDI对象,nProcess是创建进程的进程标识符。
有了这个字段,GDI就会很容易地检查当前进程标识符是否和GDI对象的nProcess字段一致,目的是强制对象句柄不能在另一个进程中访问的规则。
根据微软的文档,进程终止时,由该进程创建的所有GDI对象会被释放。如果你想知道这是怎样实现的,我们现在有一点线索了。GDI只需搜索GDI对象表并删除有指定进程标识符的对象。
(4)nUpper:再次检查句柄:我们发现GDI对象表入口的nUpper字段是4字节GDI对象句柄的高两个字节的完全拷贝——对GDI对象句柄进行错误检查是低成本的冗余校验。
(5)nType:内部对象类型:nType的低字节通常和HGDIOBJ中的是7位类型信息相同,高位字节通常是零。
(6)pUser指向用户模式数据结构。