Սկիզբ » Ուսումնական նյութեր » Օպերացիոն համակարգեր » *Nix-եր » Ծրագրի կարգաբերում gdb -ի միջոցով։

Ծրագրի կարգաբերում gdb -ի միջոցով։

| Մարտ 16, 2014 | Մեկնաբանված չէ |

regsgdb -ն gnu նախագծի source code մակարդակի, բազմապլատֆորմային debugger է։ source code մակարդակը նշանակում է, որ  տվյալ debugger-ով կարելի է կարգաբերել միայն այն ծրագրերը, որոնք թողարկվել են արդեն բեռնված օպերացիոն համակարգում։ Բացի այդ source code debugger -ի դեպքում մեքենայական կոդի փոխարեն, տեսնում ենք ծրագրի ելակետային կոդը, որը կարող է գրված լինել ինչ-որ բարձր մակարդակի լեզվով, օրինակ՝ C/C++:

cgdb

Նկար 1։ cgdb կարգաբերիչ, որի հիմքում ընկած է gdb -ն։

Դա ահագին հեշտացնում է ծրագրի կարգաբերման գործընթացը, քանի որ ի տարբերություն միջուկի մակարդակի debugger -ի (օրինակ softice, syser debugger , որոնք բեռվնում են դեռ նաղքան օպերացիոն համակարգի բեռնվելը և այս debugger -ների միջոցով կարելի է debug անել անմիջապես օպերացիոն համակարգը, դրայվերները և այլն), որտեղ մենք տեսնում ենք միայն մեքենայական հրամաններն ու հիշողության մեջ երկուական/կատարողական ֆայլի կոնտեքստը (memory dump, stack, cpu registers), այստեղ մենք տեսնում ենք ելակետային կոդը (բայց այստեղ նույնպես կարող ենք տեսնել memory dump-ի, stack-ի, cpu registers -ների պարունակությունը),

syser-debugger

Նկար 2։ Syser kernel mode Debugger

այսինքն փոփոխականների, ֆունկցիաների, տվյալների կառուցվածքների անունները։  Բազմապլատֆորմայնությունը նշանակում է, որ նախապես կոմպիլացնելով, այն կարելի է օգտագործել տարբեր պլատֆորմներում, օրինակ՝ ARM, Alpha, x86, PowerPc, SPARC և այլն։

gdb սկզբնական շրջանում գրվել է Ռիչարդ Սթոլմանի կողմից 1988 թ․-ին։ 1990-1993թթ․ դրա մշակումը շարունակվել է Ջոն Ջիմլերի կողմից, իսկ ներկայումս դրանով զբաղված է gdb ղեկավարման կոմիտեն (GDB Steering Committee):

Լավ պատմությունը թողնենք պատմաբաններին և անցնենք gdb -ի հրամանների դիտարկմանը փոքրիկ օրինակի վրա 🙂

#include <iostream>

int sum(int a, int b)
{
	int c = a + b;
	return c;
}

int main()
{
	int a = 5;
	int b = 6;
        int c = sum(a, b);  
	return 0;
}
Բացում ենք տերմինալը և գրում ենք
gdb ./main
Screenshot from 2014-03-16 13:35:44
Ծրագրի կոդը տեսնելու համար կարող ենք օգտագործել “list” կամ կրճատ “l” հրամանը՝ որպես արգումենտ տալով տողի համարը կամ ֆունկցիայի անունը։
list
Բայց այս տեսքով debug անելը չափազանց անհարմար է, դրա համար gdb -ն պետք է թողարկել տեքստային ռեժիմով։ Դուրս ենք գալիս gdb -ից տերմինալում գրելով q հրմանը՝
Screenshot from 2014-03-16 13:41:38
 
gdb -ն տեքստային ռեժիմով բացելու համար տերմինալում գրում ենք (կարող ենք նաև բացել ոչ տեքստային ռեժիմով  և գրել “ref” հրամանը)
gdb ./main -tui
‘tui’ հրամանի փոխարեն կարող ենք սեղմել Ctrl-X և A և gdb -ն նորից կբացվի տեքստային ռեժիմով։

tui

 

Ծրագիրը քայլ առ քայլ կատարելու համար, անհրաժեշտ է տեղադրել breakpoint և թողարկել ծրագիրը։ breakpoint կարող ենք դնել տողի համարի վրա, ֆունկցիայի անվան վրա և կարող ենք դնել նաև պայմանական breakpoint -ներ։

Տողի վրա breakpoint կարող ենք դնել՝ գրելով “b [տողի համարը]”, օրինակ՝

b 20

