7.4 مشاريع متعددة الملفات كتاب لينكس الشامل >>

7.4 مشاريع متعددة الملفات

الفهرس

7.4.1 مقدمة

لنفرض أننا أمام تصميم مشروع لبرنامج يقوم بوظيفة معينة. يجب أن يكون الكود المصدري مقروء ومفهوم فإذا كنت تعرف الآن ماذا قصدت بالمتغير h أو بالوظيفة h_init() فإنك لن تتذكر بعد عدة أشهر. كما يجب أن يكون الملف بسيط وقصير وغير معقد انظر CodingStyle في Kernel-Doc. ولكن كلما كبر المشروع فإن تحقيق هذا يصبح أصعب، إذا عدت لبرامج C++ التي عرضناها في الفصل السابق ستجد أنه على سهولتها إلا أنها طويلة وغير مقروءة. لوجود الكثير من الأسطر التي تتعب الناظر إليها.

tipتلميح

الزيادة في التعليقات comments تزيد من السهولة والبساطة. والزيادة في الكود لا تعني دائماً المزيد من التعقيد؛ فتقسيم البرنامج إلى وظائف أو حتى صنوف يزيد من طول الكود ولكنه يزيد من السهولة في المقابل وضع متن block داخل متن مثلاً حلقة while داخل for يزيد من التعقيد.

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

المشاريع بشكل أساسي تتكون من مكتبة و طرف خلفي Back-End وطرف أمامي Front-End. الطرف الأمامي هو برنامج سهل الاستعمال ربما بواجهة رسومية يقوم باستعمال المكتبة أو الطرف الخلفي للقيام بمهمته، أما الخلفي فهو برنامج صعب الاستعمال وفي الغالب لا يستعمله أحد بشكل مباشر ولكنه ضروري لعمل الأطراف الأمامية من الأمثلة على ذلك crafty من كطرف خلفي و glchess و gnome-chess كطرف أمامي لذلك البرنامج.

7.4.2 تقسيم المشروع

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

// one-file.c: single file project
#include<iostream.h>
// ListNode is a struct used by MyList
struct ListNode {
	int value;
	ListNode *next,*prev;
};
// MyList Class 
class MyList {
	// public members
		public:
	MyList();			// constructor
	~MyList();			// destructor
	int Count();			// tell number or items
	ListNode* Add(int val);		// add item
	void ForEach(void (*)(int &));	// do somethig with items
	int *Head();			// Goto 1st item
	int *Tail();			// Goto last item
	int *Next();			// Goto next item
	int *Prev();			// Goto previous item
	// private members
		private:
	int n;
	ListNode *head,*tail,*current;
};
// MyList methods body
MyList::MyList() {
	// set every thing to ZERO
	n=0;
	head=tail=current=NULL;
}
MyList::~MyList() {
	// free all allocated memory by going to last one then prev ...
	ListNode *tmp=tail,*prev;
	while (tmp) {
		prev=tmp->prev;
		delete tmp;
		tmp=prev;
	}
	head=tail=NULL;
}
int MyList::Count() {
	return n;
}
ListNode* MyList::Add(int val) {
	ListNode *tmp=new ListNode; // allocate a new node
	if (!tmp) return NULL; // make sure there is enough mem
	// fix it's prev,next,value members
	tmp->prev=tail; tmp->next=NULL;
	tmp->value=val;
	// make it's prev's next point to it
	if (tail) tail->next=tmp;
	// if there is no head so this is 1st element
	if (!head) head=tmp;
	// set the new as current and last element
	current=tail=tmp;
	++n;
	return tmp;
}
void MyList::ForEach(void (*myfunc)(int &)) {
	// Goto 1st element then next then next ...
	ListNode *tmp=head;
	while (tmp) {
		myfunc(tmp->value);
		tmp=tmp->next;
	}
	
}
int* MyList::Head() {
	return ((head)? &(head->value) :NULL);
}
int* MyList::Tail() {
	return ((tail)? &(tail->value) :NULL);
}
int* MyList::Next() {
	if (!current || !current->next) return NULL;
	current=current->next;
	return &(current->value);
}
int* MyList::Prev() {
	if (!current || !current->prev) return NULL;
	current=current->prev;
	return &(current->value);
}

// main part of the project
int Sum; // global var to hold sum
// functions used by MyList::ForEach
void print(int &i) { cout <<i<<endl; }
void sum(int &i) { Sum+=i; }
void add5(int &i) { i+=5; }

