Ծրագրավորողը և օպերացիոն համակարգը։ Մաս III

Երկրորդ մասում մենք ավարտեցին առաջին տողի վերլուծությունը։ Արդյունքում տեսանք, որ ֆայլային համակարգում ստեղծվեց ֆայլի նկարագրիչ և մեզ վերադարձվեց նրա համարը։ Այդ համարը օգտագործվում է գրեթե բոլոր ֆունկցիաների կողմից, ովքեր պետք է ինչ-որ գործողություններ կատարեն այդ, արդեն բացված, ֆայլի հետ։

Հիշեցնեմ, որ դիտարկում ենք հետևյալ ծրագրային հատվածը՝

int fd = open(“/home/me/file1”, O_RDONLY);

read(fd, buf, 10);

Ուշադիր դիտորդը պետք է նկատեր, որ առաջին տողն իրականում դեռ չի ավարտվել։ Այս պահին ավարտվել է միայն open() ֆունկցիայի կանչը։ Դեռ պետք է հասկանանք, թե ինչ է ընդհանրապես տեղի ունենում ֆունկցիայի կանչի ընթացքում և ինչպես է վերադարձվում նրա վերադարձրած արժեքը։ Ֆունկցիաները, կամ ավելի կոռռեկտ կլինի ասել, պրոցեդուրաները, կենտրոնական պրոցեսորի կողմից տրամադրվող հասկացություն են։ Ժամանակակից լուրջ բոլոր կենտրոնական պրոցեսրների ասսեմբլեր լեզուներում կան պրոցեդուրա հայտարարելու և կանչելու հրամաններ։ Օրինակ՝ Intel x86 ճարտարապետությունում կան համապատասխանաբար proc նշիչը և call հրամանը։ Ինչպես նաև արժե նշել ret հրամանի մասին, որը պետք է գրվի պրոցեդուրայի վերջում։

Ֆոն Նեյմանի ցիկլը ենթադրում է, որ պրոցեսորը հերթով անցնում է հրամանների վրայով և կատարում դրանք։ Մինչդեռ պետք է հասկանանք, որ կան հրամաններ, որոնք ստիպում են պրոցեսորին անցնել մեկ այլ հրամանի, այլ ոչ թե հաջորդին։ Այդպիսիք են call, jmp, jne, jnz և էլի մի շարք հրամաններ։ Դրանց շարքում առանձնակի ուշադրության է արժանի call հրամանը նրանով, որ պրոցեդուրայի ավարտից հետո պրոցեսորը պետք է հետ վերադառնա այդ կանչի հրամանին և կատարի դրան հաջորդող հրամանը։ Օրինակում բերված մյուս հրամանների դեպքերում այդպիսի խնդիր չկա։ Ինչպես գիտենք ընթացիկ հրամանի հասցեն Intel x86 պրոցեսորներում պահվում է IP ռեգիստրում։ Այսպիսով առաջանում է խնդիր՝ պրոցեդուրայի կանչի պահին հիշել IP ռեգիստրի արժեքը, որպեսզի հետագայում հնարավոր լինի վերադառնալ կանչի հրամանին հաջորդող հրամանին։ Իսկ որտե՞ղ պահել այն։ Ֆունկցիաների/պրոցեդուրաների կանչի մեխանիզմը հենց այդ միջոցների բազմությունն է, որն ապահովում է այս խնդրի լուծումը և ծրագրի անխափան աշխատանքը։

Երևի շատերը լսած կլինեն կանչի ստեկի մասին (call stack). Հենց նրա միջոցով է իրականացվում նշված մեխանիզմը։ Որպեսզի հասկանանք նրա իմաստը պետք է մի փոքր ավելի խորանանք ֆունկցիաների կանչի երևույթի մեջ։ Այսպես, եթե f() ֆունկցիան կանչում է g() ֆունկցիան, ապա մինչև g() — ն չավարտվի, f()-ը չի կարող ավարտվել։ Այսինքն ֆունկցիաների կանչի մեխանիզմն ինչ-որ առումով հանդիսանում է Last-In-First-Out տիպի կոնստրուկցիա։ Ստեկը ևս այդպիսի հատկությամբ օժտված տվյալների կառուցվածք է, այդ իսկ պատճառով ֆունկցիաների կանչի մեխանիզմն իրականացված է հենց ստեկի, այլ ոչ թե հերթի կամ բինար ծառի միջոցով։

