Accesso ai Singoli Bit in Linguaggio C

Quando si lavora a basso livello con il linguaggio C, capita spesso di dover manipolare direttamente i singoli bit di una variabile.

Questa abilità risulta essenziale in campi come la grafica (dove più pixel possono essere memorizzati in un singolo byte) o in generale quando si vuole ottimizzare lo spazio e le prestazioni.

In questa lezione vedremo come usare gli operatori bitwise, introdotti nella lezione precedente, per impostare, cancellare e testare singoli bit o campi di bit (bit-field), concludendo con un esempio pratico di cifratura tramite XOR.

Accesso ai Singoli Bit

Quando facciamo programmazione di basso livello, spesso abbiamo bisogno di memorizzare dati come singoli bit o insiemi di bit. Ad esempio, nella programmazione grafica possiamo voler stipare due (o più) pixel in un solo byte. Usando gli operatori bitwise, possiamo estrarre o modificare i dati memorizzati in pochi bit.

Supponiamo che i sia una variabile unsigned short a 16 bit. Vediamo come realizzare alcune operazioni comuni su un singolo bit di i.

In generale, per realizzare tali operazioni dobbiamo preparare delle cosiddette maschere di bit. Una maschera di bit è un numero binario che ha un 1 in una posizione specifica e 0 altrove. Usando le maschere, possiamo impostare, cancellare o testare i bit di i.

Impostare un Bit - Bit Setting

Mettiamo il caso di voler impostare il bit 4 di i. Assumiamo che il bit più a sinistra, chiamato MSB Most Significant Bit o Bit più significativo, sia numerato 15, mentre il meno significativo sia 0.

Il modo più semplice per impostare il bit 4 è eseguire un'operazione OR con il valore 0x0010, una cosiddetta maschera di bit o bit-mask che contiene un 1 in posizione 4:

i = 0x0000;    /* i ora è 0000000000000000 in binario */
i |= 0x0010;   /* i ora è 0000000000010000 in binario */

In maniera più generale, se la posizione del bit da impostare è memorizzata nella variabile j, possiamo usare l'operatore di shift per creare la maschera:

i |= (1 << j); /* imposta il bit j */
Definizione

Maschera di Bit per Impostare un Bit

Le operazioni necessarie per impostare un bit j in una variabile i sono:

  1. Creare una maschera di bit con un 1 in posizione j e 0 altrove:

    unsigned int SET_MASK = 1 << j;
    
  2. Usare l'operatore bitwise OR per impostare il bit j:

    i |= SET_MASK;
    

Cancellare un Bit - Bit Clearing

Per cancellare (impostare a 0) il bit 4 di i, usiamo una maschera che abbia lo 0 in posizione 4 e 1 altrove:

i = 0x00FF;    /* i ora è 0000000011111111 in binario */
i &= ~0x0010;  /* i ora è 0000000011101111 in binario */

Seguendo la stessa idea, possiamo scrivere un'istruzione che cancella il bit di posizione j:

i &= ~(1 << j); /* cancella il bit j */
Definizione

Maschera di Bit per Cancellare un Bit

Per cancellare un bit j in una variabile i, dobbiamo:

  1. Creare una maschera di bit con un 0 in posizione j e 1 altrove:

    unsigned int CLEAR_MASK = ~(1 << j);
    
  2. Usare l'operatore bitwise AND per cancellare il bit j:

    i &= CLEAR_MASK;
    

Testare un Bit - Bit Testing

La seguente istruzione if verifica se il bit 4 di i è impostato:

if (i & 0x0010) {
    /* il bit 4 di i è impostato */
}

Per testare se il bit j è impostato, useremo:

if (i & (1 << j)) {
    /* il bit j di i è impostato */
}
Definizione

Maschera di Bit per Testare un Bit

Per testare se un bit j è impostato in una variabile i, dobbiamo:

  1. Creare una maschera di bit con un 1 in posizione j e 0 altrove:

    unsigned int TEST_MASK = 1 << j;
    
  2. Usare l'operatore bitwise AND per testare il bit j:

    if (i & TEST_MASK) {
        /* il bit j di i è impostato */
    }
    

Spesso, per lavorare con i bit, assegniamo loro nomi: ad esempio, diciamo che i bit 0, 1 e 2 di un numero corrispondono ai colori blue, green e red. Quindi:

#define BLUE  1  /* bit 0 */
#define GREEN 2  /* bit 1 */
#define RED   4  /* bit 2 */

Impostare, cancellare e testare BLUE si fa così:

i |= BLUE;    /* imposta il bit BLUE */
i &= ~BLUE;   /* cancella il bit BLUE */
if (i & BLUE) /* testa il bit BLUE */

È anche semplice impostare, cancellare o testare più bit contemporaneamente:

i = BLUE | GREEN;  /* imposta i bit BLUE e GREEN */
i &= ~(BLUE | GREEN); /* cancella i bit BLUE e GREEN */
if (i & (BLUE | GREEN)) /* testa se BLUE o GREEN è impostato */

Usare gli Operatori Bitwise per Accedere a Campi di Bit

