سؤال

عند السؤال عن السلوك الشائع غير المحدد في C, النفوس أكثر استنارة مما أشرت إليه بقاعدة التعرج الصارمة.
عن ماذا يتحدثون أو ما الذي يتحدثون عنه؟

هل كانت مفيدة؟

المحلول

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

لذا، في هذا النوع من الإعداد، إذا أردت إرسال رسالة إلى شيء ما، فيجب أن يكون لدي مؤشران غير متوافقين يشيران إلى نفس الجزء من الذاكرة.قد أقوم بعد ذلك بسذاجة بتشفير شيء مثل هذا:

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

قاعدة الاسم المستعار الصارمة تجعل هذا الإعداد غير قانوني:إلغاء الإشارة إلى المؤشر الذي يطلق اسمًا مستعارًا على كائن ليس من a نوع متوافق أو أحد الأنواع الأخرى التي تسمح بها C 2011 6.5 الفقرة 71 هو سلوك غير محدد.لسوء الحظ، لا يزال بإمكانك البرمجة بهذه الطريقة، ربما الحصول على بعض التحذيرات، وتجميعها بشكل جيد، فقط للحصول على سلوك غريب غير متوقع عند تشغيل التعليمات البرمجية.

(تبدو دول مجلس التعاون الخليجي غير متسقة إلى حد ما في قدرتها على إعطاء تحذيرات مستعارة، حيث تقدم لنا أحيانًا تحذيرًا وديًا وأحيانًا لا).

لمعرفة سبب عدم تحديد هذا السلوك، علينا أن نفكر في ما تشتريه قاعدة التعرج الصارمة للمترجم.في الأساس، مع هذه القاعدة، لا داعي للتفكير في إدراج تعليمات لتحديث محتويات الملف buff كل تشغيل للحلقة.بدلاً من ذلك، عند التحسين، مع بعض الافتراضات غير المطبقة بشكل مزعج حول الاسم المستعار، يمكن حذف تلك التعليمات، وتحميل buff[0] و buff[1] في سجلات وحدة المعالجة المركزية مرة واحدة قبل تشغيل الحلقة، وتسريع جسم الحلقة.قبل تقديم الأسماء المستعارة الصارمة، كان على المترجم أن يعيش في حالة من جنون العظمة لدرجة أن محتوياته buff يمكن أن تتغير في أي وقت ومن أي مكان من قبل أي شخص.لذلك، للحصول على ميزة أداء إضافية، وبافتراض أن معظم الأشخاص لا يكتبون مؤشرات، تم تقديم قاعدة الأسماء المستعارة الصارمة.

ضع في اعتبارك، إذا كنت تعتقد أن المثال مفتعل، فقد يحدث هذا أيضًا إذا كنت تقوم بتمرير مخزن مؤقت إلى وظيفة أخرى تقوم بالإرسال نيابةً عنك، إذا كان لديك بدلاً من ذلك.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

وأعدنا كتابة حلقتنا السابقة للاستفادة من هذه الوظيفة المريحة

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

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

فكيف يمكنني التغلب على هذا؟

  • استخدام الاتحاد.يدعم معظم المترجمين هذا دون الشكوى من التعرجات الصارمة.هذا مسموح به في C99 ومسموح به صراحة في C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • يمكنك تعطيل الأسماء المستعارة الصارمة في برنامج التحويل البرمجي الخاص بك (f [no-] التعرجات الصارمة في دول مجلس التعاون الخليجي))

  • يمكنك استخدام char* للاستعارة بدلاً من كلمة نظامك.تسمح القواعد باستثناء char* (مشتمل signed char و unsigned char).من المفترض دائما ذلك char* الأسماء المستعارة أنواع أخرى.لكن هذا لن يعمل بالطريقة الأخرى:ليس هناك افتراض بأن البنية الخاصة بك تستخدم اسمًا مستعارًا لمخزن مؤقت للأحرف.

احذر المبتدئين

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

هامش

1 الأنواع التي يسمح C 2011 6.5 7 للقيمة بالوصول إليها هي:

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

نصائح أخرى

أفضل تفسير وجدته هو من قبل مايك أكتون، فهم التعرج الصارم.لقد ركزت قليلاً على تطوير PS3، لكن هذا في الأساس يتعلق بدول مجلس التعاون الخليجي فقط.

