Avanti Indietro Indice

4. Il porting e la compilazione

4.1 Simboli definiti automaticamente

È possibile elencare i simboli definiti automaticamente dalla propria versione di gcc eseguendolo con l'opzione -v. Ad esempio:

$ echo 'main(){printf("hello world\n");}' | gcc -E -v -
Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specs
gcc version 2.7.2
/usr/lib/gcc-lib/i486-box-linux/2.7.2/cpp -lang-c -v -undef
-D__GNUC__=2 -D__GNUC_MINOR__=7 -D__ELF__ -Dunix -Di386 -Dlinux
-D__ELF__ -D__unix__ -D__i386__ -D__linux__ -D__unix -D__i386
-D__linux -Asystem(unix) -Asystem(posix) -Acpu(i386)
-Amachine(i386) -D__i486__ -

Se si sta scrivendo del codice che utilizza delle caratteristiche specifiche per Linux, è una buona idea includere le parti non portabili in

#ifdef __linux__
/* ... altro codice ... */
#endif /* linux */

Si utilizzi __linux__ per questo scopo, non semplicemente linux. Sebbene anche il secondo sia definito, non è conforme a POSIX.

4.2 Chiamata del compilatore

La documentazione per le opzioni del compilatore è rappresentata dalla info page di gcc (in Emacs, si utilizzi C-h i quindi si selezioni la voce 'gcc'). Il proprio distributore potrebbe non aver incluso questa parte nel sistema, o la versione che si possiede potrebbe essere vecchia; la cosa migliore da fare in questo caso consiste nello scaricare l'archivio sorgente gcc da ftp://prep.ai.mit.edu/pub/gnu o da uno dei suoi siti mirror.

La pagina di manuale gcc (gcc.1) di solito è obsoleta. Tentando di leggerla si troverà questo avvertimento.

Opzioni del compilatore

L'output di gcc può essere ottimizzato aggiungendo -On alla sua riga di comando, dove n rappresenta un intero opzionale. I valori significativi di n, ed il loro esatto effetto, variano a seconda della versione; tipicamente vanno da 0 (nessuna ottimizzazione) a 2 (molte ottimizzazioni) a 3 (moltissime ottimizzazioni).

