IMPLEMENTAÇÃO DE UNIDADES PARA PROCESSOS CONCORRENTES NO AMBIENTE FURBOL

Tamanho: px
Começar a partir da página:

Download "IMPLEMENTAÇÃO DE UNIDADES PARA PROCESSOS CONCORRENTES NO AMBIENTE FURBOL"

Transcrição

1 UNIVERSIDADE REGIONAL DE BLUMENAU CENTRO DE CIÊNCIAS EXATAS E NATURAIS CURSO DE CIÊNCIAS DA COMPUTAÇÃO (Bacharelado) IMPLEMENTAÇÃO DE UNIDADES PARA PROCESSOS CONCORRENTES NO AMBIENTE FURBOL TRABALHO DE CONCLUSÃO DE CURSO SUBMETIDO À UNIVERSIDADE REGIONAL DE BLUMENAU PARA A OBTENÇÃO DOS CRÉDITOS NA DISCIPLINA COM NOME EQUIVALENTE NO CURSO DE CIÊNCIAS DA COMPUTAÇÃO - BACHARELADO PAULO HENRIQUE DA SILVA BLUMENAU JULHO/ /1-60

2 IMPLEMENTAÇÃO DE UNIDADES PARA PROCESSOS CONCORRENTES NO AMBIENTE FURBOL PAULO HENRIQUE DA SILVA ESTE TRABALHO DE CONCLUSÃO DE CURSO FOI JULGADO ADEQUADO PARA OBTENÇÃO DOS CRÉDITOS NA DISCIPLINA DE TRABALHO DE CONCLUSÃO DE CURSO OBRIGATÓRIA PARA OBTENÇÃO DO TÍTULO DE: BACHAREL EM CIÊNCIAS DA COMPUTAÇÃO Prof. José Roque Voltolini da Silva Orientador na FURB Prof. José Roque Voltolini da Silva Coordenador do TCC BANCA EXAMINADORA Prof. José Roque Voltolini da Silva Profª. Joyce Martins Prof. Jomi Fred Hübner ii

3 SUMÁRIO LISTA DE FIGURAS...IX LISTA DE QUADROS... X RESUMO...XIV ABSTRACT... XV 1 INTRODUÇÃO OBJETIVO ORGANIZAÇÃO DO TEXTO FUNDAMENTAÇÃO TEÓRICA COMPILADORES ANÁLISE LÉXICA ANÁLISE SINTÁTICA GRAMÁTICAS LIVRES DE CONTEXTO DERIVAÇÕES ÁRVORES GRAMATICAIS AMBIGÜIDADE ELIMINAÇÃO DA AMBIGÜIDADE RECURSÃO À ESQUERDA ELIMINAÇÃO DA RECURSÃO À ESQUERDA FATORAÇÃO À ESQUERDA ANÁLISE SINTÁTICA TOP-DOWN ANALISADORES SINTÁTICOS PREDITIVOS ANÁLISE SEMÂNTICA TRADUÇÃO DIRIGIDA PELA SINTAXE DEFINIÇÕES DIRIGIDAS PELA SINTAXE ESQUEMAS DE TRADUÇÃO GRAMÁTICA DE ATRIBUTOS iii

4 ATRIBUTOS SINTETIZADOS ATRIBUTOS HERDADOS DEFINIÇÕES L-ATRIBUÍDAS TRADUÇÃO TOP-DOWN ELIMINAÇÃO DE RECURSÃO À ESQUERDA DE UM ESQUEMA DE TRADUÇÃO PROJETO DE TRADUTOR PREDITIVO GERAÇÃO DE CÓDIGO INTERMEDIÁRIO LINGUAGENS INTERMEDIÁRIAS CÓDIGO DE TRÊS ENDEREÇOS TRADUÇÃO DIRIGIDA PELA SINTAXE EM CÓDIGO DE 3 ENDEREÇOS RETROCORREÇÃO GERAÇÃO DE CÓDIGO ENTRADA PARA O GERADOR DE CÓDIGO PROGRAMA-ALVO GERENCIAMENTO DE MEMÓRIA SELEÇÃO DE INSTRUÇÕES ALOCAÇÃO DE REGISTRADORES MÁQUINA-ALVO ALGORITMO SIMPLES DE GERAÇÃO DE CÓDIGO CONCEITOS DE LINGUAGENS DE PROGRAMAÇÃO UNIDADES DE PROGRAMA PROCEDIMENTOS REGISTROS DE ATIVAÇÃO ESCOPO CONCORRÊNCIA CONCORRÊNCIA FÍSICA CONCORRÊNCIA LÓGICA THREADS DE CONTROLE CONCORRÊNCIA NO NÍVEL DE SUBPROGRAMA TAREFAS iv

5 ESCALONAMENTO ALGORITMO DE ROUND-ROBIN VIVÊNCIA DE TAREFAS SINCRONIZAÇÃO E INTERAÇÃO SINCRONIZAÇÃO DE COOPERAÇÃO SINCRONIZAÇÃO DE COMPETIÇÃO REQUISITOS DE PROJETO MECANISMOS DE SINCRONIZAÇÃO SEMÁFOROS MONITORES PASSAGEM DE MENSAGENS CONCORRÊNCIA NO AMBIENTE BORLAND DELPHI DECLARAÇÃO DE OBJETOS THREAD CÓDIGO DE INICIALIZAÇÃO CÓDIGO EXECUTADO PELA THREAD CÓDIGO DE LIMPEZA ATIVAÇÃO DE THREADS DESATIVAÇÃO DE THREADS ARQUITETURA DO MICROPROCESSADOR CIRCUITOS DE SUPORTE CONTROLADOR DE INTERRUPÇÕES CONTROLADOR DE DMA 8237A GERADOR DE PULSO 8284A INTERFACE PROGRAMÁVEL DE PERIFÉRICO TEMPORIZADOR PROGRAMÁVEL CONTROLADOR DE VÍDEO CONTROLADOR DE DISQUETE PD BARRAMENTO MEMÓRIA SEGMENTAÇÃO DA MEMÓRIA REGISTRADORES INTERNOS v

6 REGISTRADORES DE PROPÓSITO GERAL REGISTRADORES DE ÍNDICE E PONTEIROS REGISTRADORES DE SEGMENTO E O PONTEIRO DA INSTRUÇÃO SINALIZADORES PILHA MODOS DE ENDEREÇAMENTO DE DADOS NO ENTRADA E SAÍDA ROM, ROM-BIOS E MS-DOS INTERRUPÇÕES INTERRUPÇÕES DE HARDWARE EXCEÇÕES INTERRUPÇÕES DE SOFTWARE TABELA DE VETORES DE INTERRUPÇÃO INTERRUPÇÃO DE TEMPORIZAÇÃO 8H (TIMER) CONJUNTO DE INSTRUÇÕES DO MOVIMENTAÇÃO DE DADOS ARITIMÉTICAS LÓGICAS DESVIO DE FLUXO ENTRADA E SAÍDA CONTROLE ROTAÇÃO SISTEMA OPERACIONAL MS-DOS FUNÇÕES DO MS-DOS MONTADORES (ASSEMBLERS) LINGUAGEM ASSEMBLY SÍMBOLOS ELEMENTOS FORMATO DO PROGRAMA SEGMENTOS VARIÁVEIS vi

7 2.7.6 RÓTULOS CONSTANTES ACESSANDO E ALTERANDO CARACTERÍSTICAS DE UM SÍMBOLO ESTRUTURA DE ARQUIVOS.COM NA LINGUAGEM ASSEMBLY AMBIENTE FURBOL INTERFACE DO AMBIENTE FURBOL DESENVOLVIMENTO DO PROTÓTIPO ASPECTOS DE IMPLEMENTAÇÃO DE CONCORRÊNCIA EM LINGUAGENS DE PROGRAMAÇÃO IMPLEMENTAÇÃO DO NÚCLEO DA LINGUAGEM FURBOL INICIALIZAÇÃO DO NÚCLEO TROCA DAS TAREFAS IMPLEMENTAÇÃO DAS TAREFAS IMPLEMENTAÇÃO DOS SEMÁFOROS FINALIZAÇÃO DO NÚCLEO ESTRUTURA DO ARQUIVO.COM CONCORRENTE EM LINGUAGEM ASSEMBLY UNIDADES CONCORRENTES E SEMÁFOROS NA LINGUAGEM FURBOL TAREFAS COMANDO REPASSA COMANDO ESPERA COMANDO MORRE SEMÁFOROS ESPECIFICAÇÃO DA LINGUAGEM FURBOL PROGRAMA PRINCIPAL E BLOCO DE COMANDOS DECLARAÇÕES DE VARIÁVEIS E DEFINIÇÕES DE TIPOS PROCEDIMENTOS E TAREFAS COMANDOS DA LINGUAGEM EXPRESSÕES ESPECIFICAÇÃO DO AMBIENTE FURBOL APRESENTAÇÃO DO PROTÓTIPO vii

8 4 CONCLUSÃO EXTENSÕES REFERÊNCIAS BIBLIOGRÁFICAS APÊNDICE viii

9 LISTA DE FIGURAS FIGURA 1 UM COMPILADOR... 5 FIGURA 2 ÁRVORE GRAMATICAL FIGURA 3 ÁRVORE GRAMATICAL PARA A SENTENÇA (ID) FIGURA 4 - INTERFACE DO AMBIENTE FURBOL FIGURA 5 - DESCRITOR DE FILA DE PCBS FIGURA 6 - DESCRITOR DE TAREFA (PCB) FIGURA 7 - CONTEXTO INICIAL DA TAREFA FIGURA 8 - DESCRITOR DE SEMÁFORO (SMF) FIGURA 9 - DIAGRAMA DE CASOS DE USO DO AMBIENTE FURBOL FIGURA 10 - DIAGRAMA DE CLASSES DO AMBIENTE FURBOL FIGURA 11 - DIAGRAMA DE SEQÜÊNCIA DO PROCESSO COMPILAR FIGURA 12 - INTERFACE FINAL DO AMBIENTE FURBOL FIGURA 13 - CÓDIGO INTERMEDIÁRIO GERADO PELO AMBIENTE FIGURA 14 - CÓDIGO DE MONTAGEM GERADO PELO AMBIENTE ix

10 LISTA DE QUADROS QUADRO 1 PRODUÇÃO REPRESENTANDO ENUNCIADO CONDICIONAL... 8 QUADRO 2 GRAMÁTICA PARA UM TIPO DE EXPRESSÕES ARITIMÉTICAS... 9 QUADRO 3 UMA PRODUÇÃO-E QUADRO 4 REPRESENTAÇÃO DE DERIVAÇÕES DA PRODUÇÃO-E QUADRO 5 GRAMÁTICA BNF AMBÍGUA DO ELSE-VAZIO QUADRO 6 PRODUÇÕES NÃO-RECURSIVAS QUADRO 7 PRODUÇÕES AGRUPADAS QUADRO 8 PRODUÇÕES SUBSTITUÍDAS QUADRO 9 PRODUÇÕES NÃO FATORADAS QUADRO 10 PRODUÇÕES FATORADAS À ESQUERDA QUADRO 11 PRODUÇÕES PARA ANALISADOR SINTÁTICO PREDITIVO QUADRO 12 - DEFINIÇÃO S-ATRIBUÍDA DE UMA CALCULADORA QUADRO 13 - DEFINIÇÃO UTILIZANDO UM ATRIBUTO HERDADO QUADRO 14 ESQUEMA DE TRADUÇÃO NÃO L-ATRIBUÍDO QUADRO 15 ESQUEMA DE TRADUÇÃO L-ATRIBUÍDO QUADRO 16 - ESQUEMA DE TRADUÇÃO COM RECURSÃO À ESQUERDA (GENÉRICO) QUADRO 17 - GRAMÁTICA SEM RECURSÃO À ESQUERDA (GENÉRICA) QUADRO 18 - ESQUEMA DE TRADUÇÃO SEM RECURSÃO À ESQUERDA (GENÉRICO) QUADRO 19 - ESQUEMA DE TRADUÇÃO COM RECURSÃO À ESQUERDA QUADRO 20 - ESQUEMA DE TRADUÇÃO SEM RECURSÃO À ESQUERDA QUADRO 21 INSTRUÇÕES DO CÓDIGO DE TRÊS ENDEREÇOS QUADRO 22 CÓDIGO DE TRÊS ENDEREÇOS PARA COMANDO DE ATRIBUIÇÃO. 24 QUADRO 23 - REPRESENTAÇÃO EM QUÁDRUPLAS QUADRO 24 - REPRESENTAÇÃO EM TRIPLAS x

11 QUADRO 25 DEFINIÇÃO DIRIGIDA PELA SINTAXE PARA GERAR CÓDIGO DE TRÊS ENDEREÇOS PARA ATRIBUIÇÕES QUADRO 26 - CÓDIGO DE TRÊS ENDEREÇOS PRODUZIDO PELA DEFINIÇÃO NO QUADRO QUADRO 27 - ALGORITMO SIMPLES DE GERAÇÃO DE CÓDIGO QUADRO 28 - OPERAÇÃO P (ESPERAR) QUADRO 29 - OPERAÇÃO V (LIBERAR) QUADRO 30 - PROCESSOS DO CONCURRENT PASCAL QUADRO 31 - MONITORES DO CONCURRENT PASCAL QUADRO 32 - EXEMPLO DE ESPECIFICAÇÃO DE TAREFA EM ADA QUADRO 33 - CORPO PARA A TAREFA ESPECIFICADA NO QUADRO QUADRO 34 - EXEMPLO DE USO DA CLÁUSULA SELECT QUADRO 35 - CLÁUSULA WHEN QUADRO 36 - DECLARAÇÃO DE DESCENDENTE DA CLASSE TTHREAD QUADRO 37 - CÓDIGO DE INICIALIZAÇÃO QUADRO 38 - INSTANCIAÇÃO E EXECUÇÃO IMEDIATA QUADRO 39 - INSTANCIAÇÃO SEM EXECUÇÃO IMEDIATA QUADRO 40 - REGISTRADORES DO QUADRO 41 - SINALIZADORES DO REGISTRADOR DE FLAGS QUADRO 42 - PORTAS E SEUS ENDEREÇOS EM COMPUTADORES PC/XT QUADRO 43 - TABELA DE VETORES DE INTERRUPÇÃO DO PC/XT QUADRO 44 - INSTRUÇÕES DE MOVIMENTAÇÃO DE DADOS QUADRO 45 - INSTRUÇÕES ARITIMÉTICAS QUADRO 46 - INSTRUÇÕES DE DESVIO DE FLUXO QUADRO 47 - VARIAÇÕES DA INSTRUÇÃO DE DESVIO DE FLUXO J(CONDIÇÃO). 71 QUADRO 48 - INSTRUÇÕES DE ENTRADA E SAÍDA QUADRO 49 - INSTRUÇÕES DE CONTROLE QUADRO 50 - FUNÇÕES DA INT 21H QUADRO 51 - FORMATO DE UMA LINHA DE INSTRUÇÃO QUADRO 52 - FORMATO DE UMA LINHA DE DIRETIVA QUADRO 53 - FORMATO DA DIRETIVA END xi

12 QUADRO 54 - DECLARAÇÃO DE UM SEGMENTO QUADRO 55 - DIRETIVA ASSUME QUADRO 56 - EXEMPLOS DE DEFINIÇÃO DE VARIÁVEIS QUADRO 57 - EXEMPLO DE RÓTULOS QUADRO 58 - DECLARAÇÃO DE UMA ROTINA QUADRO 59 - DECLARAÇÃO DE UMA CONSTANTE QUADRO 60 - ESTRUTURA DE UM ARQUIVO.COM QUADRO 61 - ESTRUTURA DE UM PROGRAMA FURBOL QUADRO 62 - EXEMPLO DE UM PROGRAMA FURBOL QUADRO 63 - ALGORITMO DE SELEÇÃO DE TAREFAS DO ESCALONADOR QUADRO 64 - ESTRUTURA DE UM ARQUIVO.COM QUADRO 65 - SINTAXE DE TAREFA NA LINGUAGEM FURBOL QUADRO 66 - EXEMPLO DE CRIAÇÃO E EXECUÇÃO DE UMA TAREFA QUADRO 67 - EXEMPLO DE CRIAÇÃO E EXECUÇÃO DE DUAS TAREFAS QUADRO 68 - EXEMPLO DE USO DO COMANDO ESPERA QUADRO 69 - EXEMPLO DE USO DO COMANDO MORRE QUADRO 70 - APLICAÇÃO PRODUTOR-CONSUMIDOR NA LINGUAGEM FURBOL 109 QUADRO 71 - DEFINIÇÃO DO PROGRAMA PRINCIPAL E BLOCOS QUADRO 72 - DEFINIÇÃO DA DECLARAÇÃO DE VARIÁVEIS QUADRO 73 - DEFINIÇÃO DOS TIPOS DE DADOS QUADRO 74 - DEFINIÇÃO DE SUB-ROTINAS E PROCEDIMENTOS QUADRO 75 - DEFINIÇÃO DE TAREFAS QUADRO 76 - DEFINIÇÃO DE PARÂMETROS FORMAIS QUADRO 77 - DEFINIÇÃO DOS COMANDOS DA LINGUAGEM QUADRO 78 - DEFINIÇÃO DOS COMANDOS DA LINGUAGEM (CONTINUAÇÃO) QUADRO 79 - DEFINIÇÃO DOS COMANDOS DE ATRIBUIÇÃO, DEFINIÇÃO DA ESTRUTURA DE CHAMADA DE PROCEDIMENTO E PARÂMERTROS ATUAIS QUADRO 80 - DEFINIÇÃO DO COMANDO DE REPETIÇÃO E COMANDOS CONDICIONAIS QUADRO 81 - DEFINIÇÃO DOS COMANDOS DE ENTRADA E SAÍDA xii

13 QUADRO 82 - DEFINIÇÃO DOS COMANDOS INCREMENTO E DECREMENTO QUADRO 83 - DEFINIÇÃO DOS COMANDOS DE CRIAÇÃO E EXCLUSÃO DE SEMÁFOROS QUADRO 84 - DEFINIÇÃO DOS COMANDOS DE OPERAÇÃO P E V EM SEMÁFOROS QUADRO 85 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES QUADRO 86 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES (CONTINUAÇÃO) QUADRO 87 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES (CONTINUAÇÃO) QUADRO 88 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES (CONTINUAÇÃO) QUADRO 89 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES (CONTINUAÇÃO) QUADRO 90 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES (CONTINUAÇÃO) QUADRO 91 - CÓDIGO FONTE DO NÚCLEO QUADRO 92 - CÓDIGO FONTE DO NÚCLEO (CONTINUAÇÃO) QUADRO 93 - CÓDIGO FONTE DO NÚCLEO (CONTINUAÇÃO) QUADRO 94 - CÓDIGO FONTE DO NÚCLEO (CONTINUAÇÃO) QUADRO 95 - CÓDIGO FONTE DO NÚCLEO (CONTINUAÇÃO) QUADRO 96 - CÓDIGO FONTE DO NÚCLEO (CONTINUAÇÃO) QUADRO 97 - CÓDIGO FONTE DO NÚCLEO (CONTINUAÇÃO) QUADRO 98 - CÓDIGO FONTE DO NÚCLEO (CONTINUAÇÃO) QUADRO 99 - CÓDIGO FONTE DO NÚCLEO (CONTINUAÇÃO) QUADRO CÓDIGO FONTE DO NÚCLEO (CONTINUAÇÃO) QUADRO CÓDIGO FONTE DO NÚCLEO (CONTINUAÇÃO) xiii

14 RESUMO Este trabalho descreve o desenvolvimento do protótipo de um ambiente de programação para a linguagem de programação FURBOL. A fundamentação teórica necessária para o desenvolvimento é apresentada. As especificações da linguagem e do ambiente, implementado em Borland Delphi 6.0, também são apresentadas. O protótipo é um ambiente de programação integrado onde é possível editar e compilar um código fonte, gerando um código de montagem na linguagem Assembly, compatível com microprocessadores O trabalho é baseado no Trabalho de Conclusão de Curso do acadêmico Anderson Adriano (Adriano, 2001), estendendo o mesmo para oferecer suporte a concorrência em nível de unidades e a sincronização. Na especificação da linguagem é utilizada gramática livre de contexto na notação BNF (Forma de Bakus-Naur) juntamente com gramática de atributos. Na especificação do ambiente é utilizada a Unified Modeling Language (UML). xiv

15 ABSTRACT This work describes the development of the prototype of an programming environment for the FURBOL programming language. The necessary theoretical foundation for the development is presented. The specifications of the language and the environment, implemented in Borland Delphi 6.0, also are presented. The prototype is an integrated programming environment where it is possible to edit and compile a source code, generating an code in the Assembly language, compatible with 8088 microprocessors. The present work is based on the Work of Course Conclusion of academic Anderson Adriano (Adriano, 2001), extending the same to offer support for unit-level multitasking (concurrency) and synchronization. In the language specification, context-free grammar in the Bakus-Naur Form (BNF) together with attribute grammar is used. In the specification of the environment the Unified Modeling Language (UML) is used. xv

16 1 1 INTRODUÇÃO Em 1987, com o artigo apresentado por Silva (1987), no I Simpósio Brasileiro de Engenharia de Software, é relatada a experiência do início do desenvolvimento de uma nova linguagem de programação. Em 1992 houve uma continuidade do trabalho, sob o título de Editor Dirigido por Sintaxe, realizada pelo acadêmico Douglas Nazareno Vargas (Vargas, 1992), através de uma pesquisa financiada pela Universidade Regional de Blumenau. Após o trabalho de Vargas (1992), vários outros acadêmicos deram continuidade ao referido trabalho. Joilson Marcos da Silva apresentou no primeiro semestre de 1993 seu Trabalho de Conclusão de Curso com o título de Desenvolvimento de um ambiente de programação para a linguagem portugol (Silva, 1993). No segundo semestre de 1993, o acadêmico Douglas Nazareno Vargas (Vargas, 1993), através do seu Trabalho de Conclusão de Curso, voltou a dar continuidade ao ambiente apresentando mais um trabalho: Definição e implementação no ambiente windows de uma ferramenta para o auxílio no desenvolvimento de programas. Em 1996 Jorge Luiz Bruxel (Bruxel, 1996) deu continuidade ao trabalho, denominando-o de FURBOL (acrônimo de FURB e ALGOL). Em seguida, Radloff (1997), Schmitz (1999), André (2000) e Adriano (2001) acrescentaram novas extensões ao ambiente FURBOL. Ao longo dos trabalhos realizados, o FURBOL deixou de ser apenas uma linguagem de programação para se tornar um ambiente integrado onde é possível editar um código fonte e compilá-lo. Na compilação, o código fonte é traduzido para um código intermediário de três endereços gerando um código equivalente na linguagem Assembly (código de montagem). Através da utilização do montador Turbo Assembler, o código de montagem é convertido para um código de máquina executável em microprocessadores Partindo do último trabalho realizado (Adriano, 2001), este trabalho adicionará uma extensão à linguagem para dar suporte à concorrência em nível de unidade de programa. Segundo Ghezzi (1991), unidades de programa são agrupamentos de instruções implementando uma abstração de ação. Sebesta (2000) define concorrência em nível de unidades como o ato de executar duas ou mais unidades de programa simultaneamente. Unidades de programa concorrentes aumentam

17 2 a flexibilidade de programação. Foram inventadas originalmente para serem utilizadas nas tarefas de sistemas operacionais e, muitas vezes, também são utilizadas para simular sistemas físicos reais que consistem em vários subsistemas concorrentes (Sebesta, 2000). A extensão desenvolvida neste trabalho para a linguagem FURBOL também inclui um mecanismo de sincronização para possibilitar a interação em tempo de execução entre as unidades concorrentes. O mecanismo de sincronização escolhido são os semáforos. O principal motivo que levou a desenvolver este trabalho foi estudar a teoria de compiladores, processos concorrentes e arquitetura de computadores, aplicando os conhecimentos na implementação de extensões para a linguagem de programação FURBOL. Outro motivo foi o fato de que esta linguagem vem sendo desenvolvida na Universidade Regional de Blumenau através de Trabalhos de Conclusão de Curso (TCCs), sendo este trabalho uma continuação da série. 1.1 OBJETIVO O objetivo do trabalho é estender a definição formal da linguagem apresentada em Adriano (2001), possibilitando concorrência em nível de unidades e controle de sincronização. 1.2 ORGANIZAÇÃO DO TEXTO O trabalho está divido em 4 capítulos, sendo que no primeiro é feita a introdução do mesmo, relatando o surgimento do ambiente de programação FURBOL. O objetivo do trabalho também é apresentado. No capítulo 2 é apresentada a fundamentação teórica utilizada para desenvolver o protótipo do trabalho, abordando assuntos como teoria de compiladores, linguagens de programação, concorrência, arquitetura do microprocessador 8088, linguagem Assembly e o ambiente FURBOL. No capítulo 3 é descrito de forma detalhada o desenvolvimento do protótipo. Primeiramente são mostrados os aspectos de uma implementação de concorrência, em seguida a concorrência implementada para o ambiente, as estruturas que serão adicionadas a linguagem FURBOL, a especificação da linguagem com as novas estruturas e a especificação do ambiente. O protótipo é apresentado no final do capítulo.

18 3 do mesmo. No capítulo 4 é apresentada a conclusão do trabalho e sugestões para possíveis extensões

19 4 2 FUNDAMENTAÇÃO TEÓRICA Neste capítulo serão abordados conceitos de compiladores e técnicas para sua implementação, conceitos de concorrência e de comunicação entre unidades concorrentes e fundamentos para a implementação da concorrência em nível de unidades no microprocessador COMPILADORES Para se estabelecer uma comunicação entre indivíduos utiliza-se a linguagem. Conforme Price (2001), na programação de computadores, um indivíduo que queira resolver um determinado problema utiliza a linguagem de programação como meio de comunicação entre ele e o computador escolhido. A linguagem de programação faz a ligação entre o pensamento humano, que muitas vezes não é estruturado, e a precisão requerida pela máquina. Quanto mais construções que refletem os elementos usados na descrição do problema existirem numa linguagem de programação, mais fácil torna-se o desenvolvimento de um programa. Price (2001) considera este tipo de linguagem como de alto nível e define como linguagem de baixo nível aquela aceita pela máquina, a qual consiste tipicamente numa seqüência de zeros e uns. Para que se tornem operacionais os programas escritos em linguagem de alto nível devem ser traduzidos para uma linguagem de máquina a fim de serem executados no computador escolhido. Essa tradução é feita por sistemas especializados denominados compiladores (Price, 2001). Price (2001) define tradutor, no contexto de linguagens de programação, como um sistema que aceita como entrada um programa escrito em uma linguagem de programação (linguagem fonte) e produz como resultado um programa equivalente em outra linguagem (linguagem objeto). Assim um compilador é um tradutor que mapeia programas escritos em linguagem de programação de alto nível (programa fonte) para programas equivalentes em linguagem simbólica ou linguagem de máquina que viabilize sua execução (programa objeto), como mostrado na fig. 1.

20 5 FIGURA 1 UM COMPILADOR Programa Fonte Compilador Programa Objeto Segundo Aho (1995), a compilação é dividida em duas grandes partes: a análise e a síntese. Na primeira o programa fonte é analisado e em seguida é criada uma representação intermediária do mesmo. Na síntese o compilador constrói o programa objeto desejado a partir da representação intermediária. A análise consiste em três fases intermediárias (Aho, 1995): a) análise léxica: um fluxo de caracteres constituindo um programa é lido da esquerda para a direita sendo agrupado em tokens, que são seqüências de caracteres tendo um significado coletivo; b) análise sintática: nesta fase os caracteres ou tokens são agrupados hierarquicamente em coleções aninhadas com significado coletivo; c) análise semântica: são feitas verificações que asseguram que os componentes de um programa combinam-se de forma significativa. As fases intermediárias da síntese são (Price, 2001): a) geração de código intermediário: após a análise é gerada uma representação intermediária do programa fonte chamada de código intermediário; b) otimização de código: o código intermediário é melhorado de tal forma que venha a resultar num código de máquina mais rápido em tempo de execução; c) geração de código objeto: é a fase final onde o código intermediário otimizado é traduzido para o código executável. Tem-se o programa objeto, resultado da compilação. Conforme Aho (1995), as fases são coletadas numa interface de vanguarda ou de retaguarda. A interface de vanguarda consiste naquelas fases ou partes de fases, que dependem primariamente da linguagem fonte e são amplamente independentes da máquina alvo, como a análise léxica, a sintática, a semântica e a geração do código intermediário. A interface de

21 6 retaguarda inclui aquelas partes do compilador que dependem da máquina alvo e que, geralmente, não dependem da linguagem fonte, como a otimização e geração de código ANÁLISE LÉXICA Segundo Price (2001), a análise léxica é a primeira fase de um compilador e o objetivo do analisador léxico, ou scanner, é fazer a leitura do programa fonte, caractere a caractere e traduzi-lo para uma seqüência de símbolos léxicos (tokens). Para Price (2001) símbolos léxicos podem ser, por exemplo, as palavras reservadas, identificadores, constantes e operadores da linguagem. Durante a análise léxica são desprezados caracteres não significativos como comentários e espaços em branco. O analisador léxico desempenha outras funções como armazenar tokens em tabelas internas e indicar a ocorrência de erros caso encontre no código fonte algum símbolo que não pertence à linguagem. A seqüência de tokens resultante da análise léxica é utilizada como entrada para o módulo seguinte de um compilador: o analisador sintático ANÁLISE SINTÁTICA Sendo a segunda fase de um compilador, a função da análise sintática é verificar se as construções usadas no programa estão gramaticalmente corretas (Price, 2001). A análise sintática segundo José (1987) engloba as seguintes funções principais: a) identificação de sentenças; b) detecção, recuperação e correção de erros de sintaxe; c) ativação do analisador léxico; d) ativação de rotinas da análise referentes às dependências de contexto da linguagem, muitas vezes denominada análise semântica estática; e) ativação de rotinas da análise semântica; f) ativação de rotinas de síntese do código objeto. Segundo Price (2001), o objetivo principal do analisador sintático é verificar se uma sentença (formada pela seqüência de tokens gerada pelo analisador léxico) pertence à linguagem gerada por uma gramática livre de contexto que especifica a linguagem fonte. O analisador

22 7 sintático, também chamado parser, produz como resultado da análise uma árvore de derivação se a sentença for válida. Caso contrário, emite uma mensagem de erro. A árvore de derivação, ou árvore sintática, pode ser construída explicitamente sendo representada através de uma estrutura de dados ou implicitamente nas chamadas das rotinas que aplicam as regras de produção da gramática durante o reconhecimento. Os analisadores sintáticos são construídos de modo que possam prosseguir na análise mesmo que encontre erros no texto fonte. Existem duas estratégias básicas para a análise sintática: a) estratégia top-down ou descendente; b) estratégia bottom-up ou redutiva. Na estratégia top-down (descendente) a árvore de derivação é construída a partir do símbolo inicial da gramática fazendo com que a árvore cresça da raiz até suas folhas. A estratégia bottom-up (redutiva) realiza a análise no sentido inverso, constrói a árvore a partir dos tokens do texto fonte (folhas da árvore de derivação) até o símbolo inicial da gramática. Na estratégia top-down, em cada passo, um lado esquerdo de produção é substituído por um lado direito (expansão); na estratégia bottom-up, em cada passo, um lado direto de produção é substituído por um símbolo não-terminal (redução). Como o objetivo deste trabalho é adicionar uma extensão na especificação apresentada em Adriano (2001), será adotada a mesma estratégia de análise sintática utilizada pelo referido trabalho, a estratégia top-down GRAMÁTICAS LIVRES DE CONTEXTO Segundo Price (2001), as gramáticas livres de contexto formam a base para a análise sintática das linguagens de programação, pois permitem especificar a maioria das linguagens de programação usadas atualmente. Sebesta (2000) complementa dizendo que a notação mais comumente utilizada para se escrever uma gramática livre de contexto é a Forma de Bakus-Naur (BNF). Mais especificamente, Aho (1995) diz que as linguagens de programação que possuem uma estrutura inerentemente recursiva podem ser especificadas por gramáticas livres de contexto. Considerando S 1 e S 2 enunciados (por exemplo, comandos da linguagem) e E uma expressão, então if E then S 1 else S 2 também é um enunciado. Utilizando a variável sintática

23 8 cmd para denotar a classe de comandos e a variável sintática expr para expressões, pode-se especificar este enunciado condicional utilizando a produção gramatical apresentada no quadro 1. QUADRO 1 PRODUÇÃO REPRESENTANDO ENUNCIADO CONDICIONAL cmd if expr then cmd else cmd Fonte: Aho (1995) Uma gramática livre de contexto possui quatro componentes: a) terminais: são os símbolos básicos dos quais as cadeias são formadas. Os tokens vindos do analisador léxico são os terminais quando se tratando de gramática de linguagens de programação. No quadro 1 cada um dos tokens if, then e else é um terminal; b) não-terminais: são variáveis sintáticas que definem conjuntos de cadeias que auxiliam na definição da linguagem gerada pela gramática. Impõem uma estrutura hierárquica na linguagem que é utilizada pela análise sintática e pela tradução; c) símbolo de partida: é um não-terminal cujo conjunto de cadeias que denota é a linguagem definida pela gramática; d) produções: especificam a forma pela qual os terminais e os não-terminais podem ser combinados a fim de formar cadeias. Uma produção consiste em um não-terminal seguido por uma seta que por sua vez é seguida por uma cadeia de não-terminais e terminais, onde a seta deve ser lida como pode ter a forma. No que diz respeito à gramática livre de contexto, este capítulo empregará as mesmas convenções descritas em Aho (1995) para representá-la. Na especificação será utilizada uma adaptação desta simbologia para que mais detalhes possam ser mostrados. A simbologia descrita em Aho (1995) é a seguir especificada. Os símbolos terminais são representados por: letras minúsculas do início do alfabeto, tais como a, b, c; símbolos de operadores (+, - etc); símbolos de pontuação (parênteses, vírgula etc); dígitos (0, 1,...,9) e cadeias em negrito (id ou if).

