sizes - logical operators in c++ with example




هل يسمح المعيار C++ لـ bool غير مهيأ بتعطل أحد البرامج؟ (4)

نعم ، تسمح ISO C ++ (ولكن لا تتطلب) للتطبيقات بإجراء هذا الاختيار.

لكن لاحظ أيضًا أن ISO C ++ يسمح لبرنامج التحويل البرمجي بإرسال كود يتعطل عن قصد (على سبيل المثال مع تعليمات غير قانونية) إذا واجه البرنامج UB ، على سبيل المثال كوسيلة لمساعدتك في العثور على الأخطاء. (أو لأنه DeathStation 9000. الالتزام الصارم لا يكفي لتطبيق C ++ ليكون مفيدًا لأي غرض حقيقي). لذا فإن ISO C ++ سوف تسمح للمترجم بإجراء ASM الذي تعطل (لأسباب مختلفة تمامًا) حتى على كود مشابه يقرأ uint32_t غير مهيأ. على الرغم من أنه مطلوب أن يكون نوع تخطيط ثابت مع عدم وجود تمثيلات اعتراض.

إنه سؤال مثير للاهتمام حول كيفية عمل التطبيقات الحقيقية ، ولكن تذكر أنه حتى لو كانت الإجابة مختلفة ، فإن الرمز الخاص بك سيظل غير آمن لأن لغة C ++ الحديثة ليست نسخة محمولة من لغة التجميع.

أنت تقوم بالتجميع لـ x86-64 System V ABI ، الذي يحدد أن bool كدالة في السجل يتم تمثيلها بواسطة أنماط البت bit false=0 و true=1 في 8 بتات منخفضة من السجل 1 . في الذاكرة ، يعد bool نوعًا واحدًا من البايتات يجب أن يكون له مرة أخرى قيمة عددية 0 أو 1.

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

لا يحددها ISO C ++ ، لكن قرار ABI هذا واسع الانتشار لأنه يجعل التحويل المنطقي رخيصًا (فقط صفر تمدد) . لست على علم بأي من ABIs التي لا تدع المترجم يفترض 0 أو 1 بالنسبة إلى bool ، لأي هندسة (ليس فقط x 86). إنها تتيح تحسينات مثل !mybool مع xor eax,1 لقلب الشيء المنخفض: أي رمز ممكن يمكنه قلب bit / integer / bool بين 0 و 1 في تعليمة CPU واحدة . أو تجميع a&&b إلى bitwise AND لأنواع bool . في الواقع ، تستفيد بعض المجمعين من القيم المنطقية مثل 8 بت في المجمعين. هل العمليات عليها غير فعالة؟ .

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

يُسمح للمترجم بالتأكيد بالاستفادة الكاملة من ضمان ABI في الكود العام الخاص به ، وجعل الكود مثلك الذي يعمل على تحسين strlen(whichString) ل
5U - boolValue . (راجع للشغل ، يعتبر هذا التحسين نوعًا من الذكاء ، ولكن ربما يكون قصر النظر مقابل المتفرعة وتضمين memcpy كمخازن للبيانات الفورية 2. )

أو ربما قام المترجم بإنشاء جدول من المؤشرات وفهرسته بقيمة عدد صحيح من bool ، مع افتراض مرة أخرى أنه كان 0 أو 1. ( هذا الاحتمال هو ما اقترحه إجابة Barmar @ .)

__attribute((noinline)) الخاص بك مع تمكين التحسين إلى رنين فقط تحميل بايت من المكدس لاستخدامه كـ uninitializedBool . لقد أتاحت مساحة للكائن بشكل main مع push rax (وهو أصغر push rax مختلفة حول الكفاءة مثل sub rsp, 8 ) ، لذلك مهما كانت القمامة في AL عند الدخول إلى main هي القيمة التي استخدمتها في uninitializedBool . هذا هو السبب في أنك حصلت بالفعل على قيم لم تكن مجرد 0 .

5U - random garbage يمكن 5U - random garbage أن تلتف بسهولة إلى قيمة كبيرة غير موقعة ، مما يؤدي إلى دخول memcpy إلى ذاكرة غير معيّنة. تكون الوجهة في التخزين الثابت ، وليس المكدس ، لذلك لا تقوم بالكتابة على عنوان المرسل أو شيء ما.