int main() {
	MyList l;
	int i=0,n;
	while (i!=-1) {
		cout << "Enter a number (-1 to EXIT) : ";
		cin  >> i;
		if (i==-1) break;
		l.Add(i);
	};
	n=l.Count();
	cout << "OK, "<<n<<" elements were acepted.\n";
	// pass the case with no elements
	if (n!=0) {
		cout <<"Those elements are:\n";
		l.ForEach(print);
		Sum=0; l.ForEach(sum);
		cout <<"Sum="<<Sum<<" Mean="<<(float)Sum/n<<endl;
		cout <<"Add 5 for each one:\n";	
		l.ForEach(add5);
		l.ForEach(print);
		Sum=0; l.ForEach(sum);
		cout <<"New Sum="<<Sum<<" New Mean="<<(float)Sum/n<<endl;

	}
	return 0;
}
في المقابل يمكن تقسيم البرنامج إلى 3 ملفات الجزء الرئيسي Front-End وهو الملف mean.cpp يسأل عن المدخلات ويضيفها للقائمة ،يتوقف عند إدخال -1، يطبعها،يحسب الوسط، يجمع 5 لكل عنصر،يحسب الوسط. لاحظ أنه يحتوي الملف mylist.h وهو ليس جزء من سي إنه ملف نحن كتبناه وهو عبارة عن تعريف صنف MyList. عند مراجعة أو تتبع هذا الملف افترض أن MyList يعمل بشكل جيد. لاحظ تعريف متغير MyListVersion على أنه معرف في ملف آخر بوساطة الكلمة المفتاحية extern
// mean.cpp: main file in this project
#include<iostream.h>
#include "mylist.h"

extern const char *MyListVersion; // get the version string from other file
int Sum; // global var to hold sum
// functions used by MyList::ForEach
void print(int &i) { cout <<i<<endl; }
void sum(int &i) { Sum+=i; }
void add5(int &i) { i+=5; }

int main() {
	MyList l;
	int i=0,n;
	cout << "Using my List version" << MyListVersion << endl ;
	while (i!=-1) {
		cout << "Enter a number (-1 to EXIT) : ";
		cin  >> i;
		if (i==-1) break;
		l.Add(i);
	};
	n=l.Count();
	cout << "OK, "<<n<<" elements were acepted.\n";
	// pass the case with no elements
	if (n!=0) {
		cout <<"Those elements are:\n";
		l.ForEach(print);
		Sum=0; l.ForEach(sum);
		cout <<"Sum="<<Sum<<" Mean="<<(float)Sum/n<<endl;
		cout <<"Add 5 for each one:\n";	
		l.ForEach(add5);
		l.ForEach(print);
		Sum=0; l.ForEach(sum);
		cout <<"New Sum="<<Sum<<" New Mean="<<(float)Sum/n<<endl;

	}
	return 0;
}
هذا ملف mylist.h ولا يجوز أن يحتوي أي متن وظيفة (إلا إذا كانت inline)
// mylist.h: MyList Class header file
#ifndef _MY_LIST_
#define _MY_LIST_
// ListNode is a struct used by MyList
struct ListNode {
	int value;
	ListNode *next,*prev;
};
class MyList {
	// public members
		public:
	MyList();			// constructor
	~MyList();			// destructor
	int Count();			// tell number or items
	ListNode* Add(int val);		// add item
	void ForEach(void (*)(int &));	// do somethig with items
	int *Head();			// Goto 1st item
	int *Tail();			// Goto last item
	int *Next();			// Goto next item
	int *Prev();			// Goto previous item
	// private members
		private:
	int n;
	ListNode *head,*tail,*current;
};
#endif
الملف الأخير وهو mylist.cpp الذي يمثل المكتبة أو الطرف الخلفي الذي يقوم بكل الحسابات عند تتبع هذا الملف لا تفترض أن mean.cpp هو الذي يستخدمه بل على أنه وحدة مستقلة، يمكنك كتابة Front-End آخر غير mean.cpp لاختباره مثلاً يقوم بعمل قائمة من أرقام محددة مسبقاً ثم طباعتها. لاحظ عدم وجود extern أمام تعريف متغير MyListVersion لأن التعريف في هذا الملف وليس في غيره
// mylist.cpp: MyList class methods serves as lib for our project
#define _MY_LIST_CPP_
#include<iostream.h> // for NULL!
#include "mylist.h"

const char * MyListVersion="0.1.9a";
MyList::MyList() {
	// set every thing to ZERO
	n=0;
	head=tail=current=NULL;
}
MyList::~MyList() {
	// free all allocated memory by going to last one then prev ...
	ListNode *tmp=tail,*prev;
	while (tmp) {
		prev=tmp->prev;
		delete tmp;
		tmp=prev;
	}
	head=tail=NULL;
}
int MyList::Count() {
	return n;
}
ListNode* MyList::Add(int val) {
	ListNode *tmp=new ListNode; // allocate a new node
	if (!tmp) return NULL; // make sure there is enough mem
	// fix it's prev,next,value members
	tmp->prev=tail; tmp->next=NULL;
	tmp->value=val;
	// make it's prev's next point to it
	if (tail) tail->next=tmp;
	// if there is no head so this is 1st element
	if (!head) head=tmp;
	// set the new as current and last element
	current=tail=tmp;
	++n;
	return tmp;
}
void MyList::ForEach(void (*myfunc)(int &)) {
	// Goto 1st element then next then next ...
	ListNode *tmp=head;
	while (tmp) {
		myfunc(tmp->value);
		tmp=tmp->next;
	}
	
}
int* MyList::Head() {
	return ((head)? &(head->value) :NULL);
}
int* MyList::Tail() {
	return ((tail)? &(tail->value) :NULL);
}
int* MyList::Next() {
	if (!current || !current->next) return NULL;
	current=current->next;
	return &(current->value);
}
int* MyList::Prev() {
	if (!current || !current->prev) return NULL;
	current=current->prev;
	return &(current->value);
}
يمكن أتمتة عملية وضع extern وذلك من خلال استشعار في أي الملفات نحن. احذف السطر extern const char *MyListVersion; ثم أضف إلى mylist.h
// ... mylist.h ...
#ifndef _MY_LIST_CPP_
		extern const char * MyListVersion;
