5. Miscellanea

5.1. Il comando nm

Il comando nm(1) può mostrare la lista dei simboli in una data libreria. Funziona sia con librerie statiche che condivise. Per la libreria indicata nm(1) può elencare i nomi dei simboli definiti, il valore di ciascun simbolo ed il corrispondente tipo. È inoltre in grado di indicare dove il simbolo era definito nel codice sorgente (tramite nome del file e numero di linea), se questa informazione è disponibile nella libreria stessa (si veda a questo proposito l'opzione -l).

Il tipo associato al simbolo richiede qualche ulteriore spiegazione. Il tipo è visualizzato tramite una lettera; una lettera minuscola significa che il simbolo è locale, mentre una lettera maiuscola significa che il simbolo è globale (a collegamento esterno). Solitamente i tipi associabili ad un simbolo comprendono: T (una normale definizione nella sezione di codice), D (sezione dati inizializzata), B (sezione dati non inizializzata), U (non definito; il simbolo è utilizzato dalla libreria, ma non è definito dalla libreria stessa), e W (debole; se anche un'altra libreria dovesse definire questo simbolo, tale definizione avrebbe priorità su questa).

Se si conosce il nome di una funzione, ma non ci si ricorda in quale libreria fosse definita, si può utilizzare l'opzione -o di nm (che visualizza il nome del file all'inizio di ogni linea) assieme ad un grep per trovare il nome della libreria. Gli utenti di bash, ad esempio, possono ricercare la funzione "cos" in tutte le librerie in /lib, in /usr/lib comprese le sue immediate sottodirectory e in /usr/local/lib con il seguente comando:
nm -o /lib/* /usr/lib/* /usr/lib/*/* \
      /usr/local/lib/* 2> /dev/null | grep 'cos$'

Informazioni molto più dettagliate su nm si possono trovare nella corrispondente documentazione "info" installata localmente sotto: info:binutils#nm.

5.2. Le funzioni costruttore e distruttore di una libreria

Le librerie dovrebbero esportare le procedure di inizializzazione e terminazione utilizzando gli attributi di funzione __attribute__((constructor)) ed __attribute__((destructor)) di gcc. Si veda a questo proposito la documentazione di gcc. Le funzioni costruttore vengono chiamate prima del ritorno dalla chiamata a dlopen (o prima che venga eseguita la funzione main() se la libreria viene caricata all'avvio del programma). Le funzioni distruttore vengono eseguite prima del ritorno della chiamata a dlclose (o dopo exit() o al termine dell'esecuzione di main() se la libreria viene caricata all'avvio del programma). I prototipi C per queste funzioni sono:
  void __attribute__((constructor)) mia_init(void);
  void __attribute__((destructor)) mia_fini(void);

Le librerie condivise non dovrebbero essere compilate facendo uso delle opzioni "-nostartfiles" o "-nostdlib" di gcc. Se questo avvenisse le procedure di costruzione/distruzione non verrebbero chiamate (a meno che non si applichino particolari accorgimenti).

5.2.1. Le speciali funzioni _init e _fini (OBSOLETO/PERICOLOSO)

Storicamente sono esistite due particolari funzioni, _init e _fini, utilizzabili nel controllo dell'inizializzazione e terminazione di una libreria. Ad ogni modo, questo meccanismo è oggi obsoleto e l'uso di queste funzioni può portare a risultati non predicibili. Le vostre librerie non ne dovrebbero quindi fare uso; si utilizzino piuttosto gli attributi constructor e destructor descritti in precedenza.

Se si dovesse lavorare su vecchi sistemi o su vecchio codice che utilizzano _init o _fini, ecco un'illustrazione di come funzionavano: erano definite due speciali funzioni per l'inizializzazione e terminazione di un modulo: _init e _fini. Se una libreria esporta una funzione "_init", questa viene chiamata la prima volta che viene caricata (tramite dlopen() o semplicemente all'avvio del programma, se si tratta di una libreria condivisa). In un programma C, questo significa semplicemente aver definito una qualche funzione chiamata _init. Esiste una corrispondente funzione chiamata _fini, che viene chiamata ogniqualvolta l'uso di una libreria termina (tramite una chiamata a dlclose() che ne porta il conteggio dei riferimenti a zero, o alla normale terminazione del programma). I prototipi C di queste funzioni sono:
  void _init(void);
  void _fini(void);

In questo caso, nel compilare il file sorgente in un file ".o" con gcc, ci si deve assicurare di aggiungere l'opzione "-nostartfiles". Questo evita che il compilatore C colleghi librerie di avvio di sistema al file ".so". In caso contrario si otterrebbero errori dovuti a definizioni multiple. Si noti che questo è completamente diverso dal compilare un modulo utilizzando gli attributi di funzione indicati. Si ringraziano Jim Mischel e Tim Gentry per il suggerimento di aggiungere questa discussione su _init e _fini e per l'assistenza nel comporla.

5.3. Le librerie condivise possono essere script

Vale la pena di notare che il caricatore GNU permette alle librerie condivise di essere comuni file di testo che utilizzano uno speciale linguaggio di scripting in luogo del consueto formato di libreria. Questo può risultare utile per combinare indirettamente altre librerie. Per esempio, questo è il listato di /usr/lib/libc.so su uno dei miei sistemi:
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a )

(Il commento presente nel listato indica che preferibilmente verrà utilizzata la libreria condivisa /lib/libc.so.6, ma che dal momento che alcune funzionalità sono presenti solo nella versione statica /usr/lib/libc_nonshared.a quest'ultima verrà utilizzata nei casi in cui la prima non fosse sufficiente. NDT) Per ulteriori informazioni a questo proposito si rimanda alla documentazione texinfo relativa agli script per il linker ld (ld command language). Informazioni generali si trovano in info:ld#Options and info:ld#Commands, mentre i comandi di uso più comune sono discussi in info:ld#Option Commands.

5.4. Versione dei simboli e script di versione

Tipicamente i riferimenti a funzioni esterne vengono collegati quando necessario e non vengono quindi tutti collegati all'avvio del programma. Se una libreria condivisa non fosse aggiornata, qualche porzione dell'interfaccia richiesta potrebbe mancare; se l'applicazione tentasse di utilizzarla potrebbe quindi improvvisamente ed inaspettatamente fallire.

Una soluzione a questo problema consiste nel controllo di versione dei simboli abbinato a script di versione. Con il controllo di versione dei simboli l'utente può ricevere dei messaggi di avvertimento all'avvio dei programmi quando le librerie in uso dovessero risultare troppo vecchie. È possibile trovare ulteriori informazioni su questo argomento nella discussione degli script di versione contenuta nel manuale di ld e reperibile presso http://www.gnu.org/manual/ld-2.9.1/html_node/ld_25.html.

5.5. GNU libtool

Se si sta sviluppando un'applicazione che dovrà essere portata su diverse piattaforme, si può prendere in considerazione l'uso di GNU libtool per la compilazione e l'installazione delle librerie. GNU libtool consiste in uno script generico di supporto all'uso di librerie. Libtool nasconde la complessità d'uso di librerie condivise dietro un'interfaccia consistente e portabile. Libtool fornisce un'interfaccia indipendente dalla piattaforma per creare file oggetto, produrre librerie (statiche e condivise), produrre ed eseguire il debug di eseguibili, installare librerie ed eseguibili. È incluso anche libltdl, che fornisce la portabilità ai i programmi con caricamento dinamico. Per maggiori informazioni si consulti la relativa documentazione presso http://www.gnu.org/software/libtool/manual.html

5.6. Rimuovere i simboli per risparmiare spazio

Tutti i simboli inclusi nei file generati risultano utili per il debug, ma incrementano le dimensioni dei file stessi. Se si dovessero avere problemi di spazio, è possibile eliminarne una parte.

L'approccio migliore consiste nel generare i file oggetto nel modo consueto ed eseguire in primo luogo le necessarie procedure di debug e verifica (che risultano fortemente agevolate dalla presenza dei simboli). Successivamente, una volta completata la verifica del programma, si usi strip(1) per rimuovere i simboli. Il comando strip(1) fornisce un buon grado di controllo su quali simboli eliminare; si consulti la documentazione a riguardo per una dettagliata descrizione.

Un differente approccio consiste nell'uso uso delle opzioni "-S" e "-s" del linker GNU ld; "-S" omette dal file prodotto in output le informazioni relative ai simboli di debug (ma non tutti i simboli), mentre "-s" omette tutti i simboli. È possibile attivare queste opzioni attraverso il compilatore gcc con "-Wl,-S" e "-Wl,-s". Se eliminare i simboli rappresenta la procedura normalmente applicata e queste opzioni si rivelano sufficienti allo scopo, questo metodo può essere utilizzato liberamente, ma si tratta di un approccio meno flessibile.

5.7. Eseguibili estremamente piccoli

L'articolo Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux potrebbe rivelarsi utile. Descrive come sia possibile produrre un eseguibile di dimensioni estremamente ridotte. Parlando francamente, la maggior parte dei trucchi descritti non dovrebbero essere utilizzati nelle normali circostanze in cui generalmente si opera, ma risultano piuttosto istruttivi, illustrando l'effettivo funzionamento del formato ELF.

5.8. C++ vs. C

Vale la pena di ricordare che se si sta scrivendo un programma in C++, e da questo si sta chiamando una funzione di libreria implementata in C, il codice C++ dovrà dichiarare tale funzione come extern "C". In caso contrario il linker non sarà in grado di localizzare la funzione C. Internamente, i compilatori C++ effettuano una "decorazione" (mangle) dei nomi delle funzioni C++ (ad esempio per necessità legate al riconoscimento dei tipi), e devono quindi essere informati del fatto che una determinata funzione deve essere chiamata come funzione C (e quindi priva di decorazione del nome).

Se si sta sviluppando una libreria di programma che potrebbe essere chiamata da C o C++ è raccomandabile includere delle dichiarazioni extern "C" nei file di intestazione così da predisporli automaticamente per gli utenti. Queste dichiarazioni possono essere abbinate alle normali direttive #ifndef necessarie ad evitare l'inclusione ripetuta di uno stesso file di intestazione. In questo modo il contenuto tipico di un generico file pippo.h, utilizzabile sia da C che da C++, avrà un aspetto simile a questo:
/* Spiegare qui cosa fa 'pippo' */

#ifndef PIPPO_H
#define PIPPO_H

#ifdef __cplusplus
extern "C" {
#endif

  ... Qui vanno le dichiarazioni delle funzioni esportate ...

#ifdef  __cplusplus
}
#endif
#endif

5.9. Velocizzare l'inizializzazione di codice C++

Gli sviluppatori di KDE hanno notato che l'avvio di applicazioni di grosse dimensioni, scritte in C++ e dotate di interfaccia grafica, può talvolta richiedere un lungo intervallo di tempo, in parte dovuto a numerose riallocazioni. Esistono numerose soluzioni a questo inconveniente. Si veda Making C++ ready for the desktop (by Waldo Bastian) per ulteriori informazioni.

5.10. Linux Standard Base (LSB)

Lo scopo del progetto Linux Standard Base (LSB) consiste nello sviluppare e promuovere un insieme di normative standardizzate che incrementino la compatibilità tra le differenti distribuzioni di Linux e consentano l'esecuzione delle applicazioni su ogni sistema Linux conforme allo standard. La home page del progetto è all'indirizzo http://www.linuxbase.org.

Un interessante articolo che riassume come sviluppare applicazioni conformi allo standard LSB è stato pubblicato da George Kraft IV (Senior software engineer, IBM's Linux Technology Center) nell'ottobre 2002, Developing LSB-certified applications: Five steps to binary-compatible Linux applications. Chiaramente, se si desidera che le applicazioni risultino portabili, si dovrà sviluppare del codice che acceda unicamente al livello di interfaccia standardizzato;. LSB fornisce inoltre agli sviluppatori di applicazioni C/C++ alcuni strumenti per la verifica della conformità allo standard; questi strumenti utilizzano alcune possibilità del linker e speciali librerie al fine di effettuare i test necessari. Ovviamente, per effettuare questo tipo di verifica si dovranno installare questi strumenti, che possono essere reperiti tramite il sito web di LSB. Fatto questo, è sufficiente utilizzare "lsbcc" come compilatore C/C++ (lsbcc crea internamente un ambiente di link che produrrà degli errori nel caso in cui determinate regole di conformità allo standard LSB non fossero soddisfatte):
 $ CC=lsbcc make mia_applicazione
  (oppure)
 $ CC=lsbcc ./configure; make mia_applicazione
Il programma lsbappchk permette di verificare che l'applicazione utilizzi solo funzioni previste dallo standard LSB:
 $ lsbappchk mia_applicazione
È inoltre necessario attenersi alle linee guida di LSB per quanto concerne i pacchetti di installazione (ad esempio utilizzare il formato RPM v3 e nomi dei pacchetti conformi allo standard; LSB prevede inoltre che il software aggiuntivo debba essere normalmente installato sotto opt). Si vedano il suddetto articolo ed il sito internet di LSB per ulteriori informazioni.

5.11. Riunire più librerie in un'unica libreria

Cosa succederebbe se si volesse prima creare delle piccole librerie e poi, in un secondo momento, riunirle in librerie di dimensioni maggiori? In un caso simile, potrebbe risultare utile l'opzione "--whole-archive" di ld, che consente di riunire efficacemente dei file .a e collegarli in un unico file .so.

Ecco un esempio di come utilizzare --whole-archive:
 gcc -shared -Wl,-soname,libmialib.so.$(VER) -o libmialib.so.$(VER).0 \
 $(FILE_OGGETTO) -Wl,--whole-archive $(LIBRERIE_DA_RIUNIRE) \
 -Wl,--no-whole-archive $(NORMALI_LIBRERIE)

Come messo in evidenza dalla documentazione di ld, ci si assicuri di utilizzare alla fine l'opzione --no-whole-archive altrimenti gcc cercherà di riunire nella libreria in output anche le librerie standard. Si ringrazia Kendall Bennett per aver suggerito l'aggiunta di questa ricetta e per averla fornita.