تركز هذه المقالة على السلوك غير المحدد وتحسينات المترجم ، خاصة في سياق تجاوز عدد صحيح موقّع.ملحوظة من المترجم: لا يوجد في روسيا مراسلات واضحة في السياق المستخدم لكلمة "التفاف" / "التفاف". هناك مصطلح رياضي "
نقل " ، وهو قريب من الظاهرة الموصوفة ، ومصطلح "حمل العلم" هو آلية لوضع علامة في المعالجات أثناء تجاوز العدد الصحيح. قد يكون خيار الترجمة الآخر عبارة "دوران / قلب / ثورة حول الصفر". يعكس بشكل أفضل معنى "التفاف" مقارنة بـ "حمل" ، لأنه يوضح انتقال الأرقام عند الفائض من النطاق الموجب إلى السلبي. ومع ذلك ، كما اتضح ، تبدو هذه الكلمات غير عادية في النص لقراء الاختبار. من أجل البساطة ، سنأخذ كلمة "نقل" في المستقبل ترجمة لمصطلح "التفاف".
يتم توجيه مُجمعي لغة C (و C ++) في عملهم بشكل متزايد بمفهوم
السلوك غير المحدود - فكرة أن سلوك برنامج لبعض العمليات لا ينظمه المعيار ، وأنه عند إنشاء رمز الكائن ، يحق للمترجم المضي قدمًا من افتراض أن البرنامج لا يؤدي مثل هذه العمليات. اعترض العديد من المبرمجين على هذا النهج ، لأن الشفرة التي تم إنشاؤها في هذه الحالة قد لا تتصرف كمؤلف البرنامج المقصود. أصبحت هذه المشكلة أكثر حدة ، حيث يستخدم المترجمون طرق تحسين أكثر تعقيدًا ، والتي ستعتمد على الأرجح على مفهوم السلوك غير المحدود.
في هذا السياق ، مثال مع تجاوز عدد صحيح موقّع هو دلالة. يكتب معظم مطوري C كودًا للأجهزة التي تستخدم
رمزًا إضافيًا لتمثيل الأعداد الصحيحة ، ويتم تنفيذ الجمع والطرح في هذا التمثيل بنفس الطريقة تمامًا ، في الحساب غير الموقع. إذا كان مجموع اثنين من الأعداد الصحيحة الموجبة مع تجاوزات الإشارة - أي أنه يصبح أكبر مما يستوعبه النوع - فسوف يعيد المعالج قيمة ، والتي يتم تفسيرها على أنها مكمل ثنائي للرقم الموقع ، سيتم اعتبارها سلبية. تسمى هذه الظاهرة "نقل" ، لأن النتيجة ، بعد أن وصلت إلى الحد الأعلى لنطاق القيم ، "يتم نقلها" وتبدأ من الحد السفلي.
لهذا السبب ، يمكنك أحيانًا رؤية هذا الرمز في C:
int b = a + 1000; if (b < a) {
تتمثل مهمة
if if في الكشف عن حالة تجاوز السعة (في هذه الحالة ، تحدث بعد إضافة 1000 إلى قيمة المتغير
a ) والإبلاغ عن خطأ. تكمن المشكلة في أنه في C ، يمثل التدفق الزائد عددًا صحيحًا إحدى حالات السلوك غير المحدد. لبعض الوقت ، اعتبر المحللون دائمًا أن هذه الشروط خاطئة: إذا أضفت 1000 (أو أي رقم موجب آخر) إلى رقم آخر ، فلا يمكن أن تكون النتيجة أقل من القيمة الأولية. في حالة حدوث تجاوز ، فهناك سلوك غير محدد ، وعدم السماح بذلك هو بالفعل (على ما يبدو) قلق المبرمج. لذلك ، يمكن للمترجم أن يقرر أن العامل الشرطي يمكن إزالته تمامًا لأغراض التحسين (بعد كل شيء ، الحالة دائمًا خاطئة ، ولا تؤثر على أي شيء ، لذلك يمكنك الاستغناء عنها).
المشكلة هي أنه مع هذا التحسين ، قام المترجم بإزالة التحقق الذي أضافه المبرمج على وجه التحديد لاكتشاف السلوك غير المحدد ومعالجته. هنا يمكنك أن ترى كيف يحدث هذا في الممارسة. (ملاحظة: موقع godbolt.org ، الذي يستضيف المثال ، رائع جدًا! يمكنك تعديل الكود والاطلاع على الفور على كيفية معالجة المترجمين المختلفين ، وهناك الكثير منهم. التجربة!). يرجى ملاحظة أن المترجم لا يزيل التحقق من التدفق الزائد إذا قمت بتغيير النوع إلى غير موقّع ، حيث يتم تعريف سلوك الفائض غير الموقّع في C (بشكل أكثر دقة ، يتم نقل النتيجة باستخدام حساب غير موقّع ، لذلك لا يحدث تجاوز فعلي).
فهل هذا خطأ؟ يقول شخص ما نعم ، على الرغم من أنه من الواضح أن العديد من مطوري المترجمين يعتبرون هذا القرار قانونيًا. إذا فهمت بشكل صحيح ، فإن الحجج الرئيسية للداعمين (تحرير: يعتمد على التنفيذ) للنقل أثناء تجاوز السعة هي كما يلي:
- الفائض هو سلوك مفيد.
- الهجرة هي السلوك الذي يتوقعه المبرمجون.
- لا تقدم دلالات سلوك تجاوز السعة غير المحدود ميزة ملحوظة.
- يسمح معيار اللغة C للسلوك غير المحدد للتنفيذ "بتجاهل الموقف تمامًا ، وستكون النتيجة غير متوقعة" ، ولكن هذا لا يمنح المترجم الحق في تحسين الشفرة بناءً على افتراض أن الموقف مع السلوك غير المحدد لا يحدث على الإطلاق.
لنحلل كل عنصر على حدة:
الهجرة الزائدة - سلوك مفيد؟يعتبر الترحيل مفيدًا بشكل أساسي عندما تحتاج إلى تتبع تجاوز حدث بالفعل. (إذا كانت هناك مشكلات أخرى يمكن حلها عن طريق النقل ولا يمكن حلها باستخدام متغيرات عدد صحيح غير موقعة ، فلا يمكنني تذكر هذه الأمثلة على الفور ، وأظن أن هناك القليل منها). في حين أن النقل يبسط مشكلة استخدام المتغيرات الفائضة بشكل غير صحيح ، فإنه بالتأكيد ليس حلاً سحريًا (تذكر ضرب أو إضافة كميتين غير معروفتين بعلامة غير معروفة).
في الحالات التافهة ، عندما يسمح لك النقل ببساطة بتتبع الفائض الذي نشأ ، ليس من الصعب أيضًا معرفة ما إذا كان سيحدث على الإطلاق أم لا. يمكن إعادة كتابة مثالنا على النحو التالي:
if (a > INT_MAX - 1000) {
بمعنى أنه بدلاً من حساب المجموع ثم معرفة ما إذا كان تجاوز السعة قد حدث أم لا ، والتحقق من نتيجة التناسق الرياضي ، يمكنك التحقق مما إذا كان المجموع يتجاوز الحد الأقصى الذي يناسبه النوع. (إذا كانت علامة كلا المعاملين غير معروفة ، فيجب أن يكون التحقق معقدًا إلى حد كبير ، ولكن الشيء نفسه ينطبق على التحقق أثناء النقل).
بالنظر إلى كل هذا ، أجد أن الحجة غير مقنعة بأن التحويل مفيد في معظم الحالات.
هل الهجرة تتوقع سلوك المبرمجين؟من الصعب مناقشة هذه الحجة ، لأنه من الواضح أن كود
بعض المبرمجين C على الأقل يفترض دلالات النقل مع تجاوز عدد صحيح موقّع. لكن هذه الحقيقة وحدها ليست كافية للنظر في مثل هذه الدلالات الأفضل (لاحظ أن بعض المترجمين يسمحون لك بتمكينها إذا لزم الأمر).
الحل الواضح للمشكلة (يتوقع المبرمجون هذا السلوك) هو جعل المترجم يعطي تحذيرًا عندما يحسن الشفرة ، بافتراض عدم وجود سلوك غير محدد. لسوء الحظ ، كما رأينا في المثال على godbolt.org باستخدام الرابط أعلاه ، لا يقوم المترجمون دائمًا بذلك (Gcc الإصدار 7.3 - نعم ، ولكن الإصدار 8.1 - لا ، لذلك هناك خطوة إلى الوراء).
هل لا تعطي دلالات سلوك تجاوز السعة غير المحدد أي ميزة ملحوظة؟إذا كانت هذه الملاحظة صحيحة في جميع الحالات ، فستكون بمثابة حجة قوية لصالح حقيقة أن المترجمين يجب أن يلتزموا بدلالات النقل بشكل افتراضي ، لأنه من الأفضل السماح بفحوصات تجاوز التدفق ، حتى إذا كانت هذه الآلية غير صحيحة من وجهة نظر فنية - على الرغم من قد يكون بسبب إمكانية استخدامه في كود من المحتمل أن يكون مكسورًا.
أفترض أن هذا التحسين (إزالة الشيكات للظروف المتناقضة رياضياً) في برامج C العادية يمكن إهماله في الغالب ، حيث يسعى مؤلفوهم للحصول على أفضل أداء مع الاستمرار في تحسين الشفرة يدويًا: أي إذا كان من الواضح أن هذا
البيان يحتوي على شرط ، والذي لن يكون صحيحًا أبدًا ، من المرجح أن يقوم المبرمج بإزالته بنفسه. في الواقع ، اكتشفت أنه في العديد من الدراسات تم التشكيك في فعالية هذا التحسين ، وتم اختباره ووجدت أنه غير ذي أهمية عمليًا في إطار اختبارات التحكم. ومع ذلك ، على الرغم من أن هذا التحسين لا يعطي أبدًا ميزة في لغة C ، إلا أن مولدات التعليمات البرمجية وتحسينات المترجم هي في معظمها عالمية ويمكن استخدامها في لغات أخرى - وقد يكون هذا الاستنتاج غير صحيح بالنسبة لهم. لنأخذ لغة C ++ بتقاليدها ، اعتمادًا على الاعتماد على المُحسِّن لإزالة الإنشاءات الزائدة في رمز القالب ، بدلاً من القيام بها يدويًا. ولكن هناك لغات يتم تحويلها من قبل الناقل إلى C ، ويتم أيضًا تحسين الشفرة الزائدة فيها بواسطة مترجمين C.
بالإضافة إلى ذلك ، حتى إذا واصلت التحقق من التدفق الزائد ، فليس من الحقيقة على الإطلاق أن التكلفة
المباشرة لنقل المتغيرات الصحيحة ستكون ضئيلة حتى على الأجهزة التي تستخدم رمزًا إضافيًا. على سبيل المثال ، لا يمكن لبنية Mips إجراء عمليات حسابية إلا في سجلات ذات حجم ثابت (32 بت). نوع
int قصير ، كقاعدة عامة ، حجمه 16 بت ، و
char - 8 بت ؛ عندما يتم تخزين متغير من هذه الأنواع في السجل ، فسيتم توسيع حجمه ، ومن أجل نقله بشكل صحيح ، سيكون من الضروري إجراء عملية إضافية واحدة على الأقل ، وربما استخدام سجل إضافي (لاستيعاب قناع البت المقابل). يجب أن أعترف أنني لم أتعامل مع رمز Mips لفترة طويلة ، لذلك لست متأكدًا من التكلفة الدقيقة لهذه العمليات ، لكنني متأكد من أنها ليست صفراً وأن المشاكل نفسها يمكن أن تحدث في بنيات RISC الأخرى.
هل يمنع معيار اللغة تجنب الالتفاف المتغير إذا كان المقصود به الهندسة المعمارية؟إذا نظرت ، هذه الحجة ضعيفة بشكل خاص. جوهرها هو أن المعيار يفترض أنه يسمح للتنفيذ (المترجم) بتفسير "السلوك غير المحدود" فقط إلى حد محدود. في نص المعيار نفسه - في هذا الجزء الذي ينادي به دعاة التحويل - يقال ما يلي (هذا جزء من تعريف مصطلح "السلوك غير المحدود"):
ملاحظة:
قد يتخذ السلوك غير المحدد شكل تجاهل الموقف تمامًا ، بينما ستكون النتيجة غير متوقعة ، ...الفكرة هي أن الكلمات "تجاهل الوضع تمامًا" لا تشير إلى أن حدثًا يؤدي إلى سلوك غير محدد - على سبيل المثال ، تجاوز السعة أثناء الإضافة - لا يمكن أن يحدث ، ولكن إذا حدث ذلك ، فيجب على المترجم الاستمرار في العمل كما لو مما لم يحدث من قبل ، ولكن ضع في اعتبارك أيضًا النتيجة التي ستظهر إذا أرسل المعالج طلبًا لإجراء مثل هذه العملية (بمعنى آخر ، كما لو تمت ترجمة شفرة المصدر إلى رمز الجهاز بطريقة مباشرة وساذجة).
بادئ ذي بدء ، تجدر الإشارة إلى أن هذا النص مقدم على أنه "ملاحظة" ، وبالتالي فهو ليس معياريًا (أي أنه لا يمكن أن يصف شيئًا) ، وفقًا لتوجيه ISO المذكور في مقدمة المعيار:
وفقًا للجزء 3 من توجيهات ISO / IEC ، فإن هذه المقدمة ومقدمة للنص والملاحظات والحواشي والأمثلة هي أيضًا لأغراض إعلامية فقط.بما أن هذا المقطع "سلوك غير محدد" هو ملاحظة ، فإنه لا يصف أي شيء. يرجى ملاحظة أن التعريف الحالي لـ "السلوك غير المحدود" هو:
السلوك الناشئ عن استخدام تصميم برنامج غير محتمل أو غير صحيح أو بيانات غير صحيحة ، والتي لا تفرض هذه المواصفة القياسية الدولية أي متطلبات .أبرزت الفكرة الرئيسية: لا تفرض أي متطلبات على السلوك غير المحدد. تحتوي قائمة "الأنواع المحتملة للسلوك غير المحدد" في الملاحظة على أمثلة فقط ولا يمكن أن تكون الوصفة النهائية. لا يمكن تفسير عبارة "لا يطالب" بغير ذلك.
البعض ، الذين طوروا هذه الحجة ، يجادلون أنه ، بغض النظر عن النص ، فإن لجنة اللغة ، عندما صاغت هذه الكلمات ،
تعني أن السلوك ككل يجب أن يتوافق مع بنية الأجهزة التي يتم تشغيل البرنامج عليها ، قدر الإمكان ، مما يعني ترجمة ساذجة إلى رمز الجهاز. قد يكون هذا صحيحًا ، على الرغم من أنني لم أر أي دليل (على سبيل المثال ، الوثائق التاريخية) لدعم هذه الحجة. ومع ذلك ، حتى لو كان الأمر كذلك ، فليس من الصحيح أن هذا البيان ينطبق على النسخة الحالية من النص.
الأفكار الأخيرةالحجج المؤيدة للنقل لا يمكن الدفاع عنها إلى حد كبير. ربما يتم الحصول على أقوى حجة إذا جمعناها: المبرمجون الأقل خبرة (الذين لا يعرفون تعقيدات لغة C والسلوك غير المحدود فيها) يتوقعون في بعض الأحيان النقل ، ولا يقلل من الأداء - على الرغم من أن الأخير غير صحيح في جميع الحالات ، والجزء الأول غير حاسم إذا كنت تفكر في ذلك بشكل منفصل.
أنا شخصياً أفضل أن يتم حظر الفائض (المحاصرة) بدلاً من الالتفاف. أي أن البرنامج يتعطل ، ولا يستمر في العمل - مع سلوك غير مؤكد أو نتائج غير صحيحة محتملة ، لأنه في كلتا الحالتين تظهر ثغرة. مثل هذا الحل ، بالطبع ، سيقلل قليلاً من الأداء في معظم البنى (؟) ، خاصة على x86 ، ولكن من ناحية أخرى ، سيتم تحديد أخطاء تجاوز السعة على الفور ولن يتمكنوا من الاستفادة أو الحصول على نتائج غير صحيحة باستخدامها على طول الطريق برامج. بالإضافة إلى ذلك ، من الناحية النظرية ، يمكن للمجمعين الذين يستخدمون هذا النهج أن يزيلوا بأمان عمليات فحص الفائض الزائدة ، لأنه لن يحدث
بالتأكيد ، على الرغم من أنه ، كما أرى ، لا تستخدم Clang أو GCC هذه الفرصة.
لحسن الحظ ، يتم تنفيذ كل من الانقطاع والتنقل في المترجم الذي أستخدمه غالبًا هو GCC. للتبديل بين الأوضاع ، يتم استخدام وسيطات سطر الأوامر
-ftrapv و
-fwrapv ، على التوالي.
بالطبع ، هناك العديد من الإجراءات التي تؤدي إلى سلوك غير محدد - تجاوز العدد الصحيح هو واحد فقط منهم. لا أعتقد على الإطلاق أنه من المفيد تفسير كل هذه الحالات على أنها سلوك غير محدد ، وأنا متأكد من أن هناك العديد من المواقف المحددة حيث يجب تحديد الدلالات من خلال اللغة أو ، على الأقل ، ترك لتقدير عمليات التنفيذ. وأخشى من التفسيرات الحرة المفرطة لهذا المفهوم من قبل الشركات المصنعة للمترجم: إذا كان سلوك المترجم لا يلبي الأفكار الحدسية للمطورين ، وخاصة أولئك الذين يقرؤون نص المعيار بشكل شخصي ، فقد يؤدي ذلك إلى أخطاء حقيقية إذا كان مكسب الأداء في هذه الحالة لا يكاد يذكر ، فمن الأفضل التخلي عن مثل هذه التفسيرات. في إحدى المشاركات التالية ، ربما سألقي نظرة على بعض هذه المشاكل.
ملحق (بتاريخ 24 أغسطس 2018)
أدركت أن الكثير مما سبق يمكن كتابته بشكل أفضل. أدناه ألخص كلماتي وأشرحها بإيجاز وأضيف بعض الملاحظات الطفيفة:
- لم أجادل في أن السلوك غير المحدود هو الأفضل لتحمل تجاوز - بدلاً من ذلك ، في الواقع ، النقل ليس أفضل بكثير من السلوك غير المحدود. على وجه الخصوص ، يمكن الحصول على مشاكل أمنية في الحالة الأولى ، وفي الحالة الثانية - وأراهن على أن العديد من نقاط الضعف التي تسببها التدفقات التي لم يتم اكتشافها في الوقت المناسب (باستثناء تلك التي يكون المترجم مسؤولًا عن حذف الشيكات الخاطئة) جاءت بالفعل من - بسبب نقل النتيجة ، ولكن ليس بسبب سلوك غير محدد مرتبط بتدفق.
- الميزة الحقيقية الوحيدة للنقل هي أنه لا يتم حذف عمليات فحص الفائض. على الرغم من أنه بهذه الطريقة يمكنك حماية الرمز من بعض سيناريوهات الهجوم ، إلا أنه من المحتمل ألا يتم فحص بعض التدفقات الزائدة على الإطلاق (على سبيل المثال ، سينسي المبرمج إضافة مثل هذا الاختيار) ولن يلاحظه أحد.
- إذا لم تكن المشكلة الأمنية في غاية الأهمية ، وكانت السرعة العالية للبرنامج في المقدمة ، فإن السلوك غير المحدد سيعطي تحسينًا أكثر ربحية وزيادة أكبر في الإنتاجية ، على الأقل في بعض الحالات. من ناحية أخرى ، إذا جاء الأمن أولاً ، فإن التحميل محفوف بالثغرات.
- هذا يعني أنه إذا اخترت بين الانقطاع ، والسلوك غير المحدد ، فهناك عدد قليل جدًا من المهام التي يمكن أن يكون التحويل مفيدًا فيها.
- أما فيما يتعلق بالتحقق من التدفق الزائد ، فأعتقد أن تركها ضار ، لأنه يخلق انطباعًا خاطئًا بأنهم يعملون وسيعملون دائمًا. مقاطعة الفائض يتجنب هذه المشكلة ؛ تحذيرات كافية - تخفيفه.
- أعتقد أنه يجب على أي مطور يكتب رمزًا مهمًا للأمان أن يكون لديه معرفة جيدة بدلالات اللغة التي يكتب بها ، وكذلك أن يكون على دراية بمزالقها. بالنسبة لـ C ، هذا يعني أنك بحاجة إلى معرفة دلالات التدفق الزائد والدقة الدقيقة للسلوك غير المحدد. من المحزن أن بعض المبرمجين لم ينموا إلى هذا المستوى.
- لقد صادفت الادعاء بأن "معظم المبرمجين C يتوقعون الهجرة كسلوك افتراضي" ، لكنني لا أعرف الدليل على ذلك. (في المقال ، كتبت "بعض المبرمجين" ، لأنني أعرف العديد من الأمثلة من الحياة الواقعية ، وبشكل عام أشك في أن أي شخص سيجادل في هذا).
- هناك مشكلتان مختلفتان: ما يتطلبه معيار لغة C وما يجب على المجمعين تنفيذه. أنا (بشكل عام) أحب الطريقة التي يحدد بها المعيار سلوك تجاوز السعة غير محدد. في هذا المنشور ، أتحدث عن ما يجب أن يفعله المترجمون.
- عندما تتم مقاطعة الفائض ، ليست هناك حاجة للتحقق من كل عملية لذلك. من الناحية المثالية ، فإن البرنامج الذي يتبع هذا النهج إما يتصرف بشكل ثابت من حيث القواعد الرياضية ، أو يتوقف عن العمل. في هذه الحالة ، يصبح من الممكن وجود "تجاوز مؤقت" ، الأمر الذي لا يؤدي إلى ظهور نتيجة غير صحيحة. ثم يمكن تحسين كلٍّ من التعبير a + b - b والتعبير (a * b) / b إلى a (يكون الأول ممكنًا أيضًا أثناء النقل ، ولكن الأخير لم يعد موجودًا).
ملاحظة تُنشر ترجمة المقال على المدونة بإذن من المؤلف. النص الأصلي: Davin McCall "
التفاف على تجاوز عدد صحيح ليس فكرة جيدة ".
روابط إضافية ذات صلة من فريق PVS-Studio:
- أندري كاربوف. السلوك غير المحدد أقرب مما تعتقد .
- ويل ديتز ، وبنغ لي ، وجون ريجير ، وفيكرام أدفي. فهم تجاوز عدد صحيح في C / C ++ .
- V1026. المتغير يزداد في الحلقة. سيحدث سلوك غير محدد في حالة تجاوز عدد صحيح موقعة .
- Stackoverflow هل لا يزال تجاوز العدد الصحيح الموقّع سلوكًا غير محدد في C ++؟