关于GDI泄漏和运气的重要性


在2019年5月,我被要求查看一个潜在危险的Chrome错误。起初,我诊断他不重要,以这种方式浪费了两个星期。后来,当我返回调查时,它成为导致浏览器进程崩溃的第一大原因,Chrome浏览器Beta通道中。哎呀

6月6日,也就是我意识到我在解释出发数据时出错的那天,该错误被标记为ReleaseBlock-Stable。这意味着我们将无法确定大多数用户的新版本的Chrome。

发生崩溃是因为我们用完了GDI对象(图形设备接口),但是我们不知道它们是什么类型的GDI对象,诊断数据没有提供有关问题所在的任何线索,因此我们无法重新创建它。

我们团队中的许多人在6月6日至7日努力解决了该错误,他们测试了他们的理论,但没有推进。 6月8日,我决定检查我的邮件,Chrome立即崩溃了。这是同样的失败

真是讽刺。在我寻找更改并检查崩溃报告时,试图找出是什么原因导致Chrome浏览器进程泄漏GDI对象,但我的浏览器中GDI对象的数量却在不断增加,到6月8日上午,它已经超过了神奇的10,000。此时,针对GDI对象的内存分配操作之一失败,并且我们故意使浏览器崩溃。真是不可思议的运气。

如果可以重现该错误,则不可避免地可以对其进行修复。我只需要弄清楚是怎么引起这个错误的,然后我们就可以消除它。

首先,这个问题的简短历史



在Chromium代码的大多数地方,当我们尝试为GDI对象分配内存时,我们首先会检查此分配是否成功。如果不可能分配内存,那么我们将一些信息写入堆栈,并有意执行崩溃,如该源代码所示。失败是有意造成的,因为如果我们不能为GDI对象分配内存,那么我们将无法在屏幕上进行渲染-报告问题(如果启用了崩溃报告)并重新启动该过程比显示一个空UI更好。默认情况下,每个进程最多可以创建10,000个GDI对象,通常只使用数百个。因此,如果我们超过此限制,则完全出错。

当我们收到一份崩溃报告,其中指出GDI对象的内存分配错误时,我们就有了调用堆栈和各种其他有用的信息。精细!但是问题是这样的崩溃转储不一定与错误有关。这是因为导致GDI对象泄漏的代码和报告失败的代码可能不是同一代码。

大致来说,我们有两种类型的代码:

无效的GoodCode(){
   自动x = AllocateGDIObject();
   如果(!x)
     CollectGDIUsageAndDie();
   UseGDIObject(x);
   FreeGDIObject(x);
}

无效BadCode(){
   自动x = AllocateGDIObject();
   UseGDIObject(x);
}

好的代码会注意到内存分配失败,并报告此错误,而坏的代码会忽略崩溃并溢出对象,从而“替换”好代码,使其负责。

铬包含几百万行代码。我们不知道哪个函数有错误,甚至不知道泄漏了什么类型的 GDI对象。我的一位同事添加了崩溃之前绕过Process Environment Block的代码以获取每种类型的GDI对象的数量,但是对于所有枚举类型(设备上下文,区域,位图,调色板,画笔,羽毛和未知),数量都不超过一百。真奇怪。

原来,我们直接为其分配内存的对象在此表中,但是内核没有代表我们创建的对象,它们存在于Windows对象管理器中。这意味着GDIView和我们一样对这个问题也视而不见(此外,GDIView仅在本地发生故障时有用)。因为我们泄漏了游标,并且游标是附加了GDI对象的USER32对象;这些GDI对象的内存是由内核分配的,我们看不到发生了什么。

误解


我们的函数CollectGDIUsageAndDie具有非常生动的名称,我想您会同意我的观点。很有表现力。

问题在于它执行了太多的动作。 CollectGDIUsageAndDie检查了GDI对象的大约十二种不同类型的内存分配失败,并且由于代码的嵌入,它们因此收到了相同的失败签名-它们都崩溃到主要功能中并合并在一起。因此,我的一位同事明智地进行了更改,将不同的检查分解为单独的(不是内置的)功能。因此,乍一看,我们可以了解到哪个检查失败了。

las,这导致了一个事实,当我们开始从CrashIfExcessiveHandles获取崩溃报告时,我自信地说:“这不是失败的原因,仅仅是由签名更改引起的。”

但是我错了。这是导致失败签名更改的原因。哎呀 尴尬的分析,道森。没有适合您的Cookie。

回到我们的故事


至此,我已经知道我在6月7日所做的某件事每天使用近10,000个GDI对象。如果我能理解的话,我将解决这个难题。


Windows任务管理器还有一个GDI对象,可用于查找泄漏。 6月7日,我在家工作,连接到我的工作机,并且在工作机上启用了此列,因为我进行了测试并试图重现崩溃情况。但是与此同时,我的家用计算机上的浏览器中存在GDI对象泄漏。

我在家中使用浏览器的主要任务是使用Chrome远程桌面(CRD)应用程序连接到正在运行的计算机。因此,我打开了家用计算机上的GDI对象列,并开始进行实验。很快我得到了结果。

实际上,该错误的时间轴表明,从“我发生故障”(14:00)到“它与CRD有某种联系”,然后到“光标所在的情况”,只有35分钟。我已经说过,当您可以在本地播放bug时,调查bug有多容易?

事实证明,每次CRD应用程序(或任何Chrome应用程序?)更改光标时,都会导致六个GDI对象泄漏。如果您在使用Chrome远程桌面时将鼠标移到屏幕的所需部分,则每分钟数百个GDI对象和每小时数千个GDI对象可能会泄漏。

在解决此问题一个月都没有进展之后,它突然从无法移动的状态变成了简单的校正。我很快写了一个修正草案,然后我的一位同事(我没有处理此错误)创建了一个真正的修正它于6月10日11:16下载,并于13:00发布。几次合并后,错误消失了。

就这样?


我们已修复了该错误,这很棒,但更重要的是,此类错误永远不会再发生。显然,使用C ++(RAII对象进行资源管理是正确的,但是在这种情况下,该错误包含在WebCursor类中。

关于内存泄漏,有一套可靠的系统。Microsoft具有堆快照,Chromium具有针对用户版本的堆概要分析和一个泄漏消除程序在测试机器上。但是,似乎GDI对象的泄漏已被忽略。进程信息块包含不完整的信息,某些GDI对象只能在内核模式下列出,并且没有单点可以为便于跟踪的对象分配和释放内存。这不是我必须处理的GDI对象的第一次泄漏,也不是最后一次泄漏,因为没有可靠的跟踪它们的方法。这是我对以下Windows版本的建议:

  • 使获取所有类型的GDI对象数量的过程变得微不足道,而不必模糊地阅读PEB(并且无需忽略游标)
  • 创建一种受支持的方式来拦截和跟踪创建和销毁GDI对象的所有操作,以进行可靠的跟踪;包括那些间接创建的
  • 在文档中反映所有这些

就这样。这样的跟踪甚至都不难实现,因为必须以不限制内存的方式限制GDI对象。如果使用这些奇怪但不可避免的GDI对象会更安全,那就太好了。哦拜托。

在这里您可以阅读有关Reddit的讨论。Twitter上的主题从此处开始

All Articles