SafeHandles Handles 库
SafeHandles 是一个使用健壮且安全的句柄来管理数据结构的库。
引言
安全句柄是程序员创建私有内存结构(可能被
唯一值引用)的下一步,该句柄类似于操作系统句柄的行为。
这个名称与 .NET 用于操作系统句柄管理的类名不谋而合,但该库面向 API 编程,或者更普遍地说,面向需要创建安全数据块引用的产品。
这个过程通常使用内存引用块的基地址作为句柄。虽然这种技术对简单程序很有效,但在内存或数据损坏是个问题的复杂应用程序中,它有很多局限性。
单纯使用内存地址有很多问题
- 用户拥有一个内存地址,可以用来访问并可能无意或有意地损坏数据。
- 如果句柄被销毁且数据被释放,保留了内存指针的用户仍然可以尝试访问或重用数据,这会导致内存访问异常或更危险的其他数据损坏。
- 两次连续的分配可能会返回相同的内存指针,其结果是不同的句柄可以共享相同的值,而数据是不同的。
- 出于上述原因,句柄的比较并不实用,因为它们可能具有相同的值,但却是完全不同的数据对象。使用“唯一”键可以保证即使使用相同的表槽,两个连续的句柄也是不同的(除非达到键字段的翻转)。
- 数据是暴露的。如果句柄管理敏感数据,则不允许隐私。
Safe handles 使用一个句柄到数据内存指针的对应表,所以句柄是一个“句柄”而不是一个
内存地址。表的大小由用于条目的位数决定 SAFEHANDLEENTRIESBITS 。
你可以根据所需的条目数进行更改。在 32 位机器上允许的最大值为 16 位。在 64 位系统上,此限制可以高达 32 位。
如果需要,Safe handles 还可以携带关于数据类型的附加信息。句柄的一个特定字段用于此目的,其大小可由用户定义。
Safe handles 具有关联的“唯一”键,可提供对无效句柄使用的附加保护。该键是一个自增计数器,每次创建新句柄时其值都会增加。由于计数器将在达到最大计数时翻转,因此两个句柄具有相同值的概率取决于密钥位的数量。用于密钥的位数是机器位数(32 或 64)与已用于条目索引和类型字段的位数之差。条目越多,为类型字段保留的位数就越少,密钥字段就越窄,唯一密钥就越不安全,重复得越频繁。
使用 Safe handles 有以下优点:
- 安全的内存引用,带有句柄的内部一致性检查。
- 数据混淆。
- 防止数据损坏和未经授权的访问。
- 类型检查强制执行(SHANDLE 类型与 void 指针)。
- 句柄复制。
- 调试支持。
- 如果需要,限制线程访问。
- 按线程进行部分垃圾回收。
SafeHandles 如何工作
安全句柄是一种类似哈希的方法来寻址数据块,通常是动态生成的,避免了内存冲突和数据损坏。
Safehandles 允许定义一个类型字段,该字段编码在句柄中,允许通过查看句柄来区分数据类型(类似于 GDI 对象)。
SafeHandles 创建一个存储键/数据对的内存表。表的大小由用户定义。SafeHandles 有 3 个主要部分:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Unique key | Type | Table Ptr | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
上面的布局适用于 32 位机器,最多有 256 个句柄(8 位)、16 种类型(4 位)和 20 位密钥。
如果定义了 SAFEHANDLEMEMSAFE 符号,句柄将始终将 MSBit 设置为寻址无效内存。这可以防止将句柄用作内存地址,如果作为内存指针使用,将触发内存异常,从而防止对真实数据的任何错误访问。
每次创建新句柄时,密钥都会更改,并在达到密钥位数允许的最大计数时环绕。此功能增加了句柄重用时的安全性。它允许进行句柄比较,并保护代码免于使用过期或已销毁的句柄。用于唯一密钥的位数应该足够安全,适合许多应用程序。但是,编译器会发出一条消息来将密钥强制执行分为强和弱。当密钥的位数少于表大小的 8 倍时,句柄被认为是弱句柄。
考虑到所有这些特性,对数据的访问非常快,并且健全性检查也很直接 :-)
库配置
要修改 SafeHandles 的默认构建,您可以定义预处理器符号值
- SAFEHANDLEENTRIESBITS 设置为用于表索引的位数。在 32 位机器上最大值为 16,在 64 位机器上为 32。
- SAFEHANDLETYPESBITS 您可以保留位数来标识程序中引用的不同对象的句柄类型。默认情况下,库不使用类型,类型位数 = 0。在 32 位机器上最大值为 8,在 64 位机器上为 16。
- SAFEHANDLEMEMSAFE 将此符号定义为 1,以防止将句柄作为内存指针无效使用而触发无效访问。每个生成的句柄都会设置 MSBit。在这种情况下,使用安全句柄作为地址将触发内存访问异常。代价是唯一密钥丢失一位。
- SAFEHANDLEDYNMEM 将此变量定义为 1,以便在库初始化时创建动态内存表。这是处理大型表的首选选项。
多线程
SafeHandles 是线程安全的,核心代码受临界区保护,以获得对内部结构的独占访问权。
错误以多线程方式处理。
作为实验性功能,SafeHandles 可以负责线程上的死数据,通过扫描表并自动删除已结束线程拥有的句柄。
提供了一些高级功能,用于在不同线程之间进行句柄访问的互锁。
当定义了符号 SAFEHANDLEKILLDEADDATA 时,它会启用控制代码,该代码在一个私有线程上运行,该线程持续扫描句柄表以查找由死线程创建的条目。如果句柄是线程本地的(设置了 SH_FLAG_THREADLOCAL 标志),并且创建句柄的线程不再运行,则句柄和相关数据会自动释放,起到垃圾回收器的作用。
句柄属性
Safe handles 可以具有通过句柄标志设置的属性。目前有 3 个可用标志:
- SH_FLAG_NODUP 句柄不能被复制。尝试复制会得到 SH_ERR_NOTALLOWED 错误。
- SH_FLAG_THREADLOCAL 句柄是创建它的线程的本地句柄。其他线程无法访问它。
- SH_FLAG_INHERITABLE 句柄提供对创建者以外的线程的有限访问(尚未实现)。
可以在创建时使用 SH_CreateHandleEx 设置属性,或稍后使用 SH_SetHandleFlags 函数设置。
库源代码
库源代码包括一个测试程序,用于检查 SafeHandles 的工作情况。
该包已准备好使用 PellesC 套件进行编译,与其他编译器一起使用也很简单,因为该库包含在单个源文件中,带有两个头文件,一个用于库配置,另一个用于将 SafeHandles 库包含在用户代码中。
库的使用文档记录在源代码中,并准备好用于 DoxyGen。
注释
这是第一个实现,并且已经知道一些问题。
根复制句柄的释放处理速度很慢,因为指向子句柄中根句柄的字段必须沿着链更新。一种更快的处理方法(如果需要大量复制)是使用指向辅助表中的 hRoot 的指针来替换 hRoot 字段。在这种情况下,当根被删除时,您不需要扫描整个链,而只需更新表条目。
更新
我修正了一些小错误和非小错误。新版本现已发布。