الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

تتبع إستدعاء الدوال

بدأه C++er في 7 أبريل 2011 · 0 رد · 583 مشاهدة · في لغة C و ++C
مشاركة: واتساب X فيسبوك تيليجرام
#1

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

بعد تجربة بعض الأفكار قمت ببرمجة واحده و هي سهله التعامل و أيضا تأخذ فى الحسبان multi-thread و يمكنك استخدامها مع اى مترجم و لأى نظام تشغيل ترغب و التعديلات التى يمكن اجرائها بها شبه معدومه.

الفكره كلها تتلخص فى وجود linked list تحتوى على البيانات الخاصه بالإستدعاء الحالي و بعض البيانات الأخرى و شكلها كالتالي:

// call information doubly linked list
struct CALLINFO
{
	void* class_instance; // instance address of current method call
	void* method_address; // method address
	void* param_list;     // first parameter address
	int   call_level;  // current method call level
	const char* method_name; // current method name
	CALLINFO* prev_call;  // previous call information
	CALLINFO* next_call;  // next call information
};

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

العنصر الثاني method_address هو عنوان الداله الحاليه (التى يتم تنفيذ الكود الخاص بها الأن).

العنصر الثالث param_list هو عنوان اول معامل تم ادخاله للداله، إن كانت الداله لا تحتوى على معاملات فقيمته NULL.

العنصر الرابع call_level هو ترتيب استدعاء هذه الداله بالنسبه للداله main بمعنى إذا كان مستوى استدعاء الداله main هو صفر فما هو رقم استدعاء الداله الحاليه؟ هذا العنصر يجيب على هذا السؤال.

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

العنصر قبل الأخير prev_call هو بيانات الداله التى قامت بإستدعاء الداله الحاليه.

العنصر الأخير next_call هو بيانات الداله التى تلي فى شجرة الإستدعاءات الداله الحاليه، قد تتسأل كيف يوجد داله تاليه فى الإستدعاء للحاليه، اليس من الطبيعى ان الداله الحاليه هى اخر داله فى شجرة الإستدعاء؟ كلامك صحيح و هذا المتغير يستخدم لغرض أخر مثلا تخيل ترتيب استدعاء الداله الحاليه هو 15 و تريد الوصول للداله ذات الترتيب 3 و بعد ان وصلت لها تريد الوصول للداله ذات الترتيب 4 فماذا ستفعل؟ هذا العنصر هو ما يتيح هذه الإمكانيه.

قمت ببناء مجموعه من الدوال و الماكرو للقيام بأغلب ما تريد و ذلك حتي تتجنب التعامل المباشر مع هذا التركيب.

لنبدأ مع الدوال التى تقوم بإعداد هذا التركيب و التعامل معه:

// push current call data (thread safe)
void push_call(void* inst, void* method, void* param, const char* name);

// pop current call (thread safe)
void pop_call();

// get instance address of current method call
void* get_class_instance();

// get current method address
void* get_method_address();

// get first parameter address
void* get_param_list();

// get current call level
int get_current_level();

// get current method name
const char* get_method_name();

// get current call information
const CALLINFO* get_current_call();

// get previous call information
const CALLINFO* get_prev_call();

// get next call information
const CALLINFO* get_next_call();

// get call information for specified level
const CALLINFO* get_level_info(int level);

// print call tree to current level for current thread to output stream
void print_call_tree();

الداله push_call تقوم بحفظ بيانات الإستدعاء الحالي داخل linked list (سيتم ذكرها لاحقا).

الداله pop_call تقوم بحذف بيانات الإستدعاء الحالي من داخل linked list (سيتم ذكرها لاحقا).

الدوال get_class_instance، get_method_address، get_param_list، get_current_level، get_method_name، get_prev_callget_next_call هما فقط accessors لعناصر التركيب.

الداله get_current_call تعود لك بنسخه بيانات الإستدعاء الحالي و هى غير قابله للتعديل. (لماذا قد يريد أحد تعديل مثل هذا البيان!)

الداله get_level_info تقوم بتمرير لها رقم مستوى معين للإستدعاء و ستعود ببيانات الإستدعاء الموجود بهذا المستوى.

الداله print_call_tree تطبع لك شجرة الإستدعاءات حتى الداله الحاليه بالشكل التالي:

main
- m1
-- m2
--- m3
---- m4

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

الدوال السابقه قد تقوم بإستدعائها بشكل مباشر او قد تستخدم الماكرو التى تقوم بتمرير المعاملات عوضا عنك (أيضا إن احتجت يوما تغيير اسماء الدوال فلن تصبح مشكله حيث انك تستخدم الماكرو):

#define PUSH_CURRENT( INSTANCE, METHOD, PARAMS ) push_call(INSTANCE, METHOD, PARAMS, #METHOD);

#define POP_CURRENT pop_call();

#define CALL_RETURN(VALUE) POP_CURRENT return (VALUE);

#define CALL_RET_NONE POP_CURRENT return;

#define CALL_RET_VALUE(VALUE) POP_CURRENT return (VALUE);

#define GET_METHOD_ADDRESS (get_method_address())

#define GET_PARAM_ADDRESS (get_param_list())

#define GET_CURRENT_LEVEL (get_current_level())

#define GET_METHOD_NAME (get_method_name())

#define GET_CURRENT_CALL (get_current_call())

#define GET_PREVIOUS_CALL (get_prev_call())

#define GET_NEXT_CALL (get_next_call())

#define GET_CALLINFO_BY_LEVEL(LEVEL) (get_level_info(LEVEL))

#define PRINT_CALL_TREE (print_call_tree())

كما ترى فى الماكرو الأول انت تقوم بتمرير اسم الداله بنفسك و انا اقوم بتحويلها لصيغه نصيه بإستخدام #.

داخل دوالك و قبل اى عمليه تقوم بإستدعاء الماكرو PUSH_CURRENT حيث المعامل الأول هو this إن كان موجود أو NULL و المعامل الثاني هو عنوان الداله الحاليه (اسم الداله بدون علامات تنصيص) و المعامل الأخير هو عنوان أول معامل بالداله.

إذا كانت دالتك تعيد قيمه معينه حينها قم بإعادتها بإستخدام الماكرو CALL_RET_VALUE و لا تستخدم return و ذلك لأنك إن لم تستخدم ذلك الماكرو فستتلف شجرة الإستدعاء، إذا كانت دالتك لا تعيد قيمه و تريد الخروج بإستخدام return حينها استدعي الماكرو CALL_RET_NONE، إذا كانت دالتك ستنتهي بشكل طبيعي (أى سيتم تنفيذ أخر سطر فيها) حينها اجعل اخر سطر بالداله لديك هو استدعاء للماكرو POP_CURRENT.

عموما لا تخرج من الداله أبدا قبل أن تستدعي الماكرو POP_CURRENT و ذلك ينطبق أيضا على رمي الإستثناءات.

الشكل المثالي للداله التى تستخدم هذه الماكرو هو:

int test_with_return(int value)
{
  PUSH_CURRENT( NULL, test_with_return, &value );

  if (value < 0) CALL_RET_VALUE(-1);

  if (value > 0xFFFF) CALL_RET_VALUE(0xFFFF);

  CALL_RET_VALUE( 0xFFFF - value );
}

void test_without_return(int value)
{
  PUSH_CURRENT( NULL, test_with_return, NULL );

  if (value < 0) CALL_RET_NONE;

  if (value > 0xFFFF) CALL_RET_NONE;

  POP_CURRENT;
}

لنلقي نظره على كود الدوال السابقه، فى البدايه ستجد لديك المتغير last_call و هو مؤشر لبيانات أخر استدعاء، هذا المتغير يتم تعديل قيمتع بإستخدام الدالتين push_call و pop_call و شكلهم كالتالي:

CALLINFO* last_call = NULL;

void push_call(void* inst, void* method, void* param, const char* name)
{
    CALLINFO* call = (CALLINFO*)malloc(sizeof(CALLINFO));

    call->class_instance = inst;
    call->method_address = method;
    call->param_list     = param;
    call->method_name    = name;
    call->next_call      = NULL;
    call->prev_call      = last_call;
    call->call_level     = last_call? last_call->call_level+1: 0;

    if (last_call) last_call->next_call = call;

    last_call = call;
}

void pop_call()
{
    if (!last_call) return;

    CALLINFO* prev = last_call->prev_call;

    free(last_call);

    last_call = prev;
}

الداله push_call تقوم بإنشاء بيانات الإستدعاء (يمكنك اعتبار أن بيان الإستدعاء يحاكي الـ call frame) و يقوم بملء بياناته، ايضا يقوم بتحديث قيمة prev_call للإستدعاء الحالي و next_call لكل من الإستدعاء الحالي و السابق.

الداله pop_call تقوم بتحرير بيانات الإستدعاء التى موقعها مسجل بالمتغير last_call و قبل تنفيذ هذه العمليه يتم تخزين موقع الإستدعاء السابق (أو بمعنى أخر الـ return address) و ذلك لتخزينه فيما بعد داخل المتغير last_call. الهدف من هذه العمليه هو انك عندما تعود من الداله الحاليه للتى قامت بإستدعائها تجد هناك البيانات الصحيحه لهذه الداله (الداله التى قامت بالإستدعاء).

الدوال التاليه تقوم بالعوده بقيمة عناصر التركيب فقط:

void* get_class_instance() { return last_call? last_call->class_instance: NULL; }

void* get_method_address() { return last_call? last_call->method_address: NULL; }

void* get_param_list() { return last_call? last_call->param_list: NULL; }

int get_current_level() { return last_call? last_call->call_level: -1; }

const char* get_method_name() { return last_call? last_call->method_name: NULL; }

const CALLINFO* get_current_call() { return last_call; }

const CALLINFO* get_prev_call() { return last_call? last_call->prev_call: NULL; }

const CALLINFO* get_next_call() { return last_call? last_call->next_call: NULL; }

الداله get_level_info شكلها كالتالي:

const CALLINFO* get_level_info(int level)
{
	if (level < 0) return NULL;

	if (level > last_call->call_level) return NULL;

	CALLINFO* temp_call = last_call;

	level = last_call->call_level - level;

	while(level--) temp_call = temp_call->prev_call;

	return temp_call;
}

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

الداله print_call_tree كودها كالتالي:

void print_call_tree()
{
	if (!last_call) return;

	int level = last_call->call_level;

	CALLINFO* root = last_call;

	while(level--) root = root->prev_call;

	while(root)
	{
		level++;
		for(int i = 0; i < level; i++) putc('-', stdout);
		printf(" %s\n", root->method_name);
		root = root->next_call;
	}
}

كل ما يحدث فيها هو انى اقوم بالحصول على دالة الإستدعاء فى المستوي صفر و منها اقوم بطباعة بيانات الإستدعاء و فى كل مره اقوم بطباعة عدد "-"، هذا العدد يساوى مستوى الإستدعاء للداله فمثلا إذا كانت الداله x مستوى الإستدعاء لها هو 7 فستجد انه يوجد 7 علامات قبل إسم الداله (x فى هذه الحاله).

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

المشكله التى ستواجهنا عندما تعمل الدوال السابقه فى نطاق الـ multi-thread هو أن المتغير last_call سيكون مشترك بين جميع الـ threads و بالتالي بيانات الإستدعاء ستتلف بكل تأكيد و لفصل بيانات استدعاء كل thread عن الأخر سنحتاج لتعديل تعريف المتغير last_call ليصبح كالتالي:

__declspec(thread) CALLINFO* last_call = NULL;

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

كما نعلم فإن داخل الـ thread الرئيسي فى الأغلب تكون الداله main فى المستوي صفر، فى الـ threads الأخري ستصبح الداله فى المستوى صفر هي أولل داله يتم تنفيذها داخل ذلك الـ thread.

التغيير الذى قمنا به فى تعريف المتغير last_call مرتبط بالمترجم و للأسف كل مترجم له اسلوب مختلف فى التعريف عن Thread Local Storage حيث ترتبط بالمترجم و بنظام التشغيل الذى سيتم أنشاء نسخه تنفيذيه له، حتى لا ارهقك معي قمت بكتابة الماكرو التالي و هى تعمل مع العديد من المترجمات لنظام ويندوز و غيره:

#ifdef WIN32 // for windows
#   if defined(_MSC_VER) // for VC++ and Intel c++
#      define _TLS __declspec( thread )
//#   elif defined(__GNUC__) // for g++
//#      define _TLS __thread
#   else // for g++ and other compilers
#      define _TLS __thread
#   endif
#else // for other operating systems
#   define _TLS __thread
#endif

و بالكود السابق يصبح تعريف المتغير last_call كالتالي:

_TLS CALLINFO* last_call = NULL;

الكود التالي: يحتوى على تجربه للمكتبه على مستوى thread واحد فقط:

#include <iostream>
#include "call_trace.h"
using namespace std;

void L1();
void L2();
void L3();
void L4();
void L5();

int main()
{
	PUSH_CURRENT(0, main, 0);

	cout << "main call tree\n";
	PRINT_CALL_TREE;
	cout << endl;

	L1();
	L5();

	POP_CURRENT;
}

void L1()
{
	PUSH_CURRENT(0, L1, 0);

	cout << "L1 call tree\n";
	PRINT_CALL_TREE;
	cout << endl;

	L2();
	L4();

	POP_CURRENT;
}

void L2()
{
	PUSH_CURRENT(0, L2, 0);

	cout << "L2 call tree\n";
	PRINT_CALL_TREE;
	cout << endl;

	L3();

	POP_CURRENT;
}

void L3()
{
	PUSH_CURRENT(0, L3, 0);

	cout << "L3 call tree\n";
	PRINT_CALL_TREE;
	cout << endl;

	L4();


	POP_CURRENT;
}

void L4()
{
	PUSH_CURRENT(0, L4, 0);

	cout << "L4 call tree\n";
	PRINT_CALL_TREE;
	cout << endl;

	L5();


	POP_CURRENT;
}

void L5()
{
	PUSH_CURRENT(0, L5, 0);

	cout << "L5 call tree\n";
	PRINT_CALL_TREE;
	cout << endl;

	POP_CURRENT;
}

نتيجة الكود السابق على جهازى كالتالي:

main call tree
 main

L1 call tree
 main
- L1

L2 call tree
 main
- L1
-- L2

L3 call tree
 main
- L1
-- L2
--- L3

L4 call tree
 main
- L1
-- L2
--- L3
---- L4

L5 call tree
 main
- L1
-- L2
--- L3
---- L4
----- L5

L4 call tree
 main
- L1
-- L4

L5 call tree
 main
- L1
-- L4
--- L5

L5 call tree
 main
- L5

كود الـ call trace

code.zip

كود الإختبار:

main.zip

و الله ولي التوفيق

تم تعديل هذه المشاركة بواسطة محمد علاء الدين في 7 أبريل 2011 في 13:01

2

مدونتي: C++ Tips and Tricks

مواضيع مشابهة