Examen CA/CB/CC 2017-2018

Urmăriți precizările din pagina de reguli.

Examen final

Puteți participa la un singur examen final.

Datele de examen de SO pentru sesiunea iunie 2018 sunt:

  • joi, 31 mai 2018, ora 14:00, sala PR001 (studenți care refac materia)
  • joi, 7 iunie 2018, ora 14:00, sala EC105 (331CA, 332CA, 333CA, 334CA, 335CA) și sala EC004 (336CA, studenți care repetă/refac materia)
  • marți, 12 iunie 2018, ora 08:00, sala PR001 (331CB, 332CB, 333CB, 334CB, 335CB)
  • marți, 12 iunie 2018, ora 11:00, sala PR001 (331CC, 332CC, 333CC, 334CC, 335CC), sala EC004 (336CC, 336CB)

Datele de examen de SO pentru sesiunea septembrie 2018 sunt:

  • joi, 30 august 2018
  • miercuri, 5 septembrie 2018

Puteți veni o singură dată la examen. Studenții care refac materia pot veni doar în primele două date de examen (31 mai 2018 și 7 iunie 2018). Pentru ei sesiunea este mai scurtă (26 mai 2018 - 8 iunie 2018).

Studenții de anul 3 pot veni în alte zile față de cea aferentă grupei/seriei lor. Studenții în alte situații (de exemplu transferați de la IS) pot veni oricând. Cei care doresc să vină în altă zi față de cele alocate, să trimită un e-mail către Elena cu subiectul [SO][Examen] Transfer: Prenume NUME, grupă, de exemplu [SO][Examen] Transfer: Ana POPESCU, 332CB.

Informații despre desfășurătorul și conținutul examenului găsiți în secțiunea aferentă din pagina de notare. În pregătirea examenului recomandăm să parcurgeți subiectele de examen anterioare.

Urmăriți precizările din pagina de reguli.

Foi de examen

Lucrări

  • Dacă nu puteți participa la seria fiecăruia, puteți veni la cealaltă serie. Pentru aceasta trimiteți un e-mail catre Alexandru Rotaru cu subiectul [SO][Lucrare X] Transfer Prenume Nume, Grupa unde:
    • X este indexul lucrării (1, 2, 3 sau 4)
    • Prenume este prenumele.
    • Nume este numa.
    • Grupa este grupa.
  • Nu există sesiune de contestații propriu-zisă; dacă sunteți de părere că nu a fost corectată corespunzător lucrarea, trimiteți un e-mail (solicitare de recorectare) către Vladimir Olteanu. Cererile de recorectare se vor trimite după publicarea soluțiilor pe această pagină, până la data limită anunțată pe lista de discuții.
    • Folosiți subiectul [SO][Lucrare X] Prenume NUME - grupa; de exemplu [SO][Lucrare 1] Andreea POPESCU - 332CA.
  • Pentru a fi punctat, răspunsul la o întrebare trebuie să fie justificat.

Lucrare 1

  • La începutul cursului 4:
    • 12 martie 2018, seria CA
    • 14 martie 2018, seria CB
    • 14 martie 2018, seria CC

3CA, varianta 1

  1. Explicați diferența dintre descriptorii rezultați din două apeluri de open către acelasi fisier, și un apel de open urmat de un apel dup pe descriptorul rezultat.
    • Răspuns: Descriptorii rezultați din două apeluri de open către același fișier vor indica spre două structuri de fișier deschis diferite, pe când în cazul unui apel open() urmat de un apel dup() descriptorii vor indica spre aceeași structură.
  2. Ce va afișa la rulare următorul cod, presupunând că fork este executat cu succes?
       int i = fork();
       if (i==0)
         i = getppid();
       else
         sleep(1);
     
       printf(%d\n”, i);
    • Răspuns: La rularea codului se va afișa:
      <pid-ul procesului părinte>
      <pid-ul procesului copil>

      Fork returnează 0 în cadrul procesului copil și pid-ul procesului copil în cadrul procesului părinte. Astfel, procesul copil va afla pid-ul procesului părinte și își va continua execuția prin printarea acestuia în timp ce procesul parinte face sleep de 1 secundă.
  3. Scrieți secvența de cod care implementează comanda ls > /dev/null.
    • Răspuns:
          pid_t pid = fork();
          switch(pid) {
          case 0:
            fd = open(/dev/null, O_WRONLY | O_TRUNC | O_CREAT, 0644);
            dup2(fd, STDOUT_FINENO);
            close(fd);
            execlp(“ls”, “ls”, NULL);
          default:
            waitpid(pid, NULL, 0);
          }


      De asemenea, au fost punctate și variantele care conțin variante în pseudocod ale apelurilor precum: open(”/dev/null”), exec(“ls”) etc.