Gestire un gruppo di bit consecutivi (un bit-field o campo di bit) è leggermente più complicato che lavorare con singoli bit. Ecco due operazioni comuni:

Modificare un Bit-Field

Per modificare un blocco di bit in i, di solito applichiamo una maschera con AND (per cancellare i bit), quindi un'operazione OR (per memorizzare i nuovi bit).
Nel seguente esempio, vogliamo scrivere in i il valore binario 101 nelle posizioni 4-6 (a partire da 0) di i:

i = i & ~0x0070 | 0x0050; /* memorizza 101 in bit 4-6 */
  • & ~0x0070 cancella i bit 4,5,6 di i.
  • | 0x0050 imposta i bit 4,6 lasciando inalterato il bit 5.

In un caso più generale, se j contiene il valore da memorizzare nei bit 4-6 di i, dobbiamo prima spostare j (ad esempio j << 4) e poi usarlo con la maschera:

i = (i & ~0x0070) | (j << 4);
Definizione

Maschera di Bit per Modificare un Bit-Field

Per modificare un blocco di bit consecutivi in una variabile i, dobbiamo:

  1. Creare una maschera di bit per cancellare i bit del bit-field:

    unsigned int CLEAR_MASK = ~0x0070;
    
  2. Creare una maschera di bit per impostare i nuovi bit del bit-field:

    unsigned int SET_MASK = j << 4;
    
  3. Usare l'operatore bitwise AND per cancellare i bit del bit-field:

    i = i & CLEAR_MASK;
    
  4. Usare l'operatore bitwise OR per impostare i nuovi bit del bit-field:

    i = i | SET_MASK;
    

Recuperare un Bit-Field

Se il bit-field si trova all'estremità destra di un numero (nei bit meno significativi), il recupero è banale. Ad esempio, per estrarre i bit 0-2 in i possiamo scrivere:

j = i & 0x0007; /* estrae i bit 0-2 */

Se invece il bit-field non si trova all'estremità destra di i, possiamo prima spostarlo a destra e poi applicare la maschera. Per estrarre i bit 4-6 di i, si fa:

j = (i >> 4) & 0x0007; /* estrae i bit 4-6 */
Definizione

Maschera di Bit per Recuperare un Bit-Field

Per recuperare un blocco di bit consecutivi in una variabile i, dobbiamo:

  1. Scorrere i bit del bit-field a destra per posizionarli all'estremità destra:

    j = i >> 4;
    
  2. Creare una maschera di bit per estrarre il bit-field:

    unsigned int EXTRACT_MASK = 0x0007;
    
  3. Usare l'operatore bitwise AND per estrarre il bit-field:

    j = j & EXTRACT_MASK;
    

Esempio: Cifratura XOR

Uno dei modi più semplici per crittografare i dati è eseguire un'operazione di exclusive-or (XOR) tra ogni carattere e una chiave segreta.

Supponiamo che la chiave sia il carattere &. Se facciamo XOR di & con il carattere z, otterremo \ (assumendo l'uso del set di caratteri ASCII):

    00100110  (codice ASCII per &)
XOR 01111010  (ASCII per z)
    --------
    01011100  (ASCII per \)

Per decrittare un messaggio, si applica lo stesso algoritmo. Crittografando una seconda volta, si recupera il testo originale. Ad esempio, XOR di & con \ ridà z.

Il seguente programma, xor.c, cifra un messaggio eseguendo l'operazione XOR su ogni carattere con la chiave &.

Il messaggio originale può essere immesso dall'utente. Il messaggio crittografato si può visualizzare a schermo o salvare in un file (reindirizzamento in output). Per recuperare il messaggio originale, basta eseguire di nuovo il programma xor sul file crittografato.

Ecco il sorgente di xor.c:

/* Esegue la crittografia XOR */
#include <ctype.h>
#include <stdio.h>

#define KEY '&'

int main(void)
{
    int orig_char, new_char;

    while ((orig_char = getchar()) != EOF) {
        new_char = orig_char ^ KEY;
        if (isprint(orig_char) && isprint(new_char))
            putchar(new_char);
        else
            putchar(orig_char);
    }
    return 0;
}

Come si vede, il programma è molto semplice. Effettuare lo XOR di un carattere con & può dare origine a caratteri di controllo invisibili per alcuni sistemi operativi. Per evitare problemi in lettura e scrittura di questi caratteri, la funzione isprint verifica se i caratteri coinvolti sono stampabili. Se uno dei due non è stampabile, il programma mantiene il carattere originale.

In Sintesi

I concetti chiave visti in questa lezione sono:

  • Impostare, cancellare e testare i bit: usando OR (|), AND (&) e complement (~) con maschere, possiamo controllare singolarmente i bit in una variabile.
  • Bit-Field: per gestire blocchi di bit consecutivi, è necessario combinare spostamenti (<<, >>) e maschere in AND/OR per sovrascrivere e leggere correttamente i dati.
  • XOR Encryption: un esempio concreto di utilizzo degli operatori bitwise è la crittografia XOR, che permette di cifrare e decifrare un testo applicando lo stesso algoritmo.