#endif
وإذا كان هناك نوعية من المتغيرات غير const (في الغالب مؤشرات) يتم وضع قيمتها بعد استدعاء وظيفة استهلالية مثلاً mylib_init يمكن عندها استعمال الطريقة التالية
// ... mylist.h ...
#ifdef _MY_LIST_CPP_
	#define EXTERN
#else
	#define EXTERN extern
#endif
ثم تعريف المتغيرات على أنها من نوع EXTERN التي قيمتها لا شيء في ملف المكتبة و extern في الملفات الأخرى

ضع الملفات الثلاثة في مجلد واحد ثم اكتب الأمر

bash$ g++ mean.cpp mylist.cpp -o mean
bash$ ./mean
يمكن عمل الاستفادة من MyList في مشروع آخر مثلاً لحساب الانحراف المعياري لعدد من الأرقام (هذا ما نسميه Software reusing في هندسة البرمجيات)
// sd.cpp: main file in the standerd deviation project
#include<iostream.h>
#include<math.h>
#include "mylist.h"

float Sum; // global var to hold sum
float SS; // global var to hold sum of squars
// functions used by MyList::ForEach
void print(int &i) { cout <<i<<endl; }
void sum(int &i) { Sum+=i; }
void ss(int &i) { SS+=(float)i*i; }
void add5(int &i) { i+=5; }

int main() {
	MyList l;
	int i=0,n;
	float sd;
	cout << "using MyList ver "<<MyListVersion<<endl;
	while (i!=-1) {
		cout << "Enter a number (-1 to EXIT) : ";
		cin  >> i;
		if (i==-1) break;
		l.Add(i);
	};
	n=l.Count();
	cout << "OK, "<<n<<" elements were acepted.\n";
	// pass the case with no elements
	if (n!=0) {
		cout <<"Those elements are:\n";
		l.ForEach(print);
		SS=0; Sum=0; l.ForEach(sum); l.ForEach(ss);
		cout <<"Sum="<<Sum<<" Mean="<<Sum/n;
		sd=sqrt((SS-(Sum*Sum)/n)/n);
		cout <<" SS="<<SS<<" SD="<<sd<<endl;
		cout <<"Add 5 for each one:\n";	
		l.ForEach(add5);
		l.ForEach(print);
		SS=0; Sum=0; l.ForEach(sum); l.ForEach(ss);
		cout <<"New Sum="<<Sum<<" New Mean="<<Sum/n;
		sd=sqrt((SS-(Sum*Sum)/n)/n);
		cout <<" New SS="<<SS<<" New SD="<<sd<<endl;
	}
	return 0;
}
من المهم جداً الاتباه لوجود فرق بين C و C++ أهمها أن الأولى تمنع تشارك عدة وظائف لننفس الاسم باخلاف المعاملات مثلاً لا يجوز في سي أن تقول
int max(int a,int b);
int max(int a,int b,int c);
ويجوز ذلك في سي++ ويسمى overloading حيث أن سي تعتمد اسم الوظيفة كمعرّف symbol id بينما تعتمد سي++ على رمز خاص(سلسلة نصية) تولده من اسم ومعاملات (يختلف من مصنف لآخر) مثلاً قد تسمي الأولى max@2i والثانية max@3i. هذا قد يحجب سي وسي++ عن بعضهما عند عمل مكتبة بواسطة سي++ فإن سي لن تتمكن من رؤية الصنوف والوظائف بسبب المزايا الإضافية. فإذا كنت تكتب مكتبة بلغة سي++ وتريد توفير جزء من الخدمات للغة سي (أو تريد عمل جزء ليتم تحميله في ما بعد بواسطة ld التي تدعم أسلوب سي فقط) فإن عليك وضع علامة في سي++ تخبرها بأن تصدر رمز المعرف بطريقة سي التقليدية. يكون ذلك بأن تضع extern "C" قبل الوظيفة (سواء المتن أم النموذج) أو المتغير كما يلي
extern "C" int i;
extern "C" void foo();
extern "C" void foo() {
	do_something();
	return
}
أو إذا كنت كسولاً يمكنك حصرها بعلامتي {}
extern "C" {
    int i;
    void foo();
}

7.4.3 طرق الربط

بالإمكان تصنيف الملف mylist.cpp مرة واحدة واستعمال الملف الناتج في المشروعين (الوسط mean.cpp والإنحراف المعياري sd.cpp) وذلك بإنتج ملفات o بالخيار -c الذي يعني أن يقوم بالتصنيف دون الربط

