Menu English Ukrainian Russo INÍCIO

Biblioteca técnica gratuita para amadores e profissionais Biblioteca técnica gratuita


Informática e tecnologias da informação. Notas de aula: resumidamente, o mais importante

Notas de aula, folhas de dicas

Diretório / Notas de aula, folhas de dicas

Comentários do artigo Comentários do artigo

Índice analítico

  1. Introdução à Ciência da Computação (Informática. Informação. Apresentação e processamento de informação. Sistemas numéricos. Representação de números num computador. Conceito formalizado de algoritmo)
  2. Linguagem Pascal (Introdução ao Pascal. Procedimentos e funções padrão. Operadores Pascal)
  3. Procedimentos e funções (O conceito de algoritmo auxiliar. Procedimentos em Pascal. Funções em Pascal. Descrições antecipatórias e conexão de sub-rotinas. Diretiva)
  4. Sub-rotinas (Parâmetros de rotina. Tipos de parâmetros de sub-rotina. Tipo string em Pascal. Procedimentos e funções para variáveis ​​​​do tipo string. Registros. Conjuntos)
  5. Arquivos (Arquivos. Operações de arquivo. Módulos. Tipos de módulos)
  6. Memória dinâmica (Tipo de dados de referência. Memória dinâmica. Variáveis ​​dinâmicas. Trabalhando com memória dinâmica. Ponteiros não digitados)
  7. Estruturas de dados abstratas (Estruturas de dados abstratas. Pilhas. Filas)
  8. Estruturas de dados em árvore (Estruturas de dados em árvore. Operações em árvores. Exemplos de implementação de operações)
  9. Contagens (O conceito de gráfico. Métodos de representação de um gráfico. Representação de um gráfico por uma lista de incidências. Algoritmo de travessia em profundidade para um gráfico. Representação de um gráfico como uma lista de listas. Algoritmo de travessia em largura para um gráfico )
  10. Tipo de dados do objeto (Tipo de objeto em Pascal. O conceito de objeto, sua descrição e uso. Herança. Criação de instâncias de objetos. Componentes e escopo)
  11. Métodos (Métodos. Construtores e destruidores. Destruidores. Métodos virtuais. Campos de dados de objetos e parâmetros de métodos formais)
  12. Compatibilidade do tipo de objeto (Encapsulamento. Objetos extensíveis. Compatibilidade de tipo de objeto)
  13. Montador (Sobre assembler. Modelo de software de microprocessador. Registros de usuário. Registros de uso geral. Registros de segmento. Registros de status e controle)
  14. Registros (Registros do sistema microprocessador. Registros de controle. Registros de endereço do sistema. Registros de depuração)
  15. Programas de montagem (Estrutura do programa Assembler. Sintaxe Assembler. Operadores de comparação. Operadores e sua precedência. Diretivas simplificadas de definição de segmento. Identificadores criados pela diretiva MODEL. Modelos de memória. Modificadores de modelo de memória)
  16. Estruturas de instruções de montagem (Estrutura de uma instrução de máquina. Métodos para especificar operandos de instrução. Métodos de endereçamento)
  17. Equipes (Comandos de transferência de dados. Comandos aritméticos)
  18. Comandos de transferência de controle (Comandos lógicos. Tabela verdade para negação lógica. Tabela verdade para OR lógico inclusivo. Tabela verdade para AND lógico. Tabela verdade para OR exclusivo lógico. Significado das abreviaturas no nome do comando jcc. Lista de comandos de salto condicional para o comando. Salto condicional comandos e sinalizadores)

PALESTRA No. 1. Introdução à ciência da computação

1. Informática. Em formação. Representação e processamento de informações

A informática está engajada em uma representação formalizada de objetos e estruturas de seus relacionamentos em vários campos da ciência, tecnologia e produção. Várias ferramentas formais são usadas para modelar objetos e fenômenos, como fórmulas lógicas, estruturas de dados, linguagens de programação, etc.

Na ciência da computação, um conceito tão fundamental como a informação tem vários significados:

1) apresentação formal de formas externas de informação;

2) significado abstrato da informação, seu conteúdo interno, semântica;

3) relação da informação com o mundo real.

Mas, via de regra, a informação é entendida como seu significado abstrato - semântica. Interpretando a representação da informação, obtemos seu significado, a semântica. Portanto, se queremos trocar informações, precisamos de visões consistentes para que a correção da interpretação não seja violada. Para isso, a interpretação da representação da informação é identificada com algumas estruturas matemáticas. Nesse caso, o processamento da informação pode ser realizado por métodos matemáticos rigorosos.

Uma das descrições matemáticas da informação é a sua representação na forma de uma função y =f(x, t), onde t é o tempo, x é um ponto em um determinado campo no qual o valor de y é medido. Dependendo dos parâmetros da função chi (as informações podem ser classificadas.

Se os parâmetros são grandezas escalares que assumem uma série contínua de valores, então a informação obtida desta forma é chamada de contínua (ou analógica). Se os parâmetros recebem uma determinada etapa de alteração, a informação é chamada de discreta. A informação discreta é considerada universal, pois para cada parâmetro específico é possível obter um valor de função com um determinado grau de precisão.

A informação discreta é geralmente identificada com a informação digital, que é um caso especial de informação simbólica de representação alfabética. Um alfabeto é um conjunto finito de símbolos de qualquer natureza. Muitas vezes, na ciência da computação, surge uma situação em que os caracteres de um alfabeto devem ser representados pelos caracteres de outro, ou seja, para realizar uma operação de codificação. Se o número de caracteres do alfabeto de codificação for menor que o número de caracteres do alfabeto de codificação, a operação de codificação em si não será complicada; caso contrário, será necessário usar um conjunto fixo de caracteres do alfabeto de codificação para uma codificação correta inequívoca.

Como a prática tem mostrado, o alfabeto mais simples que permite codificar outros alfabetos é o binário, que consiste em dois caracteres, que geralmente são denotados por 0 e 1. Usando n caracteres do alfabeto binário, você pode codificar 2n caracteres, e isso é suficiente para codificar qualquer alfabeto.

O valor que pode ser representado por um símbolo do alfabeto binário é chamado de unidade mínima de informação ou bit. Sequência de 8 bits - bytes. Um alfabeto contendo 256 sequências de 8 bits diferentes é chamado de alfabeto de bytes.

Como padrão hoje em ciência da computação, adota-se um código em que cada caractere é codificado por 1 byte. Existem outros alfabetos também.

2. Sistemas numéricos

Um sistema numérico é um conjunto de regras para nomear e escrever números. Existem sistemas numéricos posicionais e não posicionais.

O sistema numérico é chamado posicional se o valor do dígito do número depende da localização do dígito no número. Caso contrário, é chamado de não posicional. O valor de um número é determinado pela posição desses dígitos no número.

3. Representação de números em um computador

Processadores de 32 bits podem trabalhar com até 232-1 RAM e endereços podem ser escritos no intervalo 00000000 - FFFFFFFF. No entanto, em modo real, o processador opera com memória até 220-1, e os endereços ficam na faixa 00000 - FFFFF. Bytes de memória podem ser combinados em campos de comprimento fixo e variável. Uma palavra é um campo de comprimento fixo que consiste em 2 bytes, uma palavra dupla é um campo de 4 bytes. Os endereços de campo podem ser pares ou ímpares, com endereços pares realizando operações mais rapidamente.

Os números de ponto fixo são representados em computadores como números binários inteiros e seu tamanho pode ser de 1, 2 ou 4 bytes.

Os inteiros binários são representados em complemento de dois e os números de ponto fixo são representados em complemento de dois. Além disso, se um número ocupa 2 bytes, então a estrutura do número é escrita de acordo com a seguinte regra: o dígito mais significativo é atribuído ao sinal do número e o restante - aos dígitos binários do número. O código complementar de um número positivo é igual ao próprio número, e o código complementar de um número negativo pode ser obtido pela seguinte fórmula: x = 10i - \x\, onde n é a capacidade de dígitos do número.

No sistema de numeração binário, um código adicional é obtido invertendo bits, ou seja, substituindo unidades por zeros e vice-versa, e adicionando um ao bit menos significativo.

O número de bits da mantissa determina a precisão da representação dos números, o número de bits da ordem de máquina determina o intervalo de representação dos números de ponto flutuante.

4. Conceito formalizado de um algoritmo

Um algoritmo só pode existir se, ao mesmo tempo, existir algum objeto matemático. O conceito formalizado de algoritmo está ligado ao conceito de funções recursivas, algoritmos normais de Markov, máquinas de Turing.

Em matemática, uma função é chamada de valor único se, para qualquer conjunto de argumentos, existe uma lei pela qual o valor único da função é determinado. Um algoritmo pode atuar como tal lei; neste caso diz-se que a função é computável.

Funções recursivas são uma subclasse de funções computáveis, e os algoritmos que definem a computação são chamados de algoritmos de funções recursivas complementares. Primeiro, as funções recursivas básicas são fixas, para as quais o algoritmo que as acompanha é trivial, não ambíguo; em seguida, três regras são introduzidas - operadores de substituição, recursão e minimização, com a ajuda dos quais funções recursivas mais complexas são obtidas com base em funções básicas.

As funções básicas e seus algoritmos de acompanhamento podem ser:

1) uma função de n variáveis ​​independentes, identicamente igual a zero. Então, se o sinal da função for φn, independentemente do número de argumentos, o valor da função deve ser igual a zero;

2) a função identidade de n variáveis ​​independentes da forma ψni. Então, se o sinal da função for ψni, então o valor da função deve ser tomado como o valor do i-ésimo argumento, contando da esquerda para a direita;

3) Λ é uma função de um argumento independente. Então, se o sinal da função for λ, então o valor da função deve ser tomado como o valor que segue o valor do argumento. Vários estudiosos propuseram suas próprias abordagens para a formalização

representação do algoritmo. Por exemplo, o cientista americano Church sugeriu que a classe de funções computáveis ​​é esgotada por funções recursivas e, como resultado, qualquer que seja o algoritmo que processa um conjunto de inteiros não negativos em outro, existe um algoritmo que acompanha a função recursiva que é equivalente ao dado. Portanto, se é impossível construir uma função recursiva para resolver um determinado problema, então não há algoritmo para resolvê-lo. Outro cientista, Turing, desenvolveu um computador virtual que processava uma sequência de caracteres de entrada em uma saída. A este respeito, ele apresentou a tese de que qualquer função computável é Turing computável.

PALESTRA Nº 2. Linguagem Pascal

1. Introdução à linguagem Pascal

Os símbolos básicos do idioma - letras, números e caracteres especiais - compõem seu alfabeto. A linguagem Pascal inclui o seguinte conjunto de símbolos básicos:

1) 26 letras minúsculas latinas e 26 letras maiúsculas latinas:

ABCDEFGHIJKLMNOPQRSTUVWXYZ

a B C D e F G H I J K L M N o p q R S T U V W x y Z;

2) _ (sublinhado);

3) 10 dígitos: 0123456789;

4) sinais de operação:

+ - x / = <> < > <= >= := @;

5) limitadores:

., ' ( ) [ ] (..) { } (* *).. : ;

6) especificadores: ^ # $;

7) palavras de serviço (reservadas):

ABSOLUTO, ASSEMBLER, AND, ARRAY, ASM, BEGIN, CASE, CONST, CONSTRUCTOR, DESTRUCTOR, DIV, DO, DOWNTO, ELSE, END, EXPORT, EXTERNAL, FAR, FILE, FOR, FORWARD, FUNCTION, GOTO, SE, IMPLEMENTATION, EM, ÍNDICE, HERDADO, INLINE, INTERFACE, INTERRUPT, LABEL, LIBRARY, MOD, NAME, NIL, NEAR, NOT, OBJECT, OF, OR, EMBALADO, PRIVADO, PROCEDIMENTO, PROGRAMA, PÚBLICO, RECORD, REPEAT, RESIDENT, SET, SHL, SHR, STRING, THEN, TO, TYPE, UNIT, UNTIL, USA, VAR, VIRTUAL, WHILE, WITH, XOR.

Além dos listados, o conjunto de caracteres básicos inclui um espaço. Espaços não podem ser usados ​​dentro de caracteres duplos e palavras reservadas.

Conceito de tipo para dados

É costume em matemática classificar as variáveis ​​de acordo com algumas características importantes. É feita uma distinção estrita entre variáveis ​​reais, complexas e lógicas, entre variáveis ​​que representam valores individuais e um conjunto de valores, etc. Ao processar dados em um computador, essa classificação é ainda mais importante. Em qualquer linguagem algorítmica, cada constante, variável, expressão ou função é de um tipo específico.

Existe uma regra em Pascal: o tipo é especificado explicitamente na declaração de uma variável ou função que antecede seu uso. O conceito de tipo Pascal tem as seguintes propriedades principais:

1) qualquer tipo de dado define um conjunto de valores ao qual pertence uma constante, que uma variável ou expressão pode assumir, ou uma operação ou função pode produzir;

2) o tipo de valor dado por uma constante, variável ou expressão pode ser determinado por sua forma ou descrição;

3) cada operação ou função requer argumentos de tipo fixo e produz um resultado de tipo fixo.

Segue-se que o compilador pode usar informações de tipo para verificar a computabilidade e a correção de várias construções.

O tipo define:

1) valores possíveis de variáveis, constantes, funções, expressões pertencentes a um determinado tipo;

2) a forma interna de apresentação dos dados em um computador;

3) operações e funções que podem ser executadas em valores pertencentes a um determinado tipo.

Deve-se notar que a descrição obrigatória do tipo leva à redundância no texto dos programas, mas tal redundância é uma importante ferramenta auxiliar para o desenvolvimento de programas e é considerada uma propriedade necessária das modernas linguagens algorítmicas de alto nível.

Existem tipos de dados escalares e estruturados em Pascal. Os tipos escalares incluem tipos padrão e tipos definidos pelo usuário. Os tipos padrão incluem tipos inteiro, real, caractere, booleano e endereço.

Os tipos inteiros definem constantes, variáveis ​​e funções cujos valores são realizados pelo conjunto de inteiros permitidos em um determinado computador.

Os tipos reais definem os dados implementados por um subconjunto de números reais permitidos em um determinado computador.

Os tipos definidos pelo usuário são enum e range. Os tipos estruturados vêm em quatro tipos: arrays, conjuntos, registros e arquivos.

Além dos listados, o Pascal inclui mais dois tipos - procedural e object.

Uma expressão de linguagem consiste em constantes, variáveis, ponteiros de função, sinais de operador e colchetes. Uma expressão define uma regra para calcular algum valor. A ordem de cálculo é determinada pela precedência (prioridade) das operações nele contidas. Pascal tem a seguinte precedência de operador:

1) cálculos entre parênteses;

2) cálculo dos valores das funções;

3) operações unárias;

4) operações *, /, div, mod e;

5) operações +, -, ou, xor;

6) operações relacionais =, <>, <, >, <=, >=.

As expressões fazem parte de muitos operadores da linguagem Pascal e também podem ser argumentos para funções internas.

2. Procedimentos e funções padrão

Funções aritméticas

1.Função Abs(X);

Retorna o valor absoluto do parâmetro.

X é uma expressão do tipo real ou inteiro.

2. Função ArcTan(X: Estendido): Estendido;

Retorna o arco tangente do argumento.

X é uma expressão do tipo real ou inteiro.

3. Função exp(X: Real): Real;

Retorna o expoente.

X é uma expressão do tipo real ou inteiro.

4.Frac(X: Real): Real;

Retorna a parte fracionária do argumento.

X é uma expressão de tipo real. O resultado é a parte fracionária de X, ou seja.

Frac(X) = X-Int(X).

5. Função Int(X: Real): Real;

Retorna a parte inteira do argumento.

X é uma expressão de tipo real. O resultado é a parte inteira de X, ou seja, X arredondado para zero.

6. Função Ln(X: Real): Real;

Retorna o logaritmo natural (Ln e = 1) de uma expressão de tipo real X.

7.Função Pi: Estendido;

Retorna o valor Pi, que é definido como 3.1415926535.

8.Função Sin(X: Estendido): Estendido;

Retorna o seno do argumento.

X é uma expressão de tipo real. Sin retorna o seno do ângulo X em radianos.

9.Função Sqr(X: Estendido): Estendido;

Retorna o quadrado do argumento.

X é uma expressão de ponto flutuante. O resultado é do mesmo tipo que X.

10.Função Sqrt(X: Estendido): Estendido;

Retorna a raiz quadrada do argumento.

X é uma expressão de ponto flutuante. O resultado é a raiz quadrada de X.

Procedimentos e funções de conversão de valor

1. Procedimento Str(X [: Largura [: Decimais]]; var S);

Converte o número X em uma representação de string de acordo com

Opções de formatação de largura e decimais. X é uma expressão do tipo real ou inteiro. Largura e Decimais são expressões do tipo inteiro. S é uma variável do tipo String ou uma matriz de caracteres terminada em nulo se a sintaxe estendida for permitida.

2. Função Chr(X: Byte): Char;

Retorna o caractere com X ordinal na tabela ASCII.

3.Função Alta(X);

Retorna o maior valor no intervalo do parâmetro.

4.FunçãoBaixa(X);

Retorna o menor valor no intervalo de parâmetros.

5 FunctionOrd(X): Inteiro longo;

Retorna o valor ordinal de uma expressão de tipo enumerado. X é uma expressão de tipo enumerado.

6. Função Round(X: Extended): Longint;

Arredonda um valor de tipo real para o inteiro mais próximo. X é uma expressão de tipo real. Round retorna um valor Longint, que é o valor de X arredondado para o número inteiro mais próximo. Se X estiver exatamente na metade entre dois números inteiros, o número com o maior valor absoluto será retornado. Se o valor arredondado de X estiver fora do intervalo Longint, é gerado um erro em tempo de execução que você pode manipular usando a exceção EInvalidOp.

7. Função Trunc(X: Extended): Longint;

Trunca um valor de tipo real para um inteiro. Se o valor arredondado de X estiver fora do intervalo Longint, é gerado um erro em tempo de execução que você pode manipular usando a exceção EInvalidOp.

8. Procedimento Val(S; var V; var Código: Inteiro);

Converte um número de um valor de string S para um número

representação V. S - expressão tipo string - uma sequência de caracteres que forma um número inteiro ou real. Se a expressão S for inválida, o índice do caractere inválido será armazenado na variável Code. Caso contrário, o Código é definido como zero.

Procedimentos e funções para trabalhar com valores ordinais

1. Procedimento Dec(varX [; N: LongInt]);

Subtrai um ou N da variável X. Dec(X) corresponde a X:= X - 1 e Dec(X, N) corresponde a X:= X - N. X é uma variável de um tipo enumerado ou do tipo PChar se a sintaxe estendida for permitida e N for uma expressão do tipo inteiro. O procedimento Dec gera código ideal e é especialmente útil em loops longos.

2. Procedimento Inc(varX [; N: LongInt]);

Adiciona um ou N à variável X. X é uma variável do tipo enumerado ou do tipo PChar se a sintaxe estendida for permitida e N é uma expressão do tipo integral. Inc (X) corresponde à instrução X:= X + 1, e Inc (X, N) corresponde à instrução X:= X + N. O procedimento Inc gera código ideal e é especialmente útil em loops longos.

3. Função Ímpar(X: LongInt): Booleano;

Retorna True se X for um número ímpar, False caso contrário.

4.FunçãoPred(X);

Retorna o valor anterior do parâmetro. X é uma expressão de tipo enumerado. O resultado é do mesmo tipo.

5 Função Succ(X);

Retorna o próximo valor do parâmetro. X é uma expressão de tipo enumerado. O resultado é do mesmo tipo.

3. Operadores da linguagem Pascal

Operador condicional

O formato da instrução condicional completa é definido da seguinte forma: Se B então SI else S2; onde B é uma condição de ramificação (tomada de decisão), uma expressão lógica ou uma relação; SI, S2 - uma instrução executável, simples ou composta.

Ao executar uma instrução condicional, primeiro a expressão B é avaliada, depois seu resultado é analisado: se B for verdadeiro, então a instrução S1 é executada - a ramificação de then, e a instrução S2 é ignorada; se B for falso, então a instrução S2 - a ramificação else é executada e a instrução S1 é ignorada.

Há também uma forma abreviada do operador condicional. É escrito como: Se B então S.

Selecionar declaração

A estrutura do operador é a seguinte:

caso S de

c1: instrução1;

c2: instrução2;

...

cn: instruçãoN;

outra instrução

end;

onde S é uma expressão de tipo ordinal cujo valor está sendo calculado;

с1, с2..., сп - constantes do tipo ordinal com as quais as expressões são comparadas

S; instrução1,..., instruçãoN - operadores dos quais é executado aquele cuja constante corresponde ao valor da expressão S;

instrução - uma instrução que é executada se o valor da expressão Sylq não corresponder a nenhuma das constantes c1, c2.... cn.

Este operador é uma generalização do operador condicional If para um número arbitrário de alternativas. Existe uma forma abreviada da declaração onde não há outro ramo.

Instrução de loop com parâmetro

Instruções de loop de parâmetro que começam com a palavra for fazem com que a instrução, que pode ser uma instrução composta, seja executada repetidamente enquanto a variável de controle recebe uma sequência crescente de valores.

Visão geral da instrução for:

for <loop counter> := <start value> to <end value> do <statement>;

Quando a instrução for começa a ser executada, os valores inicial e final são determinados uma vez, e esses valores são retidos durante toda a execução da instrução for. A instrução contida no corpo da instrução for é executada uma vez para cada valor no intervalo entre os valores inicial e final. O contador de loops é sempre inicializado com um valor inicial. Quando a instrução for está em execução, o valor do contador de loops é incrementado a cada iteração. Se o valor inicial for maior que o valor final, a instrução contida no corpo da instrução for não será executada. Quando a palavra-chave downto é usada em uma instrução de loop, o valor da variável de controle é decrementado em um em cada iteração. Se o valor inicial em tal instrução for menor que o valor final, a instrução contida no corpo da instrução de loop não será executada.

Se a instrução contida no corpo da instrução for alterar o valor do contador de loops, isso será um erro. Após a execução da instrução for, o valor da variável de controle torna-se indefinido, a menos que a execução da instrução for tenha sido interrompida por uma instrução jump.

Instrução de loop com pré-condição

Uma instrução de loop de pré-condição (começando com a palavra-chave while) contém uma expressão que controla a execução repetida da instrução (que pode ser uma instrução composta). Forma do ciclo:

Enquanto B faz S;

onde B é uma condição lógica, cuja veracidade é verificada (é uma condição para encerrar o loop);

S - corpo do loop - uma instrução.

A expressão que controla a repetição de uma instrução deve ser do tipo Boolean. Ele é avaliado antes que a instrução interna seja executada. A instrução interna é executada repetidamente enquanto a expressão for avaliada como True. Se a expressão for avaliada como False desde o início, a instrução contida na instrução do loop de pré-condição não será executada.

Instrução de loop com pós-condição

Em uma instrução de loop com uma pós-condição (começando com a palavra repeat), a expressão que controla a execução repetida de uma sequência de instruções está contida na instrução repeat. Forma do ciclo:

repita S até B;

onde B é uma condição lógica, cuja veracidade é verificada (é uma condição para encerrar o loop);

S - uma ou mais instruções do corpo do loop.

O resultado da expressão deve ser do tipo booleano. As instruções entre as palavras-chave repeat e until são executadas sequencialmente até que o resultado da expressão seja avaliado como True. A sequência de instruções será executada pelo menos uma vez porque a expressão é avaliada após cada execução da sequência de instruções.

PALESTRA Nº 3. Procedimentos e funções

1. O conceito de um algoritmo auxiliar

O algoritmo de resolução de problemas é projetado decompondo todo o problema em subtarefas separadas. Normalmente, as subtarefas são implementadas como sub-rotinas.

Uma sub-rotina é algum algoritmo auxiliar que é usado repetidamente no algoritmo principal com valores diferentes de algumas grandezas de entrada, chamadas de parâmetros.

Uma sub-rotina em linguagens de programação é uma sequência de instruções que são definidas e escritas em apenas um local do programa, mas podem ser chamadas para execução a partir de um ou mais pontos do programa. Cada sub-rotina é identificada por um nome único.

Existem dois tipos de sub-rotinas em Pascal, procedimentos e funções. Um procedimento e uma função são uma sequência nomeada de declarações e instruções. Ao usar procedimentos ou funções, o programa deve conter o texto do procedimento ou função e a chamada para o procedimento ou função. Os parâmetros especificados na descrição são chamados de formais, os especificados na chamada da sub-rotina são chamados de reais. Todos os parâmetros formais podem ser divididos nas seguintes categorias:

1) variáveis-parâmetros;

2) parâmetros constantes;

3) valores de parâmetros;

4) parâmetros de procedimento e parâmetros de função, ou seja, parâmetros de tipo de procedimento;

5) parâmetros de variáveis ​​não tipadas.

Os textos de procedimentos e funções são colocados na seção de descrições de procedimentos e funções.

Passando nomes de procedimentos e funções como parâmetros

Em muitos problemas, especialmente em matemática computacional, é necessário passar os nomes de procedimentos e funções como parâmetros. Para isso, o TURBO PASCAL introduziu um novo tipo de dado - procedimental ou funcional, dependendo do que for descrito. (Os tipos de procedimento e função são descritos na seção de declaração de tipo.)

Uma função e um tipo de procedimento são definidos como o cabeçalho de um procedimento e uma função com uma lista de parâmetros formais, mas sem nome. É possível definir uma função ou tipo procedural sem parâmetros, por exemplo:

tipo

Proc = procedimento;

Depois de declarar um tipo procedural ou funcional, ele pode ser usado para descrever parâmetros formais - os nomes de procedimentos e funções. Além disso, é necessário escrever aqueles procedimentos ou funções reais cujos nomes serão passados ​​como parâmetros reais.

2. Procedimentos em Pascal

Cada descrição de procedimento contém um cabeçalho seguido por um bloco de programa. A forma geral do cabeçalho do procedimento é a seguinte:

Procedimento <nome> [(<lista de parâmetros formais>)];

Um procedimento é ativado com uma instrução de procedimento que contém o nome do procedimento e os parâmetros necessários. As instruções a serem executadas quando o procedimento é executado estão contidas na parte de instrução do módulo de procedimento. Se um identificador de procedimento for usado em uma instrução contida em um procedimento dentro de um módulo de procedimento, o procedimento será executado recursivamente, ou seja, ele fará referência a si mesmo quando executado.

3. Funções em Pascal

Uma declaração de função define a parte do programa na qual o valor é calculado e retornado. A forma geral do cabeçalho da função é a seguinte:

Função <nome> [(<lista de parâmetros formais>)]: <tipo de retorno>;

A função é ativada quando é chamada. Quando uma função é chamada, o identificador da função e quaisquer parâmetros necessários para sua avaliação são especificados. Uma chamada de função pode ser incluída em expressões como um operando. Quando a expressão é avaliada, a função é executada e o valor do operando passa a ser o valor retornado pela função.

A parte do operador do bloco funcional especifica as instruções que devem ser executadas quando a função é ativada. Um módulo deve conter pelo menos uma instrução de atribuição que atribua um valor a um identificador de função. O resultado da função é o último valor atribuído. Se não houver tal instrução de atribuição ou se ela não tiver sido executada, o valor de retorno da função será indefinido.

Se um identificador de função for usado ao chamar uma função dentro de um módulo, a função será executada recursivamente.

4. Encaminhar descrições e conexão de sub-rotinas. Diretiva

Um programa pode conter várias sub-rotinas, ou seja, a estrutura do programa pode ser complicada. No entanto, essas sub-rotinas podem estar no mesmo nível de aninhamento, portanto, a declaração da sub-rotina deve vir primeiro e depois a chamada a ela, a menos que uma declaração especial de encaminhamento seja usada.

Uma declaração de procedimento que contém uma diretiva de encaminhamento em vez de um bloco de instrução é chamada de declaração de encaminhamento. Em algum lugar após esta declaração, um procedimento deve ser definido por uma declaração de definição. Uma declaração de definição é aquela que usa o mesmo identificador de procedimento, mas omite a lista de parâmetros formais e inclui um bloco de instruções. A declaração de encaminhamento e a declaração de definição devem aparecer na mesma parte das declarações de procedimento e função. Entre eles, podem ser declarados outros procedimentos e funções que podem se referir ao procedimento de declaração de encaminhamento. Assim, a recursão mútua é possível.

A descrição direta e a descrição de definição são a descrição completa do procedimento. O procedimento é considerado descrito usando a descrição direta.

Se o programa contiver muitas sub-rotinas, o programa deixará de ser visual, será difícil navegar nele. Para evitar isso, algumas rotinas são armazenadas como arquivos fonte em disco e, se necessário, são conectadas ao programa principal na fase de compilação usando uma diretiva de compilação.

Uma diretiva é um comentário especial que pode ser colocado em qualquer lugar em um programa, onde um comentário normal pode estar. No entanto, eles diferem porque a diretiva tem uma notação especial: imediatamente após o colchete de fechamento sem espaço, o sinal S é escrito e, novamente, sem espaço, a diretiva é indicada.

Exemplo

1) {SE+} - emular coprocessador matemático;

2) {SF+} - forma um tipo distante de procedimento e chamada de função;

3) {SN+} - usa coprocessador matemático;

4) {SR+} - verifique se os intervalos estão fora dos limites.

Algumas opções de compilação podem conter um parâmetro, por exemplo:

{$1 file name} - inclui o arquivo nomeado no texto do programa compilado.

PALESTRA Nº 4. Sub-rotinas

1. Parâmetros do subprograma

A descrição de um procedimento ou função especifica uma lista de parâmetros formais. Cada parâmetro declarado em uma lista de parâmetros formal é local para o procedimento ou função descrita e pode ser referenciado no módulo associado a esse procedimento ou função por seu identificador.

Existem três tipos de parâmetros: valor, variável e variável sem tipo. Eles são caracterizados da seguinte forma.

1. Um grupo de parâmetros sem uma palavra-chave precedente é uma lista de parâmetros de valor.

2. Um grupo de parâmetros precedido pela palavra-chave const e seguido por um tipo é uma lista de parâmetros constantes.

3. Um grupo de parâmetros precedido pela palavra-chave var e seguido por um tipo é uma lista de parâmetros de variáveis ​​não tipadas.

4. Um grupo de parâmetros precedido pela palavra-chave var ou const e não seguido por um tipo é uma lista de parâmetros de variáveis ​​não tipadas.

2. Tipos de parâmetros de sub-rotina

Parâmetros de valor

Um parâmetro de valor formal é tratado como uma variável local para o procedimento ou função, exceto que deriva seu valor inicial do parâmetro real correspondente quando o procedimento ou função é invocado. As alterações sofridas por um parâmetro de valor formal não afetam o valor do parâmetro real. O valor do parâmetro de valor real correspondente deve ser uma expressão e seu valor não deve ser um tipo de arquivo ou qualquer tipo de estrutura que contenha um tipo de arquivo.

O parâmetro real deve ser de um tipo que seja compatível com a atribuição do tipo do parâmetro de valor formal. Se o parâmetro for do tipo string, o parâmetro formal terá um atributo size de 255.

Parâmetros constantes

Parâmetros constantes formais funcionam de forma semelhante a uma variável local somente leitura que obtém seu valor quando um procedimento ou função é invocado a partir do parâmetro real correspondente. Atribuições a um parâmetro constante formal não são permitidas. Um parâmetro constante formal também não pode ser passado como um parâmetro real para outro procedimento ou função. Um parâmetro constante correspondente a um parâmetro real em um procedimento ou instrução de função deve seguir as mesmas regras que o valor real do parâmetro.

Nos casos em que um parâmetro formal não altera seu valor quando um procedimento ou função é executado, um parâmetro constante deve ser usado em vez de um parâmetro de valor. Parâmetros constantes permitem a implementação de um procedimento ou função para proteção contra atribuições acidentais a um parâmetro formal. Além disso, para parâmetros de tipo struct e string, o compilador pode gerar um código mais eficiente quando usado em vez de parâmetros de valor para parâmetros constantes.

Parâmetros variáveis

Um parâmetro variável é usado quando um valor deve ser passado de um procedimento ou função para o programa de chamada. O parâmetro real correspondente em um procedimento ou instrução de chamada de função deve ser uma referência de variável. Quando um procedimento ou função é invocado, a variável de parâmetro formal é substituída pela variável real, quaisquer alterações no valor da variável de parâmetro formal são refletidas no parâmetro real.

Dentro de um procedimento ou função, qualquer referência a um parâmetro de variável formal resulta em acesso ao próprio parâmetro real. O tipo do parâmetro real deve corresponder ao tipo do parâmetro de variável formal, mas essa restrição pode ser contornada usando um parâmetro de variável sem tipo).

Parâmetros não digitados

Quando o parâmetro formal é um parâmetro variável sem tipo, o parâmetro real correspondente pode ser qualquer referência a uma variável ou constante, independentemente de seu tipo. Um parâmetro sem tipo declarado com a palavra-chave var pode ser modificado, enquanto um parâmetro sem tipo declarado com a palavra-chave const é somente leitura.

Em um procedimento ou função, um parâmetro de variável não tipado não tem tipo, ou seja, é incompatível com variáveis ​​de todos os tipos até que receba um tipo específico por atribuição de tipo de variável.

Embora os parâmetros não tipados forneçam mais flexibilidade, existem alguns riscos associados ao uso deles. O compilador não pode verificar a validade das operações em variáveis ​​não tipadas.

Variáveis ​​de procedimento

Após definir um tipo procedural, torna-se possível descrever variáveis ​​desse tipo. Tais variáveis ​​são chamadas de variáveis ​​procedurais. Como uma variável inteira que pode receber um valor de um tipo inteiro, uma variável procedural pode receber um valor de um tipo procedural. Tal valor poderia, é claro, ser outra variável de procedimento, mas também poderia ser um identificador de procedimento ou função. Nesse contexto, a declaração de um procedimento ou função pode ser vista como uma descrição de um tipo especial de constante cujo valor é o procedimento ou função.

Como em qualquer outra atribuição, os valores da variável do lado esquerdo e do lado direito devem ser compatíveis com a atribuição. Os tipos de procedimento, para serem compatíveis com atribuição, devem ter o mesmo número de parâmetros, e os parâmetros nas posições correspondentes devem ser do mesmo tipo. Os nomes de parâmetros em uma declaração de tipo procedural não têm efeito.

Além disso, para garantir a compatibilidade de atribuição, um procedimento ou função, se for atribuído a uma variável de procedimento, deve atender aos seguintes requisitos:

1) não deve ser um procedimento ou função padrão;

2) tal procedimento ou função não pode ser aninhado;

3) tal procedimento não deve ser um procedimento em linha;

4) não deve ser um procedimento de interrupção.

Procedimentos e funções padrão são os procedimentos e funções descritos no módulo Sistema, como Writeln, Readln, Chr, Ord. Procedimentos e funções aninhados com variáveis ​​procedurais não podem ser usados. Um procedimento ou função é considerado aninhado quando declarado dentro de outro procedimento ou função.

O uso de tipos procedurais não se limita apenas a variáveis ​​procedurais. Como qualquer outro tipo, um tipo procedural pode participar da declaração de um tipo estrutural.

Quando uma variável de procedimento recebe o valor de um procedimento, o que acontece na camada física é que o endereço do procedimento é armazenado na variável. Na verdade, uma variável de procedimento é muito semelhante a uma variável de ponteiro, só que em vez de se referir a dados, ela aponta para um procedimento ou função. Como um ponteiro, uma variável procedural ocupa 4 bytes (duas palavras) que contém um endereço de memória. A primeira palavra armazena o deslocamento, a segunda palavra armazena o segmento.

Parâmetros de tipo de procedimento

Como os tipos procedurais podem ser usados ​​em qualquer contexto, é possível descrever procedimentos ou funções que recebem procedimentos e funções como parâmetros. Os parâmetros de tipo de procedimento são especialmente úteis quando você precisa executar ações comuns em vários procedimentos ou funções.

Se um procedimento ou função deve ser passado como parâmetro, ele deve seguir as mesmas regras de compatibilidade de tipo que a atribuição. Ou seja, tais procedimentos ou funções devem ser compilados com a diretiva far, não podem ser funções internas, não podem ser aninhadas e não podem ser descritas com os atributos inline ou interrupt.

AULA #5. Tipo de dados String

1. Tipo de string em Pascal

Uma sequência de caracteres de um determinado comprimento é chamada de string. As variáveis ​​do tipo string são definidas especificando o nome da variável, a string de palavra reservada e, opcionalmente, mas não necessariamente, especificando o tamanho máximo, ou seja, o comprimento da string, entre colchetes. Se você não definir o tamanho máximo da string, por padrão será 255, ou seja, a string consistirá em 255 caracteres.

Cada elemento de uma string pode ser referido pelo seu número. No entanto, strings são entradas e saídas como um todo, não elemento por elemento, como é o caso dos arrays. O número de caracteres inseridos não deve exceder o especificado no tamanho máximo da string, portanto, se ocorrer esse excesso, os caracteres "extras" serão ignorados.

2. Procedimentos e funções para variáveis ​​do tipo string

1. Cópia da Função(S: String; Índice, Contagem: Inteiro): String;

Retorna uma substring de uma string. S é uma expressão do tipo String.

Index e Count são expressões do tipo inteiro. A função retorna uma string contendo caracteres Count começando na posição Index. Se Index for maior que o comprimento de S, a função retornará uma string vazia.

2. Procedimento Delete(var S: String; Index, Count: Integer);

Remove uma substring de caracteres de comprimento Count da string S, começando na posição Index. S é uma variável do tipo String. Index e Count são expressões do tipo inteiro. Se Index for maior que o comprimento de S, nenhum caractere será removido.

3. Inserção de Procedimento(Fonte: String; var S: String; Índice: Inteiro);

Concatena uma substring em uma string, começando em uma posição especificada. Source é uma expressão do tipo String. S é uma variável do tipo String de qualquer comprimento. Índice é uma expressão do tipo inteiro. Insert insere Source em S, começando na posição S[Index].

4. Comprimento da Função (S: String): Inteiro;

Retorna o número de caracteres realmente usados ​​na string S. Observe que, ao usar strings terminadas em nulo, o número de caracteres não é necessariamente igual ao número de bytes.

5. Função Pos(Substr: String; S: String): Integer;

Procura uma substring em uma string. Pos procura Substr dentro de S e retorna um valor inteiro que é o índice do primeiro caractere de Substr dentro de S. Se Substr não for encontrado, Pos retornará nulo.

3. Gravações

Um registro é uma coleção de um número limitado de componentes logicamente relacionados pertencentes a diferentes tipos. Os componentes de um registro são chamados de campos, cada um dos quais é identificado por um nome. Um campo de registro contém o nome do campo, seguido por dois pontos para indicar o tipo do campo. Os campos de registro podem ser de qualquer tipo permitido em Pascal, com exceção do tipo de arquivo.

A descrição de um registro na linguagem Pascal é realizada utilizando a palavra de serviço RECORD, seguida da descrição dos componentes do registro. A descrição da entrada termina com a palavra de serviço END.

Por exemplo, um bloco de anotações contém sobrenomes, iniciais e números de telefone, portanto, é conveniente representar uma linha separada em um bloco de anotações como a seguinte entrada:

tipo Linha = Registro

FIO: Sequência[20];

TEL: Sequência[7];

end;

var str: Linha;

As descrições de registro também são possíveis sem usar o nome do tipo, por exemplo:

var str : Gravar

FIO: String[20];

TEL : Sequência[7];

end;

A referência a um registro como um todo é permitida apenas em instruções de atribuição em que os nomes de registro do mesmo tipo são usados ​​à esquerda e à direita do sinal de atribuição. Em todos os outros casos, campos separados de registros são operados. Para fazer referência a um componente de registro individual, você deve especificar o nome do registro e, por meio de um ponto, especificar o nome do campo desejado. Tal nome é chamado de nome composto. Um componente de registro também pode ser um registro; nesse caso, o nome distinto conterá não dois, mas mais nomes.

A referência de componentes de registro pode ser simplificada usando o operador with append. Ele permite substituir os nomes compostos que caracterizam cada campo por apenas nomes de campo e definir o nome do registro no operador de acréscimo.

Às vezes, o conteúdo de um registro individual depende do valor de um de seus campos. Na linguagem Pascal, é permitida uma descrição de registro, consistindo em partes comuns e variantes. A parte variante é especificada usando o caso P de construção, onde P é o nome do campo da parte comum do registro. Os valores possíveis aceitos por este campo são listados da mesma forma que na declaração de variantes. No entanto, em vez de especificar a ação a ser executada, como é feito em uma instrução variante, os campos variantes são especificados entre parênteses. A descrição da parte variante termina com o fim da palavra de serviço. O tipo de campo P pode ser especificado no cabeçalho da parte variante. Os registros são inicializados usando constantes tipadas.

4. Conjuntos

O conceito de conjunto na linguagem Pascal é baseado no conceito matemático de conjuntos: é uma coleção limitada de diferentes elementos. Um tipo de dados enumerado ou de intervalo é usado para construir um tipo de conjunto concreto. O tipo de elementos que compõem um conjunto é chamado de tipo base.

Um tipo múltiplo é descrito usando o conjunto de palavras de função, por exemplo:

tipo M = Conjunto de B;

Aqui M é o tipo plural, B é o tipo base.

O pertencimento de variáveis ​​a um tipo plural pode ser determinado diretamente na seção de declaração de variáveis.

As constantes de tipo de conjunto são gravadas como uma sequência entre colchetes de elementos ou intervalos de tipo base, separados por vírgulas. Uma constante da forma [] significa um subconjunto vazio.

Um conjunto inclui um conjunto de elementos do tipo base, todos os subconjuntos de um determinado conjunto e o subconjunto vazio. Se o tipo base no qual o conjunto é construído possui K elementos, então o número de subconjuntos incluídos neste conjunto é igual a 2 elevado a K. A ordem de listar os elementos do tipo base em constantes é indiferente. O valor de uma variável de tipo múltiplo pode ser dado por uma construção da forma [T], onde T é uma variável do tipo base.

As operações de atribuição (:=), união (+), interseção (*) e subtração (-) são aplicáveis ​​a variáveis ​​e constantes de um tipo de conjunto. O resultado dessas operações é um valor do tipo plural:

1) ['A','B'] + ['A','D'] dará ['A','B','D'];

2) ['A'] * ['A','B','C'] dará ['A'];

3) ['A','B','C'] - ['A','B'] dará ['C'].

As operações são aplicáveis ​​a vários valores: identidade (=), não identidade (<>), contido em (<=), contém (>=). O resultado dessas operações tem um tipo booleano:

1) ['A','B'] = ['A','C'] dará FALSE ;

2) ['A','B'] <> ['A','C'] dará TRUE;

3) ['B'] <= ['B','C'] dará TRUE;

4) ['C','D'] >= ['A'] dará FALSE.

Além dessas operações, para trabalhar com valores de um tipo de conjunto, utiliza-se a operação in, que verifica se o elemento do tipo base à esquerda do sinal de operação pertence ao conjunto à direita do sinal de operação . O resultado desta operação é um booleano. A operação de verificar se um elemento pertence a um conjunto é frequentemente usada em vez de operações relacionais.

Quando vários tipos de dados são usados ​​em programas, as operações são executadas em sequências de bits de dados. Cada valor do tipo múltiplo na memória do computador corresponde a um dígito binário.

Valores de um tipo múltiplo não podem ser elementos de uma lista de E/S. Em cada implementação concreta do compilador da linguagem Pascal, o número de elementos do tipo base sobre o qual o conjunto é construído é limitado.

A inicialização de vários valores de tipo é feita usando constantes tipadas.

Aqui estão alguns procedimentos para trabalhar com conjuntos.

1. Procedimento Excluir(var S: Conjunto de T; I:T);

Remove o elemento I do conjunto S. S é uma variável do tipo "set" e I é uma expressão de um tipo compatível com o tipo original S. Exclude(S, I) é o mesmo que S : = S - [I], mas gera um código mais eficiente.

2. Procedure Include(var S: Conjunto de T; I:T);

Adiciona um elemento I ao conjunto S. S é uma variável do tipo "set", e I é uma expressão de um tipo compatível com o tipo S. A construção Include(S, I) é a mesma que S : = S + [I], mas gera um código mais eficiente.

PALESTRA Nº 6. Arquivos

1. Arquivos. Operações de arquivo

A introdução do tipo de arquivo na linguagem Pascal é causada pela necessidade de fornecer a capacidade de trabalhar com dispositivos de computador periféricos (externos) projetados para entrada, saída e armazenamento de dados.

O tipo de dados do arquivo (ou arquivo) define uma coleção ordenada de um número arbitrário de componentes do mesmo tipo. A propriedade comum de uma matriz, conjunto e registro é que o número de seus componentes é determinado no estágio de escrita do programa, enquanto o número de componentes de arquivo no texto do programa não é determinado e pode ser arbitrário.

Ao trabalhar com arquivos, são realizadas operações de E/S. Uma operação de entrada significa transferir dados de um dispositivo externo (de um arquivo de entrada) para a memória principal de um computador, uma operação de saída é uma transferência de dados da memória principal para um dispositivo externo (para um arquivo de saída). Os arquivos em dispositivos externos geralmente são chamados de arquivos físicos. Seus nomes são determinados pelo sistema operacional.

Em programas Pascal, os nomes dos arquivos são especificados usando strings. Para trabalhar com arquivos no programa, você deve definir uma variável de arquivo. Pascal suporta três tipos de arquivos: arquivos de texto, arquivos componentes, arquivos não digitados.

As variáveis ​​de arquivo que são declaradas em um programa são chamadas de arquivos lógicos. Todos os procedimentos e funções básicos que fornecem E/S de dados funcionam apenas com arquivos lógicos. O arquivo físico deve ser associado ao arquivo lógico antes que os procedimentos de abertura do arquivo possam ser executados.

Arquivos de texto

Um lugar especial na linguagem Pascal é ocupado por arquivos de texto, cujos componentes são do tipo caractere. Para descrever arquivos de texto, a linguagem define o tipo padrão Texto:

var TF1, TF2: Texto;

Os arquivos de texto são uma sequência de linhas e as linhas são uma sequência de caracteres. As linhas são de comprimento variável, cada linha termina com um terminador de linha.

Arquivos de componentes

Um componente ou arquivo tipado é um arquivo com o tipo declarado de seus componentes. Os arquivos de componentes consistem em representações de máquina de valores de variáveis; eles armazenam dados da mesma forma que a memória do computador.

A descrição dos valores do tipo de arquivo é:

tipo M = Arquivo de T;

onde M é o nome do tipo de arquivo;

T - tipo de componente.

Os componentes de arquivo podem ser todos os tipos escalares e de tipos estruturados - matrizes, conjuntos, registros. Em quase todas as implementações específicas da linguagem Pascal, a construção "arquivo de arquivos" não é permitida.

Todas as operações em arquivos de componentes são executadas usando procedimentos padrão.

Escreva(f,X1,X2,...XK)

Arquivos não digitados

Arquivos não digitados permitem que você grave seções arbitrárias da memória do computador no disco e as leia do disco para a memória. Os arquivos não tipados são descritos da seguinte forma:

var f: Arquivo;

Agora listamos os procedimentos e funções para trabalhar com diferentes tipos de arquivos.

1. Procedimento Assign(var F; FileName: String);

O procedimento AssignFile mapeia um nome de arquivo externo para uma variável de arquivo.

F é uma variável de arquivo de qualquer tipo de arquivo, FileName é uma expressão String ou uma expressão PChar se a sintaxe estendida for permitida. Todas as outras operações com F são realizadas com um arquivo externo.

Você não pode usar um procedimento com uma variável de arquivo já aberta.

2. Procedimento Fechar(varF);

O procedimento quebra o vínculo entre a variável de arquivo e o arquivo de disco externo e fecha o arquivo.

F é uma variável de arquivo de qualquer tipo de arquivo, aberta pelos procedimentos Reset, Rewrite ou Append. O arquivo externo associado a F é totalmente modificado e então fechado, liberando o descritor de arquivo para reutilização.

A diretiva {SI+} permite tratar erros durante a execução do programa usando tratamento de exceção. Com a diretiva {$1-} desativada, você deve usar IOResult para verificar erros de E/S.

3.Função Eof(var F): Booleana;

{Arquivos digitados ou não digitados}

Função Eof[(var F: Text)]: Booleana;

{arquivos de texto}

Verifica se a posição atual do arquivo é o final do arquivo.

Eof(F) retorna True se a posição atual do arquivo for após o último caractere do arquivo ou se o arquivo estiver vazio; caso contrário, Eof(F) retornará False.

A diretiva {SI+} permite lidar com erros durante a execução do programa usando tratamento de exceção. Com a diretiva {SI-} desativada, você deve usar IOResult para verificar erros de E/S.

4. Apagar Procedimento (var F);

Exclui o arquivo externo associado a F.

F é uma variável de arquivo de qualquer tipo de arquivo.

Antes de chamar o procedimento Erase, o arquivo deve ser fechado.

A diretiva {SI+} permite lidar com erros durante a execução do programa usando tratamento de exceção. Com a diretiva {SI-} desativada, você deve usar IOResult para verificar erros de E/S.

5. Função FileSize(var F): Integer;

Retorna o tamanho em bytes do arquivo F No entanto, se F for um arquivo digitado, FileSize retornará o número de registros no arquivo. O arquivo deve ser aberto antes de usar a função FileSize. Se o arquivo estiver vazio, FileSize(F) retornará zero. F é uma variável de qualquer tipo de arquivo.

6.Função FilePos(var F): LongInt;

Retorna a posição atual de um arquivo dentro de um arquivo.

Antes de usar a função FilePos, o arquivo deve estar aberto. A função FilePos não é usada com arquivos de texto. F é uma variável de qualquer tipo de arquivo, exceto o tipo Texto.

7. Procedimento Reset(var F [: File; RecSize: Word]);

Abre um arquivo existente.

F é uma variável de qualquer tipo de arquivo associado a um arquivo externo usando AssignFile. RecSize é uma expressão opcional que é usada se F for um arquivo sem tipo. Se F for um arquivo sem tipo, RecSize determinará o tamanho do registro usado ao transferir dados. Se RecSize for omitido, o tamanho do registro padrão será de 128 bytes.

O procedimento de Redefinição abre um arquivo externo existente associado à variável de arquivo F. Se não houver nenhum arquivo externo com esse nome, ocorrerá um erro em tempo de execução. Se o arquivo associado a F já estiver aberto, primeiro ele será fechado e depois reaberto. A posição atual do arquivo é definida para o início do arquivo.

8. Procedimento Reescrever(var F: Arquivo [; Recsize: Word]);

Cria e abre um novo arquivo.

F é uma variável de qualquer tipo de arquivo associado a um arquivo externo usando AssignFile. RecSize é uma expressão opcional que é usada se F for um arquivo sem tipo. Se F for um arquivo sem tipo, RecSize determinará o tamanho do registro usado ao transferir dados. Se RecSize for omitido, o tamanho do registro padrão será de 128 bytes.

O procedimento Rewrite cria um novo arquivo externo com o nome associado a F. Se já existir um arquivo externo com o mesmo nome, ele é excluído e um novo arquivo vazio é criado.

9. Procura de Procedimento(var F; N: LongInt);

Move a posição atual do arquivo para o componente especificado. Você só pode usar o procedimento com arquivos abertos digitados ou não digitados.

A posição atual do arquivo F é movida para o número N. O número do primeiro componente do arquivo é 0.

A instrução Seek(F, FileSize(F)) move a posição do arquivo atual para o final do arquivo.

10. Anexo de Procedimento(var F: Texto);

Abre um arquivo de texto existente para anexar informações ao final do arquivo (anexar).

Se um arquivo externo com o nome fornecido não existir, ocorrerá um erro em tempo de execução. Se o arquivo F já estiver aberto, ele fecha e reabre. A posição atual do arquivo é definida para o final do arquivo.

11.Função Eoln[(var F: Text)]: Booleano;

Verifica se a posição atual do arquivo é o final de uma linha em um arquivo de texto.

Eoln(F) retorna True se a posição atual do arquivo estiver no final de uma linha ou arquivo; caso contrário, Eoln(F) retornará Falso.

12. Procedimento Leitura(F, V1 [, V2,..., Vn]);

{Arquivos digitados e não digitados}

Procedimento Ler([var F: Texto;] V1 [, V2,..., Vn]);

{arquivos de texto}

Para arquivos digitados, o procedimento lê o componente do arquivo em uma variável. Em cada leitura, a posição atual no arquivo avança para o próximo elemento.

Para arquivos de texto, um ou mais valores são lidos em uma ou mais variáveis.

Com variáveis ​​String, Read lê todos os caracteres até (mas não incluindo) o próximo marcador de fim de linha, ou até que Eof(F) seja avaliado como True. A cadeia de caracteres resultante é atribuída à variável.

No caso de uma variável do tipo inteiro ou real, o procedimento aguarda uma sequência de caracteres que formam um número de acordo com as regras da sintaxe Pascal. A leitura pára quando o primeiro espaço, tabulação ou fim de linha é encontrado ou quando Eof(F) é avaliado como Verdadeiro. Se a string numérica não corresponder ao formato esperado, ocorrerá um erro de E/S.

13. Procedimento Readln([var F: Texto;] V1 [, V2..., Vn]);

É uma extensão do procedimento Read e é definido para arquivos de texto. Lê uma sequência de caracteres no arquivo, incluindo o marcador de fim de linha, e move para o início da próxima linha. Chamar a função Readln(F) sem parâmetros move a posição atual do arquivo para o início da próxima linha, se houver, caso contrário, salta para o final do arquivo.

14. Função SeekEof[(var F: Text)]: Booleano;

Retorna o final do arquivo e só pode ser usado para arquivos de texto abertos. Normalmente usado para ler valores numéricos de arquivos de texto.

15. Função SeekEoln[(var F: Text)]: Booleano;

Retorna o terminador de linha em um arquivo e só pode ser usado para arquivos de texto abertos. Normalmente usado para ler valores numéricos de arquivos de texto.

16. Procedimento Escrever([var F: Texto;] P1 [, P2,..., Pn]);

{arquivos de texto}

Grava um ou mais valores em um arquivo de texto.

Cada parâmetro de entrada deve ser do tipo Char, um dos tipos inteiros (Byte, ShortInt, Word, Longint, Cardinal), um dos tipos de ponto flutuante (Single, Real, Double, Extended, Currency), um dos tipos string ( PChar, AisiString , ShortString) ou um dos tipos booleanos (Boolean, Bool).

Procedimento Escrever(F, V1,..., Vn);

{Arquivos digitados}

Grava uma variável em um componente de arquivo. As variáveis ​​VI...., Vn devem ser do mesmo tipo que os elementos do arquivo. Cada vez que uma variável é escrita, a posição atual no arquivo é movida para o próximo elemento.

17. Procedimento Writeln([var F: Texto;] [P1, P2,..., Pn]);

{arquivos de texto}

Executa uma operação de gravação e, em seguida, coloca um marcador de fim de linha no arquivo.

Chamar Writeln(F) sem parâmetros grava um marcador de fim de linha no arquivo. O arquivo deve estar aberto para saída.

2. Módulos. Tipos de módulos

Um módulo (1Ж1Т) em Pascal é uma biblioteca de sub-rotinas especialmente projetada. Um módulo, ao contrário de um programa, não pode ser lançado para execução por conta própria, ele só pode participar da construção de programas e outros módulos. Os módulos permitem que você crie bibliotecas pessoais de procedimentos e funções e construa programas de praticamente qualquer tamanho.

Um módulo em Pascal é uma unidade de programa armazenada separadamente e compilada independentemente. Em geral, um módulo é uma coleção de recursos de software destinados ao uso por outros programas. Os recursos do programa são entendidos como quaisquer elementos da linguagem Pascal: constantes, tipos, variáveis, sub-rotinas. O módulo em si não é um programa executável, seus elementos são usados ​​por outras unidades de programa.

Todos os elementos do programa do módulo podem ser divididos em duas partes:

1) elementos de programa destinados ao uso por outros programas ou módulos, tais elementos são chamados de visíveis fora do módulo;

2) elementos de software que são necessários apenas para o funcionamento do próprio módulo, são chamados de invisíveis (ou ocultos).

De acordo com isso, o módulo, além do cabeçalho, contém três partes principais, denominadas interface, executável e inicializada.

Em geral, um módulo tem a seguinte estrutura:

unidade <nome do módulo>; {título do módulo}

interface

{descrição dos elementos de programa visíveis do módulo}

implementação

{descrição dos elementos de programação ocultos do módulo}

começar

{instruções de inicialização do elemento do módulo}

final.

Em um caso particular, o módulo não pode conter uma parte de implementação e uma parte de inicialização, então a estrutura do módulo será a seguinte:

unidade <nome do módulo>; {título do módulo}

interface

{descrição dos elementos de programa visíveis do módulo}

implementação

final.

A utilização de procedimentos e funções em módulos tem suas peculiaridades. O cabeçalho da sub-rotina contém todas as informações necessárias para chamá-la: nome, lista e tipo de parâmetros, tipo de resultado para funções. Essas informações devem estar disponíveis para outros programas e módulos. Por outro lado, o texto de uma sub-rotina que implementa seu algoritmo não pode ser utilizado por outros programas e módulos. Portanto, os cabeçalhos de procedimentos e funções são colocados na parte de interface do módulo, e o texto é colocado na parte de implementação.

A parte de interface do módulo contém apenas cabeçalhos visíveis (acessíveis a outros programas e módulos) de procedimentos e funções (sem a palavra de serviço forward). O texto completo do procedimento ou função é colocado na parte de implementação, e o cabeçalho não pode conter uma lista de parâmetros formais.

O código fonte do módulo deve ser compilado usando a diretiva Make do submenu Compile e gravado em disco. O resultado da compilação do módulo é um arquivo com extensão . TPU (Unidade Turbo Pascal). O nome base do módulo é obtido do cabeçalho do módulo.

Para conectar um módulo ao programa, você deve especificar seu nome na seção de descrição do módulo, por exemplo:

usa Crt, Gráfico;

Caso os nomes das variáveis ​​na parte de interface do módulo e no programa que utiliza este módulo sejam os mesmos, a referência será à variável descrita no programa. Para se referir a uma variável declarada em um módulo, você deve usar um nome composto que consiste no nome do módulo e no nome da variável, separados por um ponto. O uso de nomes compostos se aplica não apenas a nomes de variáveis, mas a todos os nomes declarados na parte de interface do módulo.

O uso recursivo de módulos é proibido.

Se um módulo tiver uma seção de inicialização, as instruções nessa seção serão executadas antes que o programa que usa esse módulo comece a ser executado.

Vamos listar os tipos de módulos.

1. Módulo do SISTEMA.

O módulo SYSTEM implementa rotinas de suporte de nível inferior para todos os recursos internos, como E/S, manipulação de strings, operações de ponto flutuante e alocação dinâmica de memória.

O módulo SYSTEM contém todas as rotinas e funções padrão e integradas do Pascal. Qualquer sub-rotina Pascal que não faça parte do Pascal padrão e não seja encontrada em nenhum outro módulo está contida no módulo System. Este módulo é usado automaticamente em todos os programas e não precisa ser especificado na instrução uses.

2. Módulo DOS.

O módulo Dos implementa várias rotinas e funções Pascal que são equivalentes às chamadas DOS mais usadas, como GetTime, SetTime, DiskSize e assim por diante.

3. Módulo CRT.

O módulo CRT implementa vários programas poderosos que fornecem controle total sobre os recursos do PC, como controle de modo de tela, códigos de teclado estendidos, cores, janelas e sons. O módulo CRT só pode ser utilizado em programas que rodam em computadores pessoais IBM PC, PC AT, PS/2 da IBM e são totalmente compatíveis com os mesmos.

Uma das principais vantagens do uso do módulo CRT é a maior velocidade e flexibilidade nas operações da tela. Programas que não funcionam com o módulo CRT exibem informações na tela usando o sistema operacional DOS, que está associado a uma sobrecarga adicional. Ao usar o módulo CRT, as informações de saída são enviadas diretamente para o sistema básico de entrada/saída (BIOS) ou, para operações ainda mais rápidas, diretamente para a memória de vídeo.

4. Módulo GRÁFICO.

Usando os procedimentos e funções incluídos neste módulo, você pode criar vários gráficos na tela.

5. Módulo OVERLAY.

O módulo OVERLAY permite reduzir os requisitos de memória de um programa DOS em modo real. De fato, é possível escrever programas que excedam a quantidade total de memória disponível, pois apenas parte do programa estará na memória em um determinado momento.

PALESTRA № 7. Memória dinâmica

1. Tipo de dados de referência. memória dinâmica. Variáveis ​​dinâmicas

Uma variável estática (alocada estaticamente) é uma variável declarada explicitamente no programa, é referida pelo nome. O lugar na memória para colocar variáveis ​​estáticas é determinado quando o programa é compilado. Ao contrário dessas variáveis ​​estáticas, os programas Pascal podem criar variáveis ​​dinâmicas. A principal propriedade das variáveis ​​dinâmicas é que elas são criadas e a memória é alocada para elas durante a execução do programa.

As variáveis ​​dinâmicas são colocadas em uma área de memória dinâmica (área de heap). Uma variável dinâmica não é especificada explicitamente nas declarações de variáveis ​​e não pode ser referenciada pelo nome. Tais variáveis ​​são acessadas usando ponteiros e referências.

Um tipo de referência (ponteiro) define um conjunto de valores que apontam para variáveis ​​dinâmicas de um determinado tipo, chamado de tipo base. Uma variável de tipo de referência contém o endereço de uma variável dinâmica na memória. Se o tipo base for um identificador não declarado, ele deverá ser declarado na mesma parte da declaração de tipo que o tipo de ponteiro.

A palavra reservada nil denota uma constante com um valor de ponteiro que não aponta para nada.

Vamos dar um exemplo da descrição de variáveis ​​dinâmicas.

var p1, p2 : ^real;

p3, p4 : ^inteiro;

2. Trabalhando com memória dinâmica. Ponteiros não digitados

Procedimentos e Funções de Memória Dinâmica

1. Procedimento Novo(var p: Ponteiro).

Aloca espaço na área de memória dinâmica para acomodar a variável dinâmica pЛ, e atribui seu endereço ao ponteiro p.

2. Procedimento Descarte(varp: Ponteiro).

Libera a memória alocada para alocação de variável dinâmica pelo procedimento New, e o valor do ponteiro p fica indefinido.

3. Procedimento GetMem(varp: Ponteiro; tamanho: Word).

Aloca uma seção de memória na área de heap, atribui o endereço de seu início ao ponteiro p, o tamanho da seção em bytes é especificado pelo parâmetro size.

4. Procedimento FreeMem(var p: Ponteiro; tamanho: Word).

Libera a área de memória, cujo endereço inicial é especificado pelo ponteiro p e o tamanho é especificado pelo parâmetro size. O valor do ponteiro p torna-se indefinido.

5. Marca de Procedimento (var p: Ponteiro)

Grava no ponteiro p o endereço do início de uma seção de memória dinâmica livre no momento de sua chamada.

6. Liberação do Procedimento (var p: Ponteiro)

Libera uma seção da memória dinâmica, a partir do endereço escrito no ponteiro p pelo procedimento Mark, ou seja, limpa a memória dinâmica que estava ocupada após a chamada ao procedimento Mark.

7. Função MaxAvaikLongint

Retorna o comprimento, em bytes, do heap livre mais longo.

8. Função MemAvaikLongint

Retorna a quantidade total de memória dinâmica livre em bytes.

9. Função auxiliar SizeOf(X):Word

Retorna a quantidade de bytes ocupados por X, onde X pode ser um nome de variável de qualquer tipo ou um nome de tipo.

O tipo interno Pointer denota um ponteiro sem tipo, ou seja, um ponteiro que não aponta para nenhum tipo específico. Variáveis ​​do tipo Pointer podem ser desreferenciadas: especificar o caractere ^ após tal variável causa um erro.

Assim como o valor denotado por nil, os valores de ponteiro são compatíveis com todos os outros tipos de ponteiro.

PALESTRA № 8. Estruturas de dados abstratas

1. Estruturas de dados abstratas

Tipos de dados estruturados, como arrays, conjuntos e registros, são estruturas estáticas porque seus tamanhos não mudam durante toda a execução do programa.

Muitas vezes, é necessário que as estruturas de dados mudem seus tamanhos no decorrer da resolução de um problema. Essas estruturas de dados são chamadas de dinâmicas. Estes incluem pilhas, filas, listas, árvores, etc.

A descrição de estruturas dinâmicas com a ajuda de arrays, registros e arquivos leva ao desperdício de memória do computador e aumenta o tempo de resolução de problemas.

Cada componente de qualquer estrutura dinâmica é um registro contendo pelo menos dois campos: um campo do tipo "ponteiro" e o segundo - para posicionamento de dados. Em geral, um registro pode conter não um, mas vários ponteiros e vários campos de dados. Um campo de dados pode ser uma variável, uma matriz, um conjunto ou um registro.

Se a parte indicadora contiver o endereço de um elemento da lista, a lista será chamada de unidirecional (ou vinculada individualmente). Se contiver dois componentes, estará duplamente conectado. Você pode realizar várias operações em listas, por exemplo:

1) adicionar um elemento à lista;

2) remover um elemento da lista com uma determinada chave;

3) procurar um elemento com um determinado valor do campo chave;

4) ordenação dos elementos da lista;

5) divisão da lista em duas ou mais listas;

6) combinar duas ou mais listas em uma;

7) outras operações.

No entanto, como regra, não surge a necessidade de todas as operações na resolução de vários problemas. Portanto, dependendo das operações básicas que precisam ser aplicadas, existem diferentes tipos de listas. Os mais populares são pilha e fila.

2. Pilhas

Uma pilha é uma estrutura de dados dinâmica, a adição de um componente ao qual e a remoção de um componente do qual são feitas de uma extremidade, chamada de topo da pilha. A pilha funciona com o princípio LIFO (Last-In, First-Out) - "Last in, first out".

Geralmente, há três operações executadas em pilhas:

1) formação inicial da pilha (registro do primeiro componente);

2) adicionar um componente à pilha;

3) seleção do componente (exclusão).

Para formar uma pilha e trabalhar com ela, você deve ter duas variáveis ​​do tipo "ponteiro", sendo que a primeira determina o topo da pilha e a segunda é auxiliar.

Exemplo. Escreva um programa que forme uma pilha, adicione um número arbitrário de componentes a ela e, em seguida, leia todos os componentes e os exiba na tela de exibição. Tome uma cadeia de caracteres como dados. Entrada de dados - do teclado, um sinal do fim da entrada - uma seqüência de caracteres END.

Programa PILHA;

usa Crt;

tipo

Alfa = Cadeia[10];

PComp = ^Comp;

Comp = registro

SD: Alfa;

pPróximo: PComp

end;

var

pTop: PComp;

sc: Alfa;

Create ProcedureStack(var pTop : PComp; var sC : Alfa);

começar

Novo(pTop);

pTopo^.pPróximo := NIL;

pTop^.sD := sC;

end;

Adicione ProcedureComp(var pTop : PComp; var sC : Alfa);

var pAux : PComp;

começar

NOVO(pAux);

pAux^.pNext := pTop;

pTop := pAux;

pTop^.sD := sC;

end;

Procedimento DelComp(var pTop : PComp; var sC : ALFA);

começar

sC := pTop^.sD;

pTopo := pTopo^.pPróximo;

end;

começar

Clrscr;

writeln('DIGITE UMA STRING');

readln(sc);

CreateStack(pTop, sc);

repetir

writeln('DIGITE UMA STRING');

readln(sc);

AddComp(pTop, sc);

até sC = 'FIM';

writeln('******* SAÍDA *****');

repetir

DelComp(pTop, sc);

escrever(sC);

até pTop = NIL;

final.

3. Filas

Uma fila é uma estrutura de dados dinâmica onde um componente é adicionado em uma extremidade e recuperado na outra extremidade. A fila funciona segundo o princípio FIFO (First-In, First-Out) - "Primeiro a entrar, primeiro a ser servido".

Para formar uma fila e trabalhar com ela, é necessário ter três variáveis ​​do tipo ponteiro, a primeira das quais determina o início da fila, a segunda - o fim da fila, a terceira - auxiliar.

Exemplo. Escreva um programa que forme uma fila, adicione um número arbitrário de componentes a ela e, em seguida, leia todos os componentes e os exiba na tela de exibição. Tome uma cadeia de caracteres como dados. Entrada de dados - a partir do teclado, o sinal do fim da entrada - uma seqüência de caracteres END.

Programa FILA;

usa Crt;

tipo

Alfa = Cadeia[10];

PComp = ^Comp;

Comp = registro

SD: Alfa;

pNext: PComp;

end;

var

pBegin, pEnd: PComp;

sc: Alfa;

Create ProcedureQueue(var pBegin,pEnd:PComp; var sC:Alfa);

começar

Novo(pBegin);

pBegin^.pNext := NIL;

pBegin^.sD := sC;

pFim := pInício;

end;

Procedimento AddQueue(var pEnd : PComp; var sC : Alfa);

var pAux : PComp;

começar

Novo(pAux);

pAux^.pPróximo := NIL;

pEnd^.pNext := pAux;

pEnd := pAux;

pEnd^.sD := sC;

end;

Procedimento DelQueue(var pBegin : PComp; var sC : Alfa);

começar

sC := pBegin^.sD;

pBegin := pBegin^.pNext;

end;

começar

Clrscr;

writeln('DIGITE UMA STRING');

readln(sc);

CreateQueue(pBegin, pEnd, sc);

repetir

writeln('DIGITE UMA STRING');

readln(sc);

AddQueue(pEnd, sc);

até sC = 'FIM';

writeln(' ***** EXIBIR RESULTADOS *****');

repetir

DelQueue(pBegin, sc);

escrever(sC);

até pInício = NIL;

final.

PALESTRA No. 9. Estruturas de dados em forma de árvore

1. Estruturas de Dados em Árvore

Uma estrutura de dados em forma de árvore é um conjunto finito de elementos-nós entre os quais existem relações - a conexão entre a fonte e o gerado.

Se usarmos a definição recursiva proposta por N. Wirth, então uma estrutura de dados de árvore com tipo base t é uma estrutura vazia ou um nó do tipo t, com o qual um conjunto finito de estruturas de árvore com tipo base t, chamado subárvores, é associado.

A seguir, damos as definições usadas ao operar com estruturas em árvore.

Se o nó y estiver localizado diretamente abaixo do nó x, então o nó y é chamado de descendente imediato do nó x, e x é o ancestral imediato do nó y, ou seja, se o nó x está no i-ésimo nível, então o nó y é correspondentemente localizado em (i + 1) - o nível.

O nível máximo de um nó de árvore é chamado de altura ou profundidade da árvore. Um ancestral não possui apenas um nó da árvore - sua raiz.

Os nós de árvore que não têm filhos são chamados de nós de folha (ou folhas de árvore). Todos os outros nós são chamados de nós internos. O número de filhos imediatos de um nó determina o grau desse nó, e o grau máximo possível de um nó em uma determinada árvore determina o grau da árvore.

Ancestrais e descendentes não podem ser intercambiados, ou seja, a conexão entre o original e o gerado atua apenas em uma direção.

Se você for da raiz da árvore para algum nó específico, o número de ramos da árvore que serão percorridos nesse caso é chamado de comprimento do caminho para esse nó. Se todos os ramos (nós) de uma árvore são ordenados, então a árvore é dita ordenada.

Árvores binárias são um caso especial de estruturas de árvore. São árvores em que cada filho tem no máximo dois filhos, chamados de subárvores esquerda e direita. Assim, uma árvore binária é uma estrutura de árvore cujo grau é dois.

A ordenação de uma árvore binária é determinada pela seguinte regra: cada nó tem seu próprio campo de chave, e para cada nó o valor da chave é maior que todas as chaves em sua subárvore esquerda e menor que todas as chaves em sua subárvore direita.

Uma árvore cujo grau é maior que dois é chamada fortemente ramificada.

2. Operações em árvores

Além disso, consideraremos todas as operações em relação às árvores binárias.

I. Construção de árvores

Apresentamos um algoritmo para construir uma árvore ordenada.

1. Se a árvore estiver vazia, os dados serão transferidos para a raiz da árvore. Se a árvore não estiver vazia, um de seus ramos desce de tal forma que a ordem da árvore não seja violada. Como resultado, o novo nó se torna a próxima folha da árvore.

2. Para adicionar um nó a uma árvore já existente, você pode usar o algoritmo acima.

3. Ao excluir um nó da árvore, você deve ter cuidado. Se o nó a ser removido for uma folha ou tiver apenas um filho, a operação é simples. Se o nó a ser excluído tiver dois descendentes, será necessário encontrar um nó entre seus descendentes que possa ser colocado em seu lugar. Isso é necessário devido à exigência de que a árvore seja encomendada.

Você pode fazer isso: troque o nó a ser removido pelo nó com o maior valor de chave na subárvore esquerda ou com o nó com o menor valor de chave na subárvore direita e exclua o nó desejado como uma folha.

II. Localizando um nó com um determinado valor de campo-chave

Ao realizar esta operação, é necessário percorrer a árvore. É necessário levar em conta as diferentes formas de escrever uma árvore: prefixo, infixo e pós-fixo.

Surge a pergunta: como representar os nós da árvore para que seja mais conveniente trabalhar com eles? É possível representar uma árvore usando um array, onde cada nó é descrito por um valor do tipo combinado, que possui um campo de informação do tipo caractere e dois campos do tipo referência. Mas isso não é muito conveniente, pois as árvores possuem um grande número de nós que não são pré-determinados. Portanto, é melhor usar variáveis ​​dinâmicas ao descrever uma árvore. Em seguida, cada nó é representado por um valor do mesmo tipo, que contém uma descrição de um determinado número de campos de informação, e o número de campos correspondentes deve ser igual ao grau da árvore. É lógico determinar a ausência de descendentes com zero. Então, em Pascal, a descrição de uma árvore binária pode ser assim:

TIPO TreeLink = ^Árvore;

árvore = registro;

Inf: <tipo de dados>;

Esquerda, Direita: TreeLink;

End.

3. Exemplos de implementação de operações

1. Construa uma árvore de n nós de altura mínima, ou uma árvore perfeitamente balanceada (o número de nós das subárvores esquerda e direita de tal árvore não deve diferir em mais de um).

Algoritmo de construção recursiva:

1) o primeiro nó é tomado como a raiz da árvore.

2) a subárvore esquerda de nl nós é construída da mesma maneira.

3) a subárvore direita de nr nós é construída da mesma maneira;

nr = n - nl - 1. Como campo de informação, tomaremos os números dos nós digitados no teclado. A função recursiva que implementa essa construção ficará assim:

Árvore de Funções(n : Byte): TreeLink;

Vart : TreeLink; nl,nr,x : Byte;

Começar

Se n = 0 então Árvore := nil

Outro

Começar

nl := n div 2;

nr = n - nl - 1;

writeln('Digite o número do vértice ');

leia(x);

novo(t);

t^.inf := x;

t^.left := Árvore(nl);

t^.right := Árvore(nr);

Árvore := t;

End;

{Árvore}

End.

2. Na árvore ordenada binária, encontre o nó com o valor dado do campo-chave. Se não houver tal elemento na árvore, adicione-o à árvore.

Procedimento de pesquisa(x : Byte; var t : TreeLink);

Começar

Se t = zero então

Começar

Novo(t);

t^inf := x;

t^.esquerda := nil;

t^.certo := nil;

Terminar

Senão se x < t^.inf então

Pesquisar(x, t^.esquerda)

Caso contrário, se x > t^.inf então

Pesquisar(x, t^.direita)

Outro

Começar

{processo encontrado elemento}

...

End;

End.

3. Escreva os procedimentos de travessia da árvore em ordem direta, simétrica e reversa, respectivamente.

3.1. Procedimento Pré-encomenda(t : TreeLink);

Começar

Se t <> nulo então

Começar

WriteIn(t^.inf);

Pré-encomenda(t^.left);

Pré-encomenda(t^.right);

End;

End;

3.2. Procedimento Inorder(t : TreeLink);

Começar

Se t <> nulo então

Começar

Inorder(t^.esquerda);

WriteIn(t^.inf);

Inordem(t^.direita);

End;

End.

3.3. Pós-ordem do procedimento(t : TreeLink);

Começar

Se t <> nulo então

Começar

postorder(t^.left);

postorder(t^.right);

WriteIn(t^.inf);

End;

End.

4. Na árvore ordenada binária, exclua o nó com o valor fornecido do campo-chave.

Vamos descrever um procedimento recursivo que levará em conta a presença do elemento requerido na árvore e o número de descendentes deste nó. Se o nó a ser excluído tiver dois filhos, ele será substituído pelo maior valor de chave em sua subárvore esquerda e só então será excluído permanentemente.

Procedimento Delete1(x : Byte; var t : TreeLink);

Var p : TreeLink;

Procedimento Delete2(var q : TreeLink);

Começar

Se q^.right <> nil então Delete2(q^.right)

Outro

Começar

p^.inf := q^.inf;

p:= q;

q := q^.esquerda;

End;

End;

Começar

Se t = zero então

Writeln('nenhum elemento encontrado')

Senão se x < t^.inf então

Delete1(x, t^.esquerda)

Caso contrário, se x > t^.inf então

Excluir1(x, t^.direita)

Outro

Começar

P:= t;

Se p^.left = nil então

t := p^.direita

Outro

Se p^.right = nil então

t := p^.esquerda

Outro

Delete2(p^.esquerda);

End;

End.

PALESTRA Nº 10. Contagens

1. O conceito de gráfico. Formas de representar um gráfico

Um grafo é um par G = (V,E), onde V é um conjunto de objetos de natureza arbitrária, chamados vértices, e E é uma família de pares ei = (vil, vi2), vijOV, chamados arestas. No caso geral, o conjunto V e/ou a família E podem conter um número infinito de elementos, mas consideraremos apenas grafos finitos, ou seja, grafos para os quais V e E são finitos. Se a ordem dos elementos incluídos em ei importa, então o gráfico é chamado direcionado, abreviado - dígrafo, caso contrário - não direcionado. As arestas de um dígrafo são chamadas de arcos. No que segue, assumimos que o termo "grafo", usado sem especificação (direcionado ou não direcionado), denota um grafo não direcionado.

Se e = , então os vértices v e u são chamados de extremidades da aresta. Aqui dizemos que a aresta e é adjacente (incidente) a cada um dos vértices v e u. Os vértices v e e também são chamados de adjacentes (incidentes). No caso geral, arestas da forma e = ; tais arestas são chamadas de laços.

O grau de um vértice em um grafo é o número de arestas incidentes nesse vértice, com os loops sendo contados duas vezes. Como cada aresta é incidente a dois vértices, a soma dos graus de todos os vértices no gráfico é igual ao dobro do número de arestas: Sum(deg(vi), i=1...|V|) = 2 * | E|.

O peso de um nó é um número (real, inteiro ou racional) atribuído a um determinado nó (interpretado como custo, rendimento, etc.). Peso, comprimento da borda - um número ou vários números que são interpretados como comprimento, largura de banda, etc.

Um caminho em um grafo (ou uma rota em um dígrafo) é uma sequência alternada de vértices e arestas (ou arcos em um dígrafo) da forma v0, (v0,v1), v1..., (vn - 1,vn ), v. O número n é chamado de comprimento do caminho. Um caminho sem arestas repetidas é chamado de cadeia; um caminho sem vértices repetidos é chamado de cadeia simples. O caminho pode ser fechado (v0 = vn). Um caminho fechado sem arestas repetidas é chamado de ciclo (ou contorno em um dígrafo); sem repetir vértices (exceto o primeiro e o último) - um loop simples.

Um grafo é dito conectado se houver um caminho entre quaisquer dois de seus vértices, e desconectado caso contrário. Um grafo desconectado consiste em vários componentes conectados (subgrafos conectados).

Existem várias maneiras de representar gráficos. Vamos considerar cada um deles separadamente.

1. Matriz de incidência.

Esta é uma matriz retangular de dimensão nx n, onde n é o número de vértices, am é o número de arestas. Os valores dos elementos da matriz são determinados da seguinte forma: se a aresta xi e o vértice vj são incidentes, então o valor do elemento da matriz correspondente é igual a um, caso contrário o valor é zero. Para grafos direcionados, a matriz de incidência é construída de acordo com o seguinte princípio: o valor do elemento é igual a -1 se a aresta xi vier do vértice vj, igual a 1 se a aresta xi entrar no vértice vj, e igual a XNUMX caso contrário .

2. Matriz de adjacência.

Esta é uma matriz quadrada de dimensão nxn, onde n é o número de vértices. Se os vértices vi e vj são adjacentes, ou seja, se existe uma aresta conectando-os, então o elemento da matriz correspondente é igual a um, caso contrário é igual a zero. As regras para construir esta matriz para gráficos direcionados e não direcionados não são diferentes. A matriz de adjacência é mais compacta que a matriz de incidência. Deve-se notar que esta matriz também é muito esparsa, mas no caso de um gráfico não direcionado ela é simétrica em relação à diagonal principal, portanto você pode armazenar não a matriz inteira, mas apenas metade dela (uma matriz triangular ).

3. Lista de adjacências (incidentes).

É uma estrutura de dados que armazena uma lista de vértices adjacentes para cada vértice do grafo. A lista é uma matriz de ponteiros, cujo i-ésimo elemento contém um ponteiro para a lista de vértices adjacentes ao i-ésimo vértice.

Uma lista de adjacências é mais eficiente que uma matriz de adjacências porque elimina o armazenamento de elementos nulos.

4. Lista de listas.

É uma estrutura de dados em forma de árvore na qual um ramo contém listas de vértices adjacentes a cada um dos vértices do grafo, e o segundo ramo aponta para o próximo vértice do grafo. Essa maneira de representar o gráfico é a mais ideal.

2. Representação de um gráfico por uma lista de incidências. Algoritmo de Percurso de Profundidade do Gráfico

Para implementar um gráfico como uma lista de incidência, você pode usar o seguinte tipo:

TipoLista = ^S;

S=registro;

inf: Byte;

próximo : Lista;

end;

Então o gráfico é definido da seguinte forma:

Var Gr : array[1..n] de Lista;

Agora vamos nos voltar para o procedimento de travessia do gráfico. Este é um algoritmo auxiliar que permite visualizar todos os vértices do gráfico, analisar todos os campos de informação. Se considerarmos uma travessia de grafos em profundidade, existem dois tipos de algoritmos: recursivos e não recursivos.

Com o algoritmo de travessia recursiva em profundidade, tomamos um vértice arbitrário e encontramos um vértice invisível (novo) v adjacente a ele. Então tomamos o vértice v como não novo e encontramos qualquer novo vértice adjacente a ele. Se algum vértice não tiver vértices não vistos mais recentes, consideramos esse vértice como usado e retornamos um nível acima do vértice de onde chegamos ao vértice usado. A travessia continua dessa maneira até que não haja novos vértices não varridos no grafo.

Em Pascal, a travessia em profundidade ficaria assim:

Procedimento Obhod(gr : Gráfico; k : Byte);

Var g : Gráfico; l : Lista;

Começar

nov[k] := falso;

g := gr;

Enquanto g^.inf <> k faz

g := g^.próximo;

eu := g^.smeg;

Enquanto l <> nil começam

Se nov[l^.inf] então Obhod(gr, l^.inf);

l := l^.próximo;

End;

End;

Nota

Neste procedimento, ao descrever o tipo Graph, nos referimos à descrição de um gráfico por uma lista de listas. Array nov[i] é um array especial cujo i-ésimo elemento é True se o i-ésimo vértice não for visitado, e False caso contrário.

Um algoritmo de travessia não recursivo também é frequentemente usado. Nesse caso, a recursão é substituída por uma pilha. Depois que um vértice é visualizado, ele é colocado na pilha e é usado quando não há mais novos vértices adjacentes a ele.

3. Representação de um gráfico por uma lista de listas. Algoritmo de Percurso de Largura

Um gráfico pode ser definido usando uma lista de listas da seguinte forma:

TipoLista = ^Tlista;

tlist=registro

inf: Byte;

próximo : Lista;

end;

Gráfico = ^TGpaph;

TGpaph = registro

inf: Byte;

smeg : Lista;

próximo : Gráfico;

end;

Ao percorrer o grafo em largura, selecionamos um vértice arbitrário e examinamos todos os vértices adjacentes a ele de uma só vez. Uma fila é usada em vez de uma pilha. O algoritmo de busca em largura é muito útil para encontrar o caminho mais curto em um grafo.

Aqui está um procedimento para percorrer um gráfico em largura em pseudocódigo:

Procedimento Obhod2(v);

{valores spisok, nov - global}

Começar

fila = O;

fila <= v;

nov[v] = Falso;

Enquanto fila <> O do

Começar

p <= fila;

Para u em spisok(p) faça

Se novo[u] então

Começar

nov[u] := Falso;

fila <= u;

End;

End;

End;

AULA #11. Tipo de dados do objeto

1. Tipo de objeto em Pascal. O conceito de um objeto, sua descrição e uso

Historicamente, a primeira abordagem à programação tem sido a programação procedural, também conhecida como programação bottom-up. Inicialmente, foram criadas bibliotecas comuns de programas padrão usados ​​em vários campos de aplicação computacional. Então, com base nesses programas, foram criados programas mais complexos para resolver problemas específicos.

No entanto, a tecnologia computacional estava em constante desenvolvimento, começou a ser usada para resolver vários problemas de produção, economia e, portanto, tornou-se necessário processar dados de vários formatos e resolver problemas não padronizados (por exemplo, não numéricos). Portanto, ao desenvolver linguagens de programação, eles começaram a prestar atenção à criação de vários tipos de dados. Isso contribuiu para o surgimento de tipos de dados complexos como combinados, múltiplos, string, arquivo, etc. Antes de resolver o problema, o programador realizou a decomposição, ou seja, dividiu a tarefa em várias subtarefas, para cada uma das quais foi escrito um módulo separado . A principal tecnologia de programação incluiu três etapas:

1) projeto de cima para baixo;

2) programação modular;

3) codificação estrutural.

Mas a partir de meados dos anos 60 do século XX, novos conceitos e abordagens começaram a se formar, que formaram a base da tecnologia de programação orientada a objetos. Nesta abordagem, a modelagem e descrição do mundo real é realizada no nível de conceitos de uma área temática específica à qual o problema a ser resolvido pertence.

A programação orientada a objetos é uma técnica de programação que se assemelha muito ao nosso comportamento. É uma evolução natural de inovações anteriores no design de linguagem de programação. A programação orientada a objetos é mais estrutural do que todos os desenvolvimentos anteriores em relação à programação estruturada. Também é mais modular e mais abstrato do que as tentativas anteriores de abstração de dados e detalhes de programação internamente. Uma linguagem de programação orientada a objetos é caracterizada por três propriedades principais:

1) Encapsulamento. A combinação de registros com procedimentos e funções que manipulam os campos desses registros forma um novo tipo de dado - um objeto;

2) Herança. Definição de um objeto e seu uso posterior para construir uma hierarquia de objetos filho com a capacidade de cada objeto filho relacionado à hierarquia acessar o código e os dados de todos os objetos pai;

3) Polimorfismo. Dando a uma ação um único nome, que é então compartilhado para cima e para baixo na hierarquia de objetos, com cada objeto na hierarquia executando essa ação de uma forma adequada.

Falando do objeto, apresentamos um novo tipo de dado - o objeto. Um tipo de objeto é uma estrutura que consiste em um número fixo de componentes. Cada componente é um campo contendo dados de um tipo estritamente definido ou um método que executa operações em um objeto. Por analogia com a declaração de variáveis, a declaração de um campo especifica o tipo de dado desse campo e o identificador que nomeia o campo: por analogia com a declaração de um procedimento ou função, a declaração de um método especifica o título de um procedimento, função, construtor ou destruidor.

Um tipo de objeto pode herdar componentes de outro tipo de objeto. Se o tipo T2 herdar do tipo T1, o tipo T2 será filho do tipo T1 e o próprio tipo T1 será pai do tipo T2. A herança é transitiva, ou seja, se TK herda de T2 e T2 herda de T1, então TK herda de T1. O escopo (domínio) de um tipo de objeto consiste em si mesmo e em todos os seus descendentes.

O código-fonte a seguir é um exemplo de uma declaração de tipo de objeto, tipo

tipo

ponto = objeto

X, Y: inteiro;

end;

Rect = objeto

A, B: Ponto T;

procedimento Init(XA, YA, XB, YB: Inteiro);

procedimento Copiar(var R: TRectangle);

procedimento Move(DX, DY: Inteiro);

procedimento Grow(DX, DY: Inteiro);

procedimento Intersect(var R: TRectangle);

procedimento União(var R: TRectangle);

função Contém(P: Ponto): Booleano;

end;

StringPtr = ^String;

CampoPtr = ^TField;

TField = objeto

X, Y, Len: inteiro;

Nome: StringPtr;

construtor Copiar(var F: TField);

construtor Init(FX, FY, FLen: Integer; FName: String);

destruidor Feito; virtual;

procedimento de exibição; virtual;

procedimento Editar; virtual;

função GetStr: String; virtual;

função PutStr(S: String): Booleano; virtual;

end;

StrFieldPtr = ^TStrCampo;

StrField = objeto(TField)

Valor: PString;

construtor Init(FX, FY, FLen: Integer; FName: String);

destruidor Feito; virtual;

função GetStr: String; virtual;

função PutStr(S: String): Booleano;

virtual;

função Get:string;

procedimento Put(S: String);

end;

NumFieldPtr = ^TNumField;

TNumField = objeto(TField)

privado

Valor, Min, Max: Longint;

público

construtor Init(FX, FY, FLen: Integer; FName: String;

FMin, FMax: Inteiro longo);

função GetStr: String; virtual;

função PutStr(S: String): Booleano; virtual;

função Get: Longint;

função Put(N: Longint);

end;

ZipFieldPtr = ^TZipField;

ZipField = objeto(TNumField)

função GetStr: String; virtual;

função PutStr(S: String): Booleano;

virtual;

final.

Ao contrário de outros tipos, os tipos de objeto só podem ser declarados na seção de declaração de tipo no nível mais externo do escopo de um programa ou módulo. Assim, os tipos de objeto não podem ser declarados em uma seção de declaração de variável ou dentro de um bloco de procedimento, função ou método.

Um tipo de componente de tipo de arquivo não pode ter um tipo de objeto ou qualquer tipo de estrutura contendo componentes de tipo de objeto.

2. Herança

O processo pelo qual um tipo herda as características de outro tipo é chamado de herança. O descendente é chamado de tipo derivado (filho) e o tipo do qual o tipo filho herda é chamado de tipo pai (pai).

Os tipos de registro Pascal conhecidos anteriormente não podem herdar. No entanto, o Borland Pascal estende a linguagem Pascal para suportar herança. Uma dessas extensões é uma nova categoria de estrutura de dados relacionada a registros, mas muito mais poderosa. Os tipos de dados nesta nova categoria são definidos usando a nova palavra reservada "objeto". Um tipo de objeto pode ser definido como um tipo completo e independente na maneira de descrever entradas em Pascal, mas também pode ser definido como um descendente de um tipo de objeto existente colocando o tipo pai entre parênteses após a palavra reservada "objeto".

3. Instanciar Objetos

Uma instância de um objeto é criada declarando uma variável ou constante de um tipo de objeto, ou aplicando o procedimento padrão New a uma variável do tipo "ponteiro para tipo de objeto". O objeto resultante é chamado de instância do tipo de objeto;

var

F: Tcampo;

Z: TZipField;

FP: PFcampo;

ZP: PZipField;

Dadas essas declarações de variáveis, F é uma instância de TField e Z é uma instância de TZipField. Da mesma forma, após aplicar New a FP e ZP, FP apontará para uma instância TField e ZP apontará para uma instância TZipField.

Se um tipo de objeto contém métodos virtuais, as instâncias desse tipo de objeto devem ser inicializadas chamando um construtor antes de chamar qualquer método virtual.

Abaixo segue um exemplo:

var

S: StrField;

por exemplo

S.Init(1, 1, 25, 'Primeiro nome');

S.Put('Vladimir');

S.Exibir;

...

S Feito;

final.

Se S.Init não foi chamado, chamar S.Display fará com que este exemplo falhe.

A atribuição de uma instância de um tipo de objeto não implica a inicialização da instância. Um objeto é inicializado pelo código gerado pelo compilador que é executado entre a invocação do construtor e o ponto em que a execução realmente atinge a primeira instrução no bloco de código do construtor.

Se a instância do objeto não for inicializada e a verificação de intervalo estiver habilitada (pela diretiva {SR+}), a primeira chamada para o método virtual da instância do objeto apresentará um erro em tempo de execução. Se a verificação de intervalo estiver desabilitada (pela diretiva {SR-}), a primeira chamada para um método virtual de um objeto não inicializado pode levar a um comportamento imprevisível.

A regra de inicialização obrigatória também se aplica a instâncias que são componentes de tipos struct. Por exemplo:

var

Comentário: array [1..5] de TStrField;

I: inteiro

começar

para I := 1 a 5 faça

Comentário [I].Init (1, I + 10, 40, 'first_name');

.

.

.

for I := 1 a 5 do Comentário [I].Done;

end;

Para instâncias dinâmicas, a inicialização geralmente é sobre o posicionamento e a limpeza é sobre a exclusão, que é obtida por meio da sintaxe estendida dos procedimentos padrão Novo e Descartar. Por exemplo:

var

SP: StrFieldPtr;

começar

