حول تسربات GDI وأهمية الحظ


في مايو 2019 ، طُلب مني إلقاء نظرة على خطأ Chrome يحتمل أن يكون خطيرًا. في البداية ، شخصته بأنه غير مهم ، وأهدر بهذه الطريقة أسبوعين. في وقت لاحق ، عندما عدت إلى التحقيق ، تحولت إلى السبب الأول لتعطل عملية المتصفح في قناة Chrome التجريبية. وجه الفتاة

6 يونيو ، في نفس اليوم الذي أدركت فيه خطأي في تفسير بيانات المغادرة ، تم وضع علامة على الخطأ على أنه ReleaseBlock-Stable. هذا يعني أننا لن نتمكن من إصدار إصدار جديد من Chrome لمعظم المستخدمين حتى نكتشف ما يجري.

يحدث العطل بسبب نفاد كائنات GDI (واجهة جهاز الرسومات) ، لكننا لم نكن نعرف نوع كائنات GDI التي كانت عليها ، ولم تقدم بيانات التشخيص أي أدلة حول مكان المشكلة ، ولم نتمكن من إعادة إنشائها.

عمل العديد من الأشخاص من فريقنا بجد على هذا الخطأ في 6-7 يونيو ، واختبروا نظرياتهم ، لكنهم لم يتقدموا. في الثامن من يونيو ، قررت التحقق من بريدي ، وتعطل Chrome على الفور. كان نفس الفشل .

يا لها من سخرية. بينما كنت أبحث عن تغييرات وفحص تقارير الأعطال ، محاولًا معرفة ما يمكن أن يسبب عملية متصفح Chrome لتسريب كائنات GDI ، كان عدد كائنات GDI في المتصفح الخاص بي يرتفع بلا هوادة ، وبحلول صباح 8 يونيو تجاوز العدد السحري 10000 . عند هذه النقطة ، فشلت إحدى عمليات تخصيص الذاكرة لكائن GDI وقمنا بتعطيل المتصفح عن قصد. لقد كان حظا لا يصدق.

إذا كان بإمكانك إعادة إنتاج الخطأ ، فمن المؤكد أنه يمكنك إصلاحه. كان علي فقط معرفة كيف تسببت في هذا الخطأ ، وبعد ذلك يمكننا القضاء عليه.

بالنسبة للمبتدئين ، تاريخ قصير للمشكلة



في معظم الأماكن في رمز Chromium ، عندما نحاول تخصيص ذاكرة لكائن GDI ، نتحقق أولاً من نجاح هذا التخصيص. إذا لم يكن من الممكن تخصيص الذاكرة ، فإننا نكتب بعض المعلومات على المكدس ونقوم عن عمد عن عمد ، كما يمكن رؤيته في شفرة المصدر هذه . يحدث الفشل عمدًا ، لأنه إذا لم نتمكن من تخصيص ذاكرة لكائنات GDI ، فلن نتمكن من العرض على الشاشة - من الأفضل الإبلاغ عن مشكلة (إذا تم تمكين تقارير الأعطال) وإعادة تشغيل العملية من عرض واجهة مستخدم فارغة. بشكل افتراضي ، يمكنك إنشاء ما يصل إلى 10000 كائنات GDI لكل عملية ، وعادة ما يتم استخدام بضع مئات فقط. لذلك ، إذا تجاوزنا هذا الحد ، حدث خطأ ما تمامًا.

عندما نحصل على أحد تقارير الأعطال التي تنص على خطأ تخصيص الذاكرة لكائن GDI ، فلدينا مجموعة مكالمات وجميع أنواع المعلومات المفيدة الأخرى. غرامة! لكن المشكلة هي أن مثل هذه المقالب لا تتعلق بالضرورة بالخلل. وذلك لأن التعليمات البرمجية التي تسبب تسرب كائنات GDI والرمز الذي يبلغ عن الفشل قد لا يكون نفس الرمز.

بمعنى آخر ، لدينا نوعان من التعليمات البرمجية:

