4

Desenvolvimento de programas em UNIX

 

 

 

Sumário:

 

  • Fases de desenvolvimento de programas
  • Compiladores
  • Ficheiros em código objecto (.o)
  • Visualização de símbolos dum ficheiro objecto (nm)
  • Unificadores (linkers) de código objecto (ld)
  • Tabelas de Símbolos
  • Código Assembler

 

 

 

 

 

 

 

 

 

Fases de desenvolvimento de programas

 

 

Há três fases principais de desenvolvimento de programas:

 

Faz-se a edição de ficheiros em código fonte (ficheiro1.c, ficheiro2.c, etc) através de um editor de texto ASCII.

Faz-se a compilação de cada um dos ficheiros em código fonte de forma a obter cada um dos respectivos ficheiros em código objecto (ficheiro1.o, ficheiro2.o, etc)

Faz-se a junção dos ficheiros em código objecto num único ficheiro.  (binary file)

Este ficheiro poderá ser um ficheiro executável (a.out, a.exe,..) (chamado executable ou application) ou um ficheiro de biblioteca (.dll, lib, .a .so etc.) (chamado code library ou assembly)

 

Notas:

Por defeito o compilador (Linux) criará um ficheiro executável com nome a.out.

A opção de mudar o nome do ouput é o flag -o

 

 


Compilação dum programa c/ 1 ficheiro

 

 

 

 

 

 

 

 

 


Exemplo 4.1:

Considere-se o seguinte ficheiro fonte sum.c:

 

A compilação e execução deste programa através

do interpretador de comandos UNIX são feitas do

seguinte modo:

 

$ cc sum.c

$ a.out

Enter a number: 4

Sum of 1 to 4: 10

 

Notas:

O programa executável criado por defeito

tem o nome a.out

Podemos alterar o nome do output do compilador

usando a opção -o  cc sum.c -o sum.exe

 

 

 

 

Compilação dum programa da linguagem C por fases

 

A compilação dum programa escrito na linguagem C geralmente poderá ser considerada a ser constituída por quatro fases, senda elas Pré-Processamento, Compilação para Código Assembler, Compilação de Código Assembler para Código Objecto e Unificação (linking) para criar um ficheiro final.  Muitas vezes o compilador de Linux (gnu cc) chame de facto 4 programas distintas – diz-se que o “cc”  é um “Compiler Driver”.

 

Fase

Comando para cc/gcc

Output

Programa do Sistema Chamada

Pre-processor 

cc –E sum.c -o sum.i                

sum.i

cpp

Compiler

cc –S sum.c

sum.s

cc1

Assembler

cc –c sum.c

sum.o

as

Linker

cc sum.o

a.out

ld

 

      Standard Libraries

 

O compilador procurará os ficheiros necessário, de inclusão na fase de pre-procesamento e as bibliotecas compilados na fase de linkagem, necessárias para compilação, nas pastas especificados por defeito no seu sistema e/ou configuração pessoal. Exemplos típicos (Linux 32 bits) são dados em baixo :

Pre-processor: 

Header Files (.h)   

Localização /usr/inlcude

Linker

Static and Dynamic Library Files (.a, .so) 

Localização  /usr/lib


 

Ficheiros de código objecto

 

Um ficheiro objecto contém dados e instruções codificados em linguagem máquina (0´s e 1´s) que o unificador (linker) usa para criar um programa executável ou uma biblioteca. Incluído também é uma tabela de identificadores ou símbolos chamado symbol table.  Cada identificador (symbol name) identifica um subprograma (função) ou um dado (variável ou constante). Há dois tipos de identificadores:

-Locais. Estes identificadores só estão acessíveis dentro do ficheiro objecto onde estão definidos. Não é possível aceder-lhes a partir doutros ficheiros objecto. Por exemplo, todos os identificadores de variáveis e funções declarados como static num programa em C são identificadores locais ou variáveis declaradas dentro dum bloco {}.

 

 

