سلام!
توی قسمت ۰، با چند تا دستور ابتدایی gdb کمی آشنا شدیم. حالا توی این بخش، چند تا دستور جدید یاد میگیریم! این دفعه قسمت بزرگی از هدفمون اینه که بتونیم با توابعی که صدا زده میشن هم کلنجار بریم! با درسهای قسمت قبل، همچین کاری غیرممکن یا سخت بود. D:
اکثر وقتها لازم نیست که ما شکل کامل دستوری که میخوایم رو تایپ کنیم. خیلی وقتها تایپ کردن یه پیشوند از اون دستور که به صورت یکتا مشخّصش کنه یا عبارات مخفّفی که شناخته شدن، کار همون دستورات رو برامون انجام میدن. برای مثال، استفاده از b به جای break و یا استفاده از bt به جای backtrace.
خب حالا برای امروز، با این کد(code01.c) قراره کار کنیم:
#include <assert.h> int fib(int n) { assert(n >= 5); if (n <= 1) return n; int x = fib(n - 1); int y = fib(n - 2); return x + y; } int main() { fib(10); return 0; }
همون طور که از ظاهر کد برمیآد، این کد قراره تابع فیبوناچی رو برامون محاسبه کنه. تنها نکتهش اینه که وقتی توی روند بازگشتی تابع fib، تابع به ازای nهای کمتر از ۵ صدا زده بشه، مقدار ورودی assert برابر با false میشه و برنامه به نوعی کرش میکنه.
برای کامپایلش هم مثل قسمت قبل:
gcc -g code01.c -o exec01
خب قبلا برای دیباگ کردن همچین برنامهای توی ترمینال مینوشتیم gdb exec01. الآن هم این کار رو میتونیم بکنیم. یکی از روشهای دیگه، اینه که توی ترمینال فقط دستور gdb رو اجرا کنیم، بدون هیچ آرگومانی. در این صورت وارد فضای gdb میشیم و بعدش با دستور file، میتونیم مشخّص کنیم که قصدمون دیباگ کردن چه برنامهای هست:
(gdb) file exec01 Reading symbols from exec01...done. (gdb)
حالا به راحتی برنامه رو با run اجرا میکنیم و میبینیم که:
(gdb) run Starting program: /home/rakeb/tmp/exec01 exec01: code01.c:5: fib: Assertion `n >= 5' failed. Program received signal SIGABRT, Aborted. __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51 51 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory. (gdb)
که به زبون آدمیزادی میگه آقا این برنامهی شما توی یه Assertion که شرطش n >= 5 بوده فِیل شده و بعد از اون، سیگنال SIGABRT برای برنامه فرستاده شده.
الآن برنامهمون هنوز خارج نشده! رفتار gdb با سیگنالهایی که مربوط به یه اروری میشن، مثل رفتارش با breakpointهاست! چون ممکنه برنامه نویس نیاز داشته باشه تا حول اون وضعیتی که منجر به کرش کردن شده، یه خرده چرخ بزنه و اطّلاعات به دست بیاره.
به هر حال، میتونیم با دستور next یا kill(درس نیمم؟!) به اجرای برنامه خاتمه بدیم. ولی فعلا این کار رو نمیکنیم تا با backtrace آشنا بشیم.
اگر با پایتون کار کرده باشید، میدونید که وقتی به یه ارور میخوره و کرش میکنه، کل روندی که توابع هم دیگه رو صدا کردن تا به اون نقطهی ایجاد خطا برسن رو چاپ میکنه براتون. اوّلش هم همچین چیزی مینویسه:
Traceback (most recent call last):
خب این اطّلاعات به دیباگ برنامه خیلی کمک میکنه. توی gdb هم همچین چیزی داریم. بلافاصله بعد از کرش کردن برنامهمون اگر دستور backtrace یا bt رو وارد کنیم، میبینیم که روند صدا شدن توابع به ترتیب از main تا این نقطهای که برنامه کرش کرده رو برامون مینویسه:
(gdb) backtrace #0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51 #1 0x00007ffff7a24801 in __GI_abort () at abort.c:79 #2 0x00007ffff7a1439a in __assert_fail_base (fmt=0x7ffff7b9b7d8 "%s%s%s:%u: %s%sAssertion `%s' failed.\n%n", assertion=assertion@entry=0x55555555475d "n >= 5", file=file@entry=0x555555554754 "code01.c", line=line@entry=5, function=function@entry=0x555555554764 <__PRETTY_FUNCTION__.1809> "fib") at assert.c:92 #3 0x00007ffff7a14412 in __GI___assert_fail (assertion=0x55555555475d "n >= 5", file=0x555555554754 "code01.c", line=5, function=0x555555554764 <__PRETTY_FUNCTION__.1809> "fib") at assert.c:101 #4 0x000055555555467a in fib (n=4) at code01.c:5 #5 0x0000555555554692 in fib (n=5) at code01.c:8 #6 0x0000555555554692 in fib (n=6) at code01.c:8 #7 0x0000555555554692 in fib (n=7) at code01.c:8 #8 0x0000555555554692 in fib (n=8) at code01.c:8 #9 0x0000555555554692 in fib (n=9) at code01.c:8 #10 0x0000555555554692 in fib (n=10) at code01.c:8 #11 0x00005555555546bd in main () at code01.c:15 (gdb)
از پایین لیست به سمت بالای لیست، مسیر رسیدن از main به عامل کرش رو میتونیم ببینیم. واضحه که خطوط ۴ تا ۱۱ مربوط به برنامهی شما هستن و خطوط ۰ تا ۳ توسّط تابع assert فراخوانی شدن. حالا با دستور kill برنامه رو میبندیم تا یه بار دیگه اجراش کنیم.
حالا میخوایم یه breakpoint روی نقطهی شروع تابع fib قرار بدیم تا وقتی که با n=5 صدا زده شد، روند اجرای برنامه رو دنبال کنیم. یک راهش اینه که خیلی ساده بنویسیم:
break fib
با این کار برنامه دقیقا لحظهای که میخواد وارد این تابع بشه، متوقّف میشه و ما میتونیم با استفاده از این دستور و دستور continue اون قدر ادامه بدیم تا به n=5 برسیم. امّا راه سادهتری که وجود داره، استفاده از breakpointهای شرطیه. خیلی ساده مینویسیم:
(gdb) break fib if n == 5 Breakpoint 1 at 0x555555554655: file code01.c, line 5. (gdb) run Starting program: /home/rakeb/tmp/exec01 Breakpoint 1, fib (n=5) at code01.c:5 5 assert(n >= 5); (gdb)
میبینید؟ دقیقا پشت اوّلین دستور از تابع وقتی که ورودیش n=5 هست برنامه متوقّف شده. این جا هم میتونیم دستور bt رو بزنیم تا روند صدا شدن توابع رو ببینیم.
(gdb) bt #0 fib (n=5) at code01.c:5 #1 0x0000555555554692 in fib (n=6) at code01.c:8 #2 0x0000555555554692 in fib (n=7) at code01.c:8 #3 0x0000555555554692 in fib (n=8) at code01.c:8 #4 0x0000555555554692 in fib (n=9) at code01.c:8 #5 0x0000555555554692 in fib (n=10) at code01.c:8 #6 0x00005555555546bd in main () at code01.c:15 (gdb)
خب حالا فرض کنید میخوایم قبل از ادامهی برنامه، یه مقدار پرسه بزنیم توی این توابع و مقدار بعضی متغیّرها رو با دستور print نگاه کنیم! خب اگر این متغیّرها توی همین تابع فعلی باشن که اوکیه، ولی اگر توی فریم توابع بالادستی باشن چی؟ واضحه که همین طوری نمیتونیم مقدار n به ازای فراخوانی تابع مربوط به خط #3 رو نگاه کنیم! (حالا این جا n ورودی تابع هست و بدیهیه مقدارش چنده. امّا تو موارد مختلف، به جای این n میشه هر متغیّر لوکالی مد نظرمون باشه که مقدارش توی backtrace قابل مشاهده نباشه.
مفهوم این ۲ تا دستور خیلی ساده هست! با دستور up، میرید توی تابعی که تابع فعلی رو صدا زده. یعنی اگر توی تابع مربوط به خط iام از خروجی backtrace باشید، منتقل میشید به خط i+1. دستور down هم خلاف این کار رو میکنه و منتقل میشید به خط i-1! دقّت کنید که این منتقل شدن، هیچ تاثیری توی روند اجرای برنامه نداره. صرفا شما به صورت مجازی به اون جا منتقل میشید تا بتونید مقدار توابع رو نگاه کنید و تغییر بدید.
لازم به ذکره که این ۲ تا دستور، میتونن یه آرگومان اختیاری هم بگیرن که بفهمن به جای یک لایه، چند لایه برن بالا یا پایین. مثلا دستور up 5 شما رو منتقل میکنه به خط i + 5.
(gdb) up #1 0x0000555555554692 in fib (n=6) at code01.c:8 8 int x = fib(n - 1); (gdb) print n $1 = 6 (gdb) up 2 #3 0x0000555555554692 in fib (n=8) at code01.c:8 8 int x = fib(n - 1); (gdb) print n $2 = 8 (gdb) down #2 0x0000555555554692 in fib (n=7) at code01.c:8 8 int x = fib(n - 1); (gdb) down #1 0x0000555555554692 in fib (n=6) at code01.c:8 8 int x = fib(n - 1); (gdb) down #0 fib (n=5) at code01.c:5 5 assert(n >= 5); (gdb) down Bottom (innermost) frame selected; you cannot go down. (gdb)
حالا میخوایم با یه روش کاملا احمقانه، کاری کنیم که برنامهمون وقتی n<=4 هست، به خط assert نرسه. برای این کار میخوایم با یه روش مشابه، یه breakpoint روی ورودی تابع fib به ازای زمانهایی که n<=4 هست بذاریم و توی این حالات، قبل از این که روند اجرای برنامه به assert برسه، خودمون دستی مقدار واقعی fib(n) رو return کنیم!
با این دستور میتونیم اطّلاعات breakpointهایی که توی برنامهمون وجود دارن رو ببینیم! در مورد همین مثال:
(gdb) info breakpoints Num Type Disp Enb Address What 1 breakpoint keep y 0x0000555555554655 in fib at code01.c:5 stop only if n == 5 breakpoint already hit 1 time (gdb)
همون طور که میبینید، اطّلاعات مختلفی رو در مورد همون یه breakpoint که توی برنامه داشتیم چاپ کرده. خصوصیتی که در ادامه باهاش کار داریم، Enb یا Enabled هست.
لازم به ذکره اگر breakpointهای بیشتری هم وجود داشته باشن، توی این لیست با Numهای مختلف نشون داده میشن.
ما میتونیم breakpointها رو غیرفعّال کنیم و یا پاکشون کنیم! برای غیر فعّال کردنشون کافیه از دستور زیر استفاده کنیم:
(gdb) disable breakpoints 1
که عدد ۱ در انتهای دستور، نشاندهندهی Num مربوط به breakpointی هستش که میخوایم غیرفعّالش کنیم.
به طریق مشابه با استفاده از دستور enable به جای disable میتونیم یک breakpoint رو فعّال کنیم و با استفاده از delete به جای disable میتونیم اون breakpoint رو پاک کنیم. تغییراتی که این دستورات اعمال میکنن رو به راحتی با دستور info breakpoints میتونیم مشاهده کنیم.
حالا برای این که اون روش احمقانه مربوط به جلوگیری از کرش کردن برنامه رو پیادهسازی کنیم، کافیه اوّل دستورات زیر رو اجرا کنیم:
(gdb) break fib if n <= 4 Breakpoint 2 at 0x555555554655: file code01.c, line 5. (gdb) run Starting program: /home/rakeb/tmp/exec01 Breakpoint 2, fib (n=4) at code01.c:5 5 assert(n >= 5); (gdb)
خب میدونیم که خروجی fib به ازای n=4 باید برابر با ۳ و به ازای n=3 باید برابر با ۲ باشه. برای همین کافیه هر وقت breakpointمون به fib(3) رسید از دستور return 2 و هر وقت به fib(4) رسید از دستور return 3 استفاده کنیم.
البته تابع fib(3) و fib(4) دفعات زیادی صدا خواهد شد و ما در نهایت پیر میشیم تا اون دستورات رو اجرا کنیم! این جا صرفا میخواستم بگم از لحاظ تئوری این کار ممکنه. :)) اگر خیلی مُصر هستید که این کار رو تجربه کنید، میتونید توی برنامهی code01.c به جای فراخوانی fib(10)، تابع fib رو به ازای یک n کوچکتر فراخوانی کنید و یا موقعی که breakpoint میذارید، به جای قرار دادن n<=4 از یه عدد بزرگتر استفاده کنید تا دفعات کمتری به اون breakpoint برسیم.
حالا که تا این جای کار اومدیم، بیاید با چند تا دستور دیگه هم آشنا بشیم! ولی قبل از هر چیز، خط مربوط به assert رو از برنامه پاک کنید و برنامه رو مجدّدا اجرا کنید. مثل حالت قبل، یه breakpoint روی تابع fib به ازای حالاتی که n=5 هست میذاریم!
(gdb) break fib if n == 5 Breakpoint 1 at 0x605: file code01.c, line 5. (gdb) run Starting program: /home/rakeb/tmp/exec01 Breakpoint 1, fib (n=5) at code01.c:5 9 if (n <= 1) (gdb)
این جا میخوایم ببینیم سورس کد برنامهمون چه شکلیه! برای این کار به راحتی دستور list رو اجرا میکنیم:
(gdb) list 4 5 6 7 int fib(int n) 8 { 9 if (n <= 1) 10 return n; 11 int x = fib(n - 1); 12 int y = fib(n - 2); 13 return x + y; (gdb) list 14 } 15 16 int main() 17 { 18 fib(10); 19 return 0; 20 } (gdb) list 0 1 #include <assert.h> 2 3 4 5 6 7 int fib(int n) 8 { 9 if (n <= 1) 10 return n; (gdb) list 11 int x = fib(n - 1); 12 int y = fib(n - 2); 13 return x + y; 14 } 15 16 int main() 17 { 18 fib(10); 19 return 0; 20 } (gdb) list Line number 21 out of range; code01.c has 20 lines. (gdb)
در واقع دستور list، دفعهی اوّل که اجرا میشه ۱۰ خط از برنامهمون(با مرکزیت خط فعلی!) رو چاپ میکنه. دفعهی بعد، ۱۰ خط بعدی رو چاپ میکنه و همین طور ادامه میده تا به پایان برنامه برسه. این دستور البته یه آرگومان اختیاری هم میتونه دریافت کنه. این آرگومان، به نوعی بهش میگه از چه خطی شروع به نمایش کد کنه و ادامه بده.
در کل این دستور خیلی چیز پیچیدهای نیست و با یه خرده بازی کردن باهاش میشه طرز کارش رو فهمید. D:
این دستور خیلی کاربردیه!! طرز عملکردش خیلی شبیه دستور next هست. با این تفاوت که وقتی به فراخوانی تابعی میرسیم و next رو اجرا میکنیم، از پیچدگیهای درونی اون تابع میگذره و در واقع توی همین تابع فعلی میمونه و سراغ دستور بعد از اون فراخوانی میره. امّا step این طوری نیست و موقعی که قبل از فراخوانی یه تابع اجراش میکنیم، وارد اون تابع میشه. مثلا توی همین نقطهای که برنامهمون وایساده، چند بار اجراش میکنیم!
(gdb) step 11 int x = fib(n - 1); (gdb) step fib (n=4) at code01.c:9 9 if (n <= 1) (gdb) bt #0 fib (n=4) at code01.c:9 #1 0x000055555555461d in fib (n=5) at code01.c:11 #2 0x000055555555461d in fib (n=6) at code01.c:11 #3 0x000055555555461d in fib (n=7) at code01.c:11 #4 0x000055555555461d in fib (n=8) at code01.c:11 #5 0x000055555555461d in fib (n=9) at code01.c:11 #6 0x000055555555461d in fib (n=10) at code01.c:11 #7 0x0000555555554648 in main () at code01.c:18 (gdb) step 11 int x = fib(n - 1); (gdb)
همون طور که میبینید، به نقطهی فراخوانی fib(n-1) میرسیم و با اجرای دستور step، از frame کنونی که fib(5) بود، وارد fib(4) شدیم.
مثل دستورات up، down و next که یه آرگومان اختیاری میگرفتن که تعداد دفعات تکرارشون بود، میتونیم یه آرگومان به step بدیم و به جای این که یک مرحله یک مرحله جلو بریم، چند مرحله چند مرحله جلو بریم! مثلا به جای این ۲ تا step که توی همین مثال اجرا کردیم، میتونستیم بزنیم step 2 و کارمون کمی خلاصهتر بشه.
این دستور در کنار دستور step خیلی به کار میآد. وقتی وسط اجرای یه تابعی هستیم، دستور finish باعث میشه برنامه تا آخر این تابع ادامه پیدا کنه و در نهایت به جایی که این تابع فراخوانی شده برگردیم. البته به شرطی که این وسط به breakpointی گیر نکنیم. D:
توی همین مثال فعلیمون، برای این که به fib(5) برگردیم:
(gdb) finish Run till exit from #0 fib (n=4) at code01.c:11 0x000055555555461d in fib (n=5) at code01.c:11 11 int x = fib(n - 1); Value returned is $1 = 3 (gdb) step 12 int y = fib(n - 2); (gdb) print x $2 = 3 (gdb)
همون طور که خود gdb هم بهمون گفت، اجرای fib(4) تموم شد و به نقطهای که این تابع فراخوانی شده بود برگشتیم. یعنی خط ۱۱م برنامه توی fib(5).
خب... این سری از آموزش gdb رو هم توی این نقطه تموم میکنیم. ولی یادتون باشه با استفاده از دستور help توی gdb میتونید هر اطّلاعاتی خواستید راجع به دستورات مختلف به دست بیارید و کاستیهای این آموزش رو جبران کنید. در واقع هدف من از این آموزشها اینه که کسایی که مشتاق هستن، با اصول اوّلیهی gdb آشنا بشن تا بتونن بعدا با سرچ و help و... راحتتر گلیم خودشون رو از آب بیرون بکشن.
وگرنه، نه من اون قدری gdb بلد هستم که بخوام کلّش رو یاد بدم، نه کل gdb نیاز همهی آدمها میشه و نه اصلا gdb اون قدری دنیای کوچیکی داره که حتّی اگر من بلد بودم، حوصله داشتم در موردش بنویسم. :))
همین دیگه...
و من الله التوفیق!