Baixe o app para aproveitar ainda mais
Prévia do material em texto
Programação de Sistemas Sumário I Introdução 1 1 Conceitos Básicos 2 1.1 Programas e Processos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.2 Software de sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.3 Desenvolvimento de Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 II Montadores e Carregadores 11 2 Princípios de Montadores 12 2.1 Definições . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.2 Instruções assembly . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.3 Pseudo-Instruções . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.4 Macromontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.5 Aspectos genéricos de projeto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 3 Estruturas de dados do montador 27 3.1 Tabelas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 3.2 Busca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 3.3 Ordenação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 3.4 Tabelas hash . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 3.5 Listas ligadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 4 Montagem e Carregamento Absolutos 41 4.1 Estrutura de programa assembly . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 4.2 Processadores de macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 4.3 Montadores em dois passos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 4.4 Montagem e carregamento assemble and go . . . . . . . . . . . . . . . . . . . . . . . . . 48 4.5 Carregamento absoluto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 5 Relocação e Ligação 50 5.1 Estruturas de dados adicionais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 5.2 Carregador de ligação direta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 5.3 Ligadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 5.4 Bibliotecas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 5.5 Carregamento e Ligação Dinâmicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 i Programação de Sistemas Ivan L. M. Ricarte Sumário Sumário III Compiladores 62 6 Princípios de Compiladores 63 6.1 Motivação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 7 Gramáticas e Reconhecimento de Sentenças 66 7.1 Terminologia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 7.2 Princípios de gramáticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 7.3 Definição formal de gramáticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 7.4 Tipos de gramáticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 7.5 Expressões regulares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 7.6 Reconhecimento de sentenças . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 8 Análise Léxica 75 8.1 Reconhecimento de símbolos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 8.2 Analisadores léxicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 8.3 Geradores de analisadores léxicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 9 Análise Sintática 84 9.1 Gramáticas livres de contexto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 9.2 Técnicas de Análise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 9.3 Analisadores de deslocamento e redução . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 9.4 Analisadores LR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 9.5 Geradores de Analisadores Sintáticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 10 Geração de Código e Otimização 98 10.1 Gerador de Códigos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 10.2 Otimização . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 IV Sistemas Operacionais 105 11 Princípios 106 12 Escalonamento de Processos 108 12.1 Conceitos Básicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 12.2 Escalonamento de Processos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 12.3 Processos em Unix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 12.4 Processos em MS-DOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 13 Concorrência e Comunicação entre Processos 113 13.1 Concorrência . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 13.2 Comunicação Interprocessos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 14 Gerência de Memória 117 14.1 Gerência Sem Permuta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 14.2 Gerência com Permuta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 14.3 Memória Virtual . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 14.4 Algoritmos de Troca de Página . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 14.5 Gerência de Memória no UNIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 14.6 Gerência de Memória em MS-DOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 c 2000 DCA/FEEC/UNICAMP ii Programação de Sistemas Ivan L. M. Ricarte Sumário Sumário 15 Manipulação de Arquivos 130 15.1 Interface do Sistema de Arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 15.2 Projeto do Sistema de Arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 15.3 O Sistema de Arquivos do Unix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135 15.4 Sistemas de Arquivos do MS-DOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 16 Entrada e Saída 138 16.1 Princípios do Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 16.2 Princípios do Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 16.3 Discos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 16.4 E/S no Unix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146 16.5 E/S em MS-DOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 V Apêndices 149 A Codificação binária 150 A.1 Representação Numérica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 A.2 Representação hexadecimal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 B Assembly do 68000 153 B.1 Organização dos Dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 B.2 Instruções assembly . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 B.3 Modos de Endereçamento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 B.4 Codificação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158 C Programação C 167 C.1 Organização Básica de Programas C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168 C.2 Tipos de Dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 170 C.3 Declarações de Variáveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 C.4 Expressões . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172 C.5 Controle do Fluxo de Execução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 C.6 Invocação de Funções . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179 C.7 Arranjos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181 C.8 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 C.9 Definição de estruturas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 C.10 Uniões . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 C.11 Enumerações . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 C.12 Definição de Nomes de Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 C.13 Ponteiros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 C.14 Argumentos na Linha de Comando . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 C.15 Rotinas para Entrada e Saída de Dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195 C.16 Rotinas para Interação com o Sistema Operacional . . . . . . . . . . . . . . . . . . . . . 200 C.17 O Pré-processador C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 C.18 Exemplo de Aplicativo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 C.19 Palavras reservadas em C e C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 C.20 Precedência de Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 Referências 211 c 2000 DCA/FEEC/UNICAMP iii Lista de Figuras 1.1 O ciclo de instrução von Neumann. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.2 Etapas para execução de programa. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.3 Processo de evolução do software. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.4 Estruturas elementares de dados. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 1.5 Mecanismos básicos da programação estruturada. . . . . . . . . . . . . . . . . . . . . . . 8 3.1 Estruturas auxiliares do montador. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 3.2 Efeito da inserção de nó em uma lista. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 7.1 Árvore sintática para expressão (x+ y)� x. . . . . . . . . . . . . . . . . . . . . . . . . . 73 8.1 Representação gráfica de uma transição. . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 8.2 Autômato finito: inteiros em octal, decimal ou hexadecimal. . . . . . . . . . . . . . . . . 77 9.1 Grafos sintáticos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 9.2 Esquema de uso do lex e yacc. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 9.3 Fluxo de Controle entre yylex() e yyparse() . . . . . . . . . . . . . . . . . . . . . . . . . 91 11.1 Papel do Sistema Operacional. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 12.1 Estados de um processo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 14.1 Organizações com partições fixas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 14.2 Organização com swapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 14.3 Organização com mapa de bits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 14.4 A posição e função da MMU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 14.5 Relação entre endereço virtual e endereço físico . . . . . . . . . . . . . . . . . . . . . . . 124 14.6 Fila de páginas candidatas a permuta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128 15.1 Organização de blocos livres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 15.2 Estrutura de i-node . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 15.3 Estrutura da FAT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 16.1 Um modelo para conexão da CPU, memória, controladores e dispositivos de E/S . . . . . 139 16.2 Níveis do sistema de E/S e funções principais de cada nível . . . . . . . . . . . . . . . . . 144 16.3 Algoritmo de escalonamento menor seek primeiro (SSF) . . . . . . . . . . . . . . . . . . 145 16.4 Escalonamento de requisições no disco através do algoritmo do elevador . . . . . . . . . . 146 16.5 Esquema básico de E/S no Unix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 B.1 Modelo de registradores do 68000. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 iv Programação de Sistemas Ivan L. M. Ricarte Lista de Figuras Lista de Figuras B.2 Organização da Memória. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 B.3 Formato de uma instrução. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156 C.1 Estrutura básica de um algoritmo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 C.2 Algoritmo que faz nada. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 C.3 Seqüência em C. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 C.4 Seleção com if : : : else em C. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176 C.5 Switch : : : case . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176 C.6 Switch : : : case sem break. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 C.7 Repetição em C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 C.8 Repetição em C com for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 c 2000 DCA/FEEC/UNICAMP v Algoritmos 1 Busca linear em tabela. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 2 Busca binária em tabela. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3 Ordenação de tabela pela busca do menor valor. . . . . . . . . . . . . . . . . . . . . . . . 31 4 Busca do menor valor em tabela. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 5 Troca de conteúdos de duas posições de uma tabela. . . . . . . . . . . . . . . . . . . . . . 32 6 Ordenação de tabela por quicksort. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 7 Ordenação de tabela por radixsort. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 8 Ordenação de tabela pela contagem de elementos. . . . . . . . . . . . . . . . . . . . . . . 34 9 Inserção de nó em lista ligada. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 10 Busca de nó com chave especificada em lista ligada. . . . . . . . . . . . . . . . . . . . . . 37 11 Remoção de nó especificado em lista ligada. . . . . . . . . . . . . . . . . . . . . . . . . . 37 12 Passo 1 do montador. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 13 Passo 2 do montador. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 14 Carregador absoluto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 15 Primeiro passo do carregador de ligação direta . . . . . . . . . . . . . . . . . . . . . . . . 54 16 Segundo passo do carregador de ligação direta . . . . . . . . . . . . . . . . . . . . . . . . 55 17 Terceiro passo do carregador de ligação direta . . . . . . . . . . . . . . . . . . . . . . . . 56 18 Algoritmo para reconhecimento de símbolo. . . . . . . . . . . . .. . . . . . . . . . . . . 79 19 Contrução ascendente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 20 Analisador sintático por deslocamento e redução. . . . . . . . . . . . . . . . . . . . . . . 87 vi Prefácio O material aqui apresentado foi desenvolvido a partir de uma necessidade real constatada nas disciplinas EA876 (Introdução a Software de Sistema) e EA877 (Mini e microcomputadores: software) da Faculdade de Engenharia Elétrica e de Computação da Universidade Estadual de Campinas (FEEC/UNICAMP). Seu objetivo é oferecer um material de apoio ao estudo do aluno que apresente de forma unificada os diversos tópicos apresentados nesse curso. Essas disciplinas, oferecidas aos alunos de graduação dos cursos de Engenharia de Computação e de Engenharia Elétrica, têm por meta oferecer uma visão geral sobre o funcionamento e utilização software que permite que usuários executem seus programas em computadores. Para os alunos de Engenharia de Computação, esse é um ponto de partida para temas que serão aprofundados em diversas outras disciplinas. Para os alunos de Engenharia Elétrica, pode significar o único contato com esse tema, essencial para o uso efetivo de computadores ao longo de sua vida profissional. Uma das dificuldades no desenvolvimento desse material está em sua amplitude. Para oferecer essa visão geral do software de sistema, agrupou-se em uma disciplina temas tão amplos como montadores, compiladores e sistemas operacionais — cada um deles suficientemente complexo para o escopo de uma ou uma série de disciplinas. Adequar o grau de profundidade da apresentação dos tópicos ao tempo e aos objetivos do curso, sem tornar essa apresentação extremamente superficial, tem sido uma tarefa que vem requerendo a constante revisão desse material. Nessa tarefa, a realimentação dos usuários do material — alunos e instrutores das disciplinas citadas — tem sido um importante auxílio. Para o desenvolvimento desse texto, algumas opções foram norteadas pela prática corrente na área ou em outras disciplinas relacionadas da FEEC/UNICAMP. Nesta categoria inclui-se a opção pelos processa- dores da família Motorola 68K para os exemplos que utilizam linguagem assembly. Na primeira categoria, o destaque maior é o uso da linguagem C para os exemplos envolvendo linguagens de alto nível. C é a linguagem de programação de sistemas por excelência, devendo ocupar esse papel por um longo tempo ainda. Com o objetivo de não tornar o corpo principal desse texto extremamente carregado ou monótono para aqueles que detêm o conhecimento nessas duas linguagens, são oferecidos amplos apêndices que as des- crevem num grau de profundidade não mais que necessário para a compreensão dos exemplos. Da mesma forma, são apresentados em apêndices fundamentos da representação de variáveis em computadores, que podem servir como revisão ou introdução ao tópico para aqueles que assim o necessitem. Campinas, fevereiro de 2000 Ivan Luiz Marques Ricarte vii Parte I Introdução 1 Capítulo 1 Conceitos Básicos Índice 1.1 Programas e Processos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.2 Software de sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.3 Desenvolvimento de Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.3.1 Projeto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.3.2 Programação Estruturada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 1.3.3 Codificação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Nas últimas décadas, o computador deixou de ser uma ferramenta cara e exclusiva dos grandes centros de pesquisas e passou a ser presença constante no cotidiano de todos. Apesar dessa quase onipresença, para muitos ainda há uma aura de mistério no que se refere ao funcionamento e à capacidade funcional de computadores. Um computador tem essencialmente duas componentes, hardware e software. Hardware é o termo co- letivo que representa todo o conjunto de circuitos que permite o funcionamento do computador. No entanto, o que determina o que o computador faz é a segunda componente. Software é o termo coletivo expressando o conjunto de programas que, juntamente com o hardware, permite a operação de computadores. 1.1 Programas e Processos Um computador nada mais faz do que executar programas. Um programa é simplesmente uma seqüên- cia de instruções definida por um programador. Assim, um programa pode ser comparado a uma receita indicando os passos elementares que devem ser seguidos para desempenhar uma tarefa. Cada instrução é executada no computador por seu principal componente, o processador ou CPU (de unidade central de processamento). Cada processador entende uma linguagem própria, que reflete as instruções básicas que ele pode exe- cutar. O conjunto dessas instruções constitui a linguagem de máquina do processador. Cada instrução da linguagem de máquina é representada por uma seqüência de bits distinta, que deve ser decodificada pela unidade de controle da CPU para determinar que ações devem ser desempenhadas para a execução da instrução. Claramente, seria extremamente desconfortável se programadores tivessem que desenvolver programas diretamente em linguagem de máquina. Programadores não trabalham diretamente com a representação 2 Programação de Sistemas Ivan L. M. Ricarte Conceitos Básicos 1.1. Programas e Processos binária das instruções de um processador. Existe uma descrição simbólica para as instruções do processador — a linguagem assembly do processador — que pode ser facilmente mapeada para sua linguagem de máquina. No entanto, mesmo assembly é raramente utilizada pela maior parte dos programadores, que trabalha com linguagens de programação de alto nível. Em linguagens de alto nível, as instruções são expressas usando palavras, ou termos, que se aproximam daquelas usadas na linguagem humana, de forma a facilitar para os programadores a expressão e compreensão das tarefas a executar. Há diversas linguagens de alto nível disponíveis para diversas máquinas distintas. Essa independência de um processador específico é uma outra vantagem no desenvolvimento de programas em linguagens de alto nível em relação ao uso de assembly. Em geral, cada linguagem de alto nível teve uma motivação para ser desenvolvida. BASIC foi de- senvolvida para ensinar princípios de programação; Pascal, para o ensino de programação estruturada; FORTRAN, para programação científica; lisp e Prolog para aplicações em Inteligência Artificial; e a lin- guagem C, para a programação de sistemas. O fato de uma linguagem ter sido desenvolvida com uma aplicação em mente não significa que ela não seja adequada para outras aplicações. A linguagem C, junta- mente com sua “sucessora” C++, é utilizada para um universo muito amplo de aplicações. Um dos atrativos da linguagem C é sua flexibilidade: um programador C tem à sua disposição comandos que permitem desenvolver programas com características de alto nível e ao mesmo tempo trabalhar em um nível muito próximo da arquitetura da máquina, de forma a explorar os recursos disponíveis de forma mais eficiente. Por este motivo, o número de aplicações desenvolvidas em C e C++ é grande e continua a crescer. Uma vez que um programa tenha sido criado, sua representação em linguagem de máquina é armaze- nada na unidade de memória do computador, de onde as instruções são transferidas para o processador para decodificação e execução. Na maior parte dos computadores modernos, a execução de um programa dá-se pela contínua repetição desse ciclo de instrução (Fig. 1.1), ou seja: 1. O processador busca da memória a seqüência de bits que representa a próxima instrução a ser exe- cutada. O endereço dessa instrução é armazenado em registrador processador dedicado a esse fim, tipicamente denominadoPC (Program Counter); 2. A unidade de controle do processador decodifica a instrução, isto é, descobre que ações devem ser realizadas para a execução da instrução; 3. Se a execução da instrução demandar dados que estejam na memória, o processador busca também esses dados (os operandos da instrução) nos endereços especificados na instrução. As várias possibi- lidades de especificar esses endereços de operandos constituem os modos de endereçamento supor- tados pelo processador. Modos usuais de endereçamento incluem endereçamento direto, indireto e indexado; 4. A unidade de execução do processador realiza a ação associada à instrução; 5. O processador armazena o resultado gerado, se houver, em um registrador ou em algum endereço da memória que tenha sido especificado na instrução. Este processo é repetido desde a primeira até a última instrução do programa. Computadores onde esse modelo de execução é obedecido são conhecidos como máquinas de von Neu- mann, uma homenagem a John von Neumann (1903–1957), matemático húngaro que atuou como consultor no Projeto ENIAC, em meados da década de 1940, e a quem é atribuída a concepção da organização interna de computadores onde o programa é armazenado na memória. A um programa em execução dá-se o nome de processo. Além das instruções do programa, um pro- cesso necessita de todo um conjunto de informações adicionais para o controle de sua execução. O estado corrente dessas informações associadas a cada programa em execução constitui o estado do processo. c 2000 DCA/FEEC/UNICAMP 3 Programação de Sistemas Ivan L. M. Ricarte Conceitos Básicos 1.2. Software de sistema Figura 1.1 O ciclo de instrução von Neumann. Decodificador Buffer dados Entrada e saída Arranjo de células controle Unidade de Unidade lógica e aritmética Barramento do sistema MemóriaCPU endereço dados R eg ist ra do re s instruções 1.2 Software de sistema Na organização de von Neumann o programa, assim como os dados, está na memória. Uma conseqüên- cia desse fato é que há programas que manipulam outros programas, como se estes fossem seus dados. O conjunto de programas com essa característica constitui o software de sistema. O software de sistema é composto pelo conjunto de programas que permitem desde a criação de um programa aplicativo até o controle de recursos utilizados pelo programa durante sua execução por um processador. No desenvolvimento de programas, o software de sistema é extensamente utilizado, com as várias etapas inter-relacionadas para a criação e execução de um programa (Fig. 1.2). Tipicamente, esse relacio- namento dá-se de forma transparente para o programador. Figura 1.2 Etapas para execução de programa. Arquivos texto Arquivos binários Programa fonte Programa assembly compilador Módulo objeto montador ligador carregamento Módulo de carregador Execução em memória Programas são usualmente descritos em linguagens de alto nível. O compilador é o programa do siste- ma que traduz um programa descrito através de uma linguagem de alto nível específica para um programa equivalente em linguagem assembly. Esse processo de tradução é denominado compilação. O montador (assembler) é o programa do sistema responsável por traduzir um programa assembly para o código de máquina. Esse processo de tradução de um programa-fonte assembly para um programa em código de máquina é denominado montagem; o resultado da montagem é um módulo objeto contendo o código binário que será posteriormente executado. Para que um programa possa ser executado, seu código de máquina deve estar presente na memória. O carregador é o programa do sistema responsável por transferir o código de máquina de um programa c 2000 DCA/FEEC/UNICAMP 4 Programação de Sistemas Ivan L. M. Ricarte Conceitos Básicos 1.3. Desenvolvimento de Software para a memória e iniciar sua execução. O processo de transferir o conteúdo de um módulo objeto para a memória principal é denominado carregamento. A execução de qualquer programa deve ser precedida por seu carregamento. Programas complexos raramente são descritos através de um único arquivo-fonte, mas sim organizados em módulos objetos interrelacionados. Tais módulos podem agregar funcionalidades da aplicação sendo desenvolvida ou recursos comuns do sistema que devem ser integrados à aplicação. O programa do sistema ligador é o responsável por interligar os diversos módulos de um programa para gerar o programa que será posteriormente carregado para a memória. Essa etapa de preparação de um programa para sua excução é denominada ligação. Finalmente, a execução de cada programa se dá sob o controle do sistema operacional. O sistema operacional é o responsável por gerenciar cada processo no computador, estabelecendo como será realiza- da sua execução. Ele também atua como um programa supervisor que estabelece uma camada de controle entre o hardware do computador e aplicações de usuários. Uma de suas funções é estabelecer uma inter- face de software uniforme entre o computador, outros programas do sistema e programas de aplicação de usuários. Outra função fundamental de um sistema operacional é gerenciar os recursos de um computador de forma a promover sua eficiente utilização. Exemplos de sistemas operacionais são MS-DOS, Windows NT, OS/2, Linux e Solaris — estes dois implementações do sistema operacional Unix. 1.3 Desenvolvimento de Software Não há dúvidas hoje em dia quanto à importância do software no desenvolvimento dos mais diversos sistemas. Com esta evolução do papel do software em sistemas computacionais, veio também uma maior complexidade de programas e uma maior preocupação em desenvolver programas que pudessem ser fa- cilmente entendidos e modificados (se necessário), não apenas pelo autor do programa mas também por outros programadores. A disciplina que estuda o desenvolvimento de bom software é conhecida como Engenharia de Software. A Engenharia de Software estabelece alguns princípios de desenvolvimento que independem da lin- guagem de programação adotada. Estes princípios são utilizados nas três grandes fases da vida de um programa, que são: Especificação: estuda o que deve ser feito pelo programa, incluindo análises do sistema e do problema a ser resolvido; Desenvolvimento: incorpora o projeto do programa (por exemplo, uma descrição do algoritmo e estruturas necessárias), a codificação (tradução do projeto em termos de uma linguagem de programação) e testes do programa; e Manutenção: é a fase de mudanças decorrentes da correção de erros e atualizações do programa. A fase de desenvolvimento costuma tomar a maior parte do ciclo de vida de criação de um software. Nesta fase são tomadas decisões que podem afetar sensivelmente o custo e a qualidade do software gerado. Uma vez estabelecida a funcionalidade do software que se deseja implementar na fase de definição, a fase de desenvolvimento propriamente dita pode ser iniciada. Esta fase pode ser dividida em três etapas principais, que são projeto, codificação e teste. A seguir, serão destacados alguns aspectos ligados ao desenvolvimento de software, que será o principal enfoque deste curso. 1.3.1 Projeto O projeto de software pode ser subdividido em dois grandes passos, projeto preliminar e projeto de- talhado. O projeto preliminar preocupa-se em transformar os requisitos especificados na fase de análise c 2000 DCA/FEEC/UNICAMP 5 Programação de Sistemas Ivan L. M. Ricarte Conceitos Básicos 1.3. Desenvolvimento de Software em arquiteturas de dados e de software. O projeto detalhado refina estas representações de arquitetura em estruturas de dados detalhadas e em representações algorítmicas do software. Uma das estratégias de projeto mais utilizadas é o desenvolvimento top-down. Neste tipo de desen- volvimento, trabalha-se com o conceito de refinamento de descriçõesdo software em distintos níveis de abstração. O conceito de abstração está relacionado com esconder informação sobre os detalhes. No nível mais alto de abstração, praticamente nenhuma informação é detalhada sobre como uma dada tarefa será im- plementada — simplesmente descreve-se qual é a tarefa. Em etapas sucessivas de refinamento, o projetista do software vai elaborando sobre a descrição da etapa anterior, fornecendo cada vez mais detalhes sobre como realizar a tarefa. O desenvolvimento top-down estabelece o processo de passagem de um problema a uma estrutura de software para sua solução (Fig. 1.3), resultando em uma estrutura de software que representa a organização dos distintos componentes (ou módulos) do programa. Observe que a solução obtida pode não ser única: dependendo de como o projeto é desenvolvido e das decisões tomadas, distintas estruturas podem resultar. Figura 1.3 Processo de evolução do software. S1 S2 S3 S4 S1 S2 S3 S4 "Problema" P2 P3 P4 P1 S1 S3 S4 S2 por software Solução Possíveis estruturas de software Outro aspecto tão importante quanto a estrutura de software é a estrutura de dados, que é uma re- presentação do relacionamento lógico entre os elementos de dados individuais. As estruturas de dados utilizadas definem a organização, métodos de acesso e alternativas de processamento para a informação manipulada pelo programa. Embora a estrutura final possa ser tão complexa quanto o projetista deseja, há alguns blocos básicos (Fig. 1.4) que permitem construir as estruturas mais sofisticadas. A estrutura de dados mais simples é um escalar, que representa um elemento simples de informação que pode ser acessado através de um identificador. Elementos escalares organizados como um grupo contí- guo constituem um arranjo ou vetor seqüencial, cujos elementos podem ser acessados através de um identificador em associação com um índice. Elementos não-contíguos podem ser agrupados através de uma lista ligada, cuja unidade de acesso é um nó que contém um item de dado (escalar ou não) e um c 2000 DCA/FEEC/UNICAMP 6 Programação de Sistemas Ivan L. M. Ricarte Conceitos Básicos 1.3. Desenvolvimento de Software ponteiro para indicar o próximo nó na lista (veja a Seção 3.5). Figura 1.4 Estruturas elementares de dados. Escalar Vetor sequencial . . . Lista ligada Uma vez estabelecidas as estruturas de software e de dados do programa, o detalhamento do projeto pode prosseguir com o projeto procedimental, onde são definidos os detalhes dos algoritmos que serão utilizados para implementar o programa. Um algoritmo é uma solução passo-a-passo para a resolução do problema especificado que sempre atinge um ponto final. Em princípio, algoritmos poderiam ser descritos usando linguagem natural (português, por exemplo). Entretanto, o uso da linguagem natural para descrever algoritmos geralmente leva a ambiguidades, de modo que se utilizam normalmente linguagens mais restri- tas para a descrição dos passos de um algoritmo. Algumas formas de especificação destas linguagens são descritas na seqüência. 1.3.2 Programação Estruturada A programação estruturada estabelece uma disciplina de desenvolvimento de algoritmos que facilita a compreensão de programas. O princípio básico de programação estruturada é que um programa é composto por blocos elementares que se interligam através de três mecanismos básicos, que são seqüência, seleção e iteração. Cada uma destas construções tem um ponto de início (o topo do bloco) e um ponto de término (o fim do bloco) de execução. Qualquer algoritmo, independentemente da área de aplicação e de sua complexidade, pode ser descrito através destes mecanismos básicos. Um dos mecanismos mais utilizados para a representação de algoritmos é o fluxograma. Em um fluxograma, o retângulo simbolicamente representa um passo do processamento (uma tarefa), o losango indica uma condição lógica e as setas mostram o fluxo de controle (Fig. 1.5). Seqüência implementa os passos de processamento necessários para descrever qualquer programa. Por exemplo, um segmento de programa da forma “faça primeiro a Tarefa a e depois a Tarefa b” seria representado por uma seqüência de dois retângulos (Fig. 1.5(a)). Uma notação textual equivalente poderia ser obtida através de uma pseudo-linguagem que se restringisse a construções estruturadas, como em SEQUENCIA() 1 Tarefa a 2 Tarefa b 3 return Seleção especifica a possibilidade de selecionar o fluxo de execução do processamento baseado em ocorrências lógicas. Há duas formas básicas de condição. A primeira forma é a construção IF (Fig. 1.5(b)), que permite representar fluxos da forma “se a condição x for verdadeira, faça a Tarefa a; senão (isto é, se a condição x for falsa), faça a Tarefa b.” As duas setas que saem do losango de condição no fluxograma recebem rótulos T e F para indicar o fluxo de execução quando a condição especificada é verdadeira ou falsa, respectivamente. O retângulo sob a seta rotulada T normalmente é denominado o bloco then da construção, enquanto que o outro retângulo é denominado bloco else. Em termos de pseudo-linguagem, a construção equivalente pode ser descrita como SELECAOIF() 1 if x c 2000 DCA/FEEC/UNICAMP 7 Programação de Sistemas Ivan L. M. Ricarte Conceitos Básicos 1.3. Desenvolvimento de Software Figura 1.5 Representação dos mecanismos básicos da programação estruturada: (a) seqüência; (b) seleção IF; (c) seleção SWITCH; (d) iteração WHILE; (e) iteração REPEAT. (a) (b) ... (c) (d) (e) T F T T T F F F F T T F 2 then Tarefa a 3 else Tarefa b A outra forma de seleção estende o número de condições que podem ser testadas para definir o fluxo de execução. Esta construção, SWITCH (Fig. 1.5(c)), permite representar fluxos da forma “se a variável v tem o valor x, faça a Tarefa a; se v tem o valor y, faça a Tarefa b ; e se tem o valor z, faça a Tarefa c.” Uma possível representação para tal construção em termos de pseudo-linguagem é: SELECAOSWITCH() 1 switch v 2 case x : 3 Tarefa a 4 case y : 5 Tarefa b 6 case z : 7 Tarefa c 8 case default : 9 Tarefa d Nesse exemplo em pseudo-linguagem, ilustra-se a inclusão de uma condição default, que indica qual deve ser a ação executada (no caso, Tarefa d) quando a variável assume um valor não explicitamente especificado pelo programador. Observe que a construçãoSWITCH não é essencial, uma vez que ela pode ser representada em termos da seleção com IF, como em SELECAOMULTIPLAIF() 1 if v = x 2 then Tarefa a 3 else if v = y c 2000 DCA/FEEC/UNICAMP 8 Programação de Sistemas Ivan L. M. Ricarte Conceitos Básicos 1.3. Desenvolvimento de Software 4 then Tarefa b 5 else if v = z 6 then Tarefa c 7 else Tarefa d Entretanto, a utilização de estruturas SWITCH simplifica a expressão de situações que ocorrem fre- quentemente em programas — por exemplo, selecionar ações dependendo de uma opção escolhida em um menu — sem ter que recorrer ao aninhamento excessivo de condições da forma IF. Iteração permite a execução repetitiva de segmentos do programa. Na forma básica de repetição, WHILE (Fig. 1.5(d)), uma condição x é verificada. Caso seja verdadeira, então uma Tarefa a é executada e a condição é reavaliada. Enquanto a condição for verdadeira, a tarefa é repetidamente executada. Em termos da pseudo-linguagem, esta construção pode ser representada por ITERACAOWHILE() 1 while x 2 do Tarefa a Uma variação dessa construção é apresentada na Fig. 1.5(e), onde inicialmente a tarefa é executada e apenas então a condição de repetição é avaliada; quando a condição torna-se verdadeira, a iteração é encerrada. Esta variante, iteração REPEAT, pode ser expressa em termos da pseudo-linguagem por ITERACAOREPEAT() 1 repeat 2 Tarefa a 3 until x A estratégia de desenvolvimento top-down pode também ser utilizada na descrição algorítmica de pro- cedimentos. Nestecaso, um retângulo ou uma linha de pseudo-código pode descrever uma tarefa tão complexa quanto necessário, sendo que esta tarefa pode ser posteriormente descrita em termos de outro(s) fluxograma(s) ou pseudo-código(s). Em geral, são aplicados tantos refinamentos quanto necessário até o ponto em que uma tarefa possa ser facilmente descrita em termos das construções suportadas pela lingua- gem de codificação. 1.3.3 Codificação A etapa de codificação traduz a representação do projeto detalhado em termos de uma linguagem de programação. Normalmente são utilizadas linguagens de alto nível, que podem então ser automaticamente traduzidas para a linguagem de máquina pelo processo de compilação. A tradução de uma especificação de um programa para uma linguagem de programação pode resultar em programas incompreensíveis se não houver um cuidado na preservação da informação presente. As linhas gerais que estão a seguir buscam estabelecer uma disciplina de codificação que, se seguida, facilita o entendimento e manutenção de programas. Código deve ser acima de tudo claro. Os compiladores modernos fazem um ótimo trabalho de otimi- zação de código, de forma que não há necessidade do programador ficar se preocupando em usar pequenos “truques” para economizar algumas instruções no código de máquina — provavelmente o compilador já faria isto para o programador (veja o Capítulo 10). A preocupação com a otimização de código só de- ve existir após a detecção da necessidade real desta otimização, e ela deve se restringir ao segmento do programa onde o problema de desempenho foi localizado. Nomes de identificadores (variáveis, procedimentos) devem denotar claramente o significado do iden- tificador. Linguagens de programação modernas raramente limitam o número de caracteres que um identi- ficador pode ter, de forma que não faz sentido restringir este tamanho. Compare as duas linhas de código a c 2000 DCA/FEEC/UNICAMP 9 Programação de Sistemas Ivan L. M. Ricarte Conceitos Básicos 1.3. Desenvolvimento de Software seguir, e observe como nomes claros podem facilitar a compreensão de um programa: d = v * t; distancia = velocidade * tempo; O uso de comentários deve ser usado como forma de documentação interna de um programa. O excesso de comentários não é recomendado, pois isto pode prejudicar a leitura de um programa. Entretanto, há comentários que devem estar presentes em qualquer programa, tais como � Comentários de prólogo, que aparecem no início de cada módulo. Devem indicar a finalidade do módulo, uma história de desenvolvimento (autor e data de criação e modificações), e uma descrição das variáveis globais (se houver); � Comentários de procedimento, que aparecem antes de cada função indicando seu propósito, uma descrição dos argumentos e valores de retorno; � Comentários descritivos, que descrevem blocos de código. Comentários devem ser facilmente diferenciáveis de código, seja através do uso de linhas em branco, seja através de tabulações. É importante que comentários sejam corretos e coerentes com o código, uma vez que um comentário errôneo pode dificultar mais ainda o entendimento de um programa do que se não houvesse comentário nenhum. Na especificação das construções estruturadas, use tabulações para indicar blocos de distintos níveis. Evite sempre que possível o uso de condições de teste complicadas e o aninhamento muito profundo de condições de teste. Use parênteses para deixar claro como expressões lógicas e aritméticas serão computa- das. Com relação a instruções de entrada e saída, todos os dados de entrada devem ser validados. O formato de entrada deve ser tão simples quanto possível e, no caso de entrada interativa, o usuário deve receber uma indicação de que o programa está aguardando dados. Da mesma forma, a apresentação de resultados deve ser clara, com indicação sobre o que está sendo apresentado. Finalmente, teste o valor de retorno de qualquer rotina que possa retornar um erro, tomando as ações que forem necessárias para minimizar o efeito do erro. c 2000 DCA/FEEC/UNICAMP 10 Parte II Montadores e Carregadores 11 Capítulo 2 Princípios de Montadores Índice 2.1 Definições . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.2 Instruções assembly . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.3 Pseudo-Instruções . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.3.1 Substituição simbólica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.3.2 Origem do segmento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.3.3 Definição de constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2.3.4 Declaração de variáveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2.3.5 Fim de programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2.4 Macromontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.4.1 Definição de macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.4.2 Uso de macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.4.3 Argumentos de macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 2.4.4 Expansão condicional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.5 Aspectos genéricos de projeto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.5.1 Leitura de arquivo fonte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 C: manipulação de arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 2.5.2 Extração de campos da linha . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 C: obtenção do conteúdo de arquivos de texto . . . . . . . . . . . . . . . . . . 23 C: manipulação de strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 2.5.3 Tratamento de pseudo-instruções . . . . . . . . . . . . . . . . . . . . . . . . . 25 2.5.4 Tratamento de macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 2.5.5 Tratamento de instruções . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 2.5.6 Tratamento de símbolos definidos . . . . . . . . . . . . . . . . . . . . . . . . . 26 Um programa em linguagem simbólica, ou linguagem assembly, contém seqüências de instruções mnemônicas que representam as operações que devem ser realizadas pelo processador. Essas instruções são definidas pelos projetistas da linguagem assembly do processador; o conjunto de todas as instruções definidas para um processador contitui seu jogo de instruções. O papel do montador é analisar cada instrução do programa em linguagem simbólica, traduzindo-a para a seqüência de bits correspondente à instrução de máquina. Como cada processador tem sua própria 12 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.1. Definições linguagem, montadores são específicos para processadores. Embora os exemplos desse capítulo utilizem um processador em particular, os princípios colocados para a operação e projeto de um montador são válidos independentemente de qual seja o processador utilizado. 2.1 Definições Montagem é o processo que realiza a tradução de cada instrução assembly para o código de máquina correspondente. Obviamente, esse o processo de montagem seria maçante e sujeito a erros se executa- do manualmente. A motivação para a existência de montadores é exatamente tornar esse processo tão mecânico como possível. Um arquivo fonte é um arquivo de texto que contém as instruções assembly que deverão passar pelo processo de montagem. O termo arquivo fonte é também utilizado para denotar arquivos que contém instruções em outras linguagens que não assembly — por exemplo, um programa em C que deverá passar pelo processo de compilação. O contexto de utilização deverádeixar claro que tipo de conteúdo está presente no arquivo fonte. O conteúdo de um arquivo fonte nada mais é do que uma seqüência de caracteres. A forma pela qual uma seqüência de bits é traduzida em termos de caracteres tem sido o objeto de diversos esforços de padronização. O padrão mais utilizado ainda é o formato ASCII (American Standard for Computer Information Interchange), seguido pelo padrão ISO-8859 e recentemente por UniCode. O formato ASCII básico (Tabela 2.1) permite representar 128 caracteres distintos (valores entre $00 e $7F), entre os quais estão diversos caracteres de controle (tais com ESC, associado à tecla de escape, e CR, associado ao carriage return) e caracteres de pontuação. Neste formato, os caracteres entre ‘0’ e ‘9’ são representados pelos valores hexadecimais $30 a $39, respectivamente (valores decimais entre 48 e 57); as letras maiúsculas ‘A’ a ‘Z’, pelos valores de $41 a $5A; e as letras minúsculas ‘a’ a ‘z’, pelos valores de $61 a $7A. ISO-8859 estende ASCII para valores entre $80 e $FF, sendo organizado em diversos subconjuntos, dos quais o ISO- 8859-1 (Latin-1) é o mais utilizado. UniCode integra várias famílias de caracteres em uma representação unificada (de até 16 bits), sendo que a base do padrão engloba as codificações ASCII e ISO-8859-1. Tabela 2.1 Codificação ASCII. Cód $-0 $-1 $-2 $-3 $-4 $-5 $-6 $-7 $-8 $-9 $-A $-B $-C $-D $-E $-F $0- NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI $1- DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US $2- [esp] ! " # $ % & ‘ ( ) * + , - . / $3- 0 1 2 3 4 5 6 7 8 9 : ; < = > ? $4- @ A B C D E F G H I J K L M N O $5- P Q R S T U V W X Y Z [ \ ] ^ _ $6- ‘ a b c d e f g h i j k l m n o $7- p q r s t u v w x y z { | } ~ DEL Um montador, ou assembler, é um programa cujos dados de entrada são seqüências de caracteres obtidas de um arquivo fonte que constituem o programa assembly e cujo resultado é um conjunto de pa- lavras de máquina (instruções, dados) gravadas em um arquivo binário constituindo um módulo que será posteriormente utilizado pelo carregador — um módulo de carregamento. Um segmento é um conjunto de palavras de máquina que deve ocupar um espaço contíguo na memória principal. O resultado gerado por um montador pode conter um ou mais segmentos de código objeto em linguagem de máquina. A posição de memória (endereço) associada ao início do segmento é denominada a sua origem. c 2000 DCA/FEEC/UNICAMP 13 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.2. Instruções assembly O uso de um montador também permite facilitar a codificação assembly, de modo que o programa- dor não precise mapear explicitamente a alocação de segmentos e variáveis à memória. Tipicamente, um montador irá oferecer um conjunto de pseudo-instruções, facilitadores que permitem a substituição de constantes e endereços por referências simbólicas definidas pelo programador. Pseudo-instruções são também denominadas pseudo-operações ou ainda diretivas. Um macromontador (macro-assembler) é um programa montador que permite adicionalmente a defi- nição e uso de macro-instruções para facilitar o processo de programação assembly. Um montador multiplataforma (cross-assembler) é um montador que permite gerar código para um processador alvo diferente do processador no qual o montador está sendo executado. Um carregador é um programa que permite transferir um programa, já montado, desde o módulo de carregamento para a memória e então iniciar sua execução. 2.2 Instruções assembly Em geral, um montador recebe como entrada um arquivo texto cujas linhas são instruções assembly. Cada linha contém tipicamente quatro campos separados por espaços ou tabulações. O formato geral de uma linha de instrução é [rotulo] operacao [operando[,operando,...]] [comentario] onde a notação [ ] indica que o campo indicado é opcional. No entanto, dependendo da operação o campo de operando ou de rótulo pode ser obrigatório. Tipicamente, espaços não são permitidos em um campo exceto no campo de comentário. Exemplos de instruções assembly são apresentados abaixo e os campos de uma instrução são detalhados na seqüência: CLR.W D0 LOOP MOVE.W (A0)+,D1 A0 foi definido alhures CMP.W D1,D0 BNE LOOP Rótulo. O rótulo permite fazer referências posteriores à posição de memória onde a instrução está arma- zenada. No exemplo acima, a segunda instrução tem um rótulo (LOOP) que é utilizado na quarta instrução para indicar o destino da instrução de desvio condicional (BNE). Sem o uso de rótulos, o programador deveria calcular e codificar explicitamente o valor do operando da instruçãoBNE — no exemplo, $FA1. Tradicionalmente, montadores reconhecem um campo de rótulo através de uma das duas alternativas: 1. O rótulo começa na primeira coluna; e/ou 2. O rótulo é encerrado pelo caráter ’:’, que não faz parte do rótulo mas age apenas como um terminador para ele. Operação. O campo de operação especifica a instrução que será montada. Um sufixo pode ser utilizado para indicar o tamanho do operando — no caso dos processadores da família 68K, .B, .W ou .L. Operandos. A presença ou não de operandos, assim como a quantidade de operandos presentes, depen- dem da instrução que está sendo executada. Se houver operandos nesse campo, não pode haver nenhum espaço entre eles exceto no interior de strings, seqüências de caracteres expressas entre aspas simples. 1O Apêndice A apresenta os fundamentos da representação de valores caracteres e inteiros em binário, incluindo a representação em complemento de 2. c 2000 DCA/FEEC/UNICAMP 14 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.2. Instruções assembly Comentários. Todo o restante da linha após o campo de operandos, ou após o campo de operação de uma instrução sem operandos, é considerada como comentário. Como tal, qualquer caráter — incluindo espaços — pode estar presente nesse campo. Tradicionalmente, montadores também permitem linhas de comentários que são iniciadas por algum caráter especial — tipicamente, ’*’. Através do uso de pseudo-instruções e dos rótulos, é possível fazer referências a posições de memória e a variáveis através de identificadores simbólicos. Isso permite que o programador possa usar esses identi- ficadores simbólicos como operandos de suas instruções sem ter que necessariamente saber a qual posição de memória a variável ou instrução está alocada. A regra para a composição de tais identificadores pode variar entre montadores. Em geral, idntificadores devem ser iniciados por caracteres alfabéticos e têm tamanho limitado — por exemplo, apenas os oito caracteres iniciais do identificador são significativos. Deve ser possível especificar constantes como parte do operando — por exemplo, para identificar um operando imediato, tipicamente indicado pelo prefixo #. A forma de identificação de valores constantes também pode mudar entre montadores distintos. A opção adotada neste texto é comum a diversos monta- dores: Constantes decimais: uma seqüência de dígitos for iniciada por um dígito (0 a 9), então assumir-se-á que a seqüência representa um valor decimal, ou seja, em base 10. Nenhum prefixo especial é utilizado. Por exemplo, MOVE.B #48,D0 coloca o valor 48 no byte menos significativo do registrador D0. Constantes binárias: uma seqüência de bits (0 ou 1) precedida pelo símbolo % identifica valores em base 2. Por exemplo, a mesma instrução acima poderia ter sido codificada como MOVE.B #%110000,D0 Constantes octais: uma seqüência de dígitos entre 0 e 7 precedidas pelo símbolo @ identifica valores em base 8. Ainda tomando a mesma instrução como exemplo, a codificação seria MOVE.B #@60,D0 Constantes hexadecimais: uma seqüência de dígitos hexadecimais (0 a 9, A a F) precedidas pelo símbolo $ identifica um valor em base 16. Para a mesma instrução, MOVE.B #$30,D0 Constantes ASCII: um valor caráter (ASCII)deverá ser apresentado entre aspas simples, como em ’A’ (valor $41). Seqüências de caracteres (strings) são definidas também entre aspas simples, como em ’ABC’ (seqüência de três bytes com valores $41, $42 e $43). Para o exemplo da instrução acima, a codificação equivalente seria MOVE.B #’0’,D0 Como pode ser observado, há muitas formas de expressar os mesmos valores. A forma selecionada deve ser aquela que torne mais claro para a leitura do programa qual o papel que a constante está desempenhando. c 2000 DCA/FEEC/UNICAMP 15 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.3. Pseudo-Instruções 2.3 Pseudo-Instruções Além das instruções do processador, um programa assembly preparado para um montador também pode conter pseudo-instruções que estabelecem a conexão entre referências simbólicas e valores a serem efetivamente referenciados. Cada montador pode oferecer um conjunto de pseudo-instruções diferenciado. As pseudo-instruções descritas a seguir, porém, representam um subconjunto significativo de facilidades oferecidas por monta- dores. 2.3.1 Substituição simbólica EQU associa um valor específico, definido pelo programador, a um símbolo. Sua forma genérica é rótulo EQU expressão onde rótulo é o símbolo que está sendo definido e expressão é o valor que será atribuído a ele. Este valor pode ser representado como uma constante ou conter uma expressão envolvendo outros símbolos já definidos, como em 1 SEG1 EQU $1000 2 DATA EQU SEG1+16 Nesse exemplo, o montador irá substituir as ocorrências do símbolo SEG1 pelo valor $1000 (hexade- cimal). Assim, o símbolo DATA estará associado ao valor $1010 e todas as suas ocorrências no programa serão substituídas por esse valor. 2.3.2 Origem do segmento ORG estabelece que a posição de memória da instrução seguinte deverá ser o valor especificado como o operando dessa pseudo-instrução. Sua forma genérica é ORG <expr> onde expr pode ser um valor constante ou um símbolo já definido. Por exemplo, assumindo as definições acima com EQU o trecho de programa a seguir 3 ORG SEG1 4 MOVE.W DATA,D0 5 MOVE.W D0,DATA+2 6 RTS indica que a primeira instrução MOVE.W (linha 4) estará alocada à posição $1000 da memória. A codifi- cação dessa instrução requer uma palavra para o código de operação, incluindo-se a definição do modo de endereçamento (direto) e do registrador de destino (D0), e duas palavras para expressar o endereço DATA, considerando por simplicidade que todos os endereços ocupam quatro bytes na codificação de máquina. Assim, a próxima instrução (linha 5) estará alocada à posição seguinte da memória, $1006. Similarmente, a instrução da linha 6 estará alocada à posição de memória $100C. c 2000 DCA/FEEC/UNICAMP 16 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.3. Pseudo-Instruções 2.3.3 Definição de constantes Para introduzir um valor específico em uma dada posição de memória, a pseudo-instrução DC (define constant) é utilizada. Sua forma genérica é <rotulo> DC.<s> <lista> onde rótulo será o símbolo utilizado para referenciar o valor (ou lista de valores) sendo definido. O valor deste rótulo será a posição de memória onde o valor estará sendo armazenado. O campo s identifica o tamanho de cada um dos valores armazenados a partir daquela posição, podendo ser B (byte), W (word) ou L (long word). Se omitido, W é assumido. Finalmente, lista identifica o valor ou seqüência de valores a armazenar na memória. Algumas possíveis definições usando DC são 1 ORG $2000 2 CONTADOR DC.L 100 3 ARR1 DC.W 0,1,1,2,3,5,8,13 4 MENSAGEM DC.B ’Alo, pessoal!’ A primeira linha com DC (linha 2) define que na posição $2000 (definida pela instrução ORG anterior) de memória será armazenado o valor 100 em quatro bytes, sendo que essa posição pode ser referenciada no programa pelo símbolo CONTADOR. A linha seguinte indica que a partir da posição seguinte de memória ($2004) será armazenado um conjunto de inteiros de dois bytes, sendo 0 o conteúdo armazenado na posição $2004, 1 na posição $2006, 1 em $2008, 2 em $200A e assim por diante. O símbolo ARR1 estará associado ao valor $2004, o início do arranjo. A pseudo-instrução na última linha expressa que a seqüência de caracteres (um byte cada um) indicada no campo de operando será armazenada a partir da posição $2014, que será o valor do símbolo MENSAGEM. 2.3.4 Declaração de variáveis Em algumas situações deseja-se reservar espaço na memória para uma variável, mas não se conhece seu valor de antemão. Neste caso, é possível usar a pseudo-instrução DS (declare storage), da forma <rotulo> DS.<s> <quant> Esta pseudo-instrução reserva espaço em memória para quant variáveis de tamanho s, que pode ser B (byte), W (word) ou L (long word). É precedida por um rótulo que define um nome simbólico para a variável, como em 1 ORG $2040 2 VALUE DS.W 1 que reserva na posição corrente ($2040) espaço para uma variável de tamanho dois bytes (word) e associa à posição dessa variável o símbolo VALUE. 2.3.5 Fim de programa Uma outra pseudo-instrução importante é END, que indica ao montador o fim do programa assembly. Seu formato geral é END rotulo_inicio c 2000 DCA/FEEC/UNICAMP 17 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.4. Macromontadores onde o rótulo no operando está associado a um rótulo do início do programa que está sendo encerrado por essa pseudo-instrução. O uso desse rótulo ficará mais claro quando for descrito o processo de carregamento (Seção 4.4). Essa pseudo-instrução só aparece uma vez no programa fonte e deve estar na sua última linha. 2.4 Macromontadores Uma instrução de macro (ou simplesmente macro) é um sinônimo para um grupo de instruções. O uso de macros facilita a utilização de trechos repetitivos de código, que podem ser invocados pelo programador como um única linha no programa. Por esse motivo, diversos montadores apresentam extensões com funcionalidades para a definição e utilização de macros — são os macromontadores. 2.4.1 Definição de macros Na sua forma mais simples, uma macro é simplesmente uma abreviatura para um grupo de instruções. A forma geral de definição de uma macro é: nome MACRO [argumentos] corpo ENDM A pseudo-instruçãoMACROmarca o início da definição da macro-instrução. Toda macro tem um nome, que será utilizado pelo programador para invocar a macro, e um corpo, que será usado pelo processador de macro para substituir o nome usado pelo programador. O fim da definição é indicado pela pseudo-instrução ENDM. O nome da macro está definido no campo de rótulo. Uma macro pode opcionalmente ter argumentos que serão usados na expansão da macro. O corpo é simplesmente uma seqüência de instruções que podem eventualmente ser parametrizadas pelos argumentos. Considere o seguinte exemplo de macro. Uma aplicação repetidamente transfere uma seqüência de quatro caracteres alfabéticos na posição de memória indicada pela constante CHARS_I para o registrador D0, onde converte essas letras para minúsculas. Os caracteres convertidos são colocados na posição de memória indicada pela constante CHARS_O. Através da definição de uma macro, essas ações podem ser especificadas como: 1 TOLOWER MACRO 2 MOVE.L CHARS_I,D0 3 ORI.L $20202020,D0 4 MOVE.L D0,CHARS_O 5 ENDM 2.4.2 Uso de macros Uma vez que uma macro esteja definida, seu nome pode ser utilizado como se fosse uma operação válida do montador. Considerando a definição acima, o uso da macro TOLOWER dar-se-ia da forma: 6 CHARS_I EQU $0400 7 CHARS_O EQU $0600 8 ORG $1000 9 PROG ... c 2000 DCA/FEEC/UNICAMP 18 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.4. Macromontadores 10 TOLOWER 11 ... 12 TOLOWER 13 ... 14 END PROG O exemplo ilustra duas invocações à macro-instrução. Após passar pelo processador de macro, o código acima seria convertidopara o seguinte código assembly onde as macros já estão expandidas: 15 CHARS_I EQU $0400 16 CHARS_O EQU $0600 17 ORG $1000 18 PROG ... 19 MOVE.L CHARS_I,D0 20 ORI.L $20202020,D0 21 MOVE.L D0,CHARS_O 22 ... 23 MOVE.L CHARS_I,D0 24 ORI.L $20202020,D0 25 MOVE.L D0,CHARS_O 26 ... 27 END PROG 2.4.3 Argumentos de macros A macro TOLOWER, da forma como foi definida, não oferece nenhuma flexibilidade, pois os endereços de entrada e saída de dados são pré-definidos. O uso de argumentos de macro permite flexibilizar essas definições e substituições. Argumentos em macros são representados por variáveis fictícias (dummy) que devem ser substituídas pelos nomes de variáveis reais durante a expansão da macro. A associação entre as variáveis reais e as va- riáveis dummy é feita pela posição da variável na declaração e invocação, assim como ocorre em subrotinas nas linguagens de alto nível. Um argumento de macro é identificado pelo símbolo &, como em 28 TOLOWER2 MACRO &IN,&OUT 29 MOVE.L &IN,D0 30 ORI.L $20202020,D0 31 MOVE.L D0,&OUT 32 ENDM Na invocação dessa macro TOLOWER2, dois argumentos devem ser fornecidos. Assim, a invocação 33 TOLOWER2 CHARS_I,CHARS_O tem exatamente a mesma expansão que a macro anterior. No entanto, é possível agora invocar a mesma macro para processar os quatro caracteres seguintes, como em 34 TOLOWER2 CHARS_I+4,CHARS_O+4 que seria expandido para 35 MOVE.L CHARS_I+4,D0 36 ORI.L $20202020,D0 37 MOVE.L D0,CHARS_O+4 Essa flexibilidade não seria possível com a definição original da macro TOLOWER. c 2000 DCA/FEEC/UNICAMP 19 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.5. Aspectos genéricos de projeto 2.4.4 Expansão condicional Outra facilidade geralmente associada a macro-instruções é a possibilidade de definir expansões con- dicionais de trechos de código. Para tanto, uma outra pseudo-instrução, AIF, é definida. Para ilustrar esse conceito, considere a situação na qual uma macro TOLOWER3 pudesse ser invocada especificando o registrador D0 como destino, 38 TOLOWER3 CHARS_I,D0 Caso o corpo de TOLOWER3 fosse idêntico ao corpo de TOLOWER2, haveria na última linha da ex- pansão da macro uma instrução desnecessária. Para evitar essa situação, TOLOWER3 poderia ser definida como 39 TOLOWER3 MACRO &IN,&OUT 40 MOVE.L &IN,D0 41 ORI.L $20202020,D0 42 AIF (&OUT EQ ’D0’).FIM 43 MOVE.L D0,&OUT 44 .FIM ENDM Na quarta linha da definição, aparece a instrução de expansão condicional. Após o código da operação AIF, a condição entre parênteses verifica se o argumento de macro &OUT é igual a D0. Se esse for o caso, a expansão continua na linha com o rótulo .FIM — rótulos de macro são precedidos pelo caráter ’.’ (ponto). Caso contrário, a expansão continua na linha seguinte. Assim, a seguinte seqüência de invocações 45 TOLOWER3 CHARS_I,CHARS_0 46 TOLOWER3 CHARS_I+4,D0 seria expandida para 47 MOVE.L CHARS_I,D0 48 ORI.L $20202020,D0 49 MOVE.L D0,CHARS_O 50 MOVE.L CHARS_I+4,D0 51 ORI.L $20202020,D0 No Apêndice C.17 apresenta-se uma facilidade de macro associada a uma linguagem de alto nível — o pré-processador C. 2.5 Aspectos genéricos de projeto As tarefas que devem ser desempenhadas por um montador dependem de características específicas do processador, da sintaxe da linguagem simbólica e do tipo de carregador que será utilizado para a execução do programa. Algumas funções são no entanto comuns a todos os montadores suportando as características apresentadas, destacando-se: � Ler um arquivo texto contendo instruções assembly, sendo capaz de identificar os vários campos de cada linha. � Realizar o pré-processamento de macros e de pseudo-instruções. c 2000 DCA/FEEC/UNICAMP 20 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.5. Aspectos genéricos de projeto � Determinar o comprimento de cada instrução. Para isso o montador precisa “conhecer” quantos operandos são requeridos para cada instrução e os modos de endereçamento destes operandos. � Substituir os mnemônicos de instruções e modos de endereçamento dos operandos pelos equivalentes binários. � Resolver e traduzir as expressões e os literais no campo de operandos para representações binárias. � Determinar posições de memória, relativas ao programa, para os endereços simbólicos (rótulos). � Substituir todas as referências a um endereço simbólico pelo endereço binário (efetivo) correspon- dente. � Gerar o resultado da montagem para algum arquivo, provavelmente usando algum formato interme- diário. Na seqüência, algumas dessas funcionalidades são detalhadas. 2.5.1 Leitura de arquivo fonte A criação do programa assembly — a seqüência de instruções que será traduzida para código de má- quina — é realizada através de qualquer programa editor. O arquivo fonte, resultado desse processo de edição, é a entrada para o montador. A primeira tarefa do montador é realizar a leitura desse arquivo fonte, cujo conteúdo é do tipo texto (caracteres). Para manipular qualquer arquivo, é preciso primeiro especificar esse arquivo — em termos computacionais, é preciso abrir o arquivo. Assim, alguma operação da forma OPENFILE(nome) deve estar presente. Essa operação pode não ter sucesso — o arquivo especificado pode não ser encontrado ou pode existir mas não oferecer permissão de acesso. Em caso de sucesso, o programa recebe uma referência para a manipulação do arquivo. Uma vez que o arquivo tenha sido aberto com sucesso, o próximo passo é realizar a leitura do seu con- teúdo. Sob o ponto de vista do montador, a principal necessidade é realizar a leitura de cada linha, pois essa é a unidade que delimita uma instrução. Assim, deve haver alguma operação da forma READLINE(arquivo), cujo retorno é uma string contendo a linha lida. Outras necessidades relacionadas à manipulação de arquivos incluem operações para detectar o fim do arquivo, ENDOFFILE(arquivo), retornando um valor booleano (verdadeiro ou falso); para fechar o arquivo após sua manipulação, CLOSEFILE(arquivo), que em princípio não precisa de nenhum valor de retorno; e retornar ao início do arquivo, REWINDFILE(arquivo). C: manipulação de arquivos Um arquivo é um recurso manipulado pelo sistema operacional ao qual a linguagem de programação tem acesso. É uma abstração que permite acessar convenientemente dispositivos externos (E/S), dos quais teclado e tela são casos particulares. Usualmente, para trabalhar com um arquivo, ele deve ser inicialmente aberto. Sua abertura indica ao sistema operacional que o arquivo será manipulado, de forma que informação que agiliza o acesso ao arquivo é mantida em memória. Após aberto, um arquivo é acessado através de um descritor, que basicamente indica onde o sistema operacional mantém a informação sobre o arquivo. Após aberto, operações de leitura e escrita podem ser efetuadas sobre o arquivo. Para que essas ope- rações de transferência de dados tenham sucesso, é preciso que haja a permissão adequada para a operação. Por exemplo, um teclado seria um dispositivo que não aceita saída de dados (escrita), mas apenas entrada (leitura). c 2000 DCA/FEEC/UNICAMP 21 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.5. Aspectos genéricos de projeto Encerradas as operações sobre um arquivo, ele deve ser fechado. Isso permite que o sistema opera- cional libere o espaço ocupado pelas informações sobre o arquivo para que esse mesmo espaço possa ser reocupado para a manipulação de outros arquivos. Esta liberação é importante, uma vez que sistemas ope- racionais tipicamente limitam a quantidade de arquivos que podem ser abertos simultaneamente devido a restrições de espaço alocado para essas estruturas auxiliares. Na linguagem de programação C, a informação sobre um arquivo é mantida em uma estrutura definida no arquivo de cabeçalho stdio.h. Um programa C que vá manipular arquivos deve então incluirno início de seu programa fonte a linha de inclusão desse arquivo de cabeçalho: #include <stdio.h> Esse arquivo de cabeçalho define o nome de tipo FILE associado a essa estrutura. Não é necessário conhecer o formato interno dessa estrutura para manipular arquivos. O programador C, para acessar ar- quivos, define variáveis ponteiros para este tipo, FILE *, que são manipuladas diretamente pelas funções da biblioteca padrão de entrada e saída. Tais variáveis são usualmente chamadas de manipuladores de arquivo. Dando continuidade ao exemplo, a função que vai manipular o arquivo deve incluir a declaração de uma variável manipulador de arquivo, como em: FILE *arqFonte; Para abrir um arquivo, a rotina fopen é invocada recebendo dois parâmetros. O primeiro é uma string com o nome do arquivo que será aberto. O segundo parâmetro é outra string que especifica o modo de acesso, que pode conter os caracteres r (leitura), w (escrita), a (escrita ao final — append), e b (acesso em modo binário). O valor de retorno é o manipulador alocado para o arquivo aberto. Por exemplo, para realizar a leitura do conteúdo de um arquivo chamado teste.asm, a seguinte expressão poderia ser usada no programa: arqFonte = fopen("teste.asm", "r"); Caso o arquivo não possa ser aberto, a função fopen retorna o ponteiro nulo. Assim, para verificar de o arquivo foi aberto sem problemas, é necessário testar o valor de retorno: if (arqFonte != 0) { /* tudo bem */ } else { /* erro */ } Quando o arquivo é aberto, a posição corrente (mantida internamente pelo sistema) é o início do ar- quivo. A cada operação executada sobre o arquivo, essa posição é atualizada. O valor da posição corrente pode ser obtido pela função ftell. A função feof retorna um valor verdadeiro (inteiro diferente de 0) se a posição corrente para o arquivo indicado é o final do arquivo, ou falso (inteiro igual a 0) em caso contrário. A função fseek permite o modificar a posição corrente para um ponto arbitrário do arquivo. O pri- meiro argumento dessa função é o manipulador do arquivo; o segundo, um valor long indicando o des- locamento desejado; e o terceiro, um valor inteiro inidcando a referência para o deslocamento, que pode ser o início do arquivo (SEEK_SET), a posição corrente (SEEK_CUR) ou o final do arquivo (SEEK_END). Um valor de retorno 0 indica que a operação foi completada com sucesso. A função rewind retorna a posição corrente para o início do arquivo, sem valor de retorno. Para fechar um arquivo previamente aberto, a rotina fclose pode ser usada. Ela recebe como argu- mento o manipulador do arquivo e não retorna nenhum valor. Assim, após encerrada a operação com o arquivo a expressão fclose(arqFonte); fecha-o. c 2000 DCA/FEEC/UNICAMP 22 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.5. Aspectos genéricos de projeto 2.5.2 Extração de campos da linha As facilidades para a leitura de um arquivo texto restringem-se tipicamente à obtenção do conjunto de caracteres de interesse — no caso do montador, uma linha do arquivo por vez. Para que o montador possa realizar seu processamento, é preciso que ele esteja informado de como extrair os campos de interesse de cada linha. Dessa forma, pode-se definir um conjunto de operações que encapsulem essa funcionalidade, recebendo como argumento uma string que é a linha sendo analisada: GETLABEL(linha) para extrair o rótulo da linha, se presente; GETOPERATION(linha) para extrair da linha o mnemônico de operação; GETOPERAND(linha) para extrair o campo de operandos da linha, se presente. O quarto campo de uma linha, de comentários, não é processado pelo montador. Assim, ele pode ser simplesmente ignorado. Quando um mnemônico de operação é obtido da linha de instrução, o seu tratamento vai depender do tipo de operação que ele representa. Assim, uma pseudo-instrução demanda um tratamento diferenciado daquele dispensado a uma instrução do processador ou a uma definição de macro. C: obtenção do conteúdo de arquivos de texto Para ler as linhas de um arquivo texto previamente aberto, a função fgets pode ser utilizada. Essa função recebe três parâmetros. O primeiro é o endereço de um arranjo de caracteres que irá receber a linha lida. O segundo é o número máximo de caracteres que deve ser lido da linha, caso a linha tenha mais caracteres que essa quantidade. O terceiro parâmetro é o manipulador do arquivo de onde a linha será lida. O retorno é um ponteiro para o início do arranjo com a linha, ou o ponteiro nulo caso o final do arquivo tenha sido atingido. O programa a seguir ilustra a manipulação de um arquivo texto em C usando essas funções. A função main recebe o nome de um arquivo pela linha de comando e abre esse arquivo. Em caso de sucesso, o conteúdo do arquivo é lido linha a linha (usando gets) e apresentado na tela (usando a função printf), precedido do número da linha. 1 /* Apresenta arquivo com linhas numeradas */ 2 #include <stdio.h> 3 #define TAMBUFF 120 4 int main(int argc, char* argv[]) { 5 FILE *arquivo; 6 char *linha; 7 char buffer[TAMBUFF]; 8 int contLinha = 0; 9 if (argc != 2) { 10 fprintf(stderr, "%s requer um nome de arquivo\n", argv[0]); 11 return 1; 12 } 13 arquivo = fopen(argv[1],"r"); 14 if (arquivo == 0) { 15 fprintf(stderr, 16 "Arquivo %s nao pode ser aberto para leitura\n", argv[1]); 17 return 2; 18 } 19 while (!feof(arquivo)) { c 2000 DCA/FEEC/UNICAMP 23 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.5. Aspectos genéricos de projeto 20 linha = fgets(buffer,TAMBUFF,arquivo); 21 if (linha != 0) 22 printf("%3d: %s", ++contLinha, linha); 23 } 24 fclose(arquivo); 25 return 0; 26 } C: manipulação de strings O arquivo de cabeçalho string.h contém a declaração de diversas funções para a manipulação de strings em C. Strings em C são arranjos de caracteres, sendo usualmente manipuladas através do ponteiro para o início do arranjo. O fim da string é indicado pela presença do caráter NUL (’\0’). O exemplo a seguir ilustra uma função para extrair rótulos de uma linha. A linha é uma string que é passada como argumento para a função. O rótulo, se presente na linha, tem início logo na primeira coluna, sendo terminado por um espaço em branco. 1 #define LABELSIZE 10 2 char* getLabel(char* linha) { 3 static char labelBuf[LABELSIZE]; 4 int pos = 0; 5 if (linha[0] == ’ ’) 6 return 0; 7 else 8 while (linha[pos] != ’ ’) 9 labelBuf[pos] = linha[pos++]; 10 labelBuf[pos] = ’\0’; 11 return labelBuf; 12 } Uma simples adaptação no programa que manipula arquivos, em combinação com o uso dessa função, permite criar um programa que identifica rótulos em um arquivo texto. Tendo como argumento o seguinte arquivo texto: CHARS_I EQU $0400 CHARS_O EQU $0600 ORG $1000 PROG ... MOVE.L CHARS_I,D0 ORI.L $20202020,D0 MOVE.L D0,CHARS_O ... MOVE.L CHARS_I,D0 ORI.L $20202020,D0 MOVE.L D0,CHARS_O ... END PROG o resultado da execução do programa é: Rotulo CHARS_I na linha 1 Rotulo CHARS_O na linha 2 Rotulo PROG na linha 4 c 2000 DCA/FEEC/UNICAMP 24 Programação de Sistemas Ivan L. M. Ricarte Princípios de Montadores 2.5. Aspectos genéricos de projeto Na função getLabel, tanto a linha (argumento) como o rótulo resultante (valor de retorno) são pas- sados através de ponteiros. A manipulação de ponteiros é fonte usual de confusão em qualquer linguagem. Por exemplo, tendo duas variáveis ponteiros char* s1 e char* s2 indicando o início de duas strings, não seria possível copiar o conteúdo de s2 para s1 simplesmente por atribuição, s1 = s2; /* copia o endereco! */ ou comparar seus conteúdos diretamente, if (s1 != s2) /* compara os enderecos! */ ... Assim, diversas rotinas são suportadas para manipular strings na biblioteca padrão, tais como: #include <string.h> char *strcat(char *s1, const char *s2); char *strncat(char *s1, const char
Compartilhar