24 9 Palavras vazias são representadas por ε e não-terminais serão representados por: letras maiúsculas do início do alfabeto (A, B e C); letra S, que será o símbolo de partida; nomes em itálico formados por letras minúsculas, como expr ou cmd. As letras maiúsculas do final do alfabeto, como X, Y e Z, representam terminais ou nãoterminais (símbolos gramaticais). Letras minúsculas, ao fim do alfabeto, como u, v,...,z, representam cadeias de terminais. Letras gregas minúsculas, como, e γ, representam cadeias de símbolos gramaticais. Dessa forma uma produção genérica pode ser escrita como A, isto é, o não-terminal A pode ter a forma de um ou mais símbolos gramaticais (terminais ou não terminais). Considerando as produções A 1, A 2,.., A k, todas com A à esquerda (produções-a), pode-se escrever A k. As cadeias de símbolos gramaticais 1, 2,.., k são chamadas de alternativas para A. O símbolo de partida é o lado esquerdo da primeira produção da gramática caso não seja explicitamente estabelecido DERIVAÇÕES Uma gramática deriva cadeias começando pelo símbolo de partida e, então, substituindo repetidamente um não-terminal pelo lado direito de uma produção para aquele nãoterminal. As cadeias de tokens que podem ser derivadas a partir do símbolo de partida formam a linguagem definida pela gramática (Aho, 1995). Este processo descrito por Aho (1995) é chamado de derivação e é representado conforme os quadros 2, 3 e 4. QUADRO 2 GRAMÁTICA PARA UM TIPO DE EXPRESSÕES ARITIMÉTICAS E E + E E * E (E) - E id Fonte: Aho (1995)

25 10 QUADRO 3 UMA PRODUÇÃO-E E - E Fonte: Aho (1995) QUADRO 4 REPRESENTAÇÃO DE DERIVAÇÕES DA PRODUÇÃO-E E - E - (E) - (id) Fonte: Aho (1995) O símbolo lê-se deriva em um passo. Obedecendo a gramática do quadro 2, tem-se a seqüência de derivações de - (id) apresentada no quadro 4. Diz-se, então, que a cadeia -(id) é uma sentença da gramática do quadro 2, pois existe derivação. Freqüentemente é necessário representar que uma derivação deriva em zero ou mais passos. Para esse propósito usa-se o símbolo *. Por exemplo, se * e γ, então * γ. Do mesmo modo usa-se + para significar deriva em um ou mais passos (Aho, 1995) ÁRVORES GRAMATICAIS Conforme Aho (1995), uma árvore gramatical ou árvore de derivação mostra graficamente como o símbolo de partida de uma gramática deriva uma cadeia de linguagem. Se um não-terminal A possui uma produção A XYZ, então uma árvore gramatical pode ter um nó interior rotulado A, com três filhos X, Y, e Z, da esquerda para a direita (fig. 2). FIGURA 2 ÁRVORE GRAMATICAL A X Y Z propriedades: Fonte: Aho (1995) Dada uma gramática livre de contexto, uma árvore gramatical possui as seguintes

26 11 a) a raiz é rotulada pelo símbolo de partida; b) cada folha é rotulada por um token ou por ε; c) cada nó interior é rotulado por um não-terminal. Assim, na fig. 3 tem-se no a representação gráfica da derivação do quadro 4. FIGURA 3 ÁRVORE GRAMATICAL PARA A SENTENÇA (id) E - E ( E ) id AMBIGÜIDADE Aho (1995) afirma que uma gramática pode ter mais de uma árvore gramatical para uma dada cadeia de tokens. Tal gramática é dita ambígua. Para ilustrar esta situação é necessária uma cadeia de tokens que possua mais de uma árvore gramatical. Tendo em vista que uma cadeia pode ter mais de uma árvore gramatical e geralmente estas árvores possuem mais de um significado, é necessário projetar gramáticas (para serem utilizadas na prática por compiladores) não ambíguas ou usar gramáticas ambíguas com regras adicionais para resolver estas ambigüidades ELIMINAÇÃO DA AMBIGÜIDADE Um exemplo prático de ambigüidade é mostrado na gramática do else-vazio no quadro 5.

27 12 QUADRO 5 GRAMÁTICA BNF AMBÍGUA DO ELSE-VAZIO cmd if expr then cmd if expr then cmd else cmd outro Fonte: Aho (1995) A alternativa outro significa qualquer outro enunciado. Aho (1995) afirma que a gramática é ambígua uma vez que a cadeia if E 1 then if E 2 then S 1 else S 2 possui duas árvores gramaticais. Isto é, o token else na primeira árvore gramatical pode pertencer ao primeiro then, e na segunda árvore gramatical, pode pertencer ao segundo then. Para resolver esta ambigüidade, as linguagens de programação que possuem enunciados condicionais desta forma, adotam a seguinte regra: associar cada else ao then anterior mais próximo ainda não associado (Aho, 1995) RECURSÃO À ESQUERDA Segundo Price (2001), gramáticas recursivas à esquerda é qualquer gramática livre de contexto que permite a derivação A + A, ou seja, um não terminal deriva ele mesmo. Os métodos de análise sintática top-down não podem processar gramáticas recursivas à esquerda (Price, 2001). Conseqüentemente uma eliminação da recursão à esquerda é necessária para a especificação deste trabalho, uma vez que será utilizada a análise sintática top-down ELIMINAÇÃO DA RECURSÃO À ESQUERDA Considerando o par de produções A A, a recursão à esquerda pode ser eliminada substituindo o par de produções pelas produções não-recursivas do quadro 6. QUADRO 6 PRODUÇÕES NÃO-RECURSIVAS A A A A ε Fonte: Aho (1995)

28 13 Não importa quantas produções-a existam na gramática, é possível eliminar a recursão imediata das mesmas. A técnica usada é: primeiro agrupa-se as produções-a onde nenhum i começa por um A, conforme o quadro 7; em seguida substituem-se as produções-a conforme o quadro 8. QUADRO 7 PRODUÇÕES AGRUPADAS A A 1 A 2... A m n Fonte: Aho (1995) QUADRO 8 PRODUÇÕES SUBSTITUÍDAS A 1A 2A... n A A 1A 2A... ma Fonte: Aho (1995) FATORAÇÃO À ESQUERDA Segundo Price (2001), fatoração à esquerda permite eliminar a indecisão sobre qual produção aplicar quando duas ou mais produções iniciam com a mesma forma sentencial. A indecisão referente às produções do quadro 9 seria eliminada fatorando as mesmas à esquerda, tornando-se as produções apresentadas no quadro 10. QUADRO 9 PRODUÇÕES NÃO FATORADAS A 1 2 Fonte: Price (2001)

29 14 QUADRO 10 PRODUÇÕES FATORADAS À ESQUERDA A X X 1 2 Fonte: Price (2001) ANÁLISE SINTÁTICA TOP-DOWN Aho (1995) define a análise sintática top-down como uma tentativa de se encontrar uma derivação mais à esquerda para uma cadeia de entrada. Ou seja, uma tentativa de se construir uma árvore gramatical, para a cadeia de entrada, a partir da raiz, criando os nós da árvore gramatical em pré-ordem ANALISADORES SINTÁTICOS PREDITIVOS Em muitos casos, escrevendo-se cuidadosamente uma gramática, eliminando-se a recursão à esquerda e fatorando-se à esquerda a gramática resultante, pode-se obter uma nova gramática processável por um analisador sintático de descendência recursiva que não necessite de retrocesso, isto é, um analisador sintático preditivo (Aho, 1995). Aho (1995) define o retrocesso em análise sintática como a realização de esquadrinhamentos repetidos na entrada, ou seja, o analisador sintático, caso necessário, pode analisar mais de uma vez uma mesma seqüência de tokens. Um analisador sintático preditivo não necessita de retrocesso. Para construir um analisador sintático preditivo, precisa-se conhecer, dado o símbolo corrente de entrada a e o não-terminal A a ser expandido, qual das alternativas da produção A n é a única que deriva uma cadeia começando por a. Ou seja, a alternativa adequada precisa ser detectável examinando-se apenas para o primeiro símbolo da cadeia que a mesma deriva. As construções de controle de fluxo na maioria das linguagens de programação, com suas palavras-chaves distintivas, são usualmente detectáveis dessa forma (Aho, 1995). Considerando as produções apresentadas no quadro 11, então as palavras-chave if, while e begin informam qual alternativa é a única que possivelmente teria sucesso, caso deseja-se encontrar um comando.

30 15 QUADRO 11 PRODUÇÕES PARA ANALISADOR SINTÁTICO PREDITIVO cmd if expr then cmd else cmd while expr do cmd begin lista_de_comandos end Fonte: Aho (1995) Na prática um analisador gramatical preditivo é um programa que executa um conjunto de procedimentos recursivos para processar a entrada. Um procedimento é associado a cada não-terminal de uma gramática e a análise gramatical começa com uma chamada para o procedimento correspondente ao não-terminal de partida. Cada procedimento decide que produção utilizar através do exame do token atual na entrada (lookahead). Considerando o exemplo do quadro 11, se o lookahead for if a produção a ser utilizada é a primeira, o token if será reconhecido e o lookahead será o próximo token da entrada. Além de decidir qual produção a ser utilizada, o procedimento imita o lado direito da mesma. Um não-terminal da produção corresponde à chamada de um procedimento para o nãoterminal e um token da produção se igualando ao símbolo lookahead resulta na leitura do próximo token da entrada. Se, em algum ponto, o token na produção não coincidir com o símbolo lookahead, um erro é declarado (Aho, 1995). Quando o lookahead atingir o final da entrada e nenhum erro for declarado, a entrada é reconhecida, ou seja, a entrada pode ser gerada pela gramática ANÁLISE SEMÂNTICA A análise semântica é responsável pela captação do sentido do programa fonte, uma operação essencial para a realização da tradução do mesmo. Esta terceira etapa da compilação é feita através das atividades de análise semântica, ou ações semânticas. Nos compiladores dirigidos por sintaxe a execução das ações semânticas é ativada sempre que forem atingidos certos estados do reconhecimento, ou sempre que determinadas transições ocorrerem durante a análise do programa fonte (José, 1987). Segundo José (1987), as ações semânticas encarregam-se, em geral, de todas as tarefas do compilador que não sejam as análises léxica e sintática, sendo sua execução, normalmente,

31 16 proida por iniciativa do analisar sintático, em compiladores dirigidos por sintaxe. Tipicamente, as ações semânticas englobam funções tais como: a) criação e manutenção de tabelas de símbolos; b) associar aos símbolos os correspondentes atributos; c) manter informações sobre o escopo dos identificadores; d) representar tipos de dados; e) analisar restrições quanto à utilização dos identificadores; f) verificar o escopo dos identificadores; g) verificar a compatibilidade de tipos; h) efetuar o gerenciamento de memória; i) representar o ambiente de execução dos procedimentos; j) efetuar a tradução do programa; k) implementação de um ambiente de execução; l) comunicação entre dois ambientes de execução; m) geração de código; n) otimização TRADUÇÃO DIRIGIDA PELA SINTAXE Tradução dirigida por sintaxe é uma técnica que permite realizar tradução (geração de código) concomitantemente com a análise sintática. Ações semânticas são associadas às regras de produção da gramática de modo que, quando uma dada produção é processada (por derivação ou redução de uma forma sentencial no processo de reconhecimento), essas ações são executadas. A execução dessas ações pode gerar ou interpretar código, armazenar informações na tabela de símbolos, emitir mensagens de erro etc. (Price, 2001). Aho (1995) apresenta duas notações para associar regras semânticas às produções da gramática: definições dirigidas pela sintaxe e esquemas de tradução DEFINIÇÕES DIRIGIDAS PELA SINTAXE Segundo Aho (1995), uma definição dirigida pela sintaxe usa uma gramática livre de contexto para especificar a estrutura sintática da entrada. A cada símbolo da gramática é associado um conjunto de atributos, e a cada produção é associado um conjunto de regras

32 17 semânticas para computar os valores dos atributos associados aos símbolos que figuram naquela produção. A gramática e o conjunto de regras semânticas constituem a definição dirigida pela sintaxe. Um atributo pode representar qualquer informação necessária para a tradução: uma cadeia de caracteres; um número; um tipo; uma localização de memória etc. Os atributos podem ser sintetizados ou herdados ESQUEMAS DE TRADUÇÃO Conforme apresentado em Aho (1995), um esquema de tradução é uma gramática livre de contexto na qual os atributos são associados aos símbolos gramaticais e as ações semânticas (envolvidas entre chaves) são inseridas nos lados direitos das produções. Um esquema de tradução é como uma definição dirigida pela sintaxe, exceto que a ordem de avaliação das regras semânticas é explicitamente mostrada. Por exemplo, no esquema de tradução resto + termo { imprimir( + ) } resto 1, a ação semântica { imprimir( + ) } será realizada depois que a sub-árvore para o termo seja percorrida, mas antes que o filho para resto 1 seja visitado GRAMÁTICA DE ATRIBUTOS As ações semânticas podem produzir efeitos colaterais (side effects) tais como imprimir um valor, armazenar um literal em uma tabela ou em um arquivo, atualizar uma variável global etc. Segundo Price (2001), quando as ações semânticas não produzem efeitos colaterais tem-se uma gramática de atributos. Neste caso, as ações semânticas são atribuições ou funções envolvendo, unicamente, os atributos da definição ATRIBUTOS SINTETIZADOS Um atributo sintetizado é aquele cujo valor é computado a partir dos valores dos atributos filhos daquele nó na árvore gramatical (Aho, 1995). Aho (1995) afirma que atributos sintetizados são usados extensivamente na prática e uma definição dirigida pela sintaxe que utiliza exclusivamente atributos sintetizados é dita uma definição S-atribuída. No quadro 12 é apresentada uma definição S-atribuída que especifica uma calculadora de mesa.

33 18 QUADRO 12 - DEFINIÇÃO S-ATRIBUÍDA DE UMA CALCULADORA Produção L E n E E 1 + T E T T T 1 * F T F F (E) F dígito Regras Semânticas Imprimir(E.val) E.val := E 1.val + T.val E.val := T.val T.val := T 1.val * F.val T.val := F.val F.val := E.val F.val := dígito.lexval Fonte: Aho (1995) A calculadora lê uma linha de entrada contendo uma expressão aritmética, envolvendo dígitos, parênteses, operadores + e * e um caractere de avanço de linha n ao fim, e imprime o valor da expressão ATRIBUTOS HERDADOS Um atributo herdado tem seu valor computado a partir dos valores dos atributos dos irmãos e pai daquele nó (Aho, 1995). Os atributos herdados são convenientes para expressar a dependência de uma construção de linguagem de programação no contexto em que a mesma figurar. Por exemplo, no quadro 13 é mostrada uma definição dirigida pela sintaxe onde é utilizado um atributo herdado L.in para determinar o tipo dos identificadores de uma lista, que é lida após o token que determina o tipo. QUADRO 13 - DEFINIÇÃO UTILIZANDO UM ATRIBUTO HERDADO Produção D T L T int T real L L 1, id L id Regras Semânticas L.in := T.tipo T.tipo := inteiro T.tipo := real L 1.in := L.in incluir_tipo(id, entrada, L.in) incluir_tipo(id, entrada, L.in) Fonte: Aho (1995) O não-terminal T possui um atributo sintetizado tipo, cujo valor é determinado pela palavra-chave na declaração. A regra semântica L.in := T.tipo, associada à produção D T L, faz o atributo herdado L.in igual ao tipo na declaração. As regras então propagam esse tipo pela

34 19 árvore gramatical abaixo, usando o atributo herdado L.in. As regras associadas às produções para L chamam o procedimento incluir_tipo para incluir o tipo de cada identificador na sua entrada respectiva na tabela símbolos (Aho, 1995). Price (2001) diz que o uso de atributos herdados, infelizmente, pode fazer com que um esquema de tradução deixe de poder ser implementando num único passo. Para um esquema de tradução com atributos herdados poder ser implementado num único passo (juntamente com um analisador top-down), é necessário que as ações semânticas satisfaçam algumas restrições. Um esquema de tradução que satisfaça essas restrições é um esquema L-atribuído DEFINIÇÕES L-ATRIBUÍDAS Na tradução dirigida pela sintaxe a ordem de avaliação dos atributos é ligada à ordem na qual os nós da árvore gramatical são criados pelo método de análise gramatical. Uma ordem natural (que caracteriza muitos métodos de tradução top-down e bottom-up) é a ordem de pesquisa em profundidade (Aho, 1995). Definições L-atribuídas é uma classe de definições dirigidas pela sintaxe onde os atributos podem, sempre, ser avaliados numa ordem de pesquisa em profundidade. As definições L-atribuídas incluem todas as definições dirigidas pela sintaxe baseadas nas gramáticas LL(1) (Aho, 1995). Uma definição dirigida pela sintaxe é L-atribuída se cada atributo herdado de X j, 1 n, do lado direito de A X 1, X 2,...,X n depender somente: a) dos atributos dos símbolos X 1, X 2,..., X j-1 à esquerda de X j na produção; b) dos atributos herdados de A. j Price (2001) resume estas restrições numa única regra: para cada símbolo X no lado direito de uma regra de produção, a ação que calcula um atributo herdado de X deve aparecer à esquerda de X. Por exemplo, a primeira regra mostrada no quadro 13 faz com que o esquema de tradução deixe de ser L-atribuído. Considerando que o esquema de tradução para a primeira regra do quadro 13 seja o apresentado no quadro 14, para satisfazer a restrição, a primeira regra teria que ser a mostrada como no quadro 15.

35 20 QUADRO 14 ESQUEMA DE TRADUÇÃO NÃO L-ATRIBUÍDO D T L { L.in := T.tipo } Fonte: adaptado de Price (2001) QUADRO 15 ESQUEMA DE TRADUÇÃO L-ATRIBUÍDO D T { L.in := T.tipo } L TRADUÇÃO TOP-DOWN As seções subseqüentes abordam a implementação de esquemas de tradução que atuam durante a análise descendente (top-down) e um algoritmo para a implementação de um tradutor preditivo ELIMINAÇÃO DE RECURSÃO À ESQUERDA DE UM ESQUEMA DE TRADUÇÃO Os analisadores top-down não admitem recursividade à esquerda. Nesta seção será apresentado como um esquema de tradução com recursividade à esquerda e atributos sintetizados pode ser adaptado para a análise top-down. A transformação de regras de produção obriga a fazer transformações nas ações semânticas associadas às regras. Serão utilizados esquemas de tradução para tornar explícita a ordem na qual as ações e as avaliações de atributos ocorrem. Fonte: adaptado de Price (2001) O quadro 16 apresenta um esquema de tradução onde cada símbolo gramatical possui um atributo sintetizado, escrito usando a letra minúscula correspondente e f e g são funções arbitrárias. Aplicando o algoritmo para eliminar a recursão à esquerda (apresentado na seção ) obtém-se a gramática do quadro 17.

36 21 QUADRO 16 - ESQUEMA DE TRADUÇÃO COM RECURSÃO À ESQUERDA (GENÉRICO) A A 1 Y { A.a := g(a 1.a, Y.y) } A X { A.a := f(x.x) } Fonte: Aho (1995) QUADRO 17 - GRAMÁTICA SEM RECURSÃO À ESQUERDA (GENÉRICA) A R X R Y R ε Levando em conta as transformações das ações semânticas, tem-se no quadro 18 o novo esquema de tradução. Fonte: Aho (1995) QUADRO 18 - ESQUEMA DE TRADUÇÃO SEM RECURSÃO À ESQUERDA (GENÉRICO) A X { R.i := f(x.x) } R { A.a := R.s } R Y { R 1.i := g(r.i, Y.y) } R 1 { R.s := R 1.s } R ε { R.s := R.i } Fonte: Aho (1995) A gramática de atributos apresentada no quadro 19, recursiva à esquerda, reconhece seqüências de dígitos e obtém o somatório desses dígitos no atributo val do símbolo raiz. A transformação da gramática para eliminar a recursividade à esquerda implica num novo conjunto de regras mostrados no quadro 20.

37 22 QUADRO 19 - ESQUEMA DE TRADUÇÃO COM RECURSÃO À ESQUERDA A A 1 dígito { A.val := A 1.val + dígito.lexval } A dígito { A.val := dígito.lexval } Fonte: adaptado de Price (2001) QUADRO 20 - ESQUEMA DE TRADUÇÃO SEM RECURSÃO À ESQUERDA A dígito { X.h := dígito.lexval } X { A.val := X.s } X dígito { X 1.h := X.h + dígito.lexval } X 1 { X.s := X 1.s } X ε { X.s := X.h } Fonte: adaptado de Price (2001) PROJETO DE TRADUTOR PREDITIVO Um tradutor preditivo é um programa que possui um procedimento para cada não terminal. Cada procedimento decide que produção usar e implementa o lado direito das produções. O algoritmo descrito nos próximos parágrafos generaliza a construção de analisadores sintáticos preditivos para implementar um esquema de tradução baseado numa gramática adequada à análise sintática top-down. Mais detalhes e exemplos de implementação deste algoritmo podem ser vistos em Aho (1995). Para cada não-terminal A, deve-se construir uma função que tenha um parâmetro formal para cada atributo herdado de A e que retorne os valores dos atributos sintetizados de A. Por simplicidade assume-se que cada não-terminal tenha exatamente um atributo sintetizado. A função para A possui uma variável local para cada atributo de cada símbolo gramatical que figure numa produção para A. O código para o não-terminal A decide que produção usar, baseado no símbolo corrente de entrada, conforme explicado na seção O código associado a cada produção realiza as seguintes operações: a) para o token X com atributo sintetizado x, salvar o valor de x na variável declarada para X.x. Em seguida, gerar uma chamada para reconhecer o token X e avançar a entrada;

38 23 b) para o não-terminal B, gerar uma atribuição c := B(b 1, b 2,..., b k ) com uma chamada de função no lado direito, onde b 1, b 2,..., b k são as variáveis para os atributos herdados de B e c é a variável para o atributo sintetizado de B; c) para uma ação, copiar o código dentro do analisador sintático, substituindo cada referência a um atributo pela variável para aquele atributo GERAÇÃO DE CÓDIGO INTERMEDIÁRIO Segundo Price (2001), a geração de código intermediário é a transformação da árvore de derivação em um segmento de código. Esse código pode, eventualmente, ser o código objeto final, mas na maioria das vezes, constitui-se num código intermediário, pois a tradução de código fonte para objeto em mais de um passo apresenta algumas vantagens: a) possibilita a otimização do código intermediário, de modo a obter-se o código objeto final mais eficiente; b) simplifica a implementação do compilador, resolvendo, gradativamente, as dificuldades da passagem de código fonte para objeto (alto-nível para baixo-nível), já que o código fonte pode ser visto como um texto condensado que explode em inúmeras instruções elementares de baixo nível; c) possibilita a tradução de código intermediário para diversas máquinas. A desvantagem de gerar código intermediário é que o compilador requer um passo a mais. A tradução direta do código fonte para objeto leva a uma compilação mais rápida. A grande diferença entre o código intermediário e o código objeto final é que o intermediário não especifica detalhes da máquina alvo, tais como quais registradores serão usados, quais endereços de memória serão referenciados etc LINGUAGENS INTERMEDIÁRIAS Price (2001) apresenta três categorias, nos quais os vários tipos de código intermediário se encaixam: a) representações gráficas: árvore e grafo de sintaxe; b) notação pós-fixada ou pré-fixada; c) código de três endereços: quádruplas e triplas.

39 24 O código intermediário gerado pelo protótipo especificado neste trabalho é o código de três endereços CÓDIGO DE TRÊS ENDEREÇOS Conforme Price (2001), no código intermediário de três endereços, cada instrução faz referência, no máximo, a três variáveis (endereços de memória). As instruções dessa linguagem intermediária são as mostradas no quadro 21. QUADRO 21 INSTRUÇÕES DO CÓDIGO DE TRÊS ENDEREÇOS A := B op C A := op B A := B goto L if A oprel B goto L Fonte: Price (2001) No quadro 21, A, B e C representam endereços de variáveis, op representa operador (binário ou unário), oprel representa operador relacional, e L representa o rótulo de uma instrução intermediária. O comando de atribuição A := X + Y * Z traduzido para o código de três endereços é o apresentado no quadro 22, onde T1 e T2 são variáveis temporárias. QUADRO 22 CÓDIGO DE TRÊS ENDEREÇOS PARA COMANDO DE ATRIBUIÇÃO T1 := Y * Z T2 := X + T1 A := T2 Fonte: Price (2001) Um código de três endereços pode ser implementado através de quádruplas ou triplas. As quádruplas são constituídas de quatro campos: um operador; dois operandos e o resultado. O quadro 23 mostra a representação do comando A := B * (- C + D) em quádruplas.

40 25 QUADRO 23 - REPRESENTAÇÃO EM QUÁDRUPLAS operador argumento 1 argumento 2 resultado (0) - C T1 (1) + T1 D T2 (2) * B T2 T3 (3) := T3 A Fonte: adaptado de Price (2001) As triplas são formadas por: um operador e dois operandos. A representação do mesmo comando através de triplas é mostrada no quadro 24. Essa representação utiliza ponteiros para a própria estrutura, evitando a nomeação explícita de temporários. QUADRO 24 - REPRESENTAÇÃO EM TRIPLAS operador argumento 1 argumento 2 (0) - C (1) + (0) D (2) * B (1) (3) := A (2) Fonte: adaptado de Price (2001) Neste trabalho será utilizada a representação em quádruplas já que a saída do código intermediário é textual, garantindo uma melhor legibilidade TRADUÇÃO DIRIGIDA PELA SINTAXE EM CÓDIGO DE 3 ENDEREÇOS Quando o código de três endereços é gerado, os nomes temporários são construídos para os nós interiores da árvore sintática. O valor do não terminal E ao lado esquerdo de E E 1 + E 2 será computado numa nova variável temporária t. Em geral, o código de três endereços para id := E consiste em código para avaliar E em alguma variável temporária t, seguido pela atribuição id.local := t. Se uma expressão se constituir em um único identificador, diga-se y, o próprio y abrigará o valor da expressão (Aho, 1995). A definição S-atribuída no quadro 25 gera código de três endereços para enunciados de atribuição. Dada a entrada a := b * - c + b * - c, a definição produz o código do quadro 26. O

41 26 atributo sintetizado S.código representa o código de três endereços para a atribuição S. O nãoterminal E possui dois atributos: a) E.local, o nome que irá abrigar o valor de E; b) E.código, a seqüência de enunciados de três endereços avaliando E. A função novo_temporário retorna uma seqüência de nomes distintos (t 1, t 2,..., t n ) em resposta às sucessivas chamadas. QUADRO 25 DEFINIÇÃO DIRIGIDA PELA SINTAXE PARA GERAR CÓDIGO DE TRÊS ENDEREÇOS PARA ATRIBUIÇÕES Produção S id := E E E 1 + E 2 E E 1 * E 2 E - E 1 E (E 1 ) Regras Semânticas S.código := E.código gerar(id.local := E.local) E.local := novo_temporário E.código := E 1.código E 2.código gerar(e.local := E 1.local + E 2.local) E.local := novo_temporário E.código := E 1.código E 2.código gerar(e.local := E 1.local * E 2.local) E.local := novo_temporário E.código := E 1.código gerar(e.local := uminus E 1.local) E.local := E 1.local; E.código := E 1.código E id E.local := id.local; E.código := Fonte: Aho (1995)

42 27 QUADRO 26 - CÓDIGO DE TRÊS ENDEREÇOS PRODUZIDO PELA DEFINIÇÃO NO QUADRO 25 t 1 := - c t 2 := b * t 1 t 3 := - c t 4 := b * t 3 t 5 := t 2 + t 4 a := t 5 Fonte: Aho (1995) RETROCORREÇÃO Segundo Aho (1995), a forma mais fácil de se implementar as definições dirigidas pela sintaxe é usar duas passagens. Primeiro constrói-se uma árvore sintática para a entrada e, em seguida, caminha-se na árvore numa ordem de busca em profundidade, computando as traduções dadas na definição. O principal problema de se gerar código para as expressões booleanas e para o fluxo de controle numa única passagem está em que, durante a mesma, não se pode conhecer os rótulos para onde o controle deve ir, no tempo em que os enunciados de desvio são gerados. Pode-se solucionar esse problema pela geração de uma série de enunciados de ramificação, com os alvos dos desvios deixados temporariamente inespecificados. Cada enunciado será colocado numa lista de enunciados de desvio cujos rótulos serão preenchidos quando o rótulo apropriado puder ser determinado. Aho (1995) define esse preenchimento subseqüente de rótulos de retrocorreção, do inglês backpatching GERAÇÃO DE CÓDIGO Conforme Aho (1995), a geração de código é a fase final de um compilador. Recebe como entrada a representação intermediária do programa-fonte e produz como saída um programa-alvo equivalente. As exigências impostas a um gerador de código são severas. O código de saída precisa ser correto e de alta qualidade, significando que o mesmo deve tornar efetivo o uso dos recursos da máquina-alvo ENTRADA PARA O GERADOR DE CÓDIGO A entrada para o gerador de código consiste na representação intermediária do programa-fonte, produzida pelas etapas anteriores ao gerador de código. Conforme visto na

43 28 seção , existem várias escolhas para as linguagens intermediárias. Neste trabalho será utilizado o código de três endereços PROGRAMA-ALVO Segundo Aho (1995), a saída do gerador de código é o programa alvo. Como o código intermediário, essa saída pode assumir uma variedade de formas como linguagem absoluta de máquina, linguagem relocável de máquina ou linguagem de montagem. A saída do gerador de código do protótipo deste trabalho será em linguagem de montagem Assembly GERENCIAMENTO DE MEMÓRIA O mapeamento dos nomes no programa-fonte para os endereços dos objetos de dados em tempo de execução é feito cooperativamente pela vanguarda e pelo gerador de código. Por exemplo, o tipo numa declaração determina a largura, isto é, a quantidade de memória necessitada para o nome declarado. A partir desta e de outras informações geradas pela vanguarda do compilador é possível determinar um endereço relativo para o nome na área de dados do programa (Aho, 1995) SELEÇÃO DE INSTRUÇÕES A natureza do conjunto de instruções da máquina-alvo determina a dificuldade da seleção das instruções. A uniformidade e completeza do conjunto de instruções são fatores importantes. Se a máquina alvo suporta cada tipo de dado de uma maneira uniforme, cada exceção à regra geral requer um tratamento especial (Aho, 1995). Decidir que seqüência de código de máquina é a melhor para uma dada construção de três endereços requer o conhecimento de informações acuradas sobre o comportamento das instruções e do contexto no qual a instrução aparece. O problema de gerar código ótimo é insolúvel (indecidível) como tantos outros. Na prática, deve-se contentar com técnicas heurísticas que geram bom código (não necessariamente ótimo) (Price, 2001).

44 ALOCAÇÃO DE REGISTRADORES Aho (1995) afirma que instruções que envolvem operadores do tipo registrador são usualmente mais curtas do que aquelas que envolvem operandos na memória. A utilização eficiente dos registradores é algo importante na geração de código de boa qualidade. A atribuição ótima de registradores a variáveis é muito difícil, e muito problemática quando a máquina trabalha com registradores aos pares (para instruções de divisão e multiplicação), ou provê registradores específicos para endereçamento e para dados (Price, 2001). O problema do uso otimizado de registradores fica simples quando a máquina possui um único registrador para realizar operações aritméticas (por exemplo, um acumulador). O problema deixa de existir quando as operações aritméticas são realizadas sobre uma pilha. Neste caso, deixa de ser necessária, inclusive, a utilização de variáveis temporárias (Price, 2001) MÁQUINA-ALVO Uma familiaridade com a máquina-alvo e seu conjunto de instruções é um pré-requisito para o projeto de um bom gerador de código (Aho, 1995). Neste trabalho será adotada como máquina-alvo o microprocessador 8088 e compatíveis. Detalhes sobre sua arquitetura e instruções suportadas são discutidas na seção ALGORITMO SIMPLES DE GERAÇÃO DE CÓDIGO O algoritmo de geração de código descrito em Aho (1995), usa descritores para controlar o conteúdo dos registradores e dos endereços para os nomes. Um descritor de registradores controla o que está correntemente em cada registrador. É consultado sempre que um novo registrador é necessitado. Assume-se que inicialmente o descritor de registradores informe que todos os registradores estão vazios. À medida que a geração de código progride, cada registrador irá abrigar o valor de zero ou mais nomes a um dado instante. Já o descritor de endereços controla a localização (ou localizações) onde o valor corrente de um nome pode ser encontrado em tempo de execução. A localização poderia ser um

45 30 registrador, uma localização na pilha, um endereço de memória ou algum conjunto desses elementos, já que, uma vez copiado, o valor também permanece onde estava originalmente. O algoritmo a seguir toma como entrada uma seqüência de enunciados de três endereços. Este algoritmo utiliza uma função chamada obter_reg que retorna uma localização L para armazenar o valor de uma atribuição, onde L pode ser um registrador ou um endereço na memória. Para cada enunciado de três endereços da forma x := y op z, realiza-se as ações descritas no quadro 27. QUADRO 27 - ALGORITMO SIMPLES DE GERAÇÃO DE CÓDIGO 1. invoca-se a função obter_reg para determinar a localização L onde o resultado do cômputo y op z deverá ficar armazenado. L usualmente será um registrador, mas poderia ser também uma localização de memória; 2. consulta-se o endereço do descritor para y de forma a determinar y, a localização corrente de y (ou uma das suas localizações). Preferir para y o registrador, caso o seu valor esteja tanto na memória quanto num registrador. Se o valor de y ainda não estiver em L, gerar a instrução MOV y,l a fim de colocar uma cópia de y em L; 3. gerar a instrução OP Z,L onde z é a localização corrente de z. De novo, preferir um registrador em vez de uma localização de memória, se z estiver em ambas. Atualizar o descritor de endereço de x para indicar que x está na localização L. Se L for um registrador, atualizar o descritor para indicar que o mesmo contém o valor de x e reer x de todos os demais descritores de registradores; 4. se os valores correntes de y e/ou z não possuírem usos subseqüentes, não estão, por conseguinte, vivos à saída do bloco, e estão em registradores; alterar, nesse caso, o descritor de registradores para indicar que, após a execução de x := y op z, aqueles registradores não irão conter y e/ou z, respectivamente. Fonte: Aho (1995) 2.2 CONCEITOS DE LINGUAGENS DE PROGRAMAÇÃO Nesta seção serão apresentados alguns conceitos básicos no que diz respeito a linguagens de programação.

