1. Introdução
Esse capítulo visa estabelecer conceitos importantes que envolvem a linguagem C, compiladores e programação no geral.
1.1. Características da Linguagem C
C é uma linguagem de programação compilada, isto é, ao escrevermos o código fonte na própria linguagem, no caso em C, um programa chamado compilador reescreve esse código para a linguagem de máquina. Sendo assim, o compilador tem como entrada um arquivo com código fonte da linguagem e que gera como saída um arquivo objeto, com código objeto, que é ligado a outros arquivos objeto para gerar um arquivo executável. O arquivo executável é um arquivo que pode ser executado no computador alvo. Na próxima seção, serão dados mais detalhes sobre o processo de compilação de um arquivo em C. Abaixo seguem algumas características importantes da linguagem C:
- Estruturada: A programação estruturada (sucedida pela programação orientada a objeto) é um paradigma formado por três componentes:
- Sequência: Uma tarefa é executada logo após a outra;
- Decisão: A tarefa é executada logo após um teste lógico;
- Iteração: A partir de um teste lógico, um trecho de código pode ser repetido finitas vezes.
- Imperativa: Descreve ações/instruções que o programa deverá executar. Ou seja, linguagens imperativas são programadas com uma sequência de comandos ordenada pelo programador;
- Procedural: Permite a construção de procedimentos que podem ser compartimentados e reutilizados, tornando partes do código mais independentes entre si;
- Padronizada: Garante que um mesmo código gere sempre o mesmo resultado, seja ele compilado e executado ou interpretado;
- Fortemente Tipada: Em C, os tipos das variáveis e funções precisam ser bem definidos durante toda a execução do programa. Com ponteiros do tipo
void
, é possível contornar essa restrição, mas isso não é aconselhável.
Abaixo segue uma tabela com os tipos de dados básicos da linguagem, onde a palavra-chave é usada para definir as variáveis e o formato indica a forma de capturar (por meio de funções como scanf
) ou de imprimir (por exemplo, com printf
):
Tabela de dados básicos de C
PALAVRA-CHAVE | TIPO | BYTES | INTERVALO | FORMATO |
---|---|---|---|---|
char / signed char |
Caracter | 1 | -128 a 127 | %c |
unsigned char |
Caracter sem sinal | 1 | 0 a 255 | %c |
short / short int / signed short / signed short int |
Inteiro curto com sinal | 2 | -32768 a 32767 | %hi ou %hd |
unsigned short / unsigned short int |
Inteiro curto sem sinal | 2 | 0 a 65535 | %hu |
signed int / signed |
Inteiro com sinal | 2 | -32768 a 32767 | %i ou %d |
unsigned / unsigned int |
Inteiro sem sinal | 2 | 0 a 65535 | %u |
long / long int / signed long / signed long int |
Inteiro com sinal | 4 | -2147483648 a 2147483647 | %li ou %ld |
unsigned long / unsigned long int |
Inteiro sem sinal | 4 | 0 a 4294967295 | %lu |
long long / signed long long / long long int / signed long long int |
Inteiro muito lingo com sinal | 8 | −2^+63 a 2^+63 −1 | %lli ou %lld |
unsigned long long / unsigned long long int |
Inteiro muito lingo sem sinal | 8 | 0 a 2^+64 −1 | %llu |
float |
Ponto flutuante simples | 4 | 3.4 X 10^-38 a 3.4 X 10^+38 | %f ou %F |
double |
Ponto flutuante em precisao dupla | 8 | 1.7 X 10^-308 a 1.7 X 10^+308 | %lf ou %lF |
long double |
Ponto flutuante em precisão estendida | 16 | 3.4 X 10^-4932 a 3.4 X 10^+4932 | %Lf ou %LF |
Vale notar que esses tipos podem variar de máquina para máquina, sendo interessante imprimir os limites dos tipos presentes no cabeçalho limits.h. As padronizações (como ANSI e ISO) da linguagem também podem afetar certos tipos e, consequentemente, o funcionamento do código. Como C é muito popular, diversos compiladores foram construídos com características distintas. As próximas seções introduzirão o processo de compilação.
1.2. O Que É Um Compilador?
O compilador é um programa de computador responsável por reescrever o código fonte em código de máquina que poderá ser executado. Assim, ele recebe como entrada um arquivo com o código fonte e gera um arquivo executável.
Em outras palavras, o compilador traduz o código fonte de uma linguagem compreensível para os seres humanos para outra que o computador possa entender. Atualmente, o compilador possui muitas funcionalidades além da simples tradução: ele pode agrupar instruções de máquina em uma única linha de código, otimizar o código, gerar arquivos intermediários, tratar erros na programação e oferecer ferramentas de depuração. Os primeiros compiladores eram focados na tradução do código fonte e na junção das bibliotecas necessárias para a execução do código objeto, num processo chamado de ligação. Esses compiladores iniciais foram escritos em Assembly e, com o tempo, surgiram diversas ferramentas para a construção de compiladores, facilitando a criação de novas linguagens.
Com a evolução das linguagens e a necessidade de novas funcionalidades, os compiladores passaram a ter características variadas e métodos de funcionamento diferentes. A seguir, os principais tipos de compiladores:
- Compilador Ahead-of-time: Compila o código fonte antes da execução do programa, gerando um arquivo objeto com instruções de máquina nativas.
- Compilador Just-in-time: Compila o código durante a execução do programa. Na primeira execução, cada linha do código fonte é traduzida para instruções de máquina (ou para uma linguagem intermediária) e executada imediatamente; em execuções subsequentes, o código já compilado permite uma execução mais rápida.
- Compilador Cruzado: Gera um arquivo executável a partir do código fonte que pode ser executado em outras máquinas, útil para sistemas embutidos ou ambientes com múltiplas arquiteturas.
- Compilador Source-to-source: Tem como saída um código fonte de alto nível, em vez de instruções de máquina. Isso possibilita a criação de extensões sintáticas que são reescritas para o código alvo. Um exemplo é o TypeScript.
Existe também o interpretador, que traduz e executa o código fonte ou bytecode diretamente, sem gerar um arquivo objeto. Esse processo é geralmente mais lento, pois cada linha precisa ser interpretada em tempo real. Por fim, programas que convertem código Assembly para linguagem de máquina e vice-versa são chamados de montador (assembler) e desmontador (disassembler), respectivamente. A descompilação, que converte código de máquina para um código de alto nível, também é utilizada, especialmente em contextos de segurança.
1.3. GNU Compiler Collection
O GNU Compiler Collection (GCC) é uma coleção de compiladores Ahead-of-time do projeto GNU, criada em 1987. Ele oferece compiladores para linguagens como ADA, C++, Fortran, Java, Objective-C e Pascal, e possui compatibilidade com arquiteturas como ARM, x86 e AMD64 (x86-64). O GCC é o compilador padrão na maioria dos sistemas Linux, o principal compilador para o MAC OS e também pode ser utilizado no Windows por meio de ferramentas como MSYS2 e MinGW. Grande parte desses compiladores é escrita em C, inclusive o próprio compilador C, num processo chamado de bootstrapping. O foco aqui será o GNU C Compiler (GCC).
- Pré-processamento: Realizado pelo pré-processador, que trata todas as linhas que começam com
#
. As diretivas principais nessa fase são#include
(para inclusão de arquivos de cabeçalho com definições e declarações) e#define
(para definição de macros e constantes). Outras diretivas, como#if...#else...#endif
e#error
, também são utilizadas para controle condicional e exibição de mensagens de erro. - Compilação: Nesta fase, o código é traduzido para assembly em vários níveis:
- Análise léxica: Verifica se os símbolos (variáveis, funções e palavras reservadas) estão corretos, removendo espaços e comentários. Erros como variáveis não definidas ou operadores inexistentes são identificados aqui.
- Análise sintática: Garante que as expressões seguem a gramática formal do C, verificando a organização correta dos símbolos.
- Análise semântica: Valida o sentido lógico das expressões, checando a consistência dos tipos, regras de visibilidade e contexto.
- Otimização de alto nível: Otimiza o código, eliminando redundâncias e trechos desnecessários.
- Montagem: Converte cada linha de assembly em código de máquina.
- Ligação: Na fase de ligação, as bibliotecas e todo o código necessário para a execução do programa são carregados e incorporados ao código objeto.
Vale ressaltar que muitas otimizações ficam desabilitadas por padrão e precisam ser ativadas por meio de flags. Após esta introdução aos conceitos básicos da linguagem C e do GCC, os próximos capítulos discutirão as formas de utilização deste compilador e as ferramentas que ele oferece.