GoodCode () {/ 0} باطل
   auto x = AllocateGDIObject () ؛
   إذا (! x)
     CollectGDIUsageAndDie () ؛
   UseGDIObject (x) ؛
   FreeGDIObject (x) ؛
}}

(BadCode) باطل {)
   auto x = AllocateGDIObject () ؛
   UseGDIObject (x) ؛
}}

يلاحظ الكود الجيد أن تخصيص الذاكرة قد فشل ، ويبلغ عن ذلك ، ويتجاهل الكود السيئ الأعطال وينتج الأشياء ، وبالتالي "استبدال" الكود الجيد بحيث يتحمل المسؤولية.

يحتوي Chromium على عدة ملايين من الأسطر من التعليمات البرمجية. لم نكن نعرف أي وظيفة تحتوي على خطأ ، ولم نكن نعرف حتى نوع كائنات GDI التي كانت تتسرب. أضاف أحد زملائي التعليمات البرمجية التي تجاوزت كتلة بيئة العملية قبل التعطل للحصول على عدد كائنات GDI من كل نوع ، ولكن بالنسبة لجميع الأنواع المذكورة (سياقات الجهاز ، والمساحات ، والصور النقطية ، ولوحات الألوان ، والفرش ، والريش وغير معروفة) لم يتجاوز الرقم مائة. هذا غريب.

اتضح أن الكائنات التي نخصص لها ذاكرة مباشرة في هذا الجدول ، ولكن لا توجد كائنات تم إنشاؤها بواسطة النواة بالنيابة عنا ، وهي موجودة في مكان ما في إدارة كائنات Windows. هذا يعني أن GDIView عمياء تمامًا عن هذه المشكلة كما نحن (بالإضافة إلى ، GDIView مفيد فقط عند لعب الفشل محليًا). لأننا قمنا بتسريب المؤشرات ، والمؤشرات هي كائنات USER32 مع كائنات GDI المرفقة بها ؛ تم تخصيص الذاكرة لكائنات GDI هذه بواسطة النواة ، ولم نتمكن من رؤية ما كان يحدث.

سوء التفسير


لدينا وظيفة CollectGDIUsageAndDie له اسم واضحة جدا، وأعتقد أنكم تتفقون معي في هذا الشأن. معبر جدا.

المشكلة هي أنها تؤدي العديد من الإجراءات. قام CollectGDIUsageAndDie بفحص حوالي اثني عشر نوعًا مختلفًا من حالات فشل تخصيص الذاكرة لكائنات GDI ، ونتيجة لتضمين الكود ، حصلوا على نفس توقيع الفشل - فقد تعطلوا جميعًا في الوظائف الرئيسية ودمجوا معًا. لذلك ، قام أحد زملائي بإجراء تغيير بحكمة ، وكسر عمليات التحقق المختلفة إلى وظائف منفصلة (غير مدمجة). بفضل هذا ، الآن ، للوهلة الأولى ، يمكننا أن نفهم أي شيك انتهى بالفشل.

للأسف ، أدى هذا إلى حقيقة أنه عندما بدأنا في الحصول على تقارير الأعطال من CrashIfExcessiveHandles، قلت بثقة: "هذا ليس سبب الفشل ، إنه ببساطة بسبب تغيير في التوقيع".

ولكنني كنت مخطئا. وكان هذا هو سبب الفشل و التغيير التوقيع. وجه الفتاة تحليل محرج ، داوسون. لا ملفات تعريف الارتباط بالنسبة لك.

عودة إلى قصتنا


في هذه المرحلة ، كنت أعلم بالفعل أن شيئًا قمت به في 7 يونيو استخدم ما يقرب من 10000 كائن GDI في اليوم. إذا استطعت فهم ذلك ، فسأحل اللغز.


يحتوي مدير مهام Windows على عمود كائنات GDI إضافي يمكنك استخدامه للعثور على التسريبات. في 7 يونيو ، كنت أعمل من المنزل ، واتصل بجهاز العمل الخاص بي ، وتم تشغيل هذا العمود على جهاز العمل لأنني أجريت اختبارات وحاولت إعادة إنتاج سيناريو الفشل. ولكن في غضون ذلك ، كانت هناك تسريبات من كائنات GDI في المتصفح على جهاز المنزل الخاص بي .