من المقال:

"الاسم المستعار الصارم هو افتراض، قام به مترجم C (أو C++)، بأن مؤشرات إلغاء الإشارة إلى كائنات من أنواع مختلفة لن تشير أبدًا إلى نفس موقع الذاكرة (أي.الاسم المستعار لبعضها البعض.)"

لذلك إذا كان لديك int* مشيرا إلى بعض الذاكرة التي تحتوي على int ومن ثم تشير أ float* إلى تلك الذاكرة واستخدامها ك float أنت تكسر القاعدة.إذا كانت التعليمات البرمجية الخاصة بك لا تحترم هذا، فمن المرجح أن يقوم مُحسِّن المترجم بكسر التعليمات البرمجية الخاصة بك.

الاستثناء من القاعدة هو أ char*, ، والذي يسمح للإشارة إلى أي نوع.

هذه هي قاعدة الأسماء المستعارة الصارمة، الموجودة في القسم 3.10 من ج++03 المعيار (توفر الإجابات الأخرى شرحًا جيدًا، لكن لم يقدم أي منها القاعدة نفسها):

إذا حاول برنامج الوصول إلى القيمة المخزنة لكائن ما من خلال قيمة تختلف عن أحد الأنواع التالية، فسيكون السلوك غير محدد:

  • النوع الديناميكي للكائن،
  • نسخة مؤهلة للسيرة الذاتية من النوع الديناميكي للكائن،
  • نوع يمثل النوع الموقع أو غير الموقع المطابق للنوع الديناميكي للكائن،
  • نوع هو النوع الموقع أو غير الموقع المطابق لنسخة مؤهلة من النوع الديناميكي للكائن،
  • نوع إجمالي أو اتحاد يتضمن أحد الأنواع المذكورة أعلاه بين أعضائه (بما في ذلك، بشكل متكرر، عضو في تجميع فرعي أو اتحاد متضمن)،
  • نوع يمثل فئة أساسية (ربما مؤهلة للسيرة الذاتية) للنوع الديناميكي للكائن،
  • أ char أو unsigned char يكتب.

سي++11 و سي++14 الصياغة (تم التأكيد على التغييرات):

إذا حاول أحد البرامج الوصول إلى القيمة المخزنة لكائن من خلال ملف com.glvalue بخلاف أحد الأنواع التالية يكون السلوك غير محدد:

  • النوع الديناميكي للكائن،
  • نسخة مؤهلة للسيرة الذاتية من النوع الديناميكي للكائن،
  • نوع مشابه (كما هو محدد في 4.4) للنوع الديناميكي للكائن،
  • نوع يمثل النوع الموقع أو غير الموقع المطابق للنوع الديناميكي للكائن،
  • نوع هو النوع الموقع أو غير الموقع المطابق لنسخة مؤهلة من النوع الديناميكي للكائن،
  • نوع مجمع أو اتحادي يضم بينه أحد الأنواع المذكورة أعلاه عناصر أو أعضاء بيانات غير ثابتة (بما في ذلك، بشكل متكرر، عنصر أو عضو بيانات غير ثابت من التجميع الفرعي أو الاتحاد الوارد)،
  • نوع يمثل فئة أساسية (ربما مؤهلة للسيرة الذاتية) للنوع الديناميكي للكائن،
  • أ char أو unsigned char يكتب.

كان هناك تغييران صغيران: com.glvalue بدلاً من lvalue, وتوضيح حالة الجامع/الاتحاد.

التغيير الثالث يقدم ضمانًا أقوى (يخفف من قاعدة الأسماء المستعارة القوية):المفهوم الجديد ل أنواع مماثلة التي أصبحت الآن آمنة للاسم المستعار.


أيضا ج الصياغة (C99؛ISO/IEC 9899:1999 6.5/7؛يتم استخدام نفس الصياغة بالضبط في ISO/IEC 9899:2011 §6.5 ¶7):