3CA, varianta 2

  1. Dați un exemplu de funcție I/O care permite transmiterea de date către disk, rețea și dispozitive de tip caracter.
    • Răspuns: O funcție de I/O care permite transmiterea de date către disk, rețea și dispozitive de tip caracter este apelul de sistem write().
  2. Ce va afișa la rulare următorul cod, presupunând că fork este executat cu succes?
        int i = fork();
     
        if (i==0)
          i++;
        else
          sleep(1);
     
        printf(%d\n”, i);
    • Răspuns: La rularea codului se va afișa:
      1
      <pid-ul procesului copil>

      Fork returnează 0 în cadrul procesului copil și pid-ul procesului copil în cadrul procesului părinte. Astfel, procesul copil va afla va incrementa variabila i, și o va afișa. Părintele va face sleep de 1 secundă și va afișa pid-ul procesului copil.
  3. Scrieți secvența de cod care lansează în execuție ls | wc -l.
    • Răspuns: O variantă de răspuns posibilă:
          fd_redirect;
       
          fd = fork();
       
          if (fd == 0) { //child
            close(STDOUT);
            dup2(STDOUT, fd_redirect);
            exec("ls");
          } else if (fd > 0) { //parent
            close(STDIN);
            dup2(STDIN, fd_redirect);
            wait(); //for child
            exec("wc -l");
          } else {
            //error
          }

3CB, varianta 1

  1. Când este necesară execuția unui apel de sistem?
    • Răspuns: Execuția unui apel de sistem este necesară în momentul în care se interacționează cu hardware-ul.
  2. Cum ați implementa redirectarea pentru următoarea comandă bash ls >> test.out (este de interes implementarea redirectării, în locul comenzii ls putea fi orice alta comandă)?
    • Răspuns: Variantă de răspuns:
          int fd = open("test.out");
          close (STDOUT_FILENO);
          dup(fd);
          close(fd);
          lseek(STDOUT_FILENO,0, SEEK_END);
  3. Câte procese se pot afla în starea RUNNING la un moment dat?
    • Răspuns: În starea RUNNING, se pot afla la un moment dat un număr de procese egal cu numărul de core-uri ale sistemului de calcul.

3CB, varianta 2

  1. Indicați cel puțin un rol al nucleului (kernel-ului) într-un sistem de calcul.
    • Răspuns: Variante de răspuns: Nucleul are rolul de a abstractiza hardware-ului SAU rol de interfațare a hardware-ului SAU Nucleul oferă controlul accesului la resurse.
  2. Ce apeluri de sistem sunt necesare pentru a porni două procese care comunică prin intermediul unui pipe (ex ls | grep test)?
    • Răspuns: O variantă de răspuns: Apelurile de sistem necesare pentru a porni două procese care comunică prin intermediul unui pipe sunt fork() pentru crearea celor două procese, pipe() pentru deschiderea unor pipe-urilor folosite de cele două procese, close() pentru închiderea unuia din capetele pipe-urilor create înainte, dup2() pentru redirectarea capetelor pipe-ului și execve() pentru execuție.
  3. Care este cauza principală pentru care un proces este în starea WAITING?
    • Răspuns: În general un proces ajunge în starea de WAITING în urma efectuării unei operații blocante.

3CC, varianta 1

  1. Stiva de rețea a unui sistem de operare poate fi implementată în user space sau în kernel space. De ce se preferă implementarea în kernel space?
    • Răspuns: Preferăm implementarea stivei de rețea în kernel space din rațiuni de performanță. Prelucrarea pachetelor se întâmplă în kernel space iar tranziția (costisitoare) către și din user space are loc doar pentru payload-ul pachetelor.
  2. Dați un exemplu de apel de bibliotecă/sistem, altul decât open()/fopen() care duce la ocuparea unei intrări din tabela de descriptori de fișier a unui proces.
    • Răspuns: Popularea unei intrări în tabela de file descriptori se poate face cu apelul dup() (sau dup2()) pentru duplicarea unei intrări existente sau cu apeluri precum socket() sau pipe() pentru crearea unui socket sau a unui pipe care ocupă, respectiv, o intrare și două intrări.
  3. Un proces execută apelul getppid() în două situații diferite. În prima situație apelul întoarce valoarea 2832, iar în a doua situație apelul întoarce valoarea 1. Cum explicați?
    • Răspuns: getppid() întoarce pid-ul procesului părinte. În prima situație valoarea returnată este diferită de 1 deoarece procesul are un părinte (creator) care încă rulează. În cea de-a doua situație procesul a fost adoptat de init deoarece părintele creator al procesului și-a terminat execuția și nu l-a așteptat.