يمكن للتطبيقات الأخرى أن تتخذ خيارات مختلفة ، مثل false=0 و true=any non-zero value . ثم لن تؤدي clang على الأرجح إلى تعطل التعليمات البرمجية لمثيل UB المحدد هذا . (لكن سيظل مسموحًا به إذا أراد ذلك.) لا أعرف أي تطبيقات تختار أي شيء آخر ما يفعله x86-64 بالنسبة إلى bool ، لكن معيار C ++ يسمح بالعديد من الأشياء التي لا يفعلها أحد أو حتى يريد القيام بها على الأجهزة هذا شيء مثل وحدات المعالجة المركزية الحالية.

ISO C ++ يتركه غير محدد ما ستجده عند فحص أو تعديل تمثيل كائن bool . (على سبيل المثال ، من خلال memcpy bool إلى unsigned char ، وهو ما يُسمح لك بالقيام به لأن char* يمكن أن يكون اسمًا مستعارًا لأي شيء. ويضمن unsigned char عدم وجود وحدات بت مبطنة ، لذلك يتيح لك معيار C ++ رسميًا تمثيلات كائن hexdump دون أي UB يختلف اختلاف رسم المؤشر لنسخ تمثيل الكائن عن تعيين char foo = my_bool ، بطبيعة الحال ، لذلك لن يحدث booleanization إلى 0 أو 1 وستحصل على تمثيل أولي للكائن.)

لقد قمت "بإخفاء" UB جزئيًا على مسار التنفيذ هذا من المحول البرمجي باستخدام noinline . حتى إذا لم تكن مضمنة ، فإن تحسينات interprocedural لا يزال بإمكانها إنشاء نسخة من الوظيفة تعتمد على تعريف وظيفة أخرى. (أولاً ، تقوم clang بعمل ملف قابل للتنفيذ ، وليس مكتبة مشتركة لـ Unix يمكن أن يحدث فيها تداخل الرموز. وثانيًا ، التعريف الموجود داخل تعريف class{} بحيث يجب أن يكون لكل وحدات الترجمة نفس التعريف. كما هو الحال مع الكلمة الأساسية inline .)

لذلك يمكن للمترجم أن ينبعث من مجرد ret أو ud2 (تعليمة غير قانونية) كتعريف لـ main ، لأن مسار التنفيذ يبدأ في الجزء العلوي من main يصادف سلوكًا غير محدد. (والذي يمكن للمترجم رؤيته في وقت التحويل البرمجي إذا قرر اتباع المسار من خلال مُنشئ غير مضمن).

أي برنامج يواجه UB غير معرف تمامًا لوجوده الكامل. لكن UB داخل دالة أو if() فرع لا يعمل فعليًا لا يفسد باقي البرنامج. يعني هذا في الممارسة العملية أن المجمعين يمكنهم أن يقرروا إصدار تعليمات غير قانونية ، أو ret أو عدم إرسال أي شيء والوقوع في الكتلة / الوظيفة التالية ، بالنسبة للكتلة الأساسية بأكملها التي يمكن إثباتها في وقت الترجمة لاحتواء أو تؤدي إلى UB.

دول مجلس التعاون الخليجي و Clang في الممارسة العملية في بعض الأحيان تنبعث منها ud2 على UB ، بدلا من محاولة إنشاء رمز لمسارات التنفيذ التي لا معنى لها. أو بالنسبة لحالات مثل السقوط في نهاية وظيفة غير void ، ستحذف gcc أحيانًا تعليمة ret . إذا كنت تفكر في أن "وظيفتي ستعود فقط مع كل ما هو موجود في RAX" ، فأنت مخطئ للغاية. لا يعامل مترجمو C ++ الحديثة اللغة مثل لغة التجميع المحمولة بعد الآن. يجب أن يكون البرنامج صالحًا لـ C ++ ، دون وضع افتراضات حول كيفية ظهور نسخة غير مدمجة مستقلة من وظيفتك في صورة asm.

مثال ممتع آخر هو لماذا الوصول غير المحاذاة إلى الذاكرة mmap'ed في بعض الأحيان segfault على AMD64؟ . إلى x86 لا خطأ على أعداد صحيحة غير محاذاة ، أليس كذلك؟ فلماذا يكون uint16_t* مشكلة؟ لأن alignof(uint16_t) == 2 ، وانتهاك هذا الافتراض أدى إلى segfault عند التعامل التلقائي مع SSE2.

