سؤال

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

#include <stdio.h>

int square(int val){
  return val*val;
}

void printit(void* ptr){
  int (*fptr)(int,int,int) = (int (*)(int,int,int)) (ptr);
  printf("Call function with parameters 2,4,8.\n");
  printf("Result: %d\n", fptr(2,4,8));
}


int main(void)
{
    printit(square);
    return 0;
}

هذا يجمع ويعمل بدون أخطاء أو تحذيرات (GCC -Wall على Linux / X86). الإخراج على نظامي هو:

Call function with parameters 2,4,8.
Result: 4

لذلك يبدو أن الحجج الزائدة غير ضرورية يتم تجاهلها بصمت.

الآن أود أن أفهم ما يحدث بالفعل هنا.

  1. بالنسبة للشرعية: إذا فهمت الإجابة على إلقاء مؤشر وظيفة إلى نوع آخر بشكل صحيح ، هذا ببساطة سلوك غير محدد. لذا فإن حقيقة أن هذا يمتد وينتج نتيجة معقولة هو مجرد حظ نقي ، صحيح؟ (أو اللطف من جانب كتاب المترجم)
  2. لماذا لن تحذرني GCC من هذا ، حتى مع Wall؟ هل هذا شيء لا يستطيع التحويل البرمجي اكتشافه؟ لماذا ا؟

أنا قادم من Java ، حيث يكون TypeChecking أكثر صرامة ، لذا فإن هذا السلوك أربكني قليلاً. ربما أعاني من صدمة ثقافية :-).

هل كانت مفيدة؟

المحلول

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

حقيقة أن هذه الدعوة عملت هي الحظ الخالص ، بناءً على الحقائق:

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

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

نصائح أخرى

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

  1. نعم ، إنه سلوك غير محدد - يمكن أن يحدث أي شيء ، بما في ذلك يبدو أنه "يعمل".

  2. يمنع فريق العمل المترجم من إصدار تحذير. أيضا ، لا يوجد المترجمون تحت أي شرط لتشخيص الأسباب المحتملة السلوك غير المحدد. والسبب في ذلك هو أنه إما أنه من المستحيل القيام بذلك ، أو أن القيام بذلك سيكون صعبًا للغاية و/أو يسبب الكثير من النفقات العامة.

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

قد تواجه مؤشرات مختلفة الحجم بسهولة على منصات مضمنة. حتى أن هناك معالجات حيث تتناول مؤشرات البيانات ومؤشر الوظيفة أشياء مختلفة (ذاكرة الوصول العشوائي لأحدها ، ROM للآخر) ، ما يسمى بنية هارفارد. في x86 في الوضع الحقيقي ، يمكنك أن يكون لديك 16 بت و 32 بت مختلطة. كان لدى Watcom-C وضع خاص لـ DOS Extender حيث كانت مؤشرات البيانات عرضة 48 بت. لا سيما مع C يجب أن يعرف المرء أنه ليس كل شيء هو posix ، لأن C قد تكون اللغة الوحيدة المتاحة على الأجهزة الغريبة.

تسمح بعض المترجمين بنماذج ذاكرة مختلطة حيث يتم ضمان حجم الكود في حدود 32 بت ، ويمكن معالجة البيانات مع 64 بت مؤشرات ، أو العكس.

يحرر: الخلاصة ، لا تلقي مؤشر البيانات على مؤشر الوظيفة.

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

int ( __cdecl * fptr)(int,int,int) = (int (__cdecl * ) (int,int,int)) (ptr);

يتم إنشاء التجميع التالي للمكالمة:

push        8
push        4
push        2
call        dword ptr [ebp-4]
add         esp,0Ch

لاحظ إضافة 12 بايت (0ch) إلى المكدس بعد المكالمة. بعد ذلك ، يكون المكدس جيدًا (على افتراض أن Callee هو __cdecl في هذه الحالة ، لذلك لا يحاول أيضًا تنظيف المكدس). ولكن مع الكود التالي:

int ( __stdcall * fptr)(int,int,int) = (int (__stdcall * ) (int,int,int)) (ptr);

ال add esp,0Ch لا يتم إنشاؤه في التجميع. إذا كان Callee __cdecl في هذه الحالة ، فسيتم تالف المكدس.

  1. من المؤكد أنني لا أعرف بالتأكيد ، لكنك بالتأكيد لا تريد الاستفادة من السلوك إذا كان الحظ أو إذا كان برنامج التحويل البرمجي محدد.

  2. لا يستحق التحذير لأن الممثلين صريح. من خلال الإلقاء ، أنت تُعلم المترجم أنك تعرف أفضل. على وجه الخصوص ، أنت تلقي void*, ، وعلى هذا النحو ، فأنت تقول "خذ العنوان الذي يمثله هذا المؤشر ، وجعله نفس الشيء مثل هذا المؤشر الآخر" - يقوم الممثلون ببساطة بإبلاغ المترجم بأنك متأكد من ما هو في العنوان المستهدف ، في الواقع ، نفس الشيء. على الرغم من أننا هنا نعلم أن هذا غير صحيح.

يجب أن أقوم بتحديث ذاكرتي عن التصميم الثنائي لاتفاقية الاتصال C في مرحلة ما ، لكنني متأكد تمامًا من أن هذا ما يحدث:

  • 1: إنه ليس حظًا خالصًا. إن اتفاقية الاتصال C محددة جيدًا ، وبيانات إضافية على المكدس ليست عاملاً لموقع الاتصال ، على الرغم من أنه قد يتم الكتابة فوقها من قبل Callee لأن Callee لا يعرف ذلك.
  • 2: يلقي "صعب" ، باستخدام قوسين ، يخبر المترجم أنك تعرف ما تفعله. نظرًا لأن جميع البيانات المطلوبة في وحدة تجميع واحدة ، فقد يكون المترجم ذكيًا بما يكفي لمعرفة أن هذا أمر غير قانوني بشكل واضح ، لكن مصمم (مصمم) C لم يركز على اصطياد حالة الزاوية غير المحتملة. ببساطة ، يثق المترجم في أنك تعرف ما تفعله (ربما بشكل غير مقبول في حالة العديد من مبرمجي C/C ++!)

للإجابة على أسئلتكم:

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

  2. لا يوجد تحذير لأنك كنت تستخدم void * مؤشر وتلقي. هذا رمز مشروع في عيون المترجم ، حتى لو كنت قد تم تحديده بشكل صريح -Wall تحول. يفترض المترجم أنك تعرف ما تفعله! هذا هو السر.

أتمنى أن يساعد هذا ، مع أطيب التحيات ، توم.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top