AOS - 11 - KERNEL PROGRAMMING BASICS IV


1 Lecture Info

  • Data: [2019-10-16 mer]

  • Sito corso: link

  • Slides: AOS - 2 KERNEL PROGRAMMING BASICS

  • Progresso unità: 4/6

  • Argomenti:

    • Linux versions

    • System call numerical codes

    • _syscallN(type, name) macros

    • Calling conventions

    • i386 stack allignment

    • x86-64 stack allignemnt

    • Homework #1: TLS

  • Introduzione: Nella scorsa lezione avevamo introdotto i meccanismi base per passare il controllo da una applicazione che gira user-space ad una system call che gira in modalità kernel. In particolare avevamo menzionato che esiste un solo GATE associato alle software traps. Tramite l'utilizzo di un dispatcher e della system call table siamo in grado di offrire un insieme di servizi. Il dispatcher, a seconda dei parametri passati dall'utente, chiama la specifica system call che deve essere eseguita. Continuiamo quindi la nostra discussione con i seguenti argomenti.

2 Linux Versions

Nel corso vedremo vari pezzi di codice presi da varie versioni del kernel di Linux. L'idea è quella di partire da versioni datate per poi passare a quelle più moderne. Possiamo dare una overview generale alle macro versioni del kernel linux:

  • La versione 2.4 è una versione incentrata all'espandibilità e alla modificabilità.

  • Dato che la versione 2.4 non era molto efficiente per sistemi multi-core, la versione 2.6 è una versione più scalabile.

  • Le versioni 3.x sono più strutturato e incentrate sulla sicurezza.

  • Le versioni 4.x sono ancora più sicure, in quanto sono stati introdotto dei meccanismi come, ad esempio, la Page Table Isolation (PTI).

3 System call (software) Components

A livello user abbiamo un modulo software che deve:

  1. Passare l'input al GATE (e quindi al dispatcher).

  2. Attivare il GATE.

  3. Prendere ed eventualmente processare il valore di ritorno della system call.

A livello kernel invece abbiamo

  • Un modulo software che implementa il dispatcher

  • Una system call table.

  • Il codice della system call.

Per poter aggiungere una nuova system call dobbiamo andare a lavorare su entrambi i lati. Notiamo inoltre che se il formato della system call che vogliamo aggiungere è compliant con la convenzione già utilizzata dal dispatcher, non dobbiamo andare a modificare il dispatcher, altrimenti potrebbe risultare necessario scrivere un nuovo dispatcher. In ogni caso, il punto fondamentale da tenere a mente per quanto riguarda la compatibilità con il dispatcher di default è il modo in cui vengono gestiti i parametri, con particolare attenzione a

  1. Quanti possono essere.

  2. Come vengono passati al dispatcher.

4 System Call Formats

Andiamo adesso ad analizzare il modo predefinito per chiamare le system call che veniva usato nella versione 2.4 del kernel Linux. In particolare quando parliamo di "formati delle system call" stiamo intendendo una serie di regole e convenzioni che vengono utilizzate per attivare le system call.

Le macro che ci permettono di interagire con questi formati standard si trovano nei file

  • include/asm-xx/unistd.h

  • asm/unistd.h

  • /usr/include/x86_64-linux-gnu/asm/unistd.h

  • /usr/include/linux/unistd.h

In questi file troviamo

  1. L'associazione tra codice numerico e nome della system call visto dal software lato user.

  2. L'implementazione dei moduli user level che avviano il dispatcher al fine di entrare in modo kernel e chiamare la system call di interesse.


4.1 System Call Numerical Codes

I numerical codes sono delle macro che associano ad un valore simbolico della forma __NR_<system_call_name> che rappresenta un particolare servizio offerto dal kernel un numero da 0 fino all'indice massimo della system call table. In questo modo è possibile associare ad ogni servizio offerto da kernel ed eventualmente implementato come system call, un codice numerico.

