Da Bitmap a Endianess

Alcune volte idee complesse si rivelano semplici. Altra volta idee semplici si rivelano complicate.

La mia idea semplice era creare una file in formato bitmap.

Un’immagine vale più di mille parole. Questo può anche non essere vero così come non è necessariamente vero che mille parole occupano meno byte rispetto ad un immagine dato che tutto dipende dalla lunghezza delle parole e dalle dimensioni dell’immagine (nonchè dalla sua profondità di colore, compressione etc…).

Nel caso di un’immagine in formato bitmap la cosa è comunque piuttosto vero, ma le dimensioni di un’immagine in formato bitmap non mi preoccupavano affatto: avevo bisogno di un grafico e il formato bitmap mi sembrava appropriato.

Non intendo descrivere in dettaglio come è strutturato un file bitmap, basta sapere che c’è un header per il file, seguito da un header per la Device Indipendent Bitmap seguito dai pixel (questo vale per una bitmap con 24 bit di profondità colore).

Ora, dato che programmo in C, per me il file header e il DIB header sono due struct.

Il mio primo problema è stato che nelle struct che ho trovato i vari membri sono WORD o DWORD. Se stessi scrivendo il programma per farlo girare sotto windows non sarebbe un problema (in questo caso penso che includendo windows.h non avrei nemmeno bisogno di crearmi le struct).

Però io stavo programmando sulla mia Ultra 10, sotto Solaris 10.

Bene, un’occhiata più attenta alla descrizione degli header e scopro che i vari membri hanno una lunghezza precisa in byte.

Qui ho fatto il mio primo errore: non ho continuato a leggere con attenzione e mi sono messo a programmare. Qualche sizeof() e qualche printf() e so che probabilmente me la posso cavare utilizzando unsigned short int, signed int e unsigned int.

Detto fatto, scrivo il programma, calcolo tutti i valori a mano per creare un file bitmap che rappresenti un’immagine di 2 pixel per 2 pixel.

Il file viene creato regolarmente, ma ovviamente non viene riconosciuto da nessun programma di grafica.

A questo punto anzichè fare la cosa più sensata, ovvero tornare a leggere la documentazione, sono andato avanti creando con un qualsiasi software grafico un file in formato bitmap da due pixel per due pixel che sarebbe stata la mia guida e un piccolo programma il cui compito era mostrarmi byte per byte del file ogni bit di detti file.

A questo, guardando gli zero e gli uno, ho capito.

Il problema era l’endianess.

Se avessi letto la documentazione con attenzione avrei dovuto notare che tutti i valori andavano scritti in formato little-endian.

Forse, se anzichè su una workstation SPARC mi fossi trovato a scrivere il programma su un’altra macchina non avrei avuto questo problema…

Bene, i processori x86 sono little-endian mentre i processori SPARC sono big-endian (non è precisamente così ma è una buona approssimazione). Solaris su x86 è little-endian. Solaris su SPARC (il mio caso) è big-endian.

Che cosa vogliono dire big-endian e little-endian? E’ un problema che riguarda l’ordine dei byte, a seconda che il byte più significativo venga per primo oppure per ultimo.

A questo punto era chiaro che quando mi trovavo un membro di una struttura della lunghezza di due byte dovevo invertire il primo byte con il secondo byte, ovvero da 0x89AC dovevo ottenere 0xAC89.

In C la cosa non è molto complessa: scorrendo di 8 bit a destra, da 0x89AC ottengo 0x0089 mentre scorrendo di 8 bit a sinistra ottengo 0xAC00. Un OR tra 0x0089 e 0xAC00 mi fa ottenere 0xAC89.

In codice:

t = 0x89AC;
t = t >> 8 | t << 8;

La cosa erano più complesse quando il membro della struttura era formata da 4 byte. In questo caso, il valore 0x1246ACEF doveva diventare 0xEFAC4612.

Uno scorrimento a destra e a sinistra da 24 bit mi permette di ottenere i valori 0x00000012 e 0xEF000000. Uno scorrimento a destra e a sinistra di 8 bit mi permette di ottenere i valori 0x001246AC e 0x46ACEF00. Un OR tra 0x0000FF00 e 0x001246AC mi fa ottenere 0x00004600 mentre un OR tra 0x00FF0000 e 0x46ACEF00 mi fa ottenere 0x00AC0000 (anzichè usare degli OR avrei potuto fare altri scorrimenti per ottenere solo questi due byte). A questo punto con un OR tra 0x00000012, 0xEF000000, 0x00004600 e 0x00AC0000 finalmente mi fa ottenere il valore desiderato.

In codice:

t = 0x1246ACEF;
t = (t >> 24) | (t >> 8 ) & 0x0000FF00) | ((t << 8 ) & 0x00FF0000)

Con le dovute correzioni adesso il mio codice funzionava e creava correttamente il file in formato bitmap, ma c’era ancora un tarlo che mi rodeva.

Come capire se mi trovavo in un ambiente little-endian oppure big-endian dall’interno del mio programma?

Inizialmente ho pensato ai campi di bit, ma l’ordine dei singoli bit all’interno di un campo di bit non è garantito.

Allora ho pensato ad una union per accedere ad esempio ad un int come se si trattasse di due char.

In codice:

union {
int i;
char c[2];
} u;

Effettivamente le cose parevano funzionare, ma esteticamente non mi piaceva.

Quindi sono passato a questo codice:

union {
int i;
char c;
} u;

Anche in questo caso non mi piaceva.

Alla fine ho provato con i puntatori, con questo codice:


int i;
char *p;
...
p = (char *) &i;

Un mio amico mi ha giustamente fatto notare che nel caso un int fosse soltanto di 8 bit le cose non funzionerebbero. Naturalmente è estremamente improbabile trovare un int da 8 bit. Ho riscritto il codice in questo modo:

long long int;
char *p;
...
p = (char *) &i;

Credo ch trovare un long long int lungo 8 bit è senz’altro più improbabile che trovare un int da 8 bit.

Naturalmente, solo a questo punto ho fatto la cosa più sensata: usare il C99 e stdint.h

La morale di questo post è:

1) Leggere bene la documentazione

2) Rileggerla

3) Conoscere la libreria standard C

This entry was posted in C, Programmazione, Solaris 10, Windows. Bookmark the permalink.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *