Um arquivo-fonte C pode compilar corretamente e, ainda assim, falhar na construção do executável quando a implementação de outra unidade está ausente. Para explicar o problema, precisamos distinguir código-fonte, declarações, unidades de tradução, arquivos-objeto, resolução de símbolos, ligação e execução. Essas etapas recebem entradas distintas e produzem classes diferentes de erros. O compilador não fornece automaticamente o corpo de toda função cuja declaração conhece. O GCC documenta a execução separada dessas fases [1][2].
Arquivos e unidades de tradução#
Uma unidade de tradução é o código entregue ao compilador C depois do pré-processamento. O arquivo-fonte e o conteúdo de seus cabeçalhos formam normalmente uma unidade. Outro arquivo-fonte é compilado como unidade diferente. As unidades podem compartilhar declarações compatíveis e contribuir com definições para o mesmo programa.
Declaração informa nome e tipo de função. Definição fornece seu corpo. Uma unidade que chama uma função pode ser compilada usando declaração correta mesmo que a implementação ainda não esteja compilada, pois o arquivo-objeto preserva referência externa para o linker. É a base de programas modulares, mas exige contrato explícito entre produtor e consumidor.
Cabeçalho é texto-fonte, não biblioteca binária já compilada. Colocar definição externa de função comum num cabeçalho incluído por vários arquivos pode produzir definições conflitantes. O exemplo mantém declaração em cabeçalho protegido por guarda e corpo da função em exatamente um arquivo de implementação.
Pré-processamento e tradução#
O pré-processador trata diretivas de inclusão e compilação condicional antes da compilação convencional. A inclusão de cabeçalho entre aspas incorpora seus tokens à unidade de tradução; guardas evitam inclusões repetidas na mesma unidade. Uma guarda não opera em tempo de execução e não impede que outros arquivos incluam o mesmo cabeçalho.
Macros são substituições de tokens e podem repetir a avaliação de expressões usadas como argumentos. Não equivalem necessariamente a funções com tipos. Prefira função real quando for importante verificar tipos e avaliar cada argumento somente uma vez.
Num compilador compatível com GCC, a flag -E termina após o pré-processamento, portanto a saída ainda é texto C. A flag -S interrompe após a compilação propriamente dita, produzindo normalmente Assembly específico da arquitetura. A opção -c compila e monta conforme necessário, mas não executa a ligação final. A saída Assembly depende de compilador, otimização e arquitetura [1].
Arquivos-objeto e símbolos#
Um arquivo-objeto costuma conter instruções, dados, símbolos e registros de relocação. Uma chamada pode referenciar símbolo definido em outro objeto. O arquivo não precisa conter o ponto completo de entrada nem os endereços finais de todas as funções usadas. O linker resolve referências compatíveis e organiza seções no programa produzido [2][3].
O nome externo double_small aparece como declaração na unidade chamadora. Seu corpo aparece na unidade de implementação. Nenhum dos dois objetos isoladamente constitui o programa completo. Durante a ligação, a referência não resolvida do chamador é associada à definição da implementação.
A ABI estabelece detalhes binários, como convenção de chamada, uso de registradores, layout de dados e regras de símbolos. Combinar arquivos compilados para alvos incompatíveis não se torna correto porque seus nomes terminam com a mesma extensão. Tampouco se deve supor que Windows, Unix e sistemas embarcados compartilhem o mesmo formato executável.
Exemplo: interface, implementação e consumidor#
O contrato aceita inteiro entre -10000 e 10000 e ponteiro de saída para inteiro gravável. Em caso de sucesso, grava o dobro da entrada e devolve 1. Argumento fora do intervalo ou ponteiro nulo produz falha 0 sem alterar a saída anterior do chamador. A magnitude do resultado é no máximo 20000, valor que cabe no intervalo mínimo garantido de int em C.
// file: math_ops.h
#ifndef MATH_OPS_H
#define MATH_OPS_H
/* On success, store twice the value and return 1.
Reject inputs outside [-10000, 10000] or null output. */
int double_small(int value, int *out);
#endif
// file: math_ops.c
#include "math_ops.h"
#include <stddef.h>
int double_small(int value, int *out) {
if (out == NULL || value < -10000 || value > 10000)
return 0;
*out = value * 2;
return 1;
}
// file: main.c
#include "math_ops.h"
#include <stdio.h>
int main(void) {
int result = -1;
if (!double_small(21, &result))
return 1;
printf("result=%d\n", result);
return 0;
}
Compile e ligue num shell semelhante ao POSIX com toolchain C11 compatível com GCC:
cc -std=c11 -Wall -Wextra -Werror -pedantic -c math_ops.c -o math_ops.o
cc -std=c11 -Wall -Wextra -Werror -pedantic -c main.c -o main.o
cc main.o math_ops.o -o demo
./demo
# result=42
O contrato de ponteiro é importante: ponteiro inválido não nulo continua perigoso, mesmo após verificação contra null. O sucesso da função pressupõe armazenamento válido fornecido por quem a chamou.
Por que definições ausentes falham na ligação#
Se math_ops.o for omitido, a compilação de main.c ainda funciona porque a declaração existe. Tentar ligar somente o objeto chamador deve falhar, pois não há definição da função. O diagnóstico depende do linker. Da mesma forma, definições externas incompatíveis e duplicadas podem gerar erros de símbolos repetidos.
Ligar objetos não é concatenar arquivos. A resolução de símbolos, relocações, organização de seções e bibliotecas alteram a saída final. Biblioteca estática costuma arquivar objetos; biblioteca compartilhada pode usar mecanismo que inclui linker dinâmico em execução. As regras exatas dependem da plataforma [2][3].
Declaração válida não demonstra que uma implementação respeita a semântica desejada. O compilador pode verificar tipos sem provar limites numéricos nem correção da aplicação. Por isso testes comportamentais continuam necessários mesmo depois de uma ligação bem-sucedida.
Ligação, carregamento e início do processo#
A ligação produz artefato executável, mas não inicia processo. O sistema operacional ainda precisa reconhecer o formato e preparar recursos de execução. Sistemas hospedados normalmente possuem código de inicialização de runtime que finalmente chama a função main de C. O ponto inicial de baixo nível não precisa literalmente ser main.
Arquitetura de CPU incompatível, biblioteca dinâmica ausente, falta de permissão ou runtime incompatível podem impedir a execução de binário válido. São problemas posteriores à compilação. O capítulo seguinte descreve a interface do processo para argumentos, ambiente, streams e código de saída.
Recompilação, verificação e custos#
O teste independente deve compilar cada unidade, ligar os dois objetos e verificar a impressão de result=42 e término bem-sucedido. Também deve omitir deliberadamente o objeto de implementação e exigir falha na ligação. Esses dois resultados diferenciam compilação correta de resolução correta de símbolos.
Ao modificar a implementação sem alterar cabeçalho, pode bastar recompilar a unidade afetada e refazer a ligação. Modificar cabeçalho incluído por vários arquivos pode exigir recompilar todos os dependentes. Sistemas de build automatizam essas dependências, sem alterar o conceito de unidade de tradução.
Não existe limite universal O(S) para toda compilação otimizada em função apenas do tamanho S: expansão de macros, parsing, transformações e estratégias de otimização influenciam o trabalho. A primeira análise útil é quantas unidades precisam ser recompiladas e ligadas após cada alteração.
Exercícios e verificação#
- Identifique a fase que processa inclusão e a que resolve o símbolo double_small.
- Compare as saídas de pré-processamento (-E), Assembly (-S) e objeto (-c) para um mesmo arquivo [1].
- Tente ligar somente o objeto chamador e explique o erro de símbolo ausente.
- Altere a implementação para divergir do próprio cabeçalho e observe o erro de compilação.
- Acrescente segunda definição externa e explique por que a ligação fica inválida.
- Diferencie ligação bem-sucedida e execução em outra máquina.
Continue com ponteiros em C, programação modular e interfaces de processo.