կամ եթե ծրագիրը ունի շատ ֆայլեր, ապա մեկ այլ ֆայլի համապատասխան տեղում  breakpoint դնելու համար գրում ենք “b [ֆայլի անուն/կամ ճանապարհ]։[տողի համար]”, օրինակ՝

 b main.cpp:6

Ֆունկցիայի անվան վրա breakpoint կարող ենք դնել՝ գրելով “b [ֆայլի անուն/կամ ճանապարհ]”:[ֆունկցիայի անուն]

b ./main.cpp:sum
Տեսնելու համար, թե ինչ ինչ breakpoint ունենք դրված, գրում ենք․
info b
Պայմանական breakpoint դնում ենք հետևյալ կերպ՝ “break [ֆայլի անունը կամ ֆայլիճանապարհը]։[տողի համարը կամ ֆունկցիայի անունը] [պայմանը], “break” -ի փոխարեն կարող ենք գրել կարճ՝ “b”։
b main.cpp:25 if b > a
Սա նշանակում է, որ եթե b > a -ից պետք է կանգնել main.cpp ֆայլի 25 -րդ տողի վրա։ Եթե debug ենք անում ընթացիկ ֆայլը, ապա կարող ենք չգրել main.cpp -ն և հրամանը կունենա հետևյալ տեսքը
b 25 if b > a
Նշված բոլոր ձևերը ցույց են տրված ստորև բերված նկարում։

break

breakpoint ջնջելու համար գրում ենք “d [breakpoint Num]”

d 2
breakpoint -ները լինում են նաև ապարատային։ Ի տարբերություն ծրագրային breakpoint-ների, ապարատայինները ավելի արագ են աշխատում, քանի որ այդ դեպքում օգտագործվում են հատուկ մեքենայական debug -ի ռեգիստրներ, օրինակ intel -ի մեքենաների համար այդ ռեգիստորներն են DRO-ից DR3։ Սական ապարատային breakpoint -ի թիվը սահմանափակ է, քանի որ սահմանափակ են  debug -ի ռեգիստրները։ Այսինքն intel -ի մեքենաների դեպքում մենք ունենք ընդամենը 4 հատ hardware breakpoint: hardware breakpoint դնում ենք “hb” հրամանով․
hb 6
Երբ արդեն breakpoint-ը դրված է կարող ենք թողարկել ծրագիրը, որի համար տերմինալում հավաքում ենք “run” կամ “r” հրամանը
r
Եթե անհրաժեշտ է ծրագրին արտաքինից պարամետրեր փոխանցել, ապա այդ դեպքում ծրագիրը թողարկում ենք հետևյալ կերպ՝ “r arg1 arg2 arg3
r  3  4   "my string"
Բացի այդ “run” հրամանին որպես արգումենտ կարելի է տալ կարճ python կամ perl սկրիպտեր, օրինակ՝
r `python -c 'print "A"*10'`

python_

Այս դեպքում ծրագրին արտաքինից կփոխանցվի “AAAAAAAAAA” տողը, որը կպահվի argv[1] -ի մեջ։
Ծրագրին կամ ֆունկցիային փոխանցված արգումենտները կարող ենք տեսնել, գրելով “info args” կամ “i args” հրամանը, այնպես ինչպես ցույց է տրված վերը բերված նկարում, օրինակ՝
i args
և էկրանին կտպվի
i args
argc = 2
argv = 0xbffff264

Լոկալ փոփոխականների արժեքները տեսնելու համար գրում ենք “info locals” կամ “i locals”

i locals

locals

 

որևէ կոնկրետ փոփոխականի արժեք տպելու համար գրում ենք “print [variable_name]” կամ “p [variable_name]”, օրինակ՝

p a

source code -ին զուգահեռ disassembler -ի կոդւ տեսնելու համար գրում ենք “layout split”

layout split

disasm

layout regs հրամանով տեսնում ենք պրոցեսորի ռեգիստրները

layout regs

regs

Ընդ որում նույն բանը կարող ենք տեսնել նաև տեքստային ռեժիմում գրելով՝

info reg

Կարող ենք տպել նաև առաձին ռեգիստրի արժեքը՝ տալով դրա անունը՝ info reg $reg_name

info reg $ebp // կտպվի օրինակ ebp   0xbffff1c8    0xbffff1c8

Եթե ուզում ենք տպել օրինակ որևէ ռեգիստրից սկսած n հատ արժեք, ապա գրում ենք հետևյալ հրամանը

x/30dw $esp

Այս դեպքում կտպվի dword ձևաչափով $esp ռեգիստրի base -ից սկսած 30 հատ  արժեք։

30regesp

 

