Criando bibliotecas estáticas no Linux usando C/C++

Um dos aspectos mais importantes das linguagens modernas é o conceito de reutilização de código. Mesmo a linguagem C permite que reutilizemos nosso código usando conceitos como funções e estruturas. A linguagem C++ segue um passo adiante e permite que agrupemos variáveis e funções em classes com o mesmo propósito – a reutilização do código. Ao usar bibliotecas vamos mais fundo nesse conceito – podemos compartilhar código entre programas completamente diferentes.

O que muda quando usamos bibliotecas? A resposta para essa pergunta é: “a fase de ligação (link)” de seu programa. Nessa fase, o linker do GNU faz a ligação de todos os módulos do código em um programa funcional. Quando falamos de bibliotecas no sistema operacional Linux temos dois conceitos básicos: biblioteca estática e dinâmica (frequentemente chamada de “compartilhada”). Nesse artigo iremos ver como utilizar esses dois conceitos usando exemplos em C.

GNU Linker (ld)

O GNU linker é a implementação no Linux do comando Unix ld criado pela associação GNU como parte do pacote GNU Binutils para manipulação do código objeto. O GNU linker é uma ferramenta crucial para a criação de arquivos executáveis e normalmente é chamado automaticamente pelo compilador GNU GCC no final do processo de compilação. Também pode ser chamado manualmente pelo uso do comando ld. Para mais informações, consulte o manual embutido do Linux com o comando man ld.

Carregador dinâmico do Linux

O carregador dinâmico carrega bibliotecas dinâmicas que o programa que está sendo iniciado precisa, e depois disso inicia o programa. Bibliotecas dinâmicas são procuradas nos seguintes locais (por ordem de pesquisa):

  1. No caminho definido pela variável de ambiente LD_LIBRARY_PATH.
  2. No caminho definido no arquivo /etc/ld-so.cache criado a partir do arquivo /etc/ld-so.conf  onde o usuário pode especificar seus próprios caminhos. O ato de criação do arquivo /etc/ld.so.cache é iniciado pela execução do comando ldconfig após cadas modificação do arquivo /etc/ld.so.conf.
  3. Nos diretórios /lib/usr/lib.

Lista de dependências dinâmicas ldd

O comando para listagem de dependências dinâmicas ldd é muito importante durante a programação e depuração de bibliotecas de terceiros. O propósito desse comando é listar todas as bibliotecas necessárias para a execução de um programa. Aqui será apresentada a saída desse comando para nossos arquivos executáveis de teste (não aplicável para arquivos executáveis com ligações estáticas).

Soname

Antes de entrar nos detalhes da programação de bibliotecas no Linux, precisamos falar sobre o que é soname. O entendimento do nome do objeto compartilhado (soname) é um aspecto muito importante da programação de bibliotecas Linux e do sistema operacional em geral. Esse nome está na parte de controle de todas as bibliotecas, e todos os arquivos executáveis contém os sonames das bibliotecas que foram ligadas à ele. O trabalho do carregado dinâmico do Linux é encontrar essas bibliotecas. Como podemos adicionar uma bibliotecas a lista de nosso programa? É simples, fazemos isso fornecendo um argumento -lsoname ao GCC onde soname é o nome da biblioteca que deve ser ligada a esse programa (sem o lib.so ou a versão). Por exemplo, quando o soname de uma bibliotecas ctest for libctest.so.1, iremos especificar -lctest durante a fase de compilação.

Como mencionamos anteriormente, o soname completo contém o nome e a versão da biblioteca que ele representa. Isso é necessário pois é uma prática comum que a versão seja aumentada quando a ABI (Application binary interface, interface binária da aplicação) é quebrada. Isso se deve pelo fato do carregador dinâmico do Linux precisar dessa informação sobre a versão quando for procurar pela biblioteca apropriada para o arquivo executável. Nós precisamos nomear nossa bibliotecas ctest como libctest.so.1.x onde 1 é a versão principal (major) e x a versão secundária (minor). A biblioteca terá o soname libctest.so.1 pois o soname contém a versão principal da biblioteca que representa. Para tornar as coisas mais flexíveis, precisamos fornecer dois symlinks para nossa bibliotecas. O primeiro é o soname completo de nossa biblioteca e é o que o carregador dinâmico procura quando busca pela biblioteca apropriada para seu programa. O segundo terá apenas como nome parte do soname, em nosso exemplo seria libctest.so e é o que o GNU linker procura quando está criando as ligações de seu programa. Qual o ponto desses symlinks? O propósito deles é que pelo ajuste deles podemos decidir qual versão da biblioteca queremos que o carregador dinâmico encontre, assim como qual versão queremos que o GNU linker encontre. Isso pode ser feito pelos usuários, mas normalmente é gerenciado pelo sistema operacional de forma que quando uma biblioteca é atualizada, seus symlinks são ajustados automaticamente.

Código exemplo