Internamente, gcc le traduce in una serie di opzioni -f e -m. È possibile vedere esattamente quale livello -O si lega a ogni opzione eseguendo gcc -v -Q (Q è un'opzione non documentata). Ad esempio, -O2 sul sistema dell'autore produce questo risultato:

enabled:        -fdefer-pop -fcse-follow-jumps -fcse-skip-blocks
            -fexpensive-optimizations
            -fthread-jumps -fpeephole -fforce-mem -ffunction-cse -finline
            -fcaller-saves -fpcc-struct-return -frerun-cse-after-loop
            -fcommon -fgnu-linker -m80387 -mhard-float -mno-soft-float
            -mno-386 -m486 -mieee-fp -mfp-ret-in-387

L'utilizzo di un livello di ottimizzazione maggiore di quanto previsto per il proprio compilatore (ad esempio, -O6) avrà lo stesso risultato che utilizzare il livello più alto che è in grado di supportare. Tuttavia, la distribuzione di codice impostato per la compilazione in questo modo non è una buona idea - se in versioni future saranno incorporate ulteriori ottimizzazioni, potrebbe accadere che interrompano il proprio codice.

Gli utenti di gcc dalla versione 2.7.0 fino alla 2.7.2 dovrebbero notare che esiste un errore in -O2. In particolare, '-fstrenght-reduction' non funziona. È disponibile una patch per risolvere questo problema, che necessita della ricompilazione del gcc, altrimenti è sufficiente assicurarsi di usare sempre l'opzione -fno-strength-reduce.

Opzioni specifiche per il processore

Esistono altre opzioni -m che non sono attivate da nessun -O, e si dimostrano utili in molti casi. Le principali sono -m386 e -m486, che indicano a gcc di favorire rispettivamente 386 o 486. Il codice compilato con una di queste due opzioni funzionerà anche su macchine dell'altro tipo; il codice 486 è più voluminoso, ma non è più lento se eseguito su 386.

Attualmente non esiste un'opzione -mpentium o -m586. Linus suggerisce di utilizzare -m486 -malign-loops=2 -malign-jumps=2 -malign-functions=2, per ottenere l'ottimizzazione del codice 486 ma senza i salti necessari per l'allineamento (di cui il pentium non ha bisogno). Michael Meissner (di Cygnus) dice:

La mia impressione è che -mno-strength-reduce produca anche un codice più veloce sull'x86 (si noti che non mi sto riferendo all'errore relativo all'opzione '-fstrength-reduction': altra questione). Ciò è dovuto alla carenza di registri dell'x86 (ed il metodo di GCC di raggruppare i registri in registri sparsi piuttosto che in altri registri non aiuta molto). 'strenght-reduce' ha come effetto l'utilizzo di registri addizionali per sostituire moltiplicazioni con addizioni. Sospetto anche che -fcaller-saves possa causare una perdita di prestazioni.
Un'altra considerazione consiste nel fatto che -fomit-frame-pointer possa o meno rappresentare un vantaggio. Da un lato, rende disponibile un altro registro per l'uso; tuttavia, il modo in cui x86 codifica il suo set di istruzioni comporta che gli indirizzi relativi di stack occupino più spazio rispetto agli indirizzi relativi di frame; di conseguenza, sarà disponibile ai programmi una quantità (leggermente) inferiore di Icache. Inoltre, -fomit-frame-pointer implica il continuo aggiustamento del puntatore allo stack da parte del compilatore, dopo ogni chiamata, quando, con un frame, può lasciare che lo stack esegua un accumulo per alcune chiamate.

L'ultima parola su questo argomento viene ancora da Linus:

Se si desidera ottenere prestazioni ottimali, non credete alle mie parole, effettuate delle prove. Esistono molte opzioni nel compilatore gcc, e può darsi che un insieme particolare dia l'ottimizzazione migliore per la propria impostazione.

Internal compiler error: cc1 got fatal signal 11

(Ovvero: "Errore interno del compilatore: cc1 ha ricevuto il segnale fatale 11").

Il segnale 11 è SIGSEGV, o 'segmentation violation'. Solitamente significa che il programma ha confuso i puntatori e ha tentato di scrivere su una porzione di memoria che non possedeva. Potrebbe trattarsi di un errore di gcc.

Tuttavia, gcc è ben verificato ed affidabile, nella maggior parte dei casi. Utilizza anche un gran numero di strutture dati complesse, e una grande quantità di puntatori. In breve, è il miglior tester di RAM tra quelli disponibili comunemente. Se non è possibile replicare l'errore - il sistema non si ferma nello stesso punto quando si riavvia la compilazione - molto probabilmente si tratta di un problema legato all'hardware (CPU, memoria, scheda madre o cache). Non lo si deve considerare un errore del compilatore solo perché il proprio computer supera i controlli di avvio del sistema o è in grado di eseguire Windows o qualunque altro programma; questi 'test' sono comunemente ritenuti, a ragione, di nessun valore. Comunque è sbagliato ritenere che si tratti di un errore, perché una compilazione del kernel si blocca sempre durante `make zImage' - è logico che ciò accada: `make zImage' probabilmente compila più di 200 file. In genere si ricerca un bug in un insieme più piccolo di così.

Se è possibile duplicare l'errore, e (ancora meglio) produrre un breve programma che lo dimostra, è possibile inviarlo come report di errori all'FSF, o alla mailing list di linux-gcc. Si faccia riferimento alla documentazione di gcc per i dettagli relativi alle informazioni effettivamente necessarie.

4.3 Portabilità

È stato detto che, in questi giorni, se non può essere portato a Linux, allora è qualcosa che non vale la pena di possedere. :-)

In generale, sono necessarie solo piccole modifiche per ottenere la conformità POSIX al 100% di Linux. Sarebbe anche molto utile restituire agli autori ogni modifica al codice in modo che, in futuro, si possa ottenere un eseguibile funzionante tramite il solo comando 'make'.