راجع أيضًا ما يجب أن يعرفه كل مبرمج C حول السلوك غير المحدد # 1/3 ، مقالة من قِبل مطوِّر clang.

النقطة الأساسية: إذا لاحظ المترجم UB في وقت التحويل البرمجي ، فقد "يكسر" (ينبعث من الدهشة asm) المسار من خلال الكود الذي يسبب UB حتى إذا كان يستهدف ABI حيث يكون أي نمط بت يمثل تمثيلًا صالحًا للكائن منطقيًا.

توقع العداء التام تجاه العديد من الأخطاء من قبل المبرمج ، وخاصة الأشياء التي يحذر المترجمون الحديثون منها. هذا هو السبب في أنك يجب أن تستخدم -Wall ، وإصلاح التحذيرات. لا تعد لغة C ++ لغة سهلة الاستخدام ، ويمكن أن يكون شيء ما في لغة C ++ غير آمن حتى لو كان آمنًا في اسمك على الهدف الذي تقوم بتجميعه. (على سبيل المثال ، تجاوز السعة الموقَّع هو UB في C ++ ، وسوف يفترض clang/gcc -fwrapv أن ذلك لن يحدث ، حتى عند التحويل إلى x86 الخاص بـ 2 ، ما لم تستخدم clang/gcc -fwrapv .)

يعد تجميع UB المرئي لوقت خطيرًا دائمًا ، ومن الصعب حقًا أن تتأكد (مع تحسين وقت الارتباط) من أنك قد أخفت UB بالفعل عن المترجم ، وبالتالي يمكن أن السبب في أي نوع من asm سيولد.

لا تكون أكثر من اللازم. غالبًا ما يسمح لك المجمعون بالإفلات من بعض الأشياء وينبعثوا من التعليمات البرمجية كما تتوقعون حتى عندما يكون هناك شيء ما هو UB. ولكن ربما ستكون هناك مشكلة في المستقبل إذا نفذت برامج التحويل البرمجي للمترجم بعض التحسينات التي تكتسب مزيدًا من المعلومات حول نطاقات القيمة (على سبيل المثال ، أن المتغير غير سلبي ، مما قد يسمح له بتحسين امتداد الإشارة لتحرير الامتداد صفر على x86- 64). على سبيل المثال ، في gcc و tmp = a+INT_MIN الحاليين ، فإن القيام tmp = a+INT_MIN لا يعمل على تحسين a<0 كما هو tmp = a+INT_MIN دائمًا ، لكن tmp يكون دائمًا سلبيًا. (نظرًا لأن INT_MIN + a=INT_MAX سلبي على الهدف التكميلي لهذا 2 ، ولا يمكن أن يكون أعلى من ذلك.)

لذلك ، لا تتراجع gcc / clang حاليًا لاشتقاق معلومات النطاق لمدخلات عملية حسابية ، فقط على النتائج بناءً على افتراض عدم تجاوز السعة الموقعة: مثال على Godbolt . لا أعرف ما إذا كان هذا التحسين "غائب" عمداً باسم سهولة المستخدم أم ماذا.

لاحظ أيضًا أنه يُسمح للتطبيقات (تُعرف أيضًا باسم compilers) بتعريف السلوك الذي يتركه ISO C ++ غير معروف . على سبيل المثال ، يجب أن تسمح جميع برامج التحويل البرمجي التي تدعم intrinsics من Intel (مثل _mm_add_ps(__m128, __m128) SIMD اليدوي) بتكوين مؤشرات محاذاة بشكل خاطئ ، وهي UB في C ++ حتى إذا لم تقم _mm_add_ps(__m128, __m128) . __m128i _mm_loadu_si128(const __m128i *) بالأحمال غير المحاذاة من خلال أخذ وسيطة __m128i* ، وليس void* أو char* . هل `reinterpret_cast`ing بين مؤشر متجه الأجهزة والنوع المقابل هو سلوك غير محدد؟

يعرّف GNU C / C ++ أيضًا سلوك التحول السلبي لرقم موقَّع سالب (حتى بدون -fwrapv ) ، بشكل منفصل عن قواعد UB العادية ذات السعة الموقعة. ( هذا هو UB في ISO C ++ ، في حين أن التحولات الصحيحة من الأرقام الموقعة محددة بالتنفيذ (منطقي مقابل الحساب) ؛ وتختار التطبيقات الجيدة النوعية الحساب على HW الذي يحتوي على نوبات حسابية صحيحة ، ولكن لا تحدد ISO C ++). تم توثيق ذلك في قسم Integer بدليل مجلس التعاون الخليجي ، إلى جانب تحديد السلوك المحدد بالتنفيذ والذي تتطلب معايير C من التطبيقات أن تحدد بطريقة أو بأخرى.

