ابدأ في جمع الأخطاء في وظائف النسخ

memcpy

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

ظاهرة Baadera-Meinhof؟ لا انا لا اعتقد ذلك


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

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

قد يجادل شخص ما في حالتين - هذه ليست انتظام. وذلك ، ربما ، لفتت الانتباه إليهم لمجرد أنهم التقوا بي بعد فترة قصيرة من الوقت وأثاروا ظاهرة Baader-Meinhof .

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

أعتقد أن الأمر ليس كذلك. كان لدي بالفعل خبرة مثل هذه الملاحظة حول وظائف المقارنة ، والتي تم تأكيدها بعد ذلك من خلال المادة التي تم جمعها: " يعيش الشر في وظائف المقارنة ".

حسنًا ، دعنا نصل إلى النقطة. المقدمة من أجل إعطاء مثالين فقط حتى الآن كانت طويلة للغاية :).

مثال N1


في المقالة حول التحقق من Zephyr RTOS ، وصفت مثل هذه المحاولة الفاشلة لتطبيق وظيفة strdup التناظرية :

static char *mntpt_prepare(char *mntpt)
{
  char *cpy_mntpt;

  cpy_mntpt = k_malloc(strlen(mntpt) + 1);
  if (cpy_mntpt) {
    ((u8_t *)mntpt)[strlen(mntpt)] = '\0';
    memcpy(cpy_mntpt, mntpt, strlen(mntpt));
  }
  return cpy_mntpt;
}

تحذير PVS-Studio: V575 [CWE-628] لا تقوم وظيفة "memcpy" بنسخ السلسلة بأكملها. استخدم الدالة 'strcpy / strcpy_s' للحفاظ على القيمة الطرفية فارغة. shell.c 427 أفاد

المحلل أن وظيفة memcpy تنسخ السطر ، لكنها لا تنسخ المطراف صفر ، وهذا أمر مريب للغاية. يبدو أنه تم نسخ هذا الطرف 0 هنا:

((u8_t *)mntpt)[strlen(mntpt)] = '\0';

لا ، يوجد خطأ مطبعي هنا ، حيث يتم نسخ المحطة صفر لنفسها. لاحظ أن الكتابة إلى مصفوفة mntpt ، وليس cpy_mntpt . نتيجة لذلك ، ترجع الدالة mntpt_prepare سلسلة غير كاملة مع صفر طرفية.

في الحقيقة ، أراد المبرمج أن يكتب هكذا:

((u8_t *)cpy_mntpt)[strlen(mntpt)] = '\0';

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

static char *mntpt_prepare(char *mntpt)
{
  char *cpy_mntpt;

  cpy_mntpt = k_malloc(strlen(mntpt) + 1);
  if (cpy_mntpt) {
    strcpy(cpy_mntpt, mntpt);
  }
  return cpy_mntpt;
}

مثال N2


void myMemCpy(void *dest, void *src, size_t n) 
{ 
   char *csrc = (char *)src; 
   char *cdest = (char *)dest; 
   for (int i=0; i<n; i++) 
     cdest[i] = csrc[i]; 
}

لم نحدد هذا الرمز بأنفسنا باستخدام PVS-Studio ، لكني التقيت به عن طريق الخطأ على موقع StackOverflow: C وتحليل الشفرة الثابتة: هل هذا أكثر أمانًا من memcpy؟

ومع ذلك ، إذا تحققت من هذه الوظيفة باستخدام محلل PVS-Studio ، فسوف يلاحظ حقًا:

  • V104 التحويل الضمني لـ 'i' لنوع الذاكرة في تعبير حسابي: i <n test.cpp 26
  • V108 نوع فهرس غير صحيح: cdest [ليس من نوع memsize]. استخدم نوع memsize بدلاً من ذلك. test.cpp 27
  • V108 نوع فهرس غير صحيح: csrc [ليس من نوع memsize]. استخدم نوع memsize بدلاً من ذلك. test.cpp 27

في الواقع ، يحتوي هذا الرمز على خلل ، كما هو موضح في إجابات StackOverflow. لا يمكنك استخدام متغير int كفهرس . في برنامج 64 بت ، يكاد يكون من المؤكد (لا نأخذ في الاعتبار البنيات الغريبة) ، سيكون المتغير int 32 بت ولن تكون الوظيفة قادرة على نسخ أكثر من INT_MAX بايت. أولئك. لا يزيد عن 2 غيغابايت.

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

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

استنتاج


حسنًا ، أنا فقط في بداية الرحلة ، وربما يستغرق الأمر أكثر من عام واحد قبل أن أجمع المواد لنشرها بشكل شامل حول هذا الموضوع. في الواقع ، الآن فقط سأبدأ في كتابة مثل هذه الحالات. شكرًا لك على اهتمامك وانظر إلى ما هو مثير للاهتمام سيجده محلل PVS-Studio في كود C / C ++ / C # / Java.



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

All Articles