GlobalLock的前世今生4: 实现细节解析

GlobalLock的前世今生4: 实现细节解析

作者:BlogUpdater |  时间:2021-12-21 |  浏览:519 |  评论已关闭 条评论

在一个内部邮件讨论中,有人提出了下面这个问题:
=========================================================
我们在一些代码中使用到了DragQueryFile这个函数,用来取出文件路径。它的函数原型如下:

在我们的代码中,我们调用了这个函数,并在第一个参数中传入了一个指向DROPFILES结构体的指针,而不是一个HDROP类型。这段代码在开始的几个月可以正常工作,直到上周我们修改了包的结构布局之后,就无法继续工作了。

我知道,问题在于我们应该传递一个HDROP句柄,而不是一个指针,但是我只是感到很奇怪,为什么之前它可以正常工作,而现在就不可以了。换句话说,操作系统如何确定一个句柄是否有效,以及一个指针在什么时候可以被当做一个句柄来使用。
=========================================================

GlobalLock会接收一个HGLOBAL参数,这个HGLOBAL会引用一块固定式GMEM_FIXED或者可移动(GMEM_MOVEABLE)内存块。在Win32的世界,它遵循这样的一条规则:如果内存是固定式的,则HGLOBAL实际上是一个指向该内存块的指针,如果是可移动内存,则HGLOBAL是一个句柄,需要将它转换为一个指针。

GlobalAlloc会和GlobalLock一起紧密的协同工作,这样才能以最快的速度运行GlobalLock。如果被操作的内存恰好被正确的对齐并通过了其他测试,则GlobalLock会认为:”嗯,不错啊,这个句柄是指向的一块固定式内存,我可以直接将它作为指针返回了”。

而上文中提到,他们修改了包的结构,这可能会潜在地修改内存的对齐,这就导致了GlobalLock不会再错误地将这个无效的参数作为一个固定式内存的句柄。它会继续向下执行其他有效性检查,最终发现这个句柄根本就是一个无效句柄。

当然,这并不是授予将虚假指针传递给GlobalLock的权限; 我只是在解释为什么问题会突然出现,即使它一直存在。
有了这个小菜做为铺垫,那么,Win32 中 GMEM_MOVEABLE 背后的真实故事是什么?

GMEM_MOVEABLE内存会分配一个句柄。这个句柄可以通过GlobalLock被转换为内存指针,你可以在一个未锁定的GMEM_MOVEABLE内存块(或者当你将GMEM_MOVEABLE标志传递给 GlobalReAlloc 时锁定的 GMEM_MOVEABLE 块,这意味着“即使它被锁定也移动它”)上调用GlobalReAlloc,然后该内存会移动,但是句柄将继续指向它。你需要重新锁定这个句柄才能得到移动后的新地址。

GMEM_MOVEABLE 在很大程度上是不必要的; 它提供了大多数人用不到的附加功能。 大多数人不介意 Realloc 交回与原始值不同的值。 GMEM_MOVEABLE 主要用于分发内存句柄,然后你决定将其重新分配到句柄背后的情况。 如果你使用 GMEM_MOVEABLE,即使它所指的内存已经移动,句柄仍然有效。

这听起来可能是一个很好的功能,但实际上它比它所带来的价值要麻烦得多。 如果你决定使用可移动内存,则必须在访问之前锁定它,然后在完成后解锁。 所有这些锁定/解锁开销都变得非常痛苦,因为你不能再使用指针了。 在使用它们之前,你必须使用句柄并将它们转换为指针。 (这也意味着没有指向可移动对象中间的指针。)

因此,可移动内存在实际项目中是不会起到什么大作用。

但是请注意,出于兼容性原因,GMEM_MOVEABLE 仍然存在于各个地方。 例如,剪贴板数据必须分配为可移动的。 如果违反此规则,某些程序将崩溃,因为它们对堆管理器如何在内部管理可移动内存块的句柄而不是调用GlobalLock将句柄转换为指针做出了未公开的假设。

一个非常常见的错误是,在使用它们之前忘记锁定全局句柄。 如果你忘记了,而是将一个可移动的内存句柄转换为一个指针,你会得到奇怪的结果(并且可能会破坏堆)。 具体来说,通过 STGMEDIUM 结构的 hGlobal 成员传递、通过 GetClipboardData 函数返回的全局句柄,以及诸如 PRINTDLG 结构的 hDevMode 和 hDevNames 成员等鲜为人知的地方都可能是可移动的。
可怕的是,如果你犯了这个错误,你的代码实际上可能会在很长一段时间内正常运行(如果你正在查看的内存恰好被分配为 GMEM_FIXED),然后突然有一天它崩溃了,因为突然有人给了你分配为 GMEM_MOVEABLE 的内存。

好的,现在关于16位内存管理器的遗留问题就写的差不多了。我的头开始有点疼了。

总结
编写并发布一个对外接口,如同做一个关乎人生的大决定,如果没有考虑清楚,就会带来无穷无尽的麻烦。
因为,后面所发生的所有事情,都依赖这个最初发布的接口。
基于这个原则,开发拓扑梅尔智慧办公平台(Topomel Box)的每一天,我都会如履薄冰,做好每一个微小抉择。
所以,猿友,请考虑清楚当前你正在编写的每一行代码。

最后
Raymond Chen的《The Old New Thing》是我非常喜欢的博客之一,里面有很多关于Windows的小知识,对于广大Windows平台开发者来说,确实十分有帮助。
本文来自:《A history of GlobalLock, part 4: A peek at the implementation》

最近我写了个东西
正如你们所知道的,拓扑梅尔智慧办公平台(Topomel Box)是一款绿色软件,主要面向经常使用电脑的朋友。它提供了各种提升办公效率的小功能,同时操作上尽可能地简单方便。
我想:你值得拥有。

评论已关闭。