Arquitectura de Computadores

Notas de estudo

Alberto José Proença

1999/00

 

Índice geral

 

  1. Organização e arquitectura dum computador
  2. Análise do funcionamento do CPU dum computador
  3. Mecanismos para execução de programas
  1. Edição do programa fonte e sua tradução para níveis mais baixos
  2. Ligação de ficheiros objectos num executável
  3. Análise detalhada um exemplo
  4. Formato dum ficheiro objecto e dum executável
  5. Utilização da memória e papel dos loaders

 

No capítulo anterior fez-se a análise do funcionamento dum computador, onde um processador central, CPU, executa um programa codificado em linguagem máquina em binário, armazenado na memória principal do computador. O objectivo deste capítulo é a análise dos mecanismos que permitem passar de um programa escrito por um utilizador - quer em HLL, quer em assembly - para o programa codificado e armazenado na memória principal do computador, pronto a ser executado. Estes mecanismos estão descritos em 3.9 e detalhados no anexo A da bibliografia básica (ver A.1, A.2, A.3, A.4 e A.5).

Assim, ao longo deste capítulo iremos ver que um programa - em HLL ou em assembly - pode ser criado com um editor de texto, traduzido para um nível de abstracção inferior por um compilador (se for HLL) ou por um assembler (se estiver em assembly), ligado a outros módulos para formar um todo usando um linker, e carregado na memória e executado com a ajuda dum loader.

 

  1. Edição do programa fonte e sua tradução para níveis mais baixos

Qualquer programa fonte é simplesmente um texto que deverá obedecer às regras semânticas e sintácticas da linguagem que pretendem representar. A construção deste texto e seu armazenamento no disco do computador é normalmente tarefa de um editor de texto, quer independente, quer associado ao compilador ou ao assembler.

Quando se apresentaram os níveis de abstracção de um computador, introduziram-se também os conversores de níveis mais populares: o compilador e o assembler. Viu-se nessa altura que a maioria dos compiladores têm já incluída a função de montar o programa em binário (um assembler). Ao ficheiro produzido quer por um compilador deste tipo, quer por um assembler, dá-se o nome de ficheiro objecto.

  1. Ligação de ficheiros objectos num executável

Um ficheiro objecto contém o programa em linguagem máquina codificado em binário, mas com eventuais lacunas: a referência a certos símbolos encontrados no ficheiro com o programa fonte mas sem indicação do seu significado nesse mesmo ficheiro. Estes símbolos - que tanto podem ser etiquetas de instruções que se encontram noutros módulos, como nomes de "variáveis" descritas noutros módulos - só poderão ser convertidos para binário depois desses outros ficheiros - com os módulos contendo as descrições em falta - serem ligados entre si. É precisamente esta a função de um linker.

Compete ao linker juntar os vários módulos que constituem uma aplicação - já no formato de um ficheiro objecto - num único ficheiro executável, de modo a resolver todos as referências que não foram completamente descritas. A maioria destas referências costumam ser a funções disponibilizadas pelo sistema operativo (para interactuar com o utilizador, quer através do teclado/visor do monitor, quer através da impressora) ou ainda a rotinas que efectuam certas operações mais complexas (controlo de janelas, por exemplo). Estas funções/rotinas estão normalmente agrupadas e organizadas numa biblioteca de funções associada ao próprio compilador ou ao assembler.

  1. Análise detalhada um exemplo

A fig. A.5 apresenta uma pequena rotina escrita em C, enquanto a fig. A.4 mostra a mesma rotina, mas escrita em assembly com algumas extensões, mas sem comentários. Os seguintes comentários deverão ser feitos à codificação em assembly da fig A.4:

- o programa em assembly contém algumas linhas de comandos exclusivos para o assembler - as quais não são convertidas para linguagem máquina - que se designam por directivas para o assembler, e que começam sempre por um ponto (no caso do assembler do MIPS);

- as directivas .text e .data indicam ao assembler onde começa a zona do programa com código, e a zona do programa reservada aos dados; .align x indica ao assembler para alinhar na memória a informação que vem a seguir, quer em bytes (x=0), em meias-palavras de 16 bits (x=1) - isto é, colocar a informação apenas em localizações de memória que comecem por um endereço par - ou em palavras de 32 bits (x=2) - isto é, colocar a informação apenas em localizações de memória que comecem por um endereço múltiplo de 4; .globl x indica ao assembler que o símbolo x é um símbolo que tem visibilidade para outros módulos externos (informação para ser usada pelo linker); .asciiz x indica ao assembler que a string x que se segue é para ser codificada em ASCII, caracter a caracter, e terminada com um caracter especial, designado em ASCII por null; várias outras directivas são suportadas pelo asembler do MIPS, mas serão apresentadas gradualmente conforme forem sendo necessárias;

