التورية النوعية
في علوم الحاسوب ، يعتبر التورية النوعية أي أسلوب برمجة يقوم بتخريب أو التحايل على نظام الأنواع في لغة البرمجة من أجل تحقيق تأثير يصعب أو يستحيل تحقيقه ضمن حدود اللغة الرسمية .
في لغتي C و C++ ، يتم توفير بنيات مثل تحويل نوع المؤشر و - تضيف C++ تحويل نوع المرجع و إلى هذه القائمة - من أجل السماح بأنواع عديدة من التورية النوعية، على الرغم من أن بعض الأنواع غير مدعومة فعليًا بواسطة اللغة القياسية.unionreinterpret_cast
في لغة برمجة باسكال ، يمكن استخدام السجلات ذات المتغيرات لمعالجة نوع بيانات معين بأكثر من طريقة، أو بطريقة غير مسموح بها عادةً.
مثال على المقابس
يُعدّ مثال واجهة مقابس بيركلي مثالًا كلاسيكيًا على التورية في أنواع البيانات. تُعرّف الدالة التي تربط مقبسًا مفتوحًا ولكن غير مُهيأ بعنوان IP على النحو التالي:
int bind ( int sockfd , struct sockaddr * my_addr , socklen_t addrlen );bindيتم استدعاء الدالة عادةً على النحو التالي :
#include <sys/socket.h>struct sockaddr_in sa = { . sin_family = AF_INET , . sin_port = htons ( port ) };int sockfd = socket ( PF_INET , SOCK_STREAM , IPPROTO_TCP ); bind ( sockfd , ( struct sockaddr * ) & sa , sizeof sa );تعتمد مكتبة Berkeley sockets بشكل أساسي على حقيقة أنه في لغة C ، يمكن تحويل المؤشر struct sockaddr_inإلى مؤشر إلى مؤشر إلى struct sockaddr؛ بالإضافة إلى أن نوعي البنية يشتركان في نفس تخطيط الذاكرة. لذلك، فإن الإشارة إلى حقل البنية my_addr->sin_family(حيث my_addrيكون من النوع struct sockaddr*) ستشير فعليًا إلى الحقل sa.sin_family(حيث saيكون من النوع struct sockaddr_in). بعبارة أخرى، تستخدم مكتبة sockets تحويل الأنواع لتنفيذ شكل بدائي من تعدد الأشكال أو الوراثة .
يُلاحظ في عالم البرمجة استخدام هياكل البيانات "المُبطّنة" للسماح بتخزين أنواع مختلفة من القيم في نفس مساحة التخزين. ويُلاحظ هذا غالبًا عند استخدام هيكلين بشكل حصري متبادل لتحسين الأداء.
مثال على الفاصلة العائمة
لا تتضمن جميع أمثلة التورية النوعية هياكل بيانات، كما في المثال السابق. لنفترض أننا نريد تحديد ما إذا كان عدد عشري سالبًا. يمكننا كتابة ما يلي:
bool is_negative ( float x ) { return x < 0.0f ; }ومع ذلك، بافتراض أن مقارنات الفاصلة العائمة مكلفة، وبافتراض أيضًا أن floatيتم تمثيلها وفقًا لمعيار الفاصلة العائمة IEEE ، وأن الأعداد الصحيحة بعرض 32 بت، يمكننا الانخراط في تحويل النوع لاستخراج بت الإشارة من رقم الفاصلة العائمة باستخدام عمليات الأعداد الصحيحة فقط:
bool is_negative ( float x ) { int * i = ( int * ) & x ; return * i < 0 ; }لاحظ أن السلوك لن يكون متطابقًا تمامًا: في حالة xكون القيمة سالبة صفرًا ، تُرجع الطريقة الأولى قيمةً falseبينما تُرجع الثانية قيمةً أخرى true. كذلك، ستُرجع الطريقة الأولى falseقيمةً لأي قيمة NaN ، بينما قد تُرجع الطريقة الثانية trueقيمةً لقيم NaN مع ضبط بت الإشارة. أخيرًا، لدينا مشكلة تتمثل في أن تخزين بيانات الفاصلة العائمة قد يكون بترتيب الذاكرة الكبير أو الصغير، وبالتالي قد يكون بت الإشارة في البايت الأقل أهمية أو البايت الأكثر أهمية. لذلك، يُعد استخدام تحويل النوع مع بيانات الفاصلة العائمة طريقةً مشكوكًا فيها ذات نتائج غير متوقعة.
يُعدّ هذا النوع من التلاعب بالأنواع أكثر خطورة من غيره. فبينما اعتمد مثال المقابس فقط على ضمانات لغة البرمجة C بشأن تخطيط البنية وقابلية تحويل المؤشرات، floatيعتمد هذا المثال على افتراضات حول مكونات النظام المادية. يحتوي معيار لغة C99 (ISO9899:1999) على التحذير التالي في القسم 6.3.2.3 (المؤشرات) : "قد يُحوّل مؤشر إلى كائن أو نوع غير مكتمل إلى مؤشر إلى كائن أو نوع غير مكتمل آخر. إذا لم يكن المؤشر الناتج مُحاذيًا بشكل صحيح للنوع المُشار إليه، فسيكون السلوك غير مُحدد." لذا، ينبغي توخي الحذر الشديد عند استخدام التلاعب بالأنواع.
قد تتطلب بعض الحالات، مثل التعليمات البرمجية الحساسة للوقت والتي يعجز المترجم عن تحسينها ، استخدام تعليمات برمجية خطيرة. في هذه الحالات، يساعد توثيق جميع هذه الافتراضات في التعليقات ، وإدخال تأكيدات ثابتة للتحقق من توقعات قابلية النقل، في الحفاظ على قابلية صيانة التعليمات البرمجية .
تشمل الأمثلة العملية على التورية في الأعداد العشرية: الجذر التربيعي العكسي السريع الذي اشتهر بفضل لعبة Quake III ، والمقارنة السريعة للأعداد العشرية كأعداد صحيحة، [ 1 ] وإيجاد القيم المجاورة عن طريق الزيادة كعدد صحيح (تنفيذ nextafter). [ 2 ]
حسب اللغة
لغة C ولغة C++
بالإضافة إلى افتراض تمثيل الأعداد العشرية بالبتات، فإن مثال تحويل نوع الأعداد العشرية المذكور أعلاه ينتهك أيضًا قيود لغة C المتعلقة بكيفية الوصول إلى الكائنات: [ 3 ] النوع المُعلن عنه xهو ، floatولكن تتم قراءته من خلال تعبير من النوع unsigned int. في العديد من المنصات الشائعة، قد يُسبب استخدام تحويل المؤشرات هذا مشاكل إذا كانت المؤشرات المختلفة مُحاذية بطرق خاصة بكل جهاز . علاوة على ذلك، قد تُؤدي المؤشرات ذات الأحجام المختلفة إلى تداخل في الوصول إلى نفس الذاكرة ، مما يُسبب مشاكل لا يتحقق منها المُصرّف. مع ذلك، حتى عندما يتطابق حجم البيانات مع تمثيل المؤشر، يُمكن للمُصرّفات الاعتماد على قيود عدم التداخل لإجراء تحسينات قد تكون غير آمنة في حالة منع التداخل.
استخدام المؤشرات
يمكن تحقيق محاولة ساذجة للتلاعب بالأنواع باستخدام المؤشرات: (يفترض المثال التشغيلي التالي تمثيل IEEE-754 للبتات للنوع float.)
// في لغة C، الدالة المنطقية is_negative ( float x ) { int32_t i = * ( int32_t * ) & x ; return i < 0 ; }// في لغة C++، الدالة المنطقية `is_negative ( float x ) ` تقوم بتحويل قيمة `x` إلى عدد صحيح من نوع ` int32_t` باستخدام الدالة ` reinterpret_cast < int32_t *> ( & x )`، ثم تُرجع قيمة `x` إذا كانت `i` أقل من صفر .تنص قواعد التسمية البديلة في معيار لغة C على أنه لا يجوز الوصول إلى القيمة المخزنة لكائن ما إلا من خلال تعبير قيمة يسارية من نوع متوافق. [ 4 ] النوعان floatغير int32_tمتوافقين، لذا فإن سلوك هذا الكود غير محدد . على الرغم من أن هذا البرنامج المحدد يُترجم ويُنفذ كما هو متوقع على GCC و LLVM ، إلا أن الأمثلة الأكثر تعقيدًا قد تتفاعل مع الافتراضات التي تقوم عليها التسمية البديلة الصارمة وتؤدي إلى سلوك غير مرغوب فيه. يضمن الخيار -fno-strict-aliasingالسلوك الصحيح للكود الذي يستخدم هذا النوع من التسمية البديلة، على الرغم من أنه يُوصى باستخدام أنواع أخرى من التسمية البديلة. [ 5 ]
استخدامunion
في لغة C، ولكن ليس في لغة C++، من الممكن أحيانًا إجراء تحويل النوع عبر a union.
دالة منطقية `is_negative ( float x )` : تقوم هذه الدالة بإنشاء اتحاد يحتوي على متغيرين : `i` و` f` . ثم تقوم بتعيين قيمة `f` إلى `x` ، وتُرجع ` i` إذا كانت قيمة `i` أقل من الصفر .يُعد الوصول my_union.iإلى عنصر آخر بعد آخر عملية كتابة إليه my_union.fشكلاً مسموحاً به من أشكال التورية النوعية في لغة C، [ 6 ] بشرط ألا يكون العنصر المقروء أكبر من العنصر الذي تم تعيين قيمته (وإلا فإن سلوك القراءة غير محدد [ 7 ] ). وينطبق الأمر نفسه من الناحية النحوية، ولكنه غير محدد السلوك في لغة C++، [ 8 ] حيث يُعتبر العنصر الأخير الذي تمت الكتابة إليه فقط unionهو الذي يحمل قيمة.
للحصول على مثال آخر على التورية النوعية، انظر خطوة المصفوفة .
استخدامmemcpy
memcpyهي طريقة آمنة وقابلة للتطبيق على نطاق واسع لتعديل أنواع البيانات، [ 9 ] ومُعتمدة في معيار لغة C++. [ 10 ] يتضمن كل من Clangmemcpy وGCC تحسينات خاصة لهذا النوع من الاستدعاءات. [ 11 ]
#include <string.h>bool is_negative ( float x ) { int i ; memcpy ( & i , & x , sizeof ( int )); // أو std::memcpy في لغة C++ return i < 0 ; }استخدامbit_cast
في لغة C++20 ، std::bit_castتسمح هذه الدالة بتغيير نوع البيانات دون حدوث سلوك غير مُعرَّف. كما تسمح بتسمية الدالة constexpr. [ 10 ] التنفيذ المرجعي عبارة عن غلاف حولها std::memcpy. [ 12 ]
استيراد std ؛باستخدام std :: numeric_limits ؛constexpr bool is_negative ( float x ) noexcept { static_assert ( numeric_limits < float >:: is_iec559 ); // (مفعّل فقط على معيار IEEE 754) int32_t i = std :: bit_cast < int32_t > ( x ); return i < 0 ; }تم تضمين آلية أخرى لتورية الأنواع في لغة C++ في الإصدار C++23 مع و . [ 13 ]std::start_lifetime_asstd::start_lifetime_as_array
باسكال
يسمح سجل المتغيرات بالتعامل مع نوع بيانات واحد كأنواع متعددة من البيانات، وذلك بحسب المتغير المُشار إليه. في المثال التالي، يُفترض أن يكون عدد صحيح (intger) مكونًا من 16 بت، بينما يُفترض أن يكون كل من عدد صحيح طويل (longint) وعدد حقيقي (real) مكونين من 32 بت، في حين يُفترض أن يكون الحرف (character) مكونًا من 8 بت.
نوع VariantRecord = سجل حالة RecType : LongInt من 1 : ( I : مصفوفة [ 1 .. 2 ] من Integer ) ; (* غير معروض هنا: يمكن أن يكون هناك عدة متغيرات في عبارة حالة سجل متغير *) 2 : ( L : LongInt ) ; 3 : ( R : Real ) ; 4 : ( C : مصفوفة [ 1 .. 4 ] من Char ) ; نهاية ;var V : VariantRecord ; K : Integer ; LA : LongInt ; RA : Real ; Ch : Character ;V.I [ 1 ] := 1 ; Ch : = V.C [ 1 ] ; ( * سيستخرج هذا البايت الأول من VI *) V.R : = 8.3 ; LA := V.L ; ( * سيخزن هذا عددًا حقيقيًا في عدد صحيح * )في لغة باسكال، يؤدي نسخ عدد حقيقي إلى عدد صحيح إلى تحويله إلى القيمة المقتطعة. هذه الطريقة ستترجم القيمة الثنائية للعدد ذي الفاصلة العائمة إلى ما يمثله كعدد صحيح طويل (32 بت)، وهو ما لن يكون مطابقًا وقد يكون غير متوافق مع قيمة العدد الصحيح الطويل على بعض الأنظمة.
يمكن استخدام هذه الأمثلة لإنشاء تحويلات غير مألوفة، مع ذلك، قد توجد في بعض الحالات استخدامات مشروعة لهذه الأنواع من البنى، مثل تحديد مواقع أجزاء معينة من البيانات. في المثال التالي، يُفترض أن يكون كل من المؤشر والعدد الصحيح الطويل من نوع 32 بت:
النوع PA = ^ Arec ;Arec = record case RT : LongInt of 1 : ( P : PA ) ; 2 : ( L : LongInt ) ; end ;var PP : PA ; K : LongInt ;جديد ( PP ) ؛ PP ^ .P := PP ؛ اكتب سطرًا ( 'المتغير PP موجود في العنوان ' , Hex ( PP ^ .L )) ؛حيث أن "new" هي الدالة القياسية في لغة باسكال لتخصيص الذاكرة لمؤشر، و"hex" هي على الأرجح دالة لطباعة السلسلة السداسية العشرية التي تصف قيمة عدد صحيح. وهذا يسمح بعرض عنوان المؤشر، وهو أمر غير مسموح به عادةً. (لا يمكن قراءة المؤشرات أو الكتابة إليها، بل يمكن فقط تعيين قيم لها). إن تعيين قيمة لمتغير عددي من المؤشر يسمح بفحص أي موقع في ذاكرة النظام أو الكتابة إليه.
PP ^. L := 0 ; PP := PP ^. P ; (* يشير PP الآن إلى العنوان 0 *) K := PP ^. L ; (* يحتوي K على قيمة الكلمة 0 *) WriteLn ( 'الكلمة 0 من هذه الآلة تحتوي على ' , K ) ;قد يتسبب هذا التركيب في حدوث فحص للبرنامج أو انتهاك للحماية إذا كان العنوان 0 محميًا من القراءة على الجهاز الذي يعمل عليه البرنامج أو نظام التشغيل الذي يعمل تحته.
تعمل تقنية إعادة تفسير التحويل من لغة C/C++ أيضًا في لغة باسكال. قد يكون هذا مفيدًا، على سبيل المثال، عند قراءة كلمات مزدوجة من دفق بايتات، ونريد التعامل معها كأعداد عشرية. إليك مثال عملي، حيث نعيد تفسير كلمة مزدوجة وتحويلها إلى عدد عشري:
نوع pReal = ^ Real ;var DW : DWord ; F : Real ;F := pReal ( @ DW ) ^;سي شارب
في لغة C# (وغيرها من لغات .NET)، يكون تحويل الأنواع أكثر صعوبة بعض الشيء بسبب نظام الأنواع، ولكن يمكن القيام بذلك على الرغم من ذلك، باستخدام المؤشرات أو اتحادات البنية.
نصائح
لا تسمح لغة C# إلا بالمؤشرات إلى ما يُسمى بالأنواع الأصلية، أي أي نوع بدائي (باستثناء الأنواع الفرعية string)، أو تعداد، أو مصفوفة، أو بنية تتكون فقط من أنواع أصلية أخرى. يُرجى ملاحظة أن المؤشرات مسموح بها فقط في كتل التعليمات البرمجية المُعلّمة بـ "غير آمنة".
غير آمن { float pi = 3.14159 ; uint piAsRawData = * ( uint * ) & pi ; }النقابات الهيكلية
يُسمح باستخدام الاتحادات الهيكلية دون أي مفهوم للرمز "غير الآمن"، ولكنها تتطلب تعريف نوع جديد.
[StructLayout(LayoutKind.Explicit)] struct FloatAndUIntUnion { [FieldOffset(0)] public float DataAsFloat ;[FieldOffset(0)] public uint DataAsUInt ; }// ...اتحاد FloatAndUInt ؛ union.DataAsFloat = 3.14159 ؛ uint piAsRawData = union.DataAsUInt ؛رمز CIL الخام
يمكن استخدام لغة CIL الخام بدلاً من لغة C#، لأنها لا تحتوي على معظم قيود الأنواع. وهذا يسمح، على سبيل المثال، بدمج قيمتين من نوع تعدادي عام.
TEnum a = ...; TEnum b = ...; TEnum combined = a | b ; // غير مسموحيمكن تجاوز ذلك من خلال رمز CIL التالي:
. method public static hidebysig !! TEnum CombineEnums < valuetype . ctor ([ mscorlib ] System . ValueType ) TEnum > ( !! TEnum a , !! TEnum b ) cil managed { . maxstack 2ldarg .0 ldarg .1 or // لن يتسبب هذا في حدوث تجاوز، لأن a و b لهما نفس النوع، وبالتالي نفس الحجم. ret }يسمح رمزcpblk CIL ببعض الحيل الأخرى، مثل تحويل بنية إلى مصفوفة بايت:
. method public static hidebysig uint8 [] ToByteArray < valuetype . ctor ([ mscorlib ] System . ValueType ) T > ( !! T & v // 'ref T' in C# ) cil managed { . locals init ( [0] uint8 [] ). ماكسستاك 3// إنشاء مصفوفة بايت جديدة بطول sizeof(T) وتخزينها في الموضع المحلي 0 sizeof !! T newarr uint8 dup // الاحتفاظ بنسخة منها على المكدس لاستخدامها لاحقًا (1) stloc .0ldc . i4 .0 ldelema uint8// memcpy(local 0, &v, sizeof(T)); // <لا تزال المصفوفة موجودة على المكدس، انظر (1)> ldarg .0 // هذا هو *عنوان* 'v'، لأن نوعه هو '!!T&' sizeof !! T cpblkldloc .0 ret }جافا
على عكس لغتي C/C++ اللتين توفران الوصول إلى الذاكرة وحسابات المؤشرات، فإن لغة جافا لا تدعم ذلك (رسميًا). ومع ذلك، يمكن محاكاة تغيير أنواع البيانات بطريقة مماثلة.
استخدامjava.nio.ByteBuffer
استيراد java.nio.ByteBuffer ؛void main ( String [] args ) { int value = 42 ;ByteBuffer buffer = ByteBuffer.allocate ( Integer.BYTES ) ; buffer.putInt ( value ) ;// تحويل هذه البيانات إلى عدد عشري (على سبيل المثال فقط) buffer.flip ( ) ; float punResult = buffer.getFloat ( ) ; }استخدامsun.misc.Unsafe
باستخدام sun.misc.Unsafe ، يمكن إجراء تحويل النوع بشكل مباشر، باستخدام الوصول المباشر إلى الذاكرة .
استيراد java.lang.reflect.Field ؛ استيراد sun.misc.Unsafe ؛void main ( String [] args ) throws NoSuchFieldException , IllegalAccessException { Field f = Unsafe . class . getDeclaredField ( "theUnsafe" ); f . setAccessible ( true ); Unsafe unsafe = ( Unsafe ) f . get ( null );long address = unsafe.allocateMemory ( 4 ) ; unsafe.putInt ( address , 42 ) ;// تفسير موقع الذاكرة كعدد عشري float result = unsafe.getFloat ( address ) ; unsafe.freeMemory ( address ) ; }مراجع
- ↑ هيرف، مايكل (ديسمبر 2001). "حيل الجذر" . الرؤية المجسمة : الرسومات .
- ↑ "حيل عائمة غبية" . مدونة تقنية عشوائية من ASCII لبروس داوسون . 24 يناير 2012.
- ↑ ISO/IEC 9899:1999 s6.5/7
- ↑ "§ 6.5/7" (ملف PDF) ، ISO/IEC 9899:2018 ، 2018، صفحة 55، مؤرشف من الأصل (ملف PDF) بتاريخ 30-12-2018،
يجب أن يكون الوصول إلى القيمة المخزنة للكائن مقتصراً على تعبير قيمة يسارية (lvalue) من أحد الأنواع التالية: [...]
- ↑ "أخطاء GCC - مشروع جنو" . gcc.gnu.org .
- ↑ "§ 6.5.2.3/3، الحاشية 97" (ملف PDF) ، ISO/IEC 9899:2018 ، 2018، ص 59، مؤرشف من الأصل (ملف PDF) بتاريخ 30-12-2018.
إذا لم يكن العضو المستخدم لقراءة محتويات كائن الاتحاد هو نفسه العضو المستخدم آخر مرة لتخزين قيمة في الكائن، يُعاد تفسير الجزء المناسب من تمثيل الكائن للقيمة كتمثيل كائن في النوع الجديد كما هو موضح في 6.2.6 (
وهي عملية تُسمى أحيانًا "تلاعب النوع"
).
قد يكون هذا تمثيلًا مُضللًا.
- ↑ "§ J.1/1، النقطة 11" (PDF) ، ISO/IEC 9899:2018 ، 2018، ص 403، مؤرشف من الأصل (PDF) في 2018-12-30،
ما يلي غير محدد: ... قيم البايتات التي تتوافق مع أعضاء الاتحاد
بخلاف العضو الأخير المخزن في
(6.2.6.1).
- ↑ ISO/IEC 14882:2011 القسم 9.5
- ↑ "الأسماء المستعارة في لغتي C و C++" . developer.arm.com . مثال: استخدم memcpy() لنسخ الأنواع بشكل آمن . تم الاطلاع عليه بتاريخ 15 يونيو 2025 .
- 1 2 "تمثيلات الكائنات عبر البث الثنائي" . www.open-std.org . مؤرشف من الأصل بتاريخ 18-08-2018 . تم الاطلاع عليه بتاريخ 10-11-2018 .
- ↑ ريغير، جون (11 يونيو 2013). " التحويل النوعي، والتعريف الصارم، والتحسين" . مُدمج في الأوساط الأكاديمية . تم الاسترجاع في 22 مارس 2026.
كلا المُترجمين يفهمان دالة memcpy بعمق كافٍ لنحصل على رمز الكائن المطلوب [...] أصبح هذا أسلوبي المُفضل للتحويل النوعي قبل بضع سنوات عندما اكتشفت أن المُترجمين يمكنهم تجاوز دالة memcpy وتوليد الرمز الصحيح.
- ↑ "bit_cast/bit_cast.h at master · jfbastien/bit_cast" . GitHub . تم الاطلاع عليه بتاريخ 15 يونيو 2025 .
- ↑ "std::start_lifetime_as, std::start_lifetime_as_array" . cppreference.com . cppreference.com . تم الاطلاع عليه بتاريخ 9 مايو 2026 .
روابط خارجية
- قسم من دليل GCC حول
-fstrict-aliasing، والذي يهزم بعض التورية النوعية - تقرير العيوب رقم 257 لمعيار C99 ، والذي يُعرّف "تورية النوع" عرضًا
union، ويناقش المشكلات المحيطة بالسلوك المحدد بالتنفيذ للمثال الأخير أعلاه - تقرير العيوب رقم 283 بشأن استخدام الاتحادات في التورية النوعية
- بنى البرمجة
