في
مقال سابق ، وعدت بأن أكشف بمزيد من التفصيل بعض التفاصيل التي حذفتها أثناء التحقيق [تعليق Gmail في Chrome على Windows - تقريبًا. لكل.] ، بما في ذلك جداول الصفحات وأقفال و WMI وخطأ vmmap. الآن أقوم بملء هذه الفجوات مع أمثلة كود محدثة. لكن أولاً ، حدد باختصار الجوهر.
كانت النقطة هي أن العملية التي تدعم
Control Flow Guard (CFG) تخصص ذاكرة قابلة للتنفيذ ، بينما تخصص أيضًا ذاكرة CFG التي لا يحررها Windows مطلقًا. لذلك ، إذا واصلت تخصيص ذاكرة قابلة للتنفيذ وتحريرها
في عناوين مختلفة ، فإن العملية تجمع كمية عشوائية من ذاكرة CFG. يقوم متصفح Chrome بذلك ، مما يؤدي إلى تسرب غير محدود للذاكرة ويتجمد على بعض الأجهزة.
وتجدر الإشارة إلى أنه من الصعب تجنب التجمد إذا بدأ VirtualAlloc في التشغيل أكثر من مليون مرة بشكل أبطأ من المعتاد.
بالإضافة إلى CFG ، هناك ذاكرة ضائعة أخرى ، على الرغم من أنها ليست بقدر ادعاءات vmmap.
CFG والصفحات
يتم تخصيص كل من ذاكرة البرنامج وذاكرة CFG في النهاية مع 4 كيلوبايت من الصفحات (المزيد عن هذا لاحقًا). نظرًا لأن 4 كيلوبايت من ذاكرة CFG يمكنها وصف 256 كيلوبايت من ذاكرة البرنامج (المزيد عن ذلك لاحقًا) ، فإن هذا يعني أنه إذا قمت بتحديد كتلة ذاكرة 256 كيلوبايت محاذاة مع 256 كيلوبايت ، فستحصل على صفحة CFG واحدة بحجم 4 كيلوبايت. وإذا قمت بتخصيص كتلة قابلة للتنفيذ بحجم 4 كيلوبايت ، فستظل تحصل على صفحة CFG بحجم 4 كيلوبايت ، ولكن لن يتم استخدام معظمها.