3CC, varianta 2

  1. Oferiți un motiv pentru care este necesară existența modului privilegiat (supervisor/kernel mode) într-un sistem de calcul.
    • Răspuns: Este nevoie de modul privilegiat pentru a asigura integritatea sistemului, izolarea între procese și accesul mediat la resurse. În absența modului privilegiat (kernel/supervisor mode), un proces ar putea scrie în memoria altui proces, ar putea corupe resurse sau ar putea citi informații de la resursele I/O fără să știe dacă sunt ale sale sau alte altcuiva.
  2. Care este un avantaj și un dezavantaj al folosirii apelului printf() în locul apelului write() pentru afișarea unui mesaj la ieșirea standard a unui proces?
    • Răspuns: Avantajele folosirii printf() sunt: formatare, portabilitate, mai puține apeluri de sistem (când mesajul este buffered). Dezavantaje: buffer-ul ocupă memorie, nu se afișează instant mesajul (este buffered).
  3. Unui proces îi expiră cuanta de rulare (time slice) în momentul în care rulează pe un procesor (se află în starea RUNNING). Ce se întâmplă cu procesul?
    • Răspuns: La expirarea cuantei de timp, un proces este mutat din starea RUNNING în starea READY, unde va rămâne până va fi din nou planificat: adică până atunci când va primi o altă cuantă de rulare și va fi lăsat să ruleze pe procesor.

Greșeli frecvente

Lucrări foarte bune

Lucrare 2

  • La începutul cursului 7:
    • 2 aprilie 2018, seria CA
    • 4 aprilie 2018, seria CB
    • 4 aprilie 2018, seria CC

3CA, varianta 1

  1. Presupunând un sistem cu timp de access la RAM de 100ns, și știind că un context conține zece valori ale regiștrilor, estimați timpul necesar pentru o schimbare de context.
    • Răspuns: Worst case: 10 read * 100ns, 10 write * 100ns = 2 microsecunde. Dacă sunt în cache, timpul poate fi mai mic.
  2. Un proces execută următorul cod:
    for (i=0; i<100000; i++)
      if (fork() == 0)
        exit(1);

    Care este consumul total de memorie fizică folosită de toate procesele copil în plus față de procesul părinte, exceptând structurile de date din kernel?

    • Răspuns: Variantă răspuns: Fiecare proces copil va aloca o pagină pentru stivă prin mecanismul copy-on-write (care va fi scrisă atunci când funcția exit își inițializează stack-frame-ul, salvând base pointer). Consumul total va fi 400MB.
      Variantă de răspuns acceptată: nu se va aloca nici o pagină în copil din cauza copy-on-write, pentru că nu se modifică nici o variabilă.
  3. Dați un exemplu de algoritm de înlocuire de pagini pentru care creșterea memoriei fizice nu garantează reducerea numărului de major page faults.
    • Răspuns: FIFO.

3CA, varianta 2

  1. Într-un sistem preemptiv cu planificator round-robin, explicați dacă este posibil ca două procese ce sunt planificate alternativ pe același procesor să primească timp de procesor diferit.
    • Răspuns: Procesele sunt I/O bound.
  2. Dați un exemplu de situație în care unei pagini din procesul părinte nu i se aplică mecanismul copy-on-write după apelul fork(), deși pagina respectivă este scrisă atât de copil cât și de părinte.
    • Răspuns: Pagina a fost mapată cu mmap (…, MAP_SHARED,…);
  3. Dați avantaj și un dezavantaj al memory-mapped IO comparativ cu standard I/O.
    • Răspuns: Memory mapped I/O este mult mai rapid în general pentru că nu implică syscalls la fiecare acces; deasemenea mecanismul de paging nu încarcă în memorie decât paginile efectiv accesate. Orice eroare de I/O, de exemplu disk removed, este transmisă procesului prin semnale de genul SIGSEGV sau SIGBUF - iar programele trebuie să prindă și să trateze aceste semnale explicit. În cazul standard IO tratarea astfel de erori e mult mai simplă (-1, look at errno).