New(SP, Init(1, 1, 25, 'nome_nome');

SP^.Put('Vladimir');

SP^.Exibir;

.

.

.

Descarte (SP, Feito);

final.

Um ponteiro para um tipo de objeto é uma atribuição compatível com um ponteiro para qualquer tipo de objeto pai, portanto, em tempo de execução, um ponteiro para um tipo de objeto pode apontar para uma instância desse tipo ou para uma instância de qualquer tipo filho.

Por exemplo, um ponteiro do tipo ZipFieldPtr pode ser atribuído a ponteiros do tipo PZipField, PNumField e PField e, em tempo de execução, um ponteiro do tipo PField pode ser nil ou apontar para uma instância de TField, TNumField ou TZipField, ou qualquer instância de um tipo filho de TField.

Essas regras de compatibilidade de ponteiro de atribuição também se aplicam a parâmetros de tipo de objeto. Por exemplo, o método TField.Cop pode receber instâncias de TField, TStrField, TNumField, TZipField ou qualquer outro tipo filho de TField.

4. Componentes e Escopo

O escopo de um identificador de bean vai além do tipo de objeto. Além disso, o escopo de um identificador de bean se estende pelos blocos de procedimentos, funções, construtores e destruidores que implementam os métodos do tipo de objeto e seus descendentes. Com base nessas considerações, a grafia do identificador do componente deve ser exclusiva no tipo de objeto e em todos os seus descendentes, bem como em todos os seus métodos.

O escopo do identificador de componente descrito na parte privada da declaração de tipo é limitado ao módulo (programa) que contém a declaração de tipo de objeto. Em outras palavras, os beans identificadores privados agem como identificadores públicos comuns dentro do módulo que contém a declaração do tipo de objeto, e fora do módulo quaisquer beans e identificadores privados são desconhecidos e inacessíveis. Ao colocar tipos de objetos relacionados no mesmo módulo, você pode ter certeza de que esses objetos podem acessar os componentes privados uns dos outros, e esses componentes privados serão desconhecidos para outros módulos.

Em uma declaração de tipo de objeto, um cabeçalho de método pode especificar os parâmetros do tipo de objeto que está sendo descrito, mesmo que a descrição ainda não esteja completa.

PALESTRA Nº 12. Métodos

1. Métodos

Uma declaração de método dentro de um tipo de objeto corresponde a uma declaração de método forward (forward). Assim, em algum lugar depois de uma declaração de tipo de objeto, mas dentro do mesmo escopo que o escopo da declaração de tipo de objeto, um método deve ser implementado definindo sua declaração.

Para métodos procedurais e funcionais, a declaração de definição assume a forma de um procedimento normal ou declaração de função, com a exceção de que, neste caso, o identificador de procedimento ou função é tratado como um identificador de método.

Para métodos construtores e destruidores, a declaração de definição assume a forma de uma declaração de método de procedimento, com a exceção de que a palavra reservada procedimento é substituída pela palavra reservada construtor ou destruidor.

A declaração do método definidor pode, mas não precisa, repetir a lista de parâmetros formais do cabeçalho do método no tipo de objeto. Nesse caso, o cabeçalho do método deve corresponder exatamente ao cabeçalho no tipo de objeto em ordem, tipos e nomes de parâmetro e no tipo de retorno do resultado da função se o método for uma função.

A descrição de definição de um método sempre contém um parâmetro implícito com o identificador Self, correspondente a um parâmetro de variável formal que possui um tipo de objeto. Dentro de um bloco de método, Self representa a instância cujo componente de método foi especificado para invocar o método. Assim, quaisquer alterações nos valores dos campos Self são refletidas na instância.

O escopo de um identificador de bean de tipo de objeto se estende a blocos de procedimentos, funções, construtores e destruidores que implementam métodos desse tipo de objeto. O efeito é o mesmo que se uma instrução with da seguinte forma fosse inserida no início do bloco do método:

com auto fazer

começar

...

end;

Com base nessas considerações, a ortografia dos identificadores de componentes, parâmetros formais do método, Self e qualquer identificador introduzido na parte executável do método deve ser exclusivo.

Se um identificador de método exclusivo for necessário, o identificador de método qualificado será usado. Ele consiste em um identificador de tipo de objeto seguido por um ponto e um identificador de método. Como acontece com qualquer outro identificador, um identificador de método qualificado pode opcionalmente ser precedido por um identificador de pacote e um ponto.

Métodos Virtuais

Os métodos são estáticos por padrão, mas, exceto pelos construtores, eles podem ser virtuais (incluindo a diretiva virtual na declaração do método). O compilador resolve as referências a chamadas de métodos estáticos durante o processo de compilação, enquanto as chamadas de métodos virtuais são resolvidas em tempo de execução. Isso às vezes é chamado de ligação tardia.

Se um tipo de objeto declara ou herda qualquer método virtual, as variáveis ​​desse tipo devem ser inicializadas chamando um construtor antes de chamar qualquer método virtual. Assim, um tipo de objeto que descreve ou herda um método virtual também deve descrever ou herdar pelo menos um método construtor.

Um tipo de objeto pode substituir qualquer um dos métodos que herda de seus pais. Se uma declaração de método em um filho especificar o mesmo identificador de método que uma declaração de método no pai, a declaração no filho substituirá a declaração no pai. O escopo de um método de substituição se expande para o escopo do filho no qual o método foi introduzido e permanecerá assim até que o identificador do método seja substituído novamente.

A substituição de um método estático é independente da alteração do cabeçalho do método. Por outro lado, uma substituição de método virtual deve preservar a ordem, os tipos e nomes de parâmetros e os tipos de resultado da função, se houver. Além disso, a redefinição deve incluir novamente a diretiva virtual.

Métodos dinâmicos

O Borland Pascal suporta métodos adicionais de ligação tardia chamados métodos dinâmicos. Os métodos dinâmicos diferem dos métodos virtuais apenas na maneira como são despachados em tempo de execução. Em todos os outros aspectos, os métodos dinâmicos são considerados equivalentes aos métodos virtuais.

Uma declaração de método dinâmico é equivalente a uma declaração de método virtual, mas a declaração de método dinâmico deve incluir o índice de método dinâmico, que é especificado imediatamente após a palavra-chave virtual. O índice de um método dinâmico deve ser uma constante inteira entre 1 e 656535 e deve ser exclusivo entre os índices de outros métodos dinâmicos contidos no tipo de objeto ou em seus ancestrais. Por exemplo:

procedimento FileOpen(var Msg: TMessage); 100 virtuais;

Uma substituição de um método dinâmico deve corresponder à ordem, tipos e nomes dos parâmetros e corresponder exatamente ao tipo de resultado da função do método pai. A substituição também deve incluir uma diretiva virtual seguida pelo mesmo índice de método dinâmico que foi especificado no tipo de objeto ancestral.

2. Construtores e destruidores

Construtores e destruidores são formas especializadas de métodos. Usado em conexão com a sintaxe estendida dos procedimentos padrão New e Dispose, construtores e destruidores têm a capacidade de colocar e remover objetos dinâmicos. Além disso, os construtores têm a capacidade de executar a inicialização necessária de objetos contendo métodos virtuais. Como todos os outros métodos, construtores e destruidores podem ser herdados e objetos podem conter qualquer número de construtores e destruidores.

Construtores são usados ​​para inicializar objetos recém-criados. Normalmente, a inicialização é baseada nos valores passados ​​ao construtor como parâmetros. Um construtor não pode ser virtual porque o mecanismo de despacho de um método virtual depende do construtor que inicializou o objeto primeiro.

Aqui estão alguns exemplos de construtores:

construtor Field.Copy(var F: Field);

começar

Próprio := F;

end;

construtor Field.Init(FX, FY, FLen: inteiro; FName: string);

começar

X := FX;

S := FY;

GetMem(Nome, Comprimento(FNome) + 1);

Nome^ := FNome;

end;

construtor TStrField.Init(FX, FY, FLen: inteiro; FName: string);

começar

herdado Init(FX, FY, FLen, FName);

Field.Init(FX,FY,FLen,FName);

GetMem(Valor, Len);

Valor^ := '';

end;

A ação principal de um construtor de um tipo derivado (filho), como o campo TSr acima. Init é quase sempre uma chamada para o construtor apropriado de seu pai imediato para inicializar os campos herdados do objeto. Após executar este procedimento, o construtor inicializa os campos do objeto que pertencem apenas ao tipo derivado.

Os destruidores são o oposto dos construtores e são usados ​​para limpar objetos depois de serem usados. Normalmente, a limpeza consiste em remover todos os campos de ponteiro do objeto.

Nota

Um destruidor pode ser virtual, e muitas vezes é. Um destruidor raramente tem parâmetros.

Aqui estão alguns exemplos de destruidores:

destruidor Campo Feito;

começar

FreeMem(Nome, Comprimento(Nome^) + 1);

end;

destruidor StrField.Done;

começar

FreeMem(Valor, Len);

Campo Feito;

end;

O destruidor de um tipo filho, como o TSrField acima. Concluído, normalmente primeiro remove os campos de ponteiro introduzidos no tipo derivado e, em seguida, como última etapa, chama o destruidor de coletor apropriado do pai imediato para remover os campos de ponteiro herdados do objeto.

3. Destruidores

O Borland Pascal fornece um tipo especial de método chamado coletor de lixo (ou destruidor) para limpar e excluir um objeto alocado dinamicamente. O destruidor combina a etapa de exclusão de um objeto com quaisquer outras ações ou tarefas necessárias para esse tipo de objeto. Você pode definir vários destruidores para um único tipo de objeto.

O destruidor é definido junto com todos os outros métodos de objeto na definição de tipo do objeto:

modelo

Temployee = objeto

Nome: string[25];

Título: string[25];

Taxa: Reais;

construtor Init(AName, ATitle: String; ARate: Real);

destruidor Feito; virtual;

função GetName: String;

função GetTitle: String;

função GetRate: Taxa; virtual;

função GetPayAmount: Real; virtual;

end;

Os destruidores podem ser herdados e podem ser estáticos ou virtuais. Como diferentes finalizadores tendem a exigir diferentes tipos de objetos, geralmente é recomendável que os destruidores sejam sempre virtuais para que o destruidor correto seja executado para cada tipo de objeto.

O destruidor de palavras reservadas não precisa ser especificado para cada método de limpeza, mesmo que a definição de tipo do objeto contenha métodos virtuais. Destrutores realmente só funcionam em objetos alocados dinamicamente.

Quando um objeto alocado dinamicamente é limpo, o destruidor executa uma função especial: ele garante que o número correto de bytes seja sempre liberado na área de memória alocada dinamicamente. Não pode haver preocupação em usar um destruidor com objetos alocados estaticamente; de fato, ao não passar o tipo do objeto para o destruidor, o programador priva um objeto desse tipo de todos os benefícios do gerenciamento dinâmico de memória no Borland Pascal.

Destrutores realmente se tornam eles mesmos quando objetos polimórficos devem ser apagados e quando a memória que eles ocupam deve ser desalocada.

Objetos polimórficos são aqueles objetos que foram atribuídos a um tipo pai devido às regras de compatibilidade de tipos estendidas do Borland Pascal. Uma instância de um objeto do tipo THourly atribuído a uma variável do tipo TEmployee é um exemplo de objeto polimórfico. Essas regras também podem ser aplicadas a objetos; um ponteiro para THourly pode ser atribuído livremente a um ponteiro para TEmployee, e o objeto apontado por esse ponteiro será novamente um objeto polimórfico. O termo "polimórfico" é apropriado porque o código que processa um objeto "não sabe" exatamente em tempo de compilação que tipo de objeto ele precisará processar. A única coisa que ele sabe é que esse objeto pertence a uma hierarquia de objetos que são descendentes do tipo de objeto especificado.

Obviamente, os tamanhos dos tipos de objetos são diferentes. Então, quando chega a hora de limpar um objeto polimórfico alocado em heap, como Dispose sabe quantos bytes de espaço de heap devem ser liberados? Em tempo de compilação, nenhuma informação sobre o tamanho do objeto pode ser extraída de um objeto polimórfico.

O destruidor resolve esse quebra-cabeça referindo-se ao local onde essas informações são gravadas - nas variáveis ​​de implementação do TCM. Cada TBM de um tipo de objeto contém o tamanho em bytes desse tipo de objeto. A tabela de métodos virtuais de qualquer objeto está disponível através do parâmetro oculto Self, enviado ao método quando o método é chamado. Um destruidor é apenas um tipo de método e, portanto, quando um objeto o chama, o destruidor obtém uma cópia de Self na pilha. Assim, se um objeto for polimórfico em tempo de compilação, ele nunca será polimórfico em tempo de execução devido à ligação tardia.

Para realizar essa desalocação de ligação tardia, o destruidor deve ser chamado como parte da sintaxe estendida do procedimento Dispose:

Descarte(P, Pronto);

(Uma chamada para o destruidor fora do procedimento Dispose não desaloca nenhuma memória.) O que realmente está acontecendo aqui é que o coletor de lixo do objeto apontado por P é executado como um método normal. No entanto, uma vez que a última ação é concluída, o destruidor procura o tamanho da implementação de seu tipo no TCM e passa o tamanho para o procedimento Dispose. O procedimento Dispose encerra o processo excluindo o número correto de bytes do espaço de heap que (o espaço) pertencia anteriormente a P^. O número de bytes a serem liberados estará correto independentemente de P apontar para uma instância do tipo TSalaried ou se apontar para um dos tipos filho do tipo TSalaried, como TCommissioned.

Observe que o próprio método destruidor pode estar vazio e executar apenas esta função:

destructorAnObject.Done;

começar

end;

O que é útil neste destruidor não é propriedade de seu corpo, porém, o compilador gera código de epílogo em resposta à palavra reservada do destruidor. É como um módulo que não exporta nada, mas faz algum trabalho invisível executando sua seção de inicialização antes de iniciar o programa. Toda a ação acontece nos bastidores.

4. Métodos Virtuais

Um método se torna virtual se sua declaração de tipo de objeto for seguida pela nova palavra reservada virtual. Se um método em um tipo pai for declarado como virtual, todos os métodos com o mesmo nome em tipos filho também deverão ser declarados virtuais para evitar um erro do compilador.

A seguir estão os objetos da folha de pagamento de exemplo, devidamente virtualizados:

modelo

PEfuncionário = ^TEFuncionário;

Temployee = objeto

Nome, Título: string[25];

Taxa: Reais;

construtor Init(AName, ATitle: String; ARate: Real);

função GetPayAmount : Real; virtual;

função GetName : String;

função GetTitle : String;

função GetRate : Real;

procedimento Mostrar; virtual;

end;

PHhourly = ^THourly;

THourly = object(TEFuncionário);

Hora: Inteiro;

construtor Init(AName, ATitle: String; ARate: Real; Time: Integer);

função GetPayAmount : Real; virtual;

função GetTime : Inteiro;

end;

PSAssalariado = ^TSAssalariado;

TSalario = objeto(TEEmpregado);

função GetPayAmount : Real; virtual;

end;

PComissionado = ^TComissionado;

TComissionado = objeto(Assalariado);

Comissão : Real;

Valor de Vendas : Real;

construtor Init(AName, ATitle: String; ARate,

AComissão, ASVendasValor: Real);

função GetPayAmount : Real; virtual;

end;

Um construtor é um tipo especial de procedimento que faz algum trabalho de configuração para o mecanismo de método virtual. Além disso, o construtor deve ser chamado antes de qualquer método virtual ser chamado. Chamar um método virtual sem primeiro chamar o construtor pode bloquear o sistema e não há como o compilador verificar a ordem na qual os métodos são chamados.

Todo tipo de objeto que possui métodos virtuais deve ter um construtor.

aviso

O construtor deve ser chamado antes de qualquer outro método virtual ser chamado. Chamar um método virtual sem uma chamada anterior ao construtor pode causar um bloqueio do sistema e o compilador não pode verificar a ordem em que os métodos são chamados.

Nota

Para construtores de objetos, sugere-se usar o identificador Init.

Cada instância de objeto distinto deve ser inicializada com uma chamada de construtor separada. Não basta inicializar uma instância de um objeto e depois atribuir essa instância a outras. Outras instâncias, mesmo que contenham dados válidos, não serão inicializadas com um operador de atribuição e bloquearão o sistema em qualquer chamada para seus métodos virtuais. Por exemplo:

var

FBee, GBee: Bee; { cria duas instâncias do Bee }

começar

FBee.Init(5, 9) { chamada do construtor para FBee }

GBee := FBee; { Gbee é inválido! }

end;

O que exatamente um construtor cria? Cada tipo de objeto contém algo chamado tabela de método virtual (VMT) no segmento de dados. O TVM contém o tamanho do tipo de objeto e, para cada método virtual, um ponteiro para o código que executa esse método. Um construtor estabelece um relacionamento entre a implementação de chamada do objeto e o tipo TCM do objeto.

É importante lembrar que existe apenas um TBM para cada tipo de objeto. Instâncias separadas de um tipo de objeto (ou seja, variáveis ​​desse tipo) contêm apenas a conexão com o TBM, mas não o próprio TBM. O construtor define o valor dessa conexão para TBM. É por isso que em nenhum lugar você pode iniciar a execução antes de chamar o construtor.

5. Campos de dados de objetos e parâmetros de métodos formais

A implicação do fato de que métodos e seus objetos compartilham um escopo comum é que os parâmetros formais de um método não podem ser idênticos a nenhum dos campos de dados do objeto. Esta não é uma nova limitação imposta pela programação orientada a objetos, mas sim as mesmas velhas regras de escopo que Pascal sempre teve. Isso é o mesmo que impedir que os parâmetros formais de um procedimento sejam idênticos às variáveis ​​locais do procedimento:

procedimento CrunchIt(Crunchee: MyDataRec, Crunchby,

Código de Erro: inteiro);

var

A, B: caractere;

Código de erro: inteiro;

começar

.

.

.

As variáveis ​​locais de um procedimento e seus parâmetros formais compartilham um escopo comum e, portanto, não podem ser idênticos. Você obterá "Erro 4: identificador duplicado" se tentar compilar algo assim, o mesmo erro ocorre quando você tenta definir um parâmetro de método formal para o nome do campo do objeto ao qual o método pertence.

As circunstâncias são um pouco diferentes, pois colocar o cabeçalho do procedimento dentro de uma estrutura de dados é um aceno para uma inovação no Turbo Pascal, mas os princípios básicos do escopo do Pascal não mudaram.

PALESTRA Nº 13. Compatibilidade de tipos de objetos

1. Encapsulamento

A combinação de código e dados em um objeto é chamada de encapsulamento. Em princípio, é possível fornecer métodos suficientes para que o usuário de um objeto nunca acesse diretamente os campos do objeto. Algumas outras linguagens orientadas a objetos, como Smalltalk, requerem encapsulamento obrigatório, mas o Borland Pascal tem uma escolha.

Por exemplo, os objetos TEmployee e THourly são escritos de tal forma que não há absolutamente nenhuma necessidade de acessar diretamente seus campos de dados internos:

tipo

Temployee = objeto

Nome, Título: string[25];

Taxa: Reais;

procedimento Init(AName, ATitle: string; ARate: Real);

função GetName : String;

função GetTitle : String;

função GetRate : Real;

função GetPayAmount : Real;

end;

THourly = object(TEFuncionário)

Hora: Inteiro;

procedimento Init(AName, ATitle: string; ARate:

Real, Atime: Inteiro);

função GetPayAmount : Real;

end;

Existem apenas quatro campos de dados aqui: Nome, Título, Taxa e Tempo. Os métodos GetName e GetTitle exibem o sobrenome e a posição do trabalhador, respectivamente. O método GetPayAmount usa Rate, e no caso de um trabalho THourly e Time para calcular o valor dos pagamentos ao trabalho. Não há mais necessidade de se referir diretamente a esses campos de dados.

Assumindo a existência de uma instância AnHourly do tipo THourly, poderíamos usar um conjunto de métodos para manipular campos de dados AnHourly como este:

com um fazer de hora em hora

começar

Init (Aleksandr Petrov, operador de empilhadeira' 12.95, 62);

{Exibe o sobrenome, cargo e valor dos pagamentos}

Exposição;

end;

Deve-se notar que o acesso aos campos de um objeto é realizado apenas com a ajuda de métodos desse objeto.

2. Objetos em expansão

Infelizmente, o Pascal padrão não oferece nenhum recurso para criar procedimentos flexíveis que permitem trabalhar com tipos de dados completamente diferentes. A programação orientada a objetos resolve esse problema com herança: se um tipo derivado for definido, os métodos do tipo pai serão herdados, mas poderão ser substituídos, se desejado. Para substituir um método herdado, simplesmente declare um novo método com o mesmo nome do método herdado, mas com um corpo diferente e (se necessário) um conjunto de parâmetros diferente.

Vamos definir um tipo filho de TEmployee que representa um funcionário que recebe uma taxa horária no exemplo a seguir:

const

Períodos de Pagamento = 26; { períodos de pagamento }

Limite de horas extras = 80; { para o período de pagamento }

Fator Hora Extra = 1.5; { taxa horária }

tipo

THourly = object(TEFuncionário)

Hora: Inteiro;

procedimento Init(AName, ATitle: string; ARate:

Real, Atime: Inteiro);

função GetPayAmount : Real;

end;

procedimento THourly.Init(AName, ATitle: string;

ARate: Real, Atime: Inteiro);

começar

TEmployee.Init(ANome, ATitle, ARate);

Hora := AHora;

end;

função THourly.GetPayAmount: Real;

var

Horas extras: Inteiro;

começar

Horas Extras := Horas - OvertimeThreshold;

se Horas extras > 0 então

GetPayAmount := RoundPay(Extratempo Limite * Taxa +

Taxa Hora Extra * Fator Hora Extra * Taxa)

outro

GetPayAmount := RoundPay(Tempo * Taxa)

end;

Uma pessoa que recebe uma taxa horária é um trabalhador: ele tem tudo o que é usado para definir o objeto TEmployee (nome, cargo, taxa), e apenas a quantidade de dinheiro recebida pelo horista depende de quantas horas ele trabalhou durante o prazo a pagar. Assim, Thourly também requer um campo Time.

Como o THourly define um novo campo Time, sua inicialização requer um novo método Init que inicializa os campos time e herdados. Em vez de atribuir valores diretamente aos campos herdados, como Nome, Título e Taxa, por que não reutilizar o método de inicialização do objeto TEmployee (ilustrado pela primeira instrução THourly Init).

Chamar um método que está sendo substituído não é o melhor estilo. Em geral, é possível que TEmployee.Init execute uma inicialização importante, mas oculta.

Ao chamar um método substituído, você deve ter certeza de que o tipo de objeto derivado inclui a funcionalidade do pai. Além disso, qualquer alteração no método pai afeta automaticamente todos os métodos filho.

Após chamar TEmployee.Init, THourly.Init pode então realizar sua própria inicialização, que neste caso consiste apenas em atribuir o valor passado em ATime.

Outro exemplo de um método substituído é a função THourly.GetPayAmount, que calcula o valor do pagamento para um funcionário horista. Na verdade, cada tipo de objeto TEmployee tem seu próprio método GetPayAmount, já que o tipo de trabalhador depende de como o cálculo é feito. O método THourly.GetPayAmount deve levar em consideração quantas horas o funcionário trabalhou, se houve horas extras, qual foi o fator de aumento para horas extras, etc.

Método TSsalariado. GetPayAmount deve apenas dividir a taxa do funcionário pelo número de pagamentos em cada ano (no nosso exemplo).

trabalhadores da unidade;

interface

const

Períodos de Pagamento = 26; {no ano}

Limite de horas extras = 80; {para cada período de pagamento}

Fator Hora Extra=1.5; {aumento em relação ao pagamento normal}

tipo

Temployee = objeto

Nome, Título: string[25];

Taxa: Reais;

procedimento Init(AName, ATitle: string; ARate: Real);

função GetName : String;

função GetTitle : String;

função GetRate : Real;

função GetPayAmount : Real;

end;

THourly = object(TEFuncionário)

Hora: Inteiro;

procedimento Init(AName, ATitle: string; ARate:

Real, Atime: Inteiro);

função GetPayAmount : Real;

função GetTime : Real;

end;

TSalariado = objeto(TEfuncionário)

função GetPayAmount : Real;

end;

TComissionado = objeto(TSalariado)

Comissão : Real;

Valor de Vendas : Real;

construtor Init(AName, ATitle: String; ARate,

AComissão, ASVendasValor: Real);

função GetPayAmount : Real;

end;

implementação

função RoundPay(Wages: Real) : Real;

{arredondar os pagamentos para ignorar valores inferiores a

unidade monetária}

começar

RoundPay := Trunc(Salários * 100) / 100;

.

.

.

TEmployee é o topo da nossa hierarquia de objetos e contém o primeiro método GetPayAmount.

função TEmployee.GetPayAmount : Real;

começar

RunError(211); { dá erro de tempo de execução }

end;

Pode ser uma surpresa que o método dê um erro em tempo de execução. Se Employee.GetPayAmount for chamado, ocorrerá um erro no programa. Por quê? Porque TEmployee é o topo da nossa hierarquia de objetos e não define um trabalhador real; portanto, nenhum dos métodos TEmployee é chamado de uma maneira específica, embora possam ser herdados. Todos os nossos funcionários são horistas, assalariados ou por peça. Um erro em tempo de execução encerra a execução do programa e gera 211, que corresponde a uma mensagem de erro associada a uma chamada de método abstrato (se o programa chamar TEmployee.GetPayAmount por engano).

Abaixo está o método THourly.GetPayAmount, que leva em consideração coisas como pagamento de horas extras, horas trabalhadas etc.

função THourly.GetPayAMount : Real;

var

Horas Extras: Inteiro;

começar

Horas Extras := Horas - OvertimeThreshold;

se Horas extras > 0 então

GetPayAmount := RoundPay(Extratempo Limite * Taxa +

Taxa Hora Extra * Fator Hora Extra * Taxa)

outro

GetPayAmount := RoundPay(Tempo * Taxa)

end;

O método TSalaried.GetPayAmount é muito mais simples; nele aposta

dividido pelo número de pagamentos:

função TSalaried.GetPayAmount : Real;

começar

GetPayAmount := RoundPay(Taxa / PayPeriods);

end;

Se você observar o método TCommissioned.GetPayAmount, verá que ele chama TSalaried.GetPayAmount, calcula a comissão e a adiciona ao valor retornado pelo método TSalaried. GetPayAmount.

função TСommissioned.GetPayAmount : Real;

começar

GetPayAmount := RoundPay(TSalaried.GetPayAmount +

Comissão * Valor de Vendas);

end;

Observação importante: Embora os métodos possam ser substituídos, os campos de dados não podem ser substituídos. Depois que um campo de dados é definido em uma hierarquia de objetos, nenhum tipo filho pode definir um campo de dados com exatamente o mesmo nome.

3. Compatibilidade de tipos de objetos

A herança modifica as regras de compatibilidade de tipo do Borland Pascal até certo ponto. Entre outras coisas, um tipo derivado herda a compatibilidade de tipo de todos os seus tipos pai.

Essa compatibilidade de tipo estendida assume três formas:

1) entre implementações de objetos;

2) entre ponteiros para implementações de objetos;

3) entre parâmetros formais e reais.

No entanto, é muito importante lembrar que em todas as três formas, a compatibilidade de tipo só se estende do filho ao pai. Em outras palavras, os tipos filho podem ser usados ​​livremente no lugar dos tipos pai, mas não vice-versa.

Por exemplo, TSalaried é filho de TEmployee e TSosh-missioned é filho de TSalaried. Com isso em mente, considere as seguintes descrições:

modelo

PEfuncionário = ^TEFuncionário;

PSAssalariado = ^TSAssalariado;

PComissionado = ^TComissionado;

var

UmFuncionário: TEFuncionário;

AAssalariado: TAssalariado;

PComissionado: TComissionado;

TEmployeePtr: PEemployee;

TSsalárioPtr: PSsalário;

TComissionadoPtr: PComissionado;

Sob essas descrições, os seguintes operadores são válidos

atribuições:

UmFuncionário :=ASAssalariado;

AAssalariado := AComissionado;

TCommissionedPtr := AComissionado;

Nota

Um objeto pai pode receber uma instância de qualquer um de seus tipos derivados. Atribuições de volta não são permitidas.

Esse conceito é novo para o Pascal e, a princípio, pode ser difícil lembrar qual é a compatibilidade do tipo de pedido. Você precisa pensar assim: a fonte deve ser capaz de preencher completamente o receptor. Os tipos derivados contêm tudo o que seus tipos pai contêm devido à propriedade de herança. Portanto, o tipo derivado é exatamente do mesmo tamanho ou (o que ocorre com mais frequência) é maior que seu pai, mas nunca menor. Atribuir um objeto pai (pai) a um filho (filho) pode deixar alguns campos do objeto filho indefinidos, o que é perigoso e, portanto, ilegal.

Nas instruções de atribuição, apenas os campos comuns a ambos os tipos serão copiados da origem para o destino. No operador de atribuição:

AnFuncionário:= AComissionado;

Somente os campos Nome, Título e Taxa de ACommissioned serão copiados para AnEmployee, pois esses são os únicos campos comuns a TCommissioned e TEmployee. A compatibilidade de tipos também funciona entre ponteiros para tipos de objetos e segue as mesmas regras gerais das implementações de objetos. Um ponteiro para um filho pode ser atribuído a um ponteiro para o pai. Dadas as definições anteriores, as seguintes atribuições de ponteiro são válidas:

TSalariedPtr:= TCommissionedPtr;

TEmployeePtr:= TSalariedPtr;

TEmployeePtr:= PComissionedPtr;

Lembre-se que atribuições inversas não são permitidas!

Um parâmetro formal (um valor ou um parâmetro de variável) de um determinado tipo de objeto pode ter como parâmetro real um objeto de seu próprio tipo ou objetos de todos os tipos filho. Se você definir um cabeçalho de procedimento como este:

procedimento CalcFedTax(Vítima: TSalaried);

então os tipos de parâmetros reais podem ser TSalaried ou TCommissioned, mas não TEmployee. A vítima também pode ser um parâmetro variável. Nesse caso, as mesmas regras de compatibilidade são seguidas.

Nota

Há uma diferença fundamental entre os parâmetros de valor e os parâmetros de variável. Um parâmetro de valor é um ponteiro para o objeto real passado como parâmetro, enquanto um parâmetro variável é apenas uma cópia do parâmetro real. Além disso, essa cópia inclui apenas os campos incluídos no tipo do parâmetro de valor formal. Isso significa que o parâmetro real é literalmente convertido para o tipo do parâmetro formal. Um parâmetro variável é mais como uma conversão para um padrão, no sentido de que o parâmetro real permanece inalterado.

Da mesma forma, se o parâmetro formal for um ponteiro para um tipo de objeto, o parâmetro real pode ser um ponteiro para esse tipo de objeto ou para qualquer tipo filho. Seja dado o título do procedimento:

procedimento Worker.Add(AWorker: PSalared);

Os tipos de parâmetros reais válidos seriam então PSalaried ou PCommissioned, mas não PEmployee.

PALESTRA Nº 14. Montadora

1. Sobre a montadora

Antigamente, assembler era uma linguagem sem saber que era impossível fazer um computador fazer algo útil. Aos poucos a situação mudou. Surgiram meios de comunicação mais convenientes com um computador. Mas, ao contrário de outras linguagens, o assembler não morreu; além disso, não poderia fazer isso em princípio. Por quê? Em busca de uma resposta, tentaremos entender o que é a linguagem assembly em geral.

Em suma, a linguagem assembly é uma representação simbólica da linguagem de máquina. Todos os processos na máquina no nível mais baixo de hardware são acionados apenas por comandos (instruções) da linguagem de máquina. A partir disso fica claro que, apesar do nome comum, a linguagem assembly para cada tipo de computador é diferente. Isso também se aplica à aparência de programas escritos em assembler e às ideias das quais essa linguagem é um reflexo.

Realmente resolver problemas relacionados a hardware (ou, ainda mais, relacionados a hardware, como melhorar a velocidade do programa) é impossível sem o conhecimento do montador.

Um programador ou qualquer outro usuário pode usar qualquer ferramenta de alto nível até programas para construir mundos virtuais e, talvez, nem mesmo suspeitar que o computador está realmente executando não os comandos da linguagem em que seu programa está escrito, mas sua representação transformada na forma de uma sequência de comandos chata e maçante de uma linguagem completamente diferente - linguagem de máquina. Agora imagine que tal usuário tenha um problema fora do padrão. Por exemplo, seu programa deve funcionar com algum dispositivo incomum ou realizar outras ações que exijam conhecimento dos princípios do hardware do computador. Não importa quão boa seja a linguagem em que o programador escreveu seu programa, ele não pode prescindir de conhecer o montador. E não é por acaso que quase todos os compiladores de linguagens de alto nível contêm meios de conectar seus módulos com módulos em assembler ou suportam acesso ao nível de programação em assembler.

Um computador é composto de vários dispositivos físicos, cada um dos quais está conectado a uma unidade, chamada de unidade do sistema. Para entender seu propósito funcional, vejamos o diagrama de blocos de um computador típico (Fig. 1). Não pretende uma precisão absoluta e visa apenas mostrar a finalidade, a relação e a composição típica dos elementos de um computador pessoal moderno.

Arroz. 1. Diagrama de blocos de um computador pessoal

2. Modelo de software do microprocessador

No mercado de computadores de hoje, há uma grande variedade de diferentes tipos de computadores. Portanto, é possível supor que o consumidor terá uma dúvida sobre como avaliar as capacidades de um determinado tipo (ou modelo) de um computador e suas características distintivas de computadores de outros tipos (modelos). Para reunir todos os conceitos que caracterizam um computador em termos de suas propriedades funcionais controladas por programas, existe um termo especial - arquitetura de computador. Pela primeira vez, o conceito de arquitetura de computadores começou a ser mencionado com o advento das máquinas de terceira geração para sua avaliação comparativa.

Faz sentido começar a aprender a linguagem assembly de qualquer computador somente depois de descobrir qual parte do computador fica visível e disponível para programação nessa linguagem. Este é o chamado modelo de programa de computador, do qual parte é o modelo de programa do microprocessador, que contém trinta e dois registradores, mais ou menos disponíveis para uso do programador.

Esses registradores podem ser divididos em dois grandes grupos:

1) 6 cadastros de usuários;

2) 16 registros do sistema.

3. Registros de usuários

Como o nome indica, os registros de usuário são chamados porque o programador pode usá-los ao escrever seus programas. Esses registros incluem (Fig. 2):

1) oito registradores de 32 bits que podem ser usados ​​por programadores para armazenar dados e endereços (também chamados de registradores de propósito geral (RON)):

eax/ax/ah/al;

ebx/bx/bh/bl;

edx/dx/dh/dl;

ecx/cx/ch/cl;

ep/pb;

esi/si;

edi/di;

esp/esp.

2) seis registradores de segmento: cs, ds, ss, es, fs, gs;

3) registros de status e controle:

bandeiras de registro de bandeiras/bandeiras;

registrador de ponteiro de comando eip/ip.

Arroz. 2. Registros de usuários

Muitos desses registros são dados com uma barra. Estes não são registros diferentes - eles são partes de um grande registro de 32 bits. Eles podem ser usados ​​no programa como objetos separados.

4. Registros gerais

Todos os registros deste grupo permitem acessar suas partes "inferiores". Apenas as partes inferiores de 16 e 8 bits desses registradores podem ser usadas para auto-endereçamento. Os 16 bits superiores desses registradores não estão disponíveis como objetos independentes.

Vamos listar os registradores pertencentes ao grupo de registradores de uso geral. Como esses registradores estão fisicamente localizados no microprocessador dentro da unidade lógica aritmética (AL>), eles também são chamados de registradores ALU:

1) eax/ax/ah/al (Registro do acumulador) - bateria. Usado para armazenar dados intermediários. Em alguns comandos, o uso deste registro é obrigatório;

2) ebx/bx/bh/bl (base cadastral) - base cadastral. Usado para armazenar o endereço base de algum objeto na memória;

3) ecx/cx/ch/cl (registro de contagem) - registro de contador. É usado em comandos que executam algumas ações repetitivas. Seu uso é muitas vezes implícito e oculto no algoritmo do comando correspondente.

Por exemplo, o comando de organização de loop, além de transferir o controle para um comando localizado em determinado endereço, analisa e decrementa o valor do registrador esx/cx em um;

4) edx/dx/dh/dl (registro de dados) - registro de dados.

Assim como o registrador eax/ax/ah/al, ele armazena dados intermediários. Alguns comandos requerem seu uso; para alguns comandos isso acontece implicitamente.

Os dois registradores a seguir são usados ​​para suportar as chamadas operações em cadeia, ou seja, operações que processam sequencialmente cadeias de elementos, cada um dos quais pode ter 32, 16 ou 8 bits de comprimento:

1) esi/si (registro de índice de origem) - índice de origem.

Este registro em operações de cadeia contém o endereço atual do elemento na cadeia de origem;

2) edi/di (registro de índice de destino) - índice do receptor (destinatário). Este registrador em operações de cadeia contém o endereço atual na cadeia de destino.

Na arquitetura do microprocessador no nível de hardware e software, uma estrutura de dados como uma pilha é suportada. Para trabalhar com a pilha no sistema de instruções do microprocessador, existem comandos especiais e, no modelo de software do microprocessador, existem registros especiais para isso:

1) esp/sp (registrador de ponteiro de pilha) - registrador de ponteiro de pilha. Contém um ponteiro para o topo da pilha no segmento de pilha atual.

2) ebp/bp (registrador de ponteiro de base) - registrador de ponteiro de base de quadro de pilha. Projetado para organizar o acesso aleatório aos dados dentro da pilha.

O uso de hard pining de registradores para algumas instruções permite que sua representação de máquina seja codificada de forma mais compacta. Conhecer esses recursos, se necessário, economizará pelo menos alguns bytes de memória ocupados pelo código do programa.

5. Registros de segmento

Existem seis registradores de segmento no modelo de software do microprocessador: cs, ss, ds, es, gs, fs.

Sua existência se deve às especificidades da organização e uso da RAM pelos microprocessadores Intel. Está no fato de que o hardware do microprocessador suporta a organização estrutural do programa na forma de três partes, chamadas de segmentos. Assim, tal organização de memória é chamada de segmentada.

Para indicar os segmentos aos quais o programa tem acesso em um determinado momento, os registradores de segmento são pretendidos. De fato (com uma pequena correção) esses registradores contêm os endereços de memória a partir dos quais os segmentos correspondentes começam. A lógica de processamento de uma instrução de máquina é construída de tal forma que ao buscar uma instrução, acessar dados do programa ou acessar a pilha, endereços em registradores de segmento bem definidos são usados ​​implicitamente.

O microprocessador suporta os seguintes tipos de segmentos.

1. Segmento de código. Contém comandos do programa. Para acessar este segmento, é utilizado o registrador cs (registro de segmento de código) - o registrador de código de segmento. Ele contém o endereço do segmento com instruções de máquina às quais o microprocessador tem acesso (ou seja, essas instruções são carregadas no pipeline do microprocessador).

2. Segmento de dados. Contém os dados processados ​​pelo programa. Para acessar este segmento, é utilizado o registro ds (registro de segmento de dados) - um registro de dados de segmento que armazena o endereço do segmento de dados do programa atual.

3. Empilhe o segmento. Este segmento é uma região da memória chamada pilha. O microprocessador organiza o trabalho com a pilha de acordo com o seguinte princípio: o último elemento escrito nesta área é selecionado primeiro. Para acessar este segmento, é usado o registrador ss (stack segment register) - o registrador de segmento de pilha que contém o endereço do segmento de pilha.

4. Segmento de dados adicional. Implicitamente, os algoritmos para executar a maioria das instruções de máquina assumem que os dados que processam estão localizados no segmento de dados, cujo endereço está no registrador do segmento ds. Se um segmento de dados não for suficiente para o programa, ele terá a oportunidade de usar mais três segmentos de dados adicionais. Mas, ao contrário do segmento de dados principal, cujo endereço está contido no registrador de segmento ds, ao usar segmentos de dados adicionais, seus endereços devem ser especificados explicitamente usando prefixos especiais de redefinição de segmento no comando. Endereços de segmentos de dados adicionais devem estar contidos nos registros es, gs, fs (registros de segmentos de dados de extensão).

6. Registros de status e controle

O microprocessador inclui vários registradores que contêm constantemente informações sobre o estado do próprio microprocessador e do programa cujas instruções estão atualmente carregadas no pipeline. Esses registros incluem:

1) bandeiras/bandeiras de registro de bandeiras;

2) registro de ponteiro de comando eip/ip.

Usando esses registradores, você pode obter informações sobre os resultados da execução do comando e influenciar o estado do próprio microprocessador. Vamos considerar com mais detalhes a finalidade e o conteúdo desses registros.

1. eflags/flags (registro de bandeira) - registro de bandeira. A profundidade de bits de eflags/flags é de 32/16 bits. Bits individuais deste registrador têm um propósito funcional específico e são chamados de sinalizadores. A parte inferior deste registrador é exatamente igual ao registrador flags para 18086. A Figura 3 mostra o conteúdo do registrador eflags.

Arroz. 3. O conteúdo do registro de eflags

Dependendo de como são utilizadas, as bandeiras do registro eflags/flags podem ser divididas em três grupos:

1) oito sinalizadores de status.

Esses sinalizadores podem mudar após a execução das instruções de máquina. Os flags de status do registrador eflags refletem as especificidades do resultado da execução de operações aritméticas ou lógicas. Isso torna possível analisar o estado do processo computacional e responder a ele usando comandos de salto condicionais e chamadas de sub-rotinas. A Tabela 1 lista os sinalizadores de status e sua finalidade.

2) uma bandeira de controle.

Denotado df (Sinalizador de Diretório). Ele está localizado no bit 10 do registrador eflags e é usado por comandos encadeados. O valor do sinalizador df determina a direção do processamento elemento a elemento nestas operações: do início da string ao final (df = 0) ou vice-versa, do final da string ao seu início (df = 1). Existem comandos especiais para trabalhar com o sinalizador df: eld (remover o sinalizador df) e std (definir o sinalizador df). O uso desses comandos permite ajustar o sinalizador df de acordo com o algoritmo e garantir que os contadores sejam incrementados ou decrementados automaticamente ao realizar operações em strings.

3) cinco sinalizadores do sistema.

Controla E/S, interrupções mascaráveis, depuração, alternância de tarefas e modo virtual 8086. Não é recomendado que programas aplicativos modifiquem esses sinalizadores desnecessariamente, pois isso fará com que o programa seja encerrado na maioria dos casos. A Tabela 2 lista os sinalizadores do sistema e sua finalidade.

Tabela 1. Sinalizadores de statusTabela 2. Sinalizadores do sistema

2. eip/ip (registrador de ponteiro de instrução) - registrador de ponteiro de instrução. O registrador eip/ip tem 32/16 bits de largura e contém o deslocamento da próxima instrução a ser executada em relação ao conteúdo do registrador de segmento cs no segmento de instrução atual. Esse registrador não é acessível diretamente ao programador, mas seu valor é carregado e alterado por vários comandos de controle, que incluem comandos para saltos condicionais e incondicionais, chamada de procedimentos e retorno de procedimentos. A ocorrência de interrupções também modifica o registrador eip/ip.

PALESTRA Nº 15. Registros

1. Registros do sistema microprocessado

O próprio nome desses registradores sugere que eles desempenham funções específicas no sistema. O uso de registros do sistema é estritamente regulamentado. São eles que fornecem o modo protegido. Eles também podem ser considerados como parte da arquitetura do microprocessador, que é deliberadamente deixado visível para que um programador de sistema qualificado possa executar as operações de nível mais baixo.

Os registradores do sistema podem ser divididos em três grupos:

1) quatro registros de controle;

2) quatro registros de endereços do sistema;

3) oito registradores de depuração.

2. Registros de controle

O grupo de registros de controle inclui quatro registros: cr0, cr1, cr2, cr3. Esses registradores são para controle geral do sistema. Os registros de controle estão disponíveis apenas para programas com nível de privilégio 0.

Embora o microprocessador tenha quatro registros de controle, apenas três deles estão disponíveis - cr1 é excluído, cujas funções ainda não estão definidas (está reservada para uso futuro).

O registrador cr0 contém sinalizadores do sistema que controlam os modos de operação do microprocessador e refletem seu estado globalmente, independentemente das tarefas específicas que estão sendo executadas.

Objetivo dos sinalizadores do sistema:

1) pe (Protect Enable), bit 0 - habilita o modo de operação protegido. O estado desse sinalizador mostra em qual dos dois modos - real (pe = 0) ou protegido (pe = 1) - o microprocessador está operando em um determinado momento;

2) mp (Math Present), bit 1 - a presença de um coprocessador. Sempre 1;

3) ts (Task Switched), bit 3 - alternância de tarefas. O processador define esse bit automaticamente quando alterna para outra tarefa;

4) am (Máscara de Alinhamento), bit 18 - máscara de alinhamento. Este bit habilita (am = 1) ou desabilita (am = 0) o controle de alinhamento;

5) cd (Cache Disable), bit 30 - desabilita a memória cache.

Usando este bit, você pode desabilitar (cd =1) ou habilitar (cd = 0) o uso do cache interno (o cache de primeiro nível);

6) pg (PaGing), bit 31 - habilitar (pg =1) ou desabilitar (pg = 0) paginação.

O sinalizador é usado no modelo de paginação de organização de memória.

O registrador cr2 é usado na paginação da RAM para registrar a situação em que a instrução atual acessou o endereço contido em uma página de memória que atualmente não está na memória.

Em tal situação, uma exceção número 14 ocorre no microprocessador e o endereço linear de 32 bits da instrução que causou essa exceção é escrito no registrador cr2. Com esta informação, o manipulador de exceção 14 determina a página desejada, troca-a na memória e retoma a operação normal do programa;