كل شيء يكون أكثر تعقيدًا إذا تم تحرير الذاكرة القابلة للتنفيذ. إذا كنت تستخدم وظيفة VirtualFree على كتلة من الذاكرة القابلة للتنفيذ التي ليست من مضاعفات 256 كيلوبايت أو غير محاذاة عند 256 كيلوبايت ، فيجب على نظام التشغيل إجراء بعض التحليل والتحقق من أن بعض الذاكرة التنفيذية الأخرى لا تستخدم صفحة CFG. قرر مؤلفو CFG ألا يزعجوا - وترك ببساطة ذاكرة CFG المخصصة إلى الأبد. إنه أمر مؤسف للغاية. هذا يعني أنه عندما يخصص برنامج الاختبار الخاص بي ثم يحرر 1 جيجا بايت من الذاكرة القابلة للتنفيذ ، فإنه يترك 16 ميغابايت من ذاكرة CFG.
من الناحية العملية ، اتضح أنه عندما يخصص محرك Chrome JavaScript ثم يطلق 128 ميجابايت من الذاكرة القابلة للتنفيذ المحاذية (لم يتم استخدام كل ذلك ، ولكن تم تخصيص النطاق بالكامل وتحريره على الفور) ، فسيظل تخصيص ما يصل إلى 2 ميجابايت من ذاكرة CFG ، على الرغم من أنه من السهل تحريره بالكامل . نظرًا لأن Chrome يخصص بشكل متكرر الذاكرة ويحررها في عناوين عشوائية ، فإن هذا يؤدي إلى المشكلة الموضحة أعلاه.
فقدان الذاكرة الإضافية
في أي نظام تشغيل حديث ، تحصل كل عملية على مساحة عنوان الذاكرة الظاهرية الخاصة بها ، بحيث يعزل نظام التشغيل العمليات ويحمي الذاكرة. يتم ذلك باستخدام
وحدة إدارة الذاكرة (MMU)
وجداول الصفحات . الذاكرة مقسمة إلى 4 كيلوبايت. هذا هو الحد الأدنى من الذاكرة التي يوفرها لك نظام التشغيل. تتم الإشارة إلى كل صفحة بسجل ثمانية بايت في جدول الصفحة ، ويتم تخزين السجلات نفسها في صفحات 4 كيلوبايت. يشير كل منها إلى 512 صفحة مختلفة كحد أقصى من الذاكرة ، لذلك نحن بحاجة إلى تسلسل هرمي لجداول الصفحات. بالنسبة لمساحة عنوان 48 بت في نظام تشغيل 64 بت ، يكون النظام كما يلي:
- يغطي جدول المستوى 1 256 تيرابايت (48 بت) ، مشيراً إلى 512 جدولاً مختلفاً على مستوى الصفحة 2
- يغطي كل جدول من المستوى 2 512 جيجابايت ، مشيرًا إلى 512 مستوى من 3 طاولات
- يغطي كل جدول من المستوى 3 1 غيغابايت ، مشيراً إلى 512 مستوى من 4 جداول
- يمتد كل جدول من المستوى 4 على 2 ميجابايت ، مشيرًا إلى 512 صفحة فعلية
تقوم MMU بفهرسة جدول المستوى الأول في أول 9 بتات (من أصل 48) من العنوان ، وجداول المستوى الثاني في البتات التسع التالية ، والمستويات المتبقية تعطى 9 بتات ، أي 36 بت فقط. يتم استخدام 12 بت المتبقية لفهرسة 4 كيلو بايت من جدول المستوى الرابع. حسنًا ، حسنًا.
إذا قمت بملء جميع مستويات الجداول على الفور ، فأنت بحاجة إلى أكثر من 512 غيغابايت من ذاكرة الوصول العشوائي ، بحيث يتم ملؤها حسب الضرورة. هذا يعني أنه عند تخصيص صفحة ذاكرة ، يختار نظام التشغيل بعض جداول الصفحات - من صفر إلى ثلاثة ، اعتمادًا على ما إذا كانت العناوين المخصصة في منطقة غير مستخدمة سابقًا تبلغ 2 ميجابايت ، أو منطقة غير مستخدمة سابقًا تبلغ 1 جيجابايت أو منطقة غير مستخدمة سابقًا 512 جيجابايت (جدول صفحات المستوى 1 تبرز دائما).
باختصار ، التخصيص للعناوين العشوائية أغلى بكثير من التخصيص للعناوين المجاورة ، لأنه في الحالة الأولى لا يمكن مشاركة جداول الصفحات. تسريبات CFG نادرة ، لذلك عندما أظهر
vmmap 412.480 كيلوبايت من جداول الصفحات المستخدمة في Chrome ، افترضت أن الأرقام صحيحة. فيما يلي لقطة شاشة لملف vmmap مع تخطيط ذاكرة chrome.exe من المقالة السابقة ، ولكن مع سطر جدول الصفحة:

لكن شيئًا بدا خاطئًا. قررت إضافة محاكي جدول صفحات إلى أداة
VirtualScan الخاصة بي. يقوم بحساب عدد صفحات جداول الصفحات المطلوبة لكل الذاكرة المخصصة أثناء عملية المسح. تحتاج فقط إلى مسح الذاكرة المخصصة ، مضيفًا إلى عداد واحد كل رقم مضاعف 2 ميجابايت ، 1 جيجابايت أو 512 جيجابايت.
سرعان ما تم اكتشاف أن نتائج المحاكي تتوافق مع vmmap على العمليات العادية ، ولكن ليس على العمليات التي تحتوي على كمية كبيرة من ذاكرة CFG. يقابل الفرق تقريبًا ذاكرة CFG المخصصة. بالنسبة للعملية المذكورة أعلاه ، حيث يتحدث vmmap عن 402.8 ميجابايت (412480 كيلوبايت) من جداول الصفحات ، تعرض أداتي 67.7 ميجابايت.
وقت المسح ، الالتزام ، جداول الصفحات ، الكتل الملتزمة
المجموع: 41.763s ، 1457.7 MiB ، 67.7 MiB ، 32112 ، 98 كتل كود
CFG: 41.759 ثانية ، 353.3 ميجابايت ، 59.2 ميجابايت ، 24866
لقد تأكدت من خطأ vmmap عن طريق تشغيل
VAllocStress ، والذي يؤدي في الإعدادات الافتراضية إلى قيام Windows بتخصيص 2 غيغابايت من ذاكرة CFG. ادعى vmmap أنه خصص 2 غيغابايت من جداول الصفحات:

وعندما أكملت العملية من خلال إدارة المهام ، أظهر vmmap أن حجم الذاكرة المخصصة انخفض بمقدار 2 غيغابايت فقط. لذا ، فإن vmmap خاطئ ، وحساباتي مع جداول الصفحات صحيحة ، وبعد
مناقشة مثمرة
على Twitter ، أرسلت تقريرًا عن خطأ vmmap ، والذي يجب إصلاحه. لا تزال ذاكرة CFG تستهلك الكثير من إدخالات جدول الصفحة (59.2 ميغابايت في المثال أعلاه) ، ولكن ليس كما تقول vmmap ، وبعد إصلاحها لن تستهلك أي شيء على الإطلاق.
ما هو CFG و CFG؟
أريد أن أتراجع قليلاً وأن أقول بمزيد من التفصيل ما هو CFG.
CFG هي اختصار لـ Control Flow Guard. هذه طريقة للحماية من الاستغلال من خلال إعادة كتابة مؤشرات الدوال. مع تمكين CFG ، يتحقق المترجم ونظام التشغيل معًا من صحة هدف الفرع. أولاً ، يتم تحميل بايت التحكم CFG المقابل من منطقة CFG المحجوزة 2 تيرابايت. تدير عملية 64 بت في Windows مساحة عنوان 128 تيرابايت ، لذا يتيح لك تقسيم العنوان على 64 العثور على بايت CFG المقابل لهذا الكائن.
uint8_t cfg_byte = cfg_base[size_t(target_addr) / 64];
لدينا الآن بايت واحد يجب أن يصف أي عناوين في نطاق 64 بايت هي أهداف فرعية صالحة. للقيام بذلك ، يعامل CFG البايت على أنه أربع قيم ثنائية البايت ، كل منها يتوافق مع نطاق 16 بايت. يتم
تفسير هذا الرقم المكون من جزأين (الذي تتراوح قيمته من صفر إلى ثلاثة) على
النحو التالي :
- 0 - جميع الأهداف في هذه الكتلة ذات 16 بايت هي أهداف غير صالحة للفروع غير المباشرة
- 1 - عنوان البداية في كتلة 16 بايت هو الهدف الصحيح للفرع غير المباشر
- 2 - مرتبطة بمكالمات CFG "المحظورة" ؛ من المحتمل أن يكون العنوان غير صالح
- 3 - تعد العناوين غير المحاذاة في كتلة 16 بايت هذه أهدافًا صالحة لفرع غير مباشر ، ولكن من المحتمل أن يكون العنوان المحاذي 16 بايت غير صالح
إذا كان هدف الفرع غير المباشر غير صالح ، تنتهي العملية ويمنع الاستغلال. مرحى!

