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

في بعض الأحيان ، نريد الالتفاف على نظام الكتابة وتفسير الكائن كنوع آخر. إعادة تفسير قطعة الذاكرة كنوع آخر يسمى نوع
التورية التورية . تعد كتابة التورية مفيدة للمهام التي تتطلب الوصول إلى التمثيل الأساسي لكائن لعرض البيانات المقدمة أو نقلها أو معالجتها. المناطق النموذجية التي يمكننا من خلالها مواجهة استخدام التورية في الكتابة: المترجمون ، التسلسل ، رمز الشبكة ، إلخ.
تقليديًا ، تم تحقيق ذلك عن طريق أخذ عنوان الكائن ، وإلقائه على مؤشر إلى النوع الذي نريد أن نترجم إليه ، ثم الوصول إلى القيمة ، أو بمعنى آخر ، استخدام الأسماء المستعارة. على سبيل المثال:
int x = 1 ; // C float *fp = (float*)&x ; // // C++ float *fp = reinterpret_cast<float*>(&x) ; // printf( “%f\n”, *fp ) ;
كما رأينا في وقت سابق ، هذا هو التعرجات غير مقبولة ، وهذا سوف يسبب سلوك غير محدد. لكن تقليديًا ، لم يستخدم المترجمون قواعد صارمة للتسميط ، وكان هذا النوع من الأكواد يعمل عادةً فقط ، والمستخدمون ، للأسف ، معتادون على السماح بمثل هذه الأشياء. هناك طريقة شائعة بديلة لكتابة الكلمات هي من خلال الاتحاد ، وهي صالحة في C ، ولكنها ستتسبب في سلوك غير محدد في C ++ (
انظر المثال ):
union u1 { int n; float f; } ; union u1 u; uf = 1.0f; printf( "%d\n”, un ); // UB(undefined behaviour) C++ “n is not the active member”
هذا غير مقبول في C ++ ، ويعتقد البعض أن النقابات مخصصة فقط لتنفيذ أنواع متباينة ، ويرون أن استخدام النقابات لكتابة التورية يعتبر إساءة.
كيفية تنفيذ لعبة الكلمات؟الطريقة المباركة القياسية لكتابة التورية في C و C ++ هي memcpy. قد يبدو هذا الأمر معقدًا بعض الشيء ، لكن المحسن بحاجة إلى التعرف على استخدام memcpy للتورية وتحسينه وإنشاء سجل لتسجيل الخطوة. على سبيل المثال ، إذا علمنا أن int64_t هو نفس حجم المضاعفة:
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17
يمكننا استخدام
memcpy
:
void func1( double d ) { std::int64_t n; std::memcpy(&n, &d, sizeof d); //…
مع وجود مستوى كافٍ من التحسين ، يقوم أي مترجم حديث لائق بإنشاء كود مماثل لطريقة reinterpret_cast المذكورة سابقًا أو طريقة الصلة للحصول على التورية. دراسة الكود الذي تم إنشاؤه ، نرى أنه يستخدم فقط سجل mov (
مثال ).
أنواع التورية والمصفوفاتولكن ماذا لو أردنا تطبيق التورية لصفيف char غير موقَّع في سلسلة من int غير الموقَّعة ثم إجراء عملية على كل قيمة int غير موقعة؟ يمكننا استخدام memcpy لتحويل مصفوفة char غير موقعة إلى نوع int غير مجزأ مؤقت. سيظل المحسن قادرًا على رؤية كل شيء من خلال memcpy وتحسين كل من الكائن المؤقت والنسخة ، والعمل مباشرة مع البيانات الأساسية (
مثال ):
// , int foo( unsigned int x ) { return x ; } // , len sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = 0; std::memcpy( &ui, &p[index], sizeof(unsigned int) ); result += foo( ui ) ; } return result; }
في هذا المثال ، نأخذ
char*p
، نفترض أنه يشير إلى عدة أجزاء من بيانات
sizeof(unsigned int)
، وتفسير كل جزء من البيانات على أنه
unsigned int
، وحساب
foo()
لكل جزء من التورية ، ونلخصها في النتيجة ، ونعيد القيمة النهائية .
يوضح التجميع لهيكل الحلقة أن المحسّن يحول الجسم إلى وصول مباشر إلى صفيف قاعدة
unsigned char
باعتباره
unsigned int
،
eax
مباشرةً إلى
eax
:
add eax, dword ptr [rdi + rcx]
نفس الكود ، ولكن باستخدام
reinterpret_cast
لتنفيذ التورية (ينتهك التعابير الصارمة):
// , len sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = *reinterpret_cast<unsigned int*>(&p[index]); result += foo( ui ); } return result; }
C ++ 20 و bit_castفي C ++ 20 ، لدينا
bit_cast
، والذي يوفر طريقة سهلة وآمنة للترجمة ، ويمكن استخدامه أيضًا في سياق
constexpr
.
التالي مثال عن كيفية استخدام
bit_cast
لتفسير عدد صحيح غير
bit_cast
في
float
(
مثال ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //, sizeof(float) == sizeof(unsigned int)
في حالة عدم وجود نفس الحجم بين الأنواع من وإلى ، فإن هذا يتطلب منا استخدام بنية وسيطة. سنستخدم بنية تحتوي على صفيف أحرف متعددة من
sizeof(unsigned int)
(يفترض int 4-بايت غير موقعة) كنوع من ، و
unsigned int
كـ إلى.
struct uint_chars { unsigned char arr[sizeof( unsigned int )] = {} ; // sizeof( unsigned int ) == 4 }; // len 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 و C ++ أن الكائنات تخضع لمتطلبات المحاذاة التي تحد من المكان الذي يمكن وضع الكائنات فيه (في الذاكرة) وبالتالي يمكن الوصول إليها.
C11 القسم 6.2.8 محاذاة حالات الكائنات :
تحتوي الأنواع الكاملة من الكائنات على متطلبات محاذاة تفرض قيودًا على العناوين التي يمكن وضع كائنات من هذا النوع عليها. المحاذاة هي قيمة عددية معرفة بالتنفيذ والتي تمثل عدد البايتات بين العناوين المتتالية التي يمكن وضع هذا الكائن فيها. يفرض نوع الكائن متطلبات محاذاة على كل كائن من هذا النوع: يمكن طلب محاذاة أكثر صرامة باستخدام
_Alignas
.
معيار مشروع C ++ 17 في القسم 1 [basic.align] :
لأنواع الكائنات متطلبات محاذاة (6.7.1 ، 6.7.2) تفرض قيودًا على العناوين التي يمكن وضع كائن من هذا النوع عليها. المحاذاة هي قيمة عددية معرفة بالتنفيذ والتي تمثل عدد البايتات بين العناوين المتتالية التي يمكن عندها وضع كائن معين. نوع الكائن يفرض شرط محاذاة على كل كائن من هذا النوع ؛ يمكن طلب محاذاة أكثر صرامة باستخدام محدد المحاذاة (10.6.2).
يشير كل من C99 و C11 بشكل صريح إلى أن التحويل الذي ينتج عنه مؤشر غير محايد هو سلوك غير محدد ، القسم 6.3.2.3.
المؤشرات تقول:
يمكن تحويل مؤشر إلى كائن أو نوع جزئي إلى مؤشر إلى كائن آخر أو نوع جزئي. إذا لم يتم محاذاة المؤشر الناتج بشكل صحيح لنوع المؤشر ، فإن السلوك غير معرف. ...
على الرغم من أن C ++ ليست واضحة ، أعتقد أن هذه الجملة من الفقرة 1
[basic.align]
كافية:
... نوع الكائن يفرض شرط محاذاة على كل كائن من هذا النوع ؛ ...
مثاللذلك دعونا نفترض:
- alignof (char) و alignof (int) هما 1 و 4 على التوالي
- sizeof (int) هو 4
وبالتالي ، فإن تفسير صفيف char بحجم 4 على أنه
int
ينتهك التعرجات الصارمة ، وقد ينتهك أيضًا متطلبات المحاذاة إذا كان للصفيف محاذاة 1 أو 2 بايت.
char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; // 1 2 int x = *reinterpret_cast<int*>(arr); // Undefined behavior
مما قد يؤدي إلى انخفاض الأداء أو خطأ ناقل في بعض الحالات. في حين أن استخدام المحاذاة لفرض نفس المحاذاة لصفيف في int سيمنع متطلبات المحاذاة من الانهيار:
alignas(alignof(int)) char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; int x = *reinterpret_cast<int*>(arr);
الذريةعقاب آخر غير متوقع للوصول غير المتوازن هو أنه ينتهك atomicity بعض المباني. قد لا تظهر المخازن الذرية ذرية لمؤشرات الترابط الأخرى في x86 إذا لم تكن محاذاة.
اصطياد الانتهاكات الصارمةليس لدينا العديد من الأدوات الجيدة لتتبع التعرجات الصارمة في C ++. الأدوات التي لدينا سوف نلاحظ بعض حالات الانتهاكات وبعض حالات التحميل والتخزين غير المناسبين.
يمكن لـ gcc باستخدام
-fstrict-aliasing
و
-Wstrict-aliasing
التقاط بعض الحالات ، على الرغم من أن ذلك لا يخلو من الإيجابيات / المشكلات الخاطئة. على سبيل المثال ، ستنشئ الحالات التالية تحذيرًا في gcc (
مثال ):
int a = 1; short j; float f = 1.f; // , TIS , 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 ، والتي يمكنها التقاط وتخزين محاذاة بطريقة خاطئة. على الرغم من أنها ليست انتهاكات مباشرة للتعرج الصارم ، إلا أنها نتيجة شائعة إلى حد ما. على سبيل المثال ، ستنشئ الحالات التالية أخطاء وقت التشغيل أثناء التجميع باستخدام
-fsanitize=address
باستخدام
-fsanitize=address
int *x = new int[2]; // 8 : [0,7]. int *u = (int*)((char*)x + 6); // x *u = 1; // [6-9] printf( "%d\n", *u ); // [6-9]
الأداة الأخيرة التي أوصي بها هي خاصة بـ C ++ ، وفي الواقع ، ليست فقط أداة ، ولكن أيضًا ممارسة تشفير لا تسمح
-Wold-style-cast
كل من
gcc
و
clang
بإجراء تشخيصات
-Wold-style-cast
باستخدام
-Wold-style-cast
. سيؤدي ذلك إلى فرض أي تورية كتابة غير محددة لاستخدام reinterpret_cast. بشكل عام ، يجب أن تكون
reinterpret_cast
منارة لتحليل الشفرة بشكل أكثر شمولاً.
من الأسهل أيضًا البحث في قاعدة البيانات عن
reinterpret_cast
لإجراء مراجعة.
بالنسبة إلى C ، لدينا جميع الأدوات الموضحة بالفعل ، ولدينا أيضًا
tis-interpreter
، وهو محلل ثابت يقوم بتحليل شامل للبرنامج لمجموعة فرعية كبيرة من C. وبالنظر إلى إصدارات C في المثال السابق ، حيث يتخطى استخدام - التعرج الدقيق - حالة واحدة (
مثال )
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 اعتراض الثلاثة ، والمثال التالي يستدعي TIS kernel كمترجم 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.
وأخيرا ،
TySan ، والتي هي قيد التطوير. يضيف هذا المطهر معلومات التحقق من النوع إلى شريحة ذاكرة الظل ويتحقق من الوصول لتحديد ما إذا كانت تنتهك قواعد الاسم المستعار. من المحتمل أن تكون الأداة قادرة على تتبع جميع انتهاكات الأسماء المستعارة ، ولكن قد يكون لها حمل كبير في وقت التشغيل.
استنتاجلقد تعلمنا عن الأسماء المستعارة للقواعد في C و C ++ ، مما يعني أن المترجم يتوقع منا اتباع هذه القواعد بصرامة وقبول عواقب عدم الوفاء بها. لقد علمنا ببعض الأدوات التي يمكن أن تساعدنا في تحديد بعض إساءة استخدام الاسم المستعار. لقد رأينا أن الاستخدام المعتاد للتعرج هو لعبة الكلمات. لقد تعلمنا أيضًا كيفية تنفيذه بشكل صحيح.
يعمل المحسّنون تدريجياً على تحسين تحليل الاسم المستعار القائم على النوع ويقومون بالفعل باختراق بعض الأكواد التي تستند إلى انتهاكات التعرجات الصارمة. يمكننا أن نتوقع أن تتحسن التحسينات وتكسر المزيد من الأكواد التي عملت من قبل.
لدينا طرق متوافقة قياسية جاهزة لتفسير الأنواع. في بعض الأحيان لبنيات التصحيح يجب أن تكون هذه الأساليب مجردة حرة. لدينا العديد من الأدوات للكشف عن انتهاكات التعرجات الشديدة ، لكن بالنسبة إلى الإصدار C ++ ، فإنهم سيشاهدون جزءًا صغيرًا فقط من الحالات ، وبالنسبة لـ C باستخدام مترجم tis ، يمكننا تتبع معظم الانتهاكات.
شكرًا لأولئك الذين علقوا على هذا المقال: ج. ف. باستين ، كريستوفر دي بيلا ، باسكال كووك ، مات ب. دزيوبينسكي ، باتريس روي وأولافور فاجي
بالطبع ، في النهاية ، كل الأخطاء تعود إلى المؤلف.
لذلك انتهت ترجمة مادة كبيرة إلى حد ما ، يمكن قراءة الجزء الأول منها
هنا . ونحن ندعوك عادة إلى
اليوم المفتوح ، الذي سيعقد في 14 مارس من قبل رئيس قسم تطوير التكنولوجيا في Rambler & Co -
Dmitry Shebordaev.