O registrador cr3 também é usado para paginar a memória. Este é o chamado registro de diretório de páginas de primeiro nível. Ele contém o endereço base físico de 20 bits do diretório de páginas da tarefa atual. Este diretório contém 1024 descritores de 32 bits, cada um dos quais contém o endereço da tabela de páginas de segundo nível. Por sua vez, cada uma das tabelas de página de segundo nível contém 1024 descritores de 32 bits que endereçam quadros de página na memória. O tamanho do quadro de página é de 4 KB.

3. Registros de endereços do sistema

Esses registradores também são chamados de registradores de gerenciamento de memória.

Eles são projetados para proteger programas e dados no modo multitarefa do microprocessador. Ao operar no modo protegido por microprocessador, o espaço de endereço é dividido em:

1) global - comum a todas as tarefas;

2) local - separado para cada tarefa.

Essa separação explica a presença dos seguintes registros de sistema na arquitetura do microprocessador:

1) o registrador da tabela global de descritores gdtr (Global Descriptor Table Register), com tamanho de 48 bits e contendo um endereço base de 32 bits (bits 16-47) da tabela global de descritores GDT e um endereço base de 16 bits (bits 0-15) XNUMX-XNUMX) valor limite, que é o tamanho em bytes da tabela GDT;

2) o registrador de tabela de descritores locais ldtr (Local Descriptor Table Register), com tamanho de 16 bits e contendo o chamado seletor do descritor da tabela de descritores locais LDT Este seletor é um ponteiro na tabela GDT, que descreve o segmento contendo a tabela de descritores locais LDT;

3) o registro da tabela de descritores de interrupção idtr (Interrupt Descriptor Table Register), tendo um tamanho de 48 bits e contendo um endereço base de 32 bits (bits 16-47) da tabela de descritores de interrupção IDT e um endereço de 16 bits (bits 0-15) valor limite, que é o tamanho em bytes da tabela IDT;

4) registrador de tarefa de 16 bits tr (Task Register), que, como o registrador ldtr, contém um seletor, ou seja, um ponteiro para um descritor na tabela GDT. Este descritor descreve o atual Task Segment Status (TSS). Este segmento é criado para cada tarefa no sistema, possui uma estrutura estritamente regulada e contém o contexto (estado atual) da tarefa. O principal objetivo dos segmentos TSS é salvar o estado atual de uma tarefa no momento de alternar para outra tarefa.

4. ​​Registros de depuração

Este é um grupo muito interessante de registradores destinados à depuração de hardware. As ferramentas de depuração de hardware apareceram pela primeira vez no microprocessador i486. Em hardware, o microprocessador contém oito registradores de depuração, mas apenas seis deles são realmente usados.

Os registros dr0, dr1, dr2, dr3 têm largura de 32 bits e são projetados para definir endereços lineares de quatro pontos de interrupção. O mecanismo utilizado neste caso é o seguinte: qualquer endereço gerado pelo programa atual é comparado com os endereços dos registradores dr0... dr3, e se houver correspondência, é gerada uma exceção de depuração com número 1.

O registro dr6 é chamado de registro de status de depuração. Os bits neste registro são definidos de acordo com os motivos que causaram a ocorrência da última exceção número 1.

Listamos esses bits e sua finalidade:

1) b0 - se este bit estiver em 1, então a última exceção (interrupção) ocorreu como resultado de atingir o checkpoint definido no registro dr0;

2) b1 - semelhante a b0, mas para um checkpoint no registro dr1;

3) b2 - semelhante a b0, mas para um checkpoint no registro dr2;

4) bЗ - semelhante a b0, mas para um checkpoint no registro dr3;

5) bd (bit 13) - serve para proteger os registradores de depuração;

6) bs (bit 14) - definido como 1 se a exceção 1 foi causada pelo estado da flag tf = 1 no registrador eflags;

7) bt (bit 15) é definido como 1 se a exceção 1 foi causada por uma mudança para uma tarefa com o bit trap definido em TSS t = 1.

Todos os outros bits neste registrador são preenchidos com zeros. O manipulador de exceção 1, com base no conteúdo de dr6, deve determinar o motivo da exceção e executar as ações necessárias.

O registro dr7 é chamado de registro de controle de depuração. Ele contém campos para cada um dos quatro registradores de ponto de interrupção de depuração que permitem especificar as seguintes condições sob as quais uma interrupção deve ser gerada:

1) local de registro do checkpoint - apenas na tarefa atual ou em qualquer tarefa. Esses bits ocupam os 8 bits inferiores do registro dr7 (2 bits para cada ponto de interrupção (na verdade, um ponto de interrupção) definido pelos registros dr0, dr1, dr2, dr3, respectivamente).

O primeiro bit de cada par é a chamada resolução local; configurá-lo informa ao ponto de interrupção para entrar em vigor se estiver dentro do espaço de endereço da tarefa atual.

O segundo bit em cada par define a permissão global, que indica que o ponto de interrupção fornecido é válido dentro dos espaços de endereço de todas as tarefas que residem no sistema;

2) o tipo de acesso pelo qual a interrupção é iniciada: somente ao buscar um comando, ao escrever ou ao escrever/ler dados. Os bits que determinam essa natureza da ocorrência de uma interrupção estão localizados na parte superior desse registro. A maioria dos registros do sistema é acessível por meio de programação.

PALESTRA Nº 16. Programas de Montagem

1. A estrutura do programa em assembler

Um programa em linguagem assembly é uma coleção de blocos de memória chamados segmentos de memória. Um programa pode consistir em um ou mais desses segmentos de blocos. Cada segmento contém uma coleção de sentenças de linguagem, cada uma das quais ocupa uma linha separada de código de programa.

As instruções de montagem são de quatro tipos:

1) comandos ou instruções, que são análogos simbólicos de comandos de máquina. Durante o processo de tradução, as instruções de montagem são convertidas nos comandos correspondentes do conjunto de instruções do microprocessador;

2) macros. São frases do texto do programa que são formalizadas de certa forma e são substituídas por outras frases durante a transmissão;

3) diretivas, que são uma indicação ao tradutor do montador para realizar determinadas ações. As diretivas não têm contrapartida na representação da máquina;

4) linhas de comentários contendo quaisquer caracteres, incluindo letras do alfabeto russo. Comentários são ignorados pelo tradutor.

2. Sintaxe de Montagem

As frases que compõem um programa podem ser uma construção sintática correspondente a um comando, macro, diretiva ou comentário. Para que o tradutor montador os reconheça, eles devem ser formados de acordo com certas regras sintáticas. Para fazer isso, é melhor usar uma descrição formal da sintaxe da linguagem, como as regras da gramática. As maneiras mais comuns de descrever uma linguagem de programação dessa maneira são diagramas de sintaxe e formulários Backus-Naur estendidos. Para uso prático, os diagramas de sintaxe são mais convenientes. Por exemplo, a sintaxe das instruções da linguagem assembly pode ser descrita usando os diagramas de sintaxe mostrados nas figuras a seguir.

Arroz. 4. Formato de frase de montagem

Arroz. 5. Formato da Diretiva

Arroz. 6. Formato de comandos e macros

Nesses desenhos:

1) nome do rótulo - um identificador, cujo valor é o endereço do primeiro byte da frase do código-fonte do programa que ele denota;

2) nome - um identificador que distingue esta diretiva de outras diretivas de mesmo nome. Como resultado do processamento pelo montador de uma determinada diretiva, certas características podem ser atribuídas a esse nome;

3) um código de operação (COP) e uma diretiva são designações mnemônicas da instrução de máquina, macroinstrução ou diretiva tradutora correspondente;

4) operandos - partes do comando, macro ou diretivas do montador, denotando objetos nos quais as operações são executadas. Os operandos Assembler são descritos por expressões com constantes numéricas e de texto, rótulos de variáveis ​​e identificadores usando sinais de operador e algumas palavras reservadas.

Como usar diagramas de sintaxe? É muito simples: tudo o que você precisa fazer é encontrar e seguir o caminho da entrada do diagrama (esquerda) até a saída (direita). Se tal caminho existir, então a sentença ou construção está sintaticamente correta. Se não houver esse caminho, o compilador não aceitará essa construção. Ao trabalhar com diagramas de sintaxe, preste atenção na direção da travessia indicada pelas setas, pois entre os caminhos pode haver aqueles que podem ser seguidos da direita para a esquerda. De fato, os diagramas sintáticos refletem a lógica do tradutor ao analisar as sentenças de entrada do programa.

Os caracteres permitidos ao escrever o texto dos programas são:

1) todas as letras latinas: A - Z, a - z. Nesse caso, letras maiúsculas e minúsculas são consideradas equivalentes;

2) números de 0 a 9;

3) sinais ?, @, S, _, &;

4) separadores.

As frases de montagem são formadas a partir de lexemas, que são sequências sintaticamente inseparáveis ​​de símbolos de linguagem válidos que fazem sentido para o tradutor.

Os tokens são os seguintes.

1. Identificadores são sequências de caracteres válidos usados ​​para designar objetos de programa, como códigos de operação, nomes de variáveis ​​e nomes de rótulos. A regra para escrever identificadores é a seguinte: um identificador pode consistir em um ou mais caracteres. Como caracteres, você pode usar letras do alfabeto latino, números e alguns caracteres especiais - _, ?, $, @. Um identificador não pode começar com um caractere de dígito. O comprimento do identificador pode ser de até 255 caracteres, embora o tradutor aceite apenas os primeiros 32 e ignore o restante. Você pode ajustar o comprimento dos identificadores possíveis usando a opção de linha de comando mv. Além disso, é possível dizer ao tradutor para distinguir entre letras maiúsculas e minúsculas ou ignorar sua diferença (o que é feito por padrão). As opções de linha de comando /mu, /ml, /mx são usadas para isso.

2. Cadeias de caracteres - sequências de caracteres entre aspas simples ou duplas.

3. Inteiros em um dos seguintes sistemas numéricos: binário, decimal, hexadecimal. A identificação de números ao escrevê-los em programas assembler é realizada de acordo com certas regras:

1) os números decimais não exigem a identificação de caracteres adicionais, por exemplo, 25 ou 139;

2) para identificar os números binários no texto fonte do programa, é necessário colocar o "b" latino depois de escrever os zeros e uns que os compõem, por exemplo, 10010101 b;

3) Os números hexadecimais têm mais convenções ao escrever:

a) em primeiro lugar, são constituídos por números 0...9, letras minúsculas e maiúsculas do alfabeto latino a, b, c, d, e, Gili D B, C, D, E, E

b) em segundo lugar, o tradutor pode ter dificuldade em reconhecer números hexadecimais devido ao facto de estes poderem consistir apenas em dígitos 0...9 (por exemplo, 190845) ou começar com uma letra do alfabeto latino (por exemplo, efl5). Para “explicar” ao tradutor que um determinado token não é um número decimal ou um identificador, o programador deve destacar o número hexadecimal de uma forma especial. Para fazer isso, escreva a letra latina “h” no final da sequência de dígitos hexadecimais que compõem um número hexadecimal. Isso é um dever. Se um número hexadecimal começar com uma letra, um zero à esquerda será escrito antes dele: 0 efl5 h.

Assim, descobrimos como as sentenças de um programa montador são construídas. Mas esta é apenas a visão mais superficial.

Quase todas as frases contêm uma descrição do objeto sobre o qual ou com a ajuda de alguma ação é realizada. Esses objetos são chamados de operandos. Eles podem ser definidos da seguinte forma: operandos são objetos (alguns valores, registradores ou posições de memória) que são afetados por instruções ou diretivas, ou são objetos que definem ou refinam a ação de instruções ou diretivas.

Operandos podem ser combinados com operadores aritméticos, lógicos, bit a bit e de atributo para calcular algum valor ou determinar um local de memória que será afetado por um determinado comando ou diretiva.

Vamos considerar com mais detalhes as características dos operandos na seguinte classificação:

1) operandos constantes ou imediatos - um número, string, nome ou expressão que possui algum valor fixo. O nome não deve ser relocável, ou seja, não deve depender do endereço do programa a ser carregado na memória. Por exemplo, pode ser definido com os operadores igual ou =;

2) operandos de endereço, defina a localização física do operando na memória especificando dois componentes do endereço: segmento e deslocamento (Fig. 7);

Arroz. 7. Sintaxe de descrição dos operandos de endereço

3) operandos relocáveis ​​- quaisquer nomes simbólicos representando alguns endereços de memória. Esses endereços podem indicar a localização de memória de algumas instruções (se o operando for um rótulo) ou dados (se o operando for o nome de uma localização de memória no segmento de dados).

Os operandos realocáveis ​​diferem dos operandos de endereço, pois não estão vinculados a um endereço de memória física específico. O componente de segmento do endereço do operando que está sendo movido é desconhecido e será determinado após o programa ser carregado na memória para execução.

O contador de endereços é um tipo específico de operando. Ele é denotado pelo sinal S. A especificidade deste operando é que quando o tradutor montador encontra este símbolo no programa fonte, ele substitui o valor atual do contador de endereços. O valor do contador de endereços, ou contador de posicionamento, como às vezes é chamado, é o deslocamento da instrução de máquina atual desde o início do segmento de código. No formato de listagem, a segunda ou terceira coluna corresponde ao contador de endereços (dependendo se a coluna com o nível de aninhamento está ou não presente na listagem). Se tomarmos qualquer listagem como exemplo, fica claro que quando o tradutor processa a próxima instrução do montador, o contador de endereços aumenta o comprimento da instrução de máquina gerada. É importante compreender este ponto corretamente. Por exemplo, o processamento de diretivas do assembler não altera o contador. As diretivas, ao contrário dos comandos do montador, são apenas instruções para o compilador executar determinadas ações para formar a representação da máquina do programa, e para elas o compilador não gera nenhuma construção na memória.

Ao usar tal expressão para saltar, fique atento ao comprimento da própria instrução na qual esta expressão é utilizada, pois o valor do contador de endereços corresponde ao deslocamento no segmento de instrução desta instrução, e não da instrução que a segue. . Em nosso exemplo, o comando jmp leva 2 bytes. Mas tenha cuidado, o comprimento de uma instrução depende de quais operandos ela usa. Uma instrução com operandos de registrador será menor que uma instrução com um de seus operandos localizado na memória. Na maioria dos casos, esta informação pode ser obtida conhecendo o formato da instrução de máquina e analisando a coluna de listagem com o código objeto da instrução;

4) operando de registro é apenas um nome de registro. Em um programa montador, você pode usar os nomes de todos os registradores de uso geral e da maioria dos registradores do sistema;

5) operandos base e índice. Este tipo de operando é usado para implementar a base indireta, endereçamento de índice indireto ou combinações e extensões dos mesmos;

6) Operandos estruturais são usados ​​para acessar um elemento específico de um tipo de dado complexo chamado estrutura.

Registros (semelhantes a um tipo struct) são usados ​​para acessar um campo de bits de algum registro.

Operandos são componentes elementares que fazem parte de uma instrução de máquina, denotando os objetos nos quais a operação é executada. Em um caso mais geral, os operandos podem ser incluídos como componentes em formações mais complexas chamadas expressões. Expressões são combinações de operandos e operadores considerados como um todo. O resultado da avaliação da expressão pode ser o endereço de alguma célula de memória ou algum valor constante (absoluto).

Já consideramos os tipos possíveis de operandos. Agora listamos os possíveis tipos de operadores de montador e as regras sintáticas para a formação de expressões de montador, e damos uma breve descrição dos operadores.

1. Operadores aritméticos. Esses incluem:

1) unário "+" e "-";

2) binário "+" e "-";

3) multiplicação "*";

4) divisão inteira "/";

5) obtendo o resto da divisão "mod".

Esses operadores estão localizados nos níveis de precedência 6,7,8 na Tabela 4.

Arroz. 8. Sintaxe das operações aritméticas

2. Os operadores de deslocamento deslocam a expressão pelo número especificado de bits (Fig. 9).

Arroz. 9. Sintaxe dos operadores de turno

3. Os operadores de comparação (retornam o valor "true" ou "false") destinam-se à formação de expressões lógicas (Fig. 10 e Tabela 3). O valor lógico "true" corresponde a uma unidade digital e "false" - a zero.

Arroz. 10. Sintaxe dos operadores de comparação

Tabela 3. Operadores de comparação

4. Os operadores lógicos realizam operações bit a bit em expressões (Fig. 11). As expressões devem ser absolutas, ou seja, tais, cujo valor numérico possa ser calculado pelo tradutor.

Arroz. 11. Sintaxe dos operadores lógicos

5. Operador de índice []. Os parênteses também são um operador, e o tradutor percebe sua presença como uma instrução para adicionar o valor de expression_1 atrás desses colchetes com expression_2 entre colchetes (Fig. 12).

Arroz. 12. Sintaxe do operador de índice

Observe que a seguinte designação é adotada na literatura sobre assembler: quando o texto se refere ao conteúdo de um registrador, seu nome é tomado entre parênteses. Também vamos aderir a esta notação.

6. O operador de redefinição de tipo ptr é usado para redefinir ou qualificar o tipo de um rótulo ou variável definida por uma expressão (Fig. 13).

O tipo pode ter um dos seguintes valores: byte, word, dword, qword, tbyte, near, far.

Arroz. 13. Sintaxe do operador de redefinição de tipo

7. O operador de redefinição de segmento ":" (dois pontos) força o cálculo de um endereço físico relativo a um componente de segmento especificamente especificado: "segment register name", "segment name" da diretiva SEGMENT correspondente ou "group name" (Fig. . 14). Ao discutir a segmentação, falamos sobre o fato de que o microprocessador no nível de hardware suporta três tipos de segmentos - código, pilha e dados. O que é esse suporte de hardware? Por exemplo, para selecionar a execução do próximo comando, o microprocessador deve necessariamente olhar o conteúdo do registrador de segmento cs e somente ele. E esse registrador, como sabemos, contém o endereço físico (ainda não deslocado) do início do segmento de instrução. Para obter o endereço de uma instrução específica, o microprocessador precisa multiplicar o conteúdo de cs por 16 (o que significa um deslocamento de quatro bits) e adicionar o valor de 20 bits resultante ao conteúdo de 16 bits do registrador ip. Aproximadamente a mesma coisa acontece quando o microprocessador processa os operandos na instrução de máquina. Se ele vê que o operando é um endereço (um endereço efetivo que é apenas parte do endereço físico), então ele sabe em qual segmento procurá-lo - por padrão, é o segmento cujo endereço inicial está armazenado no registrador de segmento ds.

Mas e o segmento de pilha? No contexto de nossa consideração, estamos interessados ​​nos registradores sp e bp. Se o microprocessador vê um desses registradores como um operando (ou parte dele, se o operando for uma expressão), então, por padrão, ele forma o endereço físico do operando usando o conteúdo do registrador ss como seu componente de segmento. Este é um conjunto de microprogramas na unidade de controle de microprogramas, cada um dos quais executa uma das instruções no sistema de instruções de máquina do microprocessador. Cada microprograma funciona de acordo com seu próprio algoritmo. Claro, você não pode alterá-lo, mas você pode corrigi-lo ligeiramente. Isso é feito usando o campo opcional de prefixo de comando de máquina. Se concordarmos em como o comando funciona, esse campo está ausente. Se quisermos fazer uma alteração (se, é claro, for permitido para um comando específico) no algoritmo do comando, é necessário formar um prefixo apropriado.

Um prefixo é um valor de um byte cujo valor numérico determina sua finalidade. O microprocessador reconhece pelo valor especificado que este byte é um prefixo e o trabalho adicional do microprograma é realizado levando em consideração a instrução recebida para corrigir seu trabalho. Agora estamos interessados ​​em um deles - o prefixo de substituição (redefinição) de segmento. Sua finalidade é indicar ao microprocessador (e, de fato, ao firmware) que não queremos usar o segmento padrão. As possibilidades para tal redefinição são, obviamente, limitadas. O segmento de comando não pode ser redefinido, o endereço do próximo comando executável é determinado exclusivamente pelo par cs:ip. E aqui segmentos de uma pilha e dados - é possível. É para isso que serve o operador ":". O tradutor montador, processando essa instrução, gera o prefixo de substituição de segmento de um byte correspondente.

Arroz. 14. Sintaxe do operador de redefinição de segmento

8. O operador de nomenclatura de tipo de estrutura "."(ponto) também força o compilador a realizar determinados cálculos se ocorrer em uma expressão.

9. O operador para obter o componente de segmento da expressão endereço seg retorna o endereço físico do segmento para a expressão (Fig. 15), que pode ser um rótulo, variável, nome de segmento, nome de grupo ou algum nome simbólico.

Arroz. 15. Sintaxe do operador de recebimento do componente de segmento

10. O operador para obter o deslocamento da expressão deslocamento permite obter o valor do deslocamento da expressão (Fig. 16) em bytes em relação ao início do segmento em que a expressão está definida.

Arroz. 16. Sintaxe do operador get de deslocamento

Assim como nas linguagens de alto nível, a execução de operadores assembler na avaliação de expressões é realizada de acordo com suas prioridades (Tabela 4). As operações com a mesma prioridade são executadas sequencialmente da esquerda para a direita. A alteração da ordem de execução é possível colocando parênteses com a precedência mais alta.

Tabela 4. Operadores e sua precedência

3. Diretrizes de segmentação

No decorrer da discussão anterior, descobrimos todas as regras básicas para escrever instruções e operandos em um programa em linguagem assembly. A questão de como formatar corretamente a sequência de comandos para que o tradutor possa processá-los e o microprocessador possa executá-los permanece em aberto.

Ao considerar a arquitetura do microprocessador, aprendemos que ele possui seis registradores de segmento, através dos quais pode trabalhar simultaneamente:

1) com um segmento de código;

2) com um segmento de pilha;

3) com um segmento de dados;

4) com três segmentos de dados adicionais.

Lembre-se mais uma vez que um segmento é fisicamente uma área de memória ocupada por comandos e (ou) dados cujos endereços são calculados em relação ao valor no registrador de segmento correspondente.

A descrição sintática de um segmento em assembler é a construção mostrada na Figura 17:

Arroz. 17. Sintaxe de descrição do segmento

É importante observar que a funcionalidade de um segmento é um pouco mais ampla do que simplesmente dividir o programa em blocos de código, dados e pilha. A segmentação faz parte de um mecanismo mais geral relacionado ao conceito de programação modular. Envolve a unificação do design dos módulos de objetos criados pelo compilador, incluindo aqueles de diferentes linguagens de programação. Isso permite combinar programas escritos em diferentes idiomas. É para a implementação de várias opções para tal união que se destinam os operandos da diretiva SEGMENT.

Vamos considerá-los em mais detalhes.

1. O atributo de alinhamento de segmento (tipo de alinhamento) informa ao compositor para garantir que o início do segmento seja colocado no limite especificado. Isso é importante porque o alinhamento adequado torna o acesso aos dados mais rápido em processadores i80x86. Os valores válidos para este atributo são os seguintes:

1) BYTE - o alinhamento não é realizado. Um segmento pode iniciar em qualquer endereço de memória;

2) WORD - o segmento inicia em um endereço que é múltiplo de dois, ou seja, o último (menos significativo) bit do endereço físico é 0 (alinhado ao limite da palavra);

3) DWORD - o segmento inicia em um endereço múltiplo de quatro, ou seja, os dois últimos bits (menos significativos) são 0 (alinhados a um limite de palavra dupla);

4) PARA - o segmento inicia em um endereço múltiplo de 16, ou seja, o último dígito hexadecimal do endereço deve ser Oh (alinhamento ao limite do parágrafo);

5) PAGE - o segmento inicia em um endereço múltiplo de 256, ou seja, os dois últimos dígitos hexadecimais devem ser 00h (alinhados ao limite de uma página de 256 bytes);

6) MEMPAGE - o segmento inicia em um endereço múltiplo de 4 KB, ou seja, os três últimos dígitos hexadecimais devem ser OOOh (o endereço da próxima página de memória de 4 KB). O tipo de alinhamento padrão é PARA.

2. O atributo combine segment (tipo combinatório) informa ao vinculador como combinar segmentos de módulos diferentes que têm o mesmo nome. Os valores de atributo de combinação de segmento podem ser:

1) PRIVADO - o segmento não será combinado com outros segmentos de mesmo nome fora deste módulo;

2) PÚBLICO - faz com que o vinculador conecte todos os segmentos com o mesmo nome. O novo segmento mesclado será completo e contínuo. Todos os endereços (offsets) dos objetos, e isso pode depender do tipo de comando e segmento de dados, serão calculados em relação ao início deste novo segmento;

3) COMUM - coloca todos os segmentos com o mesmo nome no mesmo endereço. Todos os segmentos com o nome dado irão se sobrepor e compartilhar memória. O tamanho do segmento resultante será igual ao tamanho do maior segmento;

4) AT xxxx - localiza o segmento no endereço absoluto do parágrafo (parágrafo é a quantidade de memória, um múltiplo de 16; portanto, o último dígito hexadecimal do endereço do parágrafo é 0). O endereço absoluto de um parágrafo é dado por xxx. O linker coloca o segmento em um determinado endereço de memória (isso pode ser usado, por exemplo, para acessar a memória de vídeo ou uma área ROM>), dado o atributo combine. Fisicamente, isso significa que o segmento, quando carregado na memória, estará localizado a partir desse endereço absoluto do parágrafo, mas para acessá-lo, o valor especificado no atributo deve ser carregado no registrador de segmento correspondente. Todos os rótulos e endereços em um segmento assim definido são relativos ao endereço absoluto dado;

5) PILHA - definição de um segmento de pilha. Faz com que o vinculador conecte todos os segmentos com o mesmo nome e calcule os endereços nesses segmentos em relação ao registrador ss. O tipo combinado STACK (pilha) é semelhante ao tipo combinado PUBLIC, exceto que o registrador ss é o registrador de segmento padrão para segmentos de pilha. O registrador sp é definido para o final do segmento de pilha concatenado. Se nenhum segmento de pilha for especificado, o vinculador emitirá um aviso de que nenhum segmento de pilha foi encontrado. Se um segmento de pilha foi criado e o tipo STACK combinado não é usado, o programador deve carregar explicitamente o endereço do segmento no registrador ss (semelhante ao registrador ds).

O atributo de combinação é padronizado como PRIVATE.

3. Um atributo de classe de segmento (tipo de classe) é uma string entre aspas que ajuda o vinculador a determinar a ordem de segmento apropriada ao montar um programa a partir de vários segmentos de módulo. O vinculador mescla na memória todos os segmentos com o mesmo nome de classe (o nome da classe geralmente pode ser qualquer coisa, mas é melhor se refletir a funcionalidade do segmento). Um uso típico de um nome de classe é agrupar todos os segmentos de código de um programa (geralmente a classe "código" é usada para isso). Usando o mecanismo de tipagem de classe, você também pode agrupar segmentos de dados inicializados e não inicializados.

4. Atributo de tamanho do segmento. Para processadores i80386 e superiores, os segmentos podem ser de 16 bits ou 32 bits. Isso afeta principalmente o tamanho do segmento e a ordem em que o endereço físico é formado dentro dele. O atributo pode ter os seguintes valores:

1) USE16 - significa que o segmento permite endereçamento de 16 bits. Ao formar um endereço físico, apenas um deslocamento de 16 bits pode ser usado. Assim, tal segmento pode conter até 64 KB de código ou dados;

2)USE32 - o segmento será de 32 bits. Ao formar um endereço físico, um deslocamento de 32 bits pode ser usado. Portanto, esse segmento pode conter até 4 GB de código ou dados.

Todos os segmentos são iguais em si, pois as diretivas SEGMENT e ENDS não contêm informações sobre a finalidade funcional dos segmentos. Para usá-los como segmentos de código, dados ou pilha, você deve primeiro informar o tradutor sobre isso, para o qual é usada uma diretiva especial ASSUME, que tem o formato mostrado na Fig. 18. Esta diretiva informa ao tradutor qual segmento está vinculado a qual registro de segmento. Por sua vez, isso permitirá que o tradutor vincule corretamente os nomes simbólicos definidos nos segmentos. A vinculação de segmentos a registradores de segmento é realizada utilizando os operandos desta diretiva, em que o nome_do_segmento deve ser o nome do segmento, definido no texto fonte do programa pela diretiva SEGMENT ou pela palavra-chave nothing. Se apenas a palavra-chave nothing for usada como operando, as atribuições de registradores de segmento anteriores serão canceladas e para todos os seis registradores de segmento de uma só vez. Mas a palavra-chave nada pode ser usada em vez do argumento do nome do segmento; neste caso, a conexão entre o segmento com o nome do segmento nome e o registrador de segmento correspondente será quebrada seletivamente (veja a Fig. 18).

Arroz. 18. ASSUMIR Diretiva

Para programas simples contendo um segmento para código, dados e pilha, gostaríamos de simplificar sua descrição. Para fazer isso, os tradutores MASM e TASM introduziram a capacidade de usar diretivas de segmentação simplificadas. Mas aqui surgiu um problema relacionado ao fato de que era necessário compensar de alguma forma a incapacidade de controlar diretamente a colocação e a combinação de segmentos. Para isso, juntamente com as diretivas simplificadas de segmentação, passaram a utilizar a diretiva para especificação do modelo de memória MODEL, que passou a controlar parcialmente o posicionamento dos segmentos e executar as funções da diretiva ASSUME (portanto, ao utilizar diretivas simplificadas de segmentação, o A diretiva ASSUME pode ser omitida). Esta diretiva vincula segmentos, que no caso de usar diretivas de segmentação simplificadas, têm nomes predefinidos, com registradores de segmento (embora você ainda precise inicializar ds explicitamente).

A sintaxe da diretiva MODEL é mostrada na Figura 19.

Arroz. 19. Sintaxe da diretiva MODEL

O parâmetro obrigatório da diretiva MODEL é o modelo de memória. Este parâmetro define o modelo de segmentação de memória para a POU. Supõe-se que um módulo de programa pode ter apenas certos tipos de segmentos, que são definidos pelas diretivas simplificadas de descrição de segmento que mencionamos anteriormente. Essas diretivas são mostradas na Tabela 5.

Tabela 5. Diretivas de definição de segmento simplificadas

A presença do parâmetro [nome] em algumas diretivas indica que é possível definir vários segmentos desse tipo. Por outro lado, a existência de vários tipos de segmentos de dados deve-se à necessidade de garantir a compatibilidade com alguns compiladores de linguagens de alto nível, que criam diferentes segmentos de dados para dados inicializados e não inicializados, bem como constantes.

Ao utilizar a diretiva MODEL, o tradutor disponibiliza diversos identificadores que podem ser acessados ​​durante a operação do programa para obter informações sobre determinadas características de um determinado modelo de memória (Tabela 7). Vamos listar esses identificadores e seus valores (Tabela 6).

Tabela 6. Identificadores criados pela diretiva MODEL

Agora podemos terminar de discutir a diretiva MODEL. Os operandos da diretiva MODEL são usados ​​para especificar um modelo de memória que define o conjunto de segmentos de programa, os tamanhos dos segmentos de dados e de código e o método de vinculação de segmentos e registradores de segmento. A Tabela 7 mostra alguns valores do parâmetro "memory model" da diretiva MODEL.

Tabela 7. Modelos de Memória

O parâmetro "modifier" da diretiva MODEL permite esclarecer algumas características do uso do modelo de memória selecionado (Tabela 8).

Tabela 8. Modificadores de modelo de memória

Os parâmetros opcionais "language" e "language modifier" definem alguns recursos das chamadas de procedimento. A necessidade de usar esses parâmetros surge ao escrever e vincular programas em várias linguagens de programação.

As diretivas de segmentação padrão e simplificada que descrevemos não são mutuamente exclusivas. As diretivas padrão são usadas quando o programador deseja ter controle total sobre a colocação de segmentos na memória e sua combinação com segmentos de outros módulos.

As diretivas simplificadas são úteis para programas simples e programas destinados a serem vinculados a módulos de programa escritos em linguagens de alto nível. Isso permite que o vinculador vincule módulos de diferentes idiomas de maneira eficiente, padronizando a vinculação e o gerenciamento.

PALESTRA Nº 17. Estruturas de comando no Assembler

1. Estrutura de instrução da máquina

Um comando de máquina é uma indicação ao microprocessador, codificado de acordo com certas regras, para realizar alguma operação ou ação. Cada comando contém elementos que definem:

1) o que fazer? (A resposta a esta pergunta é dada pelo elemento de comando chamado código de operação (COP).);

2) objetos sobre os quais algo precisa ser feito (esses elementos são chamados de operandos);

3) como fazer? (Esses elementos são chamados de tipos de operando e geralmente são implícitos.)

O formato de instrução de máquina mostrado na Figura 20 é o mais geral. O comprimento máximo de uma instrução de máquina é de 15 bytes. Um comando real pode conter um número muito menor de campos, até um - apenas COP.

Arroz. 20. Formato de instrução de máquina

Vamos descrever o propósito dos campos de instrução de máquina.

1. Prefixos.

Elementos de instrução de máquina opcionais, cada um dos quais é de 1 byte ou pode ser omitido. Na memória, os prefixos precedem o comando. A finalidade dos prefixos é modificar a operação realizada pelo comando. Um aplicativo pode usar os seguintes tipos de prefixos:

1) prefixo de substituição de segmento. Especifica explicitamente qual registrador de segmento é usado nesta instrução para endereçar a pilha ou os dados. O prefixo substitui a seleção de registro de segmento padrão. Os prefixos de substituição de segmento têm os seguintes significados:

a) 2eh - substituição do segmento cs;

b) 36h - substituição do segmento ss;

c) 3eh - substituição do segmento ds;

d) 26h - substituição do segmento es;

e) 64h - substituição do segmento fs;

e) 65h - substituição do segmento gs;

2) o prefixo de quantidade de bits do endereço especifica a quantidade de bits do endereço (32 ou 16 bits). Cada instrução que usa um operando de endereço recebe a largura de bits do endereço desse operando. Este endereço pode ser de 16 ou 32 bits. Se o comprimento do endereço deste comando for de 16 bits, significa que o comando contém um deslocamento de 16 bits (Fig. 20), corresponde a um deslocamento de 16 bits do operando de endereço relativo ao início de algum segmento. No contexto da Figura 21, esse deslocamento é chamado de endereço efetivo. Se o endereço for de 32 bits, significa que o comando contém um deslocamento de 32 bits (Fig. 20), corresponde ao deslocamento de 32 bits do operando de endereço em relação ao início do segmento, e seu valor forma um deslocamento de 32 bits deslocamento de -bit no segmento. O prefixo de bitness de endereço pode ser usado para alterar o bitness de endereço padrão. Essa alteração afetará apenas o comando precedido pelo prefixo;

Arroz. 21. O mecanismo de formação de um endereço físico em modo real

3) O prefixo da largura do bit do operando é semelhante ao prefixo da largura do bit do endereço, mas indica o comprimento do bit do operando (32 bits ou 16 bits) com o qual a instrução trabalha. Quais são as regras para definir os atributos de endereço e largura de bit do operando por padrão?