3CB, varianta 1

  1. Într-un sistem doar cu procese I/O intensive, cuanta de rulare pe procesor ar trebui să fie mai mică sau mai mare decât în mod obișnuit pentru a eficientiza per ansamblu sistemul? Explicați.
    • Răspuns: Cuanta de rulare trebuie să fie mai mică întrucât subsistemele I/O sunt mult mai încete ca procesorul iar o cuantă de rulare mai mare nu ar face decât să blocheze procesorul fără ca acesta să execute nimic util.
  2. Câte operații sunt necesare într-o arhitectură load/store pentru a copia o valoare între 2 locații de memorie RAM?
    • Răspuns: 2 operații (încărcare din memorie în registru și salvare din registru în memorie).
  3. Alocăm un spațiu contiguu de 128MB folosind apelul mmap. Presupunând că dimensiunea paginii este de 4K, care este numărul maxim de page-fault-uri ce vor putea fi generate?
    • Răspuns: 128MB / 4k = 32K page fault-uri.

3CB, varianta 2

  1. Într-un sistem doar cu procese CPU intensive, cuanta de rulare pe procesor ar trebui să fie mai mică sau mai mare decât în mod obișnuit pentru a eficientiza per ansamblu sistemul? Explicați.
    • Răspuns: Cuanta de rulare trebuie să fie mai mare întrucât la un context switch se pierde o perioadă importantă de timp, iar dacă cuanta ar fi prea mică, mai mult am sta în schimbarea de context decât să procesăm volumul mare de date pe procesor.
  2. Precizați un avantaj și un dezavantaj al paginării.
    • Răspuns: Avantaje: memoria ocupată per proces, un proces are acces la tot spațiul virtual de memorie propriu, etc; Dezavantaje: mai multe accese la memoria fizică, prezența hardware-ului specializat (MMU, TLB), etc
  3. Precizați 2 cazuri care generează un page fault atunci când accesăm o adresă validă de memorie.
    • Răspuns: Swapping, Demand paging.

3CC, varianta 1

  1. De ce este important ca prioritățile proceselor să fie dinamice, nu statice, în contextul planificării proceselor?
    • Răspuns: Dacă prioritățile sunt statice, atunci procesele prioritare vor rula întotdeauna în fața celor mai puțin prioritare. Dacă sunt CPU bound, atunci acestea vor ocupa foarte mult procesorul și procesele mai puțin prioritare vor avea puțin timp să ruleze, ducând la starvation.
  2. La ce este folosit PTBR (Page Table Base Register)? De ce sistemul nu ar putea funcționa fără un PTBR?
    • Răspuns: PTBR este folosit pentru a referi adresa fizică a tabelei de pagini a procesului curent. În abența PTBR-ului, nu am avea un mod prin care să identificăm tabele de pagini a unui proces și deci nici un mod prin care să putem face translatarea de adrese când folosim memorie virtuală.
  3. Apelul de mai jos este efectuat cu succes. În ce situație duce apelul la două page fault-uri?
           memset(a, 0, 8); /* fill a with 8 bytes of 0 */ 
    • Răspuns: Scrisul de 8 octeți poate genera două page fault-uri atunci când sunt “atinse” două pagini de memorie care fie sunt în demand paging fie swappate. Dacă avem, de exemplu, 4 octeți pe o pagină (la sfârșit) și 4 octeți în pagina următoare (la început) vor rezulta două page fault-uri.

3CC, varianta 2

  1. De ce procesele I/O bound primesc, în general, o cuantă de timp de rulare mai mare?
    • Răspuns: Procesele I/O bound sunt procese care se blochează des (și generează schimbări de context voluntare). Le alocăm o cuantă de timp mai mare pentru că nu vor apuca să o consume, se vor bloca. La următoarea planificare va consuma din restul de cuantă. Un proces CPU bound primește o cuantă mai mică pentru a fi forțat să iasă de pe proces (schimbare de context nevoluntară) atunci când îi expiră cuanta.
  2. De ce este utilă prezența unei zone dedicate pentru kernel în spațiul virtual de adrese al fiecărui proces, față de un spațiu virtual de adrese dedicat pentru kernel?
    • Răspuns: La un apel de sistem se schimbă nivelul de privilegiu al procesorului dar nu se schimbă tabela de pagini, pentru că nu se schimbă spațiul de adresă. În felul acesta nu mai facem flush la TLB și avem overhead mai redus. În cazul în care kernel-ul ar avea un spațiu virtual de adrese propriu ar trebui făcută schimbarea tabelei de pagini (și flush la TLB).
  3. De ce zona de text/cod a bibliotecii standard C poate fi partajată între mai multe procese, dar nu și zona de date?
    • Răspuns: Zona text este o zona read-only, pe când zona de date e modificabilă. Întrucât modificările trebuie să fie vizibile doar la nivelul procesului curent, zona de date e unică per proces (și nepartajate). Zona text nu se modifică și poate fi partajată de mai multe procese.