46 UNIDADES DE PROGRAMA Segundo Ghezzi (1991), unidades de programa são agrupamentos de instruções implementando uma abstração de ação. Linguagens de programação permitem que programas sejam compostos de unidades. Estas unidades podem ser desenvolvidas até certo ponto independentemente e podem, algumas vezes, serem traduzidas em separado e combinadas depois da tradução. Alguns exemplos conhecidos de unidades de programa são: a) subprogramas em linguagens de montagem; b) sub-rotinas em FORTRAN; c) procedimentos e blocos em ALGOL PROCEDIMENTOS Aho (1995) define procedimento como uma declaração que, na sua forma mais simples, associa um identificador a um enunciado. O identificador é o nome do procedimento e o enunciado é o corpo do procedimento. Quando o nome de um procedimento aparece dentro de um enunciado executável, dize-se que o procedimento é chamado àquele ponto. A idéia básica está em que uma chamada de procedimento execute o corpo do mesmo. Alguns dos identificadores que aparece numa definição de um procedimento são especiais e são chamados de parâmetros formais do procedimento. Argumentos, conhecidos como parâmetros atuais (ou reais), podem ser transmitidos ao procedimento chamado, substituindo os formais no corpo do procedimento. Em muitas linguagens, os procedimentos podem retornar valores e são conhecidos como funções REGISTROS DE ATIVAÇÃO Segundo Aho (1995), as informações necessitadas para uma única execução de um procedimento são gerenciadas utilizando-se um único bloco contíguo de armazenamento chamado de registro de ativação ou moldura, consistindo em uma coleção de campos. Nem todas as linguagens, nem todos os compiladores, usam esses campos. Freqüentemente os registradores do processador podem tomar o lugar de um ou mais campos. Para linguagens como Pascal e C, é usual empilhar o registro de ativação do procedimento na pilha em tempo de

47 32 execução quando o procedimento é chamado, e reê-lo da mesma quando o controle retornar ao chamador. No registro de ativação são armazenados: a) valores temporários, como aqueles vindos de avaliação de expressões; b) dados locais, como as variáveis declaradas dentro daquele procedimento; c) estado da máquina, como valores de registradores do processador antes do procedimento ser chamado; d) elo de acesso, o qual é usado para referir aos dados locais abrigados em outros registros de ativação; e) elo de controle, o qual aponta para o registro de ativação do chamador; f) parâmetros atuais, usado pelo procedimento chamador para fornecer os parâmetros do procedimento chamado; g) valor retornado, que é usado pelo procedimento chamado para retornar um valor para o procedimento chamador. O tamanho de cada um desses campos pode ser determinado em tempo de execução, na chamada do procedimento. Na prática, os tamanhos de quase todos os campos são determinados em tempo de compilação (Aho, 1995) ESCOPO A parte do programa à qual uma declaração se aplica é chamada de escopo daquela declaração. Uma ocorrência de um nome dentro de um procedimento é dita local ao mesmo se estiver no escopo de uma declaração dentro daquele procedimento; caso contrário, a ocorrência é não-local. A distinção entre nomes locais e não locais recai sobre qualquer construção sintática que possa ter declarações dentro de si (Aho, 1995). Cada procedimento possui uma tabela que contém todas as variáveis e procedimentos declarados dentro deste procedimento. Para cada nome local é criada uma entrada na tabela de símbolos. Esta entrada contém o tipo e o endereço relativo da memória para aquele nome. O endereço relativo consiste em um deslocamento a partir da base estática de dados ou do campo para os dados locais no registro de ativação. Um procedimento pode utilizar objetos definidos internamente no procedimento de programa ou definidos externamente, desde que sejam visíveis daquele procedimento. O bloco

48 33 de um procedimento pode conter chamadas para outros procedimentos. Em linguagens bloco estruturadas um procedimento pode conter chamadas para (Silva, 2000): a) ele próprio (chamada recursiva); b) seus irmãos; c) seus filhos; d) seus ancestrais (pais, avós,...); e) irmãos de seus ancestrais. 2.3 CONCORRÊNCIA A concorrência é naturalmente dividida em nível de instrução, nível de programa, nível de comando e em nível de unidade. A concorrência em nível de instrução não traz nenhuma ligação com linguagens de programação, ela ocorre quando dois ou mais processadores executam cada um uma instrução distinta, independentemente. Já a concorrência em nível de programa é responsabilidade do sistema operacional em que este é executado, também não tendo relação alguma com a linguagem de programação utilizada. A concorrência em nível de comando existe quando dois ou mais processadores executam comandos da linguagem simultaneamente. A linguagem High Performance Fortran possui construções para este tipo de concorrência que é utilizada com o objetivo de otimizar o programa. No nível de unidades, a concorrência pode ocorrer fisicamente em processadores separados (concorrência física) ou logicamente usando alguma forma de tempo fatiado em um único processador (concorrência lógica) (Sebesta, 2000). Para o protótipo deste trabalho será implementada a concorrência em nível de unidade CONCORRÊNCIA FÍSICA Sebesta (2000) define concorrência física como a categoria mais geral de concorrência, onde diversas unidades do mesmo programa literalmente executam simultaneamente, supondo que mais de um processador esteja disponível CONCORRÊNCIA LÓGICA Concorrência lógica, segundo Sebesta (2000), é quando a execução real dos processos concorrentes está desenvolvendo-se intercaladamente em um único processador. Do ponto de

49 34 vista do programador a concorrência lógica é o mesmo que física. Cabe ao implementador da linguagem fazer a correspondência da lógica com o hardware subjacente. No restante do trabalho, quando forem feitas referencias à concorrência, referir-se-á concorrência lógica THREADS DE CONTROLE Conforme Sebesta (2000), um thread de controle em um programa é a seqüência de pontos do programa atingidos à medida que o controle flui por ele. Os programas executados com concorrência física podem ter múltiplos threads de controle. Cada processador pode executar um dos threads. Ainda que a execução de programas logicamente concorrentes possa ter de fato um único thread de controle, eles somente podem ser projetados e analisados imaginando a existência de múltiplos threads de controle. Quando um programa multithreaded é executado em uma máquina com um único processador, seus threads são mapeados a uma única thread. Ele se torna, nesse caso, virtualmente um programa multithreaded (Sebesta, 2000). Sebesta (2000) define um programa multithreaded como aquele que tem dois ou mais threads CONCORRÊNCIA NO NÍVEL DE SUBPROGRAMA Nesta seção serão abordados conceitos sobre concorrência e as exigências para que ela seja útil TAREFAS Uma tarefa é uma unidade de programa que pode estar em execução concorrente com outras unidades do mesmo programa. Cada tarefa de um programa pode oferecer um thread de controle (Sebesta, 2000). Uma tarefa se diferencia de um subprograma porque pode: a) ser implicitamente iniciada, enquanto um subprograma deve ser chamado explicitamente; b) executar outras tarefas sem ter que esperar elas terminarem para continuar por si própria;

50 35 c) devolver ou não o controle para a unidade que iniciou quando ela for concluída ESCALONAMENTO Independentemente do número de processadores em uma máquina, sempre há possibilidade de haver mais tarefas do que processadores. O scheduler (escalonador) é uma rotina que gerencia o compartilhamento de processadores entre as tarefas. Basicamente ele dá uma fatia de tempo do processador para cada tarefa em execução. Só que existem diversos fatores que complicam isso, como retardamentos de sincronização e tempos de espera de operações de entrada e saída. Para resolver isto, as tarefas possuem estados: nova; pronta; rodando ou executando; bloqueada e morta. Uma tarefa está marcada como nova porque acabou de ser criada, mas ainda não iniciou sua execução. As tarefas marcadas como pronta estão prontas para rodar: ou ela ainda não recebeu sua fatia de tempo; ou já foi rodada e bloqueada. As tarefas prontas ficam numa fila chamada fila de prontas. Quando uma tarefa está em execução no processador, ela está marcada como rodando ou executando. Toda vez que uma tarefa que está em execução requisita alguma operação de entrada e saída ou qualquer outro serviço do sistema operacional, sua execução é interrompida e ela é marcada como bloqueada, permanecendo neste estado até que a operação seja completada. Uma vez que a tarefa terminou sua execução, ela é marcada como morta. Algumas linguagens podem terminar uma tarefa através de um pedido explícito no programa, como o método stop no Java ALGORITMO DE ROUND-ROBIN Conforme Sebesta (2000), uma questão importante na execução de tarefas é como uma tarefa pronta é escolhida para mudar para o estado rodando quando a tarefa atualmente em execução foi bloqueada ou sua fatia de tempo terminada. Diversos algoritmos diferentes têm sido usados para tal escolha, alguns baseados em níveis de prioridade especificáveis. O algoritmo em questão deve ser implementado no escalonador. O algoritmo escolhido para

51 36 implementar no escalonador de tarefas dos programas gerados pelo ambiente FURBOL é baseado no algoritmo de Round-Robin. Segundo Arruda (2001), Round-Robin é um dos mais antigos e simples algoritmos de escalonamento. É largamente usado e foi projetado especialmente para sistemas de compartilhamento de tempo no processador. A idéia do algoritmo é a seguinte. Uma pequena unidade de tempo, denominada timeslice ou quantum, é definida. Todas tarefas são armazenadas em uma fila circular. O escalonador percorre a fila, alocando o processador para cada processo durante um quantum. Mais precisamente, o escalonador retira a primeira tarefa da fila e procede à sua execução. Se a tarefa não termina após um quantum, ocorre uma preempção, e a tarefa é inserida no fim da fila. Se a tarefa termina antes de um quantum, o processador é liberado para a execução de novos processos. Em ambos os casos, após a liberação do processador, uma nova tarefa é escolhida na fila. Novas tarefas são inseridas no fim da fila. Quando uma tarefa é retirada da fila para o processador, ocorre uma troca de contexto, o que resulta em um tempo adicional na execução da tarefa VIVÊNCIA DE TAREFAS Uma tarefa tem a característica de vivência (liveness) quando está executando e ao final é concluída. Num ambiente onde se têm recursos compartilhados, a vivência de uma tarefa pode deixar de existir quando não pode mais prosseguir, ou seja, a tarefa não finalizará. Isto ocorre quando, por exemplo, as tarefas A e B precisam dos recursos X e Y para concluir seu trabalho. Então A ganha a posse de X e B ganha a de Y. Depois a tarefa A precisa do recurso Y para prosseguir, então solicita Y, mas deve esperar que B libere Y. Enquanto A fica esperando a liberação de Y, B solicita X devendo esperar A liberá-lo. Assim como nenhuma das tarefas libera o recurso que possui, a execução do programa nunca se completará. Esse tipo de perda de vivência é chamado de deadlock (Sebesta, 2000) SINCRONIZAÇÃO E INTERAÇÃO Normalmente, num programa, as tarefas trabalham juntas para criar simulações do mundo real e para resolver problemas dos mais variados tipos. Para isto as tarefas precisam interagir entre si. Guezzi (1991) define a interação entre tarefas como o fato das tarefas terem

52 37 acesso a objetos compartilhados. Esta interação resulta da necessidade das tarefas em atingir um objetivo em comum ou da necessidade de compartilhar algum recurso que deve ser usado por apenas uma única tarefa num determinado instante, como, por exemplo, o acesso a um arquivo. Assim, conforme Sebesta (2000), as tarefas precisam de alguma forma sincronizar suas execuções. Sebesta (2000) define sincronização como um mecanismo que controla a ordem de execução das tarefas. Existem dois tipos de sincronização de tarefas: a sincronização de cooperação e a sincronização de competição SINCRONIZAÇÃO DE COOPERAÇÃO Sincronização de cooperação é quando duas ou mais tarefas trabalham juntas para atingir um objetivo em comum. Por exemplo, tendo duas tarefas A e B, quando a tarefa A precisa aguardar que a tarefa B conclua alguma atividade específica antes que ela prossiga sua execução, é necessária uma sincronização de cooperação (Sebesta, 2000). A situação descrita anteriormente pode ser ilustrada por um tipo comum de problema chamado produtor-consumidor, que se originou no desenvolvimento de sistemas operacionais. O problema surge quando uma unidade de programa produz algum valor de dados ou de recurso e outra o usa. Os dados produzidos normalmente são colocados em uma área de memória temporária de armazenamento (buffer) pela unidade produtora e são reidos do buffer pela unidade consumidora. A seqüência de armazenamentos e de remoções deve ser sincronizada. Não deve ser permitido à unidade consumidora pegar dados do buffer se ele estiver vazio. Similarmente, não se deve permitir que a unidade produtora coloque os novos dados no buffer se ele estiver cheio. Segundo Sebesta (2000) este é um problema de sincronização de cooperação SINCRONIZAÇÃO DE COMPETIÇÃO Uma sincronização de competição é necessária entre tarefas quando elas requerem o uso de algum recurso que não pode ser usado simultaneamente. Especificamente, se a tarefa A precisar acessar a localização de um dado compartilhado X enquanto B está acessando X, a tarefa A deve aguardar que a B conclua seu processamento em X, independentemente de qual seja esse processamento.

53 REQUISITOS DE PROJETO Sebesta (2000) afirma que existem alguns requisitos de projeto importantes referentes ao suporte de linguagem para concorrência. Os principais são: a) como a sincronização de cooperação é fornecida; b) como a sincronização de competição é fornecida; c) como as tarefas são criadas; d) como e quando as tarefas iniciam e encerram a execução MECANISMOS DE SINCRONIZAÇÃO Nesta seção serão discutidas três alternativas para preencher os requisitos de sincronização de tarefas: semáforos; monitores e passagem de mensagens SEMÁFOROS Segundo Sebesta (2000), em um esforço para oferecer sincronização de competição pelo acesso mutuamente exclusivo a estruturas de dados compartilhados, Edsger Dijkstra idealizou os semáforos em Os semáforos também podem ser usados para oferecer sincronização de cooperação. Um semáforo é uma estrutura de dados que consiste em um número inteiro (contador) e uma fila que armazena descritores de tarefas. Possui apenas duas operações P e V ou Esperar e Liberar. A operação P decrementa o contador do semáforo, a operação V incrementa. Quando o contador é decrementado até 0, a próxima operação P irá bloquear a tarefa que a chamou, colocando ela na fila de espera do semáforo, mantendo o contador em 0. Quando alguma outra tarefa executar a operação V no semáforo e a fila de espera do mesmo estiver vazia, o contador é incrementado. Caso exista alguma tarefa bloqueada na fila do semáforo, a primeira tarefa da fila é retirada e o controle é passado para ela. Neste caso o contador não é incrementado. Nos quadros 28 e 29 é apresentada uma descrição concisa das operações Esperar e Liberar de um semáforo.

54 39 QUADRO 28 - OPERAÇÃO P (ESPERAR) if contador de umsemaforo > 0 then decremente o contador de umsemaforo else coloque a tarefa chamadora na fila de umsemaforo tente transferir o controle para alguma tarefa pronta Fonte: adaptado de Sebesta (2000) QUADRO 29 - OPERAÇÃO V (LIBERAR) if fila de umsemaforo estiver vazia then incremente o contador de umsemaforo else retire a primeira tarefa bloqueada da fila de umsemaforo e coloque ela na fila de prontas transferindo o controle Fonte: adaptado de Sebesta (2000) Um exemplo simples de sincronização de cooperação utilizando semáforos é a aplicação produtor-consumidor (apresentada na seção ), onde eles são utilizados para controlar a produção e o consumo realizados pelas tarefas. Quando um recurso compartilhado não pode ser acessado simultaneamente por diferentes tarefas, pode-se utilizar um semáforo binário, ou seja, um semáforo em que seu contador interno é inicializado com 1, permitindo apenas uma operação P por vez. Tem-se a sincronização de competição utilizando semáforos. Para oferecer sincronização de cooperação e competição na linguagem FURBOL, serão implementados semáforos MONITORES Segundo Sebesta (2000), o uso de semáforos para fornecer sincronização de cooperação cria um ambiente de programação inseguro. Não é possível verificar estaticamente (no código fonte) a exatidão de seu uso. Por exemplo, no problema produtor-consumidor, deixar uma operação V (liberar) fora de uma das tarefas produtoras ou consumidoras resultaria num deadlock.

55 40 Uma solução para minimizar o problema da insegurança dos semáforos em um ambiente concorrente é encapsular as estruturas de dados compartilhados com suas operações e ocultar suas representações, ou seja, transformar as estruturas de dados compartilhados em tipos de dados abstratos. Em 1971 Edsger Dijkstra sugeriu que todas as operações de sincronização em dados compartilhados fossem reunidas em uma única unidade de programa. Alguns anos depois essas estruturas foram chamadas de monitores (Sebesta, 2000). O Concurrent Pascal é uma linguagem que implementa essa solução. Possui três importantes estruturas que o diferencia: classes; processos (nome do Concurrent Pascal para tarefas) e monitores. Todos os processos são tipos, de modo que eles são definidos em instruções type, conforme o quadro 30. QUADRO 30 - PROCESSOS DO CONCURRENT PASCAL type nome_do_processo = process (parâmetros formais) -- declarações locais corpo do processo -- end Fonte: Sebesta (2000) As definições de processo são simplesmente modelos para os processos reais, uma vez que são tipos. Declarar uma variável para ser de um tipo processo cria o código para ele, mas não faz nada mais. Para causar a alocação de seus dados locais e iniciar sua execução, uma instrução init com parâmetros reais deve ser usada, como por exemplo init nome_da_variável_do_processo (parâmetros reais). Depois da execução da instrução init, o processo permacene no estado rodando ao longo da execução do programa, exceto quando é bloqueado. A forma geral dos monitores do Concurrent Pascal é apresentada no quadro 31.

56 41 QUADRO 31 - MONITORES DO CONCURRENT PASCAL type nome_do_monitor = monitor (parâmetros formais) ---- declarações de variáveis compartilhadas definições de procedimentos locais definições de procedimentos exportados código de inicialização ---- end Fonte: Sebesta (2000) Os procedimentos exportados de um monitor são sintaticamente diferentes dos procedimentos locais somente em termos de que contêm a palavra reservada entry em suas instruções procedure. O comando init também é usado para criar os monitores. Os procedimentos exportados de um monitor podem ser chamados por processos ou por procedimentos em outros monitores. Um dos recursos mais importantes dos monitores é a sincronização de competição implícita em seus dados compartilhados. Assim, o programador não sincroniza acesso exclusivo a dados compartilhados pelo uso de semáforos ou de outros mecanismos. Uma vez que todos os acessos residem no monitor, a sua implementação pode ser feita de modo a garantir acesso sincronizado, simplesmente permitindo apenas um acesso de cada vez. As chamadas a procedimentos exportados pelos monitores são enfileiradas implicitamente se o monitor estiver ocupado no momento da chamada (Sebesta, 2000). Apesar da sincronização de competição ser intrínseca em um monitor, a sincronização de cooperação ainda cabe ao programador. Para este propósito o Concurrent Pascal tem um tipo de dados especial denominado queue (fila), uma forma de semáforo, com duas operações que se relacionam às operações de um semáforo. Uma variável queue armazena processos que estão esperando para usar uma estrutura de dados compartilhada (Sebesta, 2000). A operação delay toma queue como parâmetro. Essa ação serve para colocar o processo que a chama na fila especificada (bloqueando-o) e retirar seus direitos de acesso exclusivo a estruturas de dados do monitor. Desse modo, delay difere da operação P de um semáforo porque delay sempre bloqueia o chamador.

57 42 A operação continue também toma como parâmetro queue. Sua ação é desconectar o processo que a chama do monitor, liberando-o de ser acessado por outros processos. Ao executar um continue a fila é examinada. Se contiver um processo ele será reido e sua execução, anteriormente suspensa por uma operação delay, será reiniciada. A operação V de um semáforo sempre tem algum efeito, enquanto que a continue nada faz se a fila estiver vazia PASSAGEM DE MENSAGENS Conforme Sebesta (2000), a construção de um monitor é um método confiável e seguro para fornecer sincronização de competição para acesso a dados compartilhados em unidades concorrentes que compartilham uma única memória. Entretanto o problema de sincronizar cooperativamente unidades concorrentes utilizando um monitor ainda é responsabilidade do programador. No mecanismo de passagem de mensagens, tanto a sincronização de cooperação como de competição podem ser manipuladas convenientemente. A passagem de mensagens pode ser dos tipos síncrona ou assíncrona. Na passagem de mensagem síncrona, se uma tarefa que recebeu uma mensagem receber outra mensagem de outra tarefa, esta segunda tarefa emissora irá ficar com sua execução suspensa até que a receptora termine o processamento da primeira mensagem recebida. Na passagem assíncrona, quando uma tarefa recebe uma mensagem ela pode reagir imediatamente, sem precisar esperar o término de um processamento de mensagem iniciado por outra tarefa. A linguagem de programação Ada implementa estruturas do tipo tarefa onde a passagem de mensagens é a base de projeto. Uma tarefa pode suspender sua execução em certo ponto, porque está ociosa ou porque precisa de informações de outra unidade antes que possa prosseguir. Nessa situação, considerando duas tarefas A e B, se a tarefa A quiser enviar uma mensagem a B, e se esta estiver disposta a receber, a mensagem poderá ser transmitida. Sebesta (2000) define esta transmissão real como rendezvous. Um rendezvous pode ocorrer somente se tanto o emissor como o receptor quiserem que ele aconteça. A informação da mensagem pode ser transmitida em qualquer ou ambas as direções. A forma das tarefas Ada possui duas partes, uma de especificação e uma de corpo. Ambas com o mesmo nome. Como exemplo de especificação de tarefa Ada, considere o código mostrado no quadro 32.

58 43 QUADRO 32 - EXEMPLO DE ESPECIFICAÇÃO DE TAREFA EM ADA task TAREFA_EXEMPLO is entry ENTRADA_1(ITEM : in INTEGER); end TAREFA_EXEMPLO; Fonte: Sebesta (2000) No corpo de uma tarefa deve-se incluir uma forma sintática de pontos de entrada que corresponda às cláusulas entry na parte de especificação. Esta forma sintática é especificada pela cláusula accept. A referida cláusula é definida como a faixa de instruções que se inicia com a palavra reservada accept e encerra-se com a palavra reservada end. Como corpo da especificação do quadro 32, tem-se o código do quadro 33. QUADRO 33 - CORPO PARA A TAREFA ESPECIFICADA NO QUADRO 32 task body TAREFA_EXEMPLO is begin loop -- laço accept ENTRADA_1(ITEM : in INTEGER) do -- Corpo do accept end ENTRADA_1; end loop; end TAREFA_EXEMPLO; Fonte: adaptado de Sebesta (2000) Se a execução de TAREFA_EXEMPLO iniciar-se e atingir o accept de ENTRADA_1 antes que qualquer outra tarefa envie uma mensagem a este último, TAREFA_EXEMPLO será suspensa. Se outra tarefa enviar uma mensagem a ENTRADA_1 enquanto TAREFA_EXEMPLO estiver suspensa em accept, ocorrerá um rendezvous, e o corpo de accept será executado. Então, por causa do laço, a execução prosseguirá até accept novamente. Se nenhuma tarefa de chamada adicional tiver enviado uma mensagem a ENTRADA_1, a execução será novamente suspensa para esperar pela mensagem seguinte (Sebesta, 2000). As tarefas podem ter qualquer quantidade de entradas. A ordem em que as cláusulas accept associadas aparecem nelas determina a ordem em que as mensagens podem ser aceitas. Se uma tarefa tem mais do que um ponto de entrada (entry) e exige que elas sejam capazes de

59 44 receber mensagens em qualquer ordem, deve ser utilizada a instrução select, conforme exemplificado no quadro 34. QUADRO 34 - EXEMPLO DE USO DA CLÁUSULA SELECT task body TAREFA_EXEMPLO is begin loop -- laço select accept ENTRADA_1(ITEM : in INTEGER) do -- Corpo do accept end ENTRADA_1; or accept ENTRADA_2(ITEM : in INTEGER) do -- Corpo do accept end ENTRADA_1; end select; end loop; end TAREFA_EXEMPLO; Fonte: adaptado de Sebesta (2000) Este modelo de troca de mensagens implementa naturalmente sincronização de competição, pois se o corpo de uma cláusula accept estiver sendo executado, todas as mensagens que chegarem durante o processamento serão enfileiradas e processadas, uma a uma, quando a execução do corpo do accept finalizar. Para sincronização de cooperação na linguagem Ada, cada cláusula accept pode ter uma proteção (guard). Essa proteção é possível utilizando a cláusula when (quadro 35) que pode abrir ou fechar uma entrada dependendo de uma expressão booleana associada. QUADRO 35 - CLÁUSULA WHEN when not CHEIO(BUFFER) => accept DEPOSITA(NOVO_VALOR) do Fonte: Sebesta (2000) Se a expressão booleana da when for atualmente verdadeira, accept será aberta; se a expressão booleana for falsa, a accept será fechada. Uma accept sem proteção é sempre aberta e está disponível para rendezvous; uma fechada não pode ter rendezvous.

60 45 Por exemplo, o código mostrado no quadro 35 é o fragmento de uma implementação produtor-consumidor, mais precisamente, da tarefa que controla o buffer de armazenamento. Caso o buffer estiver cheio (CHEIO(BUFFER) retorna verdadeiro), a tarefa produtora que chamar a entrada DEPOSITA ficará bloqueada até que uma tarefa consumidora retire algum item do buffer. Quando isto ocorrer, a cláusula accept DEPOSITA será aberta (CHEIO(BUFFER) retornará falso) e a tarefa produtora que estava bloqueada no ínicio, será desbloqueada e um item será depositado no buffer. Todo o mecanismo de mensagens detalhado até agora é estritamente síncrono, ou seja, enquanto a tarefa está processando uma mensagem, todas as outras tarefas que enviarem mensagens à primeira, serão bloqueadas até o término do processamento. Utilizando uma cláusula select especial, o select assíncrono, uma tarefa pode reagir imediatamente a mensagens de outras tarefas. O Ada 95 oferece essa funcionalidade juntamente com objetos protegidos, onde o acesso exclusivo aos dados compartilhados de uma tarefa é garantido mesmo quando utilizando o select assíncrono. O mecanismo de passagem de mensagens em Ada é complexo (Sebesta, 2000). Este trabalho limita-se a uma introdução ao assunto. Em Sebesta (2000) podem ser encontradas mais informações sobre o mecanismo CONCORRÊNCIA NO AMBIENTE BORLAND DELPHI 6.0 programação. O objetivo desta seção é mostrar o uso da concorrência em uma linguagem de Segundo Borland (2001), o ambiente de programação Borland Delphi 6.0 (Delphi), oferece ao programador uma série de objetos para escrever aplicações multithreaded. Para a maioria das aplicações, pode-se utilizar um objeto thread para representar um thread de controle. Os objetos threads simplificam o desenvolvimento de aplicações porque encapsulam a maioria das necessidades do programador no que diz respeito à implementação de aplicações multithreaded.

61 DECLARAÇÃO DE OBJETOS THREAD Para utilizar um objeto thread na aplicação, é necessário criar um novo descendente da classe TThread. Ao selecionar a opção File New no ambiente, o usuário deve selecionar Thread Object. Após informar o nome do novo descendente, o ambiente cria uma nova unit, com a declaração da classe descendente de TThread. A declaração gerada pelo ambiente é a mostrada no quadro 36. QUADRO 36 - DECLARAÇÃO DE DESCENDENTE DA CLASSE TTHREAD unit Unit2; interface uses Classes; type TMyThread = class(tthread) private { Declarações Private } protected procedure Execute; override; end; implementation { TMyThread } procedure TMyThread.Execute; begin { Aqui vai o código do thread } end; end. Fonte: adaptado de Borland (2001) Neste código (um esqueleto do novo objeto thread) o programador pode: a) adicionar um código de inicialização; b) escrever o código que será executado pela thread; c) adicionar um código de limpeza CÓDIGO DE INICIALIZAÇÃO Segundo Borland (2001), na inicialização da thread o programador pode informar uma prioridade padrão (quanto tempo o sistema operacional vai dedicar a execução da thread), e indicar quando ela será liberada.

62 47 A inicialização é feita reescrevendo o código do constructor do descendente criado. O quadro 37 mostra como definir uma prioridade padrão para a thread. Neste caso é definida a prioridade tpidle, que significa que o controle só será passado para a thread quando o sistema estiver ocioso. QUADRO 37 - CÓDIGO DE INICIALIZAÇÃO constructor TMyThread.Create(CreateSuspended: Boolean); begin inherited Create(CreateSuspended); Priority := tpidle; end; Fonte: Borland (2001) Normalmente quando a thread termina sua execução, o objeto thread pode ser liberado. Por padrão, um objeto thread no Delphi não é liberado, tal operação é responsabilidade do programador. Porém no código de inicialização o programador pode utilizar a propriedade booleana FreeOnTerminate para definir se o objeto em questão deve ser liberado automaticamente ao término da execução da thread correspondente. Para isto basta atribuir o valor True à propriedade CÓDIGO EXECUTADO PELA THREAD O código que será executado concorrentemente pelo objeto thread deve ser escrito no método Execute, que já vem no esqueleto gerado pelo ambiente. Neste código vários assuntos devem ser considerados (Borland, 2001): a) para usar a thread do programa principal, deve-se utilizar o método Synchronize que garante um acesso exclusivo a todos os recursos compartilhados do programa principal; b) variáveis locais da thread podem ser declaradas utilizando a palavra reservada threadvar, o que as torna variáveis globais para todas as rotinas da thread sem compartilhá-las com outras threads; c) o acesso simultâneo à recursos compartilhados pode ser evitado (sincronização de competição) através de Seções Críticas (TCriticalSection), componentes threadsafe que possuem métodos que garantem um acesso exclusivo a seus recursos

63 48 (TCanvas, TThreadList etc), e o TMultiReadExclusiveWriteSynchronizer que garante acesso exclusivo a uma área de memória para operações de escrita, e acesso liberado para leitura; d) a espera por outras threads (sincronização de cooperação) pode ser feita através do método WaitFor do objeto thread que bloqueia o chamador até que a thread termine sua execução, ou através do objeto Event o qual disponibiliza métodos que bloqueiam o chamador até que uma determinada operação seja completada (possui o mesmo comportamento de um semáforo binário, exceto por oferecer um tempo limite de espera) CÓDIGO DE LIMPEZA Quando a execução de uma thread finaliza, o objeto thread correspondente dispara o evento OnTerminate. Este evento sempre é disparado, não importa qual o caminho tomado pelo método Execute da thread. Porém este evento não é disparado na thread responsável por ele, ele é executado no contexto principal da aplicação, o que traz duas implicações (Borland, 2001): a) não é possível acessar as variáveis locais da thread do código executado neste evento; b) é possível acessar qualquer componente da aplicação sem precisar se preocupar com conflitos com outras threads ATIVAÇÃO DE THREADS Uma vez que o descendente da classe TThread está implementado, a thread pode ser utilizada pela aplicação. Primeiramente deve-se criar uma instância da classe da thread. Pode-se criar uma instância e executar a thread imediatamente passando no parâmetro CreateSuspended do constructor (mostrado no quadro 37) o valor False. O código mostrado no quadro 38 cria uma instância e executa a thread imediatamente (Borland, 2001). QUADRO 38 - INSTANCIAÇÃO E EXECUÇÃO IMEDIATA SecondThread := TMyThread.Create(false); Fonte: Borland (2001)

64 49 Caso seja passado True no parâmetro CreateSuspended, a execução da thread deve ser iniciada invocando o método Resume, conforme o quadro 39. QUADRO 39 - INSTANCIAÇÃO SEM EXECUÇÃO IMEDIATA SecondThread := TMyThread.Create(true); SecondThread.Resume; Fonte: adaptado de Borland (2001) A execução de uma thread pode ser suspensa em qualquer momento através do método Suspend e retomada com Resume. As chamadas desses métodos podem ser aninhadas, ou seja, se o método Suspend é chamado X vezes, para retomar a execução da thread é necessário chamar o método Resume também X vezes DESATIVAÇÃO DE THREADS Uma thread só pode ser desativada quando sua execução é terminada. Ou seja, o objeto thread só pode ser desalocado da memória quando o método Execute finaliza sua execução ou quando evento OnTerminate é disparado. Para forçar um término prematuro da thread, utiliza-se o método Terminate que nada faz além de atribuir o valor True à propriedade Terminated do objeto thread. Isto significa que o código do método Execute deve verificar periodicamente o valor da propriedade Terminated. Quando esta propriedade retornar True, o código do método Execute deve parar sua execução deixando o controle passar direto para o seu fim (Borland, 2001). Tendo a execução terminada, o objeto thread pode ser liberado através do método Free, caso não tenha sido inicializado com True na propriedade FreeOnTerminate. 2.4 ARQUITETURA DO MICROPROCESSADOR 8088 Nesta seção será apresentada uma descrição da arquitetura da máquina-alvo do protótipo deste trabalho, o microprocessador O 8088 é um microprocessador desenvolvido pela empresa Intel Corporation, sendo uma versão do modelo 8086, lançado em 1978 (Santos, 1989).

65 50 Ele é a unidade central de processamento (CPU) do computador PC/PC XT (Norton, 1989). Quase todos os dados que entram ou saem do computador passam pela CPU para serem processados ou redirecionados. O 8088 controla a operação básica do computador, emitindo e recebendo sinais de controle, endereços de memória, e dados de uma parte do computador para outra, ao longo de uma rede de vias eletrônicas interconectadas chamada barramento. Ao longo do barramento estão as portas de entrada e saída (E/S), que conectam os vários dispositivos de memória e de suporte ao barramento. Os dados passam através dessas portas de E/S, em ambas as direções (CPU para dispositivos, dispositivos para CPU) (Norton, 1989). Dentro do 8088, 14 registradores fornecem uma área de trabalho para a transferência e processamento de dados. Esses registradores internos formam uma área de 28 bytes de extensão, os quais são capazes de, temporariamente, armazenar dados, endereços de memória, ponteiros de instruções e sinalizadores de estado e controle. Por meio desses registradores, o 8088 pode acessar cerca de um milhão de bytes de memória e até portas de entrada e saída (Norton, 1989) CIRCUITOS DE SUPORTE Segundo Norton (1989), o microprocessador não pode ter o controle de todo o computador sem alguma ajuda externa. Certas funções são delegadas a outros circuitos. Esses circuitos de suporte podem ser responsáveis pelo controle de um dispositivo externo conectado ao computador como uma unidade de disco ou responsáveis pelo controle do fluxo de informação entre as partes que formam o computador. Nas seções subseqüentes serão apresentados os circuitos de suporte de um sistema CONTROLADOR DE INTERRUPÇÕES 8259 O 8259, ou PIC (de Programmable Interrupt Controller ), supervisiona a operação das interrupções. Segundo Norton (1989), as interrupções são sinais enviados ao microprocessador pelo hardware para requisitar sua atenção ou para pedir que alguma ação seja tomada. O 8259 intercepta os sinais, determina seu nível de importância em relação aos outros sinais que está recebendo, e finalmente emite uma interrupção ao microprocessador, baseada nessa determinação. Quando o microprocessador recebe o sinal da interrupção, ele chama um

