Static Archives & Shared Objects

Se você é que nem eu e veio do mundo do Windows, e desenvolve em C ou C++, está acostumado com as libraries (estáticas, na forma de arquivos com extensão .LIB e dinâmicas, na forma de arquivos com extensão .DLL ou .EXE – sim! .EXE! Eles também conter funções compartilhadas! É o caso de USER32.EXE, GDI32.EXE e KERNEL32.EXE, no diretório %windir% e %windir%\system32).

No mundo Windows (um mundo tenebroso, se você quer saber…) se você quer usar símbolos definidos em uma DLL precisa importá-los para uma library estática usando o utilitário implib.exe. Assim, para fazer um early binding (linkagem antecipada, numa tradução livre) precisa criar um arquivo .LIB antes. É esse arquivo que será linkado aos seus objetos para construir o arquivo executável final. Para fazer um late binding (linkagem por demanda, também numa tradução livre), precisa usar funções como LoadLibrary(), GetProcAddress() e FreeLibrary().

Mas, e no Linux?! Saiba que temos os mesmos recursos… só que de uma forma um pouco diferente…

Os arquivos .LIB, no Linux, são chamados de archives e têm extensão .a”. Na verdade o arquivo segue o formato “lib<nome>.a“, onde é o nome de sua biblioteca estática. Já o equivalente das DLLs, no Linux, são chamados de shared objects e seguem quase o mesmo padrão: “lib.so.“, ou algo assim:

$ ls -l /usr/lib/libSDL.*
/usr/lib/libSDL.a
/usr/lib/libSDL.la
/usr/lib/libSDL.so -> libSDL-1.2.so.0.11.3

Omiti as permissões, o dono do arquivo, o grupo e o tamanho para efeitos estéticos. Esse arquivo com extensão “.la” não é importante no momento (depois eu explico ele). Note apenas que existem duas versões da libSDL. Uma versão estática (libSDL.a) e uma versão shared object (libSDL-1.2.so.0.11.3). Para que o nosso compilador gcc possa identificar corretamente a biblioteca dinâmica o link simbólico libSDL.so foi criado pelo instalador da biblioteca e aponta exatamente para o shared object.

Quanto você usa a opção de compilação -lSDL, o compilador procurará por um arquivo (ou link) com o nome libSDL.so e, se não encontrar, procurará pelo arquivo libSDL.a.

Não confie muito no padrão de nomeação de shared objects (exceto no link simbólico). Alguém pode criar um shared object com o nome “libFOO-3.2-debian-2.so.2-alpha3“, por exemplo… O que interessa é o que o link simbólico aponte para o arquivo correto.

Antes de continuar, a função do arquivo “.la” é acumular informações que serão usadas pelo script configure, criado via autoconf. Assim, os scripts do configure podem colher informações sobre dependências das bibliotecas sem ter que analisar os arquivos .so e .a diretamente. Esses arquivos são gerados a partir do utilitário libtool – que está fora do escopo da discussão deste artigo.

Criando um static archive

Um static archive conterá os seus objetos (extensão .o) que serão linkados de maneira estática para formar o executável final. Dizendo de outra forma, os objetos dentro do archive são copiados para a imagem binária final do seu executável.

A maneira mais fácil de criar esses archives é gerando os arquivos de extensão .o com o nosso compilador gcc e depois empacotá-los com o utilitário ar:

$ gcc -o myfuncs.o myfuncs.c
$ ar rcs libmyfuncs.a myfuncs.o
$ sudo mv libmyfuncs.a /usr/lib/
$ sudo chown root:root /usr/lib/libmyfuncs.a

O archive pode conter tantos objetos quanto você desejar.

Criando um shared object

Um shared object também conterá os seus objetos, só que terá a extensão .so, seguida da versão da “biblioteca” (explico o porquê mais adiante). Esse shared object será carregado junto com o seu executável final, no entanto, a imagem binária de seu executável final conterá apenas código para carregar o shared object e referências aos objetos nele contidos.

Repare que não falei nada sobre criar um archive. A forma mais fácil de gerar um shared object é compilar seus códigos e gerar os arquivos-objeto (extensão .o) e depois linká-los num shared object:

$ gcc -o myfuncs.o -fpic myfuncs.c
$ gcc -shared -o libmyfuncs.so.1.0 myfuncs.o -Wl,-soname,libmyfuncs.so.1.0
$ sudo mv libmyfuncs.so.1.0 /usr/lib/
$ sudo chown root:root /usr/lib/libmyfuncs.so.1.0
$ sudo ln -s /usr/lib/libmyfuncs.so.1.0 /usr/lib/libmyfuncs.so
$ sudo chown root:root /usr/lib/libmyfuncs.so

Opa! Que diabos de opção -Wl,-soname,libmyfuncs.so.1.0 é essa ai em cima? Acontece que shared objects podem ter 3 nomes diferentes.

Shared Object Name (soname): É o nome passado para o linker. Esse nome fará parte da imagem binária do shared object e sempre segue o padrão “lib<name>.so.<versão>.<release>“;

Real Name: É o nome do arquivo binário. Como eu falei lá em cima, ele pode ser meio maluco, mas sempre começa com lib e tem um .so em algum lugar;

Linker Name: É o nome do link simbólico. Sempre terá o formato “lib<name>.so“.

A opção -Wl passa opções para o linker (ld).

É uma boa idéia adicionar a opção -fpic na compilação do arquivo-objeto (extensão .o) que comporá o shared object. Assim como no Windows, os shared objects podem ser carregados em espaços de endereçamento diferente do espaço atribuído para o executável que o usará. Tornar o shared object independente de posição (PIC = Position Independent Code) é uma boa tática, neste caso. Aliás, a documentação do gcc diz que sem a opção -fpic poderemos ter resultados não previstos, na hora de executar nossos programas linkados com os nossos shared objects.

Por que shared objects tem um número de versão no nome do arquivo?

O motivo é uma maneira elegante de não causar o famoso “inferno das DLLs” (DLL Hell), comuns no ambiente Windows. Se você usar uma biblioteca, por exemplo, a libc (versão 6) e, de repente, alguém substitui o shared object por uma outra versão com o mesmo nome de arquivo… Opa! problemas à vista.

Colocando a versão no nome do arquivo e usando links simbólicos garante-se duas coisas: O link simbólico pode apontar para qualquer versão da biblioteca desejada e ambas as versões ainda estarão disponíveis em /lib, /usr/lib ou /usr/local/lib…

Usando static archives ao invés de shared objects

Vimos que a opção -l usa shared objects por default. Se ambos as libraries existirem e você quer usar o static archive, então deve adicionar a opção -static na hora que for linkar os objetos e bibliotecas para criar a imagem binária final do executável.

E quanto ao late binding?

Imagine o seguinte cenário: Dentro de meu shared object tenho uma função chamada get_application_config() que será chamada apenas uma única vez na minha aplicação para obter dados de configuração. Pra que diabos eu preciso carregar o shared object junto com a aplicação e mantê-lo carregado?

Com o late binding (ou dinamic linking) eu posso abrir o shared object, pegar o ponteiro para a função get_application_config(), chamá-la e depois fechar o shared object. Simples assim:

#include <dlfcn.h>

typedef void (*pfn)(struct configs *);

struct configs app_config;
void *handle;
pfn get_app_config;
char *error;

handle = dlopen("libmyfuncs.so", RTLD_LAZY);
if (handle != NULL)
{
  /* Limpa possíveis erros */
  dlerror();

  /* Pega o ponteiro para a função */
  get_app_config = (pfn)dlsym(handle, "get_application_config");

  /* Verifica se houve erros */
  error = dlerror();
  if (error)
  {
    printf("%s\n", error);
    exit(1);
  }

  /* Finalmente chama a função */
  get_app_config(&app_config);

  /* E fecha o shared object */
  dlclose(handle);
}

E isso tem a vantagem de que as funções dlopen, dlsym, dlerror e dlclose serem POSIX.

A desvantagem é que para usar essas funções a imagem binária de seu executável tem que conter as informações com os nomes e ponteiros para os símbolos. Este é o mesmo problema que você enfrenta com as DLLs, no Windows (afinal, de onde você acha que a Microsoft copiou o esquema de late binding?!)… Por lá é necessária a exportação de símbolos, marcados em arquivos com extensão .DEF. Isso torna o executável e os shared objects maiores sem necessidade e, ainda por cima, mais fáceis de “quebrar”, por um hacker experiente que tenha acesso aos arquivos.

Diferente do Windows a função dlsym também procura por símbolos na imagem binária do executável, depois procura no shared object.

Uma última coisa sobre late binding: Use as opções -rdynamic e -ldl (essa última para linkar o shared object “libdl.so“, que contém as funções mostradas.

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s