bash$ g++ -c mylist.cpp
هذا ينتج ملف mylist.o ، الآن ننتج ملف mean.o ثم نربط link الملفين في الملف التنفيذي mean
bash$ g++ -c mean.cpp
bash$ g++ mean.o mylist.o -o mean
bash$ ./mean
بنفس الطريقة مع sd.cpp لاحظ سرعة انتهاء التصنيف (لأنه يصنف ملف واحد فقط هو sd.cpp) إذا كان هناك أي تعديل في ملف sd.cpp فإن كل ما عليك هو إعادة تصنيفه وربطه لوحده وليس هو والمكتبة mylist
bash$ g++ -c sd.cpp
bash$ g++ sd.o mylist.o -o sd
bash$ ./sd
ملف mylist.o يسمى object-file وفي ويندوز ودوس يكون له الإمتداد OBJ (في مصنف MSVC و BC) وهو الملف الناتج عن التصنيف لأي برنامج دون ربطه بالمكتبات. تسمى طريقة الربط هذه بين mean و mylist ب static linking لأن البرنامج الناتج يعمل دون المكتبة mylist ولكن ليس static-binary بالمطلق فهو لا يزال يحتاج لمكتبة stdc++ استعمال الخيار --static في gcc يجعله static-binary تذكر الأداة ldd التي تظهر لك على ماذا يعتمد البرنامج. علمت سابقاً أن امتداد المكتبة ال static هو a وليس o كما وأن ملف ال o هو object-file الفرق هو أن المكتبة قد تكون من أكثر من ملف (c و cpp ولا نحسب h) كل واحد ينتج ملف o فكيف نجعل أكثر من ملف object ملف a ؟ واحد الجواب هو استعمال ar كما يلي (هذا المثال يعمل مكتبة باسم mylib-static من ملفي myfile1.o و myfile2.o)
bash$ ar -rcs libmylib-static.a myfile1.o myfile2.o
والربط معها يكون بذكر اسم الملف libmylib-static.a أو بالخيار -l mylib-static عند تنفيذ gcc ، لاحظ مع l لا نضيف الامتداد a ولا السابقة lib قبل اسم الملف.

الطريقة الأخرى هي dynamic linking حيث يتم تحميل المكتبة من ملف منفصل عند تشغيل البرنامج (so في لينكس و dll في ويندوز). حيث هنا عليك عدم إزالة بيانات debug info خاصة symbols وذلك بالخيار -g وهو عكس الخيار -s،كما أن الأمر strip يزيل هذه المعلومات من الملفات (بعد تصنيفها) مثلاً strip ./myfile. ويقول Program-Library-HOWTO أن عليك استعمال -fPIC أو -fpic (الثانية أسرع) وهما اختصار ل Position independant code ولكني لم أعرف لماذا ؟ (أظن أنها تهمل). فيما بعد يمكن استخدام البرنامج addr2line لكي يحول العنوان إلى رقم السطر في المصدر وهو جزء من حزمة gdb. هذه قائمة بأهم الخيارات ل gcc التي يمكنك أن تستعملها

-Wall		all warnings on
-s		strip debuging info
-g		generate debuging info
-Wa,OPTIONS	pass options to assembler
-Wp,OPTIONS	pass options to prerocessor
-Wl,OPTIONS	pass options to linker
-l LIBNAME	link with LIBNAME library
-L LIBPATH	add LIBPATH to path of looking for libs
-I INCPATH	add INCPATH to path of looking for headers
لعمل مكتبتنا الصغيرة على شكل مكتبة ديناميكية so أولاً نصنفها للحصول على ملف mylist.o
bash$ gcc -fPIC -g -c -Wall mylist.cpp
ثم نربطها (لأن بعد المكتبات تكون أكثر من ملف o) لإنتاج ملف so
bash$ gcc -shared -Wl,-soname,libmylist.so.1 \
    -o libmylist.so.1.0.1 mylist.o -lc
الملف الناتج هو libmylist.so.1.0.1 ولكن libmylist.so.1 هو رابط يشير للأول. وهو(الأخير) الذي سنربط برامجن معه وذلك حتى إذا عملنا تعديلات (تصحيح أو تسريع) وسمينا الجديدة مثلاً libmylist.so.1.0.2 فإن البرامج التي كانت مربوطة مع الاصدار القديم ستظل تعمل (دون وجود نسختين من المكتبة، قديمة وحديثة) لأنها مربوطة مع libmylist.so.1. للتأكد من عمل الوصلات بشكل مناسب نفذ
bash$ ldconfig -n .
الآن لتصنيف برنامج mean.cpp أو sd.cpp بطريقة ديناميكية مع هذه المكتبة
bash$ gcc mean.cpp -L . -lmylist -o mean
tipتلميح

تصنيف dll في ويندوز يختلف حتى بأدوات gnu إذ عليك استعمال dllwarp لتوليد ملف dll وملف a من ملف o و dlltool تولد ملف a من ملف dll مثلاً
dlltool -D foobar.dll -l libfoobar.a

عند تنفيذ برنامج مربوط ديناميكياً فإن ld تقوم بالبحث عن المكتبات (ملفات so) في ملف /etc/ld.so.cache (الذي يحتوي اسم المكتبة والاسم المطلق لها أي مع الدليل) أو في المجلدات المحددة في متغير البيئة LD_LIBRARY_PATH.
tipتلميح