66 51 programa especial associado àquela interrupção. Esse programa é o que realmente executa a ação requisitada CONTROLADOR DE DMA 8237A Para não incomodar o microprocessador, algumas partes do computador podem transferir dados de e para a memória principal sem passar pela CPU. Essa operação é chamada de acesso direto à memória, ou DMA (de Direct Memory Access ), e o circuito que realiza essa função é conhecido como 8237A, ou controlador de DMA. A finalidade principal do controlador de DMA é permitir que a unidade de disco leia ou grave seus dados sem envolver o microprocessador. Como a E/S em disco é uma operação relativamente lenta, o DMA pode aumentar bastante a atuação geral do computador (Norton, 1989) GERADOR DE PULSO 8284A O gerador de pulso (ou clock) fornece sinais de pulso multifase, necessários para temporizar o microprocessador e os periféricos. Sua freqüência base é 14,3128 megahertz (MHz, ou milhões de ciclos por segundo). Esta freqüência geralmente é dividida por uma constante, obtendo assim, a freqüência necessária para os outros circuitos, que a utilizam, realizarem suas tarefas. O 8088 é temporizado em 4,77 MHz, um terço da freqüência base. O barramento interno e o temporizador programável 8253 usam uma freqüência de 1,193 MHz, um quarto da velocidade do 8088, e um doze avos da velocidade base (Norton, 1989) INTERFACE PROGRAMÁVEL DE PERIFÉRICO 8255 Segundo Norton (1989), o 8255 é usado para conectar alguns periféricos do computador ao barramento. A informação indo ou vindo de dispositivos como alto-falante e o gravador, passa por portas de E/S através deste circuito. Este circuito normalmente é programado pelo software do sistema TEMPORIZADOR PROGRAMÁVEL 8253 O 8253 é um temporizador e contador de múltipla finalidade, que pode gerar até três retardos de tempo precisos, sob controle do software. Ele recebe o sinal do gerador de pulso 8284A e é projetado para oscilar na freqüência de 1,190 MHz. É usado principalmente para gerar sons no alto-falante interno do PC, mas também é usado para outras funções que

67 52 dependem de freqüência, como E/S de dados em fita cassete e manutenção da hora do sistema (Norton, 1989) CONTROLADOR DE VÍDEO 6845 O 6845, também chamado de circuito de vídeo, está geralmente localizado numa placa de expansão conhecida como adaptador de vídeo. Ele tem 19 registradores internos, usados para definir e controlar a varredura do vídeo (Norton, 1989) CONTROLADOR DE DISQUETE PD765 O PD765 supervisiona e controla a operação da unidade de disquete. Ele é mais conhecido como FDC (de Floopy Disk Controller, controlador de disco flexível) (Norton, 1989) BARRAMENTO Barramento é o caminho compartilhado na placa principal do circuito, ao qual todas as partes do computador estão conectadas. Quando os dados são transferidos de um componente para outro, eles passam por esse caminho para atingir seu destino (Norton, 1989). Todo circuito de controle e todo byte da memória do PC estão conectados direta ou indiretamente ao barramento. Quando um novo componente é colocado em um dos conectores de expansão, ele se encontra diretamente conectado ao barramento, tornando-se parte igual na operação da unidade inteira. Sempre que uma porta ou célula de memória é usada como local de armazenamento, sua posição é marcada por um endereço que identifica e a distingue das outras. Toda vez que existem dados prontos para serem transferidos, o endereço de destino é inicialmente transmitido pelo barramento de endereços, após isto, os dados são transmitidos pelo barramento de dados. Assim, o barramento é utilizado para: a) informações sobre potência; b) informações de controle; c) endereços; d) dados.

68 53 Para realizar essas 4 funções diferentes o barramento é dividido em quatro partes: linhas de tensão; barramento de controle; barramento de endereços e barramento de dados. O barramento de endereços do 8088 usa 20 linhas de sinal para transmitir os endereços das células de memória e dispositivos conectados ao barramento. O barramento de dados funciona em conjunto com o barramento de endereços, transportando dados por todo o computador. O sistema baseado no 8088 usa um barramento de dados de 8 linhas de sinal, cada uma transportando um único dígito binário (bit). Isso significa que os dados são transmitidos no decorrer da barra de 8 linha em unidades de 8 bits (1 byte) (Norton, 1989). Informações sobre os outros barramentos podem ser vistas em Norton (1989) MEMÓRIA A memória é usada para ler ou gravar valores armazenados em posições de memória e identificados com endereços numéricos. Essas posições de memória podem ser acessadas diretamente por meio do circuito 8237A ou indiretamente por meio dos registradores internos do 8088 (Norton, 1989). Segundo Santos (1989), a memória em um sistema baseado no 8086 ou no 8088 é uma seqüência de bytes (1 Mbyte). A cada byte associa-se um endereço, na faixa de a FFFFF, em notação hexadecimal. Dois bytes consecutivos dentro da memória constituem uma word (palavra). Uma word contém 16 bits. O byte com o endereço de memória mais alto contém os 8 bits mais significativos desta word, enquanto que o byte de endereço mais baixo contém os menos significativos. Os dados na memória podem ser acessados na forma de bytes ou words, como for mais conveniente para o programa em execução. Existe uma diferença em relação à forma de acesso usada pelo 8086 e o Este último possui uma via de dados de 8 bits, e, quando uma word é acessada na memória, dois acessos consecutivos são realizados pela máquina. No primeiro acesso, o byte do endereço menos significativo é manipulado e, no segundo, o do endereço mais significativo. No 8086 isto não ocorre, pois sua via de dados é de 16 bits, sendo que a informação transferida em cada acesso à memória é sempre de 2 bytes. Por este motivo diz-se que o 8088 é um microprocessador híbrido, pois, embora tenha uma via de dados de 8 bits, seus

69 54 registradores internos são todos de 16 bits. Para fins de programação não importa se o sistema está baseado no 8088 ou no 8086, pois internamente ambos são praticamente idênticos (Santos, 1989) SEGMENTAÇÃO DA MEMÓRIA Conforme Santos (1989), uma vez que o microprocessador 8088 pode endereçar 1 Mbyte é necessário uma quantidade de 20 bits, ou 5 dígitos hexadecimais, para representar o endereço de uma posição qualquer na memória. Porém o microprocessador possui registradores internos que comportam valores com, no máximo, 16 bits. Para gerar um endereço de 20 bits o microprocessador 8088 utiliza dois registradores de 16 bits em conjunto. A referência a bytes ou words na memória é feita utilizando um registrador com o endereço do segmento e um registrador contendo um outro endereço de 16 bits chamado de offset, que representa o deslocamento dentro daquele segmento. O primeiro byte dentro de um segmento tem o offset 0000h e o último o FFFFh. Assim, todos os endereços de memória são calculados pela somatória do conteúdo de um registrador de segmento com um endereço de offset. Mais precisamente, o conteúdo binário do registrador de segmento é multiplicado por 16 (10h), e então somado ao endereço de offset, de modo a gerar um endereço de 20 bits REGISTRADORES INTERNOS O 8088 tem internamente um total de 14 registradores de 16 bits. Santos (1989) os divide em cinco grupos: a) registradores de propósito geral; b) registradores de índice e ponteiros; c) registradores de segmento; d) contador de programa ou ponteiro da instrução; e) flags (sinalizadores). No quadro 40 são mostrados os registradores de cada grupo.

70 55 QUADRO 40 - REGISTRADORES DO bits AH, AL, BH, BL, CH, CL, DH, DL 16 bits AX, BX, CX, DX Ponteiro SP, BP Índice SI, DI Segmento CS, SS, DS, ES Contador de programa IP Sinalizadores FLAGS Fonte: adaptado de Santos (1989) REGISTRADORES DE PROPÓSITO GERAL Segundo Santos (1989), ao todo são 4 registradores desse tipo, todos de 16 bits: AX, BX, CX e DX. As metades alta e baixa de cada um deles podem ser usadas como registradores de 8 bits. Assim, têm-se os seguintes registradores de 8 bits: AL e AH, BL e BH, CL e CH, DL e DH. Para muitas operações aritméticas e lógicas, pode-se utilizar qualquer um destes registradores. Porém existem instruções que os usam com certas funções especializadas. Instruções de multiplicação e divisão concentram-se em AL e AX. Instruções que se repetem por inúmeras vezes, como a rotação do conteúdo de um registrador para a esquerda, ou uma instrução string que e blocos de memória de uma posição à outra, podem requerer a especificação do número de vezes ou de words a er, nos registradores CL e CX. Devido seus usos especializados em algumas instruções, os registradores de propósito geral recebem os seguintes nomes descritivos: a) AX é o Acumulador, por concentrar resultados em algumas instruções aritméticas; b) BX é chamado Base, sendo o único registrador de propósito geral que pode ser usado para gerar um endereço de memória para acessar dados dentro do segmento de dados; c) CX é o Contador, por conter, normalmente, o número de vezes que se quer repetir um laço ou uma instrução string; d) DX é chamado Registrador de Dados, por ser usado para especificar um endereço usado nas instruções de entrada e saída.

71 REGISTRADORES DE ÍNDICE E PONTEIROS Os registradores deste grupo (BP, SP, SI e DI) são usados para armazenar endereços de offset dentro de segmentos, onde os dados devem ser acessados. O par SP e BP trabalha dentro do Segmento da Pilha, enquanto o par SI e DI trabalha normalmente no Segmento de Dados (Santos, 1989). O registrador SP é referenciado pelas instruções PUSH e POP para determinar o endereço de offset do topo da pilha, sugerindo assim o nome Stack Pointer (Ponteiro da Pilha) para ele. O BP é usado para indicar o endereço de uma área de dados, dentro da pilha, onde foram deixados parâmetros para manipulação por uma sub-rotina. Assim, a rotina poderá retirar os parâmetros, por exemplo, uma string digitada pelo usuário, usando o valor contido no BP, que indica o início da área de dados. O nome descritivo para o BP é Base Pointer (Ponteiro da Base). Passagem de parâmetros, através da pilha, de uma rotina à outra é uma prática muito freqüente dos procedures em linguagens de alto nível, como Pascal e C. A linguagem FURBOL também utiliza a pilha para a passagem de parâmetros. Os registradores SI e DI também são utilizados para referenciar dados na área de dados do programa. O uso especializado do par encontra-se nas instruções string MOVS, CMPS, SCAS e LODS, onde se assume que SI contém o endereço de início da área de dados (fonte), daí o nome Source Index, e o DI contém o endereço de destino de dados, daí o nome ser Destination Index. Através das instruções string, pode-se copiar blocos de memória de uma área a outra, preencher um bloco com um valor constante, comparar o conteúdo de dois blocos etc REGISTRADORES DE SEGMENTO E O PONTEIRO DA INSTRUÇÃO Segundo Santos (1989), os registradores de segmento (CS, DS, SS e ES) são usados para gerar endereços de memória de 20 bits. Identificam os quatro segmentos endereçáveis naquele momento pelo programa. Cada um deles identifica um bloco de memória de 64 Kbytes onde estão, o código do programa (CS, Code Segment), os dados (DS, Data Segment), a pilha (SS, Stack Segment) e um segmento adicional para dados (ES, Extra Segment). Isto significa que dentro de um espaço de 1 Mbyte, apenas 256 Kbytes são realmente acessíveis num momento de programa. Para acessar o conteúdo de uma posição não abrangida pelos segmentos correntes, precisa-se alterar o valor do registrador associado ao novo segmento.

72 57 Os códigos de todas as instruções são buscados no Segmento de Código. Dentro deste segmento, o registrador IP indica o endereço de offset, ou o deslocamento dentro daquele segmento, onde está a instrução a ser executada. Assim, ele atua como um Ponteiro da Instrução (Instruction Pointer). Por exemplo, considerando que o registrador de Segmento de Código (CS) contenha o valor hexadecimal 113A e que IP tenha o valor 210C, assim, o código da próxima instrução a ser executada inicia-se no endereço hexadecimal 134AC (113Ah * 10h + 210Ch = 134ACh). Conclui-se que o registrador IP associa-se sempre ao CS para formar o endereço de memória onde se encontra a instrução; da mesma forma o SP e o BP associam-se ao SS para formar o endereço de dado na pilha; e o BX, o DI e o SI associam-se ao DS para formar o endereço do dado no segmento de dados, exceto nas instruções string onde o DI trabalhará com o ES. O registrador IP não é diretamente acessível ao programador, sendo alterado indiretamente pelas instruções JMP, CALL, RET, INT e IRET, que modificam o fluxo seqüencial normal de execução. O que estas instruções fazem é modificar o valor do registrador IP SINALIZADORES O microprocessador 8088 contém em seu interior um total de 9 sinalizadores, também conhecidos como flags. Eles existem para indicar resultados obtidos sempre na última operação lógica ou aritmética executada, ou para definir o comportamento do microprocessador na execução de certas instruções. Estes 9 flags estão agrupados, para facilidade de acesso, em um registrador de 16 bits, chamado de Registrador de Flags. Dentro deste registrador de 16 bits, apenas nove são efetivamente usados (Santos, 1989). O significado de cada bit sinalizador no registrador de flags é apresentado no quadro 41.

73 PILHA QUADRO 41 - SINALIZADORES DO REGISTRADOR DE FLAGS OF (Overflow Flag) DF (Direction Flag) IF (Interrupt Flag) TF (Trap Flag) SF (Sign Flag) ZF (Zero Flag) Indica overflow do bit de alta ordem após uma operação aritmética. Este overflow é um estouro na capacidade do byte em armazenar um valor. Se o valor deste bit for 0, após a execução de uma instrução string, os registradores de índice envolvidos serão incrementados automaticamente, e em caso contrário, serão decrementados. Apenas as instruções CLD e STD manipulam este flag. Indica se as interrupções estão ou não habilitadas. O valor 1 indica que as interrupções estão habilitadas (serão atendidas quando ocorrerem). O valor zero indica que as mesmas estão desabilitadas. Pode-se definir a opção desejada com as instruções CLI e STI. Permite a operação passo a passo. Contém o sinal resultante após uma operação aritmética (0 = positivo, 1 = negativo). Indica o resultado da operação aritmética ou de comparação (0 = não zero ou diferente, 1 = zero ou igual). AF (Aux. carry Flag) Indica o transporte do bit 3 em um dado de 8 bits. Este flag tem uso restrito a operações com valores BCD. PF (Parity Flag) CF (Carry Flag) Fonte: adaptado de Santos (1989) Indica a paridade dos 8 bits de baixa ordem (1 = par, 0 = ímpar) de um resultado, após uma instrução lógica ou aritmética. Indica o transporte do bit de mais alta ordem, após uma operação aritmética, ou contém o último bit, após uma operação de rotação ou deslocamento. Segundo Norton (1989), a pilha é uma característica própria do É o local onde o programa pode armazenar dados e tomar conta do trabalho em andamento. Seu uso mais importante é a manutenção do registro do local de onde as sub-rotinas foram chamadas, e quais os parâmetros passados por ela. Também é utilizada para armazenamento temporário.

74 59 Uma pilha sempre opera na ordem último a entrar, primeiro a sair (ou LIFO, de Last- In-First-Out). Isso significa que quando a pilha é usada para armazenar o local de retorno a um programa, o programa que chamou por último é retornado primeiro. Dessa forma, uma pilha mantém o funcionamento ordenado de programas, sub-rotinas e rotinas de manipulação de interrupções, não importando a complexidade de sua operação (Norton, 1989). A pilha é usada de baixo (endereço mais alto) para cima (endereço mais baixo), de modo que, quando os dados são colocados na pilha, eles vão para os endereços de memória logo abaixo do topo. A pilha cresce para baixo. Conforme os dados são adicionados, a posição do topo da pilha e-se para endereços mais baixos, diminuindo o valor de SP a cada imento MODOS DE ENDEREÇAMENTO DE DADOS NO 8088 Seis opções estão disponíveis para o endereçamento de dados (Santos, 1989): a) imediato; b) direto; c) direto indexado; d) implícito; e) relativo; f) pilha ou stack. No tipo de endereçamento imediato, um dos operando está presente no byte seguinte ao código da instrução (op-code). Se bytes de endereçamento seguem o op-code, então o dado a ser transferido de maneira imediata virá logo após os bytes de endereçamento. Por exemplo, a instrução ADD AX,1000h soma 1000h ao conteúdo do registrador AX. O modo de endereçamento direto é feito somando-se os dois bytes seguintes ao op-code ao DS, para compor um novo endereço absoluto. Por exemplo, o registrador DS contém B000h, então a instrução MOV DX,[8000h] carregará em DX o conteúdo da posição absoluta de memória B8000h (DS:8000h). O modo de endereçamento direto indexado é feito utilizando-se os registradores SI ou DI como indexadores, somando-se um deslocamento de 8 bits ou 16 bits a um desses registradores de forma a gerar um endereço efetivo (offset). Por exemplo, MOV AL,[SI+10h] ou MOV [DI-1000h],DX.

75 60 O modo implícito nada mais é que uma versão simplificada do modo de endereçamento direto indexado. A única diferença entre eles é que, nesse modo, não é especificado um deslocamento. Como exemplo disso tem-se as instruções MOV AL,[DI] e MOV [DI],DX. O endereçamento relativo a dados utiliza o conteúdo do registrador BX para formar a base para o endereço efetivo. Todos os modos de endereçamento descritos até agora, com exceção do modo imediato, podem ser utilizados também no modo relativo. De fato, o modo relativo simplesmente soma o conteúdo do registrador BX ao endereço efetivo que seria formado caso ele não existisse. A instrução MOV AX,[BX+B000h] é um exemplo de modo relativo direto. Para o modo relativo implícito as instruções MOV CX,[BX+DI] ou MOV DH,[BX+SI], e para o modo relativo direto indexado as instruções MOV AL,[BX+SI+05h] ou MOV [BX+DI+1000h],DL. O 8088 permite também a utilização do modo de endereçamento via pilha, tal como o modo relativo descrito anteriormente. Neste caso, entretanto, o registrador BP é utilizado como registrador base. Esse modo de endereçamento é largamente utilizado quando se deseja acessar parâmetros passados através da pilha, tal como feito pelos compiladores C e Pascal, pois é possível retirar dados da pilha sem alterar seu ponteiro SP. Por exemplo, executando a instrução MOV BP,SP obtêm-se em BP o valor de SP. Assim para retirar um valor da pilha pode-se utilizar a instrução MOV AX,[BP+4h], e para armazenar um valor pode-se utilizar a instrução MOV [BP+DI+2h],AX ENTRADA E SAÍDA Segundo Norton (1989), o 8088 pode controlar e comunicar-se com muitas partes do computador por meio do uso de portas de entrada e saída (E/S). Toda porta é identificada por um número de 16 bits, que pode variar de 0 a O microprocessador envia dados ou informações de controle a uma porta em particular especificando o número da porta, e a porta responde passando dados ou informações de estado de volta ao microprocessador. Assim como quando está acessando a memória, a CPU usa o barramento de dados e endereços como canais de comunicação com as portas. Para acessar uma porta, primeiro a CPU envia um sinal, no barramento de controle, que avisa a todos os dispositivos de E/S que o endereço no barramento é de uma porta, e depois envia o endereço de porta. O dispositivo com o endereço de porta enviado responde à CPU (Norton, 1989).

76 61 O número da porta endereça uma posição de memória que é parte do dispositivo de E/S, mas não é parte da memória principal. São usadas instruções especiais de entrada/saída para indicar um acesso à porta e trocar informações com os dispositivos de E/S. No quadro 42 são mostrados as portas e seus endereços usados nos computadores baseados no QUADRO 42 - PORTAS E SEUS ENDEREÇOS EM COMPUTADORES PC/XT Descrição Endereços Controlador de DMA (8237) F Controlador de Interrupção (8259) Temporizador (8253) IPP (8255) Reg. de página de DMA (74LS612) Registrador de máscara de NMI 0A Controlador de jogos F Unidade de expansão Porta serial (principal) 3F8-3FF Porta serial (secundária) 2F8-2FF Cartão protótipo F Disco fixo F Impressora paralela (principal) F Adaptador monocromático/impressora 3B0-3BF Adaptador gráfico colorido/colorido 3D0-3DF Controlador de disquete 3F0-3F7 Fonte: adaptado Norton (1989) ROM, ROM-BIOS E MS-DOS Segundo Norton (1989), a ROM (de Read Only Memory, memória somente de leitura) de um PC/XT contém programas e dados necessários para inicializar e operar o computador e seus dispositivos periféricos e o ROM-BIOS, acrônimo de Basic Input/Output System (Sistema Básico de Entrada/Saída). Adicionalmente a ROM pode possuir o BASIC da ROM, que gera o núcleo da linguagem de programação BASIC, e as extensões da ROM (programas que são adicionados à ROM quando novos equipamentos são adicionados ao computador). Em Norton (1989) são encontrados mais detalhes sobre estes elementos da ROM.

77 62 O ROM-BIOS é a parte da ROM que está ativa por todo o tempo em que o computador está funcionando. Seu papel é fornecer os serviços fundamentais necessários à operação do computador. Cada rotina de serviço da BIOS possui um número de interrupção. O sistema operacional MS-DOS também possui serviços que podem ser utilizados pelo programa. Ao ser carregado na memória, pela parte final das rotinas de inicialização da ROM, o MS-DOS altera ou acrescenta funcionalidades aos serviços básicos de E/S, e geralmente inclui correções do ROM-BIOS existente (Norton, 1989) INTERRUPÇÕES Conforme Norton (1989), as interrupções são o modo pelo qual os circuitos de fora do 8088 informam que algo (como uma tecla pressionada) aconteceu e necessitam que alguma ação seja tomada. Sempre que um dispositivo de hardware ou um programa precisa de assistência da CPU, é enviado um sinal ou uma instrução, chamada interrupção, ao microprocessador, identificando a tarefa em particular a ser executada. Quando o microprocessador recebe um sinal de interrupção, ele geralmente para todas as outras atividades e ativa uma sub-rotina armazenada na memória, chamada manipulador de interrupção, que corresponde a um número de interrupção em particular. Após a rotina executar sua tarefa, as atividades do computador continuam a partir de onde elas estavam quando ocorreu a interrupção. Existem três tipos de interrupção: a) interrupções de hardware, geradas pelos circuitos do computador em reação a algum evento, e que são controladas pela PIC (8259) que indica a prioridade antes de enviálas à CPU; b) interrupções geradas por exceções, como uma divisão por zero que faz com que o programa que efetuou uma divisão por zero seja interrompido; c) interrupções de software, que são geradas deliberadamente por programas para chamar sub-rotinas, como os serviços do MS-DOS e da BIOS (alocar memória, gravar setor no disco etc). Qualquer que seja a interrupção enviada, a origem da interrupção não precisa saber o endereço da sua rotina de manipulação, basta o número da interrupção. O número serve como

78 63 referência em uma tabela localizada na parte mais baixa da memória principal, que contém os endereços segmentados das rotinas de tratamento das interrupções (manipuladores de interrupção). O endereço das rotinas é chamado vetor de interrupção, e a tabela é chamada tabela de vetores de interrupção. Conforme Santos (1989), o primeiro 1 Kbyte de memória, da posição 0000:0000 até 0000:03FF (respeitando o formato segmento:offset) é reservado para guardar os endereços das rotinas de tratamento das interrupções (vetores de interrupção). Cada endereço é composto de 4 bytes, sendo que as duas primeiras posições guardam o offset e as duas seguintes o segmento de memória. Assim dividindo os 1024 bytes por 4 (tamanho de cada endereço) obtêm-se 256 endereços de rotinas tratadoras de interrupção, ou 256 interrupções. Quando o 8088 detecta uma interrupção, multiplica o número desta interrupção por 4, e o valor encontrado será a posição na tabela de vetores de interrupção que contém o endereço da rotina tratadora para aquela interrupção. Interrupções podem ocorrer a qualquer momento, assim o microprocessador possui 2 recursos básicos para tratá-las (Santos, 1989): a) o processador pode desabilitar o recebimento de interrupções; b) quando ocorrer uma interrupção, o processador pode salvar as informações sobre o que ele está fazendo, a fim de retornar ao ponto anterior assim que a interrupção tenha sido tratada. O primeiro recurso é fornecido pelo Interrupt Flag (IF) do Se IF = 0, o processador irá ignorar qualquer interrupção. Somente quando IF for igual a 1 o processador irá atendê-la. Existe um outro tipo de interrupção, a Interrupção Não Mascarável (NMI), que será sempre reconhecida independente do estado de IF. A NMI ocorre quando houver um erro de paridade na memória. O segundo recurso é fornecido tratando-se as interrupções como se fossem um tipo de sub-rotina. Quando uma interrupção é recebida, os 16 bits do registrador de flags são colocados no topo da pilha. O IF e o TF (Trap Flag) são colocados em 0, para prevenir que uma outra interrupção ocorra antes de a primeira ter sido tratada. Depois o registrador CS (que define o segmento de memória onde o programa está sendo executado) é colocado na pilha, e por último o IP (que indica o ponto daquele segmento onde a execução foi interrompida).

79 64 Uma vez que o estado do processador tenha sido salvo, o mecanismo de interrupção transferirá o controle para a rotina tratadora, cujo endereço está na tabela de vetores de interrupção na posição correspondente ao número da interrupção. Quando esta rotina é completada, devolve o controle para o programa que estava sendo executado, no ponto de parada. Isto é feito pela instrução IRET, que restaura o estado do processador. Esta instrução restaura da pilha, primeiramente o IP, depois o CS e por último o flags, na ordem inversa em que foram salvos, respeitando o comportamento LIFO da pilha. Caso a rotina de tratamento de interrupção altere algum outro registrador, é de responsabilidade do programador salvá-lo na entrada da rotina e restaurá-lo na saída, para manter a integridade do programa que estava sendo executado INTERRUPÇÕES DE HARDWARE Segundo Hyde (2000), as interrupções vêm de muitas fontes diferentes, porém as principais fontes de interrupção são os circuitos de temporização do PC, teclado, portas seriais, portas paralelas etc. Ou seja, o próprio hardware da máquina. Conforme visto na seção (Controlador de Interrupções 8259) o 8259 controla as interrupções de hardware. Este circuito aceita interrupções de até 8 dispositivos diferentes. Se um destes dispositivos requisita um serviço, o 8259 envia um sinal para a CPU juntamente com o vetor da interrupção correspondente. Então a rotina tratadora da interrupção é executada. Quando uma interrupção específica ocorre, o 8259 mascara as outras interrupções deste dispositivo até que receba um sinal de End Of Interrupt (EOI) da rotina tratadora. Em programas rodando no MS-DOS, isto é feito escrevendo o valor 20h no registrador de comando do 8259, através da porta 20h. Apenas as interrupções de hardware utilizadas pelo protótipo serão detalhadas neste trabalho. Informações sobre as outras interrupções podem ser encontradas em Hyde (2000) e Santos (1989) EXCEÇÕES Interrupções geradas por exceções ocorrem quando alguma situação anormal ocorre na CPU, como por exemplo, uma divisão por zero (Hyde, 2000).

80 65 Essas interrupções, chamadas de interrupções lógicas ou interrupções de microprocessador, em sua maioria, são geradas pelo próprio microprocessador (Norton, 1989). Em Hyde (2000), Norton (1989) e Santos (1989) são encontrados maiores detalhes sobre este tipo de interrupção INTERRUPÇÕES DE SOFTWARE Interrupções de software, ou simplesmente traps, como descrito em Hyde (2000), são interrupções geradas por software através da instrução INT. Segundo Hyde (2000), o principal uso de traps é prover uma sub-rotina que vários programas podem chamar sem precisar conhecer o endereço de memória da mesma. No MS- DOS a instrução INT 21h é um exemplo de uso de trap. Os programas não precisam saber o endereço na memória de uma rotina do MS-DOS para requisitar um serviço do sistema. Ao ser carregado na memória, o MS-DOS atribui na posição correspondente a interrupção 21h, na tabela de vetores de interrupção, o endereço da rotina que provê serviços aos programas. Assim, toda a vez que um programa necessita de um serviço do MS-DOS, deve-se executar uma instrução INT 21h. Este trabalho limita-se a detalhar apenas as funções do MS-DOS utilizadas para a implementação de concorrência no processador Uma descrição completa de todas as funções disponíveis via INT 21h pode ser encontrada em Santos (1989) TABELA DE VETORES DE INTERRUPÇÃO No quadro 43 é apresentada a tabela de vetores de interrupção do PC/XT. Apenas a interrupção 8h será detalhada neste trabalho. Informações sobre as outras interrupções podem ser vistas em Norton (1989), Hyde (2000) e Santos (1989).

81 66 QUADRO 43 - TABELA DE VETORES DE INTERRUPÇÃO DO PC/XT Núm. Endereço Descrição Núm. Endereço Descrição (Núm*4) (Núm*4) Divisão por zero Teclado Passo simples C Impressora Interrupção não mascarável BASIC residente C Breakpoint Boot Overflow 1A 0068 Hora do dia Imprime tela 1B 006C Break do teclado Reservada 1C 0070 Pulso do relógio C Reservada 1D 0074 Inicialização de vídeo Timer 1E 0078 Parâmetros de disco Teclado 1F 007C Tabela de caracteres 0A 0028 Reservada Término do programa 0B 002C Comunicação Chamadas do DOS 0C 0030 Comunicação Endereço de terminação 0D 0034 Disco rígido C Pointer para CTRL+C 0E 0038 Disco flexível Erro crítico 0F 003C Impressora Leitura de disco Vídeo Gravação em disco Teste do equipamento C Permanece residente Memória 28 00A0 Reservada C Disquete/Winchester Comunicação 2E 00B8 Reservada Fita cassete 2F 00BC Controle do SPOOL Fonte: adaptado de Santos (1989) INTERRUPÇÃO DE TEMPORIZAÇÃO 8H (TIMER) Os computadores da família PC/PC XT possuem um circuito de temporização programável, o Conforme visto na seção , este circuito gera três retardos de tempo para fins diversos. Um destes retardos de tempo é interceptado pelo controlador de interrupções 8259 (PIC), sendo um dos oito dispositivos cujas interrupções são interceptadas pelo referido controlador. Mais especificamente, é o primeiro dispositivo da PIC, o que possui prioridade mais alta. Dessa forma, são geradas através da PIC, interrupções de hardware a cada 55 milisegundos aproximadamente, ou uma interrupção a cada 1 / 18,2 segundos. O número do vetor desta interrupção é 8h. Então, a cada 55 milisegundos a CPU desvia o fluxo de execução atual

82 67 para uma rotina tratadora cujo endereço está na posição correspondente à interrupção 8h na tabela de vetores de interrupção (Hyde, 2000). Os programas gerados pelo protótipo do ambiente de programação para a linguagem FURBOL instalarão uma rotina tratadora para esta interrupção. Esta rotina é responsável pelo escalonamento das tarefas do programa. Mais detalhes sobre a implementação desta rotina são vistas no capítulo 3. Segundo Hyde (2000), a interrupção 8h é utilizada pela BIOS. Instalar uma rotina de tratamento de interrupção neste vetor acarreta um mau funcionamento do sistema. Para resolver este problema utiliza-se a técnica de encadeamento de interrupções (interrupt chaining). Encadear uma interrupção significa salvar o vetor original de uma interrupção antes de instalar a nova rotina tratadora e no início, ou no final, da nova rotina tratadora, desviar a execução para o vetor original CONJUNTO DE INSTRUÇÕES DO 8088 Nesta seção será relacionado todo o conjunto de instruções do As instruções utilizadas para a implementação da concorrência serão descritas em detalhes. Elas serão representadas da mesma forma que são representadas na linguagem de montagem Assembly. Descrições das outras instruções podem ser vistas em Santos (1989). Detalhes da linguagem Assembly serão vistos na seção 2.7. Segundo Santos (1989), as instruções do 8088 podem ser classificadas em instruções de imentação de dados, aritméticas, lógicas, desvio de fluxo, entrada e saída, controle, rotação e deslocamento e instruções strings. Para a imentação de dados o 8088 disponibiliza as instruções MOV, MOVS, STOS, LODS, XCHG, LAHF, SAHF, XLAT, LEA, LDS, LES, PUSH, PUSHF, POP e POPF. As instruções aritméticas são ADD, ADC, SUB, SBB, INC, DEC, NEG, MUL, IMUL, DIV, IDIV, DAA, DAS, AAA, AAM, AAD, CBW, CWD, CMP, CMPS e SCAS. Para efetuar operações lógicas o microprocessador oferece as instruções AND, OR, XOR, TEST e NOT.

83 68 As instruções utilizadas para desvio de fluxo são JMP, J(condição), CALL, RET, INT, INTO, IRET, LOOP, LOOPE e LOOPNE. Para operações de entrada e saída existe a instrução IN e a OUT. Na categoria de controle estão NOP, HLT, WAIT, LOCK, ESC, CLC, CMC, CLD, CLI, STC, STI e SEG. SHR. Para rotação e deslocamento as instruções são RCL, RCR, ROL, ROR, SAL, SAR e As instruções string são MOVS, STOS, LODS, CMPS, SCAS e REP MOVIMENTAÇÃO DE DADOS A instruções de imentação de dados utilizadas para a implementação de concorrência no microprocessador 8088 são MOV, PUSH, PUSHF, POP e POPF. No quadro 44 é apresentado um detalhamento das instruções. QUADRO 44 - INSTRUÇÕES DE MOVIMENTAÇÃO DE DADOS Instrução Formato Descrição MOV MOV destino,fonte Copia conteúdo do operando fonte para destino PUSH PUSH fonte Coloca no topo da pilha o valor de fonte PUSHF PUSHF Coloca no topo da pilha o registrador de flags POP POP fonte Retira do topo da pilha uma word e à armazena em fonte POPF POPF Retira do topo da pilha uma word e à armazena no registrador de flags Fonte: adaptado de Santos (1989) ARITIMÉTICAS Na implementação de concorrência no protótipo foram utilizadas as instruções aritméticas mostradas no quadro 45.

84 69 QUADRO 45 - INSTRUÇÕES ARITIMÉTICAS Instrução Formato Descrição ADD ADD destino,fonte Soma o conteúdo do operando fonte ao destino, armazenando o resultado no destino. SUB SUB destino,fonte Subtrai o conteúdo do operando fonte do operando destino. O resultado é armazenado em destino. INC INC destino Soma 1 ao conteúdo do operando destino DEC DEC destino Subtrai 1 do conteúdo do operando destino DIV DIV fonte Divide o conteúdo da combinação DX:AX (32 bits) pelo conteúdo do operando fonte, armazenando o resto da divisão em DX e o resultado em AX. CMP CMP destino,fonte Igual a instrução SUB, porém não armazena o resultado, apenas modifica o registrador de flags. Fonte: adaptado de Santos (1989) LÓGICAS Apenas a instrução lógica XOR é utilizada na implementação da concorrência. Sua função é executar uma operação ou exclusivo entre dois operandos devolvendo o resultado no operando destino. Seu formato é XOR destino,fonte. A operação lógica XOR é feita bit a bit e devolve o valor 1 se ambos os bits correspondentes nos operandos forem diferentes, e 0, se forem iguais. É freqüentemente utilizada para zerar registradores, por exemplo, a instrução XOR CX,CX leva CX a zero (Santos, 1989) DESVIO DE FLUXO No quadro 46 são apresentadas as instruções de controle de fluxo utilizadas.