Greșeli frecvente

Lucrări foarte bune

Lucrare 3

  • La începutul cursului 11:
  • 7 mai 2018, seria CA
  • 9 mai 2018, seria CB
  • 9 mai 2018, seria CC

3CA, varianta 1

  1. Implementați o incrementare atomică a variabilei t folosind funcția atomică GCC _sync_bool_compare_and_swap (int* t, int r, int n).
    • Răspuns:
      do {
      	int s = t+1;
      } while (!_sync_bool_compare_and_swap(&t,s-1,s)); 
  2. Explicați cum poate un thread din procesul P1 citi memoria procesului P2.
    • Răspuns: Shared memory sau meltdown pe un system fara KPTI enabled.
  3. Un atacator dorește să atace un sistem cu stack canary protection pe 8 octeți. Serverul care este atacat face fork() pentru fiecare client și are un stack overflow exploatabil de client prin modificarea inputului. De câte încercări are nevoie atacatorul, în medie, pentru a ghici valoarea canary?
    • Răspuns: Atacatorul poate exploata faptul ca valoarea canary ramane aceeasi intre doua conexiuni ale sale (din cauza fork). El poate ataca fiecare octet din canar independent, incepand cu primul; daca greseste o sa i se inchida conexiunea; daca nu, o sa mearga. Astfel, in medie va avea 128 de incercari (i.e. conexiuni) per octet. Dupa ce gaseste primul octet il ataca pe al doilea. In total va avea nevoie de 8 * 128 = 1024 incercari in medie.

3CA, varianta 2

  1. Implementați un spinlock folosind funcția atomică GCC _sync_bool_compare_and_swap (int* t, int r, int n).
    • Răspuns:
      while (!_sync_bool_compare_and_swap(&lock,0,1));
  2. Explicați de ce este mai ieftină crearea unui thread comparativ cu crearea unui proces nou.
    • Răspuns: Nu este necesara duplicarea tabelei de pagini, nici a descriptorilor.
  3. De câte încercări are un atacator nevoie (în medie) pentru a găsi o adresă de cod validă pe un sistem de 64 biti atunci când atacă un sistem cu ASLR pornit. Acest sistem rulează un server care face fork() pentru fiecare client și care are un stack overflow exploatabil de client prin modificarea inputului.
    • Răspuns: De 1024 de incercari in medie: ataca octet cu octet adresa de retur salvata pe stiva, observand daca i se inchide conexiunea (a gresit) sau nu.

3CB, varianta 1

  1. Care e motivul pentru care secțiunile .data și .bss sunt separat puse în fișierul executabil?
    • Răspuns: .bss refera variabila globale care au valoarea 0. Gruparea acestora intr-o sectiune speciala, diferita de .data, optimizeaza consumul de spatiu intrucat nu mai este necesar sa stocam 0-uri.
  2. Se crează 2 thread-uri în cadrul aceluiași proces. Se poate ca unul din thread-uri să modifice fluxul executiei celuilalt thread?
    • Răspuns: Da, prin coruperea stivei celuilalt thread.
  3. Secventa de cod a += 5 este atomică? (la executarea secvenței de 2 thread-uri, rezultatul final va fi același tot timpul) Explicați.
    • Răspuns: Da, daca avem un singur core. Nu, daca avem mai multe core-uri.

3CB, varianta 2

  1. Care este motivul pentru care secțiunea de cod este stocată separat de restul datelor într-o zonă denumită .text?
    • Răspuns: Zona .text are o proprietate speciala: trebuie sa fie executata si nu trebuie sa poata fi scrisa (pentru a nu modifica ilegitim codul de executat). Astfel maparea acelei zone este diferita fata de celelalte zone din executabil.
  2. Cum putem preveni un atac de tip stack buffer overflow?
    • Răspuns: Toate functiile ce acceseaza la scriere variabilele de pe stiva, sa fie limitate printr-un parametru sau printr-o verificare implicita la lungimea buffer-ului alocat.
  3. De ce nu este atomică instrucțiunea add [%rax], 5 cand avem 2 core-uri în sistem?
    • Răspuns: add [%rax], 5 - Codul de asamblare presupune un acces la memorie pentru a citi valoarea initiala. Acest acces la memorie nu este atomic (ambele core-uri pot acces memoria in acelasi timp). Avem nevoie de primitiva lock pentru asigurarea accesului exclusiv la magistrala.