Queste macro sono definite in vari file, a seconda della particolare versione del kernel. Ad esempio troviamo,

  • Nel file /usr/include/asm/unistd_32.h troviamo i codici numerici per le system call in architetture x86 a 32 bit.

           #ifndef _ASM_X86_UNISTD_32_H
           #define _ASM_X86_UNISTD_32_H 1
    
           #define __NR_restart_syscall 0
           #define __NR_exit 1
           #define __NR_fork 2
           #define __NR_read 3
           #define __NR_write 4
           #define __NR_open 5
           #define __NR_close 6
           #define __NR_waitpid 7
           #define __NR_creat 8
           #define __NR_link 9
           #define __NR_unlink 10
    
  • Nel file /usr/include/asm/unistd_64.h invece troviamo i codici numerici per le system call in architetture x86 a 64 bit (x86_64).

           #ifndef _ASM_X86_UNISTD_64_H
           #define _ASM_X86_UNISTD_64_H 1
    
           #define __NR_read 0
           #define __NR_write 1
           #define __NR_open 2
           #define __NR_close 3
           #define __NR_stat 4
           #define __NR_fstat 5
           #define __NR_lstat 6
           #define __NR_poll 7
           #define __NR_lseek 8
           #define __NR_mmap 9
           #define __NR_mprotect 10
    

Notiamo che in x86 a 32 bit la system call associata al codice numero 0 è chiamata restart_syscall . Questa system call è una system call "dummy", nel senso che quando viene eseguita non fa altro che ritornare, senza fare nessun tipo di lavoro effettivo. Il codice di questa system call viene utilizzato ogni volta che nella system call table è presente un valore numerico associato ad un servizio che non è ancora stato implementato all'interno del kernel. In questo senso la system call "dummy" funzoina da stub, che blocca i buchi della system call table.


4.2 Macros to Trigger System Calls

All'interno dei file menzionati prima sono anche presenti delle macro che permettono di definire dei blocchi di codice necessari per chiamare una particolare system call. In particolare abbiamo una macro per numero di argomenti, come mostrato a seguire

  • _syscall0(type, name)

  • _syscall1(type, name, type1, arg1)

  • _syscall2(type, name, type1, arg1, type2, arg2)

