Bibliotecas

As bibliotecas são ficheiros com código compilado passível de ser reutilizado na produção de programas. Se o código em biblioteca for incorporado no ficheiro objeto executável, designa-se por ligação estática. Se o código em biblioteca for carregado em memória, apenas quando for invocado pela aplicação, designa-se por ligação dinâmica.

Ligação estática

Ligação dinâmica

Vantagens

  • Execução mais rápida.

  • Os ficheiros objeto executáveis menores.

  • Em caso de atualização da biblioteca não é necessário gerar novamente aplicação.

  • Permite que o mesmo código seja usado por várias aplicações em simultâneo.

Desvantagens

  • Os ficheiros objeto executáveis são mais longos.

  • Em caso de atualização é necessário gerar novamente a aplicação.

  • O tempo de procura da biblioteca e a resolução de símbolos afeta o tempo de execução do programa.

Tomemos como exemplo os ficheiros stack.c e fifo.c que implementam estruturas para armazenamento de conjuntos de números.

As soluções de programação utilizadas são escolhidas com o fim de exemplificar situações técnicas e não por utilidade do código, eficiência ou elegância.

Listagem 113 stack.h
#ifndef STACK_H
#define STACK_H

void stack_push(int value);
int stack_pop();

extern int * stack_pointer;

#endif

Listagem 114 stack.c
#define BUFFER_SIZE	10

static int buffer[BUFFER_SIZE];

int *stack_pointer = buffer + BUFFER_SIZE;

void stack_push(int value)
{
	*--stack_pointer = value;
}

int stack_pop()
{
	return *stack_pointer++;
}
Listagem 115 fifo .h
#ifndef FIFO_H
#define FIFO_H

void fifo_insert(int value);
int fifo_remove();
extern const int fifo_size;
extern int fifo_count;

#endif
Listagem 116 fifo.c
#include "fifo.h"

#define SIZE_ARRAY(a) (sizeof(a) / sizeof(a[0]))

static int buffer[10];
static int *fifo_put = buffer, *fifo_get = buffer;
int fifo_count;
const int fifo_size = SIZE_ARRAY(buffer);

void fifo_insert(int value)
{
	fifo_count++;
	*fifo_put++ = value;
	if (fifo_put == buffer + SIZE_ARRAY(buffer))
		fifo_put = buffer;
}

int fifo_remove()
{
	fifo_count--;
	int value = *fifo_get++;
	if (fifo_get == buffer + SIZE_ARRAY(buffer))
		fifo_get = buffer;
	return value;
}

Considere-se o seguinte programa de aplicação:

Listagem 117 main.c
#include <stdio.h>
#include "stack.h"
#include "fifo.h"

int a = 3;
int b = 8;

int main()
{
	stack_push(a);
	b = stack_pop();
	fifo_insert(33);
	fifo_remove();
}

Exercício

Gerar o executável sem criar biblioteca.

Convenção de nome

As bibliotecas são armazenadas em ficheiros com nomes da forma: libxxx.so ou libxxx.a. Os ficheiros terminados em .a contém bibliotecas de ligação estática e os terminadas em .so contém ficheiros de ligação dinâmica. A sequência xxx é diferenciadora e identifica a biblioteca.

Biblioteca de ligação estática

Criação

Uma biblioteca estática pode ser vista como um ficheiro de arquivo contendo vários objetos relocalizáveis.

$ ar cr libdemo.a stack.o fifo.o

Verificação

$ nm libdemo.a

Quando um símbolo definido numa biblioteca é referido, todo o código incluído no ficheiro a que pertence esse símbolo é incluído no ficheiro objeto executável. Por exemplo, se no programa de exemplo se remover a referência a stack_pop, o respetivo código continua a ser incluído no executável final porque a referência a stack_push se mantém.

Para evitar o crescimento dos ficheiros objeto com código não utilizado, é comum separarem-se as função da biblioteca por vários ficheiro fonte.

Utilização

Usar uma biblioteca estática é parecido com a ligação de vários ficheiros objeto relocalizáveis.

$ gcc main.o libdemo.a -o main

O código em biblioteca é copiado para o ficheiro objeto executável. Em caso de alteração do código da biblioteca, por funcionalidade ou correção de erros, é necessário gerar novamente o ficheiro objeto executável.

Exercício

