في علم الحاسوب، يعد التنفيذ الرمزي أو التقييم الرمزي وسيلة لتحليل برنامج لتحديد المدخلات التي تتسبب في تنفيذ كل جزء من البرنامج. يتبع المترجم البرنامج، بافتراض قيم رمزية للمدخلات بدلاً من الحصول على مدخلات فعلية كما يفعل التنفيذ العادي للبرنامج. وبالتالي تصل إلى التعبيرات من حيث تلك الرموز للتعبيرات والمتغيرات في البرنامج، والقيود من حيث تلك الرموز للنتائج المحتملة لكل فرع شرطي.
يطبق مجال المحاكاة الرمزية نفس المفهوم على الأجهزة. يطبق الحساب الرمزي المفهوم على تحليل التعبيرات الرياضية.
خذ بعين الاعتبار البرنامج أدناه، الذي يقرأ في قيمة ويفشل إذا كان الإدخال 6.
int f() { ... y = read(); z = y * 2; if (z == 12) { fail(); } else { printf("OK"); } }
أثناء التنفيذ العادي (التنفيذ «الملموس»)، سيقرأ البرنامج قيمة إدخال ملموسة (على سبيل المثال، 5) ويعينها إلى y. سيتابع التنفيذ بعد ذلك عملية الضرب والفرع الشرطي، والذي سيتم تقييمه إلى خطأ ويطبعOK.
OK.
أثناء التنفيذ الرمزي، يقرأ البرنامج قيمة رمزية (على سبيل المثال، λ) ويعينها إلى y. ثم يواصل البرنامج عملية الضرب ويخصص λ * 2 إلى z. عند الوصول إلى جملة if، سيتم تقييم λ * 2 == 12. في هذه المرحلة من البرنامج، يمكن أن تأخذ λ أي قيمة، وبالتالي يمكن أن يستمر التنفيذ الرمزي على طول كلا الفرعين، عن طريق «توجيه» مسارين. يحصل كل مسار على نسخة من حالة البرنامج في تعليمات الفرع بالإضافة إلى قيد المسار.
λ
λ * 2
z
if
λ * 2 == 12
في هذا المثال، قيد المسار هو λ * 2 == 12 للفرع then وλ * 2 != 12 للفرع else. يمكن تنفيذ كلا المسارين بشكل رمزي بشكل مستقل. عندما تنتهي المسارات (على سبيل المثال، نتيجة لتنفيذ fail() أو الخروج ببساطة)، يحسب التنفيذ الرمزي قيمة محددة لـ λ عن طريق حل قيود المسار المتراكمة على كل مسار. يمكن اعتبار هذه القيم الملموسة بمثابة حالات اختبار ملموسة يمكن، على سبيل المثال، مساعدة المطورين على إعادة إنتاج الأخطاء. في هذا المثال، سيحدد خلال القيد أنه من أجل الوصول إلى عبارة fail()، يجب أن λ تساوي 6.
then
λ * 2 != 12
else
fail()
لا يتناسب تنفيذ جميع مسارات البرامج الممكنة بشكل رمزي مع البرامج الكبيرة. يزداد عدد المسارات الممكنة في البرنامج بشكل كبير مع زيادة حجم البرنامج ويمكن أن يكون لانهائي في حالة البرامج ذات التكرارات غير المحدودة.[1] تستخدم حلول مشكلة انفجار المسار بشكل عام إما الاستدلال لإيجاد المسار لزيادة تغطية الشيفرة،[2] أو تقليل وقت التنفيذ عن طريق موازنة المسارات المستقلة،[3] أو عن طريق دمج مسارات مماثلة.[4]
يتم استخدام التنفيذ الرمزي للتفكير في برنامج مسار عن طريق وهو ميزة تفوق التفكير حول إدخال برنامج من خلال الإدخال مثل نماذج الاختبار الأخرى التي تستخدمها (مثل تحليل البرنامج الديناميكي). ومع ذلك، إذا سلك عدد قليل من المدخلات نفس المسار من خلال البرنامج، فلن يكون هناك الكثير من التوفير مقارنة باختبار كل مدخل على حدة.
يكون التنفيذ الرمزي أكثر صعوبة عندما يمكن الوصول إلى موقع الذاكرة نفسه من خلال أسماء مختلفة (استرداف). لا يمكن دائمًا التعرف على الاسم المستعار بشكل ثابت، لذلك لا يمكن لمحرك التنفيذ الرمزي التعرف على أن التغيير في قيمة أحد المتغيرات يغير أيضًا الآخر.[5]
نظرًا لأن المصفوفة عبارة عن مجموعة من العديد من القيم المميزة، يجب على المنفذين الرمزية إما التعامل مع المصفوفة بأكملها كقيمة واحدة أو معاملة كل عنصر من عناصر المصفوفة كموقع منفصل. المشكلة في معالجة كل عنصر من عناصر المصفوفة بشكل منفصل هي أنه يمكن تحديد مرجع مثل "[A [i" ديناميكيًا فقط، عندما تكون قيمة i ذات قيمة محددة.[5]
تتفاعل البرامج مع بيئتها عن طريق إجراء مكالمات النظام أو تلقي الإشارات وما إلى ذلك. قد تنشأ مشاكل الاتساق عندما يصل التنفيذ إلى مكونات لا تخضع لسيطرة أداة التنفيذ الرمزية (على سبيل المثال، النواة أو المكتبات). خذ بعين الاعتبار المثال التالي:
int main() { FILE *fp = fopen("doc.txt"); ... if (condition) { fputs("some data", fp); } else { fputs("some other data", fp); } ... data = fgets(..., fp); }
يفتح هذا البرنامج ملفًا، وبناءً على بعض الشروط، يكتب نوعًا مختلفًا من البيانات إلى الملف. ثم يقرأ البيانات المكتوبة لاحقًا. من الناحية النظرية، سوف ينفذ التنفيذ الرمزي مسارين عند السطر 5 وسيكون لكل مسار من هناك نسخة خاصة به من الملف. وبالتالي، فإن العبارة الموجودة في السطر 11 ستعيد البيانات التي تتوافق مع قيمة «الشرط» في السطر 5. من الناحية العملية، يتم تنفيذ عمليات الملف كمكالمات النظام في النواة، وهي خارجة عن سيطرة أداة التنفيذ الرمزية. الطرق الرئيسية لمواجهة هذا التحدي هي:
تنفيذ المكالمات إلى البيئة مباشرة. ميزة هذا النهج هو أنه سهل التطبيق. العيب هو أن الآثار الجانبية لمثل هذه المكالمات سوف تفسد جميع الحالات التي يديرها محرك التنفيذ الرمزي. في المثال أعلاه، ستعرض التعليمات الموجودة في السطر 11 «بعض البيانات الأخرى للبيانات» اعتمادًا على الترتيب التسلسلي للحالات.
نمذجة البيئة. في هذه الحالة، يقوم المحرك باستدعاء النظام باستخدام نموذج يحاكي آثارها ويحافظ على جميع الآثار الجانبية في التخزين لكل حالة. الميزة هي أن المرء سيحصل على نتائج صحيحة عند تنفيذ البرامج التي تتفاعل مع البيئة بشكل رمزي. العيب هو أن المرء يحتاج إلى تنفيذ والحفاظ على العديد من النماذج التي يحتمل أن تكون معقدة من مكالمات النظام. تستخدم أدوات مثل KLEE،[6] وCloud9 وأخرى.[7] هذا النهج من خلال تطبيق نماذج لعمليات نظام الملفات والمقابس وIPC، إلخ.
تقوية حالة النظام بالكامل. تحل أدوات التنفيذ الرمزية القائمة على الأجهزة الافتراضية مشكلة البيئة عن طريق التلاعب بحالة الآلة الافتراضية بأكملها. على سبيل المثال، في S2E، كل حالة هي لقطة آلة افتراضية مستقلة يمكن تنفيذها بشكل منفصل. يخفف هذا النهج من الحاجة إلى كتابة النماذج المعقدة وصيانتها ويسمح بتنفيذ أي برنامج ثنائي بشكل رمزي. ومع ذلك، فإنه يحتوي على تكلفة غير مباشرة لاستخدام ذاكرة أعلى (قد تكون لقطات آلة افتراضية كبيرة).
تم تقديم مفهوم التنفيذ الرمزي أكاديميًا مع وصف: نظام Select،[9] ونظام EFFIGY،[10] ونظام DISSECT،[11] ونظام Clarke.[12] انظر فهرس يضم المزيد من الأوراق الفنية المنشورة حول التنفيذ الرمزي.
Lokasi Pengunjung: 18.119.125.49