No modo real e no modo virtual 18086, os valores desses atributos são 16 bits. No modo protegido, os valores dos atributos dependem do estado do bit D nos descritores de segmentos executáveis. Se D = 0, os valores de atributo padrão são 16 bits; se D = 1, então 32 bits.

Valores de prefixo para largura de operando 66h e largura de endereço 67h. Com o prefixo de bit de endereço de modo real, você pode usar endereçamento de 32 bits, mas esteja ciente do limite de tamanho de segmento de 64 KB. Semelhante ao prefixo de largura de endereço, você pode usar o prefixo de largura de operando de modo real para trabalhar com operandos de 32 bits (por exemplo, em instruções aritméticas);

4) o prefixo de repetição é usado com comandos de cadeia (comandos de processamento de linha). Este prefixo "lança" o comando para processar todos os elementos da cadeia. O sistema de comando suporta dois tipos de prefixos:

a) incondicional (rep - OOh), forçando o comando encadeado a ser repetido um certo número de vezes;

b) condicional (repe/repz - OOh, repne/repnz - 0f2h), que, ao fazer o loop, verifica alguns flags e, como resultado da verificação, é possível a saída antecipada do loop.

2. Código de operação.

Elemento obrigatório que descreve a operação realizada pelo comando. Muitos comandos correspondem a vários códigos de operação, cada um dos quais determina as nuances da operação. Os campos subsequentes da instrução de máquina determinam a localização dos operandos envolvidos na operação e as especificidades de seu uso. A consideração desses campos está ligada às formas de especificar operandos em uma instrução de máquina e, portanto, será realizada posteriormente.

3. Modo de endereçamento byte modr/m.

O valor deste byte determina a forma de endereço do operando utilizada. Os operandos podem estar na memória em um ou dois registradores. Se o operando estiver na memória, o byte modr/m especifica os componentes (registradores de deslocamento, base e índice) usados ​​para calcular seu endereço efetivo (Figura 21). No modo protegido, o byte sib (Scale-Index-Base) pode ser usado adicionalmente para determinar a localização do operando na memória. O byte modr/m consiste em três campos (Fig. 20):

1) o campo mod determina o número de bytes ocupados pelo endereço do operando no comando (Fig. 20, campo offset no comando). O campo mod é usado em conjunto com o campo r/m, que especifica como o endereço do operando "deslocamento de instrução" é modificado. Por exemplo, se mod = 00, isso significa que não há campo de deslocamento no comando, e o endereço do operando é determinado pelo conteúdo do registrador base e (ou) índice. Quais registradores serão usados ​​para calcular o endereço efetivo é determinado pelo valor deste byte. Se mod = 01, significa que o campo offset está presente no comando, ocupa 1 byte e é modificado pelo conteúdo do registro base e (ou) índice. Se mod = 10, isso significa que o campo de deslocamento do comando está presente, ocupa 2 ou 4 bytes (dependendo do tamanho do endereço padrão ou definido pelo prefixo) e é modificado pelo conteúdo do registro base e/ou índice. Se mod = 11, isso significa que não há operandos na memória: eles estão em registradores. O mesmo valor do byte mod é usado quando um operando imediato é usado na instrução;

2) o campo reg/cop determina ou o registrador localizado no comando no lugar do primeiro operando, ou uma possível extensão do opcode;

3) o campo r/m é usado em conjunto com o campo mod e determina o registrador localizado no comando no local do primeiro operando (se mod = 11), ou os registradores base e índice usados ​​para calcular o endereço efetivo (junto com o campo de deslocamento no comando).

4. Escala de bytes - índice - base (byte sib).

Usado para expandir as possibilidades de endereçamento de operandos. A presença do byte sib em uma instrução de máquina é indicada pela combinação de um dos valores 01 ou 10 do campo mod e o valor do campo r/m = 100. O byte sib é composto por três campos:

1) campos de escala ss. Este campo contém o fator de escala para o índice do componente de índice, que ocupa os próximos 3 bits do byte sib. O campo ss pode conter um dos seguintes valores: 1, 2, 4, 8.

Ao calcular o endereço efetivo, o conteúdo do registro de índice será multiplicado por este valor;

2) campos de índice. Usado para armazenar o número do registro de índice que é usado para calcular o endereço efetivo do operando;

3) campos de base. Usado para armazenar o número do registrador base, que também é usado para calcular o endereço efetivo do operando. Quase todos os registradores de uso geral podem ser usados ​​como registradores de base e de índice.

5. Campo de deslocamento no comando.

Um inteiro com sinal de 8, 16 ou 32 bits que representa, no todo ou em parte (sujeito às considerações acima), o valor do endereço efetivo do operando.

6. O campo do operando imediato.

Um campo opcional que é um operando imediato de 8 bits, 16 bits ou 32 bits. A presença deste campo é, obviamente, refletida no valor do byte modr/m.

2. Métodos para especificar operandos de instrução

O operando é definido implicitamente no nível de firmware

Nesse caso, a instrução explicitamente não contém operandos. O algoritmo de execução do comando usa alguns objetos padrão (registros, sinalizadores em eflags, etc.).

Por exemplo, os comandos cli e sti funcionam implicitamente com o sinalizador de interrupção if no registrador eflags, e o comando xlat acessa implicitamente o registrador al e uma linha na memória no endereço especificado pelo par de registradores ds:bx.

O operando é especificado na própria instrução (operando imediato)

O operando está no código de instrução, ou seja, faz parte dele. Para armazenar tal operando em um comando, um campo de até 32 bits é alocado (Figura 20). O operando imediato só pode ser o segundo operando (origem). O operando destino pode estar na memória ou em um registrador.

Por exemplo: mov ax,0ffffti move a constante hexadecimal ffff para o registrador ax. O comando add sum, 2 adiciona o conteúdo do campo na soma do endereço com o inteiro 2 e grava o resultado no local do primeiro operando, ou seja, na memória.

O operando está em um dos registradores

Os operandos de registro são especificados por nomes de registro. Os registros podem ser usados:

1) registradores de 32 bits EAX, EBX, ECX, EDX, ESI, EDI, ESP, EUR;

2) registradores de 16 bits AX, BX, CX, DX, SI, DI, SP, BP;

3) registradores de 8 bits AH, AL, BH, BL, CH, CL, DH, DL;

4) registradores de segmento CS, DS, SS, ES, FS, GS.

Por exemplo, a instrução add ax,bx adiciona o conteúdo dos registradores ax e bx e escreve o resultado em bx. O comando dec si diminui o conteúdo de si em 1.

O operando está na memória

Esta é a maneira mais complexa e ao mesmo tempo mais flexível de especificar operandos. Ele permite que você implemente os dois tipos principais de endereçamento a seguir: direto e indireto.

Por sua vez, o endereçamento indireto tem as seguintes variedades:

1) endereçamento indireto de base; seu outro nome é endereçamento indireto de registro;

2) endereçamento indireto de base com offset;

3) endereçamento de índice indireto com offset;

4) endereçamento indireto do índice base;

5) endereçamento indireto do índice base com offset.

O operando é uma porta de E/S

Além do espaço de endereço de RAM, o microprocessador mantém um espaço de endereço de E/S, que é usado para acessar dispositivos de E/S. O espaço de endereço de E/S é de 64 KB. Os endereços são alocados para qualquer dispositivo de computador neste espaço. Um valor de endereço específico dentro desse espaço é chamado de porta de E/S. Fisicamente, a porta de E/S corresponde a um registrador de hardware (não confundir com um registrador de microprocessador), que é acessado usando instruções de montagem especiais de entrada e saída.

Por exemplo:

em al,60h; insira um byte da porta 60h

Os registros endereçados por uma porta de E/S podem ter 8,16, 32 ou XNUMX bits de largura, mas a largura do bit de registro é fixa para uma determinada porta. Os comandos de entrada e saída operam em um intervalo fixo de objetos. Os chamados registros acumuladores EAX, AX, AL são usados ​​como fonte de informação ou destinatário. A escolha do registro é determinada pela quantidade de bits da porta. O número da porta pode ser especificado como um operando imediato nas instruções de entrada e saída, ou como um valor no registrador DX. O último método permite determinar dinamicamente o número da porta no programa.

O operando está na pilha

As instruções podem não ter nenhum operando, podem ter um ou dois operandos. A maioria das instruções requer dois operandos, um dos quais é o operando de origem e o outro é o operando de destino. É importante que um operando possa estar localizado em um registrador ou memória, e o segundo operando em um registrador ou diretamente na instrução. Um operando imediato só pode ser um operando de origem. Em uma instrução de máquina de dois operandos, as seguintes combinações de operandos são possíveis:

1) cadastro - cadastro;

2) registrador - memória;

3) memória - registro;

4) operando imediato - registrador;

5) operando imediato - memória.

Existem exceções a esta regra em relação a:

1) comandos em cadeia que podem mover dados de memória para memória;

2) comandos de pilha que podem transferir dados da memória para uma pilha que também está na memória;

3) comandos do tipo multiplicação, que, além do operando especificado no comando, também utilizam um segundo operando implícito.

Das combinações de operandos listadas, registrador - memória e registrador de memória - são os mais usados. Em vista de sua importância, vamos considerá-los mais detalhadamente. Acompanharemos a discussão com exemplos de instruções do montador que mostrarão como o formato de uma instrução do montador muda quando um ou outro tipo de endereçamento é aplicado. A esse respeito, observe novamente a Figura 21, que mostra o princípio de formação de um endereço físico no barramento de endereços do microprocessador. Pode-se observar que o endereço do operando é formado pela soma de dois componentes - o conteúdo do registrador de segmento deslocado em 4 bits e o endereço efetivo de 16 bits, que geralmente é calculado como a soma de três componentes: base, deslocamento e índice.

3. Métodos de endereçamento

Listamos e, em seguida, consideramos as características dos principais tipos de operandos de endereçamento na memória:

1) endereçamento direto;

2) endereçamento básico indireto (registro);

3) endereçamento básico indireto (registro) com offset;

4) endereçamento de índice indireto com offset;

5) endereçamento indireto do índice base;

6) endereçamento indireto do índice base com offset.

Endereçamento direto

Essa é a forma mais simples de endereçar um operando na memória, pois o endereço efetivo está contido na própria instrução e nenhuma fonte ou registrador adicional é usado para formá-lo. O endereço efetivo é obtido diretamente do campo de deslocamento da instrução de máquina (veja a Figura 20), que pode ter 8, 16, 32 bits de tamanho. Esse valor identifica exclusivamente o byte, palavra ou palavra dupla localizada no segmento de dados.

O endereçamento direto pode ser de dois tipos.

Endereçamento direto relativo

Usado para instruções de salto condicional para indicar o endereço de salto relativo. A relatividade de tal transição reside no fato de que o campo de deslocamento da instrução de máquina contém um valor de 8, 16 ou 32 bits, que, como resultado da operação da instrução, será adicionado ao conteúdo da instrução de máquina. o registrador de ponteiro de instrução ip/eip. Como resultado desta adição, é obtido o endereço para o qual a transição é realizada.

Endereçamento direto absoluto

Neste caso, o endereço efetivo faz parte da instrução de máquina, mas este endereço é formado apenas a partir do valor do campo offset na instrução. Para formar o endereço físico do operando na memória, o microprocessador soma este campo com o valor do registrador de segmento deslocado em 4 bits. Várias formas deste endereçamento podem ser usadas em uma instrução assembler.

Mas esse endereçamento raramente é usado - células comumente usadas no programa recebem nomes simbólicos. Durante a tradução, o montador calcula e substitui os valores de deslocamento desses nomes na instrução de máquina que gera no campo "deslocamento de instrução". Como resultado, verifica-se que a instrução de máquina endereça diretamente seu operando, tendo, de fato, em um de seus campos o valor do endereço efetivo.

Outros tipos de endereçamento são indiretos. A palavra "indireto" no nome desses tipos de endereçamento significa que apenas parte do endereço efetivo pode estar na própria instrução, e seus demais componentes estão em registradores, que são indicados pelo seu conteúdo pelo byte modr/m e, possivelmente, pelo byte sib.

Endereçamento básico indireto (registro)

Com este endereçamento, o endereço efetivo do operando pode estar em qualquer um dos registradores de uso geral, exceto sp/esp e bp/ebp (estes são registradores específicos para trabalhar com um segmento de pilha). Sintaticamente em um comando, este modo de endereçamento é expresso colocando o nome do registrador entre colchetes []. Por exemplo, a instrução mov ax, [ecx] coloca nos registradores ax o conteúdo da palavra no endereço do segmento de dados com o deslocamento armazenado no registrador esx. Como o conteúdo do registrador pode ser facilmente alterado no decorrer do programa, este método de endereçamento permite atribuir dinamicamente o endereço de um operando para alguma instrução de máquina. Esta propriedade é muito útil, por exemplo, para organizar cálculos cíclicos e para trabalhar com várias estruturas de dados como tabelas ou arrays.

Endereçamento indireto de base (registro) com offset

Este tipo de endereçamento é uma adição ao anterior e é projetado para acessar dados com um deslocamento conhecido em relação a algum endereço base. Este tipo de endereçamento é conveniente para acessar os elementos das estruturas de dados, quando o deslocamento dos elementos é conhecido antecipadamente, na fase de desenvolvimento do programa, e o endereço base (inicial) da estrutura deve ser calculado dinamicamente, no a fase de execução do programa. A modificação do conteúdo da base cadastral permite acessar os elementos de mesmo nome em diferentes instâncias do mesmo tipo de estrutura de dados.

Por exemplo, a instrução mov ax,[edx+3h] transfere as palavras da área de memória para os registradores ax no endereço: o conteúdo de edx + 3h.

A instrução mov ax,mas[dx] move uma palavra para o registrador ax no endereço: o conteúdo de dx mais o valor do identificador mas (lembre-se que o compilador atribui a cada identificador um valor igual ao deslocamento deste identificador do início do segmento de dados).

Endereçamento de índice indireto com deslocamento

Este tipo de endereçamento é muito semelhante ao endereçamento de base indireto com deslocamento. Aqui, também, um dos registradores de uso geral é usado para formar o endereço efetivo. Mas o endereçamento de índice tem um recurso interessante que é muito conveniente para trabalhar com arrays. Ele está conectado com a possibilidade do chamado dimensionamento do conteúdo do registro de índice. O que é isso?

Veja a Figura 20. Estamos interessados ​​no byte sib. Ao discutir a estrutura desse byte, notamos que ele consiste em três campos. Um desses campos é o campo ss scale, pelo qual se multiplica o conteúdo do registro de índice.

Por exemplo, na instrução mov ax,mas[si*2], o valor do endereço efetivo do segundo operando é calculado pela expressão mas+(si)*2. Devido ao fato de que o montador não tem meios para organizar a indexação de arrays, o programador tem que organizá-la por conta própria.

A capacidade de dimensionar ajuda significativamente na solução desse problema, mas desde que o tamanho dos elementos da matriz seja de 1, 2, 4 ou 8 bytes.

Endereçamento de índice base indireto

Com este tipo de endereçamento, o endereço efetivo é formado pela soma do conteúdo de dois registradores de uso geral: base e índice. Esses registradores podem ser quaisquer registradores de uso geral, e o dimensionamento do conteúdo de um registrador de índice é frequentemente usado.

Endereçamento de índice base indireto com deslocamento

Este tipo de endereçamento é o complemento do endereçamento indexado indireto. O endereço efetivo é formado pela soma de três componentes: o conteúdo do registrador base, o conteúdo do registrador de índice e o valor do campo de deslocamento no comando.

Por exemplo, a instrução mov eax,[esi+5] [edx] move uma palavra dupla para o registrador eax no endereço: (esi) + 5 + (edx).

O comando add ax,array[esi] [ebx] adiciona o conteúdo do registrador ax ao conteúdo da palavra no endereço: o valor do array identificador + (esi) + (ebx).

PALESTRA Nº 18. Equipes

1. Comandos de transferência de dados

Por conveniência de aplicação prática e reflexão de suas especificidades, é mais conveniente considerar os comandos deste grupo de acordo com sua finalidade funcional, segundo o qual podem ser divididos nos seguintes grupos de comandos:

1) transferências de dados de uso geral;

2) entrada-saída para a porta;

3) trabalhar com endereços e ponteiros;

4) transformações de dados;

5) trabalhe com a pilha.

Comandos gerais de transferência de dados

Este grupo inclui os seguintes comandos:

1) mov é o comando básico de transferência de dados. Ele implementa uma ampla variedade de opções de envio. Observe as especificidades deste comando:

a) o comando mov não pode transferir de uma área de memória para outra. Se tal necessidade surgir, então qualquer registrador de propósito geral atualmente disponível deve ser usado como um buffer intermediário;

b) é impossível carregar um valor diretamente da memória em um registrador de segmento. Portanto, para executar essa carga, você precisa usar um objeto intermediário. Este pode ser um registrador de uso geral ou uma pilha;

c) você não pode transferir o conteúdo de um registrador de segmento para outro registrador de segmento. Isso ocorre porque não há opcode correspondente no sistema de comando. Mas muitas vezes surge a necessidade de tal ação. Você pode realizar tal transferência usando os mesmos registradores de propósito geral que os intermediários;

d) você não pode usar o registrador de segmento CS como operando de destino. A razão é simples. O fato é que na arquitetura do microprocessador, o par cs:ip sempre contém o endereço do comando que deve ser executado em seguida. Alterar o conteúdo do registrador CS com o comando mov significaria na verdade uma operação de salto, não uma transferência, o que é inaceitável. 2) xchg - usado para transferência de dados bidirecional. Para esta operação, é claro, você pode usar uma sequência de várias instruções mov, mas devido ao fato de a operação de troca ser usada com bastante frequência, os desenvolvedores do sistema de instruções do microprocessador consideraram necessário introduzir uma instrução de troca xchg separada. Naturalmente, os operandos devem ser do mesmo tipo. Não é permitido (como em todas as instruções do montador) trocar o conteúdo de duas células de memória entre si.

Comandos de E/S da porta

Observe a Figura 22. Ela mostra um diagrama conceitual altamente simplificado de controle de hardware de computador.

Arroz. 22. Diagrama conceitual de controle de hardware de computador

Como você pode ver na Figura 22, o nível mais baixo é o nível do BIOS, onde o hardware é manipulado diretamente pelas portas. Isso implementa o conceito de independência do equipamento. Ao substituir o hardware, será necessário apenas corrigir as funções correspondentes do BIOS, reorientando-as para novos endereços e a lógica das portas.

Em princípio, é fácil gerenciar dispositivos diretamente por meio de portas. Informações sobre números de porta, sua profundidade de bits, formato de informações de controle são fornecidas na descrição técnica do dispositivo. Você só precisa saber o objetivo final de suas ações, o algoritmo de acordo com o qual um determinado dispositivo funciona e a ordem de programação de suas portas, ou seja, de fato, você precisa saber o que e em qual sequência você precisa enviar para a porta (ao escrever para ela) ou ler a partir dela (ao ler) e como essa informação deve ser interpretada. Para isso, bastam dois comandos presentes no sistema de comandos do microprocessador:

1) no acumulador, port_number - entrada no acumulador da porta com o número port_number;

2) porta de saída, acumulador - envia o conteúdo do acumulador para a porta com o número port_number.

Comandos para trabalhar com endereços e ponteiros de memória

Ao escrever programas em assembler, é feito um trabalho intensivo com os endereços dos operandos que estão na memória. Para suportar este tipo de operações, existe um grupo especial de comandos, que inclui os seguintes comandos:

1) lea destino, origem - carregamento de endereço efetivo;

2) Ids destino, origem - carregando o ponteiro no registrador de segmento de dados ds;

3) les destination, source - carregando o ponteiro no registro dos segmentos de dados adicionais;

4) destino lgs, origem - carregando o ponteiro no registro do segmento de dados adicional gs;

5) lfs destino, origem - carregando o ponteiro no registro do segmento de dados adicional fs;

6) destino lss, fonte - ponteiro de carregamento no registrador de segmento de pilha ss.

O comando lea é semelhante ao comando mov, pois também executa um movimento. Entretanto, a instrução lea não transfere dados, mas sim o endereço efetivo dos dados (ou seja, o deslocamento dos dados desde o início do segmento de dados) para o registrador indicado pelo operando destino.

Muitas vezes, para realizar alguma ação em um programa, não basta saber apenas o valor do endereço efetivo dos dados, mas é necessário ter um ponteiro completo para os dados. Um ponteiro de dados completo consiste em um componente de segmento e um deslocamento. Todos os outros comandos deste grupo permitem que você obtenha um ponteiro completo para um operando na memória em um par de registradores. Neste caso, o nome do registrador de segmento, no qual o componente de segmento do endereço é colocado, é determinado pelo código de operação. Assim, o deslocamento é colocado no registro geral indicado pelo operando de destino.

Mas nem tudo é tão simples com o operando fonte. De fato, no comando como fonte, você não pode especificar diretamente o nome do operando na memória, para o qual gostaríamos de receber um ponteiro. Primeiro, você precisa obter o valor do ponteiro completo em alguma área da memória e especificar o endereço completo do nome dessa área no comando get. Para executar esta ação, você precisa lembrar as diretivas para reservar e inicializar a memória.

Ao aplicar essas diretivas, um caso especial é possível quando o nome de outra diretiva de definição de dados (na verdade, o nome de uma variável) é especificado no campo do operando. Neste caso, o endereço desta variável é formado na memória. Qual endereço será gerado (efetivo ou completo) depende da diretiva aplicada. Se for dw, apenas o valor de 16 bits do endereço efetivo é formado na memória; se for dd, o endereço completo é gravado na memória. A localização desse endereço na memória é a seguinte: a palavra baixa contém o deslocamento, a palavra alta contém o componente de segmento de 16 bits do endereço.

Por exemplo, ao organizar o trabalho com uma cadeia de caracteres, é conveniente colocar seu endereço inicial em um determinado registro e depois modificar esse valor em um loop para acesso sequencial aos elementos da cadeia.

A necessidade de usar comandos para obter um ponteiro de dados completo na memória, ou seja, o endereço do segmento e o valor de deslocamento dentro do segmento, surge, em particular, quando se trabalha com cadeias.

Comandos de conversão de dados

Muitas instruções de microprocessadores podem ser atribuídas a este grupo, mas a maioria delas possui certas características que requerem que sejam atribuídas a outros grupos funcionais. Portanto, de todo o conjunto de comandos do microprocessador, apenas um comando pode ser atribuído diretamente aos comandos de conversão de dados: xlat [address_of_transcoding_table]

Esta é uma equipe muito interessante e útil. Seu efeito é que ele substitui o valor no registrador al por outro byte da tabela de memória localizada no endereço especificado pelo operando remap_table_address.

A palavra "tabela" é muito condicional, na verdade, é apenas uma sequência de bytes. O endereço do byte na string que substituirá o conteúdo do registrador al é determinado pela soma (bx) + (al), ou seja, o conteúdo de al atua como um índice no array de bytes.

Ao trabalhar com o comando xlat, preste atenção ao seguinte ponto sutil. Mesmo que o comando especifique o endereço da cadeia de bytes da qual o novo valor deve ser recuperado, esse endereço deve ser pré-carregado (por exemplo, usando o comando lea) no registrador bx. Assim, o operando lookup_table_address não é realmente necessário (a opcionalidade do operando é mostrada colocando-o entre colchetes). Quanto à cadeia de bytes (tabela de transcodificação), é uma área de memória de 1 a 255 bytes de tamanho (o intervalo de um número sem sinal em um registrador de 8 bits).

Comandos de pilha

Este grupo é um conjunto de comandos especializados focados em organizar o trabalho flexível e eficiente com a pilha.

A pilha é uma área de memória especialmente alocada para armazenamento temporário de dados do programa. A importância da pilha é determinada pelo fato de que um segmento separado é fornecido para ela na estrutura do programa. Caso o programador tenha esquecido de declarar um segmento de pilha em seu programa, o vinculador tlink emitirá uma mensagem de aviso.

A pilha tem três registradores:

1) ss - registrador de segmento de pilha;

2) sp/esp - registrador de ponteiro de pilha;

3) bp/ebp - registrador de ponteiro de base de quadro de pilha.

O tamanho da pilha depende do modo de operação do microprocessador e é limitado a 64 KB (ou 4 GB no modo protegido).

Apenas uma pilha está disponível por vez, cujo endereço de segmento está contido no registrador SS. Essa pilha é chamada de pilha atual. Para fazer referência a outra pilha ("switch the stack"), é necessário carregar outro endereço no registrador ss. O registrador SS é usado automaticamente pelo processador para executar todas as instruções que funcionam na pilha.

Listamos mais alguns recursos para trabalhar com a pilha:

1) a escrita e a leitura de dados na pilha são realizadas de acordo com o princípio LIFO,

2) à medida que os dados são gravados na pilha, esta cresce em direção aos endereços mais baixos. Esse recurso está embutido no algoritmo de comandos para trabalhar com a pilha;

3) ao utilizar os registradores esp/sp e ebp/bp para endereçamento de memória, o montador considera automaticamente que os valores nele contidos são offsets relativos ao registrador do segmento ss.

Em geral, a pilha é organizada conforme mostrado na Figura 23.

Arroz. 23. Diagrama conceitual de organização da pilha

Os registradores SS, ESP/SP e EUR/BP são projetados para trabalhar com a pilha. Esses registradores são usados ​​de forma complexa, e cada um deles tem sua própria função funcional.

O registrador ESP/SP sempre aponta para o topo da pilha, ou seja, contém o deslocamento em que o último elemento foi colocado na pilha. As instruções da pilha alteram implicitamente esse registro para que ele sempre aponte para o último elemento inserido na pilha. Se a pilha estiver vazia, o valor de esp será igual ao endereço do último byte do segmento alocado para a pilha. Quando um elemento é colocado na pilha, o processador diminui o valor do registrador esp e, em seguida, grava o elemento no endereço do novo vértice. Ao retirar os dados da pilha, o processador copia o elemento localizado no endereço do vértice e então incrementa o registrador do ponteiro da pilha esp. Assim, verifica-se que a pilha cresce para baixo, na direção de endereços decrescentes.

E se precisarmos acessar elementos não no topo, mas dentro da pilha? Para fazer isso, use o registrador EBP O registrador EBP é o registrador do ponteiro base do quadro de pilha.

Por exemplo, um truque típico ao entrar em uma sub-rotina é passar os parâmetros desejados empurrando-os para a pilha. Se a sub-rotina também estiver trabalhando ativamente com a pilha, o acesso a esses parâmetros se tornará problemático. A saída é salvar o endereço do topo da pilha no ponteiro do quadro (base) da pilha depois de escrever os dados necessários na pilha - o registrador EUR. O valor em EUR pode ser usado posteriormente para acessar os parâmetros passados.

O início da pilha está localizado em endereços de memória mais altos. Na Figura 23, este endereço é denotado pelo par ss: fffF. O deslocamento de wT é dado aqui condicionalmente. Na realidade, esse valor é determinado pelo valor que o programador especifica ao descrever o segmento da pilha em seu programa.

Para organizar o trabalho com a pilha, existem comandos especiais para escrita e leitura.

1. push source - gravando o valor da fonte no topo da pilha.

De interesse é o algoritmo deste comando, que inclui as seguintes ações (Fig. 24):

1) (sp) = (sp) - 2; o valor de sp é reduzido em 2;

2) o valor da fonte é escrito no endereço especificado pelo par ss:sp.

Arroz. 24. Como funciona o comando push

2. atribuição de pop - escrever o valor do topo da pilha para o local especificado pelo operando de destino. O valor é assim "removido" do topo da pilha. O algoritmo do comando pop é o inverso do algoritmo do comando push (Fig. 25):

1) escrever o conteúdo do topo da pilha no local indicado pelo operando destino;

2) (sp) = (sp) + 2; aumentando o valor de sp.

Arroz. 25. Como funciona o comando pop

3. pusha - um comando de gravação de grupo na pilha. Por este comando, os registradores ax, cx, dx, bx, sp, bp, si, di são escritos sequencialmente na pilha. Observe que o conteúdo original de sp é escrito, ou seja, o conteúdo que estava antes da emissão do comando pusha (Fig. 26).

Arroz. 26. Como funciona o comando pusha

4. pushaw é quase sinônimo de comando pusha Qual é a diferença? O atributo bitness pode ser use16 ou use32. Vejamos como os comandos pusha e pushaw funcionam com cada um desses atributos:

1) use16 - o algoritmo pushaw é semelhante ao algoritmo pusha;

2) use32 - pushaw não muda (ou seja, é insensível à largura do segmento e sempre funciona com registradores de tamanho de palavra - ax, cx, dx, bx, sp, bp, si, di). O comando pusha é sensível à largura do segmento definido e quando um segmento de 32 bits é especificado, ele funciona com os registros de 32 bits correspondentes, ou seja, eax, esx, edx, ebx, esp, ebp, esi, edi.

5. pushad - executado de forma semelhante ao comando pusha, mas existem algumas particularidades.

Os três comandos a seguir executam o inverso dos comandos acima:

1) rora;

2) pipoca;

3) pop.

O grupo de instruções descrito abaixo permite que você salve o registrador de flag na pilha e escreva uma palavra ou palavra dupla na pilha. Observe que as instruções listadas abaixo são as únicas no conjunto de instruções do microprocessador que permitem (e exigem) acesso a todo o conteúdo do registrador de flag.

1. pushf - salva o registro de flags na pilha.

A operação deste comando depende do atributo de tamanho do segmento:

1) use 16 - o registrador de flags de 2 bytes de tamanho é escrito na pilha;

2) use32 - o registrador eflags de 4 bytes é escrito na pilha.

2. pushfw - salvando um registro de flags do tamanho de uma palavra na pilha. Sempre funciona como pushf com o atributo use16.

3. pushfd - salvando os flags ou flags eflags registrados na pilha dependendo do atributo de largura de bits do segmento (ou seja, o mesmo que pushf).

Da mesma forma, os três comandos a seguir executam o inverso das operações discutidas acima:

1) popf;

2) popftv;

3) popfd.

E em conclusão, notamos os principais tipos de operações quando o uso da pilha é quase inevitável:

1) chamada de sub-rotinas;

2) armazenamento temporário dos valores dos registradores;

3) definição de variáveis ​​locais.

2. Instruções aritméticas

O microprocessador pode executar operações inteiras e de ponto flutuante. Para fazer isso, sua arquitetura possui dois blocos separados:

1) um dispositivo para realizar operações inteiras;

2) um dispositivo para realizar operações de ponto flutuante.

Cada um desses dispositivos tem seu próprio sistema de comando. Em princípio, um dispositivo inteiro pode assumir muitas das funções de um dispositivo de ponto flutuante, mas isso será computacionalmente caro. Para a maioria dos problemas usando linguagem assembly, a aritmética inteira é suficiente.

Visão geral de um grupo de instruções e dados aritméticos

Um dispositivo de computação inteiro suporta um pouco mais de uma dúzia de instruções aritméticas. A Figura 27 mostra a classificação dos comandos neste grupo.

Arroz. 27. Classificação de comandos aritméticos

O grupo de instruções aritméticas inteiras trabalha com dois tipos de números:

1) números binários inteiros. Os números podem ou não ter um dígito assinado, ou seja, ser números assinados ou não assinados;

2) números decimais inteiros.

Considere os formatos de máquina nos quais esses tipos de dados são armazenados.

Números binários inteiros

Um inteiro binário de ponto fixo é um número codificado no sistema numérico binário.

A dimensão de um inteiro binário pode ser de 8, 16 ou 32 bits. O sinal de um número binário é determinado por como o bit mais significativo na representação do número é interpretado. Isso é 7,15 ou 31 bits para números da dimensão correspondente. Ao mesmo tempo, é interessante que entre os comandos aritméticos existam apenas dois comandos que realmente levam em conta este bit mais significativo como um sinal, estes são os comandos de multiplicação e divisão de inteiros imul e idiv. Em outros casos, a responsabilidade pelas ações com números assinados e, portanto, com um bit de sinal é do programador. O intervalo de valores de um número binário depende de seu tamanho e interpretação do bit mais significativo como o bit mais significativo do número ou como o bit de sinal do número (Tabela 9).

Tabela 9. Faixa de números binários Números decimais

Os números decimais são um tipo especial de representação da informação numérica, que se baseia no princípio de codificar cada dígito decimal de um número por um grupo de quatro bits. Neste caso, cada byte do número contém um ou dois dígitos decimais no chamado código decimal codificado em binário (BCD - Binary-Coded Decimal). O microprocessador armazena números BCD em dois formatos (Fig. 28):

1) formato embalado. Nesse formato, cada byte contém dois dígitos decimais. Um dígito decimal é um valor binário de 0 bits entre 9 e 4. Neste caso, o código do dígito mais alto do número ocupa os 4 bits mais altos. Portanto, o intervalo de representação de um número decimal compactado em 1 byte é de 00 a 99;

2) formato não empacotado. Nesse formato, cada byte contém um dígito decimal nos quatro bits menos significativos. Os 4 bits superiores são definidos como zero. Esta é a chamada zona. Portanto, o intervalo de representação de um número decimal descompactado em 1 byte é de 0 a 9.

Arroz. 28. Representação de números BCD

Como descrever números decimais binários em um programa? Para fazer isso, você pode usar apenas duas diretivas de descrição e inicialização de dados - db e dt. A possibilidade de usar apenas essas diretivas para descrever números BCD se deve ao fato de que o princípio de "byte baixo em endereço baixo" também é aplicável a esses números, o que é muito conveniente para seu processamento. E, em geral, ao usar um tipo de dados como números BCD, a ordem em que esses números são descritos no programa e o algoritmo para processá-los é uma questão de gosto e preferências pessoais do programador. Isso ficará claro depois de analisarmos os fundamentos do trabalho com números BCD abaixo.

Operações aritméticas em inteiros binários

Adição de números binários não assinados

O microprocessador realiza a adição de operandos de acordo com as regras de adição de números binários. Não há problemas desde que o valor do resultado não exceda as dimensões do campo do operando. Por exemplo, ao adicionar operandos de tamanho byte, o resultado não deve exceder o número 255. Se isso acontecer, o resultado está incorreto. Vamos considerar por que isso acontece.

Por exemplo, vamos fazer a adição: 254 + 5 = 259 em binário. 11111110 + 0000101 = 1 00000011. O resultado foi além de 8 bits e seu valor correto cabe em 9 bits, e o valor 8 permaneceu no campo de 3 bits do operando, o que, obviamente, não é verdade. No microprocessador, esse resultado da adição é previsto e são fornecidos meios especiais para corrigir tais situações e processá-las. Assim, para corrigir a situação de ir além da grade de bits do resultado, como neste caso, o sinalizador de transporte cf é pretendido. Ele está localizado no bit 0 do registrador de flag EFLAGS/FLAGS. É a configuração deste sinalizador que corrige o fato da transferência de um da ordem superior do operando. Naturalmente, o programador deve levar em conta a possibilidade de tal resultado da operação de adição e fornecer meios para correção. Isso envolve incluir seções de código após a operação de adição na qual o sinalizador cf é analisado. Este sinalizador pode ser analisado de várias maneiras.

