Esercitazione sui processi

L’esercitazione di oggi consiste nell’implementazione di un programma C che esegue, eventualmente anche in parallelo, il cracking di un PIN (Personal Identification Number). Il programma contenente il PIN da craccare è

  1. disponibile direttamente nel docker (folder EsercitazioneProcessi, programma ./checkPIN)
  2. scaricabile qui come file binario
    • checkPIN: Linux, 64 bit
    • checkPIN-macOS: macOS

    NOTA: Se eseguendo il file con ./checkPIN il sistema vi dice permission denied, dovete rendere il file eseguibile con chmod +x checkPIN (lo stesso vale per checkPIN-macOS)

  3. compilabile dal sorgente (in questo caso settate voi il PIN da craccare)
    #include <string.h>
    #include <stdlib.h>
    #include <stdio.h>
     
    #define SUCCESSO "PIN %s corretto! >>> Sei autenticato <<<\n"
    #define FALLIMENTO "PIN %s errato\n"
    #define PINsegreto "01234"
     
    int main(int argc, char **argv) {    
        if (argc == 1) {
            printf("Utilizzo: %s PIN\n(Il PIN è di %ld cifre e può iniziare per 0!)\n",
                    argv[0], strlen(PINsegreto));
            exit(2); // Wrong usage
        }        
    
        if (strcmp(PINsegreto,argv[1])==0) {
            printf(SUCCESSO,argv[1]);
            // qui si avrebbe accesso alle risorse se fosse un PIN vero ...
            exit(0); // success
        } else {
            printf(FALLIMENTO, argv[1]); 
            exit(1); // wrong PIN
        }
        
    }

Il programma checkPIN riceve il PIN come argomento da riga di comando e controlla se è corretto o meno. Se l’operazione ha avuto successo, checkPIN termina con exit(0), se l’operazione fallisce, checkPIN termina con exit(1). Nel caso di invocazione sbagliata (ad esempio senza passare il PIN), checkPIN termina con exit(2).

Ecco i tre esempi, in bash $? contiene il valore della exit dell’ultimo programma eseguito:

$ ./checkPIN 
Utilizzo: ./checkPIN PIN
(Il PIN è di 5 cifre e può iniziare per 0!)
$ echo $?          # mostra il valore della exit
2                  # invocazione sbagliata, exit(2)

$ ./checkPIN 12345
PIN 12345 errato
$ echo $?          # mostra il valore della exit
1                  # PIN errato, exit(1)

$ ./checkPIN *****  # PIN corretto, oscurato
PIN ***** corretto! >>> Sei autenticato <<<

$ echo $?          # mostra il valore della exit
0                  # PIN corretto, exit(0)

Come si legge nell’output della prima esecuzione, il PIN è di 5 cifre e può iniziare per zero!

Obiettivo

Dovete realizzare un programma C che prova ad eseguire checkPIN con tutti i PIN di 5 cifre (attenzione che possono iniziare per 0 quindi dovete fare “padding” nel caso il numero sia più piccolo di 5 cifre: 1234 deve diventare 01234)

Suggerimento: utilizzate snprintf con la format string “%05d” per ottenere una stringa di 5 cifre con l’opportuno padding di zeri

Il programma deve:

  • iterare su tutti il PIN p
    • fare una fork e poi una exec per eseguire checkPIN p
    • fare una wait per attendere la terminazione
    • controllare il valore della exit (con le opportune macro viste in classe) e fermarsi quando il PIN corretto è stato trovato (checkPIN fa exit(0))

Provare da soli prima di guardare la soluzione!

#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

#define PINLEN "5"         // lunghezza PIN come stringa per snprintf
#define CHECK "./checkPIN" // il programma da eseguire

int main(int argc) {
    int pid, status;
    int pin; // il pin da provare come numero intero
    int maxbuf = atoi(PINLEN)+1; // PINLEN+1, convertito in intero
    char pinstring[maxbuf];      // il buffer per la stringa contenente il pin
                                 // (serve un byte in più per il terminatore di stringa)

    /* il processo genitore sta in questo loop */
    for (pin=0;;pin++) {
        /* 
         * converte pin in pinstring mettendo un padding di zeri
         * fino a raggiungere PINLEN (5 nel nostro caso) 
         */
        if (snprintf(pinstring, maxbuf, "%0"PINLEN"d", pin) >= maxbuf) {
            /*
             * superata la PINLEN ==> nessun pin trovato, quindi esce
             * notare che snprintf ritorna >= maxbuf quando ha troncato la stringa
             * per farla stare nel buffer (vedere la manpage)
             */
            exit(EXIT_FAILURE); 
        }
        
        pid = fork(); // Crea il processo figlio
        if ( pid < 0 ) { 
            perror("errore fork");
            exit(EXIT_FAILURE); 
        }
        if (pid == 0) { 
            /* 
             * processo figlio, esegue il check lanciando CHECK 
             * con pinstring come argv[1]
             */
            execl(CHECK, CHECK, pinstring, NULL); 
            /* qui non deve arrivarci mai */
            perror("Errore exec!");
            exit(EXIT_FAILURE);
        } 
        /* solo il genitore continua e attende il figlio */
        if ((pid=wait(&status)) >= 0) {
            if (WIFEXITED(status) && WEXITSTATUS(status)==0) {
                printf("PIN trovato: Yeah!!\n");
                exit(EXIT_SUCCESS);
            }
        } else {
            perror("errore wait");
            exit(EXIT_FAILURE);
        }
    }
}