Andando ad analizzare la macro _syscall0(type, name) , che permette di chiamare una qualsiasi system call con \(0\) argomenti, troviamo il seguente codice

     #define _syscall0(type, name)                      \
       type name(void)                          \
       {                                                \
         long __res;                                    \
         __asm__ volatile ("int $0x80"          \
                           : "=a" (__res)               \
                           : "0" (__NR_##name));        \
         __syscall_return(type, __res);         \
       }

Tale codice non fa altro che chiamare una software trap indicizzanto il GATE 0x80 , che contiene il codice necessario per far partire il dispatcher. Al dispatcher poi passiamo in input tramite il registro RAX il codice numerico della sys call da utilizzare. Infine, si prende l'output risultante dalla sys call e lo si inserisce nella variabile __res . Il contenuto di questa variabile è poi controllato attraverso la macro __syscall_return .


4.3 Manage Return Value of Syscalls

Nelle varie macro che chiamano sys call con diversi argomenti alla fine è sempre presente la macro __syscall_return(type, __res) . Il compito di questa macro è gestire il valore di ritorno in modo corretto. In particolare controlla se c'è stato qualche errore, e che tipo di errore c'è stato.

Nello standard POSIX si è definito che nel caso in cui una system call fallisce, il suo valore di ritorno dovrebbe essere \(-1\). Negli ambienti di esecuzione \(C\) poi in caso di fallimento si ha che tipicamente nella variabile errno è presente il particolare numero di errore, che contiene delle informazioni più specifiche sulla natura del fallimento. Il kernel però ritorna un solo valore dalla system call. Per poter dire che c'è stato un errore, e anche che tipologia di errore c'è stata, si è deciso di far ritornare al kernel un valore a segno negativo. La libreria <asm-i386/errno.h> contiene tutti gli user-visibile error numbers tra \(-1\) e \(-124\).

Il codice della macro __syscall_return(type, __res) è il seguente

     #define __syscall_return(type, res)                        \
       do {                                                     \
       if ((unsigned long)(res) >= (unsigned long)(-125)) {     \
       errno = -(res);                                  \
       res = -1;
     }               \
     return (type) (res); \
     } while 0

Notiamo come il costrutto do/while(0) viene utilizzato per definire delle macro con varie proprietà interessanti, in quanto permette di

  • Definire una macro (#define) con più statements.

  • Nettere un ; alla fine.

  • Utilizzare la macro dentro un blocco if.


4.4 Stub for Syscall with 6 Parameters

Il codice che permette di chiamare una system call a 6 parametri sulla i386 , ovvero la versione x86 a 32 bit è più complesso, in quanto non bastano i general purpose registers per passare tutti i parametri. Infatti su i386 abbiamo solamente \(4\) registri general purpose, mentre noi dobbiamo utilizzare \(7\) registri: 6 per i parametri, e uno per il codice della syscall.

Per risolvere questo problema vengono quindi utilizzati altri registri, tra cui il registro EBP. Prima di poterlo utilizzare però il registro EBP deve essere salvato nello stack, e dopo essere ritornati dalla system call il registro deve essere ripristinato con il suo vecchio valore.

Il codice per gestire la sys call è il seguente

     #define _syscall0(type, name, type1, arg1, type2, arg2, type3, arg3, type4, arg4, type5, arg5, type6, arg6) \
       type name(type1 arg1, type2 arg2, type3 arg3, type4 arg4, type5 arg5, type6 arg6) \
       {                                                                        \
         long __res;                                                            \
         __asm__ volatile ("push %%ebp; movl %%eax, %%ebp ; movl %1 %%eax ; int $0x80 ; pop %%ebp" \
                           : "=a" (__res)                                       \
                           :    "i" (__NR_##name), "b" ((long) (arg1)), "c" ((long)(arg2)), \
                             "d" ((long)(arg3)), "S" ((long)(arg4)), "D" ((long)(arg5)), \
                             "0" ((long)(arg6)));                               \
         __syscall_return(type, __res);                                 \
       }

4.5 Calling Conventions

Durante la chiamata del dispatcher, prima che il dispatcher prende il controllo, il firmware può cambiare lo stato del processore. In particolare il dispatcher e il firmware modificando la struttura dello stack che la system call si troverà una volta che viene mandata in esecuzione. Si parla quindi di stack allignment per intendere cosa si deve trovare nello stack e in che ordine i dati devono essere organizzati.

I valori che vengono inseriti nello stack da parte del firmware sono tipicamente utilizzati per gestire il ring model. Ad esempio vengono memorizzate informazioni sul code segment originale e sullo stack pointer originale.

A seconda della versione di x86, abbiamo diversi stack allignments.


Nella modalità a 64 bit, x86-64, è stato introdotto un ulteriore metodo per passare in modo kernel. Questo metodo è stato introdotto specificatamente per l'esecuzione di system call e a differenza del meccanismo delle software traps e degli interrupt, questo nuovo metodo è più performante, anche se necessita un settaggio dello stato del processore più estensivo.

Dato che abbiamo due modalità per entrare nel kernel, il modo e l'ordine in cui vengono utilizzati i vari registri cambia a seconda se stiamo utilizzando il metodo classico (quello delle trap), o il metodo veloce. Per fare un esempio il registro RCX viene utilizzato in modo diverso dai due meccanismi appena menzionati: con il vecchio metodo viene utilizzato per passare il terzo argomento (arg3), mentre nel nuovo metodo viene utilizzato per passare l'indirizzo di ritorno syscall/sysret.

Oltre a definire lo stack allignment, la ABI (Application Binary Interface) specifica anche chi deve preservare i registri del processore. Parte di questi registri devono essere infatti preservati dal "chiamato" e alcuni dal "chiamante". In particolare se il chiamato vuole utilizzare i registri RBX, RBP e da R12 a R15, sarà suo il compito di preservarli (registri callee saved). Tutti gli altri registri sono salvati dal chiamante, se è interessato a preservane i valori.


4.5.1 ABI vs reality

Notiamo che non ci basta essere compliant con quello che descrive l'ABI per essere sicuri che i parametri vengano passati in modo corretto. Molto spesso infatti quando eseguiamo una system call esistono dei parametri impliciti, che non passiamo direttamente ma che devono modificare il funzionamento della chiamata di sistema. Un esempio di questo scenario è durante l'esecuzione della chiamata di sistema fork() , che non prende esplicitamente dei parametri, ma che utilizza lo stato della CPU come parametro implicito per forkare il processo.

Per garantire il corretto funzionamento di chiamate di sistema come la fork(), il dispatcher deve prendere un completo snapshot dei registri della CPU. Questo snapshot viene salvato nella stack di sistema. Così facendo ogni system call è in grado di leggere il completo stato del processore del processo chiamante.

Per fare questo dobbiamo anche avere un modo per permetterci di specificare il modo in cui una funzione scritta in un linguaggio di alto livello come il C debba prendere i suoi parametri. In particolare dobbiamo specificare se i parametri devono essere presi dai registri oppure dallo stack.

Detto questo, dato però che lo stato che scriviamo sullo stack è uguale allo stato del processore in esecuzione, ci basta leggere le informazioni dalla CPU per migliorare le performance.


4.5.2 i386 stack allignment

La regola di stack allignemnt su i386 è la seguente

Il registro EAX è utilizzato per impostare il valore di ritorno della system call, mentre orig_eax rappresenta il valore originale contenuto in EAX , ovvero il codice numerico della system call utilizzato dal dispatcher.

Lavorando in modalità 32 bit le informazioni sullo stato della CPU sono memorizzate nella struttura struct t_regs , che viene passata ogni volta al codice delle system call che andiamo ad eseguire. Tale struttura è definita come segue

struct pt_regs {

  // --------- Software saved ---------
  // (no distinction between caller save and callee save)
  unsigned long bx;    // Register RBX
  unsigned long cx;    // Register RCX
  unsigned long dx;    // Register RDX
  unsigned long si;    // Register RSI
  unsigned long di;    // Register RDI
  unsigned long bp;    // Register RBP
  unsigned long ax;    // Register RAX
  
  unsigned short ds;    // Selector DS
  unsigned short __dsh; // Padding
  unsigned short es;    // Selector ES
  unsigned short __esh; // Padding
  unsigned short fs;    // Selector FS
  unsigned short __fsh; // Padding
  unsigned short gs;    // Selector GS
  unsigned short __gsh; // Padding  

  unsigned long orig_ax; // Original RAX value

  // --------- Firmware saved ---------
  unsigned long ip;
  unsigned long cs;
  unsigned long __csh;
  unsigned long flags;
  unsigned long sp;
  unsigned long ss;
  unsigned long __ssh;      
} 

4.5.3 x86-64 stack allignment

Passando da i386 a x86-64, con l'introduzione della fast system call path, la Application Binary Interface (ABI) è stata aggiornata. Nella nuova versione dell'ABI gli argomenti vengono passati al dispatcher non più tramite la stack ma tramite i registri. In particolare abbiamo


Lavorando a 64 bit la struttura pt_regs è definita come segue

struct pt_regs {
  /* C ABI says these regs are callee-preserved.  They aren't saved on
     kernel entry unless syscall needs a complete, fully filled
     "struct pt_regs". */
  unsigned long r15; unsigned long r14; unsigned long r13;
  unsigned long r12; unsigned long pb; unsigned long bx;

  /* These regs are callee-clobbered. Always saved on kernel entry. */
  unsigned long r11;
  unsigned long r10;
  unsigned long r9;
  unsigned long r8;
  unsigned long ax;
  unsigned long cx;
  unsigned long dx;
  unsigned long si; unsigned long di;

  /* On syscall entry, this is syscall @- 
     On CPU exception, this is error code. 
     On hw interrupt, its IRQ number. */
  unsigned long orig_ax;

  /* Return frame for iretq (firmware managed)*/
  unsigned long ip;
  unsigned long cs;
  unsigned long flags;
  unsigned long sp;
  unsigned long ss;
  /* top of stack page */
};

5 Running Examples

Andiamo adesso a vedere qualche esempio di utilizzo delle macro appena descritte.


5.1 SYS-CALL/asm-terminal-echo.c

In questo blocco di codice costruiamo un meccanismo per eseguire echo senza utilizzare le tipiche system call read() e write() offerte dalla libreria C, ma andando a codificare all'interno del codice C un pezzo di codice ASM che chiama direttamente il dispatcher attraverso la software trap "int 0x80".

Il corpo principale del codice è un while loop che esegue due funzioni

     char c; // putting c here (low addresses) allows correct runs when
             // using ecx as pointer to c with -D m32 compilation

     char* newline="\n";
     char v[1024];

     void instring(char*);
     void outstring(char*);

     int _start(int argc){

       while(1){
         instring(v);
         outstring(v);
         outstring(newline);
       }

     }

Le funzioni di lettura e scrittura contengono il codice ASM necessari per comunicare al kernel di leggere e scrivere dal terminale e sono implementate come segue

  • instring(v) legge una stringa da terminale un byte alla volta

           void instring(char *s){
             long  i = 0;
    
             do{
           #ifndef m32
               // In modalità x86-64 abbiamo un modo alternativo per passare in
               // lato kernel per eseguire le system call. Questo medoto si basa
               // sull'istruzione syscall. 
               asm("mov %%rbx, %%rdi; \n"
                   "mov %%rcx, %%rsi; \n"
                   "mov %%rdx, %%rdx; \n"
                   "syscall "
                   :
                   : "a" (0) ,
                     "b" (0) ,
                     "c" ((unsigned long)(&c)) ,
                     "d" ((long)(1)));
           #else
               // In modalità x86 a 32 bit l'unico modo per eseguire le sys call
               // è attraverso il GATE 0x80. Questo codice non fa altro che
               // chiamare la system call write() per leggere da STDOUT un byte e
               // scriverlo nella cella di memoria di c
               //
               //        read(0, &c, 1); 
               // 
               asm("int $0x80" :
                   : "a" (3) , // Imposta RAX a 3, che è il codice numerico della
                               // systemc all read(). 
    
                     "b" (0) , // Primo parametro della sys call, 0 = STDOUT. 
    
                     "c" ((unsigned long)(&c)) , // Secondo parametro della sys
                                                 // call, indirizzo della variabile c.
    
                     "d" ((long)(1)));  // Terzo parametro della sys call, numero
                                        // di byte da scriveren nel terminale. 
           #endif
               s[i] = c; 
               i++; 
             }
             while ( (c!='\n') && (c!=' ') && (c!='\t')); // Ripeti finché non
                                                          // troviamo uno spazio,
                                                          // una newline, o una tab. 
             i--;
             s[i]='\0';
           }
    
    
  • outstring(v) , che scrive a terminale.

           void outstring(char *s){
             long  i=0;
             while (s[i++]!='\0'); // compute length of string to print
             i--;
      
           #ifndef m32
             asm("mov %%rbx, %%rdi; \n"
                 "mov %%rcx, %%rsi; \n"
                 "mov %%rdx, %%rdx; \n"
                 "syscall "
                 :
                 : "a" (1) ,
                   "b" (1) ,
                   "c" ((unsigned long)(s)) ,
                   "d" ((long)(i)) );
           #else
             asm("int $0x80"
                 :
                 : "a" (4) ,
                   "b" (1) ,
                   "c" ((unsigned long)(s)) ,
                   "d" ((long)(i)) );
           #endif
           }
    

5.2 SYS-CALL/sys-call-macro.c

In questo esempio abbiamo definito a mano una macro per poter definire lato user una nuova system call che effettua una fork()

     #include <unistd.h>
     #define __NR_my_fork 2

     #define _new_syscall0(name)                        \
       int name(void)                           \
       {                                                \
         asm("int $0x80" : : "a" (__NR_##my_fork) ); \
         return 0;                                      \
       }                                                \

     _new_syscall0(my_fork)

       int main(int a, char** b){
       my_fork();
       pause();
     }

6 Homework #1: TLS

Segue adesso la descrizione di uno dei primi homeworks che sono stati assegnati durante il corso. Il codice dettagliato degli homework è presente nella cartella HOMEWORK 1 - TLS. Qui riportiamo solo una breve discussione sui punti salienti di una possibile soluzione.


6.1 Requirements

This homework deals with the implementation of a TLS support standing aside of the original one offered by gcc. It should be based on a few macros with the following meaning:

  • PER_THREAD_MEMORY_START and PER_THREAD_MEMORY_END are used to delimitate the variable declarations that will be part of TLS

  • READ_THREAD_VARIABLE(name) is used to return the value of a TLS variable with a given name

  • WRITE_THREAD_VARIABLE(name, value) is used to update the TLS variable with a given name to a given value

Essentially, the homework is tailored to implementing an interface for managing per-thread memory resembling the one offered by the Linux kernel for managing per-CPU memory.


6.2 Solution

L'idea base è quella di definire le macro di creazione della TLS in modo da wrappare le variabili che vogliamo all'interno della TLS all'interno di una struct.

     #define PER_THREAD_MEMORY_START typedef struct tls_zone {  \
       unsigned long tls_zone_address;

     #define PER_THREAD_MEMORY_END } tls_zone;

dove nel campo tls_zone_address ci andremmo ad inserire l'indirizzo della tls_zone in modo da poterlo recuperare velocemente. Così facendo se scriviamo

     PER_THREAD_MEMORY_START
     int x;
     int y;
     double z;
     PER_THREAD_MEMORY_END

La macro verrà risolta durante la compilazione nel seguente modo

     typedef struct tls_zone {      
       unsigned long tls_zone_address;
       int x;
       int y;
       double z;
     } tls_zone;

La creazone della TLS avviene nel seguente modo: nella funzione di creazione del thread ci andiamo ad allocare della memoria con mmap() , ci salviamo le informazioni sull'indirizzo della TLS zone all'interno della TLS zone, e settiamo la base del segmento puntato dal selettore GS con la funzione arch_prctl() .

     void *init(void *arg)
     {
       // Get ID info
       unsigned long me = (unsigned long)arg;
  
       // Allocate memory for TLS zone
       void *mem = mmap(NULL,
                        sizeof(struct tls_zone),
                        PROT_READ | PROT_WRITE,
                        MAP_ANONYMOUS | MAP_PRIVATE,
                        0, 
                        0);

       // Put address information in the TLS zone
       ((tls_zone *)mem)->tls_zone_address = (unsigned long) mem;

       // Set base for GS segment
       arch_prctl(ARCH_SET_GS, mem);
       printf("(%d) - Base address for GS segment is at %p\n", me, mem);  

       // Do actual work
       work(me);
  
       return NULL;
     }

Così facendo ogni volta che il thread è in esecuzione possiamo risalire all'indirizzo della TLS utilizzando l'indirisso base del segmento GS.


Al fine di definire le funzioni per la lettura la scrittura delle variabili nella nostra TLS definiamo prima la funzione che ritorna l'indirizzo base del TLS. Dato che il TLS verrà caricato all'indirizzo base del segmento puntato dal selettore GS, possiamo definire tale funzione come segue

     unsigned long get_tls_address(void)
     {
       unsigned long addr;
       asm("mov %%gs:(0), %0": "=b" (addr));
       return addr;
     }

Riportiamo quindi l'implementazione delle macro di lettura e scrittura.

     #define READ_THREAD_VARIABLE(name) \
       ((tls_zone*)(get_tls_address()))->name

     #define WRITE_THREAD_VARIABLE(name, value) \
       ((tls_zone*)(get_tls_address()))->name = value;

Per testare il codice appena scritto possiamo quindi utilizzare la funzione work()

     void work(unsigned long id)
     {
       int c;
       double d;

       printf("(%d) - Before work\n", id);
       c = READ_THREAD_VARIABLE(x);
       printf("(%d) - Variable x has value: %d\n", id, c);

       // ------
       if(id == 1) {
         WRITE_THREAD_VARIABLE(x, 10);
       } else if (id == 2) {
         WRITE_THREAD_VARIABLE(x, 20);
       }

       // ------
       printf("(%d) - After work\n", id);
       c = READ_THREAD_VARIABLE(x);
       printf("(%d) - Variable x has value: %d\n", id, c);
     }

Infine, come main() possiamo semplicemente creare due thread e vedere il risultato

     int main(int argc, char **argv)
     {
       pthread_t tid1, tid2;

       pthread_create(&tid1, NULL, init, (void*) 1);
       pthread_create(&tid2, NULL, init, (void*) 2);

       pause();
  
       return 0;
     }

Compilando ed seguendo

   gcc tls.c -lpthread
   ./a.out

otteniamo il seguente risultato

     (1) - Base address for GS segment is at 0x7f5c20f56000
     (1) - Before work
     (1) - Variable x has value: 0
     (1) - After work
     (1) - Variable x has value: 10
     (2) - Base address for GS segment is at 0x7f5c20f55000
     (2) - Before work
     (2) - Variable x has value: 0
     (2) - After work
     (2) - Variable x has value: 20

Come possiamo vedere, ogni thread ha una propria versione locale della variabile \(x\), anche se il codice utilizzato per accedere alla variabile è lo stesso: l'offset generato dal compilato non cambia, ma durante l'esecuzione l'indirizzo base associato al selettore GS utilizzato dal primo thread è diverso dall'indirizzo base associato al selettore GS utilizzando dal secondo thread.