Suprimir a referência à função stack_top. Verificar a manutenção do código dessa função como conteúdo do executável.

Suprimir as referências às funções fifo_insert e fifo_remove. Verificar a supressão do código dessas funções do conteúdo do executável.

Biblioteca de ligação dinâmica

Criação

Os módulos que constituem a biblioteca são compilados em separado com a opção -fpic. Esta opção dá indicação ao compilador para gerar código que executa independentemente do endereço de memória onde for carregado.

$ gcc -c -fpic stack.c fifo.c

O ficheiro a que chamamos biblioteca é gerado com o seguinte comando.

$ gcc -shared -o libdemo.so stack.o fifo.o

O ficheiro produzido (libdemo.so) é designado por shared object. Contém o código e a informação necessária para ser carregado em memória e integrar aplicações.

Utilização

Geração do programa executável:

$ gcc main.c libdemo.so -o main

ou

$ gcc main.c -ldemo -L. -o main

Na primeira forma o ficheiro da biblioteca é indicado explicitamnete.

Na segunda forma é o linker (ld) que vai selecionar o ficheiro a utilizar. O linker dá preferência à ligação dinâmica (libdemo.so). Aplica ligação estática (usa libdemo.a) se a opção -static for indicada ou se a versão dinâmica (libdemo.so) não existir.

Na ligação dinâmica o linker incorpora no ficheiro objeto executável meios de referência aos objetos da biblioteca, mas não o código da biblioteca.

A chamada das funções da biblioteca são realizadas indiretamente via PLT.

O código da biblioteca é carregado em memória no momento da execução do programa se ainda não tiver sido carregado, na sequência do arranque de outro processo.

Dependências

No ficheiro objeto executável ficam registados os nomes de versão (soname) das bibliotecas necessária para a sua execução. O utilitário ldd permite verificar estas dependências e se estão acessíveis para carregamento.

$ ldd main
     linux-vdso.so.1 (0x00007ffed3342000)
     libdemo.so => not found
     libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007de11b400000)
     /lib64/ld-linux-x86-64.so.2 (0x00007de11b788000)