هناك بالتأكيد مشكلات في جودة التنفيذ يهتم بها مطورو برنامج التحويل البرمجي ؛ إنهم لا يحاولون بشكل عام جعل المترجمين العدائيين عن عمد ، ولكن الاستفادة من جميع الحفر UB في C ++ (باستثناء تلك التي يختارون تحديدها) لتحسين أفضل يمكن تمييزها تقريبًا في بعض الأحيان.

الحاشية 1 : يمكن أن يكون الجزء العلوي البالغ 56 بت من القمامة التي يجب على المستدعي تجاهلها ، كالعادة بالنسبة للأنواع الأضيق من السجل.

( تقوم ABIs الأخرى بعمل اختيارات مختلفة هنا . بعضها يتطلب أن تكون أنواع الأعداد الصحيحة الضيقة صفرية أو ممددة لملء السجل عند تمريره أو إرجاعه من الوظائف ، مثل MIPS64 و PowerPC64. راجع القسم الأخير من هذه الإجابة x86-64 الذي يقارن مقابل تلك المعايير الدولية السابقة .)

على سبيل المثال ، ربما قام المتصل بحساب a & 0x01010101 في RDI واستخدمها لشيء آخر ، قبل الاتصال bool_func(a&1) . يمكن للمتصل تحسين &1 لأنه فعل ذلك بالفعل إلى البايت المنخفض كجزء من and edi, 0x01010101 ، ويعرف أن هناك حاجة لتجاهل البايتات العالية.

أو إذا تم تمرير منطقي كـ الوسيطة الثالثة ، فربما يقوم المتصل الذي يقوم بتحسين حجم movzx edx, [mem] باستخدام mov dl, [mem] بدلاً من movzx edx, [mem] ، مما يوفر 1 بايت بتكلفة الاعتماد الخاطئ على القديم قيمة RDX (أو أي تأثير جزئي للتسجيل ، اعتمادًا على طراز وحدة المعالجة المركزية). أو بالنسبة للوسيطة الأولى ، mov dil, byte [r10] بدلاً من movzx edi, byte [r10] ، لأن كلاهما يتطلب بادئة REX على أي حال.

هذا هو السبب في أن movzx eax, dil تنبعث من movzx eax, dil في Serialize ، بدلاً من sub eax, edi . (بالنسبة إلى عدد صحيح من الأعداد الصحيحة ، تنتهك clang قاعدة ABI هذه ، بدلاً من ذلك اعتمادًا على السلوك غير الموثق لـ gcc و clang على الأعداد الصحيحة الضيقة الصفرية أو الممتدة إلى 32 بت. هل يلزم وجود إشارة أو امتداد صفري عند إضافة إزاحة 32 بت إلى مؤشر لـ جهاز x86 - 64 ABI؟ لذا كنت مهتمًا برؤية أنه لا يفعل نفس الشيء بالنسبة إلى bool .)

الحاشية 2: بعد المتفرعة ، سيكون لديك فقط mov مؤلف من 4 بايت ، أو متجر 4 بايت + 1 بايت. الطول ضمني في عرض المتجر + الإزاحة.

سوف تقوم OTOH ، glibc memcpy بعمل حملتين / مخازن 4 بايت مع تداخل يعتمد على الطول ، لذلك ينتهي الأمر حقًا إلى جعل الأمر كله خاليًا من الفروع الشرطية في المنطقة المنطقية. انظر L(between_4_7): حظر في memcpy / memmove glibc. أو على الأقل ، استخدم نفس الطريقة لأي من منطقية في المتفرعة memcpy لتحديد حجم قطعة.

إذا كنت مضمنًا ، فيمكنك استخدام 2x mov cmov + cmov وإزاحة شرطية ، أو يمكنك ترك بيانات السلسلة في الذاكرة.

أو إذا كان توليف Intel Lake Lake ( مع ميزة Fast Short REP MOV ) ، قد يكون rep movsb الفعلي هو الأمثل. قد يبدأ تطبيق glibc memcpy استخدام rep movsb لأحجام صغيرة على وحدات المعالجة المركزية (CPU) باستخدام هذه الميزة ، مما يوفر الكثير من المتفرعة.