Cracking multi-core

Una volta realizzato il cracker sequenziale potete provare a realizzarne uno che “riempia” gli N core disponibili a tale scopo il programma deve, intuitivamente:

  • fare N fork in modo da utilizzare tutti gli N core
  • fare una wait e poi di nuovo una fork (un core si è liberato e viene subito riutilizzato)
  • prima di uscire fare tutte le wait necessarie ad attendere eventuali processi figli ancora in esecuzione

Provate a confrontare la soluzione sequenziale con quella multi-core utilizzando time che vi dice il tempo di esecuzione complessivo.

$ time ./cracker
...
PIN trovato: Yeah!!
real	0m2.438s
user	0m0.503s
sys	0m0.431s

$ time ./crackerMultiCore
...
PIN trovato: Yeah!!
real	0m0.733s
user	0m0.504s
sys	0m0.801s
...
$

Notare lo speedup di circa 4 volte nella versione multicore. Notare inoltre che alcuni processi figlio continuano anche dopo la terminazione in quanto sono in esecuzione concorrente (parallela) con il processo genitore.

Provare da soli prima di guardare la soluzione!

Le parti nuove sono evidenziate nel sorgente:

#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

#define PINLEN "5"         // lunghezza PIN come stringa per snprintf
#define CHECK "./checkPIN" // il programma da eseguire
#define NCORES 8           // Numero di core

/* 
 * Funzione che fa il check dello status ed esce quando
 * la exit ha ritornato zero.
 * Viene riutilizzata in diversi punti del main
 */
void check(int status) {
    if (WIFEXITED(status) && WEXITSTATUS(status)==0) {
        printf("PIN trovato: Yeah!!\n");
        exit(EXIT_SUCCESS);
    }
}

int main(int argc) {
    int pid, status;
    int pin;                     // il pin da provare come numero intero
    int maxbuf = atoi(PINLEN)+1; // PINLEN+1, convertito in intero
    char pinstring[maxbuf];      // il buffer per la stringa contenente il pin
                                 // (il byte in più è per il terminatore di stringa)
    int activecores = 1;         // numero di core attivi

    /* il processo genitore sta in questo loop */
    for (pin=0;;pin++) {
        /* 
         * converte pin in pinstring mettendo un padding di zeri
         * fino a raggiungere PINLEN (5 nel nostro caso) 
         */
        if (snprintf(pinstring, maxbuf, "%0"PINLEN"d", pin) >= maxbuf) {
            /*
             * superata la PINLEN ==> nessun pin trovato, quindi esce
             * notare che snprintf ritorna >= maxbuf quando ha troncato la stringa
             * per farla stare nel buffer (vedere la manpage)
             */
            exit(EXIT_FAILURE); 
        }
        
        pid = fork(); // Crea il processo figlio
        if ( pid < 0 ) { 
            perror("errore fork");
            exit(EXIT_FAILURE); 
        }
        if (pid == 0) { 
            /* 
             * processo figlio, esegue il check lanciando CHECK 
             * con pinstring come argv[1]
             */
            execl(CHECK, CHECK, pinstring, NULL); 
            /* qui non deve arrivarci mai */
            perror("Errore exec!");
            exit(EXIT_FAILURE);
        } 
        /* 
         * solo il genitore continua: ha fatto la fork 
         * incrementa i numero di core occupati
         */
        activecores++; 
        /* 
         * Se i core sono pieni attende un figlio:
         * a regime appena uno finisce ne lancia un altro
         */
        if (activecores==NCORES) {
            if((pid=wait(&status)) >= 0) {
                activecores--; // il figlio ha liberato un core
                check(status); // controlla se ha trovato il PIN
            }
        }
    }
    /* attende eventuali figli ancora in esecuzione */
    while((pid=wait(&status)) >= 0) {
        check(status);
    }
}