المكتبة المسؤولة عن التحميل والربط هي ld والملفت للنظر أنها أيضا ملف تنفيذي جرب تنفيذ /lib/ld-linux.so.2 لتعرف بعض الخيارات مثلاً
/lib/ld-linux.so.2 --list my_program تقوم بما يقوم به ldd أي عرض المكتبات التي يعتمد عليها. ويمكنك تنفيذ برنامج مع تحديد مسار مختلف للمكتبات
/lib/ld-linux.so.2 --library-path PATH my_program

وحيث أن مكتبتنا ليست موجودة في /etc/ld.so.cache ولا نريد نقلها إلى ما يشير له LD_LIBRARY_PATH مثل /usr/lib لهذا نقوم بتعديل هذا المتغير لنضيف له الدليل الحالي إما بشكل مؤقت قبل اصدار أي أمر يزول بعد تنفيذ الأمر
bash$ LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH  ldd ./mean
bash$ LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH  ./mean
أو بشكل دائم (يزول عند إعادة التشغير)
bash$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
bash$ ./mean
وعندما ننهي المشروع ننقلها إلى /usr/lib وإذا أردنا نضيفها إلى /etc/ld.so.cache وذلك بتنفيذ ldconfig وأنت جذر.

7.4.4 صيغة Makefile

عند تصنيف مشروع أكثر من مرة فإن طباعة كل تلك الخيارات أمر ممل ، لهذا تفضل استعمال make وهي الأداة التي تحدثنا عنها في فصل تركيب الحزم المصدرية كانت تتم بواسطة أمر make الذي يقرأ كيف يفعل ذلك من ملف Makefile الذي يولد من تنفيذ configure وهو نص تنفيذي shell-script غالباً يكون من حزمة autoconf التي تولد Makefile يناسب كل نظام سنتحدث عن صيغة هذا الملف وليس عن autoconf على الرغم من أهميته لأنه يناسب تلك المشاريع لتي تعد للعمل على أكثر من نظام كل منها له إعدادات مختلفة وهذا ليس من أهدافنا فنحن نحصر اهتمامنا بنظام جنو GNU.

صيغة الأمر هي make [[VAR=VAL]... [TARGET]] مثلاً make all و make clear و make install و make uninstall ... إلخ. وإذا لم نحدد الهدف كان all هو الهدف المقصود. هذا يعني أن Makefile يحتوي طريقة عمل كل هدف. صيغة الملف المبسطة هي كما يلي

# silly makefile
all: mean
mean:
	g++ mean.cpp mylist.cpp -lstdc++ -o mean
test: mean
	./mean
install:
	echo "Not ready"
uninstall:
	echo "Not ready"
clean:
	rm *.o
mrproper: clean
نذكر في سطر واحد اسم الهدف ثم ‘:‘ ثم الأهداف(تفصلها مسافة) التي يعتمد عليها هذا الهدف (التي يجب أن تنجز قبل عمل هذا الهدف) ، ثم في الأسطر اللاحقة نذكر الأوامر التي يجب أن تنفذ. ويمكن استعمال متغيرات مثل CC و CFLAGS التي تذكر قبل اسم الهدف في أمر make لتغيير القيمة التلقائية لها التي تكون مثبتة في ملف ال Makefile كما في هذا المثال
# nice Makefile

# Select compiler
CC=g++
# Parameters given to the compiler
CFLAGS= -O2 -s
INC=-I/usr/include/g++
LIBS=-lstdc++
# Output filename (*.exe)
OUTPUT=mean
PREFIX=/usr/bin
# Source files
SRCS=mean.cpp
# Output object files (*.o)
OBJS=mean.o

all: mean
mean:
	$(CC) -c $(SRCS) $(CFLAGS) $(INC)
	$(CC) -o $(OUTPUT) $(OBJS) $(CFLAGS) $(LIBS)
test:
	$(CC) $(SRCS) $(CFLAGS) $(LIBS) $(INC) -o $(OUTPUT)
	./$(OUTPUT)
install:
	cp ./$(OUTPUT) $(PREFIX)/$(OUTPUT)
uninstall:
	rm $(PREFIX)/$(OUTPUT)
clean:
	rm *.o
mrproper: clean
لاحظ أن المتغيّرات الموجودة في الملف يتم تجاهلها إذا ممرت قيم خرى عبر make كما أن أخذ/تعويض قيمة المتغير تكون بوضع قوسين حول اسمه وسبقه بعلامة $ كما $(VAR). يمكنك كتابة نص تنفيذي shell-script يقوم بتوليد هكذا ملف بنفسك وتسميته configure حتى يتمكن المستخدم العادي من بناء المشروع بالخطوات التقليدية.

7.4.5 تحميل المكتبات وقت التنفيذ

هل تسألت كيف يستطيع برنامج xmms أن يتقبل إضافات plugin لتشغيل ملفات جديدة. تكون هذه الإضافات عبارة عن مكتبة تحمل وقت التنفيذ أي أن المبرمج لا يربط برنامجه معها (ولا حتى ديناميكياً) ، بل يستخدم مكتبة تعمل على تحميل هذه المكتبة (سمها plugin أو module أو مكتبة وحسب) وعند الطلب تعيد مؤشر لوظيفة محددة داخل هذه المكتبة تقوم أنت بحفظ هذا المؤشر واستدعاؤه عند الحاجة. هذا مثال اخترته من Program-Library-HOWTO