-Globais : Todos os identificadores que são acessíveis a partir doutros ficheiros.

 Por exemplo uma função declarada por defeito ou uma variável ou constante declarada fora dum bloco de código. Referenciado num outro ficheiro com a palavra chave extern

 

Referências externas (external reference)

 

Uma referência externa é qualquer referência a um identificador pertencente a outro ficheiro. Se, porventura, na fase de unificação esta referência externa (p.ex., o identificador duma função) não encontra a sua definição (o código da função), o unificador ld debita para o dispositivo de saída a informação de que o identificador não pode ser resolvido.

 

Por exemplo, após a compilação do ficheiro teste.c, segue-se a unificação do identificador pritf com o seu código existente na biblioteca libc (por defeito). Só que o identificador pritf não pode ser resolvido, visto que não existe esta função na biblioteca libc.

 

 

 

$ cc teste.c

ld:

Unresolved:

pritf

$

 

 

A resolução das referências externas só é feita na fase de unificação. Isto é assim porque na fase de compilação só é compilado um ficheiro de cada vez. Portanto, se uma função estiver implementada num ficheiro e a ser usada noutro ficheiro é impossível fazer a resolução ou “associação” entre o identificador da função e o seu código. 

A mesma situação ocorre na utilização de variáveis declaradas extern num ficheiro mas são definidas num outro. A criação da tabela de símbolos em cada ficheiro objecto serve, pois, para ser possível efectuar a resolução de símbolos na fase de unificação.

 

 

cc -c teste2.c

cc -o teste2 teste2.o

teste2.o: In function `main':

teste2.c:(.text+0x6): undefined reference to `x'

collect2: error: ld returned 1 exit status            (linker ld returns error)

 

Compilação dum programa c/ vários ficheiros

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Exemplo 4.2:

 

Considere-se os seguintes ficheiros fonte,

mainsum.c e funcsum.c:

 

A compilação e execução deste programa através

do interpretador de comandos UNIX são feitas do

seguinte modo:

 

$ cc mainsum.c funcsum.c

$ a.out

Enter a number: 4

Sum of 1 to 4: 10

 

 

 


Exemplo 4.3

 

Os ficheiros fontes podem ser compilado separadamente para ficheiros objectos.

A seguir podem ser “unidos” (linked) num único ficheiro executável.

 

$ cc –c funcsum.c

$ cc -c mainsum.c

$ cc –o myprog funcsum.o mainsum.o

$ myprog

Enter a number: 4

Sum of 1 to 4: 10

 

 

 


 

Dispositivo de compilação por dentro

 

$ cc -v mainsum.c funcsum.c

mainsum.c:

/lib/cpp mainsum.c /tmp/ctmAAAa01061 -D__hp9000s800 -D__hpux D__unix

cc: Entering Preprocessor

/lib/ccom /tmp/ctmAAAa01061 main.o -00 -v -Ac

cc: Entering Scanner and Parser.

cc: Entering Semantic Analyzer.

cc: Entering Allocator.

cc: Entering Code Generator.

cc: Entering Code Optimizer.

funcsum.c:

/lib/cpp funcsum.c /tmp/ctmAAAa01061 -D__hp9000s800 -D__hpux D__unix

cc: Entering Preprocessor

/lib/ccom /tmp/ctmAAAa01061 funcsum.o -00 -v -Ac

cc: Entering Scanner and Parser.

cc: Entering Semantic Analyzer.

cc: Entering Allocator.

cc: Entering Code Generator.

cc: Entering Code Optimizer.

/bin/ld /lib/crt0.o -u main mainsum.o funcsum.o -lc

cc: Entering Link editor.

 
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


O dispositivo de compilação ou compilador compreende três fases fundamentais:

·        Pre-processamento – interpretação dos diretivas # (#include, #define , #if #ifdef..)

·        Compilação propriamente dita incluindo Assemblagem

·        unificação (linking) de código objecto.

 

A geração de código objecto é feita na fase de compilação - pela operação de converter o código Assembler em Instruções maquinas, a qual compreende 6 subfases, desde o pre-processamento  (Entering Preprocessor) até ao optimizador de código (Entering Code Optimizer).

