Padrão ELF para arquivos-objeto
- por Sergio Prado
O ELF (Executable and Linkable Format) é um padrão para arquivos executáveis, arquivos-objeto e bibliotecas. Foi usado inicialmente no System V Unix, sistema operacional comercial da AT&T na década de 80. Em 1993 o controle do padrão passou para um Comitê, o TIS (Tool Interface Standards) formado por um grupo de empresas como IBM, Intel e Borland. Em 1995 foi lançada a versão 1.2 do padrão, versão atual usada até hoje em muitos sabores UNIX, incluindo o GNU/Linux.
Mas porque precisamos de um padrão para arquivos-objeto? A resposta é portabilidade. Para que possamos linkar códigos gerados por diferentes compiladores, linkar nossa aplicação com bibliotecas de diferentes fornecedores, executar o código em diferentes sistemas operacionais, etc.
Na definição do padrão, existem 3 tipos principais de arquivos-objeto:
- Arquivos relocáveis: são os arquivos-objeto propriamente ditos, por exemplo os arquivos “.o” gerados pelo gcc quando compilamos um módulo “.c”
- Arquivos executáveis: são arquivos que podem ser executados pelo sistema operacional.
- Arquivos-objeto compartilhados: são as bibliotecas, “.so” ou “.a” em sistemas GNU/Linux.
O padrão ELF tem a estrutura exibida na figura abaixo:
O Header descreve informações sobre o arquivo, como o tipo de arquivo (objeto, biblioteca ou executável), arquitetura (MIPS, x86 68K, etc), versão, endereços de memória, etc. Já o Section/Segment são os trechos de código compilados. O Program Header Table é usado pelo SO para carregar o programa executável em memória, e o Session Header Table possui informações sobre o Session (trechos) do programa.
Nosso programa de teste será o mais simples possível. Nosso objetivo aqui é apresentar ferramentas de análise do formato ELF presente em sistemas GNU/Linux.
1 2 3 4 5 6 7 |
#include "stdio.h" int main(void) { printf("Blog do Sergio Prado\n"); return 0; } |
Compile com o gcc, usando os comandos abaixo:
$ gcc -c main.c $ gcc main.o -o app |
Foram gerados dois arquivos, o arquivo objeto “main.o” e o executável “app”.
O comando “file” irá exibir informações genéricas sobre os arquivos gerados:
$ file main.o main.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped $ file app app: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not stripped |
Com o comando “readelf”, podemos analisar todas as informações do arquivo gerado. Vamos olhar o header o arquivo objeto main.o:
$ readelf -h main.o ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2s complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 220 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 11 Section header string table index: 8 |
Veja que o header é flexivel o suficiente para manter a portabilidade do formato entre diferentes arquiteturas.
Com o comando readelf você pode ler qualquer informação de um arquivo no formato ELF, mas existem alguns outros comandos que podem te ajudar a analisar um arquivo objeto:
O comando “nm” lista os simbolos do arquivo objeto:
$ nm app 08049f20 d _DYNAMIC 08049ff4 d _GLOBAL_OFFSET_TABLE_ 080484bc R _IO_stdin_used w _Jv_RegisterClasses 08049f10 d __CTOR_END__ 08049f0c d __CTOR_LIST__ 08049f18 D __DTOR_END__ 080482b8 T _init 08048330 T _start 0804a014 b completed.7021 0804a00c W data_start 0804a018 b dtor_idx.7023 080483c0 t frame_dummy ........................... 080483e4 T main U puts@@GLIBC_2.0 |
O comando “ldd” lista as dependencias de bibliotecas linkadas dinamicamente:
$ ldd app linux-gate.so.1 => (0x0088d000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00d1f000) /lib/ld-linux.so.2 (0x0099f000) |
Mas porque você precisa saber de tudo isso? Talvez porque você vai se aventurar no desenvolvimento de um compilador. Ou porque você encontrou algum problema ao tentar linkar arquivos-objeto ou bibliotecas geradas por diferentes toolchains. Ou porque você esta tendo alguma dificuldade ao tentar executar uma aplicação compilada em um ambiente diferente do ambiente de execução.
Recentemente trabalhei em um projeto onde uma aplicação compilada em uma máquina x86 não rodava em outra máquina de mesma arquitetura por incompatibilidade da versão da biblioteca GLIBC. Na minha máquina rodava a GLIBC_2.7 e na máquina destino rodava a GLIBC_2.6.
Tinhas duas opções, recompilar a aplicação em um ambiente com a versão da GLIBC correta, que poderia levar um certo tempo. Ou então deixar minha aplicação mais portável para rodar em qualquer versão de GLIBC, e encontrar qual a função que estava requisitando a GLIBC_2.7.
Com o comando “nm” descobri que a função “sscanf” estava requisitando a GLIBC_2.7.
$ nm app | grep 2.7 U sscanf@@GLIBC_2.7 |
Reescrevi o trecho da aplicação que usava a função sscanf, e o problema foi resolvido de forma bem rápida.
Vale a pena dar uma lida no padrão ELF e entender como todo o processo de linkagem de objetos, geração do executável, carga de bibliotecas dinâmicas e execução funciona. O padrão pode ser baixado aqui.
Para quem se interessar, existem ainda outros formatos além do ELF, como o a.out usado nos primórdios de sistemas UNIX, o COFF usado em alguns UNIX e adaptado para o Microsoft Windows e o Mach-O usado no MacOS X.
Um abraço,
Sergio Prado