Թույլ տվեք առանց երկար֊բարակ նախաբանների միանգամից անցնել գործի 🙂
system call ֊ը ինտերֆեյս է, որը թույլ է user-space ծրագրերին դիմել օպերացիոն համակարգի միջուկին (ring 0 ) և պահանջել նրանից որոշակի գործողությունների կատարում, օրինակ ֆայլ բացել, դրա մեջ ինչ֊որ բաներ գրել, նոր պրոցեսս ստեղծել և այլն։ Այլ կերպ ասած, system call ֊ը միջոց է user mode ֊ից անվտանգ փոխազդելու օպերացիոն համակարգի միջուկի հետ։ Տեխնիկական իմաստով system call ֊ը նշանակում է մեքենայական հրաման, որը բարձրացնում են cpu ֊ի արտոնությունների մակարդակը (privilege level) և փոխանցում է կառավարումը միջուկի կոդի նախապես սահմանված հատվածին։
linux ֊ում համակարգային կանչ իրականացնելու համար կարելի է օգտագործել syscall մեքենայական հրամանը կան 0x80 ընդհատումը (x86 համատեղելի մեքենաների համար)։ Դրանց մի միջև կան որոշակի տարբերություններ, բայց մենք դրանց մեջ չենք խորանա և մեր կոդերում կօգտագործենք 0x80 ընդհատումը։
Յուրաքանչյուր համակարգային կանչ ունի որոշակի անուն և համար, որոնք կարելի է նայել հետևյալ տեղից
/usr/include/asm/unistd_32.h
Բացելով այդ ֆայլը՝ կտեսնենք հետևյալ պատկերը
Նկարից պարզ երևում է, որ օրինակ նոր պրոցեսս ստեղծելու համար պետք է համակարգային կանչ կատարել __NR_fork իդենտիֆիկատորով, կամ ֆայլի մեջ ինչ֊որ բան գրելու համար պետք է օգտագործել __NR_write ֊ը։ Համակարգային կանչերի այս իդենտիֆիկատորները պետք է փոխանցել 0x80 ընդհատմանը։
0x80 ընդհատումը կանչելու համար պետք է նախ ռեգիստորներին փոխանցել որոշակի արժեքներ, թե ինչ արժեքներ և որ ռեգիստորներին, կարող ենք նայել ստորև բերված աղյուսակից․
Syscall # | Param 1 | Param 2 | Param 3 | Param 4 | Param 5 | Param 6 |
---|---|---|---|---|---|---|
eax | ebx | ecx | edx | esi | edi | ebp |
Return value |
---|
eax |
Աղյուսակից երևում է, որ 0x80 ընդհատման պետք է փոխանցել sys_call֊ի համարը կամ իդենտիֆիկատորը և կանչ կատարելու համար անհրաժեշտ մյուս պարամետրերը։ Վերադարձվող արժեքը պահվում է eax ռեգիստորում։
Եկեք, համակարգային կանչի փոքր օրինակ գրենք։ Օրինակ՝ եկեք էկրանին տպենք դասական “hello world”֊ը։
Դրանից առաջ բերենք write ֆունկցիայի C-կան declaration֊ը․
ssize_t write(int fd, const void *buf, size_t count);
Հիմա կանչենք այս ֆունկցիան օգտագործելով system call:
char *hello = "Hello world\n"; void start() { asm volatile ( "mov $4,%eax;" "mov $1,%ebx;" "mov hello,%ecx;" "mov $12,%edx;" "int $0x80" ); } int main() { start(); return 0; }
Այստեղ, քանի որ օգտագործվավծ են ցածր մակարդակի մեքենայական հրամաններ, կոմպիլյատորը կարող է մեզ ոչ ձեռնտու ինչ֊որ օպտիմիզացիաներ կատարել, դրա համար օգտագործում ենք volatile keyword֊ը։
Կոդից պարզ երևում է, որ eax ռեգիստորին փոխանցում ենք __NR_write կանչի համարը (4), ebx ―ին փոխանցում ենք stdout֊ի թիվը (1), ecx ֊ին մեր տողը, edx ֊ին էլ տողի երկարությունը և դրանցից հետո նոր կանչում ենք 0x80 ընդհատումը։
Եկեք compile անենք կոդը և համոզվենք որ ամեն ինչ նորմալ է։ compile անելու համար օգտագործում ենք հետևյալ հրամանը ․
gcc -o main main.c
Բայց իրականում ամեն ինչ այսքան բարդ չէ և gcc֊ն մեզ հնարավորություն է տալիս էականորեն պարզեցնել մեր կոդը։ Այդ նպատակով կարող ենք օգտագործել հետևյալ կոնստրուկցիան
asm ( assembler template : output operands /* optional */ : input operands /* optional */ : list of clobbered registers /* option ); The syntax after “:” is: <constraint> (<C expression>), ....
Այստեղ output ֊ում պահում ենք մեր այն արժեքը որն ուզում ենք վերադարձնել, input֊ում տալիս ենք մեր արգումենտները, իսկ երրորդ պարամետրը անպայման չէ և մենք այն չենք դիտարկի։
Վերն ասվածը ավելի լավ պատկերացնելու համար, բերենք նույն մեր hello world ֊ի կոդը այս անգամ օգտագործելով նոր նկարագրված inline assembler ֊ի գրելաձևը
char hello[] = "Hello world\n"; void start() { int retval; asm volatile ("int $0x80" : "=a" (retval) : "a" (4), "b" (1), "c" (hello), "d" (sizeof(hello)-1)); asm volatile ("int $0x80" : : "a" (1), "b" (0)); } int main() { start(); return 0; }
Այժմ, երբ պարզ է թէ ինչ է իրենից ներկայացնում system call֊ը անցնենք բուն թեմային։ Նաղքան malloc ֊ին անցնելը, մենք պետք է լուծենք մեկ այլ ֆունկցիայի՝ sbrk()֊ի հարցերը։ Ճիշտն ասած, յուրաքանչյուր իրեն հարգող *nix համակարգի libc ֊ում այդ ֆունկցիան արդեն պատրաստի կա իրականացված, բայց քանի որ վերնագրում արդեն օգտագործել ենք անատոմիա բառը, չենք կարող մինչև վերջ չմերկացնել malloc֊ին 🙂 , հակառակ դեպքում էլ ինչ անատոմիա 🙂 sbrk ֆունկցիան հանդիսանում է *nix համակարգերի memory managment֊ի հիմնական երկու ֆունկցիաներից մեկը (մյուսն է brk ) և նախատեսված է heap ֊ից հիշողություն պոկելու համար և այնուհետև ազատ հիշողությանը կցելու համար։ Ընդ֊որում heap֊ում հիշողության փոփոխությանը կից տեղի է ունենում նաև այսպես կոչված program break֊ի փոփոխություն։ program break ֊ը իրենից ներկայացնում է heap ֊ի ընթացիկ գագաթը։
Հետևաբար այդ գագաթի փոփոխությունն էլ կնշանակի հիշողության աճ կամ կրճատում։ Հենց դրա համար էլ նախատեսված է sbrk ֆունկցիան։ Ընդ֊որում heap ֊ի փնթացիկ գագաթը կարող ենք ստանալ sbrk() ֊ին տալով 0 արգումենտը՝
void* start_addr = sbrk(0);
Հիմա, երբ արդեն ունենք heap֊ի գագաթի հասցեն, կարող ենք աճեցնել որոշակի չափով՝ գումարելով այդ հասցեին մեր ուզած չափը
void* new_ptr = sbrk((intptr_t)start_addr + size);
Փաստորեն heap ֊ից հիշողություն ուզելու ընդհանուր ալգորիթմը հետևյալն է (***)․
- ստանալ heap֊ի ընթացիկ գագաթի հասցեն start_addr, կանչելով sbrk(0),
- վերցնել start_addr + size չափանի հիշողություն։
Մեզ մնում է միայն գրել կոդերը։ Բայց նախ և առաջ կգրենք system_call֊ի կոդը․
int __syscall1( int number, intptr_t p1 ){ int ret; asm volatile ("int $0x80" : "=a" (ret) : "a" (number), "b" (p1)); return ret; }
Ամեն ինչ շատ պարզ է, տալիս ենք համակարգային կանչի համարը կամ իդենտիֆիկատորը (sbrk֊ի համար այն __NR_brk է) ու հիշողության չափը որքան որ ուզում ենք վերցնել heap֊ից։ Վերադարձվող արժեքը պահում ենք ret փոփոխականում, որն էլ վերադարձնում ենք ֆունկցիայից։
Հիմա գրենք բուն sbrk() ֆունկցիան, համաձայն (***) ալգորիթմի
void* __sbrk__(intptr_t increment) { /* * Calling sbrk() with an increment of 0 can be used to find the current * location of the program break. * The program break is the first location after the end of the * uninitialized data segment. Program break means the top of the Heap). * Increasing the program break has the effect of allocating memory * to the process; decreasing the break deallocates memory. */ void *new, *old = (void *)__syscall1(__NR_brk, 0); new = (void *)__syscall1(__NR_brk, ((uintptr_t)old) + increment); return (((uintptr_t)new) == (((uintptr_t)old) + increment)) ? old : (void *)-1; }
Չնայած ամեն ինչ արդեն գրել եմ և կոդի մեկնաբանություններում էլ լավ նկարագրված է, բայց ամեն դեպքում մի անգամ էլ անցնենք ֆունկցիայի մարմնի վրայով։ Առագին տողով՝ կանչելով մեր գրած __sbrk__ ֆունկցիան, ստանում ենք heap ֊ի գագաթի հասցեն և արդեն sbrk֊ի երկրորդ կանչով վերցնում ենք մեզ անհրաժեշտ հիշողությունը։ Եթե heap ֊ում այլևս չկա ազատ հիշողություն, ապա վերադարձնում ենք -1: Այսքանը համակարգային կանչերի և malloc֊ի implementation֊ի համար երևի թե ամենակրիտիկական թեմայից։ Մյուս մասում, որը անպայման կլինի, կգրենք malloc ֊ն ու free ֊ն 🙂 Ամբողջ կոդերը կարող ենք նայել այստեղից :
Comments: no replies