Սկիզբ » Ուսումնական նյութեր » Օպերացիոն համակարգեր » *Nix-եր » malloc ֊ի անատոմիան։ Մաս 1, համակարգային կանչեր (system calls):

malloc ֊ի անատոմիան։ Մաս 1, համակարգային կանչեր (system calls):

| Մարտ 11, 2015 | Մեկնաբանված չէ |

Թույլ տվեք առանց երկար֊բարակ նախաբանների միանգամից անցնել գործի 🙂

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

Բացելով այդ ֆայլը՝ կտեսնենք հետևյալ պատկերը

sys_calls_nums

 

Նկարից պարզ երևում է, որ օրինակ նոր պրոցեսս ստեղծելու համար պետք է համակարգային կանչ կատարել  __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 ֊ի ընթացիկ գագաթը։

 

heap

Հետևաբար այդ գագաթի փոփոխությունն էլ կնշանակի հիշողության աճ կամ կրճատում։ Հենց դրա համար էլ նախատեսված է sbrk ֆունկցիան։ Ընդ֊որում heap ֊ի փնթացիկ գագաթը կարող ենք ստանալ sbrk() ֊ին տալով 0 արգումենտը՝

void* start_addr = sbrk(0);

Հիմա, երբ արդեն ունենք heap֊ի գագաթի հասցեն, կարող ենք աճեցնել որոշակի չափով՝ գումարելով այդ հասցեին մեր ուզած չափը

void* new_ptr = sbrk((intptr_t)start_addr + size);

Փաստորեն heap ֊ից հիշողություն ուզելու ընդհանուր ալգորիթմը հետևյալն է (***)․

  1. ստանալ heap֊ի ընթացիկ գագաթի հասցեն start_addr, կանչելով sbrk(0),
  2. վերցնել 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 ֊ն 🙂 Ամբողջ կոդերը կարող ենք նայել այստեղից :

malloc ֊ի անատոմիան։ Մաս 1, համակարգային կանչեր (system calls):, 9.6 out of 10 based on 12 ratings

Նշագրեր: , , , ,

Բաժին: *Nix-եր, Assembler, C և C++, Ժեշտ, Օպերացիոն համակարգեր

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

VN:F [1.9.20_1166]
Rating: 9.6/10 (12 votes cast)

Մեկնաբանեք

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

284