3CC, varianta 1

  1. De ce trecerea peste limita unui buffer (buffer overflow) nu generează, de obicei, excepție de acces la memorie (de tipul Segmentation fault)?
    • Răspuns: Întrucât sistemul de operare gestionează memoria la nivel de pagini. Un buffer este o parte a unei pagini. Dacă trecerea peste limita unui buffer (buffer overflow) nu duce la trecerea într-o altă pagină (nevalidă), atunci nu rezultă în excepție de acces la memorie, accesul fiind valid din punctul de vedere al sistemului de operare.
  2. De ce schimbarea de context între două thread-uri user-level este mai rapidă decât schimbarea de context între două thread-uri kernel-level ale aceluiași proces?
    • Răspuns: Thread-urile cu implementare kernel-level fac schimbarea de context la nivelul nucleului sistemului de operare, lucru care presupune schimbarea nivelului de privilegiu din user space în kernel space și invocarea planificatorului. Thread-urile cu implementare user-level fac schimbarea de context în user space, fără apel de sistem sau tranziție în kernel space (cauzatoare de overhead) și este așadar mai rapidă.
  3. De ce instrucțiunea a++ din C poate fi atomică pe un sistem x86 single-core dar niciodată pe un sistem x86 multi-core?
    • Răspuns: Instrucțiunea a++ se poate traduce în add [mem_address], 1. O astfel de instrucțiune este atomică pe un singur core (nu poate fi întreruptă în mijlocul execuției). Însă, pe un sistem multi-core, instrucțiunea se “desface” în acțiuni de load, add, store și lucru cu magistrala de acces la memorie, magistrală partajată. Pe un astfel de sistem multi core, pot exista întrețeseri de acces la magistrală din parte mai multor core-uri care fac instrucțiunea neatomică și duc la rezultate nedeterministe.

3CC, varianta 2

  1. Pentru a putea folosi un shellcode pe un sistem care are suport DEP (Data Execution Prevention) un atacator folosește apelul mprotect(). De ce și cum folosește atacatorul apelul mprotect()?
    • Răspuns: Un sistem cu suport DEP va marca zonele writable din spațiul virtual de adrese al unui proces (data, bss, stack, heap) ca neexecutabile. Un shellcode este injectat într-o zonă writable. Pentru a putea fi executat trebuie ca acea zonă să fie executabilă. Pentru aceasta folosim apelul mprotect() care adaugă permisiuni de execuție (PROT_EXEC) acelei zone în care am injectat shellcode-ul.
  2. De ce schimbarea de context între două thread-uri ale aceluiași proces e mai rapidă decât schimbarea de context între două procese?
    • Răspuns: Schimbarea de context între două thread-uri ale aceluiași proces nu schimbă spațiul virtual de adrese, deci nu schimbă tabela de pagini, care duce la anularea intrărilor din TLB. Schimbarea de context între două thread-uri din procese diferite duce la schimbarea spațiului virtual de adrese, lucru care înseamnă schimbarea tabelei de pagini și are overhead suplimentar.
  3. Cum arată în pseudo-asamblare implementarea primitivei lock() pentru un spinlock?
    • Răspuns: Un spinlock este o variabilă care este actualizată atomic. Operația lock() pe spinlock este un busy waiting cât timp acea variabilă este ocupată, adică cineva a achiziționat deja spinlock-ul. Această verificare trebuie să fie atomică, lucru realizat cu operații atomice de tipul CAS (compare-and-exchange) și lock pe magistrală. Dacă valoarea 1 înseamnă spinlock liber, iar 0 spinlock ocupat, în pseudo-asamblare implementarea operației lock() va fi:
      spinlock = 1; /* initial value is 1 - open */
      lockfn:
              lock cmpxchg spinlock, 1, 0
              cmp old_value, 0 ; if old_value (stored in a register by cmpxchg) is 0, busy wait
              je lockfn 

