O objetivo desta atividade é a familiarização com as ferramentas e o ambiente de trabalho da disciplina.
Este documento contém informações sobre como funciona o processo de compilação do software com mais detalhes. Ele também o auxiliará na tarefa de organizar e automatizar um processo de compilação que envolve várias etapas. Dentre esses processos, você irá perceber que o programa gradativamente fica mais próximo da linguagem de máquina até o momento em que um arquivo com as instruções que o processador irá executar é criado, o executável.
Como estudo de caso, crie um arquivo com um programa em código C de nome ola.c. O conteúdo do arquivo deve ser:
#include <stdio.h> int main () { printf("Ola!\n"); return 0; }
Para compilar o programa C apresentado, a abordagem mais simples é utilizar, no shell linux (terminal), a linha de comando com o compilador gcc como a seguir:
usuario@maquina$ gcc ola.c -o ola
Este comando irá produzir um executável ola, o seu programa em linguagem de máquina, que poderá ser executado com o comando ./ola. Por exemplo:
usuario@maquina$ ./ola Ola! usuario@maquina$
Contudo, o compilador gcc esconde nesse comando uma série de programas que são articulados para produzir o executável final (o programa ola). Nesta disciplina, você irá se familiarizar com a construção de softwares em nível mais baixo do que o da linguagem C. Para isso, você deve entender quais as etapas percorridas pelo compilador até chegar na linguagem de máquina, aquela que realmente é entendida pelo processador e dispositivos integrados no momento de executar seu programa.
Toolchain é o conjunto de ferramentas do compilador para produzir software para um sistema (no nosso caso, um desktop compatível com a arquitetura Intel x86). Todo compilador começa por traduzir cada arquivo de linguagem C para código de linguagem de montagem (arquivos com a extensão .s no Linux ou .asm na maioria dos compiladores para os S.O. Windows). Em seguida, uma ferramenta chamada "montador" (assembler, em inglês) irá ler os arquivos em linguagem de montagem (assembly, em inglês) e produzir um código objeto (extensão .o no Linux ou .obj na maioria dos compiladores para o S.O. Windows). Existe um objeto para cada arquivo de linguagem C original. Note que um software grande pode conter diversos arquivos de código-fonte, o que irá levar a vários arquivos objeto durante o processo de compilação. A etapa final consiste em "ligar" todos os arquivos objeto em um único arquivo final, o executável. Essa etapa é realizada pelo ligador (linker, em inglês).
O ligador irá ler diversos arquivos objeto como entrada, ligá-los entre si, e também incluir código de bibliotecas que você usa. Um exemplo clássico de biblioteca é a biblioteca padrão C (no Linux, é a glibc ou GNU C Library). Ela contém, por exemplo, a implementação da função printf() usada por nosso programa. Sem o trabalho do ligador, nosso programa não conseguiria chamar a função printf. Após conectar todos os módulos e bibliotecas e fundí-los em um mesmo arquivo, o resultado é o executável final (sem extensão em sistemas Linux ou ".exe" no Windows), o programa que pode ser executado pelo usuário. A Figura 1 ilustra o processo típico de compilação.
Figura 1: O processo de compilação de um programa utilizando as ferramentas da GNU.
Você deverá percorrer o processo de compilação executando cada programa individualmente (compilador C, montador e ligador) até chegar no executável final do programa. O código fonte que você irá utilizar contém dois arquivos C com o seguinte conteúdo:
prog1.c:
#include <stdio.h> void funcao(); int main() { printf("Ola!\n"); funcao(); printf("Adeus!\n"); return 0; }
prog2.c:
#include <stdio.h> void funcao() { printf("Estou no arquivo 2!\n"); }
Você deverá fazer o processo uma vez manualmente, depois automatizar o processo com um script makefile. Crie uma regra para cada arquivo intermediário, até chegar no arquivo final. Execute a regra final e veja o que acontece.
Para compilar um código-fonte C em um código de linguagem de montagem .s:
gcc -S prog1.c -o prog1.s
Você pode verificar o conteúdo do arquivo prog1.s abrindo em seu editor de texto favorito. Ele contém o mesmo programa que você escreveu em C, porém transcrito em linguagem de montagem para o processador de arquitetura Intel x86, mais próximo da linguagem de máquina. Para alcançar o estágio de linguagem de máquina, use o montador (assembler):
as prog1.s -o prog1.o
Você não pode abrir o arquivo produzido (prog1.o) em qualquer editor de texto, pois é um arquivo binário em linguagem de máquina. Para analisar esse arquivo, você precisa de programas especiais chamados "desmontadores", que fazem o processo de engenharia reversa. Você pode utilizar a ferramenta objdump para desmontar o arquivo binário em linguagem de máquina. Tente objdump -D prog1.o e compare o resultado com o arquivo prog1.s.
Em seguida, você precisa ligar todos os outros módulos para produzir o executável final utlizando o ligador. Utilize:
ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib64/crt1.o /usr/lib64/crti.o -L/usr/lib64 prog1.o prog2.o [...] -lc /usr/lib64/crtn.o -o prog
O parâmetro -L não adiciona módulos ao seu programa, mas apenas especifica uma pasta onde encontrar bibliotecas (esse caminho pode variar entre distribuições Linux). O parâmetro -lc, por sua vez, informa que a biblioteca C é necessária. Os outros arquivos objeto .o (ponto ó) mencionados acima são pequenos módulos auxiliares necessários para todo programa em linguagem C. O trecho [...] não pode ser digitado na linha de comando. Ele apenas informa que você pode colocar quantos arquivos objeto .o quiser. Os arquivos objeto fornecidos como parâmetro são dispostos no executável final na mesma ordem dos parâmetros, por isso o crtn.o, que compreende comandos de finalização do suporte em tempo de execução C (C RunTime - eNd), aparece por último, e o crt1.o, que compreende a inicialização do C runtime aparece primeiro.
Note que o gcc é um comando multi-uso que invoca outros comandos por você e já provê os parâmetros corretos para o ligador (ld). Nesse caso, como estamos contruindo o programa sem o auxílio do gcc, nós precisamos passar todos os módulos adicionais que devem ser ligados ao seu executável para que ele funcione. Na dúvida, você pode compilar um programa de exemplo com o gcc e adicionar a flag -v para visualizar todos os comandos executados pelo gcc durante a compilação. Neste exercício nós invocamos diretamente as ferramentas as e ld para montagem e ligação dos programas, entretanto, na maior parte dos programas para linux, estas ferramentas são chamadas indiretamente, com o auxílio do gcc.
Após produzir o arquivo executável (p.ex. prog), o mesmo pode ser executado diretamente na linha de comando:
./prog
O processo de desenvolvimento de software envolve diversas iterações que envolvem correções de bugs e recompilações. Entretanto, muitos destes projetos possuem uma quantidade grande de arquivos de programa e a compilação de todos os arquivos é um processo lento. Os arquivos .o precisam ser ligados novamente para formar o novo binário, no entanto, apenas os arquivos modificados precisam ser recompilados. Dessa forma é importante ter um mecanismo automático para recompilar apenas os arquivos necessários. Para isso, existe uma modalidade de script específica para automatizar a compilação de softwares. O GNU makefile é um exemplo largamente utilizado no mundo GNU/Linux.
Para fazer o seu próprio script que irá orientar o GNU make a construir o seu programa, você deve especificar em um arquivo texto chamado Makefile, que deve estar na mesma pasta dos códigos-fonte, contendo regras para a criação de cada arquivo. Por exemplo, você deve especificar como o arquivo .s (em linguagem de montagem) é criado (utilizando o compilador gcc), especificar como os arquivos objetos .o (códigos objeto) são criados (utilizando o montador) e assim em diante. Exemplo de criação de regras:
ola.s: ola.c gcc -S ola.c -o ola.s ola.o: ola.s as -o ola.o ola.s
Neste exemplo existem duas regras: ola.o e ola.s. A regra ola.o deve corresponder ao arquivo que é produzido com essa regra. Os arquivos necessários para produzir o arquivo ola.o devem aparecem em uma lista (separada por espaços) após o caractere ":" (no nosso caso, ola.s é necessário para criar ola.o). Em seguida, você deve, na linha seguinte, usar uma tabulação (apertar a tecla tab) e digitar o comando que será executado no shell para produzir esse arquivo. No nosso exemplo, chamamos o compilador gcc para traduzir um arquivo em linguagem C para linguagem de montagem, e em outra regra, chamamos o montador GNU as para transformar um arquivo em linguagem de montagem .s em um arquivo objeto .o. Note que você pode especificar como arquivo de entrada de uma regra o nome de outra regra, esta outra regra será chamada antes para produzir o arquivo de entrada necessário. ATENÇÃO: O script não funcionará se não houver uma tabulação (tab) antes do comando! Não use espaços! É recomendado o uso do Emacs ou VI para a criação do Makefile.
Você pode criar várias regras em um mesmo arquivo Makefile. Para executar o script, na linha de comando, digite make nome-da-regra. Por exemplo:
usuario@maquina$ make nome-da-regraO programa make irá executar os comandos associados à regra nome-da-regra, descrita no Makefile. Note que o programa make sempre lê o arquivo de nome Makefile na pasta em que você está e o usa como script. Se você não utilizar esse nome de arquivo (Makefile com "M" maiúsculo), o script irá falhar. Se você invocar o comando make sem parâmetros ele executará a primeira regra do arquivo Makefile.
Agora você criará o arquivo Makefile para gerar um programa a partir do seu código fonte. O programa será construído a partir de 3 arquivos fontes com código escrito na linguagem C: São eles:
Referência para Makefile:
Responda às perguntas do formulário: http://goo.gl/forms/VZMFzMgvGk
O prazo para a entrega é dia: 14/03/16 às 13:59hs.