من هذا يمكننا أن نستنتج أنه لتحقيق أقصى قدر من الأمان ، يجب محاذاة الأهداف غير المباشرة للفرع بمقدار 16 بايت ، ويمكننا أن نفهم لماذا تكون ذاكرة CFG للعملية حوالي 1/64 من ذاكرة البرنامج.
في الواقع ، تقوم CFG بتحميل 32 بت في المرة الواحدة ، ولكن هذه تفاصيل التنفيذ. تصف العديد من المصادر ذاكرة CFG على أنها 8 بت أحادية البت بدلاً من 16 بايت مزدوجة البت. تفسيري أفضل.
لهذا السبب كل شيء سيء
توقف Gmail لسببين. أولاً ، يعد فحص ذاكرة CFG على نظام التشغيل Windows 10 16299 أو إصدارًا سابقًا بطيئًا بشكل
مؤلم . لقد رأيت كيف يستغرق مسح مساحة العنوان لعملية 40 ثانية أو أكثر ، وبشكل حرفي 99.99٪ من هذا الوقت يتم فحص ذاكرة CFG المحجوزة ، على الرغم من أنها لا تشكل سوى 75٪ من كتل الذاكرة الثابتة. لا أعرف لماذا كان المسح بطيئًا للغاية ، لكنهم قاموا بإصلاحه في Windows 10 17134 ، لذلك لا معنى لدراسة المشكلة بمزيد من التفاصيل.
تسبب المسح البطيء في حدوث تباطؤ لأن Gmail أراد تكرارا CFG ، وعقد WMI القفل طوال فترة الفحص. ولكن لم يتم قفل قفل الذاكرة طوال الفحص. في المثال الخاص بي ، هناك ما يقرب من 49000 كتلة في منطقة CFG ، وتم
استدعاء وظيفة
NtQueryVirtualMemory ، التي تتلقى القفل
وتحرره ، مرة واحدة لكل منها. لذلك ، تم الحصول على القفل وتحريره ~ 49000 مرة ، وتم الاحتفاظ به في كل مرة لمدة تقل عن 1 مللي ثانية.
ولكن على الرغم من تحرير القفل 49000 مرة ، إلا أن عملية Chrome لسبب ما لم تتمكن من الحصول عليه. هذا غير عادل!
هذا هو جوهر المشكلة. كما كتبت آخر مرة:
هذا لأن أقفال Windows غير عادلة بطبيعتها - وإذا قام الخيط بتحرير القفل ثم طلبه مرة أخرى على الفور ، فيمكنه الحصول عليه إلى الأبد.
القفل المقبول يعني أن خيطين متنافسين سيحصلان عليه بدوره. ولكن هذا يعني الكثير من مفاتيح السياق الباهظة الثمن ، لذلك لن يتم استخدام القفل لفترة طويلة.