Greșeli frecvente

  • Buffer Overflow (scrierea peste dimensiunea buffer-ului) este considerat a fi același lucru cu atacul de tip Stack Buffer Overflow.
  • Buffer Overflow (trecerea peste dimesiunea unui buffer) se întâmplă doar pe stivă. Un buffer overflow poate să apară și pe heap.
  • Codul este pus într-o zonă diferită față de celelalte date pentru a evita suprascrierea de cod. Nu se explică faptul că diferă permisiunile, presupun că fac un buffer overflow până în cod.
  • Pentru a preveni atacuri de tip stack buffer overflow se folosesc ASLR și DEP. ASLR și DEP sunt folosite pentru a îngreuna anumite atacuri, nu pentru a preveni buffer overflow-ul în sine. DEP previne execuția unui shellcode introdus de atacator deoarece zona respectivă nu are concomitent permisiuni de write și execute. Singura legătură cu buffer overflow-ul e că suprascriind adresa de retur a funcției să pointeze către shellcode, acesta nu va putea fi executat. ASLR pune adresele funcțiilor din libc random pentru a îngreuna apelul unor funcții de bibliotecă precum system. Însă, folosind return-to-libc (prin exploatarea unui buffer overflow), se pot leak-ui adresele funcțiilor din libc și se poate calcula offset-ul funcțiilor dorite, făcându-se bypass ASLR-ului. Astfel, ASLR nu previne buffer overflow-ul.

Lucrări foarte bune

Lucrare 4

  • La începutul cursului 13:
    • 21 mai 2018, seria CA
    • 23 mai 2018, seria CB
    • 23 mai 2018, seria CC

3CA, varianta 1

  1. Explicați pașii necesari pentru a transfera un bloc de la un HDD către aplicația care citește un fișier.
    • Răspuns: Pas 1. Driver-ul SO programează controller-ul de hard-disc să citească blocul.
      Pas 2. HDD-ul citește blocul, plasând conținutul într-un buffer intern al HDD;
      Pas 3. controller inițiază DMA pentru a transfera datele în memorie;
      Pas 4. când DMA se finalizează, se generează o întrerupere pentru a anunța procesorul că datele sunt disponibile.
  2. La momentul t0 apelul send întoarce valoarea 100. Presupunând că apelurile pe sockeți sunt blocante, este posibil ca apelul recv efectuat la celălalt capăt al conexiunii la momentul t1 > t0 să întoarcă valoarea -1?
    • Răspuns: Da, dacă rețeaua sau oricare dintre mașini se deconectează; succesul apelului send indică doar copierea datelor în buffer-ul nucleului transmițătorului.
  3. Dorim să folosim un sistem de fișiere FAT pentru un disc de 10TB și dimensiunea blocului de 4KB. Cât de mare va fi tabela FAT pentru acest disc?
    • Răspuns: Numărul total de blocuri (și de intrări în tabela FAT): 10 * 10^12 / 4 / 10^3 = 2.5*10^9. E nevoie de 4 octeți per bloc pentru a encoda FAT-ul; deci vor fi necesari în total 4 * 2.5 * 10^9B RAM= 10GB.

3CA, varianta 2

  1. Dați un exemplu în care polling este preferabil în defavoarea întreruperilor pentru lucrul cu dispozitive I/O.
    • Răspuns: La plăcile de rețea cu viteză mare (10Gbps sau mai mult), când vin pachete mici (e.g. 64B) se vor genera milioane de întreruperi pe secundă, și fiecare întrerupere este costisitoare (salvare context, etc). Dacă folosim polling, evităm acest cost și performanța este mai bună.
  2. Atunci când se transmit 10.000 octeți folosind un socket TCP, de câte apeluri recv(fd, buf, 10000, 0) va nevoie pentru a primi toate datele?
    • Răspuns: Minim 1 apel, maxim 10.000 de apeluri - teoretic SO poate întoarce orice număr de octeți per apel citire, chiar și 1 octet.
  3. Pe un HDD cu capacitate de 1TB dorim să instalăm un sistem de fișiere ext2 cu dimensiunea blocului de 4KB. Câte nivele de indirectare sunt necesare pentru a permite unui fișier să ocupe (aproape) tot spațiul de pe HDD?
    • Răspuns: Număr blocuri fișier maxim: 1TB/4KB = 10^12 / 4 / 10^3 = 250*10^6 blocuri. Dacă folosim 1 nivel indirectare ⇒ fișierul maxim poate avea 1000 blocuri; 2 nivele indirectare ⇒ 1.000.000 de blocuri; 3 nivel ⇒ 10^9. Sunt necesare 3 nivele de indirectare.

