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 */
Maschera di Bit per Impostare un Bit
Le operazioni necessarie per impostare un bit j
in una variabile i
sono:
-
Creare una maschera di bit con un 1 in posizione
j
e 0 altrove:unsigned int SET_MASK = 1 << j;
-
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 */
Maschera di Bit per Cancellare un Bit
Per cancellare un bit j
in una variabile i
, dobbiamo:
-
Creare una maschera di bit con un 0 in posizione
j
e 1 altrove:unsigned int CLEAR_MASK = ~(1 << j);
-
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 */
}
Maschera di Bit per Testare un Bit
Per testare se un bit j
è impostato in una variabile i
, dobbiamo:
-
Creare una maschera di bit con un 1 in posizione
j
e 0 altrove:unsigned int TEST_MASK = 1 << j;
-
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 dii
.| 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);
Maschera di Bit per Modificare un Bit-Field
Per modificare un blocco di bit consecutivi in una variabile i
, dobbiamo:
-
Creare una maschera di bit per cancellare i bit del bit-field:
unsigned int CLEAR_MASK = ~0x0070;
-
Creare una maschera di bit per impostare i nuovi bit del bit-field:
unsigned int SET_MASK = j << 4;
-
Usare l'operatore bitwise AND per cancellare i bit del bit-field:
i = i & CLEAR_MASK;
-
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 */
Maschera di Bit per Recuperare un Bit-Field
Per recuperare un blocco di bit consecutivi in una variabile i
, dobbiamo:
-
Scorrere i bit del bit-field a destra per posizionarli all'estremità destra:
j = i >> 4;
-
Creare una maschera di bit per estrarre il bit-field:
unsigned int EXTRACT_MASK = 0x0007;
-
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.