Exercícios

  1. Instalação dos ficheiros necessários:
    1. Faça o download da página da disciplina do ficheiro fact.tgz.
    2. Descomprima o ficheiro com o comando:
      $ tar xvfz fact.tgz
      Foi criado um directório "fact" com o conteúdo do ficheiro.
    3. Entre dentro desse directório com o comando:
      $ cd fact
    4. Liste os ficheiros com o comando:
      $ ls -l
  2. Compilação:
    1. Visualize o ficheiro main.c com um editor de texto à sua escolha
      • declaração da função main: argumentos e tipo de retorno.
      • valores de retorno.
      • função do programa
      • includes e globais
      • ver fact.h, iter.c e recurs.c
    2. Compile os módulos fonte do programa iterativo e faça a ligação do código objecto resultante para produzir o executável.
      $ gcc -ansi -pedantic -Wall -o iter main.c iter.c
    3. Execute o programa e verifique que este imprime o factorial de 5.
  3. Passos intermédios do processo de compilação:
    1. Pré-processamento: fase que antecede a compilação e que executa as directivas iniciadas por #. Por exemplo, no ficheiro main.c serão processadas as directivas include e define.
      $ gcc -E main.c
      O resultado do pré-processamento é enviado para o terminal. (O pré processador pode ser invocado separadamente com o comando cpp)
    2. Compilação: fase que gera código final em assembly. O assembly ainda tem um formato textual, pode ser lido e modificado por um vulgar editor de texto, mas o código gerado já depende do processador, arquitectura e sistema operativo.
      $ gcc -S iter.c
      Verifique o código assembly gerado no ficheiro iter.s. Compare as variantes do ficheiro iter.s quando utiliza o optimizador (adicionar a opção -O) e a informação para o debugger (adicionar a opção -g).
    3. Montagem ou assemblagem: fase que produz os códigos binários, ficheiros objecto (nada tem a ver com linguagens orientada para objectos), que serão processados pelo CPU. Esta fase é independente da linguagem de alto nível utilizada: C, Pascal, Fortran, etc.
      $ gcc -c main.c
      Verifique o tipo de ficheiro gerado, com o comando:
      $ file main.o
      e as dimensões das secções, com o comando:
      $ size main.o
      e a tabela de símbolos, com o comando:
      $ nm main.o
      (O assemblador pode ser invocado separadamente com o comando as)
    4. Ligação ou Linkagem: fase que produz o ficheiro executável final através da interligação dos vários ficheiros objectos ou de bibliotecas (conjuntos de ficheiros objecto).
      $ gcc -o factorial main.o iter.s
      Verifique o tipo de ficheiro gerado e as dimensões das secções. Use o comando ldd para verificar as dependências das bibliotecas dinâmicas. Tente gerar um ficheiro executável com as variantes iterativa e recursiva, simultaneamente,
      $ gcc main.c iter.c recurs.c
      e verifique que existem duas realizações de factorial com o mesmo nome. Por outro lado, se tentar criar um ficheiro executável apenas com o ficheiro main.c,
      $ gcc main.c
      falta um ficheiro ou biblioteca que forneça uma realização de factorial.
    5. O processo completo (executável estático): o comando gcc permite, como já pode observar, controlar todo o processo de compilação para a linguagem C. Contudo, pode verificar a execução das diversas fases através da opção -v.
      $ gcc -v -static main.c iter.c
      Neste exemplo, geramos um executável estático, isto é, não depende na execução da existência das bibliotecas dinâmicas. Como contrapartida, o executável final fica muito maior, pois inclui no próprio ficheiro uma cópia de todas as funções utilizadas, como por exemplo o printf. Verifique o tipo, dimensões do ficheiro ( ls -l), dimensões das secções e dependências do ficheiro a.out gerado, face ao executável dinâmico gerado na alínea anterior. Retire a informação simbólica, com o comando strip, e verifique que o executável ficou mais pequeno e que já não é possível saber a posição dos símbolos (comando nm).
  4. Utilização do depurador ou debugger:
    1. Recompile o programa factorial com geração de informação para o debugger ( -g):
      $ gcc -ansi -pedantic -Wall -g -o iter main.c iter.c
    2. Execute o programa iter dentro do debugger gdb e visualise o código fonte:
      $ gdb iter
      gdb> list
    3. Recorrendo ao debugger, insira um breakpoint no início da função main; imprima o valor da variável n e verifique que ainda não foi iniciada; execute a linha que contém essa instrução e verifique que a variável já contém o valor 5; modifique a variável para que seja calculado o factorial de 7; continue a execução do programa, agora que o valor da variável foi modificada, e verifique que foi calculado o factorial de 7:
      gdb> break main
      gdb> run
      gdb> print n
      gdb> next
      gdb> print n
      gdb> next
      gdb> print n
      gdb> set n=7
      gdb> continue

    4. Verifique os valores parciais do cálculo do factorial, colocando um breakpoint na instrução do ciclo for, imprimindo o valor da variável fact em cada iteração: gdb> list factorial
      gdb> break 9
      gdb> run
      gdb> display fact
      gdb> continue
      gdb> continue
      ...

  5. Gestão de configurações:
    Para automatizar o processo de compilação de programas (e não só), utiliza-se uma ferramenta de gestão de configurações, make no caso de sistemas Unix.
    1. A ferramenta make já inclui um conjunto de regras que permitem tratar muitas situações comuns. Por exemplo, verifique que sabe gerar um ficheiro objecto a partir de um ficheiro C, com o mesmo nome:
      $ make recurs.o
    2. Situações mais complexas obrigam a descrever as regras específicas no ficheiro Makefile da directoria corrente. O ficheiro Makefile é constituído por um conjunto de regras do tipo:
      dependência : lista-de-dependentes
      <-tab-> comando-1
      <-tab-> comando-2
      <-tab-> ...
      <-tab-> comando-n

      Se algum dos ficheiros, ou regras, na lista-de-dependentes tiver uma data mais actual que a dependência, os comandos ( zero ou mais ) são executados por forma a gerar uma nova versão da dependência. Note-se que a ferramenta não garante que os comandos gerem uma nova versão da dependência, mas caso não seja gerada uma versão com a data actual o processo de geração da configuração é interrompido.
    3. Modifique a Makefile disponibilizada por forma a gerar o factorial recursivo através da utilização dos ficheiros main.c e recurs.c. Não se esqueça que para gerar o main.o necessita quer do main.c quer do fact.h. (alterações ao fact.h podem alterar a geração de código das rotinas contidas no main.c)
    4. Modifique a Makefile anterior por forma a gerar simultaneamente o factorial recursivo e iterativo, respectivamente iter e recurs.

Attachments