Հաջորդ տող անցնելու համար օգտագործում ենք “next” կամ “n” հրամանը

n

նկարում պատկերված օրինակում “n” հրամանով 20-րդ տողից անցում կատարվեց 21 տողը

next

 

“continue” կամ որ նույնն է “c” հրամանով շարունակում ենք ծրագրի կատարումը

c

Այդպիսի դեպք կարող է առաջանալ այն ժամանակ, երբ որ ասենք կանգնել ենք breakpoint -ի վրա և ուզում ենք շարունակել ծրագրի կատարումը ավտոմատ ռեժիմով (ոչ քայլ-քայլ) կամ մինչև մյուս breakpoint -ին հանդիպելը։
Հիմա, ենթադրենք հասել ենք 25-րդ տողին, որտեղ գրված է ֆունկցիայի կանչ և մեզ պետք է մտնել այդ ֆունկցիայի մեջ։ Եվ եթե այժմ օգտագործենք “next “հրամանը, ապա ոչ մի բան էլ չենք տեսնի, քանի որ այն մեզ չի տանի get_sum(…) ֆունկցիայի կոդի մեջ։ Այս դեպքում օգտագործում ենք “step” կամ “s”

հրամանը՝

s

Ենթադրենք հասել ենք sum(…) ֆունկցիայի  6 -րդ տողի վրա և layout split հրամանով բացել ենք disassembler կոդը և ուզում ենք տեսնել թե մեր “int c = a + b” c++ -ական կոդը ինչպես է տակից կատարվում՝ ասսեմբլերի մակարդակով։ Քանի որ,  համարյա միշտ բարձ լեզի մեկ հրամանը կատարվում է մի քանի մեքենայական հրամաններ միջոցով և մեզ պետք է ոչ թե 6-րդ տողից անմիջապես գալ 7-րդ տող այլ անցնել ասսեմբլերական հրամանների վրայով, այդ դեպքում օգտագործում ենք “stepi” հրամանը

stepiassm

 

Որպեսզի հասկանանք թե ինչ է տեղի ունենում մեր կոդում հիշենք ստեկի կառուցվածքը

stack

 

