original in en Lorne Bailey
en to pt Bruno Sousa
O Lorne vive em Chicago e trabalha como consultor informático, especializado em obter dados de e para bases de dados Oracle. Desde que se mudou para um ambiente de programação *nix, evitou por completo a 'DLL Hell'. Está, presentemente a trabalhar no mestrado sobre Ciência de Computação.
Consegue-se imaginar a compilar software livre com um compilador
proprietário e fechado? Como é que sabe o que vai no seu executável? Pode
haver qualquer tipo de "back door" ou cavalo de Tróia. O Ken Thompson, numa das
piratarias de todos os tempos, escreveu um compilador que deixava uma "back
door" no programa de 'login' e perpetuava o cavalo de Tróia quando o
compilador se apercebia que estava a compilar a si mesmo. Leia a descrição
dele para todos clássicos de todos os tempos
aqui.
Felizmente, temos o gcc. Sempre que faz um configure; make; make
install o gcc faz um trabalho pesado que não se vê.
Como é que fazemos o gcc trabalhar para nós?
Começaremos por escrever um jogo de cartas, mas só escreveremos o
necessário para demonstrar a funcionalidade do compilador. Visto que
estamos a começar do zero, é preciso compreender o processo de compilação
para saber o que deve ser feiro para se ter um executável e em que ordem.
Daremos uma visto de olhos geral como um programa C é compilado e as opções
que o gcc tem para fazer o que queremos.
Os passos (e os utilitários que os fazem) são Pré-compilação (gcc -E), Compilação (gcc), Assemblagem (as), e Linkagem (ld) -
Ligação.
Primeiro pensamento, devíamos saber como invocar o compilador em primeiro lugar. É realmente simples. Começaremos com o clássico de todos os tempos, o primeiro programa C. (Os de velhos tempos que me perdoem).
#include <stdio.h> int main()
{ printf("Hello World!\n"); }
Guarde este ficheiro como game.c. Pode compilá-lo na linha de
comandos, correndo:
gcc game.cPor omissão, o compilador C cria um executável com o nome de
a.out.
Pode corrê-lo digitando:
a.out Hello WorldCada vez que compilar o programa, o novo
a.out sobreporá o
programa anterior. Não conseguirá dizer que programa criou o
a.out actual.
Podemos resolver este problema dizendo ao gcc o nome que queremos dar com
a opção -o. Chamaremos este programa de game,
contudo podíamos nomeá-lo com qualquer coisa, visto que o C não tem as
restrições que o Java tem, para os nomes.
gcc -o game game.c
game Hello World
Até este ponto, ainda estamos longe de um programa útil. Se pensa que isto é uma coisa má, deve reconsiderar o facto que temos um programa que compila e corre. À medida que adicionarmos funcionalidade, a pouco e pouco, queremos certificar-nos que o mantemos capaz de correr. Parece que todos os programadores iniciantes querem escrever 1,000 linhas de código e depois corrigi-las de uma vez só. Ninguém, mas mesmo ninguém pode fazer isto. Você faz um programa pequeno que correm faz as alterações e torna-o executável novamente. Isto limita os erros que tem de corrigir de uma só vez. E ainda por cima, você sabe exactamente o que fez e que não trabalha, então sabe onde concentrar-se. Isto evita-lhe criar algo que você pensa que trabalha e até compile mas nunca se torna num executável. Lembre-se que só por ter compilado não quer dizer que esteja correcto.
O nosso próximo passo é criar um ficheiro cabeçalho para o nosso jogo. Um ficheiro cabeçalho concentra os tipos de dados e a declaração de funções num só sítio. Isto assegura que as estruturas de dados estão definidas consistentemente, assim qualquer parte do nosso programa vê tudo, exactamente do mesmo modo.
#ifndef DECK_H
#define DECK_H
#define DECKSIZE 52
typedef struct deck_t
{
int card[DECKSIZE];
/* number of cards used */
int dealt;
}deck_t;
#endif /* DECK_H */
Guarde este ficheiro como deck.h. Só o ficheiro
.c é que é compilado, assim temos de alterar o nosso game.c.
Na linha 2 do game.c, escreva #include "deck.h". Na linha 5,
escreva deck_t deck; para ter a certeza que não falhámos
nada, compile-o novamente.
gcc -o game game.c
Se não houver erros, não há problema. Se não compilar resolva-o até compilar.
Como é que o compilador sabe que tipo é o deck_t ? Porque
durante a pré-compilação, ele, na verdade copia o ficheiro "deck.h" para
dentro do ficheiro "game.c".
As directivas do pré-compilador no código fonte começam por um "#". Pode
invocar o pré-compilador através do frontend do gcc com a opção
-E.
gcc -E -o game_precompile.txt game.c wc -l game_precompile.txt 3199 game_precompile.txtPraticamente 3,200 linhas de saída! A maioria delas vem do ficheiro incluído
stdio.h, mas se der uma vista de olhos nele, as suas
declarações também lá estão. Se não der um nome de ficheiro para saída com
a opção -o, ele escreve para a consola. O processo de
pré-compilação dá mais flexibilidade ao código ao atingir três grandes
objectivos.
-E isoladamente, mas
deixará passar a sua saída para o compilador.
Como um passo intermediário, o gcc traduz o seu código em linguagem Assembler. Para fazer isto, ele deve descobrir o que é que tinha intenção de fazer ao passar por todo o seu código. Se cometer um erro de sintaxe ele dir-lhe-à e a compilação falhará. Muitas vezes as pessoas confundem este passo com todo o processo inteiro. Mas ainda há mais trabalho para o gcc fazer.
O as transforma o código Assembler em código objecto. O
código objecto não pode ainda correr no CPU, as está muito perto. A opção
do compilador -c transforma um ficheiro .c num ficheiro
objecto com a extensão .o. Se corrermos:
gcc -c game.ccriamos automaticamente um ficheiro chamado game.o. Aqui caímos num ponto importante. Podemos tomar qualquer ficheiro .c e criar um ficheiro objecto a partir dele. Como vemos abaixo, podemos combinar estes ficheiros objecto em executáveis no passo de Ligação. Continuemos com o nosso exemplo. Visto estarmos a programar um jogo de cartas e definimos um baralho de cartas como um
deck_t, escreveremos um função para baralhar o
baralho. Esta função recebe um ponteiro do tipo deck e correga-o com um
valores aleatórios para as cartas. Mantém rasto das cartas já desenhadas,
com o vector de 'desenho'. Este vector com membros do DECKSIZE evita-nos
duplicar o valor de uma carta.
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include "deck.h"
static time_t seed = 0;
void shuffle(deck_t *pdeck)
{
/* Keeps track of what numbers have been used */
int drawn[DECKSIZE] = {0};
int i;
/* One time initialization of rand */
if(0 == seed)
{
seed = time(NULL);
srand(seed);
}
for(i = 0; i < DECKSIZE; i++)
{
int value = -1;
do
{
value = rand() % DECKSIZE;
}
while(drawn[value] != 0);
/* mark value as used */
drawn[value] = 1;
/* debug statement */
printf("%i\n", value);
pdeck->card[i] = value;
}
pdeck->dealt = 0;
return;
}
Guarde este ficheiro como shuffle.c.
Pusemos uma frase de depuração no código para quando correr, escrever o número
das cartas que gera. Isto não adiciona nenhuma funcionalidade ao programa,
mas agora, é crucial para vermos o que se passa. Visto estarmos somente a
começar o nosso jogo, não temos outro modo senão que termos a certeza que
a nossa função está a fazer o que pretendemos. Com a frase printf, podemos
ver exactamente o que está acontecer e assim quando passarmos para a
próxima fase sabemos que o baralho esta bem baralhado. Depois de estarmos
satisfeitos com o seu funcionamento podemos remover esta linha do nosso
código. Esta técnica de fazer depuração aos programas parece arcaica mas
fá-lo com um mínimo de trabalho irrelevante. Discutiremos mais tardes
depuradores mais sofisticados.
shuffle.c não tem uma função 'main' por
conseguinte não pode ser um executável por si só. Devemos combiná-lo com
outro programa que tenha uma função 'main' e que chame a função shuffle.
Corra o comando
gcc -c shuffle.ce certifique-se que cria um novo ficheiro chamado
shuffle.o.
Edite o ficheiro game.c, e na linha 7, após a declaração da variável
deck_t deck, adicione a linha
shuffle(&deck);Agora, se tentarmos criar um executável do mesmo modo como antes obtemos um erro
gcc -o game game.c /tmp/ccmiHnJX.o: In function `main': /tmp/ccmiHnJX.o(.text+0xf): undefined reference to `shuffle' collect2: ld returned 1 exit statusO compilador teve sucesso porque a nossa sintaxe estava correcta. A fase de ligação falhou porque não dissemos ao compilador onde se encontra a função 'shuffle'. O que é que é a ligação e como é que dizemos ao compilador onde pode encontrar esta função? Ligação
O linker, ld, pega no código objecto previamente criado com
as e transforma-o num executável através do comando
gcc -o game game.o shuffle.oIsto combinará os dois objectos e criará o executável
game.
O linker encontra a função shuffle a partir do objecto
shuffle.o e inclui-o no executável. A verdadeira beleza dos ficheiros
objecto vem do facto se quisermos utilizarmos esta função novamente, só
temos de incluir o ficheiro "deck.h" e ligar ao código do novo executável o
ficheiro objecto shuffle.o.
O aproveitamento do código está sempre a acontecer. Não escrevemos o código
da função printf quando a chamámos em cima como uma declaração
de depuração, O linker encontra a sua definição no ficheiro que incluímos
#include <stdlib.h> e liga-o ao código objecto
armazenada na biblioteca C (/lib/libc.so.6).
Deste modo podemos utilizar a função de alguém que sabemos trabalhar
correctamente e preocuparmo-nos em resolver os nossos problemas. É por este
motivo que os ficheiros de cabeçalho só contêm as definições de dados e de
funções e não o corpo das funções. Normalmente você cria os ficheiros
objecto ou bibliotecas para o linker por no executável. Um problema podia
ocorrer com o nosso código visto que não pusemos nenhum definição no
nosso ficheiro cabeçalho. O que é que podemos fazer para ter a certeza que
tudo corre sem problemas?
A opção -Wall activa todo o tipo de avisos relativamente à
sintaxe da linguagem para nos ajudar a ter a certeza que o nosso código
está correcto e portável tanto quanto possível. Quando utilizamos esta
opção e compilamos o nosso código podemos ver algo como:
game.c:9: warning: implicit declaration of function `shuffle'Isto diz-nos que temos mais algum trabalho a fazer. Precisamos de pôr uma linha no ficheiro de cabeçalho, onde diremos ao compilador tudo sobre a nossa função
shuffle assim poderá fazer as verificações que
precisa de fazer. Parece complicado, mas separa a definição da
implementação e permite-nos utilizar a função em qualquer lado bastando
incluir o nosso novo ficheiro cabeçalho e ligá-lo ao nosso código objecto.
Introduziremos esta linha no ficheiro deck.h.
void shuffle(deck_t *pdeck);Isto evitará a mensagem de aviso.
Uma outra opção comum do compilador é a optimização
-O# (ou seja -O2).
Isto diz ao compilador o nível de optimização que quer. O compilador tem um
saco cheio de truques para tornar o seu código mais rápido. Para um pequeno
programa como o nosso não notaremos qualquer diferença, mas para programas
grandes pode melhorar um pouco a rapidez. Você vê esta opção em todo o lado
por isso devia saber o que significa.
Como todos sabemos, só porque o nosso código compilou não quer dizer que vai trabalhar do modo que queremos. Pode verificar que são utilizados todos os números de uma só vez correndo
game | sort - n | lesse vendo que não falta nada. O que é que devemos fazer se houver um problema? Como é que olhamos por debaixo da madeira e encontramos o erro? Pode verificar o seu código com um depurador. A maioria das distribuições fornecem um depurador clássico, o gdb. Se a linha de comandos o atrapalha como a mim, o KDE oferece um front-end bastante simpático, com o KDbg. Existem outros front-ends, e são muito semelhantes. Para começar a depurar, escolha File->Executable e depois encontre o seu programa, o
jogo. Quando prime F5 ou escolhe Execution->Run a partir do
menu, você devia ver uma saída numa janela à parte. O que é que acontece ?
Não vemos nada na janela. Não se preocupe, o KDbg não está a falhar. O
problema vem do facto de não termos posto nenhuma informação de depuração
no executável, assim i KDbg não nos pode dizer o que se passa internamente.
A flag do compilador -g põe a informação necessária dentro dos
ficheiros objecto. Deve compilar os ficheiros objecto (extensão .o) com
esta flag, assim o comando passa a ser:
gcc -g -c shuffle.c game.c gcc -g -o game game.o shuffle.oIsto põe marcas no executável que permitem ao gdb e ao KDbg saber o que está a fazer. Fazer depuração é uma tarefa importante e vale a pena o tempo gasto em aprender como o fazer correctamente. O modo como os depuradores ajudam os programadores é a habilidade de definir "pontos de paragem" no código fonte. Tente agora definir um através de um clique com o botão esquerdo na linha com a chamada à função
shuffle. Deve
aparecer um pequeno circulo vermelho na linha seguinte. Agora, prime F5 e o
programa pára a execução nessa linha. Prima F8 para entrar dentro da
função shuffle. Bem, estamos agora a olhar para o código a partir do
ficheiro shuffle.c! Podemos controlar a execução passo a passo
e ver o que se passa. Se deixar o apontador sobre uma variável local, verá
o valor que guarda. Apreciável. Muito melhor que aquelas frases com
printf's, não é?
Esta artigo apresentou uma visita clara à compilação e depuração aos programas
C. Discutimos os passos que o compilador faz e as opções que devemos passar
ao gcc para ele fazer esses passos. Introduzimos a ligação a bibliotecas
partilhadas e terminámos com uma introdução aos depuradores. Requer bastante
trabalho para saber realmente, o que está a fazer, mas espero que isto o
ajude a começar com o pé direito. Pode encontrar mais informação nas
páginas man e info acerca do gcc,
as e do ld.
Escrever código por si mesmo ensina-lhe o mais importante. Para praticar, podia utilizar estas bases simples do programa de jogo de cartas utilizado neste artigo e escrever um jogo de blackjack. Aproveite o tempo para aprender a utilizar um depurador. É muito mais fácil começar com um GUI como o KDbg. Se adicionar funcionalidade aos poucos, saberá as coisas sem se dar por isso. Lembre-se mantenha-o a correr!
Aqui estão algumas coisas que poderá precisas para criar um jogo completo.