BSD (inclusi bsd_ioctl, demone e <sgtty.h>)

È possibile compilare il proprio programma con -I/usr/include/bsd ed eseguire il link con -lbsd (ossia, aggiungere -I/usr/include/bsd a CFLAGS e -lbsd alla riga LDFLAGS nel proprio Makefile). Non è più necessario aggiungere -D__USE_BSD_SIGNAL se si desidera un comportamento dei segnali di tipo BSD, dal momento che questa caratteristica viene ottenuta automaticamente quando si ha -I/usr/include/bsd e l'include <signal.h>.

Segnali 'mancanti' (SIGBUS, SIGEMT,SIGIOT, SIGTRAP, SIGSYS ecc)

Linux è conforme a POSIX. Quelli elencati nel seguito sono segnali non definiti in POSIX - ISO/IEC 9945-1:1990 (IEEE Std 1003.1-1990), paragrafo B.3.3.1.1:

"I segnali SIGBUS, SIGEMT, SIGIOT, SIGTRAP, e SIGSYS sono stati omessi da POSIX.1 perché il loro comportamento dipende dall'implementazione e non è stato possibile classificarlo adeguatamente. Implementazioni conformi potranno contenere questi segnali, ma devono documentare le circostanze in cui sono rilasciati ed elencare ogni restrizione riguardante il loro rilascio."

Il modo più economico e scadente di gestire la cosa consiste nel ridefinire questi segnali in SIGUNUSED. Il modo corretto consiste nell'inserire il codice che li gestisce in appropriati #ifdef

#ifdef SIGSYS
/* ... codice SIGSYS non-posix .... */
#endif

Codice K & R

GCC è un compilatore ANSI; una grande quantità di codice esistente non è ANSI. Non c'è molto da fare per risolvere questo problema, oltre all'aggiungere -traditional alle opzioni del compilatore. Si invita a consultare l'info page di gcc.

Si noti che -traditional ha altri effetti oltre a cambiare il linguaggio accettato da gcc. Ad esempio, attiva -fwritable-strings, che sposta le stringhe costanti nello spazio dati (dallo spazio di codice, dove non possono essere scritte). Questo aumenta l'occupazione di memoria del programma.

Conflitto dei simboli di preprocessore con prototipi nel codice

Uno dei problemi più frequenti consiste nel fatto che alcune funzioni comuni sono definite come macro negli header file di Linux e il preprocessore si rifiuterà di eseguire il parsing di definizioni prototipo simili. I più comuni sono atoi() e atol().

sprintf()

Soprattutto quando si esegue il porting da SunOS, è necessario essere consapevoli del fatto che sprintf(string, fmt, ...) restituisce un puntatore a stringhe, mentre Linux (seguendo ANSI) restituisce il numero di caratteri che sono stati messi nella stringa.

fcntl e affini. Quali sono le definizioni di FD_*?

Le definizioni si trovano in <sys/time.h>. Se si sta utilizzando fcntl, probabilmente si vorrà includere anche <unistd.h>.

In genere, la pagina di manuale di una funzione elenca gli #include necessarie nella sua sezione SYNOPSIS.

Il timeout di select(). Programmi in busy-waiting

Una volta, il parametro timeout di select() era utilizzato a sola lettura. Già allora, le pagine di manuale avvertivano:

select() dovrebbe probabilmente restituire il tempo rimanente dal timeout originale, se esistente, modificando il valore del tempo. Questo potrà essere implementato in versioni future del sistema. Pertanto, sarebbe sbagliato ritenere che il puntatore al timeout non sarà modificato dalla chiamata select().

Ora il futuro è arrivato. Di ritorno da una select(), l'argomento di timeout sarà impostato al tempo rimanente che si sarebbe atteso se i dati non fossero arrivati. Se non è arrivato alcun dato, il valore sarà zero, e le chiamate future utilizzando la stessa struttura timeout eseguiranno immediatamente il return.

Per rimediare, in caso di errore, si metta il valore di timeout nella struttura ogni volta che si chiama select(). Si modifichi il codice come:


struct timeval timeout;
timeout.tv_sec = 1; timeout.tv_usec = 0;
while (some_condition)
        select(n,readfds,writefds,exceptfds,&timeout);

in


struct timeval timeout;
while (some_condition) {
        timeout.tv_sec = 1; timeout.tv_usec = 0;
        select(n,readfds,writefds,exceptfds,&timeout);
}

Alcune versioni di Mosaic evidenziavano questo problema. La velocità dell'animazione del globo rotante era inversamente correlata alla velocità con cui i dati giungevano dalla rete!

Chiamate di sistema interrotte

Sintomo

Quando un programma viene interrotto utilizzando Ctrl-Z e poi viene riavviato - o in altre situazioni che generano dei segnali: interruzione con Ctrl-C, terminazione di un processo figlio ecc. - si ottengono messaggi del tipo "interrupted system call" o "write: unknown error".

Problema

I sistemi POSIX controllano la presenza di segnali più frequentemente rispetto a sistemi UNIX più vecchi. Linux può eseguire dei gestori di segnali:

Per altri sistemi operativi potrebbe essere necessario includere in questa lista le chiamate di sistema creat(), close(), getmsg(), putmsg(), msgrcv(), msgsnd(), recv(), send(), wait(), waitpid(), wait3(), tcdrain(), sigpause(), semop().

Se un segnale (per il quale il programma ha installato un gestore) avviene durante una chiamata di sistema, viene chiamato il gestore. Quando il gestore restituisce il controllo (alla chiamata di sistema), essa rileva che è stata interrotta e restituisce immediatamente -1 e errno = EINTR. Il programma non si aspetta che questo accada, pertanto si blocca.

È possibile scegliere tra due alternative, per rimediare.

Seguono due esempi per read() e ioctl().

Una parte di codice originale utilizzante read()


int result;
while (len > 0) {
        result = read(fd,buffer,len);
        if (result < 0) break;
        buffer += result; len -= result;
}

diventa


int result;
while (len > 0) {
        result = read(fd,buffer,len);
        if (result < 0) { if (errno != EINTR) break; }
        else { buffer += result; len -= result; }
}

e una parte di codice utilizzante ioctl()


    int result;
    result = ioctl(fd,cmd,addr);

diventa


    int result;
    do { result = ioctl(fd,cmd,addr); }
    while ((result == -1) && (errno == EINTR));

Si noti che in alcune versioni di Unix BSD il comportamento predefinito consiste nel riavviare le chiamate di sistema. Per ottenere l'interruzione delle chiamate di sistema è necessario utilizzare i flag SV_INTERRUPT o SA_INTERRUPT.

Stringhe scrivibili (il programma genera 'segmentation fault' in modo casuale)

GCC ha una visione ottimistica dei suoi utenti, considerando le costanti stringa esattamente quello che sono - delle costanti. Pertanto, le memorizza nell'area del codice, dove possono essere inserite ed estratte dall'immagine di disco del programma (invece di occupare uno swapspace). Ne consegue che ogni tentativo di riscriverle causerà 'segmentation fault'.

Questo può causare dei problemi a vecchi programmi che, per esempio eseguono una chiamata mktemp() con una stringa costante come argomento. mktemp() tenta di riscrivere il suo argomento.

Per correggere,

Perché la chiamata execl() fallisce?

Probabilmente accade perché viene eseguita in modo errato. Il primo argomento per execl è il nome del programma da eseguire. Il secondo e i successivi diventano l'array argv del programma che si sta chiamando. Ricordare che: argv[0] viene impostato anche per un programma eseguito senza argomenti. Pertanto si dovrebbe scrivere


    execl("/bin/ls","ls",NULL);

e non solo


    execl("/bin/ls", NULL);

L'esecuzione del programma senza nessun argomento è interpretata come un invito a stampare le sue dipendenze a librerie dinamiche, almeno utilizzando a.out. ELF si comporta diversamente.

(Se si desidera questa informazione di libreria, esistono interfacce più semplici; si veda il paragrafo relativo al caricamento dinamico, o la pagina di manuale per ldd).


Avanti Indietro Indice