/* dl-demo.c : Run time loading of cosine function.
 * From Program-Library-HOWTO,
 * by David A. Wheeler
 */
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h> /* this is the dl lib header */

int main(int argc, char **argv) {
	void *handle; /* holds what lib we are talking about */
	/* a pointer to a function. like prototype but with (*) */
	double (*cosine)(double); 
	char *error; /* an error string */

	handle = dlopen ("/lib/libm.so.6", RTLD_LAZY); /* load math lib */
	if (!handle) {
		fputs (dlerror(), stderr);
		exit(1);
	}
	cosine = dlsym(handle, "cos"); /* search for cos(); in it */
	if ((error = dlerror()) != NULL)  {
		fputs(error, stderr);
		exit(1);
	}
	printf ("%f\n", (*cosine)(2.0)); /* call it */
	printf ("%f\n", (*cosine)(1.0471975512)); /* call it again */
	dlclose(handle); /* close it and unload it */
}
لتصنيف البرنامج
bash$ gcc -o dl-demo -l dl dl-demo.c
لاحظ بدأ البرنامج ب #include<dlfcn.h> ثم فتح المكتبة ب dlopen التي تعيد مؤشر نحتفظ به لنشير إلى هذه المكتبة، وتأخذ هذه الوظيفة معاملين هما اسم المكتبة و علامة يمكن أن تكون RTLD_LAZY أو RTLD_NOW الأولى أسرع في الفتح و أبطئ في البحث عن الوظيفة، بعد ذلك نبحث عن أي symbol الذي قد يكون متغير أو وظيفة فتعيد مؤشر على القيمة إذا حدث خطأ فإن الوظيفة dlerror تحدد السبب وإلا تكون NULL. بهذا نستطيع التمييز بين حالة أن تكون قيمة المتغير NULL وبين حالة عدم وجود متغير بهذا الاسم. على أي حال تركيزنا هو على الوظائف وليس على المتغيرات. في هذا المثال طلبنا البحث عن اقتران جيب التمام cos وخزّنا المؤشر في cosine وبعد ذلك يمكننا استدعاؤه من خلال الأخير وقتما نشاء وبعد الإنتهاء من المكتبة نلغي تحميلها ب dlclose.

عند عمل plugins قد يرغب مصممها بأن يظهر شعار أو ملاحظة .. تشير له في كل برنامج يحمل هذه المكتبة. يكون ذلك بعمل وظيفة باسم void _init(); حيث تستدعى بمجرد فتح المكتبة ب dlopen. بنفس الأسلوب عن إلغاء التحميل قد يرغب المبرمج بتحرير ذاكرة أو إغلاق ملفات يكون ذلك بوضعه داخل وظيفة باسم void _fini();.

7.4.6 أداة تصحيح الأخطاء Debuger

أحد مستخدمي لينكس حديثي العهد كان يسأل عن وجود برنامج يعادل Borland C++ Compiler حاولنا قراءة أفكاره فدللناه على gcc فلم يكن هذا ما يسأل عنه فدللناه على kdevaloper و anjuta فلم يكن هذا ما يبحث عنه لأن ما يبحث عنه. قال أنه يريد مصنف سي يستطيع كشف أخطاء تحدث وقت التنفيذ run-time errors ! حاولنا أن نقول له أن لغات البرمجة التصنيفية مثل سي/سي++ لا يمكنها ذلك ولكنه أصر أن Borland يمكنها ذلك. ما كان يبحث صاحبنا عنه هو برنامج Debuger وهي تعني أداة تساعد على تصحيح الأخطاء (وتتبع عمل البرامج) فإذا تم تصنيف برنامج شاملاً معلومات التصحيح debuging information ثم تم تنفيذه من خلال برنامج التصحيح debuger فإنه يمكن أن يتوقف عند حدوث خطأ حتى لو كان run-time ويعطيك السطر الذي حدث عنده الخلل ويمكنك عرض قيم المتغيرات ويمكنك تنفيذ البرنامج سطر فسطر لتتبع كيفية حدوث الخلل. يوفر نظام جنو أفضل مصحح أخطاء معروف حتى الآن اسمه gdb - GNU Debuger ولا يوجد برنامج منافس يؤمن نصف العدد الكبير من مزاياه ومنها أنه يدعم معظم اللغات التصنيفية المعروفة وليس فقط سي/سي++ كما يدعم أكبر طيف من الأنظمة(بما فيها لينكس وويندوز) وطرز الأجهزة ويدعم تسريع بعض عمليات التتبع بواسطة عمليات عتادية (لا تأخذ من وقت المعالج). كما يمكنه تتبع برامج لم تشغل عن طريقه ويمكنه تتبع برامج بعد أن تتحطم! وذلك من خلال ملف الحطام core dump (هل لديك تعريب أفضل)

