7.4 مشاريع متعددة الملفات | كتاب لينكس الشامل | >> |
لنفرض أننا أمام تصميم مشروع لبرنامج يقوم بوظيفة معينة.
يجب أن يكون الكود المصدري مقروء ومفهوم فإذا
كنت تعرف الآن ماذا قصدت بالمتغير h أو بالوظيفة h_init()
فإنك لن تتذكر بعد عدة أشهر. كما يجب أن يكون الملف بسيط
وقصير وغير معقد انظر CodingStyle في Kernel-Doc.
ولكن كلما كبر المشروع فإن تحقيق هذا يصبح أصعب، إذا
عدت لبرامج C++
التي عرضناها
في الفصل السابق ستجد أنه على سهولتها إلا أنها طويلة وغير مقروءة.
لوجود الكثير من الأسطر التي تتعب الناظر إليها.
الزيادة في التعليقات comments تزيد من السهولة والبساطة. والزيادة في الكود لا تعني دائماً المزيد من التعقيد؛ فتقسيم البرنامج إلى وظائف أو حتى صنوف يزيد من طول الكود ولكنه يزيد من السهولة في المقابل وضع متن block داخل متن مثلاً حلقة while داخل for يزيد من التعقيد.
المشاريع بشكل أساسي تتكون من مكتبة و طرف خلفي Back-End وطرف أمامي Front-End. الطرف الأمامي هو برنامج سهل الاستعمال ربما بواجهة رسومية يقوم باستعمال المكتبة أو الطرف الخلفي للقيام بمهمته، أما الخلفي فهو برنامج صعب الاستعمال وفي الغالب لا يستعمله أحد بشكل مباشر ولكنه ضروري لعمل الأطراف الأمامية من الأمثلة على ذلك crafty من كطرف خلفي و glchess و gnome-chess كطرف أمامي لذلك البرنامج.
لنأخذ هذا المشروع المكوّن من ملف واحد. وعلى الرغم من بساطته (قائمة بأرقام يتم إضافة أرقام إليها ثم حساب الوسط...) ، إلا أنه لا يسهل تتبعه.
// 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; }
// 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: 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: 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 const char *MyListVersion;
ثم أضف إلى mylist.h
// ... mylist.h ... #ifndef _MY_LIST_CPP_ extern const char * MyListVersion; #endif
// ... mylist.h ... #ifdef _MY_LIST_CPP_ #define EXTERN #else #define EXTERN extern #endif
ضع الملفات الثلاثة في مجلد واحد ثم اكتب الأمر
bash$ g++ mean.cpp mylist.cpp -o mean bash$ ./mean
// 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);
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(); }
بالإمكان تصنيف الملف mylist.cpp مرة واحدة واستعمال
الملف الناتج في المشروعين (الوسط mean.cpp والإنحراف المعياري sd.cpp)
وذلك بإنتج ملفات o بالخيار -c
الذي يعني أن يقوم بالتصنيف دون الربط
bash$ g++ -c mylist.cpp
bash$ g++ -c mean.cpp bash$ g++ mean.o mylist.o -o mean bash$ ./mean
bash$ g++ -c sd.cpp bash$ g++ sd.o mylist.o -o sd bash$ ./sd
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
bash$ gcc -fPIC -g -c -Wall mylist.cpp
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 .
bash$ gcc mean.cpp -L . -lmylist -o mean
تصنيف dll في ويندوز يختلف حتى بأدوات gnu إذ عليك
استعمال dllwarp لتوليد ملف dll وملف a من ملف o
و dlltool تولد ملف a من ملف dll مثلاً
dlltool -D foobar.dll -l libfoobar.a
/etc/ld.so.cache
(الذي يحتوي اسم المكتبة والاسم المطلق لها أي مع الدليل)
أو في المجلدات المحددة في متغير البيئة LD_LIBRARY_PATH.
المكتبة المسؤولة عن التحميل والربط هي 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
وأنت جذر.
عند تصنيف مشروع أكثر من مرة فإن طباعة كل تلك الخيارات
أمر ممل ، لهذا تفضل استعمال 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
# 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
$(VAR)
.
يمكنك كتابة نص تنفيذي shell-script يقوم بتوليد
هكذا ملف بنفسك وتسميته configure
حتى يتمكن
المستخدم العادي من بناء المشروع بالخطوات التقليدية.
هل تسألت كيف يستطيع برنامج 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();
.
أحد مستخدمي لينكس حديثي العهد كان يسأل عن وجود برنامج يعادل 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");
l -5
تعرض قبل الموقع الحالي بخمسة أسطر.
وتكرار العملية يعرض المجموعة التالية من الأسطر
يمكن تكرار آخر عملية بضغط ENTER دون كتابة أي أمر.
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 وليس السلوك التقليدي بإيقاف البرنامج والخروج.
كتاب لينكس الشامل | التالي >> |