Abaixo estão os códigos fontes de nossos programas de teste em linguagem C. Usaremos duas bibliotecas e uma programa que usa essas duas bibliotecas. O código a seguir é bem simples: em nosso programa temos três variáveis xyz. A primeiro variável x recebe um valor da função ctest1() da primeira biblioteca e a variável y recebe um valor da função ctest2() da segunda bibliotecas. A variável z recebe o valor da divisão entre y.

Segue abaixo o código para a primeiro bibliotecas definida no arquivo ctest1.c:

void ctest1(int *i){
   *i=100;
}

E agora temos o código para a segunda biblioteca, definida no arquivo ctest2.c:

void ctest2(int *i){
   *i=5;
}

Poderiamos definir nossa biblioteca em apenas um arquivo de código de fonte, mas qual seria a graça disso? Agora, segue o código fonte para nosso programa, definido no arquivo cprog.c:

#include <stdio.h>
void ctest1(int *);
void ctest2(int *);
 
int main(){
    int x;
    int y;
    int z;
    ctest1(&x);
    ctest2(&y);
    z = (x / y);
    printf("%d / %d = %d\n", x, y, z);
    return 0;
}

Uma coisa a ser mencionada é que é uma prática comum colocar todos os protótipos para a biblioteca ctest em um arquivo de cabeçalho ctest.h e então incluir esse arquivo em todos os programas que fazer ligação com essa biblioteca. Aqui mantemos todos esse arquivos em um único local por questões de simplicidade.

Bibliotecas estáticas

Bibliotecas estáticas são antigas e representam nada mais do que uma coleção empacotada de sequências em código de máquina. Elas são ligadas ao seu programa durante a compilação e seu programa carrega elas aonde ele for. Quando você faz uma ligação estática com uma biblioteca você pode apagar ela após a compilação que seu programa irá funcionar perfeitamente pois a biblioteca estática é fundida ao seu programa durante a compilação. Abaixo vemos como criar uma biblioteca estática libctest.a e em seguida ligar nosso programa ctest a essa biblioteca:

gcc -Wall -c ctest1.c ctest2.c
ar rcs libctest.a ctest1.o ctest2.o
gcc -static cprog.c -L. -lctest -o cprog
./cprog

Agora vamos explicar isso: a primeira linha faz a tradução do código fonte da biblioteca em código objeto. A segunda linha cria um arquivo de biblioteca com o nome libctest.a usando o comando GNU ar. Com a terceira linha nós compilamos nosso programa cprog e ligamos ele estaticamente com a biblioteca libctest.a. Você pode perceber que não criamos nenhum symlink como descrito na seção soname. Isso se deve ao fato de que bibliotecas estáticas não são buscadas pelo sistema e o carregador dinâmico do Linux não é utilizado quando um programa com ligações estáticas é iniciado. A biblioteca estática inteira é simplesmente “colada” ao arquivo executável que é ligado a essa biblioteca. A saída desse comando será 100/5 = 20.

Bibliotecas dinâmicas (compartilhadas)

Bibliotecas dinâmicas deferem das bibliotecas estáticas pelo fato de que ao serem usadas, durante o processo de compilação o GCC adiciona apenas “ganchos” de código e o soname. Esses “ganchos” e os sonames são usados durante a inicialização da aplicação para carregar a biblioteca correta e conectar o código interno ao externo.

Existem diversas vantagens para usar bibliotecas dinâmicas. Em primeiro lugar, o carregador dinâmico do Linux carrega dinamicamente a biblioteca apenas uma vez, e todos os programas que usarão a mesma cópia dessa biblioteca. Imagine quantos programas são ligados à biblioteca padrão do C e quanta memória seria necessária para armazenar uma cópia dessa biblioteca para todos os programas que fizessem uso dela. Por isso o sistema operacional Linux moderno faz uso quase que exclusivamente de bibliotecas dinâmicas e não bibliotecas estáticas. O mesmo pode ser aplicado ao espaço em disco ocupado.

Abaixo segue um exemplo de criação de uma biblioteca dinâmica, ligando o programa a essa biblioteca e testando se tudo está funcionando como deveria. O código desses exemplos é o mesmo do exemplo anterior.

gcc -Wall -fPIC -c ctest1.c ctest2.c
gcc -shared -Wl,-soname,libctest.so.1 -o libctest.so.1.0 ctest1.o ctest2.o
ln -sf libctest.so.1.0 libctest.so
ln -sf libctest.so.1.0 libctest.so.1
gcc -Wall -L. cprog.c -lctest -o cprog
export LD_LIBRARY_PATH=.
./cprog

Na primeira linha do código criamos um código objeto, da mesma forma que fizemos quando criamos uma biblioteca estática. A segunda linha cria uma biblioteca dinâmica armazenada no arquivo libctest.so.1.0 com o soname libctest.so.1. Depois de criar a biblioteca dinâmica, precisamos criar symlinks que são usados para direcionar o GNU linker e o carregadosrdinâmico do Linux para a versão apropriada de nossa biblioteca. Agora que temos a biblioteca, podemos compilar nosso programa, ligando ele à biblioteca criada anteriormente. Fazemos isso com a quinta linha do código acima. No final precisamos configurar LLD_LIBRARY_PATH para o diretório atual pois o carregados dinâmico do Linux usa essa variável para localizar a biblioteca. Podemos também adicionar a biblioteca aos outros caminhos que o GNU linker e o carregador dinâmico usam , o que evitaria termos que ajustar a variável LD_LIBRARY_PATHNo final, executamos nosso programa de teste para verificar se tudo está em ordem.