يجب أن يكون للكائن قيمته المخزنة التي يتم الوصول إليها فقط من خلال تعبير LVALUE الذي يحتوي على أحد الأنواع التالية 73) أو 88):

  • نوع متوافق مع النوع الفعال للكائن،
  • نسخة مؤهلة من نوع متوافق مع النوع الفعال للكائن ،
  • نوع هو النوع الموقّع أو غير الموقّع المقابل للنوع الفعال للكائن ،
  • نوع هو النوع الموقّع أو غير الموقّع المقابل لإصدار مؤهل من النوع الفعال للكائن ،
  • نوع إجمالي أو اتحاد يتضمن أحد الأنواع المذكورة أعلاه بين أعضائها (بما في ذلك ، على نحو متكرر ، عضوًا في اتحاد فرعي أو محتوى) ، أو
  • نوع الحرف.

73) أو 88) الغرض من هذه القائمة هو تحديد تلك الظروف التي قد يكون أو لا يكون فيها الكائن مستعارًا.

ملحوظة

هذا مقتبس من كتابي "ما هي قاعدة التعرج الصارمة ولماذا نهتم؟" الكتابة.

ما هو الاسم المستعار الصارم؟

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

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

لفهم المزيد حول سبب اهتمامنا، سنناقش المشكلات التي تظهر عند انتهاك قواعد الاسم المستعار الصارمة، وتلاعب الكلمات نظرًا لأن التقنيات الشائعة المستخدمة في كتابة الكلمات غالبًا ما تنتهك قواعد الاسم المستعار الصارمة وكيفية كتابة الكلمات بشكل صحيح.

أمثلة أولية

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

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

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

يوضح المثال التالي الاسم المستعار الذي يؤدي إلى سلوك غير محدد (مثال حي):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

في الوظيفة foo نحن نأخذ كثافة العمليات * و أ يطفو*, ، في هذا المثال نسميه foo وقم بتعيين كلا المعلمتين للإشارة إلى نفس موقع الذاكرة الذي يحتوي في هذا المثال على كثافة العمليات.لاحظ ال reinterpret_cast هو إخبار المترجم بمعاملة التعبير كما لو كان يحتوي على النوع المحدد بواسطة معلمة القالب الخاصة به.في هذه الحالة نطلب منه معالجة التعبير &x كما لو كان له نوع يطفو*.وقد نتوقع بسذاجة نتيجة الثانية cout يكون 0 ولكن مع تمكين التحسين باستخدام -O2 ينتج عن كل من gcc و clang النتيجة التالية:

0
1

وهو ما قد لا يكون متوقعًا ولكنه صحيح تمامًا لأننا قمنا باستدعاء سلوك غير محدد.أ يطفو لا يمكن الاسم المستعار بشكل صحيح كثافة العمليات هدف.ولذلك يمكن للمحسن أن يفترض ثابت 1 تخزينها عند إلغاء الإشارة أنا ستكون قيمة الإرجاع منذ المتجر F لا يمكن أن تؤثر بشكل صحيح على كثافة العمليات هدف.يُظهر توصيل الكود في Compiler Explorer أن هذا هو بالضبط ما يحدث (مثال حي):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

محسن باستخدام تحليل الاسم المستعار على أساس النوع (TBAA) يفترض 1 سيتم إرجاعها ونقل القيمة الثابتة مباشرة إلى التسجيل com.eax الذي يحمل قيمة الإرجاع.يستخدم TBAA قواعد اللغات حول الأنواع المسموح بها للاسم المستعار لتحسين عمليات التحميل والمخازن.في هذه الحالة، يعرف TBAA أن أ يطفو لا يمكن الاسم المستعار و كثافة العمليات ويحسن بعيدا تحميل أنا.

والآن إلى كتاب القواعد

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

ماذا يقول معيار C11؟

ال ج11 المعيار يقول ما يلي في القسم 6.5 التعبيرات الفقرة 7:

يجب أن يتم الوصول إلى القيمة المخزنة للكائن فقط من خلال تعبير lvalue الذي يحتوي على أحد الأنواع التالية:88)- نوع متوافق مع النوع الفعال للكائن،

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- نسخة مؤهلة من نوع متوافق مع النوع الفعال للكائن،

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- نوع يمثل النوع الموقع أو غير الموقع المطابق للنوع الفعال للكائن،

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc/clang له امتداد و أيضًا الذي يسمح بتعيين كثافة العمليات غير الموقعة* ل كثافة العمليات * على الرغم من أنها ليست أنواع متوافقة.