85 70 QUADRO 46 - INSTRUÇÕES DE DESVIO DE FLUXO Instrução Formato Descrição JMP JMP alvo Provoca um desvio incondicional no fluxo de processamento transferindo a execução para o operando alvo. J(condição) J(condição) alvo_curto Desviar o fluxo do processamento para o operando alvo se uma condição testada for encontrada. Ver quadro 47 para as variações da instrução. CALL CALL alvo Salva na pilha o valor de IP e desvia o fluxo do processamento para o operando alvo. RET RET [imed] Restaura da pilha o registrador IP e desvia o fluxo de execução para o endereço apontado por IP recém restaurado. Se imed (dado imediato) for informado, é adicionado o valor de imed ao SP. INT INT tipo Salva na pilha o registrador de flags, apaga os flags IF e TF, salva o registrador CS na pilha e salva o registrador IP na pilha. Após isto desvia o fluxo de processamento para o endereço na tabela de vetores de interrupção correspondente ao valor do operando tipo. IRET IRET Restaura da pilha o registrador IP, restaura o registrador CS, restaura o registrador de flags, e desvia o fluxo do processamento para o endereço apontado por CS:IP recém restaurados. LOOP LOOP alvo Decrementa o conteúdo do registrador CX e desvia o fluxo de execução para o operando alvo enquanto CX for diferente de 0. Fonte: adaptado de Santos (1989)

86 71 QUADRO 47 - VARIAÇÕES DA INSTRUÇÃO DE DESVIO DE FLUXO J(CONDIÇÃO) ENTRADA E SAÍDA das portas. Instrução Descrição Condição JA Acima CF = 0 e ZF = 0 JAE Acima ou igual CF = 0 JB Abaixo CF = 1 JBE Abaixo ou igual CF = 1 ou ZF = 1 JC Transporte CF = 1 CXZ CX = 0 CX = 0 JE ou JZ Igual (zero) ZF = 1 JG Maior ZF = 0 e SF = OF JGE Maior ou igual SF = OF JL Menor (SF xor OF) = 1 JLE Menor ou igual (SF xor OF) = 1 ou ZF = 1 JNA Não acima CF = 1 ou ZF = 1 JNAE Não acima nem igual CF = 1 JNB Não abaixo CF = 0 JNBE Não abaixo nem igual CF = 0 e ZF = 0 JNC Não transporte CF = 0 JNE ou JNZ Diferente (não zero) ZF = 0 JNG Não maior ((SF xor OF) or ZF) = 1 JNGE Não maior ou igual (SF xor OF) = 1 JNL Não menor SF = OF JNLE Não menor nem igual ZF = 0 e SF = OF JNO Não overflow OF = 0 JNP ou JPO Não paridade (ímpar) PF = 0 JNS Não sinal SF = 0 JO Overflow OF = 1 JP ou JPE Paridade (par) PF = 1 JS Sinal SF = 1 Fonte: adaptado de Santos (1989) As instruções de entrada e saída (quadro 48) são utilizadas para escrever e ler valores QUADRO 48 - INSTRUÇÕES DE ENTRADA E SAÍDA Instrução Formato Descrição IN OUT IN acumulador,porta IN acumulador,dx OUT porta,acumulador OUT DX,acumulador Fonte: adaptado de Santos (1989) Transfere os dados de uma porta de entrada para o acumulador AL ou AX. Quando utilizadado AL o número da porta deve ser entre 0h e FFh. Quando utilizado DX, o número da porta pode variar entre 0h e FFFFh. Transfere os dados do acumulador AX ou AL para uma porta. Quando utilizadado AL o número da porta deve ser entre 0h e FFh. Quando utilizado DX, o número da porta pode variar entre 0h e FFFFh.

87 CONTROLE Para o controle do comportamento da CPU são utilizadas as instruções do quadro 49 na implementação da concorrência no processador ROTAÇÃO QUADRO 49 - INSTRUÇÕES DE CONTROLE Apenas a instrução de rotação SHR é utilizada para implementar a concorrência no Seu objetivo é deslocar o conteúdo do registrador ou posição de memória à direita, inserindo um 0 na posição do bit mais significativo do operando e levando o bit menos significativo, após o deslocamento, ao flag CF, cujo valor inicial é perdido. O número de vezes a deslocar é 1 ou o contido no registrador CL. Seu formato pode ser SHR destino,1 ou SHR destino,cl. Instrução Formato Descrição CLI CLI Zera o bit IF no registrador de flags, desabilitando as interrupções de hardware mascaráveis. STI STI Liga o bit IF no registrador de flags, habilitando as interrupções de hardware mascaráveis. Fonte: adaptado de Santos (1989) 2.5 SISTEMA OPERACIONAL MS-DOS Os programas gerados pelo protótipo deste trabalho são programas de 16 bits para o microprocessador 8088 no formato.com, compatível com o sistema operacional Microsoft Disk Operating System (MS-DOS). Segundo Santos (1989), o sistema operacional MS-DOS é composto por 4 arquivos independentes entre si. Todos são carregados do disco à memória para a execução. O primeiro deles é o registro de carregamento inicial, residente na trilha 0, setor 1 e face 0 do disco que contém o sistema. Após a inicialização do equipamento o BIOS acessa o disco, verificando se este programa está presente nele, e efetua sua carga para a memória. Quando este programa entra em execução, carrega os demais módulos do MS-DOS para a memória. O segundo componente é o programa chamado IO.SYS, que contém extensões do ROM- BIOS. O terceiro é o MSDOS.SYS que gerencia o diretório e os arquivos no disco e contém as rotinas de funções do MS-DOS. Estas funções podem ser invocadas pelos programas através de

88 73 um conjunto de interrupções. Deste conjunto a mais importante é a 21h, que permite o acesso a um grande grupo de rotinas secundárias conhecidas como funções do MS-DOS. O quarto arquivo chama-se COMMAND.COM. Este programa é o interpretador de comandos digitados pelo usuário. Também é responsável pela carga de todos os programas do usuário para a execução. Os programas do usuário no sistema operacional MS-DOS podem ser de dois formatos: arquivos.exe e arquivos.com. Este trabalho limita-se a descrever os arquivos.com, uma vez que os programas gerados pelo compilador FURBOL são deste formato. Detalhes sobre arquivos.exe podem ser encontrados em Santos (1989). Quando um programa no formato.com é carregado pelo sistema operacional para execução, o sistema cria o Program Segment Prefix (PSP), dentro do segmento de código do programa, nos primeiros 256 bytes, sendo o programa carregado logo em seguida, no endereço 100h. O PSP possui vários campos, sendo que no primeiro campo existe uma instrução INT 20h, um serviço do MS-DOS que avisa ao sistema operacional que o programa terminou sua execução, transferindo o controle para o sistema. Em Santos (1989) são encontradas mais informações sobre o PSP FUNÇÕES DO MS-DOS Para facilitar a implementação da concorrência foram utilizadas algumas funções do MS- DOS que são chamadas através da instrução INT 21h. Estas funções são apresentadas no quadro 50. Informações sobre outras funções da INT 21h podem ser vistas em Santos (1989).

89 74 Função Descrição Exemplo QUADRO 50 - FUNÇÕES DA INT 21H 2h 9h 48h 49h 4Ah Escreve um único caractere na tela. Imprime na tela, a partir da posição do cursor, uma cadeia de caracteres apontada por DX. O caractere $, dentro da cadeia deve ser usado como seu delimitador final. Efetua alocação dinâmica de memória. BX deve conter o número de parágrafos que se deseja alocar. Se CF = 0, a alocação foi efetuada com sucesso e AX contém o valor do segmento do bloco alocado. Libera para o sistema operacional o bloco de memória alocado pela função 48h. ES deve conter o segmento de memória que deve ser liberado. Se CF = 0 a operação foi efetuada com sucesso. Permite que se aumente ou diminua o bloco anteriormente alocado. ES contém o segmento onde está o bloco. BX contém o novo tamanho em parágrafos. Se CF = 0 a função foi executada com sucesso. MOV Ah,2h ; número da função MOV DL,41h ; caractere em DL INT 21h ; chama o DOS msg: DB Mensagem$ MOV AH,9h ; número da função LEA DX,msg ; endereço da msg INT 21h ; chama o DOS MOV AH,48h ; número da função MOV BX,1h ; 1 parágrafo ou 16 Bytes INT 21h ; chama DOS JC erro MOV ES,AX ; utiliza o bloco MOV AH,49h ; número da função MOV ES,segm ; bloco que deve ser lib. INT 21h ; chama o DOS JC erro MOV AH,4Ah ; número da função MOV ES,segm ; bloco à alterar MOV BX,0C00h ; aumenta para 48K INT 21h ; chama o DOS JC erro Fonte: adaptado de Santos (1989) 2.6 MONTADORES (ASSEMBLERS) Segundo Quadros (1986), o microprocessador executa instruções que são armazenadas em memória como números na linguagem de máquina. A programação destas instruções diretamente nesta linguagem é bastante trabalhosa. Em primeiro lugar, é necessário consultar uma tabela dos códigos das várias instruções. Em seguida determina-se o endereço das

90 75 instruções para as quais são efetuados desvios. Uma vez obtido o programa, se houver necessidade de incluir ou retirar instruções, estes endereços terão de ser re-determinados. O Assembler (montador) é um programa que automatiza estas tarefas e outras mais. Ele recebe como entrada o programa escrito em outra forma, na linguagem de montagem Assembly e produz como saída um programa equivalente na linguagem de máquina. Segundo Quadros (1986), na linguagem Assembly as instruções do microprocessador 8088 são referenciadas por mnemônicos (abreviações de fácil memorização) e os endereços por símbolos (nomes aos quais o Assembler associa os endereços reais). O protótipo deste trabalho gera um código de montagem na linguagem Assembly e utiliza o montador Turbo Assembler para converter o código de montagem em código de máquina. No final do processo utiliza o utilitário Turbo Link (que faz o processo de ligação) para converter o código objeto gerado pelo montador num formato executável compatível com o sistema operacional MS-DOS. 2.7 LINGUAGEM ASSEMBLY Nas seções subseqüentes serão vistos detalhes da linguagem gerada pelo protótipo após a tradução de um programa na linguagem FURBOL que não foram vistos nas seções anteriores SÍMBOLOS Segundo Quadros (1986), símbolos são nomes definidos pelo programador para indicar os endereços de instruções e variáveis. Existem duas classes de símbolos na linguagem: os rótulos e as variáveis. Os rótulos correspondem a endereços de instruções e as variáveis correspondem a endereços de dados. Todo símbolo possui três características: o segmento em que foi definido; o deslocamento em relação ao segmento e o tipo do símbolo. Os rótulos podem ser do tipo NEAR ou do tipo FAR. Os desvios para um rótulo NEAR envolvem apenas alteração de deslocamento (alteração do registrador IP). Os desvios para um rótulo FAR envolvem a alteração de segmento e deslocamento (alteração de registradores CS e IP). As variáveis podem ser dos tipos byte, word e dword. As variáveis do tipo byte ocupam 1 byte na memória. Variáveis do tipo word, 2 bytes. As do tipo dword ocupam 4 bytes.

91 ELEMENTOS Conforme descrito em Quadros (1986), os elementos utilizados pela linguagem são: constantes numéricas; cadeias de caracteres e nomes. Uma constante numérica pode ser fornecida nas bases binária, octal, decimal ou hexadecimal. A base de uma constante é indicada através de uma letra ao final da constante: b para binário; o para octal; d para decimal e h para hexadecimal. Caso a letra indicadora da base da constante não for informada, a linguagem assume a base decimal para a constante. No caso de constante hexadecimal, deve-se obrigatoriamente iniciá-la um com um dígito (0 a 9). Por exemplo, a constante hexadecimal FF deve ser representada por 0FFh. Caso o 0 seja omitido o montador interpretará a constante como um símbolo qualquer. Uma cadeia de caracteres é uma seqüência de caracteres cercada por apóstrofos (por exemplo cadeia ). Os nomes são formados por uma seqüência de letras e dígitos, iniciada por uma letra. Os e? também são considerados como letras. O caractere? não pode ser usado sozinho como nome. Um nome pode ter até 31 caracteres FORMATO DO PROGRAMA Segundo Quadros (1986) um programa escrito em linguagem Assembly é composto por: a) linhas em branco; b) linhas de comentário; c) linhas de instrução; d) linhas de diretiva. As linhas de comentário caracterizam-se por terem como primeiro caractere não branco um ;. Os caracteres seguintes ao ; são ignorados pelo montador. As linhas de instrução são as que contêm um mnemônico e irão gerar uma instrução de máquina. O formato de uma linha de instrução é mostrado no quadro 51.

92 77 QUADRO 51 - FORMATO DE UMA LINHA DE INSTRUÇÃO [nome:] [prefixo] mnemônico [operandos] [; comentário] Fonte: adaptado de Quadros (1986) O único campo sempre obrigatório numa linha de instrução é o do mnemônico. O campo de operandos será obrigatório ou opcional, dependendo da instrução. No caso de se ter mais de um operando, eles são separados por vírgula. Em casos especiais pode ser necessário utilizar o campo de prefixo. Por exemplo, numa máquina com múltiplos processadores utiliza-se o prefixo LOCK quando se deseja acesso exclusivo a memória no momento em que a instrução for executada. As diretivas (Quadros, 1986) ou pseudo-operadores (Santos, 1989) servem para instruir o montador a efetuar o seu trabalho. Os pseudo-operadores não geram qualquer código de máquina, sendo inclusive, muitos deles, opcionais. O formato de uma linha de diretiva é mostrado no quadro 52. QUADRO 52 - FORMATO DE UMA LINHA DE DIRETIVA [nome] diretiva [operandos] [; comentário] Fonte: adaptado de Quadros (1986) Quando utilizada, o único campo sempre obrigatório na linha de diretiva é o campo diretiva. Os campos de nome e operandos podem ser obrigatórios ou opcionais, conforme a diretiva. O nome inicial não é seguido de :, ao contrário da linha de instrução. Todo programa fonte em linguagem Assembly termina com a diretiva END. Seu formato é mostrado no quadro 53. QUADRO 53 - FORMATO DA DIRETIVA END END rótulo Fonte: adaptado de Quadros (1986)

93 78 O rótulo no quadro 53 indica a primeira instrução a ser executada (ponto de entrada do programa) SEGMENTOS Um segmento é iniciado pela diretiva SEGMENT e terminado com a diretiva ENDS, conforme mostrado no quadro 54. QUADRO 54 - DECLARAÇÃO DE UM SEGMENTO nome nome SEGMENT.. código assembly.. ENDS Fonte: adaptado de Quadros (1986) Todos os símbolos declarados entre estas duas diretivas terão associado a si o segmento nome. Para verificar se um determinado símbolo pode ser acessado e como deve ser acessado, o montador precisa saber quais valores o programador colocou nos registradores de segmento. Isto é feito através da diretiva ASSUME (quadro 55). QUADRO 55 - DIRETIVA ASSUME ASSUME segreg:segnome[,segreg:segnome...] Fonte: adaptado de Quadros (1986) No quadro 55 segreg corresponde ao nome de um registrador de segmento (CS, DS, ES, ou SS) e segnome, ao nome de um segmento ou a palavra NOTHING. A execução de instruções é sempre feita no segmento indicado por CS. O acesso à pilha via registrador SP ou BP utiliza sempre o segmento indicado por SS. Algumas instruções strings utilizam sempre o segmento contido em ES. Nos demais casos o registrador de segmento padrão para efetuar acessos à memória é o DS. Por exemplo, a instrução MOV [BX],AX copia o

94 79 conteúdo de AX para o local de memória apontado pela combinação do segmento em DS e do offset em BX. O segmento padrão (DS) pode ser substituído por um outro através da colocação de um prefixo no operando, o segment override. Por exemplo, a instrução MOV ES:[BX],AX copia o conteúdo do registrador AX para o local de memória apontado pela combinação do segmento em ES e do offset em BX. Quando é associada a palavra NOTHING à um registrador de segmento na diretiva ASSUME, o montador é informado de que o conteúdo do registrador de segmento é indefinido, só sendo possível o seu uso através da codificação explícita do segment override VARIÁVEIS Segundo Quadros (1986), a definição de variáveis é feita através de três diretivas: DB (define byte); DW (define word) e DD (define double-word). Estas diretivas definem variáveis de 8, 16 e 32 bits respectivamente. Além de definir os símbolos e reservar espaço, estas diretivas permitem especificar o conteúdo inicial da variável, conforme exemplificado no quadro 56.

95 80 QUADRO 56 - EXEMPLOS DE DEFINIÇÃO DE VARIÁVEIS ; ; Define a variável qnt_proc ; Tipo byte ; Valor inicial 0 qnt_proc DB 0 ; ; Define a variável desloc ; Tipo word ; Valor inicial indeterminado desloc DW? ; ; Define a variável msg_erro ; Com 5 bytes ; Valores iniciais E, r, r, o e $ ; O símbolo tab_msg permite acessar o primeiro byte msg_erro DB Erro$ ; ; Define variável tab_num ; Com 10 words ; Todos com valor inicial 5 ; O símbolo tab_num permite acessar o primeiro word tab_num DW 10 DUP (5) Fonte: adaptado de Quadros (1986) RÓTULOS Segundo Quadros (1986), a forma mais comum de definir um rótulo é colocando no início da linha o nome do rótulo, seguido de :, como exemplificado no quadro 57. QUADRO 57 - EXEMPLO DE RÓTULOS aqui: MOV AX,BX INC BX ali: ADD AX,BX JMP aqui Fonte: adaptado de Quadros (1986)

96 81 O rótulo aqui aponta para a instrução MOV AX,BX, o rótulo ali para a instrução ADD AX,BX. Utilizando esta forma de definição, o rótulo sempre fica com o tipo NEAR. No caso de rotinas, é preferível o uso das diretivas PROC e ENDP (quadro 58). QUADRO 58 - DECLARAÇÃO DE UMA ROTINA nome nome PROC tipo.. código da rotina.. ENDP Fonte: adaptado de Quadros (1986) Neste caso, nome aponta para a instrução seguinte à diretiva PROC e tem o tipo NEAR ou FAR nela especificado. Este tipo irá influir na chamada ao procedimento, que pode ser intrasegmento ou inter-segmento. No primeiro envolve apenas a alteração de deslocamento, sendo salvo na pilha apenas o valor IP. No segundo envolve alteração de segmento e deslocamento, sendo salvo na pilha o valor de CS e IP. Rotinas em Assembly são chamadas com a instrução CALL nome. Para voltar ao ponto que chamou a rotina, utiliza-se a instrução RET, que restaura da pilha os valores de IP ou IP e CS que foram salvos pela última instrução CALL CONSTANTES Conforme em Quadros (1986), na definição de rótulo e variáveis associa-se nomes a endereços de memória. É possível também associar nomes a valores, isto é, definir constantes. Isto é feito através da diretiva EQU (equate), como exemplificado no quadro 59. QUADRO 59 - DECLARAÇÃO DE UMA CONSTANTE tamanho EQU 10 area DB tamanho dup (?) Fonte: adaptado de Quadros (1986)

97 82 No exemplo do quadro 59 o nome tamanho é associado ao valor 10 e o utiliza para definir uma área de 10 bytes ACESSANDO E ALTERANDO CARACTERÍSTICAS DE UM SÍMBOLO Segundo Quadros (1986), ao se referenciar um símbolo é possível precedê-lo de um operador de forma a acessar ou alterar uma de suas características. Os operadores utilizados são: OFFSET, SEG e PTR. Considerando que VAR é uma varável tipo word, o operador OFFSET fornece o deslocamento do símbolo. Por exemplo, a instrução MOV AX,OFFSET VAR copia o deslocamento da variável VAR (dentro do segmento na qual ela foi definida) para AX. O operador SEG fornece o segmento do símbolo. A instrução MOV AX, SEG VAR copia para AX o segmento em que foi definida VAR. Para alterar o tipo de um símbolo utiliza-se o operador PTR. Por exemplo, INC BYTE PTR VAR incrementa o primeiro byte de VAR ESTRUTURA DE ARQUIVOS.COM NA LINGUAGEM ASSEMBLY Os arquivos da família.com contêm apenas um segmento definido. Este segmento contém todos os dados do programa, inclusive o PSP e a pilha. Isto é feito criando-se um segmento para o código e apontando-se através do pseudo-operador ASSUME todos os quatro registradores para o segmento de código (Santos, 1989). O sistema operacional cria o PSP e o inclui nos primeiros 256 bytes do segmento, assim, o ponto de entrada para o programa deve ser definido em 100h através do pseudo-operador ORG. Para definir a pilha o MS-DOS põe no SS o segmento do programa e define SP com o endereço mais alto dentro do segmento. Uma palavra 0000h é colocada na pilha. Todas as rotinas, inclusive a principal em um programa da família.com devem ter o atributo NEAR. No fim do bloco principal, a instrução RET faz com que a palavra 0000 seja retirada da pilha e colocada no registrador IP. Assim, a instrução INT 20h, que está nos 2 bytes iniciais do PSP, é executada e o controle volta ao MS-DOS.

98 83 quadro 60. A estrutura completa de um arquivo.com em linguagem Assembly é apresentada no QUADRO 60 - ESTRUTURA DE UM ARQUIVO.COM CODIGO SEGMENT ASSUME CS:CODIGO,SS:CODIGO,DS:CODIGO,ES:CODIGO ORG 100h ENTRADA: JMP COMECO.. definição das variáveis.. COMECO PROC NEAR... corpo de instruções... RET COMECO ENDP CODIGO ENDS END ENTRADA Fonte: adaptado de Santos (1989) 2.8 AMBIENTE FURBOL O FURBOL é um ambiente de programação integrado onde é possível editar e compilar um código fonte em linguagem com o mesmo nome. O FURBOL vem sendo desenvolvido na Universidade Regional de Blumenau através de Trabalhos de Conclusão de Curso do curso de Ciências da Computação. A última implementação do ambiente foi feita utilizando o ambiente de programação Borland Delphi. A linguagem FURBOL caracteriza-se por ser uma linguagem de alto-nível, blocoestruturada e que utiliza vocabulário da língua portuguesa. O compilador é um tradutor dirigido pela sintaxe que utiliza o método de análise gramatical preditiva, onde a sintaxe é definida utilizando uma gramática livre de contexto apresentada na BNF e a semântica utilizando gramática de atributos. O tradutor traduz o código fonte em um código de montagem na linguagem Assembly que é convertido para linguagem de máquina compatível com o microprocessador Intel 8088 através do montador Turbo Assembler. No final do processo

99 84 obtém-se um arquivo executável no formato.com compatível com o sistema operacional MS- DOS. O último trabalho que adicionou novas extensões à linguagem foi apresentado por Adriano (2001), o qual implementou suporte a estruturas de mapeamento finito (matrizes). Após o último trabalho, o FURBOL possui as seguintes características: a) tipos de dados lógico, inteiro e matriz; b) comandos condicionais se, então e senão; c) comando de repetição enquanto faça; d) comando de saída de dados, através do comando imprime( cadeia ); e) unidades do tipo procedimento com passagem de parâmetro por cópia-valor e referência; O formato de um programa na linguagem FURBOL é apresentado no quadro 61. Quando o programa é executado, ele inicia no bloco de comandos. Deste bloco é possível acessar as variáveis e chamar os procedimentos. QUADRO 61 - ESTRUTURA DE UM PROGRAMA FURBOL programa exemplo;.. variáveis globais.. procedimentos.. inicio... bloco de comandos... fim. Exemplo de declaração de variáveis globais, procedimentos, variáveis locais e comandos da linguagem são mostrados no quadro 62.

100 85 QUADRO 62 - EXEMPLO DE UM PROGRAMA FURBOL programa oimundo; var i: inteiro; sair: logico; m: matriz [1..2]: inteiro; procedimento rotina1; var j: inteiro; inicio se sair = verdadeiro entao inicio imprime( saindo ); fim; fim; inicio i := 0; enquanto i < 2 faca inicio imprime( oi mundo ); inc(i); fim; sair := verdadeiro; rotina1; fim INTERFACE DO AMBIENTE FURBOL A tela principal do ambiente FURBOL apresentado por Adriano (2001) é mostrada na fig. 4. Nesta tela o programador escreve o programa fonte, compila, e executa. Adicionalmente o programador pode abrir ou salvar um programa fonte em arquivos no formato.fur.

101 86 FIGURA 4 - INTERFACE DO AMBIENTE FURBOL Compilar Gerar.COM Executar Visualização do código intermediário Visualização do código de montagem Edição do código fonte O programa escrito no editor de código fonte é compilado quando o usuário clica no botão Compilar (fig. 4). O botão Gerar.COM executa o montador Turbo Assembler que converte o código de montagem gerado após a compilação para um arquivo.com. O botão de executar, executa o arquivo.com gerado pelo montador.

102 87 3 DESENVOLVIMENTO DO PROTÓTIPO Neste capítulo é descrita implementação das unidades concorrentes e dos semáforos no ambiente FURBOL. A seção 3.1 apresenta os aspectos de implementação de concorrência em linguagens de programação segundo Guezzi (1991), onde são vistos aspectos práticos e conceitos sobre a implementação. A implementação da concorrência na linguagem FURBOL é baseada em Sebesta (2000), Arruda (2001) e Guezzi (1991). Na seção 3.2 é descrita a implementação em linguagem Assembly do núcleo utilizado pelo FURBOL. É apresentada uma descrição do funcionamento do núcleo juntamente com descrições detalhadas das estruturas internas e dos procedimentos para manipular essas estruturas. Esses procedimentos, de baixo-nível, são utilizados pela linguagem FURBOL de modo a prover unidades concorrentes e semáforos. A seção 3.3 apresenta as novas estruturas e procedimentos de alto-nível adicionadas a linguagem FURBOL, que permitem a utilização de unidades concorrentes e semáforos pelo programador. A especificação completa da linguagem FURBOL, incluindo as extensões para as unidades concorrentes e para os semáforos, é apresentada na seção 3.4. Na última seção deste capítulo é apresentada a especificação do novo ambiente FURBOL utilizando a UML, onde o código do ambiente apresentado por Adriano (2001) foi revisado e reaproveitado. 3.1 ASPECTOS DE IMPLEMENTAÇÃO DE CONCORRÊNCIA EM LINGUAGENS DE PROGRAMAÇÃO Segundo Guezzi (1991), em um sistema concorrente, processos (tarefas) estão suspensos por alguma forma primitiva de sincronização ou estão potencialmente ativos, isto é, não existem obstáculos lógicos à sua execução. Em geral, somente um subconjunto dos processos potencialmente ativos pode estar em execução, a não ser que existam tantos processadores quantos são os processos potencialmente ativos. No caso comum de sistemas com um único processador, somente um processo pode estar em execução em um certo momento. É então

103 88 comum dizer que um processo pode estar no estado bloqueado, no estado pronto (ou potencialmente ativo, mas no momento não está em execução) e executando. Na programação concorrente, um programador não é responsável pela mudança de estado de um processo de pronto para executando. Isto é feito pela implementação da concorrência na linguagem. Um processo pode mudar do estado pronto para executando em conseqüência de uma preempção, ou seja, uma tomada de posse do controle da CPU. A preempção é uma ação executada pela implementação, que força o processo a abandonar seu estado de execução mesmo que sua execução pudesse continuar sem problemas. A preempção em um processo pode ocorrer depois que ele executa um comando de sincronização ou quando ocorre o fim de um período de tempo (timeslice). A parte de apoio de tempo de execução de uma linguagem concorrente, responsável pela implementação das mudanças de estado é chamada de núcleo (kernel). A informação que o núcleo precisa sobre um processo é representada em um descritor de processo (PCB). Este descritor contém todas as informações necessárias para transformar um processo esperando ou bloqueado em processo em execução. Estas informações são chamadas de status do processo e incluem toda a informação necessária ao processador, como a identidade e ponto de execução do processo (conteúdo dos registradores de máquina: indicador de instruções, registradores, acumuladores etc.). Uma das tarefas do núcleo é a guarda do status do processo, quando este é suspenso, e a restauração do seu status, quando ele volta à execução. O núcleo possui várias estruturas internas que não são acessíveis diretamente pelo programador. O único modo de ter acesso a essas estruturas é através de procedimentos fornecidos pelo núcleo ao programador. As estruturas internas, ou privadas, do núcleo estão organizadas em filas de descritores de processos. Os descritores de processos prontos são enfileirados na fila de prontos. Existem também filas adicionais para cada condição que possa suspender um processo, ou seja, existe uma fila para cada semáforo e uma fila para cada objeto declarado do tipo fila em um monitor. Existe também uma variável interna que indica o descritor que está em execução no momento. A divisão do tempo é implementada por uma interrupção de relógio. Esta interrupção ativa a operação seguinte no núcleo, que suspende o processo mais recente em execução

104 89 colocando-o na fila de prontos e transfere um processo pronto para um estado de execução. Caso o processo tenha sido bloqueado por um semáforo, o núcleo coloca-o na fila do semáforo que o bloqueou e transfere um processo da fila de prontos para um estado de execução. Se algum processo foi desbloqueado por um semáforo, o núcleo coloca o processo em execução no momento na fila de prontos, retira o processo desbloqueado da fila do semáforo e transfere seu estado para executando. 3.2 IMPLEMENTAÇÃO DO NÚCLEO DA LINGUAGEM FURBOL Partindo do princípio de que cada unidade concorrente (tarefa) deve executar por um determinado período de tempo numa máquina com um único microprocessador 8088, a implementação da concorrência é baseada numa rotina tratadora para a interrupção de hardware 8h. A cada 55 milisegundos o controle da CPU é passado para a rotina tratadora da interrupção em questão. Implementando um escalonador de tarefas nesta rotina tratadora é possível efetuar a troca das tarefas do programa gerado pelo Ambiente FURBOL toda vez que a interrupção 8h é gerada pela PIC. Quando o programa gerado pelo FURBOL (programa FURBOL) é executado pelo sistema operacional, ele instala no vetor de interrupção 8h o endereço do escalonador de tarefas. Este escalonador de tarefas é gerado pelo próprio FURBOL e incluído no código de montagem do programa FURBOL como uma rotina qualquer. Após o escalonador ter sido instalado, são chamadas as rotinas de criação de tarefa. Estas rotinas também são incluídas no código de montagem gerado pelo FURBOL. Estas primeiras chamadas têm como função executar o programa principal e seus procedimentos como uma tarefa. Desta forma o programa principal é executado concorrentemente com as outras tarefas que podem ou não ser criadas. O papel da rotina tratadora da interrupção 8h é, toda a vez que for executada via interrupção de hardware ou chamada explícita, fazer a preempção da tarefa que está executando. Quando ocorre a preempção a rotina salva o contexto ou estado da tarefa que estava executando e insere-a no final da fila de prontas. Salvar o contexto da tarefa significa salvar todos os registradores da CPU na própria pilha da tarefa, inclusive seu ponto de execução no momento da preempção. Após a tarefa atual ter sido colocada no final da fila de prontas é retirada uma

105 90 tarefa do início da fila de prontas, seu estado é restaurado, e o controle é passado para ela que fica executando até ocorrer uma nova preempção. Fica claro que na rotina tratadora é implementado um algoritmo baseado no algoritmo de escalonamento de Round- Robin, descrito na seção No geral, o núcleo implementado possui 3 etapas durante a execução de um programa FURBOL: a) inicialização; b) troca das tarefas ou execução concorrente do programa FURBOL; c) finalização. As estruturas de dados internas do núcleo utilizadas para fazer o controle da concorrência e sincronização são: a) descritores de tarefas ou processos (PCBs); b) descritores de semáforos; c) descritores de filas; d) lista de PCBs; e) lista de descritores de semáforos; f) pilha do núcleo; g) pilhas das tarefas. h) fila de prontas; i) filas de semáforos. Internamente no núcleo existem procedimentos que não estão disponíveis diretamente ao programador, mas são utilizados para manipular as estruturas de dados internas. Os procedimentos internos são: a) instalar escalonador (inst_escalonador_proc); b) desinstalar escalonador (deinst_escalonad_proc); c) rotina tratadora da interrupção 8h ou escalonador (escalonador_proc); d) criar nova fila (nova_fila_proc); e) liberar fila (libera_fila_proc); f) inserir no final de fila (insere_fila_proc); g) retirar do início de fila (retira_fila_proc);

106 91 h) criar novo PCB (novo_pcb_proc); i) liberar PCB (libera_pcb_proc); j) criar novo semáforo (novo_smf_proc); k) liberar semáforo (libera_smf_proc). Os procedimentos disponibilizados pelo núcleo para prover concorrência em nível de unidade de programa e controle de sincronização através de semáforos para a linguagem FURBOL são: a) disparar processo (disparar_proc); b) matar processo (morre_proc); c) repassar controle (repassa_proc); d) esperar tarefas filhas (esperar_proc); e) criar semáforo (criar_smf_proc); f) excluir semáforo (excluir_smf_proc); g) operação P em semáforo (p_smf_proc); h) operação V em semáforo (v_smf_proc). O código fonte em Assembly de cada um destes procedimentos está no apêndice 1 deste trabalho. Nas seções subseqüentes é descrito o funcionamento destes procedimentos INICIALIZAÇÃO DO NÚCLEO No momento que o programa.com gerado pelo FURBOL é executado é feita a inicialização do núcleo. A primeira operação feita durante a inicialização é a liberação da memória não utilizada pelo programa FURBOL. Quando o MS-DOS carrega um programa.com, toda a memória disponível é alocada, impedindo a utilização de funções de alocação dinâmica de memória do MS-DOS. Funções de alocação dinâmica de memória são utilizadas intensamente pelo núcleo para alocar e liberar tarefas e semáforos. Essa liberação da memória não utilizada pelo programa é feita redimensionando o bloco de memória do programa.com para o tamanho real do mesmo. O tamanho real é calculado através de um rótulo no final do programa (após todas as instruções) cujo deslocamento equivale ao tamanho do programa, já que o mesmo inicia sempre no deslocamento (offset) 0 de um segmento. A função de redimensionamento de um bloco de