أدوات للكشف عن UB واستخدام القيم غير المهيأة

في gcc و clang ، يمكنك الترجمة باستخدام -fsanitize=undefined لإضافة أدوات وقت التشغيل التي ستحذر أو يحدث خطأ في UB يحدث في وقت التشغيل. هذا لن يمسك المتغيرات الوحدوية. (لأنه لا يزيد من أحجام الكتابة لإفساح المجال لبت "غير مهيأ").

راجع https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/

للعثور على استخدام البيانات غير المهيأة ، يوجد معقم العنوان ومطهر الذاكرة في clang / LLVM. يعرض https://github.com/google/sanitizers/wiki/MemorySanitizer أمثلة على clang -fsanitize=memory -fPIE -pie يكتشف قراءات ذاكرة غير مهيأة. قد يعمل بشكل أفضل إذا قمت بالتجميع دون تحسين ، بحيث ينتهي تحميل كل قراءات المتغيرات بالفعل من الذاكرة في asm. يظهرون أنه يتم استخدامه عند -O2 في حالة لن -O2 الحمل بها بعيدًا. لم أحاول ذلك بنفسي. (في بعض الحالات ، على سبيل المثال ، عدم تهيئة جهاز تجميع قبل تجميع صفيف ، ستطلق clang -O3 رمزًا يجمع في سجل متجه لم تتم تهيئته أبدًا. لذا ، مع التحسين ، يمكنك الحصول على حالة لا توجد فيها ذاكرة للقراءة مرتبطة بـ UB لكن -fsanitize=memory تغيير -fsanitize=memory الذي تم إنشاؤه ، وقد ينتج عنه فحص لذلك.)

سوف تتسامح مع نسخ الذاكرة غير المستهلكة ، وكذلك عمليات المنطق والحساب البسيطة معها. بشكل عام ، يتعقب MemorySanitizer بصمت انتشار البيانات غير المهيأة في الذاكرة ، ويبلغ عن تحذير عند أخذ فرع الشفرة (أو عدم التقاطه) اعتمادًا على قيمة غير مهيأة.

تنفذ MemorySanitizer مجموعة فرعية من الوظائف الموجودة في Valgrind (أداة Memcheck).

يجب أن تعمل مع هذه الحالة لأن استدعاء glibc memcpy length محسوب من ذاكرة غير مهيأة (داخل المكتبة) سينتج عنه فرع على أساس length . إذا كان يحتوي على نسخة cmov الفروع بالكامل والتي استخدمت cmov والفهرسة cmov ، فقد لا تعمل.

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

ربما تكون الفكرة وراء عدم وضع علامة على الحمل فقط هي أن الهياكل يمكن أن تحتوي على حشوة ، ونسخ الهيكل بالكامل (بما في ذلك الحشو) مع تحميل / تخزين متجه واسع النطاق ليس خطأ حتى لو كان الأعضاء الفرديون مكتوبون واحد فقط في كل مرة. على مستوى asm ، تم فقد المعلومات المتعلقة بما كان الحشوة وما هو في الواقع جزء من القيمة.

أعلم أن "السلوك غير المحدد" في C ++ يمكن أن يسمح للمترجم إلى حد كبير بالقيام بأي شيء يريده. ومع ذلك ، تعرضت لحادث فاجأني ، حيث افترضت أن الشفرة كانت آمنة بدرجة كافية.

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

جربت عدة أشياء من أجل إعادة إنتاج المشكلة وتبسيطها إلى الحد الأقصى. فيما يلي مقتطف من دالة تسمى Serialize ، والتي ستستغرق معلمة منطقية ، وانسخ السلسلة true أو false إلى مخزن مؤقت وجهة موجود.

هل ستكون هذه الوظيفة في مراجعة الكود ، لن تكون هناك طريقة لإخبارها بأنها ، في الواقع ، يمكن أن تتعطل إذا كانت المعلمة المنطقية قيمة غير مهيأة؟

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

إذا تم تنفيذ هذا الرمز باستخدام clang 5.0.0 + أمثلية ، فسيتم تعطلها.