— نوع يمثل النوع الموقع أو غير الموقع المطابق لإصدار مؤهل من النوع الفعال للكائن،

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- نوع إجمالي أو اتحاد يتضمن أحد الأنواع المذكورة أعلاه بين أعضائه (بما في ذلك، بشكل متكرر، عضو في تجميع فرعي أو اتحاد متضمن)، أو

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- نوع الحرف.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

ما تقوله مسودة معيار C++ 17

مسودة معيار C++ 17 في القسم [basic.lval] الفقرة 11 يقول:

إذا حاول برنامج الوصول إلى القيمة المخزنة لكائن من خلال قيمة gl بخلاف أحد الأنواع التالية، فسيكون السلوك غير محدد:63(11.1) - النوع الديناميكي للكائن،

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - نسخة مؤهلة من النوع الديناميكي للكائن،

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - نوع مشابه (كما هو محدد في 7.5) للنوع الديناميكي للكائن،

(11.4) - نوع هو النوع الموقع أو غير الموقع المطابق للنوع الديناميكي للكائن،

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - نوع هو النوع الموقع أو غير الموقع المطابق لنسخة مؤهلة من النوع الديناميكي للكائن،

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - نوع إجمالي أو اتحاد يتضمن أحد الأنواع المذكورة أعلاه بين عناصره أو أعضاء البيانات غير الساكنة (بما في ذلك، بشكل متكرر، عنصر أو عضو بيانات غير ثابت في تجميع فرعي أو اتحاد مضمن)،

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - نوع يمثل فئة أساسية (ربما مؤهلة للسيرة الذاتية) للنوع الديناميكي للكائن،

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - نوع char أو char غير موقع أو std::byte.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

لا يستحق شيئا حرف موقّع لم يتم تضمينه في القائمة أعلاه، وهذا فرق ملحوظ عن ج الذي يقول نوع الحرف.

ما هو نوع Punning

لقد وصلنا إلى هذه النقطة، وقد نتساءل، لماذا نريد الاسم المستعار لـ؟الجواب عادة هو اكتب التورية, ، غالبًا ما تنتهك الأساليب المستخدمة قواعد الاسم المستعار الصارمة.

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

تقليديًا، يتم تحقيق ذلك عن طريق أخذ عنوان الكائن، وإحالته إلى مؤشر من النوع الذي نريد إعادة تفسيره عليه ثم الوصول إلى القيمة، أو بمعنى آخر عن طريق الاسم المستعار.على سبيل المثال:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

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

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

هذا غير صالح في C++ ويعتبر البعض أن غرض النقابات هو تنفيذ أنواع مختلفة فقط ويشعرون أن استخدام النقابات لعقاب النوع يعد إساءة.

كيف نكتب التورية بشكل صحيح؟

الطريقة القياسية ل اكتب الضرب في كل من C وC++ com.memcpy.قد يبدو هذا ثقيلًا بعض الشيء ولكن يجب على المُحسِّن التعرف على استخدام com.memcpy ل اكتب الضرب وقم بتحسينه وإنشاء سجل لتسجيل الحركة.على سبيل المثال إذا كنا نعرف int64_t هو نفس الحجم مزدوج:

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

يمكننا ان نستخدم com.memcpy:

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

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

C++20 و bit_cast

في C++ 20 قد نكسب bit_cast (التنفيذ متاح في الرابط من الاقتراح) مما يوفر طريقة بسيطة وآمنة للكتابة على الكلمات بالإضافة إلى كونها قابلة للاستخدام في سياق contexpr.

وفيما يلي مثال على كيفية الاستخدام bit_cast لكتابة التورية أ كثافة العمليات غير الموقعة ل يطفو, (رؤيته على الهواء مباشرة):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

في حالة حيث ل و من الأنواع ليس لها نفس الحجم، فهي تتطلب منا استخدام بنية وسيطة15.سوف نستخدم بنية تحتوي على أ حجم (كثافة العمليات غير الموقعة) مصفوفة الحروف (يفترض 4 بايت غير موقعة int) ليكون من اكتب و كثافة العمليات غير الموقعة كما ل يكتب.:

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

ومن المؤسف أننا بحاجة إلى هذا النوع الوسيط ولكن هذا هو القيد الحالي bit_cast.

ضبط المخالفات الصارمة

ليس لدينا الكثير من الأدوات الجيدة لاكتشاف الأسماء المستعارة الصارمة في لغة C++، والأدوات المتوفرة لدينا ستكتشف بعض حالات انتهاكات الاسم المستعار الصارمة وبعض حالات التحميل والمخازن غير المتوافقة.