107 92 memória é a função 4Ah da interrupção 21h (serviços do MS-DOS). Mais informações sobre esta liberação de memória num programa.com podem ser vistas em Moon (1999). Em seguida é alocada uma pilha para o núcleo através da função 48h da interrupção 21h. Esta pilha possui um tamanho de 2048 bytes e é utilizada toda a vez que o controle da CPU é passado para escalonador e durante a inicialização e finalização do núcleo. A criação desta pilha se deve a fato de que após a liberação de memória inicial, a memória total alocada pelo.com corresponde a exatamente seu tamanho, sendo necessário alocar um outro bloco de memória para a pilha. Após selecionar a pilha do núcleo (passando para o registrador SS e SP os valores da nova pilha) é criada a fila de prontas através do procedimento interno nova_fila_proc. Este procedimento aloca um descritor de fila e coloca seu endereço de memória na variável interna tmp_fila que é copiado para o ponteiro fila_prontas o qual referencia a fila de prontas durante toda a execução do programa. A fila de prontas e todas as filas dos semáforos criados durante a execução do programa são criadas através do procedimento interno nova_fila_proc. Tanto a fila de prontas quanto as filas dos semáforos são filas de descritores de tarefas ou filas de tarefas. O formato do descritor de filas de PCBs é mostrado na fig. 5. Quando um descritor de fila é criado todos os seus campos são inicializados com 0. FIGURA 5 - DESCRITOR DE FILA DE PCBS 6 bytes Ponteiro para o início da fila Contador da fila Ponteiro para o final da fila Em seguida é criada a tarefa que representa o programa principal. A criação da tarefa principal consiste em duas etapas: alocação e inserção na fila de prontas. A criação da tarefa é feita através do procedimento interno novo_pcb_proc que seleciona um PID para a tarefa, aloca um descritor de tarefa (PCB), inicializa o descritor, aloca a pilha para a tarefa (2048 bytes), cria um contexto inicial para a tarefa e salva este contexto na pilha recém criada. O contexto inicial

108 93 da tarefa consiste em salvar na pilha o endereço do procedimento morre_proc o endereço do procedimento gerado pelo FURBOL que contém o código da tarefa e valores iniciais dos registradores da CPU para aquela tarefa. O descritor de tarefa é mostrado na fig. 6 e o contexto inicial da tarefa armazenado na pilha é mostrado na fig. 7. FIGURA 6 - DESCRITOR DE TAREFA (PCB) 11 bytes PID SS da tarefa Estados 0 - Nova 1 - Pronta 2 Executando 3 - Bloqueada 4 - Desbloqueou 5 - Morta SP da tarefa PID da tarefa pai Quantidade de tarefas SMF filhas semáforo que bloqueou ou desbloqueou Ponteiro para próximo PCB na fila em que se encontra o descritor Quando o PCB é alocado o campo PID (fig. 6) é inicializado com o valor do PID selecionado. O PID pode variar de 0 a 255. O critério de seleção de um PID é selecionar o PID de valor mais baixo que não está sendo utilizado. O campo estado é iniciado com 0 (tarefa nova), SS e SP são inicializados no final da rotina novo_pcb_proc. O campo SMF é inicializado com 0 e o campo PID da tarefa pai é inicializado num processo posterior. O campo de quantidade de tarefas filhas e o ponteiro para o próximo PCB também são inicializados com 0.

109 94 FIGURA 7 - CONTEXTO INICIAL DA TAREFA 2048 bytes Demais registradores. De baixo para cima: AX=0, BX=0, CX=0, DX=0, SI=0, DI=0, DS=CS, ES=CS e BP=0 Base da pilha da tarefa Registrador IP = offset do procedimento da tarefa Registrador CS = segmento do programa.com Registrador de flags = 0 offset de morre_proc Quando a pilha da tarefa (fig. 7) é alocada, o procedimento novo_pcb_proc armazena nos bytes 2046 e 2047 (base da pilha) o offset do procedimento morre_proc. Nos dois bytes anteriores (2042 e 2044) é armazenado o registrador de flags que vai ser utilizado pela nova tarefa, em seguida o valor de CS, depois o offset do procedimento que contém o código da tarefa. Depois mais 18 bytes para os valores iniciais dos registradores da tarefa. Ao fim, a rotina novo_pcb_proc coloca no campo SS do descritor da tarefa (fig. 6) o valor do segmento da pilha

110 95 que foi alocada e no campo SP o ponteiro para o último valor de registrador (BP) do contexto inicial salvo na pilha, ou seja, o topo da pilha da tarefa. Após o descritor da tarefa e a sua pilha terem sido alocados, é colado na lista de PCBs, na posição correspondente ao valor do campo PID, o endereço do PCB alocado. A lista de PCBs é um vetor com 256 posições, cada uma com 2 bytes, todas inicializadas com 0. Se um PCB possui PID 0, seu endereço (apenas o segmento) é colocado na posição 0 do vetor (primeira posição). Se o PID for 1, o endereço do PCB é colocando na posição 1 (segunda posição). Toda a vez que uma tarefa termina (PCB liberado, juntamente com a pilha) é colocado o valor 0 na posição correspondente ao PID na lista de PCBs. Deste modo um programa FURBOL tem um limite de 256 tarefas simultâneas. Ao final da alocação do PCB, o contador interno de PCBs é incrementado. Este processo de alocação de PCBs é o mesmo para todas as tarefas criadas durante a execução do programa. Feita a alocação da tarefa principal, o PCB da tarefa é inserido na fila de prontas através do procedimento interno insere_fila_proc. A penúltima parte da inicialização do núcleo é a instalação do procedimento do escalonador no vetor de interrupção 8h, feita pela rotina inst_escalonador_proc. Esta rotina salva o vetor original numa variável interna do núcleo e coloca no lugar o endereço do escalonador. A última parte é a execução forçada da interrupção 8h através da instrução INT 8h para chamar o escalonador. Ao executar a instrução INT é colocado na pilha do núcleo o registrador de flags, o CS e o IP, sendo que o IP aponta para a próxima instrução imediatamente após a instrução INT 8h TROCA DAS TAREFAS A troca de tarefas é iniciada quando o escalonador é executado pela primeira vez e só termina quando todas as tarefas terminam sua execução. Quando a interrupção 8h for acionada será executada a rotina do escalonador, interrompendo a execução da tarefa atual. Sua operação básica consiste, quando executado, em salvar na pilha da tarefa interrompida o registrador de flags, o registrador de segmento CS, o registrador de ponteiro de instrução atual (IP) e todos os valores dos outros registradores (exceto

111 96 SS e SP), exatamente na mesma ordem apresentada na seção anterior, utilizando instruções PUSH. A rotina de escalonamento não precisa se preocupar em salvar os três primeiros registradores (flags, CS e IP) e nem restaurá-los, pois isto é feito automaticamente pela CPU toda a vez que uma interrupção é processada. Após salvar o contexto da tarefa na pilha da mesma, o escalonador salva a posição da pilha (SP) no campo SP do descritor da tarefa interrompida. Em seguida o escalonador verifica o estado da tarefa (campo estado no PCB) e decide o que fazer. No núcleo implementado para o FURBOL as tarefas podem ter 6 estados: a) nova, quando a tarefa acabou de ser criada; b) pronta, quando a tarefa está pronta para ser executada; c) executando, quando a tarefa está executando; d) bloqueada, quando a tarefa foi bloqueada por executar uma operação P em um semáforo. Neste caso o campo SMF contém o identificador do semáforo em questão; e) desbloqueou, quando a tarefa executou uma operação V em um semáforo e desbloqueou outra tarefa que estava na fila de espera do semáforo. Neste caso o campo SMF do PCB contém o identificador do semáforo em questão; f) morta, quando a tarefa executou o procedimento morre_proc e deve ser liberada da memória. O algoritmo de seleção de tarefas do escalonador, baseado em Sebesta (2000) e Guezzi (1991), é apresentado no quadro 63.

112 97 QUADRO 63 - ALGORITMO DE SELEÇÃO DE TAREFAS DO ESCALONADOR pcb := (PCB da tarefa interrompida); início: se pcb.estado = nova então pcb.estado := executando; despacha; se pcb.estado = pronta então pcb.estado := executando; despacha; se pcb.estado = executando então pcb.estado := pronta; insere_fila(prontas, pcb); pcb := retira_fila(prontas); vai para início; se pcb.estado = bloqueada então insere_fila(pcb.smf.fila, pcb); pcb := retira_fila(prontas); vai para início; se pcb.estado = desbloqueou então pcb.estado := pronta; insere_fila(prontas, pcb); pcb := retira_fila(pcb.smf.fila); pcb.estado := pronta; vai para início; se pcb.estado = morta então se pcb = pcb_principal então termina_programa; senão dec(pcb.pai.filhas); libera_pcb(pcb); pcb := retira_fila(prontas); vai para início; O procedimento despacha (quadro 63) transfere o controle da CPU para a tarefa no pcb. Esta transferência do controle é feita selecionando a pilha da tarefa (isto é feito colocando valores dos campos SS e SP do PCB nos registradores SS e SP), restaurando os registradores BP, ES, DS, DI, SI, DX, CX, BX e AX através de instruções POP, e por fim executando a instrução IRET que restaura da pilha atual (pilha da tarefa) os registradores IP, CS e flags. Assim o controle é transferido para o ponto onde a tarefa tinha sido interrompida. Internamente o procedimento despacha é apenas um rótulo Assembly na rotina do escalonador.

113 98 O procedimento insere_fila recebe como parâmetros uma fila e um PCB. Sua função é inserir o PCB no final da fila informada nos parâmetros. Esta fila pode ser uma fila de semáforo ou a fila de prontas. Internamente este procedimento é o procedimento Assembly insere_fila_proc. A rotina retira_fila (internamente é o procedimento retira_fila_proc) recebe como parâmetro uma fila e retorna um PCB. Quando esta rotina é chamada, ela retira do início da fila passada como parâmetro um PCB e retorna o PCB retirado. No quadro 63 o procedimento termina_programa (internamente é apenas um rótulo Assembly) executa a instrução IRET sem selecionar pilha alguma de tarefa, ou seja, a pilha ativa no momento em que este procedimento é executado é a pilha do núcleo. Durante o processo de inicialização do núcleo foi salvo na pilha o endereço de retorno imediatamente após a instrução INT 8h inicial. Executando um IRET com a pilha do núcleo ativa, irá desviar o fluxo de execução para este ponto, executando o código de finalização do núcleo e do programa. Este procedimento só é executando quando a tarefa cujo estado é morta for a tarefa principal. Caso seja uma outra tarefa, o contador de tarefas filhas da tarefa pai (campo tarefa pai do PCB) é decrementado e a tarefa liberada através do procedimento libera_pcb (internamente libera_pcb_proc). Em seguida é retirada da fila de prontas uma outra tarefa e o controle é passado para esta tarefa. No final da rotina do escalonador é enviado para a porta 20h da PIC um EOI indicando o final do tratamento da interrupção de hardware, conforme visto na seção Na primeira execução do escalonador nenhum contexto de tarefa é salvo em pilha, o fluxo é desviado diretamente para o algoritmo descrito no quadro 63. Na última execução do escalonador (fim de programa) nenhum contexto é restaurado, o controle é passado diretamente para as rotinas de finalização do núcleo conforme descrito no parágrafo anterior IMPLEMENTAÇÃO DAS TAREFAS A implementação de concorrência desenvolvida para o FURBOL executa um procedimento Assembly concorrentemente com outros procedimentos. O único pré-requisito para que o procedimento seja compatível com a implementação da concorrência descrita nas seções anteriores é que antes de executar a instrução RET, o procedimento deve deixar a pilha no ponto em que ela estava no momento em que o fluxo de execução entrou no procedimento. Ao executar a instrução RET será restaurado da pilha o offset do procedimento morre_proc

114 99 (colocado no momento da alocação da tarefa) transferindo o controle para este. Este procedimento marcará a tarefa como morta e em seguida chamará o escalonador que se encarregará de liberar a tarefa. O procedimento morre_proc não marca a tarefa como morta diretamente. Antes de marcar a tarefa como morta ele executa o procedimento espera_proc. O procedimento espera_proc verifica no PCB da tarefa quantas tarefas filhas a tarefa atual possui. Se o valor do campo tarefas filhas for 0, o procedimento retorna e a execução é continuada. Se o valor for maior que zero o escalonador é chamado. Quando o controle voltar para a tarefa em questão, será verificado novamente a quantidade de filhas, e o escalonador será chamado novamente caso a quantidade não seja 0. Desse modo, a tarefa é impedida de prosseguir enquanto tiver filhas. Para uma tarefa chamar o escalonador, ou seja, passar o controle para outra tarefa sem precisar esperar que ocorra uma interrupção 8h existe o procedimento repassa_proc. Este procedimento executa a instrução INT 8h e é utilizado pelo procedimento espera_proc, pelo morre_proc, pelas operações com semáforos, e pela rotina de criação de tarefas em tempo de execução. A partir de uma tarefa é possível criar outras tarefas. Este processo é feito pelo procedimento disparar_proc que agrega as duas operações descritas da criação da tarefa do programa principal na seção O procedimento requer que seja passado como parâmetro o offset de um procedimento Assembly que será executado concorrentemente. Além das operações de alocação de PCB e inserção na fila de prontas, o procedimento disparar_proc faz mais três operações: incrementa o contador de tarefas filhas do PCB da tarefa que o chamou; coloca no campo pai do PCB da tarefa recém criada o PID da tarefa chamadora e chama o escalonador através de repassa_proc IMPLEMENTAÇÃO DOS SEMÁFOROS Os semáforos implementados para o controle de sincronização das tarefas são os semáforos descritos na seção Foram implementados 2 procedimentos em Assembly para a alocação e desalocação de semáforos pelo núcleo: a) novo_smf_proc, procedimento interno chamado pelo criar_smf_proc. Sua função é

115 100 selecionar um identificador para o semáforo (SID), alocar o descritor do semáforo, inicializar o descritor com o valor do contador passado no registrador DI e por último alocar a fila de PCBs do semáforo utilizando a função nova_fila_proc; b) libera_smf_proc, procedimento interno chamado pelo excluir_smf_proc. Sua função é liberar a fila do semáforo através do procedimento libera_fila_proc e liberar o descritor do semáforo. Na fig. 8 é apresentado o formato do descritor de um semáforo. FIGURA 8 - DESCRITOR DE SEMÁFORO (SMF) 5 bytes SID Pointeiro para descritor da fila do semáforo Contador do semáforo O programa FURBOL pode ter no máximo 256 semáforos simultâneos. Existe uma lista interna de descritores de semáforos. Esta lista possui o mesmo formato da lista de PCBs e o SID de um semáforo selecionado no procedimento novo_smf_proc obedece o mesmo critério de seleção do PID de uma tarefa. Para a utilização de semáforos pela linguagem FURBOL foram implementadas em Assembly 4 procedimentos: a) criar_smf_proc, cria um novo semáforo e recebe dois parâmetros: o primeiro com 1 byte que irá receber o identificador do semáforo (SID) e o segundo com 2 bytes (word) onde deve ser passado o valor inicial do contador do semáforo; b) excluir_smf_proc, exclui o semáforo cujo SID (1 byte) é passado como parâmetro; c) p_smf_proc operação P ou esperar do semáforo. Recebe como parâmetro o SID do semáforo que se deseja executar a operação; d) v_smf_proc operação V ou liberar do semáforo. Recebe como parâmetro o SID do semáforo que se deseja executar a operação.

116 101 O funcionamento dos procedimentos criar_smf_proc e excluir_smf_proc foi explicado nos parágrafos acima. Será detalhado nos próximos parágrafos o funcionamento dos procedimentos p_smf_proc e v_smf_proc. O procedimento p_smf_proc executa a operação P no semáforo. Quando chamado por uma tarefa, o procedimento verifica o contador do semáforo. Se for igual a 0 a tarefa é marcada como bloqueada, e é colocado no campo SMF do PCB da tarefa o SID do semáforo. Em seguida é chamado o escalonador através do procedimento repassa_proc que irá tomar as medidas necessárias para esta tarefa, conforme descrito no algoritmo do quadro 63. Se o contador for maior que 0, ele é decrementado e o controle continua com a tarefa chamadora. Quando chamado por uma tarefa, o procedimento v_smf_proc verifica a fila de PCBs do semáforo. Se o campo contador do descritor da fila for maior que 0, o procedimento marca a tarefa chamadora como desbloqueou, coloca no campo SMF do PCB da tarefa chamadora o SID do semáforo, e chama o escalonador que irá se encarregar de desbloquear a tarefa que está no início da fila do semáforo (conforme o quadro 63). Caso a fila do semáforo não tenha nenhum PCB (campo contador do descritor da fila igual a 0) o contador do semáforo é incrementado e o controle continua com a tarefa chamadora FINALIZAÇÃO DO NÚCLEO O código de finalização do núcleo é executado quando todas as tarefas terminam suas execuções. A primeira operação é desinstalar o escalonador da interrupção 8h e restaurar o vetor antigo que foi guardado numa variável interna. Esta operação é feita pelo procedimento deinst_escalonad_proc. Em seguida o PCB da tarefa principal é liberado através da rotina libera_pcb_proc. Na seqüência é liberada a fila de prontas, através da chamada do procedimento libera_fila_proc. Após isto a pilha do núcleo é liberada através de uma chamada direta da função 49h da interrupção 21h. Por último é executada a instrução INT 20h que devolve o controle para o MS-DOS.

117 ESTRUTURA DO ARQUIVO.COM CONCORRENTE EM LINGUAGEM ASSEMBLY Para oferecer suporte a concorrência a estrutura de um arquivo.com em linguagem Assembly foi ligeiramente alterada se comparada com a estrutura apresentada na seção No quadro 64 é apresentada a nova estrutura. CODIGO_SEGMENTO QUADRO 64 - ESTRUTURA DE UM ARQUIVO.COM SEGMENT ASSUME ORG 100h ENTRADA: JMP COMECO.. definição das variáveis.. PROGRAMA_PRINCIPAL PROC NEAR... corpo de instruções... RET PROGRAMA_PRINCIPAL ENDP... procedimentos do núcleo... CS: CODIGO_SEGMENTO, DS: CODIGO_SEGMENTO, SS:NOTHING,ES:NOTHING COMECO:.. código de inicialização do núcleo.. INT 8H.. código de finalização do núcleo.. CODIGO_SEGMENTO ENDS END ENTRADA

118 UNIDADES CONCORRENTES E SEMÁFOROS NA LINGUAGEM FURBOL Este trabalho implementa na linguagem apresentada por Adriano (2001) duas novas estruturas: tarefas e semáforos. Essas novas estruturas estão associadas diretamente aos procedimentos Assembly (descritos nas seções anteriores) disponibilizados pelo núcleo para a linguagem. Essa associação dos procedimentos Assembly com a linguagem são vistos na especificação da linguagem FURBOL na seção TAREFAS Uma unidade do tipo tarefa na linguagem FURBOL assemelha-se a uma unidade do tipo procedimento. Ao todo, existem três diferenças entre tarefas e procedimentos na linguagem FURBOL: a) as tarefas não possuem parâmetros; b) as tarefas não podem ser aninhadas (uma tarefa não pode ser declarada dentro da declaração de outra tarefa; c) ao chamar uma tarefa é criada, além de um novo escopo, uma nova linha de execução (thread) que é executada concorrentemente com a linha de execução do chamador e com as linhas de execução das outras tarefas. Todo programa FURBOL possui no mínimo uma tarefa implícita. Esta tarefa é responsável pela linha de execução do programa principal, permitindo que este seja executado concorrentemente com as outras tarefas explicitamente criadas. Uma tarefa é formada pelo cabeçalho, pela declaração da estrutura de dados locais (variáveis locais), pela declaração dos procedimentos locais e pelo corpo. O cabeçalho consiste na palavra reservada tarefa seguida de um identificador que representa o nome da mesma, e ao fim, um ponto e vírgula. Em seguida, opcionalmente, são declaradas as variáveis locais, obedecendo a sintaxe do FURBOL para este tipo de construção. Após a declaração das variáveis locais, também opcionalmente, são declarados os procedimentos locais da tarefa, também seguindo a sintaxe do FURBOL. Por último é declarado o corpo da tarefa, onde são colocados os enunciados que serão executados concorrentemente com o resto do programa. O corpo da tarefa obedece a sintaxe do FURBOL para os blocos de comandos, onde uma seqüência de

119 104 comandos da linguagem é colocada entre as palavras reservadas inicio e fim. As tarefas devem ser declaradas no programa principal. Não podem ser declaradas dentro de procedimentos (tarefas locais) nem dentro de outras tarefas (tarefas aninhadas). No quadro 65 é mostrada a sintaxe de declaração de tarefa na linguagem FURBOL. QUADRO 65 - SINTAXE DE TAREFA NA LINGUAGEM FURBOL tarefa nome_da_tarefa;. variáveis locais. procedimentos locais.. inicio.. comandos.. fim; As regras de escopo para as tarefas são as mesmas regras para os procedimentos e variáveis apresentadas na seção Todas as chamadas a procedimentos declarados no programa principal, o qual possui sua própria linha de execução, feitas dentro do corpo de uma tarefa, executarão os procedimentos correspondentes na mesma linha de execução da tarefa. Para criar uma tarefa e iniciar sua execução é necessário chamá-la através do nome da tarefa seguido de um ponto e vírgula, da mesma forma que é chamado um procedimento. Chamadas posteriores de uma mesma tarefa criarão outras tarefas que, apesar de terem o mesmo código, terão dados locais próprios. No quadro 66 é mostrado um exemplo de criação e execução de tarefa.

120 105 QUADRO 66 - EXEMPLO DE CRIAÇÃO E EXECUÇÃO DE UMA TAREFA programa exemplo1; var j: inteiro; tarefa calcula; inicio j := j + 1 se j = 2 entao inicio imprime( j = 2 ); fim; fim; inicio j := 1; imprime( j = 1 ); calcula; fim. No exemplo do quadro 66 é atribuído a variável global j o valor 1, que em seguida é impresso na tela. Após isto, é criada a calcula que soma 1 ao valor de j e imprime o novo valor de j na tela. No quadro 67 é apresentado um outro exemplo que mostra a execução concorrente de duas tarefas.

121 106 QUADRO 67 - EXEMPLO DE CRIAÇÃO E EXECUÇÃO DE DUAS TAREFAS programa exemplo2; tarefa tarefa1; var i: inteiro; inicio i := 0; enquanto i < 9999 faca inicio imprime( tarefa1 ); i := i + 1; fim; fim; tarefa tarefa2; var i: inteiro; inicio i := 0; enquanto i < 9999 faca inicio imprime( tarefa2 ); i := i + 1; fim; fim; inicio imprime( inicio ); tarefa1; tarefa2; imprime( fim ); fim. Ao executar o programa do quadro 67, primeiramente, será impresso na tela a palavra inicio. Após isto, será criada a tarefa1 que iniciará sua execução. Durante sua execução, que é independente da execução do programa principal, será criada a tarefa2. As duas tarefas estarão executando concorrentemente, e cada uma irá escrever seu nome 9999 vezes na tela. A ordem que esses nomes irão aparecer é incerta, pois ambas escreverão o que for possível durante o tempo que é dado a cada uma pelo escalonador. Do mesmo modo, o programa principal irá escrever na tela a palavra fim na primeira chance que tiver, mas especificamente, no momento em que o escalonador passar o controle a ele após a criação da segunda tarefa.

122 COMANDO REPASSA O procedimento repassa_proc, que é bastante utilizado no núcleo, serve para forçar uma chamada do escalonador. Segundo Sebesta (2000), a linguagem Java possui um comando chamado yield que passa o controle para outra tarefa. Na linguagem FURBOL o procedimento repassa_proc pode ser chamado através do comando repassa. Dessa forma é possível forçar uma preempção da tarefa sem ter que esperar que a PIC gere uma interrupção para chamar o escalonador COMANDO ESPERA O procedimento espera_proc do núcleo foi disponibilizado na linguagem FURBOL através do comando espera. A função desde comando é interromper a execução da tarefa atual enquanto tarefas filhas não terminarem sua execução. No quadro 68 é mostrado um exemplo de uso deste comando. QUADRO 68 - EXEMPLO DE USO DO COMANDO ESPERA programa exemploespera; tarefa filha; var x: inteiro; inicio x := 0; enquanto x < faca inicio imprime( filha ); inc(x); fim; fim; tarefa pai; inicio imprime( pai ); filha; espera; imprime( filha terminou ); fim; inicio pai; fim.

123 108 O programa do quadro 68 cria uma tarefa chamada pai que escreve na tela seu nome e cria uma tarefa filha. A tarefa filha escreve vezes na tela a palavra filha. Porém a tarefa pai só vai escrever filha terminou quando sua filha terminar a execução por completo. Uma tarefa A é filha de uma tarefa B quando a tarefa A é executada dentro do código da tarefa B COMANDO MORRE De modo a forçar o término da execução de uma tarefa, é disponibilizado na linguagem FURBOL o comando morre. Todas as instruções após o comando morre não são executadas. Caso a tarefa que chamou morre tenha filhas ativas, esta ficará bloqueada até que suas filhas terminem suas execuções. O comando morre executa o comando espera implicitamente antes de terminar a execução de uma tarefa. No quadro 69 é mostrada a utilização do comando. QUADRO 69 - EXEMPLO DE USO DO COMANDO MORRE programa exemplo_morre; tarefa tarefa1; inicio imprime( tarefa1 ); morre; imprime( não será impresso ); fim; inicio tarefa1; fim SEMÁFOROS Foi implementado na linguagem FURBOL um novo tipo de dado, os semáforos. Para se utilizar um semáforo é necessário declarar uma variável tipo semaforo. Os semáforos fornecem para a linguagem FURBOL sincronização de competição e cooperação. Para a utilização deste mecanismo existem 4 comandos na linguagem: a) criarsmf, cria um semáforo e inicializa o contador interno; b) excluirsmf, exclui um semáforo (libera da memória); c) p, efetua a operação P no semáforo;

124 109 d) v, efetua a operação V no semáforo. Um exemplo de uso de semáforos na linguagem FURBOL é mostrado no quadro 70, onde é apresentada uma implementação da aplicação produtor-consumidor, descrita na seção QUADRO 70 - APLICAÇÃO PRODUTOR-CONSUMIDOR NA LINGUAGEM FURBOL programa produtor_consumidor; var cheio, vazio: semaforo; tarefa produtor; var c: inteiro; inicio c := 4; enquanto c > 0 faca inicio imprime("p -> Produzindo..."); p(vazio); imprime("p -> Armazenado!"); v(cheio); dec(c); fim; fim; tarefa consumidor; var c: inteiro; inicio c := 4; enquanto c > 0 faca inicio p(cheio); imprime("c -> Retirado!"); v(vazio); imprime("c -> Consumindo..."); dec(c); fim; fim; inicio criarsmf(cheio, 0); criarsmf(vazio, 2); produtor; consumidor; espera; excluirsmf(cheio); excluirsmf(vazio); fim.

125 110 Todo semáforo criado através do comando criarsmf deve ser liberado quando não for mais utilizado. A liberação é feita com o comando excluirsmf. O primeiro parâmetro do comando criarsmf deve ser uma variável do tipo semáforo, que receberá o SID do semáforo criado. O segundo parâmetro pode ser uma variável, expressão ou literal do tipo inteiro e deve conter o valor inicial do contador interno do semáforo. Os comandos p e v possuem apenas um único parâmetro que é o semáforo no qual se deseja efetuar a operação P ou V, respectivamente. O comando excluirsmf também possui apenas um único parâmetro do tipo semáforo que referencia o semáforo que se deseja liberar. 3.4 ESPECIFICAÇÃO DA LINGUAGEM FURBOL Nesta seção é apresentada a especificação da linguagem de programação FURBOL. Ela é uma extensão da especificação apresentada em Adriano (2001) na qual foram incluídas regras e ações semânticas para o tratamento das estruturas de tarefas e semáforos. Também estão incluídas nesta especificação rotinas de controle de escopo apresentadas em Bieging (2002). Os símbolos em negrito são símbolos terminais da gramática. As palavras entre apóstrofes ( ) também são consideradas como símbolos terminais, podendo ser palavras reservadas ou qualquer outro token. A palavras em itálico DeclaraDadosGlobais, EmiteInter e EmiteObjeto geram a declaração das variáveis globais, a saída do tradutor em código de três endereços e a saída do tradutor em linguagem de montagem Assembly, respectivamente. A palavra Simbolos e demais palavras associadas fazem parte das rotinas de gerenciamento de escopo. Informações sobre estas rotinas de escopo podem ser vistas em Bieging (2002). A palavra CRLF representa uma quebra de linha ou nova linha. Quebras de linha são necessárias para gerar o código intermediário e de montagem na saída do tradutor, uma vez que as instruções são separadas por linha. As demais palavras são consideradas elementos não-terminais (como por exemplo, EstruturaDados, ComandoComposto etc), os quais possuem derivações. A palavra vazia é representada pelo símbolo circunflexo (^). Outras palavras que possuem significado especial são descritas no decorrer da especificação.

126 111 Os atributos codigo e codasm contêm o código intermediário e o código Assembly respectivamente. O símbolo é utilizado para representar a concatenação dos enunciados e atributos que armazenam os códigos na medida que são gerados. Essas definições foram implementadas no protótipo conforme a regra de implementação de analisadores sintáticos preditivos descrita na seção , onde cada não-terminal é implementado na forma de um procedimento e o seu código imita o lado direito da produção PROGRAMA PRINCIPAL E BLOCO DE COMANDOS A definição do programa e dos blocos de comandos é mostrada no quadro 71. As palavras CarregaProcsNucleo e CarregaIniFinalNucleo representam a carga dos procedimentos do núcleo e o código de inicialização e finalização, respectivamente. Ambas as palavras são agrupamentos de chamadas seqüenciais da função CarregaRotina que carrega um arquivo que contém o código da rotina passada no parâmetro.

127 112 QUADRO 71 - DEFINIÇÃO DO PROGRAMA PRINCIPAL E BLOCOS Programa 'programa' EmiteObjeto('.8086'); EmiteObjeto('codigo_segmento segment'); EmiteObjeto('assume cs:codigo_segmento,' 'ds:codigo_segmento,ss:nothing,es:nothing'); EmiteObjeto('org 100h'); id EmiteInter(id.nome '0'); EmiteInter('goto ' id.nome); EmiteObjeto('entrada: jmp ' id.nome); ';' Simbolos.AbrirEscopo('Principal'); EstruturaDados EstruturaSubRotinas CComposto Programa.codigo := EstruturaSubRotinas.codigo CRLF 'principal proc near' CRLF CComposto.codigo CRLF 'ret' CRLF 'endp' CRLF id.nome ':' CRLF 'int 8h' ':' CRLF 'int 20h' CRLF Programa.codasm := EstruturaSubRotinas.codasm CRLF 'principal_proc proc near' CRLF ' bp' CRLF ' bp,sp' CRLF CComposto.codasm CRLF ' bp' CRLF 'principal_proc endp' CRLF CarregaProcsNucleo CRLF id.nome ':' CRLF CarregaIniFinalNucleo CRLF EmiteInter(Programa.codigo); EmiteObjeto(Programa.codasm); '.' DeclaraDadosGlobais; EmiteObjeto('codigo_segmento ends'); EmiteObjeto('end entrada'); CComposto 'inicio' Comando CComposto.codigo := Comando.codigo; CComposto.codasm := Comando.codasm; 'fim'

128 DECLARAÇÕES DE VARIÁVEIS E DEFINIÇÕES DE TIPOS A definição da declaração das variáveis (estruturas de dados) é mostrada no quadro 72. QUADRO 72 - DEFINIÇÃO DA DECLARAÇÃO DE VARIÁVEIS EstruturaDados 'var' id Se não Simbolos.SimboloRedeclarado(id.nome) então Simbolos.Instalar(TSimbolo.Create(id.nome)); ListaID (Matriz ^) Simbolos.AtualizarUltimosSimbolos(ListaID.tipo); ';' Declaracoes ^ Declaracoes id Se não Simbolos.SimboloRedeclarado(id.nome) então Simbolos.Instalar(TSimbolo.Create(id.nome)); ListaID (Matriz ^) Simbolos.AtualizarUltimosSimbolos(ListaID.tipo); ';' Declaracoes ^ ListaID ',' id Se não Simbolos.SimboloRedeclarado(id.nome) então Simbolos.Instalar(TSimbolo.Create(id.nome)); ListaID (Matriz ^) Simbolos.AtualizarUltimosSimbolos(ListaID.tipo); ':' Tipo ListaID.tipo := Tipo.tipo;

129 114 No quadro 73 é mostrada a definição dos tipos de dados da linguagem FURBOL. O tipo semáforo faz parte da extensão da linguagem desenvolvida neste trabalho. QUADRO 73 - DEFINIÇÃO DOS TIPOS DE DADOS Tipo 'inteiro' Tipo.tipo := tsinteiro; 'logico' Tipo.tipo := tslogico; 'matriz' Tipo.tipo := tsmatriz; 'semaforo' Tipo.tipo := tssemaforo; Matriz LimitesM Simbolos.AtualizarUltimosSimbolos(LimitesM.tipo) ':' ListaID Matriz.tipo := ListaID.tipo LimitesM '[' Dimensao ']' ^ Dimensao num Dimensao.liminf := num.valor '..' num Dimensao.limsup := num.valor MaisDimensao MaisDimensao ',' Dimensao ^