A fase de unificação análise e junte  todos os ficheiros de código objecto criados na primeira fase, juntando-lhe ainda (no exemplo apresentado) o ficheiro /lib/crt0.o, sem o qual neste sistema não é possível correr um programa.

Nota que optimizações podem ser feitas em múltiplas e varias fases de operação e não apenas ao nível de Assembler ou geração das Instruções Maquinas.

O compilador coloca simplesmente as referências externas nas tabelas de símbolos

dos vários ficheiros objecto, deixando para o unificador (linker) o acasalamento das referências externas com as definições globais.

Além do acasalamento das referências externas com as definições globais existentes nos ficheiros objecto, o unificador (ld) acasala referências externas com as definições globais existentes nas bibliotecas (libraries).

Isto acontece porque uma biblioteca é um ficheiro objecto que contém subprogramas e dados que podem ser usados por vários programas.

 

 

 

Visualização da tabela de símbolos

 

A visualização dos identificadores existentes na tabela de símbolos dum ficheiro objecto é feita através do comando UNIX nm:

 

$ nm -p funcsum.o

1073741824  d  $THIS_DATA$    Other symbols created from compiling

1073741824  d  $THIS_SHORTDATA$

1073741824  b  $THIS_BSS$

1073741824  d  $THIS_SHORTBSS$

0000000000  T  somatorio      Global definition of somatorio

$ nm -p mainsum.o

0000000000  U  $global$       Other symbols created from compiling

1073741824  d  $THIS_DATA$

1073741824  d  $THIS_SHORTDATA$

1073741824  b  $THIS_BSS$

1073741824  d  $THIS_SHORTBSS$

0000000000  T  main           Global definition of main

0000000000  U  printf         External reference to printf

0000000000  U  scanf          External reference to scanf

0000000000  U  somatorio      External reference to somatorio

 
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


cc -o soma -v funcsum.c mainsum.c

 

Thread model: posix

gcc version 4.9.2 20150212 (Red Hat 4.9.2-6) (GCC)

 

COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64'

 /usr/libexec/gcc/x86_64-redhat-linux/4.9.2/cc1 funcsum.c .. -o /tmp/ccE5lPZL.s

as -v --64 -o /tmp/ccguJPUg.o /tmp/ccE5lPZL.s

 

/usr/libexec/gcc/x86_64-redhat-linux/4.9.2/cc1  mainsum.c  -o /tmp/ccE5lPZL.s

as -v --64 -o /tmp/ccmQ5JVL.o /tmp/ccE5lPZL.s

 

/usr/libexec/gcc/x86_64-redhat-linux/4.9.2/collect2 .... -o soma /usr/lib/gcc/x86_64-redhat-linux/4.9.2/../../../../lib64/crt1.o …..

 

Nota: collect2 calls the linker (ld)

 

Código Assembler

 

Podemos embutir código assembler nos nossos programas.

 

https://www.codeproject.com/Articles/15971/Using-Inline-Assembly-in-C-C

 

Experimente este programa que é baseado no link em cima !

#include <stdio.h>

 

int main() {

    /* Add 10 and 20 and store result into register %eax */

    asm ( "movl $10, %eax;"

          "movl $20, %ebx;"

          "addl %ebx, %eax;"

    );

 

    /* Subtract 20 from 10 and store result into register %eax */

    asm ( "movl $10, %eax;"

          "movl $20, %ebx;"

          "subl %ebx, %eax;"

    );

 

    /* Multiply 10 and 20 and store result into register %eax */

    asm ( "movl $10, %eax;"

          "movl $20, %ebx;"

          "imull %ebx, %eax;"

    );

 

    return 0 ;

}

 

Podem imprimir o valor do registo %eax com o codigo seguinte

 

    int eax_value;

 

    // Inline assembly to move the value of eax register to eax_value variable

    asm("movl %%eax, %0;" : "=r" (eax_value));

 

    // Printing the value of eax using printf

    printf("Value of %%eax register: %d\n", eax_value);

 

 

 

 

Mais uma Refrence

http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html