Նկարում լոկալ փոփոխականները պահվում են ebp (bp) ռեգիստրի վերևում, իսկ ֆունկցիայի արգումենտները ներքևում։ Այն մեքենայական ճարտարապետություններում, որտեղ համակարգային ստեկը աճում է նվազման ուղղությամբ, առաջին արգումենտի հասցեն կստանանք ebp ռեգիստրի հասցեին գումարելով 8 բայթ ( կախված մեքենայի ճարտարապետությունից կարող է լինել տարբեր 4 բայթից), ինչու 8 բայթ քանի որ ebp -ից հետո 4 բայթ հատկացվում է վերադարձման հասցեյի պահման համար, իսկ վերադարձվող հասցեյից անմիջապես հետո գալիս են արգումենտները, եթե այդպիսիք կան։ Դա նշանակում է, որ մեր առաջին արգումենտի` a-ի հասցեն կլնի  addr(a) = $ebp + 4 + 4 կամ addr(a) = $ebp + 0x8, b -ի հասցեն կլինի՝ addr(b) = $ebp + 4 + 4 + 4 կամ addr(a) = $ebp + 0xC:

Մեր int c = a + b գործողության կատարման համար, ասսեմբլերի մակարդակով կատարվում է 4 հրաման․ առաջին երկու հրամանով a և b արժեքները պահվում են առանձին ռեգիստրների մեջ, իսկ 3-րդ հրամանով կատարվում  է դրանց գումարումը, իսկ 4-րդ հրամանով էլ $ebp – 4 հասցեում տեղծվում է լոկալ փոփոխական, որտեղ պահվում է գումարման արժեքը։ Ասվածը կարելի է տեսնել ստորև բերված նկարում․

 

regsss

 

Ասվածը նշանակում է, որ մինչև 6 -րդ տողի հրամանը կատարվի մենք 4 հատ stepi հրամանով կանցնենք հետևյալ 4 տողերի վրայով․

0x8048572 <sum(int, int)+6>             mov    0xc(%ebp),%eax                                                                                                                                        
0x8048575 <sum(int, int)+9>             mov    0x8(%ebp),%edx                                                                                                                                       
0x8048578 <sum(int, int)+12>            add    %edx,%eax                                                                                                                                               
0x804857a <sum(int, int)+14>            mov    %eax,-0x4(%ebp)

Եթե ծրագրում ունենք segmentation fault, ապա դրա պատճառը նույնպես կարելի է գտնել gdb -ի միջոցով։

ՈՒնենք հետևյալ կոդը, որը պարունակում է segmentation fault սխալ առաջացնող տող, քանի որ ոչ վալիդ (զրոյական հասցեյով) ցուցիչին արժեք է վերագրվում։

#include <iostream>

int sum(int a, int b)
{
	int c = a + b;
	return c;
}

int* get_sum(int a, int b)
{
	int* c = 0;
	int s = sum(a, b);
	*c = s;
	return c;
}

int main(int argc, char** argv)
{
	int a = 5;
	int b = 6;
	if (b > a)
	{
		int* c = get_sum(a, b);
	}
	return 0;
}
վերագրվում

Նման սխալներ պարունակող ծրագրերը debug անելու համար վարվում ենք հետևյալ կերպ։ Նախ ասենք, որ segmentation fault -ից հետո gdb ստեղծում է core  անունով ֆայլ, որտեղ պահպանվում է  անհաջող ավարտի վերաբերյալ ինֆորմացիա։ Լռության սկզբունքով core ֆայլի չափը դրված է 0 և հետևաբար մենք պետք է բացահայտ տանք դրա չափը, որպեսզի այն ստեղծվի։ Բացում ենք տերմինալը (հենց տերմինալը և ոչ թե gdb -ն) և գրում ենք հետևյալ հրամանը

ulimit -c unlimited

Այս հրամանը նշանակում է, որ gdb -ն կարող է կամայական մեծությամբ core ֆայլ գեներացնել։

Հիմա, գրում ենք հետևյալ հրամանը և թողարկում ենք gdb -ն՝ մեր կատարողական և core ֆայլերով միաժամանակ։

gdb ./main core -tui

Դրանից հետո gdb -ն կկանգնի հենց այն տողի վրա որտեղ տեղի է ունեցել segmentation fault։

backtrace

Որպեսզի տեսնենք թե ինչ ֆունկցիաների կանչ է տեղի ունեցել մինչև այս, օգտագործում ենք “backtrace” կամ “bt” հրամանը։

bt

backtrace հրամանին կարող ենք փոխանցել նաև արգումենտ, որ միջոցով կարող ենք ստանալ միայն վերջին n հատ ֆունկցիաների կանչը

bt 2

Այս հրամանի արդյունքում կտպվեն վերջին կանչված 2 ֆունկցիանեը։

Մենք կարող ենք նաև ֆուկցիա առ ֆունկցիա շարժվել ֆունկցիաների կանչի շղթայի վրայով՝ օգտագործելով “down” և “up” հրամանները։

up  // հրամանով գնում ենք հետ
down // հրամանով գնում ենք առաջ, մինչև ընթացիկ ֆունկցիան
Յուրաքանչյուր նոր ֆունկցիայի համար ստեկից հատկացվում է հիշողություն՝ այդ ֆունկցիայի արգումենտների, լոկալ փոփոխականների, վերադարձման արժեքի և կոմպիլյատորից կախված ծառայողական ինֆորմացիայի պահպանման համար։ Ամեն ֆունկցիայի համար ստեկից զբաղեցված հիշողության հատվածը կոչվում է frame: Մենք մեր օրինակում ունենք 2 frame, քանի որ ունենք 2 ֆունկցիա (մինչ segmentation fault -ը, եթե segmentation fault չլիներ ապա կունենային 3 ֆունկցիա և 3 frame): frame մասին ինֆորմացիան կարող ենք տպել հետևյալ հրամանով
info frame

Արդյունք կտպվի՝

frame

Իսկ կոնկրետ frame -ի մասին ինֆորմացիա ստանալու համար գրում ենք “frame [frame_number]”

frame 1

Այս հրամանով կտպվի 1-ին frame -ի վերաբերյալ ինֆորմացիան։

gdb -ն շատ հզոր debugger  է և ունի շատ մեծ թվով հրամաններ և հետևաբար մեկ հոդվածով դրանք բոլորը թվարկել հնարավոր չէ և նյութում բերված են իմ պրակտիկայում առավել հաճախ կիրառվող հրամանները։

Ծրագրի կարգաբերում gdb -ի միջոցով։, 10.0 out of 10 based on 7 ratings

Նշագրեր: , , ,

Բաժին: *Nix-եր, Assembler, C և C++, WIndows, Լինուքս/Յունիքս հրամաններ, Օպերացիոն համակարգեր

Կիսվել , տարածել , պահպանել

VN:F [1.9.20_1166]
Rating: 10.0/10 (7 votes cast)

Մեկնաբանեք

Կհաստատվեն միայն մեսրոպատառ հայերենով գրած մեկնաբանությունները

312