Quando o programa for a executar o loader procura as bibliotecas no sistema de ficheiros por esta ordem:

  1. No path incluído no executável definido pela opção -rpath; Incluir um path de pesquisa no próprio executável:

    $ gcc main.o -L. -ldemo -Wl,-rpath,/home/ezequiel/lib -o main
    
  2. Nas diretorias indicadas na variável de ambiente LD_LIBRARY_PATH;

    $ export LD_LIBRARY_PATH=/usr/ezequiel/lib
    
  3. Na cache – ficheiro /etc/ld.so.cache. A cache é atualizada pelo utilitário ldconfig, que introduz os caminhos definidos nos ficheiros /etc/ld.so.conf.d/*.conf, as diretorias /lib e /usr/lib ou definidos na linha de comando.

    • Visualizar /etc/ld.so.conf.d/*.conf

      $ cat  /etc/ld.so.conf.d/*.conf
      
      /usr/lib/x86_64-linux-gnu/libfakeroot
      # Multiarch support
      /usr/local/lib/i386-linux-gnu
      /lib/i386-linux-gnu
      /usr/lib/i386-linux-gnu
      /usr/local/lib/i686-linux-gnu
      /lib/i686-linux-gnu
      /usr/lib/i686-linux-gnu
      # libc default configuration
      /usr/local/lib
      # Multiarch support
      /usr/local/lib/x86_64-linux-gnu
      /lib/x86_64-linux-gnu
      /usr/lib/x86_64-linux-gnu
      
    • Acrescentar um caminho de pesquisa na cache

      $ sudo ldconfig /home/ezequiel/lib
      
    • Verificar se o caminho está na cache

      $ ldconfig -p | grep libdemo.so
      
    • Eliminar caminhos da cache. Remover o caminho do ficheiro /etc/ld.so.conf.d/*.conf em seguida executar o comando abaixo. Os caminhos acrescentados pela linha de comando também serão eliminados.

      $ sudo ldconfig
      

SONAME

Tabela 8 Title

Nome de ligação (linker name)

libXXX.so

Corresponde geralmente a um link. É o nome que é usado para referenciar uma dada biblioteca na altura da geração do programa, sem definir a versão.

Nome de versão (soname)

libXXX.so.N

O N indica a versão de especificação da biblioteca. Muda de versão sempre que a interface da biblioteca se torna incompatível com as anteriores. Corresponde normalmente a um link.

Nome real

libXXX.so.N.M.R

Este é o ficheiro real onde se encontra o conteúdo da biblioteca.

N – versão principal (major number); interface incompatível com outras versões

M – versão secundária (minor number); interface diferente mas compatível com a versão principal.

R – variante de implementação; modificações internas como correções de erros ou melhoramentos.

O objetivo deste esquema de nomes é facilitar as atualizações e lidar com várias versões.

Para incorporar a informação de versão (SONAME) na biblioteca deve ser usada a opção -soname.

$ gcc -shared -Wl,-soname,libdemo.so.1 -o libdemo.so.1.0.0 stack.o fifo.o

Verificar o SONAME da biblioteca.

$ readelf -d libdemo.so.1.0.0

Dynamic section at offset 0x2e58 contains 18 entries:
  Tag        Type                         Name/Value
 0x000000000000000e (SONAME)             Library soname: [libdemo.so.1]
 0x000000000000000c (INIT)               0x1000
 0x000000000000000d (FINI)               0x11ac
 ...

Um executável que seja produzido com ligação a uma biblioteca com SONAME fica dependente desse SONAME.

$ gcc main.c -ldemo -L. -o main

Verificar as dependências:

$ readelf -d main

Dynamic section at offset 0x2d98 contains 28 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libdemo.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x1000
 0x000000000000000d (FINI)               0x1274
 ...

pkg-config

O utilitário pkg-config permite obter informação sobre bibliotecas instaladas. Essa informação inclui as opções de compilação e de ligação necessárias para utilizar uma dada biblioteca.

Exemplo

$ pkg-config glib-2.0 -libs

-lglib-2.0

Indica que para ligar com a biblioteca Glib deve ser incluida a opção -lglib-2 na linha de comando do ld.

$ pkg-config glib-2.0 -cflags

-I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include

Indica que para encontrar o ficheiro de inclusão da biblioteca Glib devem ser usada as opções de compilação -I/usr/include/glib-2.0 e -I/usr/lib/x86_64-linux-gnu/glib-2.0/include.

A informação relativa a cada biblioteca é guardada em ficheiros com a extensão .pc que se encontram num dos seguintes locais:

$ find /usr/ -name "pkgconfig"

/usr/share/pkgconfig
/usr/local/lib/pkgconfig
/usr/lib/pkgconfig
/usr/lib/x86_64-linux-gnu/pkgconfig
$ ls -l /usr/lib/x86_64-linux-gnu/pkgconfig/

total 432
-rw-r--r-- 1 root root  283 mar 31  2024 alsa.pc
-rw-r--r-- 1 root root  648 ago  9 03:33 dbus-1.pc
-rw-r--r-- 1 root root  289 set 10 11:17 expat.pc
-rw-r--r-- 1 root root  427 mar 31  2024 geany.pc
-rw-r--r-- 1 root root  761 nov 13 17:42 gio-2.0.pc
-rw-r--r-- 1 root root  211 nov 13 17:42 gio-unix-2.0.pc
-rw-r--r-- 1 root root  522 nov 13 17:42 glib-2.0.pc

Nas linhas 18 e 20 pode-se ver a informação destinada a responder às perguntas -libs e cflag.

 1$ cat /usr/lib/x86_64-linux-gnu/pkgconfig/glib-2.0.pc
 2
 3prefix=/usr
 4bindir=${prefix}/bin
 5datadir=${prefix}/share
 6includedir=${prefix}/include
 7libdir=${prefix}/lib/x86_64-linux-gnu
 8
 9glib_genmarshal=${bindir}/glib-genmarshal
10gobject_query=${bindir}/gobject-query
11glib_mkenums=${bindir}/glib-mkenums
12glib_valgrind_suppressions=${datadir}/glib-2.0/valgrind/glib.supp
13
14Name: GLib
15Description: C Utility Library
16Version: 2.80.0
17Requires.private: libpcre2-8 >= 10.32
18Libs: -L${libdir} -lglib-2.0
19Libs.private: -lm -pthread
20Cflags: -I${includedir}/glib-2.0 -I${libdir}/glib-2.0/include

Carregamento em execução

Uma biblioteca de ligação dinâmica pode ser carregada em qualquer altura da execução da uma aplicação – não apenas no momento do arranque da aplicação.

API para carregamento de bibliotecas em tempo de execução:

#include <dfcn.h>
void *dlopen(const char *filename, int flags);
void *dlsym(void *handle, char *symbol);
int dlclose(void *handle);
const char *dlerror(void);

dlopen – carrega a biblioteca indicada por filename e invoca o linker dinâmico para resolver referências a símbolos externos, definidos noutras bibliotecas ou no programa executável. Para que os símbolos definidos numa biblioteca fiquem disponíveis, essa biblioteca deve ter sido carregada com a flag RTLD_GLOBAL. Para que os símbolos definidos no programa executável fiquem disponíveis, esse executável deve ter sido criado com a opção -rdynamic.

dlsym – recebe a referência para uma biblioteca previamente carregada com dlopen e o nome de um símbolo e retorna o endereço de memória desse símbolo.

dlclose – descarrega uma biblioteca previamente carregada com dlopen.

dlerror – retorna uma string com a descrição do erro mais recente.

Exemplo

Fonte do programa executável:

Listagem 118 main.c
#include <dlfcn.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
	if (argc != 2) {
		fprintf(stderr, "usage: %s libXXX.so\n", argv[0]);
		return EXIT_FAILURE;
	}
	void* handle = dlopen(argv[1], RTLD_LAZY);
	if (handle == NULL) {
		fprintf(stderr, "%s\n", dlerror());
		return EXIT_FAILURE;
	}
	int (*f)(void) = dlsym(handle, "lib_func");
	if (f == NULL) {
		fprintf(stderr, "Could not find lib_func: %s\n", dlerror());
		return EXIT_FAILURE;
	}
	printf("Calling lib_func\n");
	int ret = f();
	printf("lib_func returned %d\n", ret);

	if (dlclose(handle) != 0) {
		fprintf(stderr, "Could not close plugin: %s\n", dlerror());
		return EXIT_FAILURE;
	}
}

Para gerar o executável:

$ gcc main.c -ldl -o main

Fonte do código em biblioteca:

Listagem 119 lib_func.c
#include <stdio.h>

int lib_func() {
	printf("Executing lib_func\n");
	return 77;
}

Geração da biblioteca de ligação dinâmica:

$ gcc -fpic lib_func.c -shared -o lib_func.so

1ª experiência

$ ./main
usage: ./main libXXX.so

Não foi indicado o ficheiro da biblioteca.

2ª experiência

$ ./main lib_func.so

lib_func.so: cannot open shared object file: No such file or directory

O ficheiro da biblioteca não foi encontrado porque a diretoria corrente não se encontra nos caminhos de procura de bibliotecas.

3ª experência

$ export LD_LIBRAY_PATH=.
$ ./main lib_func.so
Calling lib_func
Executing lib_func
lib_func returned 77

Exemplo

Habilitar um programa executável a incorporar novas funcionalidades, através da ligação dinâmica em execução (plugin).

O exemplo baseia-se no programa de simulação de fila de espera tratado em Programa de simulação de fila de espera na versão Lista duplamente ligada.

Os comandos do programa são agrupados numa lista ligada em que a informação relativa a cada comando é composta pelo ponteiro para a função que executa o comando, a descrição textual do comando e a letra identificadora.

Listagem 120 wqueue.c
struct command {
	void (*f) (char *);
	char c;
	char *desc;
	struct command *next;
};

static struct command *commands = NULL;

void command_insert(char c, char *desc, void (*f)(char *))
{
	struct command *new_command = malloc(sizeof (struct command));
	new_command->c = c;
	new_command->desc = strdup(desc);
	new_command->f = f;
	new_command->next = commands;
	commands = new_command;
}

void command_execute(char c, char *param)
{
	for (struct command *p = commands; p != NULL; p = p->next)
		if (p->c == c) {
			p->f(param);
			return;
		}
}

void command_list(char *unused)
{
	for (struct command *p = commands; p != NULL; p = p->next)
		printf("%c%s\n", p->c, p->desc);
}

A incorporação de novo comando é realizada pela função commando_new. Esta função recebe o nome do ficheiro contendo o shared object e extrai através da função dlsym os ponteiros para os elementos do novo comando.

A partir do momento em que estes elementos são inseridos na lista de comandos, o novo comando passa a estar disponível, a par dos comandos originais.

Listagem 121 wqueue.c
static void *handle;

static void command_new(char *lib)
{
	void *handle = dlopen(lib, RTLD_LAZY);
	if (handle == NULL) {
		fprintf(stderr, "%s\n", dlerror());
		return;
	}
	void (*f)(char *) = dlsym(handle, "command_function");
	if (f == NULL) {
		fprintf(stderr, "%s\n", dlerror());
		return;
	}
	char *c = dlsym(handle, "command_letter");
	if (c == NULL) {
		fprintf(stderr, "%s\n", dlerror());
		return;
	}
	char **desc = dlsym(handle, "command_description");
	if (desc == NULL) {
		fprintf(stderr, "%s\n", dlerror());
		return;
	}
	command_insert(*c, *desc, f);
}

O executável é gerado sob o controlo do seguinte makefile:

Listagem 122 makefile
 1CFLAGS = -g -Wall -std=c2x
 2
 3wqueue: wqueue.o
 4	gcc wqueue.o -ldl -o wqueue -rdynamic
 5
 6wqueue.o: wqueue.c
 7	gcc -c $(CFLAGS) wqueue.c
 8
 9clean:
10	rm -rf *.o wqueue

A criação de um novo comando consiste em gerar um shared object contendo os elementos do novo comando: a função que executa o comando, a descrição textual e a letra identificadora.

Em seguida exemplifica-se a criação de um comando para vazar a fila de espera.

Listagem 123 toempty.c
#include <stdlib.h>
#include "user.h"

extern struct user queue;

void command_function(char *name) {
	struct user *next;
	for (struct user *p = queue.next; p != &queue; p = next) {
		next = p->next;
		free(p->name);
		free(p);
	}
	queue.next = queue.prev = &queue;
}

char command_letter = 'k';

char *command_description = "\t - Eliminar todos os utentes da fila de espera";

Os elementos do comando são obrigatoriamente designados pelos símbolos command_function, command_letter e command_description, pois serão estes os símbolos utilizados por parte do programa executável, ao invocar a função dlsym (linhas 40, 45 e 50).

O shared object é gerado sob o controlo do seguinte makefile:

Listagem 124 makelib
1libtoempty.so: toempty.o
2	gcc -shared toempty.o -o libtoempty.so
3
4toempty.o: toempty.c
5	gcc -c -fpic toempty.c
6
7clean:
8	rm -rf *.o libtoempty.so

1ª experiência

Remover a opção -rdynamic da linha de geração do executável wqueue.

$ ./wqueue

>c ./libtoempty.so
./libtoempty.so: undefined symbol: queue
>

Este erro deve-se ao programa executável, por omissão, não exportar símbolos para ligação dinâmica com os shared objects. A referência indefinida ao símbolo externo queue, existente em libtoempty.so, não poder ser resolvida pelo linker dinâmico.

$ readelf --dyn-syms libtoempty.so

Symbol table '.dynsym' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     ...
     4: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND queue
     ...

O símbolo queue, definido no programa executável, só fica disponível para ligação dinâmica, se o executável for gerado com a opção -rdynamic (linha 4 do makefile).

Nessa altura todos os símbolos globais são incluídos na tabela de símbolos de ligação dinâmica.

$ readelf --dyn-syms wqueue

Symbol table '.dynsym' contains 40 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     ...
    27: 0000000000004010    24 OBJECT  GLOBAL DEFAULT   25 queue
     ...

2ª experiência

Depois de o programa executável ser produzido com a opção -rsymbol, a ligação dinâmica do símbolo queue sucede bem e o código acabado de carregar pode ser utilizado.

$ ./wqueue

>c libtoempty.so
>h
k        - Eliminar todos os utentes da fila de espera
n <nome> - Chegada de novo utente
a        - Atender utente
l        - Listar fila de espera
d <nome> - Desistencia de utente
c        - Incorporar novo comando
h        - Listar os comandos existentes
s        - Sair
>

Algoritmo do linker

Os símbolos definidos em bibliotecas são ignorados se na altura do processamento da biblioteca ainda não existirem referências para eles.

Por essa razão as bibliotecas são colocadas, na linha de comando, depois dos ficheiros objeto.

Exercício

Inverter a ordem de colocação da biblioteca em relação ao objeto main.o.

$ gcc libdemo.a main.o -o main

Referências

Program Library HOWTO