تتمثل المهمة الرئيسية التي استخدمتها المتصفح في المنزل في الاتصال بجهاز يعمل باستخدام تطبيق سطح المكتب البعيد من Chrome (CRD) . لذلك قمت بتشغيل عمود كائنات GDI على الجهاز المنزلي وبدأت في التجربة. قريبا حصلت على النتائج.

في الواقع ، يوضح الجدول الزمني للخلل أنه منذ لحظة "لقد فشلت" (14:00) إلى "أنها مرتبطة بطريقة ما مع CRD" ، ثم إلى "الحالة في المؤشرات" مرت 35 دقيقة فقط. لقد قلت بالفعل كم هو أسهل للتحقيق في الأخطاء عندما يمكنك تشغيلها محليًا؟

اتضح أنه في كل مرة قام فيها تطبيق CRD (أو أي تطبيق Chrome؟) بتغيير المؤشرات ، أدى ذلك إلى تسرب ستة كائنات GDI. إذا حركت الماوس فوق الجزء المطلوب من الشاشة أثناء العمل مع سطح المكتب البعيد من Chrome ، فقد يتسرب مئات كائنات GDI في الدقيقة والآلاف في الساعة.

بعد شهر من غياب أي تقدم في حل هذه المشكلة ، تحولت فجأة من مشكلة غير قابلة للإزالة إلى تصحيح بسيط. كتبت بسرعة مسودة إصلاح ، ثم أنشأ أحد زملائي (لم أعمل على هذا الخطأ) إصلاحًا حقيقيًا. تم تنزيله في 10 يونيو الساعة 11:16 ، وتم إصداره الساعة 13:00. اختفى الخطأ بعد عمليات دمج قليلة.

هذا كل شئ؟


أصلحنا الخطأ ، وهو أمر رائع ، ولكن الأهم من ذلك ألا تحدث مثل هذه الأخطاء مرة أخرى. من الواضح أنه من الصحيح استخدام كائنات C ++ ( RAII ) لإدارة الموارد ، ولكن في هذه الحالة تم تضمين الخطأ في فئة WebCursor.

عندما يتعلق الأمر بتسريبات الذاكرة ، هناك مجموعة موثوقة من الأنظمة. لدى Microsoft لقطات من الكومة ، يحتوي Chromium على ملفات تعريف كومة لإصدارات المستخدم ومزيل للتسربعلى آلات الاختبار. ولكن يبدو أن تسريبات كائنات GDI قد حرمت من الانتباه. تحتوي كتلة معلومات العملية على معلومات غير مكتملة ، ويمكن سرد بعض كائنات GDI فقط في وضع kernel ، ولا توجد نقطة واحدة لتخصيص وتحرير الذاكرة للكائنات التي يمكن أن تسهل التتبع. لم يكن هذا أول تسرب لكائنات GDI التي تعاملت معها ، ولن يكون الأخير ، لأنه لا توجد طريقة موثوقة لتتبعها. فيما يلي توصياتي لإصدارات Windows التالية:

  • اجعل عملية الحصول على عدد من جميع أنواع كائنات GDI تافهة ، دون الحاجة إلى قراءة PEB بشكل غامض (ودون تجاهل المؤشرات)
  • إنشاء طريقة مدعومة لاعتراض وتتبع جميع عمليات إنشاء وتدمير كائنات GDI من أجل التتبع الموثوق به ؛ بما في ذلك تلك التي تم إنشاؤها بشكل غير مباشر
  • تعكس كل هذا في الوثائق

هذا كل شئ. مثل هذا التعقب ليس من الصعب تنفيذه ، لأن كائنات GDI محدودة بالضرورة بطريقة لا تقتصر فيها الذاكرة. سيكون من الرائع إذا كان استخدام كائنات GDI الغريبة ولكن الحتمية أكثر أمانًا. ارجوك.

هنا يمكنك قراءة المناقشة على Reddit. يبدأ الموضوع على تويتر من هنا .

All Articles