130 PROCEDIMENTOS E TAREFAS No quadro 74 é apresentada a definição de sub-rotinas e a definição de procedimentos. No quadro 75 é apresentada a definição da unidade de programa tarefa que foi adicionada neste trabalho. QUADRO 74 - DEFINIÇÃO DE SUB-ROTINAS E PROCEDIMENTOS EstruturaSubRotinas 'procedimento' EstruturaProcedimento EstruturaSubRotinas.codigo := EstruturaProcedimento.codigo; EstruturaSubRotinas.codasm := EstruturaProcedimento.codasm; 'tarefa' Se Simbolos.EscopoAtual.Nivel > 0 então erro; EstruturaTarefa EstruturaSubRotinas.codigo := EstruturaTarefa.codigo; EstruturaSubRotinas.codasm := EstruturaTarefa.codasm; ^ EstruturaProcedimento id Se não Simbolos.SimboloRedeclarado(id.nome) então id.simbobj := TSimbolo.Create(id.nome, tsprocedimento) Simbolos.Instalar(id.simbobj) id.simbobj.tabelaagregada := Simbolos.AbrirEscopo(id.nome); ParamFormais ';' EstruturaDados EstruturaSubRotinas CComposto EstruturaProcedimento.codasm := (id.nome 'proc near' CRLF ' bp' CRLF ' bp,sp' CRLF 'sub sp,' Simbolos.EscopoAtual.LarguraVariaveis CRLF CComposto.codasm CRLF ' sp,bp' CRLF ' bp' CRLF 'ret ' Simbolos.EscopoAtual.LarguraParametros CRLF id.nome 'endp' CRLF EstruturaSubRotinas.codasm; Simbolos.FecharEscopo; ';' EstruturaSubRotinas EstruturaProcedimento.codigo := EstruturaProcedimento.codigo EstruturaSubRotinas.codigo; EstruturaProcedimento.codasm := EstruturaProcedimento.codasm EstruturaSubRotinas.codasm;

131 116 QUADRO 75 - DEFINIÇÃO DE TAREFAS EstruturaTarefa id Se não Simbolos.SimboloRedeclarado(id.nome) então id.simbobj := TSimbolo.Create(id.nome, tstarefa) Simbolos.Instalar(id.simbobj) id.simbobj.tabelaagregada := Simbolos.AbrirEscopo(id.nome); ';' EstruturaDados EstruturaSubRotinas CComposto EstruturaTarefa.codasm := (id.nome 'proc near' CRLF ' bp' CRLF ' bp,sp' CRLF 'sub sp,' Simbolos.EscopoAtual.LarguraVariaveis CRLF CComposto.codasm CRLF ' sp,bp' CRLF ' bp' CRLF 'ret ' Simbolos.EscopoAtual.LarguraParametros CRLF id.nome 'endp' CRLF EstruturaSubRotinas.codasm; Simbolos.FecharEscopo; ';' EstruturaSubRotinas EstruturaTarefa.codigo := EstruturaTarefa.codigo EstruturaSubRotinas.codigo; EstruturaTarefa.codasm := EstruturaTarefa.codasm EstruturaSubRotinas.codasm; Conforme mostrado no quadro 75 toda vez que é encontrada a palavra reservada tarefa, o compilador inclui um código de montagem que chama o procedimento do núcleo dispara_proc que aloca e executa a tarefa passada como parâmetro. No quadro 76 é apresentada a definição de parâmetros formais dos procedimentos da linguagem FURBOL.

132 117 QUADRO 76 - DEFINIÇÃO DE PARÂMETROS FORMAIS ParamFormais '(' (ParamValor ParamRef) SecaoParam ')' ^ SecaoParam ';' (ParamValor ParamRef) SecaoParam ^ ParamValor id Se não Simbolos.SimboloRedeclarado(id.nome) então Simbolos.Instalar(TSimbolo.Create(id.nome)); ListaID Simbolos.AtualizarUltimosSimbolos; ParamRef 'ref' id Se não Simbolos.SimboloRedeclarado(id.nome) então Simbolos.Instalar(TSimbolo.Create(id.nome)); ListaID Simbolos.AtualizarUltimosSimbolos; COMANDOS DA LINGUAGEM Nos quadros 77 a 84 são apresentadas as definições das estruturas dos comandos da linguagem. Os comandos são reconhecidos nas definições dos quadros 77 e 78 e o código para eles é gerado pelas definições dos quadros que vão do 79 até o quadro 84. As definições para o reconhecimento e geração de código dos novos comandos implementados neste trabalho são mostrados nos quadros 77, 78, 83 e 84.

133 118 QUADRO 77 - DEFINIÇÃO DOS COMANDOS DA LINGUAGEM Comando id ChamProc L, Atribuicao Simb := Simbolos.ProcurarSimbolo(id.nome); Se Simb.tipo = tsprocedimento então Comando.codigo := ChamProc.codigo GeraCodigoEloEstatico 'chamada ' id.nome; Comando.codasm := ChamProc.codasm GeraCodigoEloEstatico 'call ' id.nome; Senão Se Simb.tipo = tstarefa então Comando.codigo := 'disparar(' id.nome ')'; Comando.codasm := ' ax' CRLF ' ax,offset ' id.nome CRLF ' ax' CRLF 'call disparar_proc' CRLF ' ax' CRLF; Senão Comando.codigo := Atribuicao.codigo; Comando.codasm := Atribuicao.codasm; Virgula Comando.codigo := Comando.codigo Virgula.codigo; Comando.codasm := Comando.codasm Virgula.codasm; CCondicional Virgula Comando.codigo := CCondicional.codigo Virgula.codigo; Comando.codasm := CCondicional.codasm Virgula.codasm; CRepeticao Virgula Comando.codigo := CRepeticao.codigo Virgula.codigo; Comando.codasm := CRepeticao.codasm Virgula.codasm; CEntrada Virgula Comando.codigo := CEntrada.codigo Virgula.codigo; Comando.codasm := CEntrada.codasm Virgula.codasm; CSaida Virgula Comando.codigo := CSaida.codigo Virgula.codigo; Comando.codasm := CSaida.codasm Virgula.codasm; CInc Virgula Comando.codigo := CInc.codigo Virgula.codigo; Comando.codasm := CInc.codasm Virgula.codasm; CDec Virgula Comando.codigo := CDec.codigo Virgula.codigo; Comando.codasm := CDec.codasm Virgula.codasm; 'nl' Virgula Comando.codigo := 'nl' Virgula.codigo; Comando.codasm := 'nop' Virgula.codasm;

134 119 QUADRO 78 - DEFINIÇÃO DOS COMANDOS DA LINGUAGEM (CONTINUAÇÃO) Comando 'morre' Virgula Comando.codigo := 'morre' Virgula.codigo; Comando.codasm := 'call morre_proc' Virgula.codasm; 'espera' Virgula Comando.codigo := 'espera' Virgula.codigo; Comando.codasm := 'call espera_proc' Virgula.codasm; 'repassa' Virgula Comando.codigo := 'repassa' Virgula.codigo; Comando.codasm := 'call repassa_proc' Virgula.codasm; 'criarsmf' CCriarSmf Virgula Comando.codigo := CCriarSmf.codigo Virgula.codigo; Comando.codasm := CCriarSmf.codasm Virgula.codasm; 'excluirsmf' CExcluirSmf Virgula Comando.codigo := CExcluirSmf.codigo Virgula.codigo; Comando.codasm := CExcluirSmf.codasm Virgula.codasm; 'p' CP Virgula Comando.codigo := CP.codigo Virgula.codigo; Comando.codasm := CP.codasm Virgula.codasm; 'v' CV Virgula Comando.codigo := CV.codigo Virgula.codigo; Comando.codasm := CV.codasm Virgula.codasm; Virgula Comando.codigo := Virgula.codigo; Comando.codasm := Virgula.codasm; No quadro 78 é mostrado a definição para fazer o reconhecimento dos comandos das tarefas (morre, espera e repassa) juntamente com a geração do código de montagem deles. Para os comandos de semáforos é mostrada apenas a definição para fazer o reconhecimento dos

135 120 mesmos (criarsmf, excluirsmf, p e v). As regras semânticas para geração de código destes comandos é apresentada mais adiante nos quadros 83 e 84. QUADRO 79 - DEFINIÇÃO DOS COMANDOS DE ATRIBUIÇÃO, DEFINIÇÃO DA ESTRUTURA DE CHAMADA DE PROCEDIMENTO E PARÂMERTROS ATUAIS Virgula ';' Comando Virgula.codigo := Comando.codigo Virgula.codasm := Comando.codasm ^ Atribuicao ':=' Se Atribuicao.deslocamento <> '' então RI := Novo_T; IndCodAsm := ' ' RI ',' Ldesloca CRLF ' ' RI; PreIdAt := ' di'; Expressao E.local := Expresssao.local; Se Expressao.tipo <> Atribuicao.tipo entao erro; Se Expressao.deslocamento <> ' ' então Atribuicao.codigo := gerar(idat'['l.deslocamento']:='e.local); Senão Atribuicao.codigo := gerar(idat ':=' E.Local); RX := Registrador; Atribuicao.codasm := IndCodAsm Expressao.codasm AtPrelocal CRLF ' ' RX ',' atlocal CRLF PreIdAT CRLF ' ' IdAT ',' RX; ChamProc '(' ParN := 0; ParamAtuais ChamProc.codasm := ParamAtuais.codasm; ')' ^ ParamAtuais id ParN := ParN + 1; SimbVar := Simbolos.SimboloPorNome(id.nome); SimbPar := TabelaSimbolos.[ParN - 1]; Se SimbVar.tipo <> SimbPar.tipo então erro('tipos incompatíveis'); Se SimbPar.parametro = tpvalor então ParamAtuais.codasm := ' ' SimbVar.nome CRLF ParamAtuais.codasm; senão ParamAtuais.codasm := 'lea di,' SimbVar.nome CRLF ' di' CRLF ParamAtuais.codasm; ',' ParamAtuais ^

136 121 QUADRO 80 - DEFINIÇÃO DO COMANDO DE REPETIÇÃO E COMANDOS CONDICIONAIS CRepeticao 'enquanto' CRepeticao.inicio := Novo_L; Expressao.v := Novo_L; Expressao.f := CRepeticao.prox; Expressao 'faca' CComposto CComposto.prox := CRpeticao.inicio; CRepeticao.codigo:= CRepeticao.inicio ':' Expressao.codigo E.v ':' CRLF CComposto.codigo CRLF 'goto ' CRepeticao.inicio; CRepeticao.codasm := CRepeticao.inicio ':" Expressao.codasm E.v ':' CRLF CComposto.codasm CRLF 'jmp' CRepeticao.inicio; CCondicional 'se' Expressao 'entao' CComposto CComposto1.prox := CCondicional.prox; CCondicional.codigo := Expressao.codigo E.v ':' CRLF CComposto.codigo; CCondicional.codasm := Expressao.codasm E.v ':' CRLF CComposto.codasm; CCondicional2 CCondicional.codigo := CCondicional.codigo CCondicional2.codigo; CCondicional.codasm := CCondicional.codasm CCondicional2.codasm; CCondicional2 'senao' CComposto fproximo := proximo; CCondicional2.codigo := Expressao.codigo CRLF 'goto ' proximo CRLF f ':' CRLF CComposto.codigo; CCondicional2.codasm := Expressao.codasm CRLF 'jmp ' proximo CRLF f ':' CRLF CComposto.codasm; ^ CCondicional2.codigo := CCondicional2.codigo CRLF f ':'; CCondicional2.codasm := CCondicional2.codasm CRLF f ':';

137 122 QUADRO 81 - DEFINIÇÃO DOS COMANDOS DE ENTRADA E SAÍDA CEntrada 'leitura' '(' id Se Simbolos.ProcurarSimbolo(id.nome) = nil então erro('identificador não declarado'); LeituraListaID ')' CEntrada.codigo := 'leitura(' id.nome ',' LeituraListaID.codigo; LeituraListaID ',' id Se Simbolos.ProcurarSimbolo(id.nome) = nil então erro('identificador não declarado'); LeituraListaID.codigo := LeituraListaID.codigo ',' id.nome; LeituraListaID ^ CSaida 'imprime' '(' Expressao ListaExpressao ')' CSaida.codigo := 'imprime(' Expressao.codigo ',' ListaExpressao.codigo ; ListaExpressao ',' Expressao ListaExpressao.codigo := ListaExpressao.codigo Expressao.codigo; ListaExpressao ^

138 123 QUADRO 82 - DEFINIÇÃO DOS COMANDOS INCREMENTO E DECREMENTO CInc '(' id Se Simbolos.ProcurarSimbolo(id.nome) = nil então erro('identificador não declarado'); Se id.tipo <> tsinteiro então erro('tipo incopatível'); CInc.codigo := 'inc ' '(' id.nome ')'; CInc.codasm := 'inc ' id.nome; ')' CDec '(' id Se Simbolos.ProcurarSimbolo(id.nome) = nil então erro('identificador não declarado'); Se id.tipo <> tsinteiro então erro('tipo incopatível'); CDec.codigo := 'dec ' '(' id.nome ')'; CDec.codasm := 'dec ' id.nome; ')' No quadro 83 é mostrada a definição dos comandos de criação e exclusão de semáforos. Essas definições reconhecem os parâmetros dos comandos e geram o código de montagem. Para o comando criarsmf, o qual deve retornar o identificador do semáforo numa variável, foi implementada uma passagem de parâmetro diferente das suportadas pelo FURBOL. O procedimento Assembly criar_smf_proc, quando executado com sucesso, altera diretamente na pilha o valor parâmetro que deve receber o identificador do novo semáforo. Em seguida a definição gera um código que copia o valor colocado na pilha por criar_smf_proc para a variável que foi passada como parâmetro. A palavra ObtemCódigoAssembly é uma função que calcula o endereço de um símbolo (no caso, da variável que deverá receber o identificador do semáforo) e retorna-o num formato de operando Assembly que endereça um dado na memória no modo direto indexado. Esta função faz parte das definições de escopo utilizadas para a implementação do protótipo e é utilizada também para os comandos P e V. Informações adicionais sobre estas definições podem ser vistas em Bieging (2002).

139 124 QUADRO 83 - DEFINIÇÃO DOS COMANDOS DE CRIAÇÃO E EXCLUSÃO DE SEMÁFOROS CCriarSmf '(' id Se Simbolos.ProcurarSimbolo(id.nome) = nil então erro('identificador não declarado'); Se id.tipo <> tssemaforo então erro('tipo incopatível'); ',' Expressao Se Expressao.tipo <> tsinteiro então erro; ')' CCriarSmf.codigo := Expressao.codigo 'criarsmf(' id.nome Expressao.local ')'; CCriarSmf.codasm := Expressao.codasm ' di' CRLF ' cx' CRLF ' ' Expressao.local CRLF 'sub sp,2 CRLF 'call criar_smf_proc' CRLF ' cx' CRLF ' ' GeraCodigoAssembly(id.nome) ',cl' CRLF 'add sp,2' CRLF ' cx' CRLF ' di' CRLF; CExcluirSmf '(' id ')' CExcluirSmf.codigo := 'excluirsmf(' id.nome ')'; CExcluirSmf.codasm := ' di' CRLF ' cx' CRLF 'xor cx,cx' CRLF ' cl,' GeraCodigoAssembly(id.nome) CRLF ' cx' CRLF 'call excluir_smf_proc' CRLF ' cx' CRLF ' di' CRLF;

140 125 No quadro 84 são apresentadas as definições responsáveis pelo reconhecimento dos parâmetros e pela geração de código dos comandos P e V para semáforos. QUADRO 84 - DEFINIÇÃO DOS COMANDOS DE OPERAÇÃO P E V EM SEMÁFOROS CP '(' id ')' CP.codigo := 'p(' id.nome ')'; CP.codasm := ' di' CRLF ' cx' CRLF 'xor cx,cx' CRLF ' cl,' GeraCodigoAssembly(id.nome) CRLF ' cx' CRLF 'call p_smf_proc' CRLF ' cx' CRLF ' di' CRLF; CV '(' id ')' CV.codigo := 'v(' id.nome ')'; CV.codasm := ' di' CRLF ' cx' CRLF 'xor cx,cx' CRLF ' cl,' GeraCodigoAssembly(id.nome) CRLF ' cx' CRLF 'call v_smf_proc' CRLF ' cx' CRLF ' di' CRLF; EXPRESSÕES A estrutura de controle de expressões pode ser vista nos quadros 85, 86, 87, 88, 89 e 90. Esta definição foi construída a partir da definição de precedência de operadores definida em Aho (1995).

141 126 QUADRO 85 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES Expressao Expressao2 Relacao.v := Expressao2.v; Relacao.f := Expressao2.f; Relacao.local := Expressao2.local; Relacao.codigo := Expressao2.codigo; Relacao.codasm := Expressao2.codasm; Relacao Expressao.local := Relacao.local; Expressao.codigo := Relacao.codigo; Expressao.codasm := Relacao.codasm; Expressao.v := Relacao.v; Expressao.f := Relacao.f; Expressao2 TC ELi.v := TC.v; ELi.f := TC.f; ELi.local := TC.local; ELi.codigo := TC.codigo; ELi.codasm := TC.codasm; EL Expressao2.local := ELs.local; Expressao2.codigo := ELs.codigo; Expressao2.codasm := ELs.codasm; QUADRO 86 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES (CONTINUAÇÃO) Relacao '=' Relacao.codigo := 'se ' Relacao.local ' = ' Expressao2.local 'goto ' E.v 'goto ' E.f; Relacao.codasm := ' ' R0 ',' Relacao.local CRLF 'cmp ' R0 ',' local CRLF 'je ' Rv CRLF 'jmp ' Rf; '<>' Relacao.codigo := 'se ' Relacao.local ' <> ' Expressao2.local 'goto' E.v 'goto' E.f; Relacao.codasm := ' ' R0 ',' Relacao.local CRLF 'cmp ' R0 ',' local CRLF 'jne ' Rv CRLF 'jmp ' Rf; '<' Relacao.codigo := 'se ' Relacao.local ' < ' Expressao2.local 'goto ' E.v 'goto ' E.f; Relacao.codasm := ' ' R0 ',' Relacao.local CRLF 'cmp ' R0 ',' local CRLF 'jb ' Rv CRLF 'jmp ' Rf; '>' Relacao.codigo := 'se ' Relacao.local ' > ' Expressao2.local 'goto ' E.v 'goto ' E.f; Relacao.codasm := ' ' R0 ',' Relacao.local CRLF 'cmp ' R0 ',' local CRLF 'ja ' Rv CRLF 'jmp ' Rf; ^

142 127 QUADRO 87 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES (CONTINUAÇÃO) EL '+' TC EL1i.local := Novo_T; EL1i.codigo := EL.codigo TC.codigo CRLF EL1i.local ':=' EL.local '+' TC.local; EL1i.codasm := EL.codigo TC.codigo CRLF ' ' RX ',' EL.local CRLF 'add ' RX ',' Tc.local CRLF ' ' EL1i.local ',' RX; EL1 EL.local := EL1s.local; EL.codigo := EL1s.codigo; EL.codasm := EL1s.codasm; '-' TC EL1i.local := Novo_T; EL1i.codigo := EL.codigo TC.codigo CRLF EL1i.local ':=' EL.local '-' TC.local; EL1i.codasm := EL.codigo TC.codigo CRLF ' ' RX ',' EL.local CRLF 'sub ' RX ',' Tc.local CRLF ' ' EL1i.local ',' RX; EL1 EL.local := EL1s.local; EL.codigo := EL1s.codigo; EL.codasm := EL1s.codasm; ^ EL.v := ELs.v; EL.f := ELs.f; EL.local := ELs.local; EL.codigo := ELs.codigo; EL.codasm := ELs.codasm; TC F TL1.v := F.v; TL1.f := F.f; TL1.local := F.local; TL1.codigo := F.codigo; TL1.codasm := F.codasm; TL T.v := TLs.v; T.f := TLs.f; TC.local := TLs.local; TC.codasm := TLs.codasm; TC.codigo := TLs.codigo;

143 128 QUADRO 88 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES (CONTINUAÇÃO) TL '*' F TL1.local := Novo_T; TL1.codigo := TL.codigo F.codigo CRLF TL1.local ':=' TL.local '*' F.local; TL1.codasm : = TL.codigo F.codigo CRLF ' ax,' TLLOCAL CRLF 'mul ' Local CRLF ' ' TliLocal ',ax'; '/' TL1 TL.local := TL1s.local; TL.codigo := TL1s.codigo; TL.codasm := TL1s.codasm; F TL1.local := Novo_T; TL1.codigo := TL.codigo F.codigo CRLF TL1.local ':=' TL.local '/' F.local; TL1.codasm : = TL.codigo F.codigo CRLF ' ax, ' TLLOCAL CRLF 'div ' Local CRLF ' ' TliLocal ',ax' CRLF ; TL1 TL.local := TL1s.local; TL.codigo := TL1s.codigo; TL.codasm := TL1s.codasm; 'E' F TL.v := Novo_L; TL.f := TL1.f; F.v := TL1.v; F.f := TL1.f; TL1.codigo := TL.codigo TL1.v ':' F.codigo; TL1.codasm := TL.codasm TL1.v ':' F.codasm; TL1 TL.v := TL1s.v; TL.f := TL1s.f; TL.codasm := TL1s.codasm; TL.codigo := TL1s.codigo; ^ TL.v := TLs.v; TL.f := TLs.f; TL.local := TLs.local; TL.codigo := TLs.codigo; TL.codasm := TLs.codasm;

144 129 QUADRO 89 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES (CONTINUAÇÃO) F '(' Expressao.v := F.v; Expressao.f := F.f; Expressao ')' F.local := Expressao.local; F.codigo := Expressao.codigo; F.codasm := Expressao.codasm; '-' Expressao F.local := Novo_T; F.codigo := Expressao.codigo CRLF gerar(f.local ':=' ' uminus ' E.local); 'nao' Expressao.v := F.f; Expressao.f := F.v; Expressao L Se Simbolos.ProcurarSimbolo(id.nome) então Se L.deslocamento := 0 então F.local := L.local; senão F.local := Novo_T; gerar(f.local ':=' L.local '[' L.deslocamento ']'); num F.local := num.valor; F.codigo := ''; F.codasm := ''; ^ L Lista_E ']' L.local := Novo_T; L.deslocamento := Novo_T; L.codigo := gerar(l.local := c(lista_e.array); L.codigo := gerar(l.deslocamento ':=' Lista_E.local '*' Largura(Lista_E.array)); L.codasm := Lista_E.codasm CRLF ' dx,' Lista_E.local CRLF ' al,' VerificaTipoMatriz(Lista_E.array) CRLF 'mul dx' CRLF ' ' L.deslocamento ',ax' CRLF 'add ' L.deslocamento ',' Lc CRLF 'add ' L.deslocamento ',2'; id L.local := id.local; L.deslocamento := '';

145 130 QUADRO 90 - DEFINIÇÃO DA ESTRUTURA DE CONTROLE DE EXPRESSÕES (CONTINUAÇÃO) Lista_E id '[' Expressao Ri.matriz := id.local; Ri.local := Expressao.local; Ri.ndim := 1; R Lista_E.matriz := Rs.matriz; Lista_E.local := Rs.local; Lista_E.ndim := Rs.ndim; R Expressao t := Novo_T; m := Ri.ndim + 1; gerar(t ':=' Lista_E.local) limite(lista_e.matriz,m); R1i.matriz := Ri.matriz; R1i.local := t; R1i.ndim := m; R1i.codasm := Expressao.codasm CRLF ' cx,' local CRLF ' al,' limite(lista_e.array,m) CRLF 'mul cx' CRLF ' ' t ',ax' CRLF 'add ' t ',' Elocal; R1 Rs.matriz := R1s.matriz; Rs.local := R1s.local; Rs.ndim := R1s.ndim; Rs.codasm := R1s.codasm; ^ Rs.matriz := Ri.matriz; Rs.local := Ri.local; Rs.ndim := Ri.ndim; Rs.codasm := Ri.codasm;

146 ESPECIFICAÇÃO DO AMBIENTE FURBOL A implementação do ambiente de programação FURBOL reutilizou parte do código do analisador léxico e do analisador sintático apresentado em Adriano (2001). O código reutilizado foi revisado e estendido para a implementação das extensões da definição formal da linguagem FURBOL. O restante do ambiente (editor, rotinas que executam o montador e o ligador) foi reescrito. A programação estruturada do ambiente adotada em Adriano (2001) foi remodelada para seguir padrões de orientação a objetos. A primeira etapa desta remodelagem foi a especificação do ambiente na UML utilizando a ferramenta case Rational Rose 4.0 e a segunda e última etapa foi a implementação da especificação no ambiente Borland Delphi 6.0. Nas seções seguintes são apresentados 3 diagramas da UML que formam a especificação do trabalho. Os diagramas são mostrados nesta ordem: diagrama de casos de uso (fig. 9), diagrama de classes (fig. 10) e diagrama seqüência (fig. 11). Este último especificando apenas o processo principal do ambiente, a compilação. FIGURA 9 - DIAGRAMA DE CASOS DE USO DO AMBIENTE FURBOL No diagrama de casos de uso (fig. 9) são especificados 4 casos: a) entrada do fonte, que pode ser feita com o comando Abrir ou Novo; neste último o usuário digita o texto no editor; b) salvar fonte, onde o usuário salva o fonte digitado ou modificado num arquivo.fur;

147 132 c) compilar, onde o usuário compila o fonte. Esta operação inclui a análise léxica, análise sintática, montagem e ligação; d) executar, onde o usuário executa o programa.com gerado pelo ambiente. No diagrama de classes (fig. 10) são mostradas as classes implementadas no ambiente FURBOL. FIGURA 10 - DIAGRAMA DE CLASSES DO AMBIENTE FURBOL Na fig. 10 as linhas pontilhadas indicam uma relação de dependência entre duas classes. Este tipo de relação serve para mostrar que uma classe cliente depende de uma classe

148 133 fornecedora porque a cliente precisa de certos serviços da fornecedora. Por exemplo, a classe cliente executa métodos da fornecedora, ou, a classe cliente lê valores de atributos da classe fornecedora. O triângulo indica qual a classe é a cliente (Rational, 2002). No diagrama de classes (fig. 10) a classe TFormPrincipal representa a interface com o usuário. Os métodos Novo, Abrir, Salvar, SalvarComo, Sair, Compilar, Executar e VerSaida são implementados utilizando o componente TActionList disponível no ambiente Borland Delphi 6.0. O usuário executa diretamente estes métodos através de botões e menus. O método Novo limpa o editor para que seja escrito um novo programa fonte. O comando Abrir serve para carregar um arquivo.fur no editor e o comando Salvar serve para salvar o programa fonte atual em um arquivo.fur. O comando Executar executa o programa compilado. A opção VerSaída mostra para o usuário as mensagens de montagem e ligação emitidas pelo Turbo Assembler e pelo Turbo Link durante o processo de montagem e ligação que ocorre automaticamente após a compilação do programa fonte. O comando Compilar compila o código fonte e gera o arquivo.com. Este processo é detalhado no diagrama de seqüência (fig. 11). A classe TAnalisadorLexico implementa a parte de análise léxica do compilador. Sua principal função é enviar ao analisador sintático (TAnalisadorSintatico) o fluxo de tokens que este necessita para traduzir o código fonte. O analisador sintático efetua chamadas a métodos do analisador léxico para pedir o próximo token do código fonte e para verificar o tipo do token. Desta forma, a classe TAnalisadorSintatico é uma classe cliente da TAnalisadorLexico. O analisador léxico tem como entrada o código fonte digitado ou carregado pelo programador. Este código fonte é lido via atributo TFormPrincipal.MemoFonte.Lines. Assim, a classe TAnalisadorLexico é cliente da classe TFormPrincipal. A classe TAnalisadorSintatico traduz o fluxo de tokens vindo do analisador léxico em um código intermediário de três endereços e o código de montagem. Ela implementa um analisador sintático preditivo. Todo processo de compilação inicia quando o método Traduzir da classe é invocado. Este método inicializa o analisador sintático e executa o procedimento correspondente à primeira produção da gramática da linguagem FURBOL, onde a partir daí começa a tradução do código fonte. Os procedimentos que implementam a especificação da linguagem não foram especificados na UML embora estejam implementados como métodos

149 134 privados na classe. A implementação desses procedimentos foi feita diretamente a partir da especificação da linguagem. A classe TMontadorLigador tem como função executar o montador Turbo Assembler e o ligador Turbo Link para converter o código de montagem vindo do analisador sintático em um executável no formato.com. Desta forma a classe TMontadorLigador é uma classe cliente da TAnalisadorSintatico. Por fim, o processo de compilação é especificado no diagrama de seqüência na fig. 11. O parâmetro TStrings no método Create do analisador léxico serve para passar o atributo TFormPrincipal.MemoFonte.Lines que contém as linhas de código do programa fonte no editor.

150 135 FIGURA 11 - DIAGRAMA DE SEQÜÊNCIA DO PROCESSO COMPILAR 3.6 APRESENTAÇÃO DO PROTÓTIPO O objetivo deste trabalho foi adicionar uma extensão à linguagem FURBOL possibilitando concorrência em nível de unidade e controle de sincronização através de semáforos, utilizando métodos formais na especificação da extensão da linguagem e UML para especificar o ambiente integrado.

151 136 A especificação da extensão da linguagem foi feita utilizando a gramática de atributos da mesma forma que foi feito no trabalho apresentado por Adriano (2001). A especificação do ambiente foi feita na UML com o auxílio da ferramenta case Rational Rose 4.0. A implementação da especificação, tanto a da linguagem quanto a do ambiente, foi feita no ambiente Borland Delphi 6.0 usando a linguagem de programação Object Pascal. O código apresentado em Adriano (2001) foi revisado e reaproveitado. O desenvolvimento do núcleo responsável pela concorrência da linguagem foi escrito na linguagem Asssembly e seu código é incorporado ao programa fonte durante a compilação. Isto é feito por uma rotina que carrega do disco os arquivos fonte do núcleo. Estes arquivos fazem parte do protótipo. Se durante a execução de um programa FURBOL todas as tarefas forem bloqueadas por semáforos (um deadlock no programa) a fila de prontas do núcleo ficará vazia, pois todos os PCB s estarão nas filas dos semáforos. Caso esta situação aconteça, será impressa na tela uma mensagem informando que ocorreu um underflow na fila de prontas (o escalonador irá tentar retirar um PCB da fila de prontas que está vazia) e a execução do programa FURBOL será interrompida. No apêndice 1 deste trabalho está todo o código fonte do núcleo em linguagem Assembly, devidamente comentado. Na fig. 12 é mostrada a interface final do ambiente FURBOL.

152 137 FIGURA 12 - INTERFACE FINAL DO AMBIENTE FURBOL Compilar Executar Editor de código fonte Código Intermediário Código de montagem

153 138 Na fig. 13 é mostrado o código intermediário de 3 endereços e na fig. 14 o código de montagem em linguagem Assembly gerado pelo compilador. FIGURA 13 - CÓDIGO INTERMEDIÁRIO GERADO PELO AMBIENTE FIGURA 14 - CÓDIGO DE MONTAGEM GERADO PELO AMBIENTE

Introdução à Programação

Introdução à Programação Introdução à Programação Linguagens de Programação: sintaxe e semântica de linguagens de programação e conceitos de linguagens interpretadas e compiladas Engenharia da Computação Professor: Críston Pereira

Leia mais

Tradução Dirigida Pela Sintaxe

