Aula de Laboratório

Criando bibliotecas estáticas e dinâmicas em C/C++

Programação para Software Básico

08/2014

Objetivos:
Obs: vocês deve realizar esta aula no terminal do Linux

1 Ligação estática x dinâmica

Como vimos na aula passada, as bibliotecas são incluídas na etapa de ligação (linking), feita normalmente de forma automática pelo compilador.
Há duas formas de ligação:
Esta aula deve ser realizada no Linux, de preferência, no terminal.

1.1 Ligação estática

A ligação estática é o que ocorre normalmente ao compilarmos um programa com vários módulos (exemplo: várias classes, separadas em arquivos diferentes). Cada código-fonte é compilado e produz um arquivo com código objeto equivalente (.c ou .cpp para .o, no caso do Linux).
É possível também ligar bibliotecas de forma estática. Uma biblioteca nada mais é do que uma coleção de módulos, contendo funcionalidades relacionadas (ex: biblioteca de funções matemáticas, biblioteca gráfica, etc). No Linux, bibliotecas estáticas são arquivos com o sufixo .a (de archive) e são criadas com o aplicativo ar (detalhes mais adiante).
As bibliotecas estáticas normalmente ficam nos diretórios /usr/lib ou /usr/local/lib. Por exemplo, verifique o conteúdo de /usr/lib:
ls /usr/lib/*.a

Se a biblioteca já estiver no diretório /usr/lib, você pode especificá-la na linha de comando de forma mais simples, com a opção -l (letra “ele”, não "um"):
gcc -o programa programa.c -lz

A opção -l faz com que o ligador procure a biblioteca com o prefixo lib..., ou seja, no caso acima ele irá procurar em /usr/lib/libz.a
É possível também especificar um ou mais diretórios adicionais de busca, com a opção -L:
gcc -o programa programa.c -L/usr/local/lib -lgraphics

A linha acima irá procurar a libgraphics.a também em /usr/local/lib.

1.2 Criando uma biblioteca estática

Para criar uma biblioteca estática, utiliza-se a seguinte linha de comando:
ar rcs minhalib.a modulo1.o modulo2.o modulo3.o ...

Onde minhalib.a é o nome do arquivo resultante e modulo*.o são os diversos módulos a ser inseridos na biblioteca.
Uma vez criada a biblioteca, basta especificá-la na linha de comando do compilador:
gcc -o programa programa.c minhalib.a

Considerando que minhalib.a e programa.cpp estejam no mesmo diretório, esse comando irá utilizar a biblioteca durante a fase de ligação. Observação: mesmo assim, para compilar o programa é necessário ter acesso aos headers (.h) utilizados pela biblioteca.

Exercício 1:

Faça o download do arquivo glshapes.zip - é uma implementação completa de uma biblioteca de funções para desenhar figuras. O projeto contém um Makefile, então para compilar o código digite make no terminal. Resista à tentação de usar o Code::Blocks!
A partir dessa implementação, crie uma biblioteca estática da seguinte forma:
  1. Compile uma vez toda a aplicação com o comando make:
    make
    
    
  2. Como já sabemos, isso gera os arquivos *.o (códigos-objetos, não executáveis) de todos os módulos. Como queremos criar uma biblioteca para as funções de desenho, usamos agora o comando ar:
    ar rcs libshapes.a shapes.o
    
    
  3. Para visualizar o conteúdo de uma biblioteca, use:
    ar t libshapes.a
    
    
Observe que cada módulo incluído aparece nessa listagem. Também lembre-se de um detalhe importante: só serão incluídos no código-objeto final os módulos realmente utilizados pelo programa. Isso significa que quanto mais modular for a biblioteca, melhor (dentro dos limites do bom senso, é claro). Por exemplo, se houver módulos muito grandes, o uso de uma única função ou classe do módulo faz com que o código do módulo inteiro seja incorporado. Isso tende a criar executáveis maiores (acontece muito no Windows).
Agora que você já sabe fazer como os homens (e mulheres) de verdade faziam antigamente, pode também usar o Code::Blocks, que automatiza boa parte do processo. Para tanto, basta criar um projeto do tipo static library.

Exercício 2:

  1. Altere o Makefile, de forma que o projeto passe a usar a biblioteca ao invés de compilar tudo novamente. Nesta linha:
    LDFLAGS = -lGL -lGLU -lglut -lm
    
    
    Troque para:
    LDFLAGS = -lGL -lGLU -lglut -lm -L.
    
    
    Relembrando: a opção -L informa ao ligador um caminho adicional para procurar bibliotecas, no caso, o diretório corrente (“.”).
    Também é possível automatizar a criação da biblioteca dentro do próprio Makefile. Para isso, acrescente o código-fonte da biblioteca, como demonstrado a seguir:
  2. Primeiro, remova o módulo de desenho da linha abaixo:
    SOURCES = main.c
    
    
  3. Agora, acrescente a linha abaixo, que define os módulos a serem incorporados na biblioteca:
    SOURCES2 = shapes.c
    
    
  4. Duplique a linha que começa com OBJECTS... e troque o nome para OBJECTS2:
    OBJECTS2 = $(OBJECTS2:.c=.o)
    
    
  5. Na regra que define o executável (glshapes), troque para o seguinte:
    shapes: $(OBJECTS) libshapes.a
    	gcc $(CFLAGS) $(OBJECTS) -o shapes -l shapes $(LDFLAGS)
    
    
    Isso faz com que o executável também dependa da existência da biblioteca.
  6. E finalmente, acrescente as seguintes linhas, que definem a regra para criação da biblioteca.
    libshapes.a: ${OBJECTS2}
    	ar rcs libshapes.a ${OBJECTS2}
    
    
Dessa forma, ao digitar make no terminal, a biblioteca será automaticamente compilada. Obs: cuide para que antes de "ar rcs..." tenha um caractere TAB: esse formato é obrigatório!
Se quiser, faça o mesmo no Code::Blocks: basta incluir esses parâmetros no linker options (i.e. em caminhos para bibliotecas e bibliotecas adicionais).

1.3 Ligação dinâmica

Atualmente, a forma mais comum do uso de bibliotecas é através de ligação dinâmica. Isso significa que o código executável (programa) não irá mais conter todo os módulos utilizados de cada biblioteca, mas apenas uma referência para eles. No momento da execução, o sistema operacional identifica essas referências e carrega as bibliotecas necessárias, automaticamente. Isso torna o código menor, e permite que a implementação interna das bibliotecas sofra alterações, sem que seja necessário recompilar todos os programas que dependem dela. Isso também tem um efeito colateral benéfico: o sistema consegue manter uma cópia de cada biblioteca utilizada na memória, e compartilha ela com todos os programas que a utilizam.
Você já usou ligação dinâmica: as bibliotecas GL, GLU e glut, utilizadas no exemplo anterior, são todas dinâmicas. Mas como identificar isso ?
Existe um comando que permite identificar as ligações dinâmicas de um executável, denominado ldd. Por exemplo, para visualizar as ligações dinâmicas do programa, experimente:
ldd shapes

Você verá uma saída semelhante à seguinte:
    linux-vdso.so.1 =>  (0x00007fff75c5e000)
	libGL.so.1 => /usr/lib/x86_64-linux-gnu/mesa/libGL.so.1 (0x00007fce04ae1000)
	libglut.so.3 => /usr/lib/x86_64-linux-gnu/libglut.so.3 (0x00007fce0489b000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fce044db000)
	libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fce041df000)
	libglapi.so.0 => /usr/lib/x86_64-linux-gnu/libglapi.so.0 (0x00007fce03fb9000)
    ...

Ou seja, o programa depende das bibliotecas libGL.so.1, libGLU.so.1, ... O sufixo .so significa shared object, ou seja, objeto compartilhado. Repare que o comando também informa a localização das bibliotecas, o que é uma informação muito útil.
No Mac OSX, o sufixo é .dylib (dynamic library), e deve-se usar a ferramenta otool para obter a mesma informação:
otool -L shapes

Resultando em:
shapes:
	@executable_path/../Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
	@executable_path/../Frameworks/GLUT.framework/Versions/A/GLUT (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa (compatibility version 1.0.0, current version 15.0.0)
	/usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.9.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 125.2.10)
	/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 227.0.0)
	/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 550.42.0)
	/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices (compatibility version 1.0.0, current version 38.0.0)
	/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 751.53.0)
	/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit (compatibility version 45.0.0, current version 1038.35.0)
...

O equivalente no Windows são os arquivos .dll (dynamic link library).

1.4 Criando uma biblioteca dinâmica

Para criar uma biblioteca dinâmica, utiliza-se o próprio gcc. Acrescente as seguintes linhas ao Makefile:
libshapes2.so: ${OBJECTS2}
	gcc -shared -Wl,-soname,libshapes2.so -o libshapes2.so ${OBJECTS2}

A opção -soname... é passada diretamente ao linker (via -Wl), e especifica o nome do arquivo, que também é informado na saída do compilador (-o).
Uma vez criada, a biblioteca dinâmica é utilizada da mesma forma que a estática, ou seja, através da opção -l do compilador.
A última etapa é adicionar uma regra para criar um executável que use a biblioteca dinâmica:
shapes-shared: $(OBJECTS) libshapes2.so
	$(CC) $(OBJECTS) -o shapes-shared -lshapes2 $(LDFLAGS) 

Observe que dessa forma, o código executável resultante (shapes-shared) fica menor que o gerado no exercício 2.

Exercício 3:

  1. Execute o comando:
    ldd shapes-shared
    
    
    Se você notou algo estranho na listagem (uma biblioteca faltando), é porque normalmente o diretório corrente não é pesquisado por default para as bibliotecas dinâmicas. Porém, existe uma solução: a variável de ambiente LD_LIBRARY_PATH permite especificar caminhos adicionais para o ligador dinâmico. Por exemplo, digite:
    export LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH
    
    
  2. Isso faz com que essa variável receba o diretório corrente (resultado do comando `pwd`), mais os caminhos que já possuía ($LD_LIBRARY_PATH). Agora, execute novamente o comando ldd. O que mudou ?
  3. Você pode também visualizar o conteúdo da biblioteca dinâmica em si, através do comando nm:
    nm libshapes2.so
    
    
    Esse comando mostra uma lista das funções contidas na biblioteca, entre outras informações (incluindo dependências dinâmicas dela mesma).
Assim como nas bibliotecas estáticas, é possível criar bibliotecas dinâmicas com o Code::Blocks: basta escolher o tipo de projeto shared library.

Para maiores detalhes:

http://www.ibm.com/developerworks/linux/library/l-dynamic-libraries/