أقفال غير عادلة أرخص ولا تجعل الخيوط تنتظر في الطابور. إنهم فقط يلتقطون القفل ، كما هو مذكور في
مقالة جو دافي . يكتب أيضًا:
إن إدخال أقفال غير عادلة يمكن أن يؤدي بلا شك إلى الجوع. ولكن إحصائيًا ، يميل الوقت في الأنظمة الموازية إلى أن يكون متغيرًا إلى حد أن كل خيط سوف يتلقى في النهاية دورًا للتنفيذ ، من وجهة نظر احتمالية.
كيف يمكن ربط بيان جو من عام 2006 حول ندرة الجوع بتجربتي حول مشكلة متكررة وطويلة الأمد بنسبة 100٪؟ أعتقد أن السبب الرئيسي هو ما حدث في عام 2006.
أصدرت Intel
Core Duo ، وأجهزة الكمبيوتر متعددة النواة موجودة في كل مكان.
بعد كل شيء ، اتضح أن مشكلة الجوع هذه تحدث فقط على نظام متعدد النواة! في مثل هذا النظام ، سيقوم مؤشر ترابط WMI بتحرير القفل ، والإشارة إلى مؤشر ترابط Chrome للتنبيه ، والمتابعة. نظرًا لأن دفق WMI قيد التشغيل بالفعل ، فإنه يحتوي على "إعاقة" أمام دفق Chrome ، لذلك يمكن بسهولة استدعاء
NtQueryVirtualMemory مرة أخرى والحصول على القفل مرة أخرى قبل أن تتاح لـ Chrome فرصة للقيام بذلك.
من الواضح أنه في نظام أحادي النواة ، يمكن أن يعمل خيط واحد فقط في كل مرة. كقاعدة ، يزيد Windows أولوية سلسلة المحادثات الجديدة ، وتعني زيادة الأولوية أنه عندما يتم تحرير القفل ، ستكون سلسلة محادثات Chrome الجديدة جاهزة وستتقدم فورًا
إلى سلسلة رسائل WMI. يمنح هذا مؤشر ترابط Chrome الكثير من الوقت للاستيقاظ والحصول على قفل ، والجوع لا يأتي أبدًا.
هل تفهم؟ في نظام متعدد النواة ، لا تؤثر زيادة الأولوية في معظم الحالات على دفق WMI ، لأنها ستعمل على نواة مختلفة!
هذا يعني أن النظام الذي يحتوي على نوى إضافية يمكن أن
يستجيب بشكل أبطأ من النظام الذي له نفس عبء العمل وعدد أقل من النوى. استنتاج آخر مثير للفضول: إذا كان جهاز الكمبيوتر الخاص بي يحتوي على حمولة ثقيلة - خيوط ذات أولوية مقابلة ، والعمل على جميع نوى المعالج - فيمكن تجنب حدوث تعليق (لا تحاول تكرار ذلك في المنزل).
وبالتالي ، تؤدي
الأقفال غير العادلة إلى زيادة الإنتاجية ، ولكنها يمكن أن تؤدي إلى الجوع. أشك في أن الحل قد يكون ما أسميه أقفال "عادلة في بعض الأحيان". لنفترض أن 99٪ من المرات ستكون غير عادلة ، ولكن بنسبة 1٪ تعطي قفلًا لعملية أخرى. هذا سيحافظ على فوائد الإنتاجية مع المزيد ، وتجنب مشكلة الجوع. في السابق ، كانت الأقفال في Windows توزع بشكل عادل ، وربما يمكنك العودة جزئيًا إلى ذلك ، للعثور على التوازن المثالي. إخلاء المسؤولية: لست خبيرًا في الأقفال أو مهندس نظام تشغيل ، لكني مهتم بسماع أفكار حوله ، وعلى الأقل لست
أول من يقدم شيئًا كهذا .
يقدر Linus Torvalds مؤخرًا أهمية الأقفال العادلة:
هنا وهنا . ربما حان الوقت للتغيير على Windows أيضًا.
لتلخيص : القفل لبضع ثوان ليس جيدًا ، فهو يحد من التزامن. ولكن في الأنظمة متعددة النواة ذات الأقفال غير العادلة ، فإن إزالة القفل ثم استلامه على الفور مرة أخرى يتصرف
تمامًا مثل ذلك - لا توجد خيوط أخرى للعمل.
تقريبا فشل مع ETW