3CB, varianta 1

  1. În directorul curent există fișierul in.dat. Executăm comanda /usr/bin/time -v cp in.dat out.dat. Aceasta durează 1 secundă. Mai executăm o dată comanda /usr/bin/time -v cp in.dat out.dat. A doua oară aceasta va dura doar 0.1 secunde. Care ar putea fi cauza?
    • Răspuns: Page cache / buffer cache (caching-ul fișierului înainte de a fi scris pe disc)
  2. Executăm linia următoare int n = recv(s,buf,1000,0);. Dați exemplu de un caz în care n va fi 1 și de un caz în care n va fi 1000.
    • Răspuns: n va fi 1 atunci când buffer-ul de receive are un singur octet și va fi 1000 când va avea cel puțin 1000 de octeți.
  3. Creăm un hardlink la un fișier. Dacă ștergem acel fișier, link-ul devine invalid? Explicați mecanismul din spate.
    • Răspuns: Nu devine invalid. Hardlink-ul este un dentry ce pointează către același inode al fișierului. O dată șters fișierul, inode-ul nu se va șterge întrucât există o referință la acesta (hard-link-ul)

3CB, varianta 2

  1. Dorim să modificăm setările unui driver în Linux. Ce apel de sistem folosim pentru acest lucru?
    • Răspuns: ioctl
  2. Executăm linia următoare int n = send(s,buf,2000,0);. Dați exemplu de un caz în care n va fi 1 și de un caz în care n va fi 2000.
    • Răspuns: n va fi 1 atunci când buffer-ul de send este aproape plin (mai are un singur octet liber) și va fi 2000 atunci când buffer-ul este gol și are cel putin 2000 de octeți liberi.
  3. Gestiunea spațiului liber într-un sistem de fișiere se face folosind o structura de tipul vector de biți sau o listă înlănțuită. Precizați câte un dezavantaj al fiecărei metode.
    • Răspuns: Dezavantaj vector de biți: parcurgere liniară. Dezavantaj lista înlănțuită: overhead de memorie.

3CC, varianta 1

  1. Un program rulează prima dată într-un sistem, apoi este oprit. Apoi este pornit din nou. Se observă că timpul de pornire a doua oară este mult mai mic. Care este explicația?
    • Răspuns: Când pornește prima oară, secțiunile din fișierul executabil al programului/procesului sunt aduse prima oară în RAM de pe disc (suportul persistent), acțiune care durează. După ce procesul este oprit, mare parte din secțiuni rămân în buffer cache, astfel că a doua oară nu mai este nevoie de aducerea lor de pe disc, iar pornirea e mai rapidă.
  2. În ce situație se blochează un apel send() pe un socket?
    • Răspuns: Un apel send() pe un socket se blochează în momentul în care buffer-ul de send/transmit al socket-ului este plin și nu mai există nici măcar un octet liber unde să fie transferate date din buffer-ul din user space.
  3. Ce conțin blocurile de date ale unui director?
    • Răspuns: Blocurile de date ale unui director conțin un vector de dentry-uri; fiecare dentry conține un nume și un număr de inode aferent acelui nume. Aceste dentry-uri sunt intrările din directorul respectiv, vizibile în Unix prin rularea comenzii ls.

3CC, varianta 2

  1. Care este o diferență între o operație non-blocantă și o operație asincronă?
    • Răspuns: Operațiile non-blocante sunt operații care nu blochează execuția procesului, întorcându-se imediat; operațiile non-blocante fie întorc datele disponibile atunci (sincron), fie declanșează în spate execuția unei operații (asincron). Operațiile asincrone sunt operații care se execută separat de fluxul de execuție al procesului (asincron); rezultatul acestor operației este disponibil la încheierea operației prin notificarea procesului sau dacă procesul interoghează starea operației. În general operațiile asincrone sunt non-blocante.
  2. În ce situație se blochează un apel recv() pe un socket?
    • Răspuns: Un apel recv() pe un socket se blochează în momentul în care buffer-ul de receive al socket-ului este gol și nu mai există nici măcar un octet care să fie transferat în buffer-ul din user space.
  3. Cu ce diferă un hard link de un symbolic link?
    • Răspuns: Un hard link este un nume/dentry al unui fișier (inode). Crearea unui hard link înseamnă crearea unui dentry nou. Un symbolic link este un inode care conține calea către un alt inode. Crearea unui symbolic link înseamnă crearea unui inode. Astfel că un symbolic link poate avea unul sau mai multe hard link-uri.

Greșeli frecvente

  • Funcția recv() se blochează atunci când buffer-ul e plin.
  • Operatiile non-blocante sunt doar cele de genul pe fișiere cu flag-ul O_NONBLOCK - dau datele imediat. Practic a fost făcută o comparație între O_NONBLOCK si ASYNC IO.
  • Operațiile non-blocante nu notifică procesul, iar operațiile asincrone notifică procesul.

Lucrări foarte bune

Examene anterioare

so/meta/examen/2017-2018.txt · Last modified: 2019/05/27 10:33 by razvan.deaconescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0