دول مجلس التعاون الخليجي باستخدام العلم -fstrict-aliasing و -التعرج الصارم يمكن اكتشاف بعض الحالات ولكن ليس بدون نتائج إيجابية/سلبية كاذبة.على سبيل المثال، ستؤدي الحالات التالية إلى إنشاء تحذير في دول مجلس التعاون الخليجي (رؤيته على الهواء مباشرة):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

على الرغم من أنها لن تلتقط هذه الحالة الإضافية (رؤيته على الهواء مباشرة):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

على الرغم من أن clang يسمح بهذه العلامات، فمن الواضح أنه لا ينفذ التحذيرات فعليًا.

هناك أداة أخرى متاحة لنا وهي Asan والتي يمكنها التقاط الأحمال والمخازن غير المتوافقة.على الرغم من أن هذه ليست انتهاكات صارمة للاستعارة بشكل مباشر، إلا أنها نتيجة شائعة لانتهاكات الأسماء المستعارة الصارمة.على سبيل المثال، ستؤدي الحالات التالية إلى حدوث أخطاء في وقت التشغيل عند إنشائها باستخدام clang -fsanitize=address

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

الأداة الأخيرة التي سأوصي بها هي أداة خاصة بـ C++ وليست أداة بشكل صارم ولكنها ممارسة ترميزية، ولا تسمح بالقوالب ذات النمط C.سينتج كل من gcc و clang تشخيصًا للقوالب ذات النمط C باستخدام - أسلوب القفر.سيؤدي هذا إلى إجبار أي تورية من النوع غير المحدد على استخدام reinterpret_cast، بشكل عام، يجب أن تكون reinterpret_cast علامة لمراجعة التعليمات البرمجية عن كثب.من الأسهل أيضًا البحث في قاعدة التعليمات البرمجية الخاصة بك عن reinterpret_cast لإجراء التدقيق.

بالنسبة للغة C، لدينا جميع الأدوات التي تم تغطيتها بالفعل ولدينا أيضًا tis-interpreter، وهو محلل ثابت يقوم بتحليل شامل لبرنامج لمجموعة فرعية كبيرة من لغة C.بالنظر إلى إصدارات C من المثال السابق حيث يتم استخدام -fstrict-aliasing يغيب عن حالة واحدة (رؤيته على الهواء مباشرة)

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter قادر على التقاط الثلاثة، المثال التالي يستدعي tis-kernal كمترجم tis (يتم تحرير الإخراج للإيجاز):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

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

لا يشير الاسم المستعار الصارم إلى المؤشرات فحسب، بل إنه يؤثر على المراجع أيضًا، وقد كتبت ورقة بحثية حول هذا الموضوع لصالح موقع wiki الخاص بمطوري البرامج وقد تم استقبالها بشكل جيد لدرجة أنني حولتها إلى صفحة على موقع الويب الاستشاري الخاص بي.إنه يشرح تمامًا ما هو ولماذا يربك الناس كثيرًا وما يجب فعله حيال ذلك. ورقة بيضاء صارمة التعرج.على وجه الخصوص، يشرح لماذا تعد النقابات سلوكًا محفوفًا بالمخاطر لـ C++، ولماذا استخدام memcpy هو الإصلاح الوحيد المحمول عبر كل من C وC++.نأمل أن يكون هذا مفيدا.

كإضافة لما قاله دوغ ت.كتبت بالفعل ، إليك حالة اختبار بسيطة والتي ربما تثيرها مع GCC:

تحقق. ج

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

جمع مع gcc -O2 -o check check.c .عادةً (مع معظم إصدارات دول مجلس التعاون الخليجي التي جربتها) ينتج عن هذا "مشكلة اسم مستعار صارمة"، لأن المترجم يفترض أن "h" لا يمكن أن يكون نفس عنوان "k" في وظيفة "التحقق".ولهذا السبب يقوم المترجم بتحسين if (*h == 5) بعيدًا ويستدعي دائمًا printf.

بالنسبة لأولئك المهتمين، يوجد هنا رمز مجمع x64، الذي تم إنتاجه بواسطة gcc 4.6.3، ويعمل على ubuntu 12.04.2 لـ x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

