خطأ في تجزئة الذاكرة

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

تُعدّ أخطاء تجزئة الذاكرة نوعًا شائعًا من الأخطاء في البرامج المكتوبة بلغات تدعم المؤشرات التي يمكن أن تكون فارغة (null) ، أو التي يمكن تعيينها إلى قيمة عشوائية، أو التي تدعم المصفوفات ، والتي لا تُجرى فيها سوى عمليات فحص قليلة للذاكرة أو لا تُجرى على الإطلاق. وتنشأ هذه الأخطاء في المقام الأول نتيجةً لأخطاء في استخدام المؤشرات لعنونة الذاكرة الافتراضية . وهناك نوع آخر من أخطاء الوصول إلى الذاكرة وهو خطأ ناقل البيانات ، والذي له أيضًا أسباب متعددة، ولكنه نادر الحدوث اليوم؛ ويحدث هذا الخطأ في المقام الأول نتيجةً لعنونة الذاكرة الفعلية غير الصحيحة، أو نتيجةً للوصول غير المتوافق إلى الذاكرة - أي مراجع ذاكرة لا يمكن للأجهزة الوصول إليها، وليست مراجع لا يُسمح للعملية بالوصول إليها.

تتضمن العديد من لغات البرمجة آليات مصممة لمساعدة المبرمجين على تجنب أخطاء تجزئة الذاكرة في برامجهم وتحسين فحص الأجهزة التي تتمتع بحماية الذاكرة. على سبيل المثال، تستخدم لغة Rust نموذجًا قائمًا على الملكية [ 2 ] لمحاولة ضمان فحص سلامة الذاكرة. [ 3 ] وتستخدم لغات أخرى، مثل Lisp و Java ، آلية جمع البيانات المهملة [ 4 التي تتجنب أنواعًا معينة من أخطاء الذاكرة التي قد تؤدي إلى أخطاء تجزئة الذاكرة. [ 5 ]

ملخص

مثال على إشارة من صنع الإنسان
خطأ في تجزئة الذاكرة يؤثر على برنامج كريتا في بيئة سطح المكتب KDE
خطأ في الوصول إلى مؤشر فارغ في نظام التشغيل ويندوز 8

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

يُستخدم مصطلح "التجزئة" في الحوسبة لأغراضٍ متعددة؛ ففي سياق "خطأ التجزئة"، يُشير إلى مساحة عناوين البرنامج . [ 6 ] مع حماية الذاكرة، لا يُمكن قراءة سوى مساحة عناوين البرنامج نفسه، ومن هذه المساحة، لا يُمكن الكتابة إلا إلى المكدس وجزء القراءة/الكتابة من مقطع البيانات ، بينما لا يُمكن الكتابة إلى البيانات المخصصة للقراءة فقط في مقطع الثوابت ومقطع التعليمات البرمجية . وبالتالي، فإن محاولة القراءة من خارج مساحة عناوين البرنامج، أو الكتابة إلى جزء مخصص للقراءة فقط من مساحة العناوين، يُعد انتهاكًا لتجزئة البرنامج، ومن هنا جاءت التسمية.

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

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

على مستوى نظام التشغيل، يتم رصد هذا الخطأ وإرسال إشارة إلى العملية المُسببة له، مما يُفعّل مُعالج الإشارة الخاص بها. تختلف أسماء الإشارات المُستخدمة للدلالة على حدوث خطأ تجزئة الذاكرة باختلاف أنظمة التشغيل. في أنظمة التشغيل الشبيهة بنظام يونكس ، تُرسل إشارة تُسمى SIGSEGV(اختصارًا لـ segmentation violate ) إلى العملية المُسببة للخطأ. أما في نظام مايكروسوفت ويندوز ، فتتلقى العملية المُسببة للخطأ STATUS_ACCESS_VIOLATIONاستثناءً .

الأسباب

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

فيما يلي بعض الأسباب الشائعة لحدوث خطأ في تجزئة الذاكرة:

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

وغالباً ما يكون سبب ذلك أخطاء برمجية تؤدي إلى وصول غير صالح إلى الذاكرة:

  • إلغاء مرجعية مؤشر فارغ ، والذي يشير عادةً إلى عنوان ليس جزءًا من مساحة عناوين العملية.
  • إلغاء مرجعية أو تعيين قيمة لمؤشر غير مهيأ ( مؤشر جامح ، والذي يشير إلى عنوان ذاكرة عشوائي)
  • إلغاء المرجعية أو التعيين إلى مؤشر تم تحريره ( مؤشر معلق ، والذي يشير إلى ذاكرة تم تحريرها/إلغاء تخصيصها/حذفها)
  • تجاوز سعة المخزن المؤقت
  • موقع Stack Overflow
  • محاولة تشغيل برنامج لا يتم تجميعه بشكل صحيح. (بعض المترجمات تُخرج ملفًا تنفيذيًا على الرغم من وجود أخطاء وقت التجميع.)

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

char * p1 = NULL ; // مؤشر فارغ char * p2 ; // مؤشر غير مهيأ: لم تتم تهيئته إطلاقًا. char * p3 = ( char * ) malloc ( 10 * sizeof ( char )); // مؤشر مهيأ إلى الذاكرة المخصصة (بافتراض أن malloc لم تفشل)free ( p3 ); // أصبح p3 الآن مؤشرًا معلقًا، حيث تم تحرير الذاكرة.

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

التعامل

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

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

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

أمثلة

خطأ في تجزئة الذاكرة على لوحة مفاتيح EMV

الكتابة إلى ذاكرة للقراءة فقط

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

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

int main ( void ) { char * s = "hello world" ; * s = 'H' ; }

عند تجميع البرنامج الذي يحتوي على هذا الكود، تُوضع السلسلة النصية "hello world" في قسم البيانات للقراءة فقط (rodata) من ملف البرنامج القابل للتنفيذ . عند تحميل البرنامج، يضعه نظام التشغيل مع سلاسل نصية أخرى وبيانات ثابتة في قسم للقراءة فقط من الذاكرة. عند التنفيذ، يُعيّن متغيرٌ للإشارة إلى موقع السلسلة النصية، وتُجرى محاولة لكتابة الحرف H من خلال هذا المتغير في الذاكرة، مما يتسبب في خطأ تجزئة الذاكرة. يؤدي تجميع هذا البرنامج باستخدام مُجمِّع لا يتحقق من تعيين مواقع للقراءة فقط أثناء التجميع، وتشغيله على نظام تشغيل شبيه بنظام يونكس، إلى ظهور خطأ وقت التشغيل التالي :s

$ gcc segfault.c -g -o segfault $ ./segfault خطأ في تجزئة الذاكرة

تتبع مسار الملف الأساسي من GDB :

تلقى البرنامج إشارة SIGSEGV ، خطأ في تجزئة الذاكرة . 0x1c0005c2 في الدالة الرئيسية () في الملف segfault.c : 6 6 * s = 'H' ;

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

char s [] = "hello world" ; s [ 0 ] = 'H' ; // أو ما يعادله، *s = 'H';

على الرغم من أنه لا ينبغي تعديل القيم النصية الحرفية (هذا له سلوك غير محدد في معيار C)، إلا أنها في C من static char[]النوع [ 11 ] [ 12 ] [ 13 لذلك لا يوجد تحويل ضمني في الكود الأصلي (الذي يشير إلى char*تلك المصفوفة)، بينما في C++ فهي من static const char[]النوع، وبالتالي يوجد تحويل ضمني، لذلك ستكتشف المترجمات هذا الخطأ المحدد بشكل عام.

محاولة الوصول إلى مؤشر فارغ

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

int * ptr = NULL ; printf ( "%d" , * ptr );

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

يؤدي فك مرجعية مؤشر فارغ ثم إسناد قيمة إليه (كتابة قيمة إلى هدف غير موجود) عادةً إلى حدوث خطأ في تجزئة الذاكرة:

int * ptr = NULL ; * ptr = 1 ;

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

int * ptr = NULL ; * ptr ;

تجاوز سعة المخزن المؤقت

يحاول الكود التالي الوصول إلى مصفوفة الأحرف sخارج حدودها العليا. قد يؤدي ذلك، بحسب المُصرّف والمعالج، إلى خطأ في تجزئة الذاكرة.

char s [] = "hello world" ; char c = s [ 20 ];

موقع Stack Overflow

مثال آخر هو الاستدعاء الذاتي بدون حالة أساسية:

int main ( void ) { return main (); }

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

انظر أيضاً

مراجع

  1. برمجة C الخبراء: أسرار C العميقة بقلم بيتر فان دير ليندن، صفحة 188
  2. "لغة برمجة Rust - الملكية" .
  3. "التزامن الجريء مع لغة Rust - مدونة لغة برمجة Rust" .
  4. مكارثي، جون (أبريل 1960). "الدوال التكرارية للتعبيرات الرمزية وحسابها آليًا، الجزء الأول" . مجلة اتصالات رابطة مكائن ​​الحوسبة . 4 (3): 184-195 . doi : 10.1145/367177.367199 . S2CID 1489409. تاريخ الاسترجاع: 22 سبتمبر 2018 . 
  5. دهورجاتي، ديناكار؛ كوشيك، سومانت؛ أدفي، فيكرام؛ لاتنر، كريس (1 يناير 2003). "سلامة الذاكرة بدون فحوصات وقت التشغيل أو جمع البيانات المهملة" (ملف PDF) . وقائع مؤتمر ACM SIGPLAN لعام 2003 حول اللغة والمترجم والأداة للأنظمة المدمجة . المجلد 38. ACM. الصفحات 69-80 . doi : 10.1145/780732.780743 . ISBN   1581136471. S2CID 1459540 . تم الاسترجاع بتاريخ 22-09-2018 . 
  6. "تصحيح أخطاء تجزئة الذاكرة ومشاكل المؤشرات - Cprogramming.com" . www.cprogramming.com . تاريخ الاسترجاع: 2021-02-03 .
  7. "التعافي السليم من أخطاء تجزئة الذاكرة في أنظمة ويندوز ولينكس (32 بت، x86)" . تم الاطلاع عليه بتاريخ 23 أغسطس 2020 .
  8. "تنفيذ معالج SIGSEGV/SIGABRT الذي يطبع تتبع مكدس التصحيح" . GitHub . تم الاسترجاع في 23 أغسطس 2020 .
  9. "كيفية تحديد عمليات القراءة أو الكتابة لخطأ الصفحة عند استخدام معالج sigaction على SIGSEGV؟ (لينكس)" . تم الاطلاع عليه بتاريخ 23 أغسطس 2020 .
  10. "لينكس - كتابة معالجات الأخطاء" . 12 نوفمبر 2017. تم الاطلاع عليه بتاريخ 23 أغسطس 2020 .
  11. "6.1.4 السلاسل النصية الحرفية". ISO/IEC 9899:1990 - لغات البرمجة -- C.
  12. "6.4.5 السلاسل النصية الحرفية". ISO/IEC 9899:1999 - لغات البرمجة -- C.
  13. "6.4.5 السلاسل النصية الحرفية". ISO/IEC 9899:2011 - لغات البرمجة -- C.
  14. "ما الفرق بين خطأ تجزئة الذاكرة وتجاوز سعة المكدس؟" . موقع Stack Overflow . تم الاطلاع عليه بتاريخ 11 نوفمبر 2023 .