في كل هذا البحث ، اعتمدت على تتبع ETW ، لذلك شعرت بالخوف قليلاً عندما اتضح في بداية التحقيق أن Windows Performance Analyzer (WPA) لا يمكنه تحميل أحرف Chrome. أنا متأكد من أن كل شيء نجح في الأسبوع الماضي. ماذا حدث ...
حدث أن تم طرح Chrome M68 ، وتم ربطه باستخدام lld-link بدلاً من رابط VC ++. إذا قمت بتشغيل
سلة المهملات ونظرت إلى معلومات التصحيح ، سترى:
C:\b\c\b\win64_clang\src\out\Release_x64\./initialexe/chrome.exe.pdb
حسنًا ، ربما لا تحب WPA هذه الخطوط المائلة. ولكن لا يزال هذا غير منطقي ، لأنني غيرت الرابط إلى lld-link ، وأتذكر أنني اختبرت WPA قبل ذلك ، فماذا حدث ...
اتضح أن السبب كان في الإصدار الجديد لـ WPA 17134. لقد اختبرت تنسيق lld-Link - وعملت بشكل جيد في WPA 16299. يا لها من مصادفة! الرابط الجديد و WPA الجديد غير متوافقين.
لقد قمت بتثبيت الإصدار القديم من WPA لمواصلة التحقيق (xcopy من جهاز بإصدار قديم) وأبلغت عن
خطأ ارتباط lld ، والذي قام المطورون بإصلاحه بسرعة. يمكنك الآن الرجوع إلى WPA 17134 عندما يتم تجميع M69 مع رابط ثابت.
Wmi
مشغل تجميد WMI هو أداة إضافية لـ
Windows Management Instrumentation ، وأنا لست جيدًا في ذلك. لقد وجدت أنه في عام
2014 أو قبل ذلك ، واجه شخص ما مشكلة استخدام وحدة المعالجة المركزية الهامة في
WmiPrvSE.exe داخل
perfproc! GetProcessVaData ، لكنهم لم يقدموا معلومات كافية لفهم أسباب الخطأ. في وقت ما ، أخطأت وحاولت معرفة ما قد يؤدي طلب WMI المجنون إلى تعليق Gmail لبضع ثوان. لقد ربطت
بعض الخبراء بالتحقيق وقضيت الكثير من الوقت في محاولة العثور على هذا الاستعلام السحري. لقد سجلت نشاط
Microsoft-Windows-WMI-Activity في آثار ETW ، وجربت مع PowerShell للعثور على جميع طلبات Win32_Perf ، وضاعت في بعض الطرق الدائرية التي تكون مملة للغاية ولا يمكن مناقشتها. في النهاية ، اكتشفت أن تعليق Gmail تسبب في هذا العداد
Win32_PerfRawData_PerfProc_ProcessAddressSpace_Costly ، والذي تم تشغيله بواسطة سطر واحد من PowerShell:
measure-command {Get-WmiObject -Query “SELECT * FROM Win32_PerfFormattedData_PerfProc_ProcessAddressSpace_Costly”}
ثم أصبحت مرتبكًا
أكثر بسبب اسم العداد ("عزيزي"؟ حقًا؟) ولأن هذا العداد يظهر ويختفي بناءً على عوامل لا أفهمها.
لكن تفاصيل WMI لا يهم. لم يفعل WMI شيئًا خاطئًا - ليس حقًا - فقد قام فقط بفحص الذاكرة. تبين أن كتابة كود المسح الضوئي الخاص بك هو أكثر فائدة في التحقيق في المشكلة.
مشاحنات لمايكروسوفت
أصدر Chrome تصحيحًا ، والباقي لشركة Microsoft.
تسريع مسح منطقة CFG - حسنًا ، لقد انتهى الأمر- قم بتحرير ذاكرة CFG عند تحرير الذاكرة القابلة للتنفيذ - على الأقل في حالة محاذاة 256K ، يكون الأمر سهلاً
- ضع في اعتبارك علامة تسمح بتخصيص ذاكرة قابلة للتنفيذ بدون ذاكرة CFG ، أو استخدم PAGE_TARGETS_INVALID لهذا الغرض. لاحظ أن دليل الإصدار السابع من الجزء الداخلي لـ Windows Internals ينص على أنه "يجب عليك تحديد صفحات [CFG] مع مجموعة بت واحدة على الأقل {1 ، X}" - إذا قام Windows 10 بتطبيق ذلك ، فإن علامة PAGE_TARGETS_INVALID (التي يستخدمها المحرك حاليًا) الإصدار 8 ) سيتجنب تخصيص الذاكرة
- إصلاح حساب جداول الصفحات في vmmap للعمليات التي تحتوي على عدد كبير من عمليات تخصيص CFG
تحديثات التعليمات البرمجية
لقد قمت بتحديث
أمثلة التعليمات البرمجية ، خاصة VAllocStress. هناك 20 خطًا مدرجًا لتوضيح كيفية العثور على حجز CFG للعملية. أضفت أيضًا رمز اختبار يستخدم
SetProcessValidCallTargets للتحقق من قيمة بتات CFG وإظهار الحيل اللازمة للاتصال بها بنجاح (تلميح: من المحتمل أن يؤدي الاتصال عبر GetProcAddress إلى انتهاك CFG!)