لذلك اختفى شرط if تمامًا من كود المجمّع.

اكتب الضرب يعد عبر قوالب المؤشر (بدلاً من استخدام الاتحاد) مثالًا رئيسيًا على كسر الأسماء المستعارة الصارمة.

وفقًا للأساس المنطقي C89، لم يرغب مؤلفو المعيار في مطالبة المترجمين بإعطاء كود مثل:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

يجب أن تكون هناك حاجة لإعادة تحميل قيمة x بين بيان المهمة والعودة وذلك للسماح لاحتمال ذلك p قد يشير إلى x, ، والتكليف به *p قد يغير بالتالي قيمة x.فكرة أنه يجب على المترجم أن يفترض أنه لن يكون هناك أسماء مستعارة في حالات مثل ما سبق كان غير مثير للجدل.

لسوء الحظ، كتب مؤلفو C89 قاعدتهم بطريقة، إذا تمت قراءتها حرفيًا، ستجعل حتى الوظيفة التالية تستدعي سلوكًا غير محدد:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

لأنه يستخدم قيمة من النوع int للوصول إلى كائن من النوع struct S, ، و int ليس من بين الأنواع التي يمكن استخدامها للوصول إلى ملف struct S.لأنه سيكون من السخافة التعامل مع جميع استخدامات الأعضاء من غير نوع الأحرف في البنيات والاتحادات على أنها سلوك غير محدد، ويدرك الجميع تقريبًا أن هناك على الأقل بعض الظروف التي يمكن فيها استخدام قيمة من نوع واحد للوصول إلى كائن من نوع آخر .ولسوء الحظ، فشلت لجنة معايير C في تحديد ماهية تلك الظروف.

يرجع جزء كبير من المشكلة إلى تقرير العيوب رقم 028، الذي سأل عن سلوك برنامج مثل:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

ينص تقرير العيوب رقم 28 على أن البرنامج يستدعي السلوك غير المحدد لأن إجراء كتابة عضو اتحاد من النوع "مزدوج" وقراءة واحد من النوع "int" يستدعي السلوك المحدد للتنفيذ.مثل هذا المنطق لا معنى له، ولكنه يشكل الأساس لقواعد الكتابة الفعالة التي تؤدي إلى تعقيد اللغة دون داع بينما لا تفعل شيئًا لمعالجة المشكلة الأصلية.

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

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

ليس هناك صراع في الداخل inc_int لأن كل الوصول إلى التخزين يتم الوصول إليه من خلاله *p تتم مع قيمة من النوع int, ، وليس هناك صراع في test لأن p مشتق بشكل واضح من أ struct S, ، وفي المرة القادمة s يتم استخدام جميع عمليات الوصول إلى هذا التخزين الذي سيتم تنفيذه من خلاله p سيكون قد حدث بالفعل.

إذا تم تغيير الرمز قليلاً ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

هنا، هناك صراع مستعار بين p والوصول إليها s.x على السطر المحدد لأنه في تلك المرحلة من التنفيذ يوجد مرجع آخر والتي سيتم استخدامها للوصول إلى نفس التخزين.

لو قال تقرير العيوب 028 إن المثال الأصلي استدعى UB بسبب التداخل بين إنشاء المؤشرين واستخدامهما، فإن ذلك كان سيجعل الأمور أكثر وضوحًا دون الحاجة إلى إضافة "أنواع فعالة" أو أي تعقيد آخر من هذا القبيل.

بعد قراءة العديد من الإجابات، أشعر بالحاجة إلى إضافة شيء:

الاسم المستعار الصارم (الذي سأصفه بعد قليل) مهم لأن:

  1. يمكن أن يكون الوصول إلى الذاكرة مكلفًا (من حيث الأداء)، ولهذا السبب تتم معالجة البيانات في سجلات وحدة المعالجة المركزية قبل كتابتها مرة أخرى إلى الذاكرة الفعلية.

  2. إذا كانت البيانات الموجودة في اثنين من سجلات وحدة المعالجة المركزية المختلفة ستتم كتابتها على نفس مساحة الذاكرة، لا يمكننا التنبؤ بالبيانات التي "ستنجو" عندما نقوم بالرمز في C.

    أثناء التجميع، حيث نقوم بتشفير تحميل وتفريغ سجلات وحدة المعالجة المركزية يدويًا، سنعرف البيانات التي تظل سليمة.لكن C (لحسن الحظ) يلخص هذه التفاصيل بعيدًا.