O mais fácil e acessível é usar o comando de ramificação condicional jcc. Esta instrução tem como operando o nome da etiqueta no segmento de código atual. A transição para este rótulo é realizada se, como resultado da operação do comando anterior, o sinalizador cf for definido como 1. Existem três comandos de adição binários no sistema de comandos do microprocessador:

1) operando inc - operação de incremento, ou seja, aumentar o valor do operando em 1;

2) add operando_1, operando_2 - instrução de adição com o princípio de funcionamento: operando_1 = operando_1 + operando_2;

3) adc operando_1, operando_2 - instrução de adição levando em consideração o carry flag cf. Princípio de operação do comando: operando_1 = operando_1 + operando_2 + valor_sG.

Preste atenção ao último comando - este é o comando de adição, que leva em consideração a transferência de um de ordem superior. Já consideramos o mecanismo para o surgimento de tal unidade. Assim, a instrução adc é uma ferramenta do microprocessador para adicionar números binários longos, cujas dimensões excedem os comprimentos dos campos padrão suportados pelo microprocessador.

Adição Binária Assinada

Na verdade, o microprocessador "não está ciente" da diferença entre números assinados e não assinados. Em vez disso, ele tem os meios de corrigir a ocorrência de situações características que se desenvolvem no processo de cálculos. Cobrimos alguns deles ao discutir a adição não assinada:

1) o cf carry flag, definindo-o como 1, indica que os operandos estavam fora de alcance;

2) o comando adc, que leva em consideração a possibilidade de tal saída (transportar do bit menos significativo).

Outro meio é registrar o estado do bit de alta ordem (sinal) do operando, o que é feito usando o sinalizador de estouro no registrador EFLAGS (bit 11).

Claro, você se lembra de como os números são representados em um computador: positivo - em binário, negativo - em complemento de dois. Considere várias opções para adicionar números. Os exemplos pretendem mostrar o comportamento dos dois bits mais significativos dos operandos e a correção do resultado da operação de adição.

Exemplo

30566 = 0111011101100110

+

00687 = 00000010

=

31253 = 01111010

Monitoramos as transferências do 14º e 15º dígitos e a exatidão do resultado: não há transferências, o resultado está correto.

Exemplo

30566 = 0111011101100110

+

30566 = 0111011101100110

=

1132 = 11101110

Houve uma transferência da 14ª categoria; não há transferência da 15ª categoria. O resultado está errado, porque há um estouro - o valor do número acabou sendo maior do que um número com sinal de 16 bits (+32 767) pode ter.

Exemplo

-30566 = 10001000 10011010

+

-04875 = 11101100 11110101

=

-35441 = 01110101 10001111

Houve transferência do 15º dígito, não há transferência do 14º dígito. O resultado está incorreto, porque em vez de um número negativo, acabou sendo positivo (o bit mais significativo é 0).

Exemplo

-4875 = 11101100 11110101

+

-4875 = 11101100 11110101

=

09750 = 11011001

Há transferências do 14º e 15º bits. O resultado está correto.

Assim, examinamos todos os casos e descobrimos que a situação de estouro (definindo o sinalizador OF para 1) ocorre durante a transferência:

1) a partir do 14º dígito (para números positivos com sinal);

2) a partir do 15º dígito (para números negativos).

Por outro lado, não ocorre overflow (ou seja, o sinalizador OF é redefinido para 0) se houver um transporte de ambos os bits ou se não houver transporte em ambos os bits.

Portanto, o estouro é registrado com o sinalizador de estouro de. Além do sinalizador de, ao transferir do bit de ordem superior, o sinalizador de transferência CF é definido como 1. Como o microprocessador não sabe da existência de números com e sem sinal, o programador é o único responsável pelas ações corretas com os números resultantes. Você pode analisar os sinalizadores CF e OF com as instruções de salto condicional JC\JNC e JO\JNO, respectivamente.

Quanto aos comandos para adicionar números com sinal, eles são os mesmos que para números sem sinal.

Subtração de números binários sem sinal

Assim como na análise da operação de adição, discutiremos a essência dos processos que ocorrem ao realizar a operação de subtração. Se o minuendo for maior que o subtraendo, não há problema - a diferença é positiva, o resultado está correto. Se o minuendo for menor que o subtraído, há um problema: o resultado é menor que 0, e este já é um número com sinal. Nesse caso, o resultado deve ser encapsulado. O que isto significa? Com a subtração usual (em uma coluna), eles fazem um empréstimo de 1 da ordem mais alta. O microprocessador faz o mesmo, ou seja, pega 1 do dígito seguinte ao mais alto na grade de bits do operando. Vamos explicar com um exemplo.

Exemplo

05 = 00000000

-10 = 00000000 00001010

Para fazer a subtração, vamos fazer

empréstimo imaginário sênior:

100000000 00000101

-

00000000 00001010

=

11111111 11111011

Assim, em essência, a ação

(65 + 536) - 5 = 10

0 aqui é, por assim dizer, equivalente ao número 65536. O resultado, claro, está incorreto, mas o microprocessador considera que está tudo bem, embora corrija o fato de pedir uma unidade emprestada definindo o sinalizador de transporte cf. Mas observe novamente com atenção o resultado da operação de subtração. É -5 em complemento de dois! Vamos fazer um experimento: represente a diferença como uma soma de 5 + (-10).

Exemplo

5 = 00000000

+

(-10)= 11111111 11110110

=

11111111 11111011

ou seja, obtivemos o mesmo resultado do exemplo anterior.

Assim, após o comando para subtrair números sem sinal, é necessário analisar o estado do sinalizador CE. Se estiver definido como 1, isso indica que houve um empréstimo da ordem superior e o resultado foi obtido em um código adicional .

Assim como as instruções de adição, o grupo de instruções de subtração consiste no menor conjunto possível. Esses comandos realizam a subtração de acordo com os algoritmos que estamos considerando agora, e as exceções devem ser levadas em consideração pelo próprio programador. Os comandos de subtração incluem:

1) operando dec - operação de decremento, ou seja, diminui o valor do operando em 1;

2) sub operando_1, operando_2 - comando de subtração; seu princípio de funcionamento: operando_1 = operando_1 - operando_2;

3) sbb operando_1, operando_2 - comando de subtração levando em consideração o empréstimo (ci flag): operando_1 = operando_1 - operando_2 - valor_sG.

Como você pode ver, entre os comandos de subtração existe um comando sbb que leva em consideração a flag de carry cf. Este comando é semelhante ao adc, mas agora o sinalizador cf atua como um indicador de empréstimo de 1 do dígito mais significativo ao subtrair números.

Subtração binária assinada

Aqui tudo é um pouco mais complicado. O microprocessador não precisa ter dois dispositivos - adição e subtração. Basta ter apenas um - o dispositivo de adição. Mas para a subtração por meio da adição de números com um sinal em um código adicional, é necessário representar ambos os operandos - tanto o reduzido quanto o subtraído. O resultado também deve ser tratado como um valor de complemento de dois. Mas aqui surgem as dificuldades. Em primeiro lugar, eles estão relacionados ao fato de que o bit mais significativo do operando é considerado um bit de sinal. Considere o exemplo da subtração de 45 - (-127).

Exemplo

Subtração de números assinados 1

45 = 0010

-

-127 = 1000 0001

=

-44 = 1010 1100

A julgar pelo bit de sinal, o resultado acabou sendo negativo, o que, por sua vez, indica que o número deve ser considerado um complemento igual a -44. O resultado correto deve ser 172. Aqui, como no caso da adição com sinal, encontramos um estouro de mantissa, quando o bit significativo do número mudou o bit de sinal do operando. Você pode acompanhar essa situação pelo conteúdo do sinalizador de estouro de. Defini-lo como 1 indica que o resultado está fora do intervalo de números com sinal (ou seja, o bit mais significativo foi alterado) para um operando desse tamanho e o programador deve tomar medidas para corrigir o resultado.

Exemplo

Subtração de números assinados 2

-45-45 = -45 + (-45) = -90.

-45 = 11010011

+

-45 = 11010011

=

-90 = 1010 0110

Tudo está bem aqui, o sinalizador de estouro de é redefinido para 0 e 1 no bit de sinal indica que o valor do resultado é um número de complemento de dois.

Subtração e adição de operandos grandes

Se você notar, as instruções de adição e subtração funcionam com operandos de dimensão fixa: 8, 16, 32 bits. Mas e se você precisar adicionar números de uma dimensão maior, por exemplo 48 bits, usando operandos de 16 bits? Por exemplo, vamos adicionar dois números de 48 bits:

Arroz. 29. Adicionando operandos grandes

A Figura 29 mostra a tecnologia para adicionar números longos passo a passo. Pode-se ver que o processo de adição de números multi-byte ocorre da mesma forma que ao adicionar dois números "em uma coluna" - com a implementação, se necessário, de transferência de 1 para o bit mais alto. Se conseguirmos programar esse processo, expandiremos significativamente o intervalo de números binários nos quais podemos realizar operações de adição e subtração.

O princípio de subtração de números com uma faixa de representação que excede as grades de bits padrão dos operandos é o mesmo da adição, ou seja, o sinalizador de transporte cf é usado. Você só precisa imaginar o processo de subtração em uma coluna e combinar corretamente as instruções do microprocessador com a instrução sbb.

Para concluir nossa discussão sobre as instruções de adição e subtração, além do cf e dos sinalizadores, existem alguns outros sinalizadores no registrador eflags que podem ser usados ​​com instruções aritméticas binárias. Estas são as seguintes bandeiras:

1) zf - sinalizador zero, que é definido como 1 se o resultado da operação for 0 e como 1 se o resultado não for igual a 0;

2) sf - sinalizador de sinal, cujo valor após as operações aritméticas (e não apenas) coincide com o valor do bit mais significativo do resultado, ou seja, com o bit 7, 15 ou 31. Assim, este sinalizador pode ser usado para operações em números assinados.

Multiplicação de números sem sinal

O comando para multiplicar números sem sinal é

mul fator_1

Como você pode ver, o comando contém apenas um operando multiplicador. O segundo operando factor_2 é especificado implicitamente. Sua localização é fixa e depende do tamanho dos fatores. Como, em geral, o resultado de uma multiplicação é maior que qualquer um de seus fatores, seu tamanho e localização também devem ser determinados de forma única. As opções para os tamanhos dos fatores e a colocação do segundo operando e o resultado são mostradas na Tabela 10.

Tabela 10. Arranjo dos operandos e resultado na multiplicação

Pode-se observar na tabela que o produto é composto por duas partes e, dependendo do tamanho dos operandos, é colocado em dois lugares - no lugar do fator_2 (parte inferior) e no registrador adicional ah, dx, edx (parte superior papel). Como, então, saber dinamicamente (ou seja, durante a execução do programa) que o resultado é pequeno o suficiente para caber em um registrador, ou que ultrapassou a dimensão do registrador e a parte mais alta foi parar em outro registrador? Para fazer isso, usamos os sinalizadores cf e overflow já conhecidos por nós da discussão anterior:

1) se a parte inicial do resultado for zero, então após a operação do produto os flags cf = 0 e of = 0;

2) se esses sinalizadores forem diferentes de zero, isso significa que o resultado foi além da menor parte do produto e consiste em duas partes, que devem ser levadas em consideração em trabalhos posteriores.

Multiplicar números assinados

O comando para multiplicar números com um sinal é

[imul operando_1, operando_2, operando_3]

Este comando é executado da mesma forma que o comando mul. Uma característica distintiva do comando imul é apenas a formação do signo.

Se o resultado for pequeno e couber em um registrador (isto é, se cf = of = 0), então o conteúdo do outro registrador (a parte alta) é extensão de sinal - todos os seus bits são iguais ao bit alto (bit de sinal ) da parte baixa do resultado. Caso contrário (se cf = of = 1), o sinal do resultado é o bit de sinal da parte alta do resultado e o bit de sinal da parte baixa é o bit significativo do código de resultado binário.

Divisão de números não assinados

O comando para dividir números sem sinal é

divisor div

O divisor pode estar na memória ou em um registrador e ter 8, 16 ou 32 bits de tamanho. A localização do dividendo é fixa e, como na instrução de multiplicação, depende do tamanho dos operandos. O resultado do comando de divisão são os valores do quociente e do resto.

As opções de localização e tamanho dos operandos da operação de divisão são mostradas na Tabela 11.

Tabela 11. Arranjo dos operandos e resultado na divisão

Depois que a instrução de divisão é executada, o conteúdo dos sinalizadores fica indefinido, mas a interrupção número 0, chamada "dividir por zero", pode ocorrer. Este tipo de interrupção pertence às chamadas exceções. Este tipo de interrupção ocorre dentro do microprocessador devido a algumas anomalias durante o processo de computação. Interromper O, "dividir por zero", durante a execução do comando div pode ocorrer por um dos seguintes motivos:

1) o divisor é zero;

2) o quociente não está incluído na grade de bits alocada para ele, o que pode acontecer nos seguintes casos:

a) ao dividir um dividendo com valor de palavra por um divisor com valor de bytes, e o valor do dividendo for mais de 256 vezes maior que o valor do divisor;

b) ao dividir um dividendo com valor de palavra dupla por um divisor com valor de palavra, e o valor do dividendo for mais de 65 vezes maior que o valor do divisor;

c) ao dividir o dividendo com valor de palavra quádruplo por um divisor com valor de palavra duplo, e o valor do dividendo for mais de 4 vezes o valor do divisor.

Divisão com um sinal

O comando para dividir números com um sinal é

divisor idiv

Para este comando, todas as disposições consideradas sobre comandos e números assinados são válidas. Apenas notamos as características da ocorrência da exceção 0, "divisão por zero", no caso de números com sinal. Ocorre ao executar o comando idiv por um dos seguintes motivos:

1) o divisor é zero;

2) o quociente não está incluído na grade de bits alocada para ele.

Este último, por sua vez, pode acontecer:

1) ao dividir um dividendo com valor de palavra com sinal por um divisor com valor de byte com sinal, e o valor do dividendo for mais de 128 vezes o valor do divisor (assim, o quociente não deve estar fora do intervalo de -128 para + 127);

2) ao dividir o dividendo por um valor de palavra dupla sinalizada pelo divisor por um valor de palavra sinalizada, e o valor do dividendo for superior a 32 vezes o valor do divisor (portanto, o quociente não deve estar fora do intervalo de - 768 a +32);

3) ao dividir o dividendo por um valor quadword com sinal por um divisor de palavra dupla com sinal, e o valor do dividendo for superior a 2 vezes o valor do divisor (portanto, o quociente não deve estar fora do intervalo de -147 a + 483 648 2 147).

Instruções Auxiliares para Operações Inteiras

Existem várias instruções no conjunto de instruções do microprocessador que podem facilitar a programação de algoritmos que executam cálculos aritméticos. Vários problemas podem surgir neles, para cuja resolução os desenvolvedores de microprocessadores forneceram vários comandos.

Comandos de conversão de tipo

E se os tamanhos dos operandos envolvidos nas operações aritméticas forem diferentes? Por exemplo, suponha que em uma operação de adição, um operando seja uma palavra e o outro seja uma palavra dupla. Foi dito acima que operandos do mesmo formato devem participar da operação de adição. Se os números não forem assinados, a saída será fácil de encontrar. Neste caso, com base no operando original, um novo (formato de palavra dupla) pode ser formado, cujos bits altos podem ser simplesmente preenchidos com zeros. A situação é mais complicada para números assinados: como levar em conta o sinal do operando dinamicamente, durante a execução do programa? Para resolver tais problemas, o conjunto de instruções do microprocessador possui as chamadas instruções de conversão de tipo. Essas instruções expandem bytes em palavras, palavras em palavras duplas e palavras duplas em palavras quádruplas (valores de 64 bits). As instruções de conversão de tipo são especialmente úteis na conversão de inteiros com sinal, pois preenchem automaticamente os bits de ordem superior do operando recém-construído com os valores do bit de sinal do objeto antigo. Essa operação resulta em valores inteiros de mesmo sinal e mesma magnitude do original, mas em formato mais longo. Essa transformação é chamada de operação de propagação de sinal.

Existem dois tipos de comandos de conversão de tipo.

1. Instruções sem operandos. Esses comandos funcionam com registradores fixos:

1) cbw (Convert Byte to Word) - um comando para converter um byte (no registrador al) em uma palavra (no registrador ah) espalhando o valor do bit al alto para todos os bits do registrador ah;

2) cwd (Convert Word to Double) - um comando para converter uma palavra (no registrador ax) em uma palavra dupla (nos registradores dx:ax) espalhando o valor do bit alto ax para todos os bits do registrador dx;

3) cwde (Convert Word to Double) - um comando para converter uma palavra (no registrador ax) em uma palavra dupla (no registrador eax) espalhando o valor do bit alto ax para todos os bits da metade superior do registrador eax ;

4) cdq (Convert Double Word to Quarter Word) - um comando para converter uma palavra dupla (no registrador eax) em uma palavra quádrupla (nos registradores edx: eax) espalhando o valor do bit mais significativo de eax para todos bits do registrador edx.

2. Comandos movsx e movzx relacionados a comandos de processamento de strings. Esses comandos têm uma propriedade útil no contexto do nosso problema:

1) movsx operando_1, operando_2 - envia com propagação de sinal. Estende um valor de 8 ou 16 bits do operando_2, que pode ser um registrador ou um operando de memória, para um valor de 16 ou 32 bits em um dos registradores, usando o valor do bit de sinal para preencher as posições mais altas do operando_1. Esta instrução é útil para preparar operandos assinados para operações aritméticas;

2) movzx operando_1, operando_2 - envia com extensão zero. Estende o valor de 8 bits ou 16 bits do operando_2 para 16 bits ou 32 bits, limpando (preenchendo) as posições altas do operando_2 com zeros. Esta instrução é útil para preparar operandos sem sinal para aritmética.

Outros comandos úteis

1. xadd destino, origem - troca e adição.

O comando permite que você execute duas ações em sequência:

1) valores de destino e origem da troca;

2) coloque o operando destino no lugar da soma: destino = destino + origem.

2. operando neg - negação com complemento de dois.

A instrução inverte o valor do operando. Fisicamente, o comando executa uma ação:

operando = 0 - operando, ou seja, subtrai o operando de zero.

O comando operando neg pode ser usado:

1) mudar o sinal;

2) para realizar a subtração de uma constante.

Operações aritméticas em números binários decimais

Nesta seção, veremos as especificidades de cada uma das quatro operações aritméticas básicas para números BCD compactados e descompactados.

A questão pode surgir com razão: por que precisamos de números BCD? A resposta pode ser: os números BCD são necessários em aplicativos de negócios, ou seja, onde os números precisam ser grandes e precisos. Como já vimos no exemplo dos números binários, as operações com esses números são bastante problemáticas para a linguagem assembly. As desvantagens de usar números binários incluem o seguinte:

1) Os valores em formato word e double word possuem um intervalo limitado. Se o programa for projetado para funcionar no campo das finanças, limitar o valor em rublos a 65 (para uma palavra) ou até 536 (para uma palavra dupla) restringirá significativamente o escopo de sua aplicação;

2) a presença de erros de arredondamento. Já imaginou um programa rodando em algum lugar de um banco que não leva em conta o valor do saldo ao operar com inteiros binários e opera com bilhões? Eu não gostaria de ser o autor de tal programa. O uso de números de ponto flutuante não salvará - o mesmo problema de arredondamento existe lá;

3) apresentação de uma grande quantidade de resultados em forma simbólica (código ASCII). Os programas de negócios não fazem apenas cálculos; uma das finalidades de seu uso é a pronta entrega de informações ao usuário. Para fazer isso, é claro, a informação deve ser apresentada de forma simbólica. Converter números de binário para ASCII requer algum esforço computacional. Um número de ponto flutuante é ainda mais difícil de traduzir em uma forma simbólica. Mas se você observar a representação hexadecimal de um dígito decimal descompactado e seu caractere correspondente na tabela ASCII, poderá ver que eles diferem em 30h. Assim, a conversão para a forma simbólica e vice-versa é muito mais fácil e rápida.

Você provavelmente já viu a importância de dominar pelo menos o básico das ações com números decimais. Em seguida, considere os recursos de realizar operações aritméticas básicas com números decimais. Notamos imediatamente o fato de que não existem comandos separados para adição, subtração, multiplicação e divisão de números BCD. Isso foi feito por razões bastante compreensíveis: a dimensão de tais números pode ser arbitrariamente grande. Os números BCD podem ser somados e subtraídos, tanto compactados quanto descompactados, mas apenas números BCD descompactados podem dividir e multiplicar. Por que isso é assim será visto a partir de uma discussão mais aprofundada.

Aritmética em números BCD descompactados

Adicionar números BCD descompactados

Vamos considerar dois casos de adição.

Exemplo

O resultado da adição não é superior a 9

6 = 0000

+

3 = 0000

=

9 = 0000

Não há transferência da tétrade júnior para a sénior. O resultado está correto.

Exemplo

O resultado da adição é maior que 9:

06 = 0000

+

07 = 0000

=

13 = 0000

Não recebemos mais um número BCD. O resultado está errado. O resultado correto no formato BCD descompactado deve ser 0000 0001 0000 0011 em binário (ou 13 em decimal).

Depois de analisar este problema ao adicionar números BCD (e problemas semelhantes ao realizar outras operações aritméticas) e possíveis maneiras de resolvê-lo, os desenvolvedores do sistema de comando do microprocessador decidiram não introduzir comandos especiais para trabalhar com números BCD, mas introduzir vários comandos corretivos .

O objetivo dessas instruções é corrigir o resultado da operação de instruções aritméticas comuns para os casos em que os operandos nelas são números BCD.

No caso da subtração no exemplo 10, percebe-se que o resultado obtido precisa ser corrigido. Para corrigir a operação de adição de dois números BCD descompactados de um dígito no sistema de comando do microprocessador, existe um comando especial - aaa (ASCII Adjust for Addition) - correção do resultado da adição para representação em forma simbólica.

Esta instrução não possui operandos. Ele funciona implicitamente apenas com o registro al e analisa o valor de sua tétrade inferior:

1) se este valor for menor que 9, então o flag cf é resetado para XNUMX e a transição para a próxima instrução é realizada;

2) se este valor for maior que 9, as seguintes ações são executadas:

a) 6 é adicionado ao conteúdo do tetrad al inferior (mas não ao conteúdo de todo o registro!) Assim, o valor do resultado decimal é corrigido na direção correta;

b) o sinalizador cf é definido como 1, fixando assim a transferência para o bit mais significativo para que possa ser levado em consideração nas ações subsequentes.

Assim, no exemplo 10, assumindo que o valor da soma 0000 1101 está em al, após a instrução aaa, o registrador terá 1101 + 0110 = 0011, ou seja, binário 0000 0011 ou decimal 3, e o sinalizador cf será definido como 1, ou seja, a transferência foi armazenada no microprocessador. Em seguida, o programador precisará usar a instrução de adição adc, que levará em conta o carry do bit anterior.

Subtração de números BCD descompactados

A situação aqui é bastante semelhante à adição. Vamos considerar os mesmos casos.

Exemplo

O resultado da subtração não é maior que 9:

6 = 0000

-

3 = 0000

=

3 = 0000

Como você pode ver, não há empréstimo do caderno sênior. O resultado está correto e não requer correção.

Exemplo

O resultado da subtração é maior que 9:

6 = 0000

-

7 = 0000

=

-1 = 1111 1111

A subtração é realizada de acordo com as regras da aritmética binária. Portanto, o resultado não é um número BCD.

O resultado correto no formato BCD descompactado deve ser 9 (0000 1001 em binário). Neste caso, assume-se um empréstimo do dígito mais significativo, como em um comando de subtração normal, ou seja, no caso de números BCD, a subtração de 16 - 7 deve ser realizada. caso de adição, o resultado da subtração deve ser corrigido. Para isso, existe um comando especial - aas (ASCII Adjust for Substraction) - correção do resultado da subtração para representação na forma simbólica.

A instrução aas também não possui operandos e opera no registrador al, analisando sua tétrade de menor ordem da seguinte forma:

1) se seu valor for menor que 9, então o sinalizador cf é redefinido para 0 e o controle é transferido para o próximo comando;

2) se o valor tetrad em al for maior que 9, o comando aas executa as seguintes ações:

a) subtrai 6 do conteúdo da tétrade inferior do registro al (nota - não do conteúdo de todo o registro);

b) redefine a tétrade superior do registrador al;

c) define o sinalizador cf como 1, fixando assim o empréstimo imaginário de alta ordem.

É claro que o comando aas é usado em conjunto com os comandos básicos de subtração sub e sbb. Neste caso, faz sentido usar o comando sub apenas uma vez, ao subtrair os dígitos mais baixos dos operandos, então deve-se usar o comando sbb, que levará em conta um possível empréstimo da ordem mais alta.

Multiplicação de números BCD descompactados

Usando o exemplo de adição e subtração de números descompactados, ficou claro que não existem algoritmos padrão para realizar essas operações em números BCD, e o próprio programador deve, com base nos requisitos de seu programa, implementar essas operações.

A implementação das duas operações restantes - multiplicação e divisão - é ainda mais complicada. No conjunto de instruções do microprocessador, existem apenas meios para a produção de multiplicação e divisão de números BCD descompactados de um dígito.

Para multiplicar números de dimensão arbitrária, você mesmo precisa implementar o processo de multiplicação, com base em algum algoritmo de multiplicação, por exemplo, "em uma coluna".

Para multiplicar dois números BCD de um dígito, você deve:

1) colocar um dos fatores no registro AL (conforme exigido pela instrução mul);

2) colocar o segundo operando em um registrador ou memória, alocando um byte;

3) multiplique os fatores com o comando mul (o resultado, como esperado, ficará em ah);

4) o resultado, claro, estará em código binário, então precisa ser corrigido.

Para corrigir o resultado após a multiplicação, é utilizado um comando especial - aam (ASCII Adjust for Multiplication) - correção do resultado da multiplicação para representação em forma simbólica.

Não possui operandos e opera no registrador AX da seguinte forma:

1) divide al por 10;

2) o resultado da divisão é escrito da seguinte forma: quociente em al, resto em ah. Como resultado, após a execução da instrução aam, os registradores AL e ah contêm os dígitos BCD corretos do produto de dois dígitos.

Antes de encerrarmos nossa discussão sobre o comando aam, precisamos observar mais um uso para ele. Este comando pode ser usado para converter um número binário no registro AL em um número BCD descompactado, que será colocado no registro ah: o dígito mais significativo do resultado está em ah, o dígito menos significativo está em al. É claro que o número binário deve estar no intervalo 0...99.

Divisão de números BCD descompactados

O processo de realizar a operação de divisão de dois números BCD descompactados é um pouco diferente das outras operações consideradas anteriormente com eles. Ações de correção também são necessárias aqui, mas devem ser realizadas antes da operação principal que divide diretamente um número BCD por outro número BCD. Primeiro, no registro ah, você precisa obter dois dígitos BCD descompactados do dividendo. Isso deixa o programador confortável para ele de certa forma. Em seguida, você precisa emitir o comando aad - aad (ASCII Adjust for Division) - correção de divisão para representação simbólica.

A instrução não possui operandos e converte o número BCD descompactado de dois dígitos no registrador ax em um número binário. Este número binário desempenhará posteriormente o papel do dividendo na operação de divisão. Além da conversão, o comando aad coloca o número binário resultante no registrador AL. O dividendo será naturalmente um número binário no intervalo 0...99.

O algoritmo pelo qual o comando aad realiza essa conversão é o seguinte:

1) multiplique o dígito mais alto do número BCD original em ah (o conteúdo de AH) por 10;

2) execute a adição AH + AL, cujo resultado (número binário) é inserido em AL;

3) redefinir o conteúdo de AH.

Em seguida, o programador precisa emitir um comando de divisão div normal para realizar a divisão do conteúdo de ax por um único dígito BCD localizado em um registrador de bytes ou em um local de memória de bytes.

Semelhante ao aash, o comando aad também pode ser usado para converter números BCD descompactados do intervalo 0...99 para seu equivalente binário.

Para dividir números de maior capacidade, assim como no caso de multiplicação, você precisa implementar seu próprio algoritmo, por exemplo, "em uma coluna", ou encontrar uma maneira mais otimizada.

Aritmética em números BCD compactados

Conforme observado acima, os números BCD compactados só podem ser adicionados e subtraídos. Para executar outras ações neles, eles devem ser convertidos adicionalmente em um formato descompactado ou em uma representação binária. Devido ao fato de que os números BCD compactados não são de grande interesse, vamos considerá-los brevemente.

Adicionando números BCD compactados

Primeiro, vamos chegar ao cerne do problema e tentar adicionar dois números BCD compactados de dois dígitos. Exemplo de adição de números BCD compactados:

67 = 01100111

+

75 = 01110101

=

142 = 1101 1100 = 220

Como você pode ver, em binário o resultado é 1101 1100 (ou 220 em decimal), o que é incorreto. Isso ocorre porque o microprocessador não tem conhecimento da existência de números BCD e os soma de acordo com as regras de adição de números binários. Na verdade, o resultado em BCD deve ser 0001 0100 0010 (ou 142 em decimal).

Pode-se ver que, assim como para números BCD não empacotados, para números BCD empacotados há a necessidade de corrigir de alguma forma os resultados das operações aritméticas.

O microprocessador disponibiliza para este comando daa - daa (Ajuste Decimal para Adição) - correção do resultado da adição para apresentação na forma decimal.

O comando daa converte o conteúdo do registrador al em dois dígitos decimais compactados de acordo com o algoritmo dado na descrição do comando daa. A unidade resultante (se o resultado da adição for maior que 99) é armazenada no sinalizador cf, levando assim em conta a transferência para o bit mais significativo.

Subtração de números BCD compactados

Semelhante à adição, o microprocessador trata os números BCD compactados como binários e subtrai os números BCD como binários de acordo.

Exemplo

Subtração de números BCD compactados.

Vamos subtrair 67-75. Como o microprocessador realiza a subtração na forma de adição, seguiremos o seguinte:

67 = 01100111

+

-75 = 10110101

=

-8 = 0001 1100 = 28

Como você pode ver, o resultado é 28 em decimal, o que é um absurdo. Em BCD, o resultado deve ser 0000 1000 (ou 8 em decimal).

Ao programar a subtração de números BCD compactados, o programador, bem como ao subtrair números BCD descompactados, deve controlar o próprio sinal. Isso é feito usando o sinalizador CF, que corrige o empréstimo de alta ordem.

A própria subtração de números BCD é realizada por um simples comando de subtração sub ou sbb. A correção do resultado é realizada pelo comando das - das (Ajuste Decimal para Subtração) - correção do resultado da subtração para representação na forma decimal.

O comando das converte o conteúdo do registrador AL para dois dígitos decimais compactados de acordo com o algoritmo fornecido na descrição do comando das.

PALESTRA Nº 19. Comandos de transferência de controle

1. Comandos lógicos

Juntamente com os meios de cálculos aritméticos, o sistema de comando do microprocessador também possui meios de conversão lógica de dados. Por meios lógicos tais transformações de dados, que são baseadas nas regras da lógica formal.

A lógica formal opera no nível de declarações verdadeiras e falsas. Para um microprocessador, isso geralmente significa 1 e 0, respectivamente. Para um computador, a linguagem de zeros e uns é nativa, mas a unidade mínima de dados com a qual as instruções de máquina funcionam é um byte. No entanto, no nível do sistema, muitas vezes é necessário poder operar no nível mais baixo possível, o nível de bits.

Arroz. 29. Meios de processamento lógico de dados

Os meios de transformação lógica de dados incluem comandos lógicos e operações lógicas. O operando de uma instrução assembler geralmente pode ser uma expressão, que por sua vez é uma combinação de operadores e operandos. Entre esses operadores podem existir operadores que implementam operações lógicas em objetos de expressão.

Antes de considerar essas ferramentas em detalhes, vamos considerar quais são os próprios dados lógicos e quais operações são executadas neles.

Dados booleanos

A base teórica para o processamento lógico de dados é a lógica formal. Existem vários sistemas de lógica. Um dos mais famosos é o cálculo proposicional. Uma proposição é qualquer afirmação que pode ser considerada verdadeira ou falsa.

O cálculo proposicional é um conjunto de regras usadas para determinar a verdade ou falsidade de alguma combinação de proposições.

O cálculo proposicional combina-se muito harmoniosamente com os princípios do computador e os métodos básicos de sua programação. Todos os componentes de hardware de um computador são construídos em chips lógicos. O sistema de representação de informações em um computador no nível mais baixo é baseado no conceito de bit. Um bit, tendo apenas dois estados (0 (falso) e 1 (verdadeiro)), se encaixa naturalmente no cálculo proposicional.

De acordo com a teoria, as seguintes operações lógicas podem ser executadas em instruções (em bits).

1. Negação (NOT lógico) - uma operação lógica em um operando, cujo resultado é o recíproco do valor do operando original.

Esta operação é caracterizada exclusivamente pela seguinte tabela verdade (Tabela 12).

Tabela 12. Tabela verdade para negação lógica

2. Adição lógica (OR inclusivo lógico) - uma operação lógica em dois operandos, cujo resultado é "verdadeiro" (1) se um ou ambos os operandos forem verdadeiros (1), e "falso" (0) se ambos os operandos forem falso (0).

Esta operação é descrita usando a seguinte tabela verdade (Tabela 13).

Tabela 13. Tabela verdade para OR lógico inclusivo

3. Multiplicação lógica (AND lógico) - uma operação lógica em dois operandos, cujo resultado é verdadeiro (1) somente se ambos os operandos forem verdadeiros (1). Em todos os outros casos, o valor da operação é "false" (0).

Esta operação é descrita usando a seguinte tabela verdade (Tabela 14).

Tabela 14. Lógica E tabela verdade

4. Adição exclusiva lógica (OR exclusivo lógico) - uma operação lógica em dois operandos, cujo resultado é "verdadeiro" (1), se apenas um dos dois operandos for verdadeiro (1), e falso (0), se ambos os operandos são falsos (0) ou verdadeiros (1). Esta operação é descrita usando a seguinte tabela verdade (Tabela 15).

Tabela 15. Tabela verdade para XOR lógico

O conjunto de instruções do microprocessador contém cinco instruções que suportam essas operações. Essas instruções realizam operações lógicas nos bits dos operandos. As dimensões dos operandos, é claro, devem ser as mesmas. Por exemplo, se a dimensão dos operandos for igual à palavra (16 bits), então a operação lógica é executada primeiro nos bits zero dos operandos, e seu resultado é escrito no lugar do bit 0 do resultado. Em seguida, o comando repete essas ações sequencialmente em todos os bits do primeiro ao décimo quinto.

Comandos lógicos

O sistema de comando do microprocessador possui o seguinte conjunto de comandos que suporta o trabalho com dados lógicos:

1) e operando_1, operando_2 - operação de multiplicação lógica. O comando realiza uma operação lógica AND (conjunção) bit a bit nos bits dos operandos operando_1 e operando_2. O resultado é escrito no lugar do operando_1;

