Bibliotecas Compartilhadas (Shared Libraries) em Linux
Organizar o código de uma aplicação em múltiplos arquivos simplifica o desenvolvimento e a manutenção. Vamos considerar um exemplo prático.
Primeiro, criamos um arquivo print.asm com uma função que imprime texto no console:
global print
; Função print: imprime um texto no console
; Parâmetros:
; RDI - número de caracteres
; RSI - endereço da string
section .text
print:
mov rdx, rdi ; rdx = número de caracteres
mov rdi, 1 ; stdout
mov rax, 1 ; syscall write
syscall
retEsta função recebe o endereço de uma string em RSI e seu tamanho em RDI.
Em seguida, criamos o arquivo principal da aplicação, app.asm, que utilizará essa função:
global _start
extern print
section .data
message: db "Hello World!", 10
count equ $ - message
section .text
_start:
mov rdi, count
lea rsi, [message]
call print
mov rax, 60
syscallAqui, a função print é declarada como externa e chamada para imprimir a message.
Linkagem Estática (Static Linking)
O método mais direto para combinar esses arquivos é a linkagem estática. Compilamos cada arquivo .asm em um arquivo-objeto (.o) e, em seguida, o linker (ld) une todos em um único executável.
$ nasm -felf64 print.asm -o print.o $ nasm -felf64 app.asm -o app.o $ ld app.o print.o -o app $ ./app Hello World!
Neste processo, o código da função print é copiado para dentro do arquivo executável app.
Desvantagens da Linkagem Estática:
- Aumento do Tamanho: O código de funções comuns é duplicado em cada programa que as utiliza, aumentando o tamanho de cada executável.
- Manutenção: Se um bug for encontrado na função
print, todos os programas que a utilizam precisam ser recompilados e linkados novamente.
Linkagem Dinâmica e Bibliotecas Compartilhadas
O Linux oferece uma alternativa mais flexível: a linkagem dinâmica (dynamic linking). Com este método, o código das bibliotecas não é incorporado ao aplicativo. Em vez disso, as bibliotecas permanecem como arquivos separados, e o aplicativo apenas contém referências a elas. A união ocorre em tempo de execução, quando o programa é carregado.
Essas bibliotecas externas são chamadas de bibliotecas compartilhadas (shared libraries) ou objetos compartilhados (shared objects), e em sistemas Linux, elas têm a extensão .so. Abordagens semelhantes existem em outros sistemas, como as .dll no Windows e as .dylib no macOS.
Vantagens da Linkagem Dinâmica:
- Flexibilidade: Bibliotecas podem ser atualizadas independentemente dos aplicativos. Uma correção de segurança na biblioteca beneficia todos os programas que a utilizam, sem a necessidade de recompilá-los.
- Economia de Espaço: O código da função existe em um único lugar no sistema de arquivos, economizando espaço em disco.
Criando uma Biblioteca Compartilhada (.so)
Vamos transformar nosso arquivo print.asm em uma biblioteca compartilhada. É necessário fazer uma pequena alteração:
global print:function ; Especifica o tipo do símbolo como 'function'
; Função print: imprime um texto no console
; Parâmetros:
; RDI - número de caracteres
; RSI - endereço da string
section .text
print:
mov rdx, rdi
mov rdi, 1
mov rax, 1
syscall
retA diretiva global print:function informa ao linker que print é uma função, o que é necessário para a vinculação dinâmica.
Agora, seguimos os passos para criar a biblioteca:
Compilar para um arquivo-objeto:
$ nasm -felf64 print.asm -o print.o
Criar a biblioteca compartilhada: Usamos o linker
ldcom a opção-shared.$ ld -shared print.o -o libprint.so
É uma convenção em Linux nomear bibliotecas com o prefixo
lib. Agora temos o arquivolibprint.so, nossa biblioteca compartilhada.
Usando a Biblioteca Compartilhada
Vamos compilar nosso app.asm e linká-lo dinamicamente com a biblioteca que criamos.
Compilar o aplicativo principal:
$ nasm -felf64 app.asm -o app.o
Linkar dinamicamente:
$ ld app.o -lprint -L. -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o app
Vamos entender os novos parâmetros do linker (
ld):-lprint: Pede ao linker para procurar uma biblioteca chamadalibprint.so.-L.: Adiciona o diretório atual (.) à lista de locais onde o linker deve procurar por bibliotecas.--dynamic-linker: Especifica o caminho para o loader dinâmico do sistema. Em sistemas x86-64, geralmente é/lib64/ld-linux-x86-64.so.2.
Ao tentar executar o programa, você provavelmente encontrará um erro:
$ ./app ./app: error while loading shared libraries: libprint.so: cannot open shared object file: No such file or directory
Isso acontece porque, em tempo de execução, o loader dinâmico procura bibliotecas apenas em caminhos padrão do sistema (como /lib e /usr/lib). Nosso arquivo libprint.so não está em nenhum desses locais.
Para resolver isso, podemos usar a variável de ambiente LD_LIBRARY_PATH para adicionar temporariamente o diretório atual ao caminho de busca do loader:
$ export LD_LIBRARY_PATH=. $ ./app Hello World!
Agora o programa funciona! O comando export define a variável apenas para a sessão atual do terminal.
O Papel do Loader Dinâmico
Quando você executa um programa linkado dinamicamente, o sistema operacional primeiro carrega o loader dinâmico (ex: ld-linux-x86-64.so.2) na memória. Este loader é responsável por:
- Ler as dependências do executável.
- Encontrar e carregar as bibliotecas compartilhadas necessárias (
.so) na memória. - Realizar a realocação, ajustando os endereços de memória para que o código do aplicativo possa chamar corretamente as funções na biblioteca.
- Transferir o controle para o ponto de entrada do aplicativo (
_start).
Podemos inspecionar as dependências de um executável com o comando ldd:
$ ldd app
linux-vdso.so.1 (0x00007ffc455f3000)
libprint.so => ./libprint.so (0x00007efc8c478000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3c3a000000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3c3a42b000)A saída mostra que nosso app depende da libprint.so (encontrada no diretório atual) e de outras bibliotecas do sistema.
GOT e PLT
Para fazer a mágica da linkagem dinâmica funcionar, o linker utiliza duas tabelas importantes dentro do executável:
- GOT (Global Offset Table): Uma tabela que armazena os endereços de funções e variáveis externas. Inicialmente, ela não contém os endereços reais.
- PLT (Procedure Linkage Table): Uma tabela de "trampolins". Quando o programa chama uma função externa pela primeira vez (como
print), a chamada vai para uma entrada na PLT. Essa entrada contém um código que instrui o loader dinâmico a encontrar o endereço real da funçãoprint, armazená-lo na GOT e, em seguida, saltar para lá. Nas chamadas subsequentes, a PLT saltará diretamente para o endereço já resolvido na GOT, tornando o processo mais rápido. Esse mecanismo é conhecido como lazy loading (carregamento tardio).
Resumo
- Linkagem estática copia todo o código para o executável, enquanto a linkagem dinâmica mantém as bibliotecas como arquivos separados.
- Bibliotecas compartilhadas em Linux têm a extensão
.soe são criadas com a opção-shareddo linkerld. - Para linkar um aplicativo com uma biblioteca compartilhada, usam-se as opções
-l<nome>e-L<caminho>. - O loader dinâmico é responsável por carregar as bibliotecas
.soem tempo de execução. A variável de ambienteLD_LIBRARY_PATHpode ser usada para especificar caminhos de busca adicionais.