و boolValue ? "true" : "false" المشغل المتوقع boolValue ? "true" : "false" boolValue ? "true" : "false" بدت آمنة بدرجة كافية بالنسبة لي ، كنت أفترض ، "مهما كانت قيمة البيانات المهملة في boolValue ، فلن يتم تقييمها بشكل صحيح أو خطأ."

لقد قمت بإعداد مثال Compiler Explorer يوضح المشكلة في التفكيك ، وهنا المثال الكامل. ملاحظة: من أجل تكرار المشكلة ، فإن المجموعة التي وجدتها نجحت هي استخدام Clang 5.0.0 مع تحسين -O2.

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

تنشأ المشكلة بسبب المُحسِّن: كان ذكيًا بما يكفي لاستنتاج أن السلاسل "صواب" و "خطأ" تختلف فقط في الطول بمقدار 1. لذلك بدلاً من حساب الطول حقًا ، فإنه يستخدم قيمة القيمة المنطقية نفسها ، والتي ينبغي كن من الناحية الفنية إما 0 أو 1 ، ويذهب مثل هذا:

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

في حين أن هذا "ذكي" ، إذا جاز التعبير ، سؤالي هو: هل يسمح المعيار C ++ للمترجم أن يفترض وجود منطقي يمكن أن يكون له تمثيل رقمي داخلي لـ "0" أو "1" واستخدامه بهذه الطريقة؟

أم أن هذه هي حالة محددة بالتنفيذ ، وفي هذه الحالة يفترض التنفيذ أن جميع منطقياته لن تحتوي إلا على 0 أو 1 ، وأي قيمة أخرى هي منطقة سلوك غير محددة؟


الوظيفة نفسها صحيحة ، لكن في برنامج الاختبار الخاص بك ، تؤدي العبارة التي تستدعي الوظيفة إلى سلوك غير محدد باستخدام قيمة متغير غير مهيأ.

يوجد خطأ في وظيفة الاستدعاء ، ويمكن اكتشافه عن طريق مراجعة التعليمات البرمجية أو التحليل الثابت لوظيفة الاستدعاء. باستخدام رابط مستكشف برنامج التحويل البرمجي ، يقوم برنامج التحويل البرمجي gcc 8.2 بالكشف عن الخطأ. (ربما يمكنك تقديم تقرير خطأ ضد clang بأنه لا يجد المشكلة).

السلوك غير المحدد يعني أن أي شيء يمكن أن يحدث ، والذي يتضمن تعطل البرنامج لعدة أسطر بعد الحدث الذي أدى إلى حدوث سلوك غير محدد.

NB. الإجابة على "هل يمكن أن يؤدي السلوك غير المحدد إلى _____؟" هو دائما "نعم". هذا هو حرفيا تعريف السلوك غير المحدد.


يُسمح للقيمة المنطقية فقط بالاحتفاظ بالقيم 0 أو 1 ، ويمكن أن يفترض الكود الذي تم إنشاؤه أنه يحتفظ بواحدة من هاتين القيمتين فقط. يمكن أن يستخدم الكود الذي تم إنشاؤه للثلاثي في ​​المهمة القيمة كمؤشر في مجموعة من المؤشرات إلى السلسلتين ، أي يمكن تحويلها إلى شيء مثل:

     // the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

إذا كان boolValue غير مهيأ ، فقد يحتفظ فعليًا بأي قيمة عددية صحيحة ، مما قد يؤدي إلى الوصول إلى خارج حدود مجموعة strings .


يُسمح للمترجم بافتراض أن القيمة المنطقية التي تم تمريرها كوسيطة هي قيمة منطقية صالحة (على سبيل المثال ، القيمة التي تمت تهيئتها أو تحويلها إلى true أو false ). لا يجب أن تكون القيمة true هي نفس العدد الصحيح 1 - في الواقع ، يمكن أن يكون هناك تمثيلات مختلفة true false - ولكن يجب أن تكون المعلمة عبارة عن تمثيل صحيح لإحدى هاتين القيمتين ، حيث "التمثيل الصحيح" يتم تعريف التنفيذ.

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

50) قد يؤدي استخدام قيمة منطقية بالطرق الموصوفة في هذه المواصفة القياسية الدولية على أنها "غير محددة" ، مثل فحص قيمة كائن تلقائي غير مهيأ ، إلى التصرف كما لو كان غير صحيح ولا خطأ. (الحاشية للفقرة 6 من الفقرة 6.1.9 ، الأنواع الأساسية)





abi