2) og operando_1, operando_2 - operação lógica de adição. O comando executa uma operação OR lógica bit a bit (disjunção) nos bits dos operandos operando_1 e operando_2. O resultado é escrito no lugar de operando_1;

3) xor operando_1, operando_2 - operação de adição lógica exclusiva. O comando executa uma operação XOR lógica bit a bit nos bits dos operandos operando_1 e operando_2. O resultado é escrito no lugar do operando;

4) test operando_1, operando_2 - operação "teste" (usando o método de multiplicação lógica). O comando executa uma operação AND lógica bit a bit nos bits dos operandos operando_1 e operando_2. O estado dos operandos permanece o mesmo, apenas os flags zf, sf e pf são alterados, o que possibilita analisar o estado de bits individuais do operando sem alterar seu estado;

5) não operando - operação de negação lógica. O comando realiza uma inversão bit a bit (substituindo o valor pelo oposto) de cada bit do operando. O resultado é escrito no lugar do operando.

Para entender o papel dos comandos lógicos no conjunto de instruções do microprocessador, é muito importante entender as áreas de sua aplicação e os métodos típicos de seu uso na programação.

Com a ajuda de comandos lógicos, é possível selecionar bits individuais no operando com a finalidade de configurá-los, restaurá-los, invertê-los ou simplesmente verificar um determinado valor.

Para organizar esse trabalho com bits, o operando_2 geralmente desempenha o papel de uma máscara. Com a ajuda dos bits desta máscara definidos no bit 1, são determinados os bits operando_1 necessários para uma determinada operação. Vamos mostrar quais comandos lógicos podem ser usados ​​para essa finalidade:

1) para definir certos dígitos (bits) para 1, o comando og operando_1, operando_2 é usado.

Nesta instrução, o operando_2, que funciona como máscara, deve conter 1 bit no lugar daqueles bits que devem ser setados em 1 no operando_XNUMX;

2) para redefinir determinados dígitos (bits) para 0, é usado o comando e operando_1, operando_2.

Nesta instrução, o operando_2, que funciona como máscara, deve conter zero bits no lugar daqueles bits que devem ser definidos como 0 no operando_1;

3) comando xor operando_1, operando_2 é aplicado:

a) descobrir quais bits no operando_1 e operando diferem;

b) inverter o estado dos bits especificados no operando_1.

Os bits de máscara que nos interessam (operando_2) ao executar o comando xor devem ser únicos, o restante deve ser zero;

O comando test operando_1, operando_2 (verificar operando_1) é utilizado para verificar o estado dos bits especificados.

Os bits verificados do operando_1 na máscara (operando_2) devem ser definidos como um. O algoritmo do comando test é semelhante ao algoritmo do comando and, mas não altera o valor do operando_1. O resultado do comando é definir o valor do sinalizador zero zf:

1) se zf = 0, então, como resultado da multiplicação lógica, obtém-se um resultado zero, ou seja, um bit unitário da máscara, que não corresponde ao bit unitário correspondente do operando;

2) se zf = 1, então, como resultado da multiplicação lógica, obtém-se um resultado diferente de zero, ou seja, pelo menos um bit unitário da máscara coincide com o bit unitário correspondente do operando_1.

Para reagir ao resultado do comando de teste, é aconselhável usar o comando jump jnz label (Jump if Not Zero) - jump se o sinalizador zero zf for diferente de zero, ou o comando reverse action - jz label (Jump if Zero ) - salta se o sinalizador zero zf = 0.

Os dois comandos a seguir procuram o primeiro bit do operando definido como 1. A busca pode ser realizada tanto do início quanto do final do operando:

1) bsf operando_1, operando_2 (Bit Scanning Forward) - varrendo bits para frente. A instrução procura (varre) os bits do operando_2 do menos significativo ao mais significativo (do bit 0 ao bit mais significativo) em busca do primeiro bit definido como 1. Se um for encontrado, o operando_1 é preenchido com o número de este bit como um valor inteiro. Se todos os bits do operando_2 forem 0, então o sinalizador zero zf será definido como 1, caso contrário, o sinalizador zf será redefinido para 0;

2) bsr operando_1, operando_2 (Bit Scanning Reset) - varre os bits na ordem inversa. A instrução pesquisa (varre) os bits do operando_2 do mais significativo ao menos significativo (do bit mais significativo ao bit 0) em busca do primeiro bit definido como 1. Se um for encontrado, o operando_1 é preenchido com o número de este bit como um valor inteiro. É importante que a posição do primeiro bit de unidade à esquerda ainda seja contada em relação ao bit 0. Se todos os bits do operando_2 forem 0, então o sinalizador zero zf será definido como 1, caso contrário, o sinalizador zf será redefinido para 0.

Nos modelos mais recentes de microprocessadores Intel, apareceram mais algumas instruções no grupo de instruções lógicas que permitem acessar um bit específico do operando. O operando pode estar na memória ou em um registrador geral. A posição do bit é dada pelo deslocamento do bit em relação ao bit menos significativo do operando. O valor de deslocamento pode ser especificado como um valor direto ou contido em um registro de uso geral. Você pode usar os resultados dos comandos bsr e bsf como o valor de deslocamento. Todas as instruções atribuem o valor do bit selecionado ao flag CE.

1) operando bt, bit_offset (teste de bits) - teste de bits. A instrução transfere o valor do bit para o sinalizador cf;

2) operando bts, offset_bit (Bit Test and Set) - verificando e configurando um bit. A instrução transfere o valor do bit para o sinalizador CF e, em seguida, define o bit a ser verificado em 1;

3) operando btr, bit_offset (Bit Test and Reset) - verificando e redefinindo um bit. A instrução transfere o valor do bit para o sinalizador CF e, em seguida, define esse bit como 0;

4) operando btc, offset_bit (Bit Test and Convert) - verificando e invertendo um bit. A instrução envolve o valor de um bit no sinalizador cf e então inverte o valor desse bit.

Comandos de Mudança

As instruções deste grupo também fornecem manipulação de bits individuais dos operandos, mas de uma maneira diferente das instruções lógicas discutidas acima.

Todas as instruções de deslocamento movem os bits no campo do operando para a esquerda ou para a direita, dependendo do opcode. Todas as instruções de deslocamento têm a mesma estrutura - copiar operando, shift_count.

O número de bits a serem deslocados - counter_shifts - está localizado no local do segundo operando e pode ser configurado de duas maneiras:

1) estaticamente, que envolve definir um valor fixo usando um operando direto;

2) dinamicamente, o que significa inserir o valor do contador de deslocamento no registrador cl antes de executar a instrução de deslocamento.

Com base na dimensão do registrador cl, fica claro que o valor do contador de deslocamento pode variar de 0 a 255. Mas, na verdade, isso não é inteiramente verdade. Para fins de otimização, o microprocessador aceita apenas o valor dos cinco bits menos significativos do contador, ou seja, o valor está na faixa de 0 a 31.

Todas as instruções de deslocamento definem o sinalizador de transporte cf.

À medida que os bits saem do operando, eles primeiro atingem o sinalizador de transporte, definindo-o igual ao valor do próximo bit fora do operando. Onde este bit vai em seguida depende do tipo de instrução de deslocamento e do algoritmo do programa.

Os comandos de deslocamento podem ser divididos em dois tipos de acordo com o princípio de operação:

1) comandos de deslocamento linear;

2) comandos de deslocamento cíclico.

Comandos de deslocamento linear

Comandos deste tipo incluem comandos que mudam de acordo com o seguinte algoritmo:

1) o próximo bit que é pressionado define o sinalizador CF;

2) o bit inserido no operando da outra extremidade tem o valor 0;

3) quando o próximo bit é deslocado, ele vai para o flag CF, enquanto o valor do bit deslocado anterior é perdido! Os comandos de deslocamento linear são divididos em dois subtipos:

1) comandos de deslocamento linear lógico;

2) instruções de deslocamento linear aritmético.

Os comandos de deslocamento linear lógico incluem o seguinte:

1) operando shl, counter_shifts (Shift Logical Left) - deslocamento lógico para a esquerda. O conteúdo do operando é deslocado para a esquerda pelo número de bits especificado por shift_count. À direita (na posição do bit menos significativo) são inseridos zeros;

2) operando shr, shift_count (Shift Logical Right) - deslocamento lógico para a direita. O conteúdo do operando é deslocado para a direita pelo número de bits especificado por shift_count. À esquerda (na posição do bit de sinal mais significativo), os zeros são inseridos.

A Figura 30 mostra como esses comandos funcionam.

Arroz. 30. Esquema de trabalho de comandos de deslocamento lógico linear

As instruções de deslocamento linear aritmético diferem das instruções de deslocamento lógico, pois operam no bit de sinal do operando de uma maneira especial.

1) operando sal, shift_counter (Shift Aritmética Esquerda) - deslocamento aritmético para a esquerda. O conteúdo do operando é deslocado para a esquerda pelo número de bits especificado por shift_count. À direita (na posição do bit menos significativo), os zeros são inseridos. A instrução sal não preserva o sinal, mas define o sinalizador com / no caso de uma mudança de sinal pelo próximo bit avançado. Caso contrário, o comando sal é exatamente igual ao comando shl;

2) operando sar, shift_count (Deslocamento Aritmético para a Direita) - deslocamento aritmético para a direita. O conteúdo do operando é deslocado para a direita pelo número de bits especificado por shift_count. Zeros são inseridos no operando à esquerda. O comando sar preserva o sinal, restaurando-o após cada troca de bit.

A Figura 31 mostra como funcionam as instruções de deslocamento aritmético linear.

Arroz. 31. Esquema de operação dos comandos de deslocamento aritmético linear

Comandos de rotação

As instruções de deslocamento cíclico incluem instruções que armazenam os valores dos bits deslocados. Existem dois tipos de instruções de deslocamento cíclico:

1) comandos simples de deslocamento cíclico;

2) comandos de deslocamento cíclico através do sinalizador de transporte cf.

Os comandos simples de deslocamento cíclico incluem:

1) rol operando, shift_counter (Rotate Left) - deslocamento cíclico para a esquerda. O conteúdo do operando é deslocado para a esquerda pelo número de bits especificado pelo operando shift_count. Os bits deslocados para a esquerda são escritos no mesmo operando da direita;

2) operando gog, counter_shifts (Rotate Right) - deslocamento cíclico para a direita. O conteúdo do operando é deslocado para a direita pelo número de bits especificado pelo operando shift_count. Os bits deslocados à direita são escritos no mesmo operando à esquerda.

Arroz. 32. Esquema de operação de comandos de um simples deslocamento cíclico

Como pode ser visto na Figura 32, as instruções de um simples deslocamento cíclico no curso de seu trabalho realizam uma ação útil, a saber: o bit deslocado ciclicamente não é apenas empurrado para o operando da outra extremidade, mas ao mesmo tempo seu value se torna o valor do sinalizador CE.

Os comandos de deslocamento cíclico através do sinalizador de transporte CF diferem dos comandos de deslocamento cíclico simples em que o bit deslocado não entra imediatamente no operando de sua outra extremidade, mas é escrito primeiro no sinalizador de transporte CE Somente a próxima execução deste comando de deslocamento ( desde que executado em loop) faz com que o bit previamente avançado seja colocado na outra extremidade do operando (Fig. 33).

O seguinte está relacionado aos comandos de deslocamento cíclico por meio do sinalizador de transporte:

1) operando rcl, shift_count (Rotate through Carry Left) - deslocamento cíclico para a esquerda através de carry.

O conteúdo do operando é deslocado para a esquerda pelo número de bits especificado pelo operando shift_count. Os bits deslocados, por sua vez, tornam-se o valor do sinalizador de transporte cf.

2) operando rsg, shift_count (Rotate through Carry Right) - deslocamento cíclico para a direita através de um carry.

O conteúdo do operando é deslocado para a direita pelo número de bits especificado pelo operando shift_count. Os bits deslocados, por sua vez, tornam-se o valor do sinalizador de transporte CF.

Arroz. 33. Girar Instruções via Carry Flag CF

A Figura 33 mostra que ao deslocar através do sinalizador de transporte, aparece um elemento intermediário, com a ajuda do qual, em particular, é possível substituir bits deslocados ciclicamente, em particular, o descasamento de sequências de bits.

Doravante, incompatibilidade de uma sequência de bits significa uma ação que permite de alguma forma localizar e extrair as seções necessárias dessa sequência e gravá-las em outro local.

Comandos de deslocamento adicionais

O sistema de comando dos modelos mais recentes de microprocessadores Intel, começando com o i80386, contém comandos shift adicionais que expandem os recursos discutidos anteriormente. Estes são os comandos de mudança de precisão dupla:

1) shld operando_1, operando_2, shift_counter - deslocamento à esquerda de precisão dupla. O comando shld realiza uma substituição deslocando os bits do operando_1 para a esquerda, preenchendo seus bits à direita com os valores dos bits deslocados do operando_2 conforme diagrama da Fig. 34. O número de bits a serem deslocados é determinado pelo valor shift_counter, que pode estar no intervalo de 0 a 31. Este valor pode ser especificado como um operando imediato ou contido no registrador cl. O valor do operando_2 não é alterado.

Arroz. 34. O esquema do comando shld

2) shrd operando_1, operando_2, shift_counter - deslocamento para a direita de precisão dupla. A instrução realiza a substituição deslocando os bits do operando_1 para a direita, preenchendo seus bits à esquerda com os valores dos bits deslocados do operando_2 conforme diagrama da Figura 35. A quantidade de bits deslocados é determinada por o valor do shift_counter, que pode estar no intervalo 0... 31. Este valor pode ser especificado pelo operando imediato ou contido no registro cl. O valor do operando_2 não é alterado.

Arroz. 35. O esquema do comando shrd

Como observamos, os comandos shld e shrd deslocam até 32 bits, mas devido às peculiaridades de especificação de operandos e do algoritmo de operação, esses comandos podem ser usados ​​para trabalhar com campos de até 64 bits.

2. Comandos de Transferência de Controle

Conhecemos alguns comandos a partir dos quais são formadas as seções lineares do programa. Cada um deles geralmente realiza alguma conversão ou transferência de dados, após o que o microprocessador transfere o controle para a próxima instrução. Mas muito poucos programas funcionam de forma tão consistente. Normalmente, há pontos em um programa em que uma decisão deve ser tomada sobre qual instrução será executada em seguida. Esta solução pode ser:

1) incondicional - neste ponto, é necessário transferir o controle não para o comando que vem a seguir, mas para outro, que está a alguma distância do comando atual;

2) condicional - a decisão sobre qual comando será executado em seguida é feita com base na análise de algumas condições ou dados.

Um programa é uma sequência de comandos e dados que ocupam uma certa quantidade de espaço na RAM. Esse espaço de memória pode ser contíguo ou consistir em vários fragmentos.

Qual instrução de programa deve ser executada em seguida, o microprocessador aprende com o conteúdo do cs: (e) par de registradores ip:

1) cs - registrador de segmento de código, que contém o endereço físico (base) do segmento de código atual;

2) eip/ip - registrador de ponteiro de instrução, que contém um valor que representa o deslocamento na memória da próxima instrução a ser executada em relação ao início do segmento de código atual.

Qual registrador específico será usado depende do modo de endereçamento definido use16 ou use32. Se usar 16 for especificado, então ip será usado, se usar32, então eip será usado.

Assim, as instruções de transferência de controle alteram o conteúdo dos registradores cs e eip/ip, como resultado do qual o microprocessador seleciona para execução não a próxima instrução do programa em ordem, mas a instrução em alguma outra seção do programa. O pipeline dentro do microprocessador é redefinido.

De acordo com o princípio de operação, os comandos do microprocessador que fornecem a organização das transições no programa podem ser divididos em 3 grupos:

1. Transferência incondicional de comandos de controle:

1) um comando de ramal incondicional;

2) um comando para chamar um procedimento e retornar de um procedimento;

3) um comando para chamar interrupções de software e retornar das interrupções de software.

2. Comandos para transferência condicional de controle:

1) comandos de salto pelo resultado do comando de comparação p;

2) comandos de transição de acordo com o estado de um determinado sinalizador;

3) instruções para pular o conteúdo do registrador esx/cx.

3. Comandos de controle de ciclo:

1) um comando para organizar um ciclo com um contador ехх/сх;

2) um comando para organizar um ciclo com um contador ех/сх com possibilidade de saída antecipada do ciclo por uma condição adicional.

Saltos incondicionais

A discussão anterior revelou alguns detalhes do mecanismo de transição. As instruções de salto modificam o registrador de ponteiro de instrução eip/ip e possivelmente o registrador de segmento de código cs. O que exatamente precisa ser modificado depende de:

1) sobre o tipo de operando na instrução de desvio incondicional (próximo ou distante);

2) de especificar um modificador antes do endereço de salto (na instrução de salto); neste caso, o próprio endereço de salto pode estar localizado diretamente na instrução (salto direto), ou em um registrador ou célula de memória (salto indireto).

O modificador pode assumir os seguintes valores:

1) near ptr - transição direta para um rótulo dentro do segmento de código atual. Somente o registro eip/ip é modificado (dependendo do tipo de segmento de código use16 ou use32 especificado) com base no endereço (label) especificado no comando ou em uma expressão usando o símbolo de extração de valor - $;

2) far ptr - transição direta para um rótulo em outro segmento de código. O endereço de salto é especificado como um operando ou endereço imediato (rótulo) e consiste em um seletor de 16 bits e um deslocamento de 16/32 bits, que são carregados nos registradores cs e ip/eip, respectivamente;

3) word ptr - transição indireta para um rótulo dentro do segmento de código atual. Apenas eip/ip é modificado (pelo valor de deslocamento da memória no endereço especificado no comando ou de um registrador). Tamanho de deslocamento 16 ou 32 bits;

4) dword ptr - transição indireta para um rótulo em outro segmento de código. Ambos os registradores - cs e eip/ip - são modificados (por um valor da memória - e somente da memória, a partir de um registrador). A primeira palavra/dword deste endereço representa o deslocamento e é carregada em ip/eip; a segunda/terceira palavra é carregada em cs. jmp instrução de salto incondicional

A sintaxe do comando para um salto incondicional é jmp [modificador] jump_address - um salto incondicional sem salvar informações sobre o ponto de retorno.

Jump_address é o endereço na forma de um rótulo ou o endereço da área de memória na qual o ponteiro de salto está localizado.

No total, no sistema de instruções do microprocessador existem vários códigos de instruções de máquina para o salto incondicional jmp.

Suas diferenças são determinadas pela distância de transição e pela forma como o endereço de destino é especificado. A distância do salto é determinada pela localização do operando jump_address. Este endereço pode estar no segmento de código atual ou em algum outro segmento. No primeiro caso, a transição é chamada de intra-segmento, ou próxima, no segundo - inter-segmento, ou distante. Um salto intra-segmento assume que apenas o conteúdo do registrador eip/ip é alterado.

Existem três opções para uso intra-segmento do comando jmp:

1) curto reto;

2) reto;

3) indireto.

Procedimentos

A linguagem assembly possui várias ferramentas que resolvem o problema de duplicar seções de código. Esses incluem:

1) mecanismo de procedimentos;

2) montador de macros;

3) mecanismo de interrupção.

Um procedimento, muitas vezes também chamado de sub-rotina, é a unidade funcional básica para decompor (dividir em várias partes) uma tarefa. Um procedimento é um grupo de comandos para resolver uma subtarefa específica e tem como meio de receber o controle do ponto em que a tarefa é chamada em um nível superior e retornar o controle a esse ponto.

No caso mais simples, o programa pode consistir em um único procedimento. Em outras palavras, um procedimento pode ser definido como um conjunto bem formado de comandos, que, sendo descritos uma vez, podem ser chamados em qualquer lugar do programa, se necessário.

Para descrever uma sequência de comandos como um procedimento em linguagem assembly, duas diretivas são usadas: PROC e ENDP.

A sintaxe da descrição do procedimento é a seguinte (Fig. 36).

Arroz. 36. Sintaxe da descrição do procedimento no programa

A Figura 36 mostra que no cabeçalho do procedimento (diretiva PROC), apenas o nome do procedimento é obrigatório. Dentre o grande número de operandos da diretiva PROC, deve-se destacar [distance]. Esse atributo pode levar os valores próximos ou distantes e caracteriza a possibilidade de chamar o procedimento de outro segmento de código. Por padrão, o atributo [distance] é definido como próximo.

O procedimento pode ser colocado em qualquer lugar do programa, mas de forma que não seja controlado aleatoriamente. Se o procedimento for simplesmente inserido no fluxo de instruções geral, o microprocessador perceberá as instruções do procedimento como parte desse fluxo e, consequentemente, executará as instruções do procedimento.

Saltos condicionais

O microprocessador possui 18 instruções de salto condicional. Esses comandos permitem que você verifique:

1) a relação entre operandos com um signo ("maior - menor");

2) a relação entre operandos sem sinal ("superior - inferior");

3) estados dos sinalizadores aritméticos ZF, SF, CF, OF, PF (mas não AF).

Os comandos de salto condicional têm a mesma sintaxe:

jcc jump_label

Como você pode ver, o código mnemônico de todos os comandos começa com "j" - da palavra jump (jump), ele - determina a condição específica analisada pelo comando.

Quanto ao operando jump_label, este rótulo só pode ser localizado dentro do segmento de código atual; a transferência de controle entre segmentos em saltos condicionais não é permitida. A esse respeito, não há dúvida sobre o modificador, que estava presente na sintaxe dos comandos de salto incondicional. Nos primeiros modelos do microprocessador (i8086, i80186 e i80286), as instruções de desvio condicional só podiam executar saltos curtos - de -128 a +127 bytes da instrução seguinte à instrução de desvio condicional. A partir do modelo de microprocessador 80386, essa restrição é removida, mas, como você pode ver, apenas dentro do segmento de código atual.

Para tomar uma decisão sobre onde o controle será transferido para o comando de salto condicional, uma condição deve primeiro ser formada, com base na qual a decisão de transferir o controle será tomada.

As fontes de tal condição podem ser:

1) qualquer comando que altere o estado dos sinalizadores aritméticos;

2) a instrução de comparação p, que compara os valores de dois operandos;

3) o estado do registro esx/cx.

comando de comparação cmp

O comando de comparação de página tem uma maneira interessante de trabalhar. É exatamente o mesmo que o comando de subtração - sub operando, operando_2.

A instrução p, como a subinstrução, subtrai operandos e define sinalizadores. A única coisa que não faz é escrever o resultado da subtração no lugar do primeiro operando.

A sintaxe de comando str - str operando_1, operando_2 (comparar) - compara dois operandos e define sinalizadores com base nos resultados da comparação.

Os sinalizadores definidos pelo comando p podem ser analisados ​​por instruções especiais de desvio condicional. Antes de analisá-los, vamos prestar um pouco de atenção aos mnemônicos dessas instruções de salto condicional (Tabela 16). Compreender a notação ao formar o nome dos comandos de salto condicional (o elemento no nome do comando jcc, nós o designamos) facilitará sua memorização e posterior uso prático.

Tabela 16. Significado das abreviações no nome do comando jcc Tabela 17. Lista de comandos de salto condicional para o comando p operando_1, operando_2

Não se surpreenda com o fato de que vários códigos mnemônicos diferentes de comandos de desvio condicional correspondem aos mesmos valores de flag (eles são separados uns dos outros por uma barra na Tabela 17). A diferença de nome se deve ao desejo dos desenvolvedores de microprocessadores de facilitar o uso de instruções de salto condicional em combinação com certos grupos de instruções. Portanto, nomes diferentes refletem uma orientação funcional diferente. No entanto, o fato de esses comandos responderem aos mesmos sinalizadores os torna absolutamente equivalentes e iguais no programa. Portanto, na Tabela 17 eles estão agrupados não por nome, mas pelos valores dos sinalizadores (condições) aos quais respondem.

Instruções e sinalizadores de desvio condicional

A designação mnemônica de algumas instruções de salto condicional reflete o nome do sinalizador com o qual eles trabalham e tem a seguinte estrutura: o primeiro caractere é "j" (Saltar, pular), o segundo é a designação do sinalizador ou o caractere de negação " n", seguido do nome do sinalizador . Essa estrutura de equipe reflete seu propósito. Se não houver nenhum caractere "n", o estado do sinalizador será verificado, se for igual a 1, será feita uma transição para o rótulo de salto. Se o caractere "n" estiver presente, o estado do sinalizador será verificado quanto à igualdade com 0 e, se for bem-sucedido, será feito um salto para o rótulo de salto.

Mnemônicos de comando, nomes de sinalizadores e condições de salto são mostrados na Tabela 18. Esses comandos podem ser usados ​​após qualquer comando que modifique os sinalizadores especificados.

Tabela 18. Instruções e sinalizadores de salto condicional

Se você observar atentamente as tabelas 17 e 18, poderá ver que muitas das instruções de salto condicional nelas são equivalentes, pois ambas são baseadas na análise dos mesmos sinalizadores.

Instruções de salto condicional e o registro esx/cx

A arquitetura do microprocessador envolve o uso específico de muitos registradores. Por exemplo, o registrador EAX/AX/AL é usado como acumulador, e os registradores BP, SP são usados ​​para trabalhar com a pilha. O registrador ECX/CX também tem um certo propósito funcional: ele atua como um contador em comandos de controle de loop e ao trabalhar com cadeias de caracteres. É possível que funcionalmente a instrução de desvio condicional associada ao registrador esx/cx seja atribuída mais corretamente a este grupo de instruções.

A sintaxe para esta instrução de desvio condicional é:

1) jcxz jump_label (Saltar se ex for zero) - pular se cx for zero;

2) jecxz jump_label (Jump Equal ех Zero) - salta se ех for zero.

Esses comandos são muito úteis ao fazer loops e ao trabalhar com cadeias de caracteres.

Deve-se notar que há uma limitação inerente ao comando jcxz/jecxz. Ao contrário de outras instruções de transferência condicional, a instrução jcxz/jecxz só pode endereçar saltos curtos -128 bytes ou +127 bytes da instrução seguinte.

Organização de ciclos

O ciclo, como você sabe, é uma estrutura algorítmica importante, sem o uso do qual, provavelmente, nenhum programa pode fazer. Você pode organizar a execução cíclica de uma determinada seção do programa, por exemplo, usando a transferência condicional de comandos de controle ou o comando de salto incondicional jmp. Com esse ciclo de organização, todas as operações para sua organização são realizadas manualmente. Mas, dada a importância de um elemento algorítmico como um ciclo, os desenvolvedores do microprocessador introduziram um grupo de três comandos no sistema de instruções, o que facilita a programação dos ciclos. Essas instruções também usam o registrador esx/cx como um contador de loop.

Vamos dar uma breve descrição desses comandos:

1) loop transaction_label (Loop) - repita o ciclo. O comando permite organizar loops semelhantes aos loops for em linguagens de alto nível com decremento automático do contador de loops. O trabalho da equipe é fazer o seguinte:

a) decremento do registro ECX/CX;

b) comparação do registrador ECX/CX com zero: se (ECX/CX) = 0, então o controle é transferido para o próximo comando após a malha;

2) loope/loopz jump_label

Os comandos loope e loopz são sinônimos absolutos. O trabalho dos comandos é executar as seguintes ações:

a) decremento do registro ECX/CX;

b) comparação do registro ECX/CX com zero;

c) análise do estado do sinalizador zero ZF se (ECX/CX) = 0 ou XF = 0, o controle é transferido para o próximo comando após a malha.

3) loopne/loopnz jump_label

Os comandos loopne e loopnz também são sinônimos absolutos. O trabalho dos comandos é executar as seguintes ações:

a) decremento do registro ECX/CX;

b) comparação do registro ECX/CX com zero;

c) análise do estado da bandeira zero ZF: se (ECX/CX) = 0 ou ZF = 1, o controle é transferido para o próximo comando após a malha.

Os comandos loope/loopz e loopne/loopnz são recíprocos em sua operação. Eles estendem a ação do comando loop analisando adicionalmente o sinalizador zf, o que torna possível organizar uma saída antecipada do loop, usando esse sinalizador como indicador.

A desvantagem dos comandos de loop loop, loope/loopz e loopne/loopnz é que eles implementam apenas saltos curtos (de -128 a +127 bytes). Para trabalhar com loops longos, você precisará usar saltos condicionais e a instrução jmp, então tente dominar as duas formas de organizar loops.

Autor: Tsvetkova A.V.

Recomendamos artigos interessantes seção Notas de aula, folhas de dicas:

Microeconomia. Berço

Mercado de ações e bods. Berço

Doenças oculares. Berço

Veja outros artigos seção Notas de aula, folhas de dicas.

Leia e escreva útil comentários sobre este artigo.

<< Voltar

Últimas notícias de ciência e tecnologia, nova eletrônica:

Couro artificial para emulação de toque 15.04.2024

Em um mundo tecnológico moderno, onde a distância está se tornando cada vez mais comum, é importante manter a conexão e uma sensação de proximidade. Os recentes desenvolvimentos em pele artificial por cientistas alemães da Universidade de Saarland representam uma nova era nas interações virtuais. Pesquisadores alemães da Universidade de Saarland desenvolveram filmes ultrafinos que podem transmitir a sensação do toque à distância. Esta tecnologia de ponta oferece novas oportunidades de comunicação virtual, especialmente para aqueles que estão longe de seus entes queridos. As películas ultrafinas desenvolvidas pelos investigadores, com apenas 50 micrómetros de espessura, podem ser integradas em têxteis e usadas como uma segunda pele. Esses filmes atuam como sensores que reconhecem sinais táteis da mãe ou do pai e como atuadores que transmitem esses movimentos ao bebê. O toque dos pais no tecido ativa sensores que reagem à pressão e deformam o filme ultrafino. Esse ... >>

Areia para gatos Petgugu Global 15.04.2024

Cuidar de animais de estimação muitas vezes pode ser um desafio, especialmente quando se trata de manter a casa limpa. Foi apresentada uma nova solução interessante da startup Petgugu Global, que vai facilitar a vida dos donos de gatos e ajudá-los a manter a sua casa perfeitamente limpa e arrumada. A startup Petgugu Global revelou um banheiro exclusivo para gatos que pode liberar fezes automaticamente, mantendo sua casa limpa e fresca. Este dispositivo inovador está equipado com vários sensores inteligentes que monitoram a atividade higiênica do seu animal de estimação e são ativados para limpeza automática após o uso. O dispositivo se conecta à rede de esgoto e garante a remoção eficiente dos resíduos sem a necessidade de intervenção do proprietário. Além disso, o vaso sanitário tem uma grande capacidade de armazenamento lavável, tornando-o ideal para famílias com vários gatos. A tigela de areia para gatos Petgugu foi projetada para uso com areias solúveis em água e oferece uma variedade de recursos adicionais ... >>

A atratividade de homens atenciosos 14.04.2024

O estereótipo de que as mulheres preferem “bad boys” já é difundido há muito tempo. No entanto, pesquisas recentes conduzidas por cientistas britânicos da Universidade Monash oferecem uma nova perspectiva sobre esta questão. Eles observaram como as mulheres respondiam à responsabilidade emocional e à disposição dos homens em ajudar os outros. As descobertas do estudo podem mudar a nossa compreensão sobre o que torna os homens atraentes para as mulheres. Um estudo conduzido por cientistas da Universidade Monash leva a novas descobertas sobre a atratividade dos homens para as mulheres. Na experiência, foram mostradas às mulheres fotografias de homens com breves histórias sobre o seu comportamento em diversas situações, incluindo a sua reação ao encontro com um sem-abrigo. Alguns dos homens ignoraram o sem-abrigo, enquanto outros o ajudaram, como comprar-lhe comida. Um estudo descobriu que os homens que demonstraram empatia e gentileza eram mais atraentes para as mulheres do que os homens que demonstraram empatia e gentileza. ... >>

Notícias aleatórias do Arquivo

As geleiras do Ártico estão cheias de vida 04.06.2023

Um novo estudo provou que o gelo do Ártico não é tão sem vida quanto pode parecer à primeira vista.

Pode parecer que as geleiras do Ártico estão completamente desprovidas de vida, mas os cientistas garantem que não é o caso. Na verdade, os tapetes de gelo e neve na Groenlândia e na Islândia estão literalmente cheios de formas de vida microscópicas.

Além disso, como zumbis sazonais, muitos desses organismos hibernam e acordam de um sono gelado com o início do verão e o derretimento das geleiras. De acordo com um microbiologista da Universidade de Aarhus, na Suécia, Alexander Anesio, mesmo em uma pequena poça de água derretida de uma geleira, quase 4 espécies diferentes podem ser facilmente acordadas.

Os pesquisadores observam que, na maioria das vezes, esses organismos microscópicos prosperam em bactérias, algas, vírus e fungos microscópicos. Na verdade, eles são todo um ecossistema, cuja existência os pesquisadores nada sabiam há muito tempo.

Tudo isso mudou quando os cientistas estudaram o gelo e a neve em duas geleiras no meio do verão, uma na Islândia e outra na Groenlândia. A maioria das bactérias descobertas pelos cientistas estava ativa, outras não se moviam ou estavam mortas. Mas o interessante é que os cientistas descobriram que um dia depois de serem descongelados, alguns desses “micróbios adormecidos” reviveram e recuperaram a capacidade de ler genes e produzir os blocos de construção dos aminoácidos. Os resultados do estudo indicam que, após três dias de descongelamento em laboratório, as amostras continham 35% a mais de micróbios ativos.

Os dados obtidos pelos pesquisadores sugerem que as comunidades microbianas na neve e no gelo podem, de fato, responder rapidamente ao derretimento. Adaptar-se às mudanças climáticas é geralmente considerado muito benéfico, disse Anesio, mas também significa que uma mudança repentina em um organismo pode desestabilizar todo um ecossistema.

Outras notícias interessantes:

▪ iSuppli prevê uma desaceleração no mercado de eletrônicos de consumo

▪ Malária atrai mosquitos para humanos

▪ Os corações dos cantores estão em sincronia

▪ A obesidade hereditária nem sempre é determinada por genes

▪ Resolvido o mistério da fábrica de cogumelos naturais

Feed de notícias de ciência e tecnologia, nova eletrônica

 

Materiais interessantes da Biblioteca Técnica Gratuita:

▪ seção do site Sistemas acústicos. Seleção de artigos

▪ artigo Planador NK-24. dicas para modelista

▪ artigo Onde está a tradição hooligan de queimar uma enorme cabra de Natal? Resposta detalhada

▪ artigo Desmurgia, ciência e bandagem. Assistência médica

▪ artigo Conversor analógico-digital de uma placa de som. Enciclopédia de rádio eletrônica e engenharia elétrica

▪ artigo Radiotelefones e tudo sobre eles. Enciclopédia de rádio eletrônica e engenharia elétrica

Deixe seu comentário neste artigo:

Имя:


E-mail opcional):


Comentário:





Todos os idiomas desta página

Página principal | Biblioteca | Artigos | Mapa do Site | Revisões do site

www.diagrama.com.ua

www.diagrama.com.ua
2000-2024