Tradução Dirigida Pela Sintaxe Tradução Dirigida Pela Sintaxe Julho 2006 Sugestão de leitura: Livro do Aho, Sethi, Ullman (dragão) Seções 5.1 5.5 Tradução dirigida pela sintaxe É uma técnica que permite realizar tradução (geração de

Leia mais

IMPLEMENTAÇÃO DE MAPEAMENTO FINITO (ARRAYS) DINÂMICO NO AMBIENTE FURBOL

IMPLEMENTAÇÃO DE MAPEAMENTO FINITO (ARRAYS) DINÂMICO NO AMBIENTE FURBOL UNIVERSIDADE REGIONAL DE BLUMENAU CENTRO DE CIÊNCIAS EXATAS E NATURAIS CURSO DE CIÊNCIAS DA COMPUTAÇÃO (Bacharelado) IMPLEMENTAÇÃO DE MAPEAMENTO FINITO (ARRAYS) DINÂMICO NO AMBIENTE FURBOL TRABALHO DE

Leia mais

Um Compilador Simples. Definição de uma Linguagem. Estrutura de Vanguarda. Gramática Livre de Contexto. Exemplo 1

Um Compilador Simples. Definição de uma Linguagem. Estrutura de Vanguarda. Gramática Livre de Contexto. Exemplo 1 Definição de uma Linguagem Linguagem= sintaxe + semântica Especificação da sintaxe: gramática livre de contexto, BNF (Backus-Naur Form) Especificação Semântica: informal (textual), operacional, denotacional,

Leia mais

Compiladores. Motivação. Tradutores. Motivação. Tipos de Tradutores. Tipos de Tradutores

Compiladores. Motivação. Tradutores. Motivação. Tipos de Tradutores. Tipos de Tradutores Motivação Prof. Sérgio Faustino Compiladores Conhecimento das estruturas e algoritmos usados na implementação de linguagens: noções importantes sobre uso de memória, eficiência, etc. Aplicabilidade freqüente

Leia mais

Compiladores. Introdução à Compiladores

Compiladores. Introdução à Compiladores Compiladores Introdução à Compiladores Cristiano Lehrer, M.Sc. Introdução (1/2) O meio mais eficaz de comunicação entre pessoas é a linguagem (língua ou idioma). Na programação de computadores, uma linguagem

Leia mais

INE5416 Paradigmas de Programação. Ricardo Azambuja Silveira INE CTC UFSC E Mail: URL:

INE5416 Paradigmas de Programação. Ricardo Azambuja Silveira INE CTC UFSC E Mail: URL: INE5416 Paradigmas de Programação Ricardo Azambuja Silveira INE CTC UFSC E Mail: silveira@inf.ufsc.br URL: www.inf.ufsc.br/~silveira Conceitos Léxica estudo dos símbolos que compõem uma linguagem Sintaxe

Leia mais

FERRAMENTA DE AUXÍLIO AO PROCESSO DE DESENVOLVIMENTO DE SOFTWARE INTEGRANDO TECNOLOGIAS OTIMIZADORAS

FERRAMENTA DE AUXÍLIO AO PROCESSO DE DESENVOLVIMENTO DE SOFTWARE INTEGRANDO TECNOLOGIAS OTIMIZADORAS FERRAMENTA DE AUXÍLIO AO PROCESSO DE DESENVOLVIMENTO DE SOFTWARE INTEGRANDO TECNOLOGIAS OTIMIZADORAS Acadêmico: Roger Anderson Schmidt Orientador : Marcel Hugo Supervisor : Ricardo de Freitas Becker Empresa

Leia mais

V.2 Especificação Sintática de Linguagens de Programação

V.2 Especificação Sintática de Linguagens de Programação V.2 Especificação Sintática de Linguagens de Programação Deve ser baseada: No planejamento da Linguagem / Compilador Objetivos, Filosofia, Potencialidades,... Nos critérios de projeto/avaliação Legibilidade,

Leia mais

COMPILADORES. Análise semântica. Prof. Geovane Griesang Universidade de Santa Cruz do Sul UNISC Departamento de informática

COMPILADORES. Análise semântica. Prof. Geovane Griesang Universidade de Santa Cruz do Sul UNISC Departamento de informática Universidade de Santa Cruz do Sul UNISC Departamento de informática COMPILADORES Análise semântica Parte 01 Prof. geovanegriesang@unisc.br Sumário Data 18/11/2013 Análise sintática Parte 01 25/11/2013

Leia mais

Análise Sintática. Fabiano Baldo

Análise Sintática. Fabiano Baldo Compiladores Análise Sintática Fabiano Baldo Gramáticas Livre de Contexto (GLC) É utilizada na especificação formal lda sintaxe de uma linguagem de programação. É um conjunto de produções ou regras gramaticais

Leia mais

Projeto de Compiladores

Projeto de Compiladores Projeto de Compiladores FIR Faculdade Integrada do Recife João Ferreira 12 e 13 de fevereiro de 2007 Questionário 1. Em quais linguagens de programação você já programou? 2. O que você sabe sobre compiladores?

Leia mais

Lembrando análise semântica. Compiladores. Implementação de esquemas de tradução L-atribuídos. Exemplo de implementação top-down (1)

Lembrando análise semântica. Compiladores. Implementação de esquemas de tradução L-atribuídos. Exemplo de implementação top-down (1) Lembrando análise semântica Compiladores Geração de código intermediário (1) Parser Bottom-up: squema S-atribuído sem problema Apenas atributos sintetizados squema L-atribuído: ok, mas deve-se usar variáveis

Leia mais

Universidade de Santa Cruz do Sul UNISC Departamento de informática COMPILADORES. Introdução. Geovane Griesang

Universidade de Santa Cruz do Sul UNISC Departamento de informática COMPILADORES. Introdução. Geovane Griesang Universidade de Santa Cruz do Sul UNISC Departamento de informática COMPILADORES Introdução geovanegriesang@unisc.br Processadores de linguagem Linguagens de programação são notações para se descrever

Leia mais

CP Compiladores I Prof. Msc.. Carlos de Salles

CP Compiladores I Prof. Msc.. Carlos de Salles CP 5017.9 Prof. Msc.. Carlos de Salles 1 - EMENTA O Processo de Compilação. Deteção e Recuperação de Erros. Introdução à geração de Código Intermediário. Geração de Código de Máquina. Otimização. Uma visão

Leia mais

Introdução Uma linguagem de programação apoiada em um paradigma imperativo apresenta algum grau de dificuldade nos aspectos relativos ao contexto;

Introdução Uma linguagem de programação apoiada em um paradigma imperativo apresenta algum grau de dificuldade nos aspectos relativos ao contexto; Introdução Uma linguagem de programação apoiada em um paradigma imperativo apresenta algum grau de dificuldade nos aspectos relativos ao contexto; Nas linguagens imperativas o processamento é baseado no

Leia mais

Análise Sintática I. Eduardo Ferreira dos Santos. Abril, Ciência da Computação Centro Universitário de Brasília UniCEUB 1 / 42

Análise Sintática I. Eduardo Ferreira dos Santos. Abril, Ciência da Computação Centro Universitário de Brasília UniCEUB 1 / 42 Análise Sintática I Eduardo Ferreira dos Santos Ciência da Computação Centro Universitário de Brasília UniCEUB Abril, 2017 1 / 42 Sumário 1 Introdução 2 Derivações 3 Ambiguidade 4 Análise sintática descendente

Leia mais

FACULDADE LEÃO SAMPAIO

FACULDADE LEÃO SAMPAIO FACULDADE LEÃO SAMPAIO Paradigmas de Programação Curso de Análise e Desenvolvimento de Sistemas Turma: 309-5 Semestre - 2014.2 Paradigmas de Programação Prof. MSc. Isaac Bezerra de Oliveira. 1 PARADIGMAS

Leia mais

Interfaces de Vanguarda do Compilador

Interfaces de Vanguarda do Compilador Interfaces de Vanguarda do Compilador Stefani Henrique Ramalho¹, Prof Mário Rubens Welerson Sott¹ ¹DCC Departamento de Ciência da Computação Universidade Presidente Antônio Carlos (UNIPAC) Barbacena MG

Leia mais

Universidade Federal de Goiás Bacharelado em Ciências da Computacão Compiladores

Universidade Federal de Goiás Bacharelado em Ciências da Computacão Compiladores Universidade Federal de Goiás Bacharelado em Ciências da Computacão Compiladores 2013-2 Compilador para a Linguagem Cafezinho Especificação dos trabalhos: T2 (Geração da Representação Intermediária e Análise

Leia mais

15/03/2018. Professor Ariel da Silva Dias Aspectos sintáticos e semânticos básicos de linguagens de programação

15/03/2018. Professor Ariel da Silva Dias Aspectos sintáticos e semânticos básicos de linguagens de programação Professor Ariel da Silva Dias Aspectos sintáticos e semânticos básicos de linguagens de programação Conjunto de regras que definem a forma da linguagem; Como as sentenças podem ser formadas como sequências

Leia mais

Compiladores I Prof. Ricardo Santos (cap 1)

Compiladores I Prof. Ricardo Santos (cap 1) Compiladores I Prof. Ricardo Santos (cap 1) Compiladores Linguagens de programação são notações que permitem descrever como programas devem executar em uma máquina Mas, antes do programa executar, deve

Leia mais

Análise Sintática II. Eduardo Ferreira dos Santos. Outubro, Ciência da Computação Centro Universitário de Brasília UniCEUB 1 / 34

Análise Sintática II. Eduardo Ferreira dos Santos. Outubro, Ciência da Computação Centro Universitário de Brasília UniCEUB 1 / 34 Análise Sintática II Eduardo Ferreira dos Santos Ciência da Computação Centro Universitário de Brasília UniCEUB Outubro, 2016 1 / 34 Sumário 1 Introdução 2 Ambiguidade 3 Análise sintática descendente 4

Leia mais

Introdução. Compiladores Análise Semântica. Introdução. Introdução. Introdução. Introdução 11/3/2008

Introdução. Compiladores Análise Semântica. Introdução. Introdução. Introdução. Introdução 11/3/2008 Compiladores Análise Semântica Fabiano Baldo Análise Semântica é por vezes referenciada como análise sensível ao contexto porque lida com algumas semânticas simples tais como o uso de uma variável somente

Leia mais

Compiladores. Análise Sintática

Compiladores. Análise Sintática Compiladores Análise Sintática Cristiano Lehrer, M.Sc. Introdução (1/3) A análise sintática constitui a segunda fase de um tradutor. Sua função é verificar se as construções usadas no programa estão gramaticalmente

Leia mais

Sintaxe e Semântica. George Darmiton da Cunha Cavalcanti.

Sintaxe e Semântica. George Darmiton da Cunha Cavalcanti. Sintaxe e Semântica George Darmiton da Cunha Cavalcanti (gdcc@cin.ufpe.br) Tópicos Introdução O problema de descrever a sintaxe Métodos formais para descrever a sintaxe Gramáticas de atributos Descrevendo

Leia mais

Compiladores. Introdução

Compiladores. Introdução Compiladores Introdução Apresentação Turma Noite Continuada I 20/03 Continuada II 22/05 Atividades Regimental 05/06 Total 1 Ponto 1 Ponto 1 Ponto 7 Pontos 10 Pontos Aulas expositivas teórico-práticas Exercícios

Leia mais

COMPILADORES. Análise sintática. Prof. Geovane Griesang Universidade de Santa Cruz do Sul UNISC Departamento de informática

COMPILADORES. Análise sintática. Prof. Geovane Griesang Universidade de Santa Cruz do Sul UNISC Departamento de informática Universidade de Santa Cruz do Sul UNISC Departamento de informática COMPILADORES Análise sintática Parte 02 Prof. geovanegriesang@unisc.br Data Conteúdo 23/09/2013 3. Análise Sintática: 3.1 analisadores

Leia mais

Compiladores. Análise Léxica

Compiladores. Análise Léxica Compiladores Análise Léxica Regras Léxicas Especificam o conjunto de caracteres que constituem o alfabeto da linguagem, bem como a maneira que eles podem ser combinados; Exemplo Pascal: letras maiúsculas

Leia mais

Compiladores e Computabilidade

Compiladores e Computabilidade Compiladores e Computabilidade Prof. Leandro C. Fernandes UNIP Universidade Paulista, 2013 GERAÇÃO DE CÓDIGO INTERMEDIÁRIO Geração de Código Intermediário Corresponde a 1ª etapa do processo de Síntese

Leia mais

Plano da aula. Compiladores. Os erros típicos são sintáticos. Análise Sintática. Usando Gramáticas. Os erros típicos são sintáticos

Plano da aula. Compiladores. Os erros típicos são sintáticos. Análise Sintática. Usando Gramáticas. Os erros típicos são sintáticos Plano da aula Compiladores Análise sintática (1) Revisão: Gramáticas Livres de Contexto 1 Introdução: porque a análise sintática? Noções sobre Gramáticas Livres de Contexto: Definição Propriedades Derivações

Leia mais

IMPLEMENTAÇÃO DE MAPEAMENTO FINITO (ARRAY S) NO AMBIENTE FURBOL

IMPLEMENTAÇÃO DE MAPEAMENTO FINITO (ARRAY S) NO AMBIENTE FURBOL UNIVERSIDADE REGIONAL DE BLUMENAU CENTRO DE CIÊNCIAS EXATAS E NATURAIS CURSO DE CIÊNCIAS DA COMPUTAÇÃO (Bacharelado) IMPLEMENTAÇÃO DE MAPEAMENTO FINITO (ARRAY S) NO AMBIENTE FURBOL TRABALHO DE CONCLUSÃO

Leia mais

Vantagens de uma Gramática. Sintaxe de uma Linguagem. Analisador Sintático - Parser. Papel do Analisador Sintático. Tiposde Parsers para Gramáticas

Vantagens de uma Gramática. Sintaxe de uma Linguagem. Analisador Sintático - Parser. Papel do Analisador Sintático. Tiposde Parsers para Gramáticas Sintaxe de uma Linguagem Cada LP possui regras que descrevem a estrutura sintática dos programas. specificada através de uma gramática livre de contexto, BNF (Backus-Naur Form). 1 Vantagens de uma Gramática

Leia mais

INE5421 LINGUAGENS FORMAIS E COMPILADORES

INE5421 LINGUAGENS FORMAIS E COMPILADORES INE5421 LINGUAGENS FORMAIS E COMPILADORES PLANO DE ENSINO Objetivo geral Conhecer a teoria das linguagens formais visando sua aplicação na especificação de linguagens de programação e na construção de

Leia mais

Compiladores Analisador Sintático. Prof. Antonio Felicio Netto Ciência da Computação

Compiladores Analisador Sintático. Prof. Antonio Felicio Netto Ciência da Computação Compiladores Analisador Sintático Prof. Antonio Felicio Netto antonio.felicio@anhanguera.com Ciência da Computação 1 Análise Sintática - A Análise Sintática constitui a segunda fase de um tradutor de uma

Leia mais

II.1 Conceitos Fundamentais. Uma delas é programar o =>

II.1 Conceitos Fundamentais. Uma delas é programar o => II.1 Conceitos Fundamentais II.2 Gerações das Linguagens de Programação II.3 Linguagem de Programação II.4 Sistema Operacional II.5 Tradutores II.5.1 Estrutura de um tradutor II.5.1.1 Análise Léxica II.5.1.3

Leia mais

CAP. VI ANÁLISE SEMÂNTICA

CAP. VI ANÁLISE SEMÂNTICA CAP. VI ANÁLISE SEMÂNTICA VI.1 Introdução Semântica SIGNIFICADO, SENTIDO LÓGICO, COERÊNCIA,... Diferença entre SINTAXE e SEMÂNTICA Sintaxe : descreve as estruturas de uma linguagem; Semântica : descreve

Leia mais

Apresentação. !! Familiarização com os métodos de construção de compiladores de linguagens e com as técnicas de compilação mais habituais.

Apresentação. !! Familiarização com os métodos de construção de compiladores de linguagens e com as técnicas de compilação mais habituais. Apresentação Universidade dos Açores Departamento de Matemática www.uac.pt/~hguerra/!! Aquisição de conceitos sobre a definição de linguagens de programação.!! Familiarização com os métodos de construção

Leia mais

Especificações Gerais do Compilador e Definição de FRANKIE

Especificações Gerais do Compilador e Definição de FRANKIE Especificações Gerais do Compilador e Definição de FRANKIE 1. Especificações Gerais do Compilador (Decisões de projeto) 2. Especificações da Linguagem Fonte Definição Informal Considerações Léxicas Considerações

Leia mais

Projeto de Compiladores

Projeto de Compiladores Projeto de Compiladores FIR Faculdade Integrada do Recife João Ferreira 26 e 27 de fevereiro de 2007 Agenda da Aula Revisão Linguagem de Programação Tradutores Compilador As Fases de Um Compilador Linguagem

Leia mais

Compiladores. Prof. Bruno Moreno Aula 8 02/05/2011

Compiladores. Prof. Bruno Moreno Aula 8 02/05/2011 Compiladores Prof. Bruno Moreno Aula 8 02/05/2011 RECONHECIMENTO DE TOKENS Reconhecimento de Tokens Até aqui aprendemos a identificar tokens Para reconhecimento, a única abordagem utilizada foi árvores

Leia mais

Linguagens Livres de Contexto

Linguagens Livres de Contexto Linguagens Livres de Contexto 1 Roteiro Gramáticas livres de contexto Representação de linguagens livres de contexto Formas normais para gramáticas livres de contexto Gramáticas ambíguas Autômatos de Pilha

Leia mais

Construção de Compiladores

Construção de Compiladores Construção de Compiladores Parte 1 Introdução Linguagens e Gramáticas F.A. Vanini IC Unicamp Klais Soluções Motivação Porque compiladores? São ferramentas fundamentais no processo de desenvolvimento de

Leia mais

Universidade Estadual da Paraíba - UEPB Curso de Licenciatura em Computação

Universidade Estadual da Paraíba - UEPB Curso de Licenciatura em Computação Universidade Estadual da Paraíba - UEPB Curso de Licenciatura em Computação Análise Semântica Disciplina: Compiladores Equipe: Luiz Carlos dos Anjos Filho José Ferreira Júnior Compiladores Um compilador

Leia mais

V Análise Sintática. V.1.1 Gramáticas Livres de Contexto Definições de GLC

V Análise Sintática. V.1.1 Gramáticas Livres de Contexto Definições de GLC V Análise Sintática V.1 Fundamentos Teóricos V.1.1 G.L.C V.1.2 Teoria de Parsing V.2 Especificação Sintática de Ling. de Prog. V.3 - Implementação de PARSER s V.4 - Especificação Sintática da Linguagem

Leia mais

UNIVERSIDADE FEDERAL RURAL DO SEMI-ÁRIDO CURSO: CIÊNCIA DA COMPUTAÇÃO. Prof.ª Danielle Casillo

UNIVERSIDADE FEDERAL RURAL DO SEMI-ÁRIDO CURSO: CIÊNCIA DA COMPUTAÇÃO. Prof.ª Danielle Casillo UNIVERSIDADE FEDERAL RURAL DO SEMI-ÁRIDO CURSO: CIÊNCIA DA COMPUTAÇÃO Prof.ª Danielle Casillo Diferentes computadores podem ter diferentes arquiteturas e os diversos tipos de linguagem de programação.

Leia mais

Conceitos de Linguagens de Programação

Conceitos de Linguagens de Programação Conceitos de Linguagens de Programação Aula 04 Sintaxe e Semântica Edirlei Soares de Lima Sintaxe e Semântica A descrição de uma linguagem de programação envolve dois aspectos principais:

Leia mais

Introdução aos Compiladores

Introdução aos Compiladores Universidade Católica de Pelotas Introdução aos Compiladores André Rauber Du Bois dubois@ucpel.tche.br 1 MOTIVAÇÃO Entender os algor ıtmos e estruturas usados para se implementar linguagens de programação

Leia mais

Linguagens de Programação

Linguagens de Programação O estudante estuda muito. Regras: 7 9 12 14. . Regras: 2 4 . Regras: 1 Representar através de uma árvore de derivação. 77 O estudante estuda muito.

Leia mais

Conversões de Linguagens: Tradução, Montagem, Compilação, Ligação e Interpretação

Conversões de Linguagens: Tradução, Montagem, Compilação, Ligação e Interpretação Conversões de Linguagens: Tradução, Montagem, Compilação, Ligação e Interpretação Para executar uma tarefa qualquer, um computador precisa receber instruções precisas sobre o que fazer. Uma seqüência adequada

Leia mais

Conceitos de Linguagens de Programação

Conceitos de Linguagens de Programação Conceitos de Linguagens de Programação Aula 06 Análise Sintática (Implementação) Edirlei Soares de Lima Análise Sintática A maioria dos compiladores separam a tarefa da análise sintática

Leia mais

Paradigmas de Programação

Paradigmas de Programação Paradigmas de Programação Prof.: Edilberto M. Silva http://www.edilms.eti.br Aula 2 Linguagens de Programação Desenvolvimento e execução de programas Características de linguagens Execução de programas

Leia mais

Linguagens de Programação Aula 3

Linguagens de Programação Aula 3 Aula 3 Celso Olivete Júnior olivete@fct.unesp.br Na aula passada... Classificação das LPs (nível, geração e paradigma) Paradigmas Imperativo, OO, funcional, lógico e concorrente 2/33 Na aula de hoje...

Leia mais

Introdução à Programação Aula 03. Prof. Max Santana Rolemberg Farias Colegiado de Engenharia de Computação

Introdução à Programação Aula 03. Prof. Max Santana Rolemberg Farias Colegiado de Engenharia de Computação Aula 03 Prof. Max Santana Rolemberg Farias max.santana@univasf.edu.br Colegiado de Engenharia de Computação Linguagens de Programação A primeira linguagem de programação foi criada por Ada Lovelace. Amiga

Leia mais

Pró-Reitoria Acadêmica Diretoria Acadêmica Assessoria Pedagógica da Diretoria Acadêmica

Pró-Reitoria Acadêmica Diretoria Acadêmica Assessoria Pedagógica da Diretoria Acadêmica FACULDADE: CENTRO UNIVERSITÁRIO DE BRASÍLIA UniCEUB CURSO: CIÊNCIA DA COMPUTAÇÃO DISCIPLINA: CONSTRUÇÃO DE COMPILADORES CARGA HORÁRIA: 75 H. A. ANO/SEMESTRE: 2017/02 PROFESSOR: EDUARDO FERREIRA DOS SANTOS

Leia mais

Compiladores - Especificando Sintaxe

Compiladores - Especificando Sintaxe Compiladores - Especificando Sintaxe Fabio Mascarenhas - 2013.1 http://www.dcc.ufrj.br/~fabiom/comp Análise Sintática A análise sintática agrupa os tokens em uma árvore sintática de acordo com a estrutura

Leia mais

Análise Sintática. Eduardo Ferreira dos Santos. Outubro, Ciência da Computação Centro Universitário de Brasília UniCEUB 1 / 18

Análise Sintática. Eduardo Ferreira dos Santos. Outubro, Ciência da Computação Centro Universitário de Brasília UniCEUB 1 / 18 Análise Sintática Eduardo Ferreira dos Santos Ciência da Computação Centro Universitário de Brasília UniCEUB Outubro, 2016 1 / 18 Sumário 1 Introdução 2 Derivações 2 / 18 1 Introdução 2 Derivações 3 /

Leia mais

DESENVOLVIMENTO DO COMPILADOR PARA A LINGUAGEM SIMPLE

DESENVOLVIMENTO DO COMPILADOR PARA A LINGUAGEM SIMPLE DESENVOLVIMENTO DO COMPILADOR PARA A LINGUAGEM SIMPLE Jeferson MENEGAZZO 1, Fernando SCHULZ 2, Munyque MITTELMANN 3, Fábio ALEXANDRINI 4. 1 Aluno 5ª fase do Curso de Ciência da Computação do Instituto

Leia mais

Compiladores. Exemplo. Caraterísticas de Gramáticas. A αβ 1 αβ 2. A αx X β 1 β 2. Lembrando... Gramáticas Livres de Contexto

Compiladores. Exemplo. Caraterísticas de Gramáticas. A αβ 1 αβ 2. A αx X β 1 β 2. Lembrando... Gramáticas Livres de Contexto Compiladores Análise sintática (2) Análise Top-Down Lembrando... Gramáticas Livres de Contexto Análise sintática = parsing. Baseada em GLCs Gramática: S A B Top-Down Bottom-Up S AB cb ccbb ccbca S AB A

Leia mais

Tratamento dos Erros de Sintaxe. Adriano Maranhão

Tratamento dos Erros de Sintaxe. Adriano Maranhão Tratamento dos Erros de Sintaxe Adriano Maranhão Introdução Se um compilador tivesse que processar somente programas corretos, seu projeto e sua implementação seriam grandemente simplificados. Mas os programadores

Leia mais

PROGRAMAÇÃO I. Introdução

PROGRAMAÇÃO I. Introdução PROGRAMAÇÃO I Introdução Introdução 2 Princípios da Solução de Problemas Problema 1 Fase de Resolução do Problema Solução na forma de Algoritmo Solução como um programa de computador 2 Fase de Implementação

Leia mais

Estruturas de Controle: Nível de Unidades de Programação

Estruturas de Controle: Nível de Unidades de Programação Paradigmas de Linguagens I 1 1.5... Estruturas de Controle: Nível de Unidades de Programação As estruturas de controle no nível de unidades de programação são mecanismos de linguagens utilizados para especificar

Leia mais

Compiladores. Parser LL 10/13/2008

Compiladores. Parser LL 10/13/2008 Compiladores Fabiano Baldo Usa uma pilha explícita ao invés de chamadas recursivas para realizar a análise sintática. LL(k) parsing significa que ktokensà frente são utilizados. O primeiro L significa

Leia mais

Introdução parte II. Compiladores. Mariella Berger

Introdução parte II. Compiladores. Mariella Berger Introdução parte II Compiladores Mariella Berger Sumário Partes de um compilador Gerador da Tabela de Símbolos Detecção de erros As fases da análise As fases de um compilador Montadores O que é um Compilador?

Leia mais

Universidade de Santa Cruz do Sul UNISC Departamento de informática COMPILADORES. Síntese. Prof. Geovane Griesang

Universidade de Santa Cruz do Sul UNISC Departamento de informática COMPILADORES. Síntese. Prof. Geovane Griesang Universidade de Santa Cruz do Sul UNISC Departamento de informática COMPILADORES Síntese Prof. geovanegriesang@unisc.br Data 18/11/2013 Análise sintática Parte 01 25/11/2013 Análise sintática Parte 02

Leia mais

Capítulo 5. Nomes, Vinculações e Escopos

Capítulo 5. Nomes, Vinculações e Escopos Capítulo 5 Nomes, Vinculações e Escopos Tópicos do Capítulo 5 Introdução Nomes Variáveis O conceito de vinculação Escopo Escopo e tempo de vida Ambientes de referenciamento Constantes nomeadas Introdução

Leia mais

Tokens, Padroes e Lexemas

Tokens, Padroes e Lexemas O Papel do Analisador Lexico A analise lexica e a primeira fase de um compilador e tem por objetivo fazer a leitura do programa fonte, caracter a caracter, e traduzi-lo para uma sequencia de símbolos lexicos

Leia mais

Compiladores - Gramáticas

Compiladores - Gramáticas Compiladores - Gramáticas Fabio Mascarenhas - 2013.1 http://www.dcc.ufrj.br/~fabiom/comp Análise Sintática A análise sintática agrupa os tokens em uma árvore sintática de acordo com a estrutura do programa

Leia mais

Conteúdo. Introdução a compiladores Tradução x Interpretação Processo de Compilação

Conteúdo. Introdução a compiladores Tradução x Interpretação Processo de Compilação Compiladores Conteúdo Introdução a compiladores Tradução x Interpretação Processo de Compilação Quando se inventou o computador criou se uma máquina a mais, quando se criou o compilador criou se uma nova

Leia mais

COMPILADORES. Análise semântica. Prof. Geovane Griesang Universidade de Santa Cruz do Sul UNISC Departamento de informática

COMPILADORES. Análise semântica. Prof. Geovane Griesang Universidade de Santa Cruz do Sul UNISC Departamento de informática Universidade de Santa Cruz do Sul UNISC Departamento de informática COMPILADORES Análise semântica Tradução dirigida pela sintaxe Parte 02 Prof. geovanegriesang@unisc.br Sumário Data 18/11/2013 Análise

Leia mais

Compiladores - Análise Ascendente

Compiladores - Análise Ascendente Compiladores - Análise Ascendente Fabio Mascarenhas - 2013.1 http://www.dcc.ufrj.br/~fabiom/comp Análise Descendente vs. Ascendente As técnicas de análise que vimos até agora (recursiva com retrocesso,

Leia mais

COMPILAÇÃO. Ricardo José Cabeça de Souza

COMPILAÇÃO. Ricardo José Cabeça de Souza COMPILAÇÃO Ricardo José Cabeça de Souza www.ricardojcsouza.com.br Programas Código-fonte escrito em linguagem de programação de alto nível, ou seja, com um nível de abstração muito grande, mais próximo

Leia mais

Conceitos Básicos de Programação

Conceitos Básicos de Programação BCC 201 - Introdução à Programação Conceitos Básicos de Programação Guillermo Cámara-Chávez UFOP 1/53 Conceitos básicos I Variável 2/53 Conceitos básicos II Posição de memoria, identificada através de

Leia mais

Compiladores - Análise Ascendente

Compiladores - Análise Ascendente Compiladores - Análise Ascendente Fabio Mascarenhas - 2013.2 http://www.dcc.ufrj.br/~fabiom/comp Análise Descendente vs. Ascendente As técnicas de análise que vimos até agora (recursiva com retrocesso,

Leia mais

Compiladores - Gramáticas

Compiladores - Gramáticas Compiladores - Gramáticas Fabio Mascarenhas 2018.1 http://www.dcc.ufrj.br/~fabiom/comp Análise Sintática A análise sintática agrupa os tokens em uma árvore sintática de acordo com a estrutura do programa

Leia mais

Compiladores. Conceitos Básicos

Compiladores. Conceitos Básicos Compiladores Conceitos Básicos Processadores de Linguagem De forma simples, um compilador é um programa que recebe como entrada um programa em uma linguagem de programação a linguagem fonte e o traduz

Leia mais

Compiladores. Lex e Yacc / Flex e Bison. Ferramentas Flex/Bison

Compiladores. Lex e Yacc / Flex e Bison. Ferramentas Flex/Bison Ferramentas Flex/Bison Prof. Sergio F. Ribeiro Lex e Yacc / Flex e Bison São ferramentas de auxílio na escrita de programas que promovem transformações sobre entradas estruturadas. São ferramentas desenvolvidas

Leia mais

Gramáticas Livres de Contexto Parte 1

Gramáticas Livres de Contexto Parte 1 Universidade Estadual de Feira de Santana Engenharia de Computação Gramáticas Livres de Contexto Parte 1 EXA 817 Compiladores Prof. Matheus Giovanni Pires O papel do Analisador Sintático É responsável

Leia mais

Disciplina: LINGUAGENS FORMAIS, AUTÔMATOS E COMPUTABILIDADE Prof. Jefferson Morais

Disciplina: LINGUAGENS FORMAIS, AUTÔMATOS E COMPUTABILIDADE Prof. Jefferson Morais UNIVERSIDADE FEDERAL DO PARÁ INSTITUTO DE CIÊNCIAS EXATAS E NATURAIS FACULDADE DE COMPUTAÇÃO CURSO DE BACHARELADO EM CIÊNCIA DA COMPUTAÇÃO Disciplina: LINGUAGENS FORMAIS, AUTÔMATOS E COMPUTABILIDADE Prof.

Leia mais

ACH2043 INTRODUÇÃO À TEORIA DA COMPUTAÇÃO

ACH2043 INTRODUÇÃO À TEORIA DA COMPUTAÇÃO ACH2043 INTRODUÇÃO À TEORIA DA COMPUTAÇÃO 2. Linguagens Livres-do-Contexto Referência: SIPSER, M. Introdução à Teoria da Computação. 2ª edição, Ed. Thomson Prof. Marcelo S. Lauretto marcelolauretto@usp.br

Leia mais

Compiladores. Bruno Lopes. Bruno Lopes Compiladores 1 / 32. Instituto de C

Compiladores. Bruno Lopes. Bruno Lopes Compiladores 1 / 32. Instituto de C ompiladores Introdução Bruno Lopes Bruno Lopes ompiladores 1 / 32 Apresentação Em que período estão? O quanto sabem de programação? Quais linguagens? O quanto sabem de unix? O quanto sabem de Linguagens

Leia mais

Apêndice A. Pseudo-Linguagem

Apêndice A. Pseudo-Linguagem Apêndice A. Pseudo-Linguagem Apostila de Programação I A.1 Considerações Preliminares Os computadores convencionais se baseiam no conceito de uma memória principal que consiste de células elementares,

Leia mais

EA876 - Introdução a Software de Sistema

EA876 - Introdução a Software de Sistema A876 - Introdução a Software de Sistema Software de Sistema: conjunto de programas utilizados para tornar o hardware transparente para o desenvolvedor ou usuário. Preenche um gap de abstração. algoritmos

Leia mais

I LINGUAGENS E PROCESSADORES: INTRODUÇÃO 1

I LINGUAGENS E PROCESSADORES: INTRODUÇÃO 1 PREÂMBULO PREFÂCIO xiii xv I LINGUAGENS E PROCESSADORES: INTRODUÇÃO 1 1 1.1 1.1.1 1.1.2 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 2 2.1 2.2 2.2.1 2.2.2 2.3 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.4 2.4.1 2.4.2

Leia mais

Universidade Católica de Pelotas Bacharelado em Ciência da Computação Linguagens Formais e Autômatos TEXTO 6 Introdução à Compilação

Universidade Católica de Pelotas Bacharelado em Ciência da Computação Linguagens Formais e Autômatos TEXTO 6 Introdução à Compilação Universidade Católica de Pelotas Bacharelado em Ciência da Computação 364018 Linguagens Formais e Autômatos TEXTO 6 Introdução à Compilação Prof. Luiz A M Palazzo Maio de 2011 Um COMPILADOR é um programa

Leia mais

Capítulo 7. Expressões e Sentenças de Atribuição

Capítulo 7. Expressões e Sentenças de Atribuição Capítulo 7 Expressões e Sentenças de Atribuição Introdução Expressões são os meios fundamentais de especificar computações em uma linguagem de programação Para entender a avaliação de expressões, é necessário

Leia mais

Linguagens de Programação

Linguagens de Programação 45 Linguagens de Programação O paradigma de programação imperativo está diretamente atrelado à arquitetura básica dos computadores sobre os quais os programas eram executados. Boa parte dos computadores

Leia mais

Expressões e sentença de atribuição

Expressões e sentença de atribuição Expressões e sentença de atribuição Marco A L Barbosa malbarbo.pro.br Departamento de Informática Universidade Estadual de Maringá cba Este trabalho está licenciado com uma Licença Creative Commons - Atribuição-CompartilhaIgual

Leia mais

SEMÂNTICA. Rogério Rocha. rode = program simples = var x : int := 3 in x := x + 5 end.

SEMÂNTICA. Rogério Rocha. rode = program simples = var x : int := 3 in x := x + 5 end. SEMÂNTICA program simples = var x : int := 3 in x := x + 5 end. rode =? Rogério Rocha Roteiro Introdução Sintaxe Semântica Dinâmica (Métodos formais) Operacional Axiomática Denotacional Estática Conclusão

Leia mais

Compiladores. Análise Léxica

Compiladores. Análise Léxica Compiladores Análise Léxica Cristiano Lehrer, M.Sc. Introdução (1/3) Análise léxica é a primeira fase do compilador. A função do analisador léxico, também denominado scanner, é: Fazer a leitura do programa

Leia mais

Linguagens de Programação. Marco A L Barbosa

Linguagens de Programação. Marco A L Barbosa Expressões e sentença de atribuição Linguagens de Programação Marco A L Barbosa cba Este trabalho está licenciado com uma Licença Creative Commons - Atribuição-CompartilhaIgual 4.0 Internacional. http://github.com/malbarbo/na-lp-copl

Leia mais

V Teoria de Parsing. Termos Básicos: Parser Analisador Sintático Parsing Analise Sintática Parse Representação da analise efetuada

V Teoria de Parsing. Termos Básicos: Parser Analisador Sintático Parsing Analise Sintática Parse Representação da analise efetuada V Teoria de Parsing Termos Básicos: Parser Analisador Sintático Parsing Analise Sintática Parse Representação da analise efetuada Ascendentes: S + x (* Seq. Invertida Reducao *) dir Exemplo: Descendentes:

Leia mais

Capítulo 8. Estruturas de Controle no Nível de Sentença

Capítulo 8. Estruturas de Controle no Nível de Sentença Capítulo 8 Estruturas de Controle no Nível de Sentença Níveis de fluxo de controle Computações são realizadas por meio da avaliação de expressões e da atribuição dos valores a variáveis Para tornar a computação

Leia mais

Compiladores I Prof. Ricardo Santos (cap 3 Análise Léxica: Introdução, Revisão LFA)

Compiladores I Prof. Ricardo Santos (cap 3 Análise Léxica: Introdução, Revisão LFA) Compiladores I Prof. Ricardo Santos (cap 3 Análise Léxica: Introdução, Revisão LFA) Análise Léxica A primeira fase da compilação Recebe os caracteres de entrada do programa e os converte em um fluxo de

Leia mais

Como construir um compilador utilizando ferramentas Java

Como construir um compilador utilizando ferramentas Java Como construir um compilador utilizando ferramentas Java p. 1/2 Como construir um compilador utilizando ferramentas Java Aula 1 - Introdução Prof. Márcio Delamaro delamaro@icmc.usp.br Como construir um

Leia mais

Acadêmica: Giselle Mafra Schlosser Orientador: Everaldo Artur Grahl

Acadêmica: Giselle Mafra Schlosser Orientador: Everaldo Artur Grahl AVALIAÇÃO DA QUALIDADE DO CÓDIGO FONTE ESCRITO EM PL/SQL Acadêmica: Giselle Mafra Schlosser Orientador: Everaldo Artur Grahl Roteiro Introdução Objetivos do trabalho Fundamentação teórica Desenvolvimento

Leia mais

Prof. Adriano Maranhão COMPILADORES

Prof. Adriano Maranhão COMPILADORES Prof. Adriano Maranhão COMPILADORES LINGUAGENS: INTERPRETADAS X COMPILADAS Resumo: Linguagem compilada: Se o método utilizado traduz todo o texto do programa, para só depois executar o programa, então

Leia mais

Compilação: Erros. Detecção de Erros: * Analisadores Top-Down - Preditivo Tabular (LL) - Feito a mão. * Analisadores Botton-Up: - Shift-Reduce (SLR)

Compilação: Erros. Detecção de Erros: * Analisadores Top-Down - Preditivo Tabular (LL) - Feito a mão. * Analisadores Botton-Up: - Shift-Reduce (SLR) Compilação: Erros Detecção de Erros: * Analisadores Top-Down - Preditivo Tabular (LL) - Feito a mão * Analisadores Botton-Up: - Shift-Reduce (SLR) * Erros no Lex * Erros no Yacc * Erros na Definição da

Leia mais

Programação de Computadores:

Programação de Computadores: Instituto de C Programação de Computadores: Introdução a Linguagens de Programação Luis Martí Instituto de Computação Universidade Federal Fluminense lmarti@ic.uff.br - http://lmarti.com Seis Camadas Problema

Leia mais