للحصول على أفضل نتيجة يجب أن لا تزيل معلومات التصحيح بواسطة strip ولا باستعمال الخيار s في gcc وللتأكد أضف الخيار g الذي يؤكد على وضع معلومات التصحيح مثلاً gcc -g foo.c -o foo. لتنفيذ برنامج التصحيح اكتب gdb متبوعاً باسم البرنامج الذي تريد تصحيحه (يجب أن يكون الملف المصدري في نفس الدليل وإلا عليك إخباره أين يجدها ببعض الخيارات). وإذا كنت تريد تصحيح برنامج بعد فوات الأوان (أي بعد تولد الخطأ وإغلاق البرنامج) يمكنك اضافة معامل بعد اسم البرنامج هو ملف coredump (كلمة core تعني لب وهي تشير للذاكرة رام وكلمة dump تعني مكب حطام) مثلاً gdb foo coredump. كما يمكنك تتبع برنامج يعمل الآن خارج سيطرة gdb بإضافة معامل بعد اسمه هو معرف تلك العملية PID التي تحصل عليها من ps أو pidof كما في المثال gdb foo 3047.

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

برنامج gdb أداة تفاعلية تستطيع التفاعل معها بكتابة الأمر (أو اختصاره) أو طلب المساعدة بالأمر help متبوعاً بما تريد معرفته. ويتوفر عدد من الواجهات الرسومية لهذه الأداة القوية لكن استعمال الأداة الأصلية ليس صعباً. أضف نقاط التوقف بالأمر break أو b متبوعة باسم الوظيفة فالصيغة التي نعطي بها الوظائف هي بذكر اسم الوظيفة (دون أي معاملات) مثلاً b max ولتجنب الغموض في لغة سي++ حيث يجوز أن تأخذ أكثر من وظيفة نفس الاسم اذكر أنواع المعاملات بين قوسين كما في نموذج الوظيفة مع وضع علامة اقتباس مفردة مثلاً b 'max(int,int)'. أجمل ما في الموضوع توفر زر إكمال الكتابة TAB يمكنك أن تكتب جزء من اسمها وتضغط TAB (مرة أو مرتين) ليعطيك البدائل. إذا كان هناك أكثر من وظيفة بنفس الاسم في مشروع متعدد الملفات حدد اسم الملف ثم ‘:‘ ثم اسم الوظيفة. كما يمكنك تحديد نقاط توقف بذكر السطر الذي تريد التوقف قبله وذلك بذكر رقم السطر أو الإزاحة عن الموقع الحالي (+/-) ويمكنك تحديد في أي الملفات هو (خصوصاً في المشاريع) مثلاً b foo.c:35. أما مراقبة متغيّر فتكون عبر الأمر watch مثلاً watch var1 للتوقف كلما تغيّرت قيمة var1. نستطيع حذف كل نقاط التوقف والمراقبة بالأمر delete أو d ولحذف أحدها فقط نلحق به الرقم المتسلسل لتلك النقطة (الذي ظهر عند إضافتها).

لتحديد المعاملات لكل تمرر للبرنامج قبل تشغيله استعمل set args بالصيغة set args ARG1 ARG2... ولتشغيل البرنامج اكتب الأمر run ويمكن تمرير المعاملات بطريقة أخرى من خلال run بالصيغة run ARG1 ARG2.... لنفرض أن لديك برنامج التحية الشهيرة ‘Hello, world!‘ اسمه foo واسم المصدر هو foo.c

bash$ gdb foo
GDB is free software and you are welcome to distribute copies
 of it under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 5.3-debian, Copyright 1999 Free Software Foundation, Inc...
(gdb) break main
Breakpoint 1 at 0x62f4: file foo.c, line 26.
(gdb) run
run
Starting program: /home/ali/foo.c
...loading SO/DLL goes here...
Breakpoint 1, main (argc=1, argv=0x7507) at foo.c:26
26	printf("Hello, world!\n");
لقد وضعنا نقطة توقف عند وظيفة main ثم نفذنا البرنامج فتوقف البرنامج مباشرة لأنها أول ما ينفذ من البرنامج وكتب لنا أن السطر القادم هو 26 وعرض محتوياته (يقفز تلقائياً عن أسطر التعليق أو التي لا تنفذ) لعرض أسطر من مصدر البرنامج حول السياق اكتب list أو l يمكنك تحديد رقم السطر أو الإزاحة (+/-) عن الموقع الحالي أو حتى الوظيفة ... مثلاً l -5 تعرض قبل الموقع الحالي بخمسة أسطر. وتكرار العملية يعرض المجموعة التالية من الأسطر
tipتلميح

يمكن تكرار آخر عملية بضغط ENTER دون كتابة أي أمر.

لمحبي لغة التجميع assembly أو في البرامج التي لا تحتوي معلومات التصحيح ولا تملك ملفها المصدري يمكن عرض البرنامج بلغة التجميع وذلك بعملية تسمى disassemble والأمر الذي يقوم بذلك هو disass متبوع باسم الوظيفة أو العنوان مثلاً disass main وهناك نكهتان(!) من هذه اللغة (تحدثت عنهما في الملحق الصفر) هما Intel Style و AT&T Style تستطيع اخبار gdb أيها يعرض بالأمر set disassembly-flavor intel|att.