- as etiquetas main:, loop: e str: indicam ao assembler o início de pedaços de código ou de dados (isto é, endereços de memória) para referência noutros locais do programa, poupando ao programador de assembly a necessidade de efectuar todos os cálculos para a localização de código e de dados;

- embora não exista nenhum registo com o nome $sp, o assembler aceita que certos registos sejam referenciados por uma designação diferente, de acordo com uma convenção que se verá adiante; o registo que se aconselha a usar como stack pointer é o $29, e o assembler converte qualquer referência ao $sp em $29 (repare que a instrução que contém esta referência está um pouco incoerente quanto a esta facilidade, e não só, pois a instrução deveria ser "subiu $sp, $sp, 32");

- o programa em C faz uma referência (printf) que não é resolvida no próprio módulo (externa), e essa mesma referência transita para o código em assembly;

- o código apresentado contém várias pseudo-instruções, que facilitam a interpretação do seu conteudo por um ser humano; faltam contudo comentários que facilitem a sua leitura, e que deveriam ser iniciados sempre pelo caracter # (em cada linha de texto que apareçam).

A fig. A.3 mostra o mesmo programa, mas em assembly e sem quaisquer extensões: já não há directivas para o assembler, não há etiquetas nem comentários, todos os símbolos foram substituídos por valores binários (até mesmo a referência externa, printf) e as pseudo-instruções foram convertidas para instruções reais do instruction set do MIPS (excepto uma; consegue identificá-la?). Este código é integralmente equivalente ao código em linguagem máquina em binário (na fig. A.2), usando apenas símbolos para que os humanos identifiquem com mais facilidade as instruções, os registos referenciados, e os valores numéricos (em decimal).

  1. Formato dum ficheiro objecto e dum executável

Um ficheiro objecto em Unix é normalmente constituído por 6 campos:

- cabeçalho: descreve os tamanhos/localização do campos do ficheiro;

- segmento de texto: contém o código em linguagem máquina, que poderá não estar completo devido a referências não resolvidas;

- segmento de dados: contém a representação binária dos dados, podendo estar também incompleto por referências a dados noutros ficheiros;

- informação de recolocação: identifica o conjunto de instruções e de dados com endereços absolutos, os quais poderão ter de ser alterados para permitir a instalação do programa em qualquer zona da memória principal, para execução;

- tabela de símbolos: identifica/localiza as referências a labels externas e símbolos não resolvidos

- informação para debugging, para facilitar a ligação deste ficheiro objecto ao programa fonte.

De notar que este formato é bastante semelhante ao de um ficheiro executável; a principal diferença reside na tabela de símbolos, desnecessária no ficheiro executável.

  1. Utilização da memória e papel dos loaders

Após um programa fonte ser convertido para objecto e depois junto com outros num ficheiro executável, este é guardado em disco. Falta agora saber:

- para que posições de memória gera o assembler código dum programa, isto é, sempre que são feitas referências directas à memória num programa (por exemplo, em saltos incondicionais ou quando se refere a uma variável armazenada na memória), que posições de memória (endereço) o assembler convenciona usar;

- quando é necessário carregar o programa para a memória principal para ser executado, para onde vai o programa e que outras acções são necessárias.

A convenção adoptada na escrita de programas no MIPS considera a seguinte divisão da memória principal (ver A.5):

- de 0 a 400 000h: área reservada

- de 400 000h a 10 000 000h: segmento de texto, i.e., zona reservada para o código do programa (directiva .text);

- de 10 000 000h a 7f fff fffh: reservado primeiro para dados estáticos (conhecidos durante a compilação), depois para os dados dinâmicos (alocados durante a execução do programa) e por fim para a stack; a stack vai crescendo no sentido decrescente dos endereços de memória, com início em 7f fff fffh.

Quando o ficheiro executável vai ser carregado na memória principal para a sua execução, compete ao loader executar as seguintes tarefas:

- ler o tamanho dos segmentos de código e de dados, presente no cabeçalho do ficheiro executável, e alocar espaço na memória principal para os colocar, conjuntamente com espaço para a stack;

- transferir os segmentos de código e de dados do disco para a memória principal, com eventual correcção dos endereços absolutos (na tabela de recolocação);

- inicializar os conteúdos de registos que necessitem de uma valor inicial (caso do $sp), e colocando os outros registos a zero;

- invocar uma rotina de start-up que carrega o endereço do main no program counter, passando assim o controlo do CPU para a aplicação do utilizador; no fim da execução desta aplicação, o controlo é devolvido à rotina de start-up.

 Em resumo: vimos ao longo deste capítulo que um programa - em HLL ou em assembly - pode ser criado com um editor de texto, traduzido para um nível de abstracção inferior por um compilador (se for HLL) ou por um assembler (se estiver em assembly), ligado a outros módulos para formar um todo usando um linker, e carregado na memória e executado com a ajuda dum loader. A figura que se segue ilustra esta sequência.