Conclusão

Para finalizar, iremos ver a maneira mais interessante de reutilizar código usando bibliotecas no sistema operacional Linux – usando a interface de programação de aplicações POSIX (“Portable Operating System Interface for Unix”). Usando as funções POSIX dlopen()dlsym()dlclose()dlerror() você pode carregar e encerrar suas bibliotecas estáticas durante a execução do programa. Essas funções apresentam a interface para o carregador dinâmico do Linux visto no início do artigo. Essas chamadas de sistema são normalmente usadas para implementar coisas como plugins para sua aplicação de forma que você possa carregar funcionalidades fornecidas por eles apenas quando for necessário. A seguir temos um exemplo simples de carregamento dinâmico de bibliotecas no programa cprog visto anteriormente.

Para implementar essas funções POSIX precisamos modificar o código fonte de nosso programa cprog. As bibliotecas ctest1ctest2 permanecem inalteradas.

Precisamos levar em consideração que o C++ faz o infame  name mangling de forma que além desse novo código para cprog.c precisamos instruir o GCC para deixar intacto os nomes das funções da biblioteca. Por isso definimos o arquivo de cabeçalho ctest.h com o seguinte código:

#ifndef CTEST_H
#define CTEST_H

#ifdef __cplusplus
extern "C" {
#endif

void (*ctest1)(int *);
void (*ctest2)(int *);

#ifdef __cplusplus
}
#endif

#endif

Ao incluir o arquivo de cabeçalho ctest.h em nosso código ctest.c, os nomes das funções ctest1()ctest2() não serão alterados e ligação a ser feita durante a execução será bem sucessida. Abaixo segue o código para nosso nova cprog.c:

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include "ctest.h"
 
int main(){
    void *handle;
    char *error;
    int x, y, z;
 
    handle = dlopen ("libctest.so.1", RTLD_LAZY);
    if (!handle) {
        fputs (dlerror(), stderr);
        exit(1);
    }
 
    ctest1 = dlsym(handle, "ctest1");
    if (( error = dlerror() ) != NULL)  {
        fputs(error, stderr);
        exit(1);
    }
 
    ctest2 = dlsym(handle, "ctest2");
    if (( error = dlerror() ) != NULL)  {
        fputs(error, stderr);
        exit(1);
    }
 
    ctest1(&x);
    ctest2(&y);
    z = (x / y);
    printf("%d / %d = %d\n", x, y, z);
    dlclose(handle);
    return 0;
}

Agora vamos explicar esse código: as primeira duas linhas incluem bibliotecas padrão do C e não possuem nenhuma característica especial. A terceira linha inclui dlfcn.h com as macros necessárias como RTLD_LAZY (man dlopenman dlfcn para mais detalhes) e a quarta linha inclui nosso arquivo de cabeçalho ctest.h para evitar que o C++ de alterar os nomes das funções. Depois de obtermos uma ligação para nossa biblioteca libctest.so.1 usando dlopen() e armazenar os ponteiros para as funções ctest1()ctest2() declaradas no arquivo de cabeçalho ctest.h. Chamar dlsym() irá gerar um erro se o linker não encontrar as funções ctest1() ctest2() (se as definições dessas funções não existirem na biblioteca libctest.so.1) ou se deixarmos que o C++ gerencie os nomes das funções. Nesse ponto, podemos usar as nossas funções para encontrar a solução para nosso problema matemático. Quando tiver terminado de usar as funções, devemos chamar dlclose() informando como parâmetro a ligação para a nossa biblioteca. Para mais informações sobre essa chamada consulte o manual embutido do Linux usando o comando man.

A seguir temos o código para compilar nossa biblioteca ctest  e nosso programa cprog:

gcc -Wall -fPIC -c ctest1.c ctest2.c
gcc -shared -Wl,-soname,libctest.so.1 -o libctest.so.1.0 ctest1.o ctest2.o
ln -sf libctest.so.1.0 libctest.so
ln -sf libctest.so.1.0 libctest.so.1
gcc -Wall -o cprog cprog.c -ldl
export LD_LIBRARY_PATH=.
./cprog

Se você comparar esse código de compilação com o anterior, verá que a única coisa que mudou foi a linha cinco, onde criamos a ligação do programa com a biblioteca. Agora não precisamos do parâmetro -lctest pois não iremos executar nenhuma ligação com a biblioteca ctest durant a execução. Ao invés disso, iremos executar a ligação apenas quando precisarmos utilizar a função durante a execução do programa.

traduzido de techytalk.info