إذا اكتشفت الخطأ يمكنك تعديل بأي محرر نصوص في نافذة أخرى (إذا كنت تستعمل إكس) ثم إعادة تصنيف البرنامج واختباره أو يمكنك الضغط على CTRL+Z لإيقاف برنامج التصحيح ثم تشغيل محرر نصوص مثل VIM أو emacs ثم إعادة تصنيف البرنامج ثم العودة لبرنامج التصحيح من خلال الأمر fg. لكن لا داعي لذلك فأمر shell يمكنك من تشغيل أي برنامج تريد دون الخروج من gdb مثلاً shell ls أو shell vim foo.c أو shell gcc -g foo.c -o foo

في حال توقف البرنامج وظهور محث gdb مرة أخرى قمت بالتدقيق ووجدت أنك لا تريد التوقف هنا استعمل الأمر continue أو c الذي يتابع تنفيذ البرنامج حتى حصول نقطة توقف أخرى لأي سبب كان (مثلاً breakpoint أو watch أو حدوث run-time error). قد يكون من الأجدى عند تتبع برنامج أن تراه ينفذ سطراً فسطر وذلك من خلال الأمر next أو n التي تنفذ السطر التالي ثم تتوقف وتعرض الذي يليه وهي تقفز عن أي استدعائيات لوظائف دون تتبعها، أما إذا كنت تريد الدخول في حال وجود استدعاء لوظيفة وتتبع الإجراء الفرعي استعمل step أو s مثلاً إذا توقف البرنامج قبل سطر i=max(5,10); فإن ضغط n يقوم بتنفيذ هذا السطر والانتقال للسطر التالي. أما s فإنها أولاً تتبع استدعاء max وتذهب للتوقف عند أول سطر هناك. وتظل هكذا تدقق بواسطة أوامر متتالية من c و n و s حتى ترى كيف يسير البرنامج من سطر لآخر وماذا يحدث في كل خطوة.

الآن برنامجنا متوقف ، لعرض مكدس الوظائف (مثلاً إذا استدعت main الوظيفة my_init التي استدعت foo فإن foo تكون في أعلى المكدس ويقع دونها my_init ودونهما main) عرض المكدس يم بواسطة backtrace أو bt.

تستطيع عرض قيم المتغيرات بالأمر print أو p مثلاً p var1 أو حتى p 'myfunc(int)':var1 . وتستطيع القيام ببعض الحسابات بلغتك المفضلة (التي كتب بها البرنامج) من خلال gdb دون تصنيف (كما اللغات التفسيرية) مثلاً مثلاً p 500&((1<<7)-1) تعطي 244 وحتى يمكنك تعديل قيم المتغيرات مثلاً p var1=1024 واستدعاء وظائف مثلاً p myfunc(15). يمكنك العرض بأسلوب معين (نوع المعروض) وذلك بالأوامر p/d و p/u و p/x و p/o و p/t و p/f و p/c و p/s وهي على الترتيب عشري بإشارة و دونها و بالست-عشري وثماني وثنائي ونسبي ومحرف وسلسلة نصية مثلاً p/d 0x10 تعطي 16 و p/x 16 تعطي 0x10. كما تستطيع تحديد النوع كما تفعل في لغة سي وذلك بذكره بين أقواس ولعرض منظومة ضع * قبل العنوان(اسم المتغيّر) ويمكنك تحديد الطول الذي تريد عرضه بعلامة @ متبوعة بالطول مثلاً لعرض أول 15 عنصر p *MyArray@15 ولعرض العنصر الثاني من المنظومة p *(MyArray+1) لتجريب ذلك وأنت تدقق البرنامج وتشغله وعند التوقف بعد main يمكنك طباعة اسم البرنامج بواسطة p *argv ولطباعة كل المعاملات (واسم البرنامج) فالأمر هو p *argv@argc

الأمر x يستعمل لعرض محتويات عنوان في الذاكرة، ويمكن أن يكون عنوان الذاكرة كرقم أو اسم الرمز(المتغيّر أو الوظيفة) الذي يشير إليه. ويمكنه أن يحدد الكم فالنوع (النوع كما في p) مثلاً x/2t var1 تعرض أول 2-بايت من var1 بالثنائي x/2s *argv تعرض سلسلتين نصيتين من argv أي argv[0] و argv[1] أما x/8c *argv تعرض عند تصحيح برنامج /bin/ls

(gdb) x/8c *argv
x/8c *argv
0x7450:  47 '/'  98 'b' 105 'i' 110 'n'  47 '/' 108 'l' 115 's'   0 '\000'

يمكن لبرنامج gdb أن يتحكم في الإشارات التي تصل البرنامج الذي تعمل على تصحيحه تلك التي ترسلها بواسطة CTRL+C أو أمر kill وذلك بالأمر handle ثم اسم الإشارة (دون SIG) مثلاً INT ثم ماذا يفعل مثلاً stop تتصبح handle INT stop تجعل gdb يترجم الإشارة CTRL+C إلى breakpoint من أجل التدقيق وتستطيع المتابعة ب c وليس السلوك التقليدي بإيقاف البرنامج والخروج.


<[Atd width="30%" align="right"> << السابق
كتاب لينكس الشامل التالي >>