وبما أن مؤشرين يمكن أن يشيرا إلى نفس الموقع في الذاكرة، فقد يؤدي ذلك إلى كود معقد يتعامل مع الاصطدامات المحتملة.

هذا الرمز الإضافي بطيء و يؤذي الأداء لأنه ينفذ عمليات قراءة/كتابة في الذاكرة الإضافية والتي تكون أبطأ و(ربما) غير ضرورية.

ال تسمح لنا قاعدة الاسم المستعار الصارمة بتجنب رموز الآلة الزائدة عن الحاجة في الحالات التي فيها يجب ان يكون من الآمن افتراض أن المؤشرين لا يشيران إلى نفس كتلة الذاكرة (انظر أيضًا ملف restrict الكلمة الأساسية).

ينص الاسم المستعار الصارم على أنه من الآمن افتراض أن المؤشرات إلى أنواع مختلفة تشير إلى مواقع مختلفة في الذاكرة.

إذا لاحظ المترجم أن مؤشرين يشيران إلى أنواع مختلفة (على سبيل المثال، int * و أ float *)، فسوف يفترض أن عنوان الذاكرة مختلف سوف لن الحماية من تصادمات عناوين الذاكرة، مما يؤدي إلى كود الجهاز بشكل أسرع.

على سبيل المثال:

لنفترض الوظيفة التالية:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

من أجل التعامل مع القضية التي a == b (يشير كلا المؤشرين إلى نفس الذاكرة)، نحتاج إلى ترتيب واختبار الطريقة التي نحمل بها البيانات من الذاكرة إلى سجلات وحدة المعالجة المركزية، لذلك قد ينتهي الأمر بالرمز على النحو التالي:

  1. حمولة a و b من الذاكرة.

  2. يضيف a ل b.

  3. يحفظ b و إعادة تحميل a.

    (احفظ من سجل وحدة المعالجة المركزية إلى الذاكرة وقم بالتحميل من الذاكرة إلى سجل وحدة المعالجة المركزية).

  4. يضيف b ل a.

  5. يحفظ a (من سجل وحدة المعالجة المركزية) إلى الذاكرة.

الخطوة 3 بطيئة جدًا لأنها تحتاج إلى الوصول إلى الذاكرة الفعلية.ومع ذلك، فهو مطلوب للحماية من الحالات التي a و b أشر إلى نفس عنوان الذاكرة.

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

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

    void merge_two_numbers(int *a, long *b) {...}
    
  2. باستخدام restrict الكلمة الرئيسية.أي.:

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

الآن، من خلال استيفاء قاعدة التعرج الصارمة، يمكن تجنب الخطوة 3 وسيتم تشغيل التعليمات البرمجية بشكل أسرع بشكل ملحوظ.

في الواقع، من خلال إضافة restrict الكلمة الأساسية، يمكن تحسين الوظيفة بأكملها من أجل:

  1. حمولة a و b من الذاكرة.

  2. يضيف a ل b.

  3. حفظ النتيجة على حد سواء ل a و ل b.

لم يكن من الممكن إجراء هذا التحسين من قبل، بسبب الاصطدام المحتمل (حيث a و b سيتم مضاعفة ثلاث مرات بدلاً من مضاعفة).

لا يسمح الاسم المستعار الصارم لأنواع مختلفة من المؤشرات بنفس البيانات.

هذا المقال يجب أن تساعدك على فهم المشكلة بالتفصيل الكامل.

من الناحية الفنية في لغة C++، ربما لا تكون قاعدة الاسم المستعار الصارمة قابلة للتطبيق على الإطلاق.

لاحظ تعريف المراوغة (* المشغل أو العامل):

ينفذ المشغل الأحادي * عملية غير مباشرة:يجب أن يكون التعبير الذي يتم تطبيقه مؤشرًا إلى نوع كائن ، أو مؤشر إلى نوع وظيفة و والنتيجة هي قيمة تشير إلى الكائن أو وظيفة الذي يشير إليه التعبير.

أيضا من تعريف القيمة

glvalue هو تعبير يحدد تقييمه هوية كائن ، (... القنص)

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

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top