Բայց և այնպես՝ ի՞նչ է պահվում այդ ստեկում։ Վերը բերված դատողություններից կարելի է հեշտությամբ դուրս բերել այն, որ ստեկում պետք է պահվի վերադարձի հասցեն՝ IP ռեգիստրի արժեքը։ Բայց պատմությունն այդքանով չի ավարտվում։ Բանն այն է, որ ստեկը, կոմպիլյատորի և օպերացիոն համակարգի ջանքերով, օգտագործվում է մի շարք այլ նպատակներով ևս։ Մասնավորապես՝ ստեկի միջոցով է կոմպիլյատորը փոխանցում ֆունկցիայի արգումնենտները և վերադարձվող արժեքը։ Բացի դրանից ստեկում են պահվում ֆունկցիաների լոկալ փոփոխականները։ Սա էլ դեռ ամենը չէ։ Պրոցեսորները հիմնականում տրամադրում են ստեկի աշխատող push և pop հրամաններ, որոնք հասանելի են ծրագրավորողներին և կարող են կանչվել տարբեր առիթներով։

Այս ամենը վերահսկողության տակ պահելւ նպատակով ստեկը բաժանվում է հատվածների (stack frame), որոնցից յուրաքանչյուրը հատկացվում է մի կանչվող ֆունկցիայի։ Այսինքն ֆունկցիայի կանչի պահին ժամանակավորապես «փակվում է» ստեկի ակտիվ մասը, հատկացվում է նոր հատված կանչվող ֆունկցիայի համար, որի ավարտից հետո պետք է հետ վերականգնել կանչող ֆունկցիայի ստեկի հատվածը։ Պրոցեսորը ստեկի հետ աշխատում է օգտվելով երկու ռեգիստրներից՝ base pointer (bp) և stack pointer (sp)։ Առաջինը պարունակում է տալիս ստեկի (ակտիվ մասի) հատակի հասցեն, իսկ երկրորդը՝ գագաթի հասցեն։ Այսպիսով՝ ստեկի ակտիվ մասի հասցեն պահելու և վերականգնելու նպատակով բավական է պահել bp ռեգիստրի արժեքը։ Իսկ պահելու համար կրկին օգտագործվում է այդ նույն ստեկը։

Որքան էլ , որ այս պատմությունը խճճված լինի, այն ունի հստակ տրամաբանություն։ Տրամաբանությունը հետևյալն է՝ կա խնդիր, կապված ֆունկցիաների և դրանց կանչի հետ, և պետք է տալ ամենահեշտ, ամենահասանելի լուծումը։ Որքան էլ, որ այն խճճված չթվա, սա է այս պահին մարդկության հնարած ամենահարմար եղանակը։ Ընթերցողը կարող է ազատ լինել լուծման սեփական տարբերակը մշակելու և ներկայացնելու հարցում։

Ի վերջո, ֆունկցիայի ավարտից հետո, երբ արդեն փախվում է ստեկի ակտիվ հատվածը և վերականգնվում է կանչող ֆունկցիայինը, պետք է նրա համար հասանելի դարձնել կանչված ֆունկցիայի վերադարձրած արժեքը։ Եվ կրկին, ամենահարմար տեղը այդ նույն ստեկն է։

Վերադառնալով մեր դիտարկած կոդի հատվածին՝ պետք է նշել, որ հայտարարվում է նոր փոփոխական, որին վերագրվում է open() ֆունկցիայի վերադարձրած դեսկրոիպտորի համարը, որն արդեն հայտնվել էր ստեկում։

Պատմության ավարտին մնացին հաշված րոպեներ։ Իրադարձությունները գնալով ավելի սրընթաց են դառնում, պատմության գլխավոր հերոսի գործն ամեն պահի ավելի է բարդանում։ Բայց հանգուցալուծումը անխուսափելիորեն կգա, երբ հերոսն ի վերջո կկարդա առաջին տասը բայթերը՝ իր համար արդեն նվիրական դարձած ֆայլից։

Շարունակելի․․․