Menu English Ukrainian Russo Casa

Libreria tecnica gratuita per hobbisti e professionisti Libreria tecnica gratuita


Appunti delle lezioni, cheat sheet
Libreria gratuita / Elenco / Appunti delle lezioni, cheat sheet

Informatica e tecnologie dell'informazione. Appunti delle lezioni: in breve, il più importante

Appunti delle lezioni, cheat sheet

Elenco / Appunti delle lezioni, cheat sheet

Commenti sull'articolo Commenti sull'articolo

Sommario

  1. Introduzione all'informatica (Informatica. Informazione. Presentazione ed elaborazione dell'informazione. Sistemi numerici. Rappresentazione dei numeri in un computer. Concetto formalizzato di algoritmo)
  2. Linguaggio Pascal (Introduzione al Pascal. Procedure e funzioni standard. Operatori Pascal)
  3. Procedure e funzioni (Il concetto di algoritmo ausiliario. Procedure in Pascal. Funzioni in Pascal. Descrizioni anticipate e connessione di subroutine. Direttiva)
  4. Subroutine (Parametri di routine. Tipi di parametri di subroutine. Tipo stringa in Pascal. Procedure e funzioni per variabili di tipo stringa. Record. Insiemi)
  5. file (File. Operazioni sui file. Moduli. Tipi di moduli)
  6. Memoria dinamica (Tipo di dati di riferimento. Memoria dinamica. Variabili dinamiche. Utilizzo della memoria dinamica. Puntatori non tipizzati)
  7. Strutture dati astratte (Strutture dati astratte. Stack. Code)
  8. Strutture di dati ad albero (Strutture dati degli alberi. Operazioni sugli alberi. Esempi di implementazione delle operazioni)
  9. Conta (Il concetto di grafico. Metodi di rappresentazione di un grafico. Rappresentazione di un grafico mediante una lista di incidenze. Algoritmo di attraversamento in profondità per un grafico. Rappresentazione di un grafico come elenco di liste. Algoritmo di attraversamento in larghezza per un grafico )
  10. Tipo di dati dell'oggetto (Tipo di oggetto in Pascal. Il concetto di oggetto, la sua descrizione e utilizzo. Ereditarietà. Creazione di istanze di oggetti. Componenti e ambito)
  11. Методы (Metodi. Costruttori e distruttori. Distruttori. Metodi virtuali. Campi dati oggetto e parametri del metodo formale)
  12. Compatibilità del tipo di oggetto (Incapsulamento. Oggetti estensibili. Compatibilità del tipo di oggetto)
  13. Assemblatore (Informazioni sull'assembler. Modello software del microprocessore. Registri utente. Registri di uso generale. Registri di segmento. Registri di stato e di controllo)
  14. Registri (Registri di sistema del microprocessore. Registri di controllo. Registri di indirizzo di sistema. Registri di debug)
  15. Programmi di assemblaggio (Struttura del programma assembler. Sintassi assembler. Operatori di confronto. Operatori e loro precedenza. Direttive semplificate per la definizione dei segmenti. Identificatori creati dalla direttiva MODEL. Modelli di memoria. Modificatori del modello di memoria)
  16. Strutture di istruzioni di assemblaggio (Struttura di un'istruzione macchina. Metodi per specificare gli operandi dell'istruzione. Metodi di indirizzamento)
  17. comandi (Comandi di trasferimento dati. Comandi aritmetici)
  18. Comandi di trasferimento di controllo (Comandi logici. Tabella di verità per negazione logica. Tabella di verità per OR logico inclusivo. Tabella di verità per AND logico. Tabella di verità per OR logico esclusivo. Significato delle abbreviazioni nel nome del comando jcc. Elenco dei comandi di salto condizionale per il comando. Salto condizionale comandi e flag)

LEZIONE N. 1. Introduzione all'informatica

1. Informatica. Informazione. Rappresentazione ed elaborazione delle informazioni

L'informatica è impegnata in una rappresentazione formalizzata degli oggetti e delle strutture delle loro relazioni in vari campi della scienza, della tecnologia e della produzione. Vari strumenti formali vengono utilizzati per modellare oggetti e fenomeni, come formule logiche, strutture dati, linguaggi di programmazione, ecc.

In informatica, un concetto fondamentale come l'informazione ha diversi significati:

1) presentazione formale di forme informative esterne;

2) significato astratto dell'informazione, suo contenuto interno, semantica;

3) relazione dell'informazione con il mondo reale.

Ma, di regola, l'informazione è intesa come il suo significato astratto: la semantica. Interpretando la rappresentazione dell'informazione, otteniamo il suo significato, la semantica. Pertanto, se vogliamo scambiare informazioni, abbiamo bisogno di punti di vista coerenti in modo che non venga violata la correttezza dell'interpretazione. Per fare ciò, l'interpretazione della rappresentazione delle informazioni viene identificata con alcune strutture matematiche. In questo caso, l'elaborazione delle informazioni può essere eseguita con metodi matematici rigorosi.

Una delle descrizioni matematiche dell'informazione è la sua rappresentazione sotto forma di una funzione y =f(x, t), dove t è il tempo, x è un punto in un determinato campo in cui viene misurato il valore di y. A seconda dei parametri della funzione chi (le informazioni possono essere classificate.

Se i parametri sono grandezze scalari che assumono una serie continua di valori, l'informazione così ottenuta viene chiamata continua (o analogica). Se ai parametri viene assegnato un determinato passaggio di modifica, le informazioni vengono chiamate discrete. L'informazione discreta è considerata universale, poiché per ogni parametro specifico è possibile ottenere un valore di funzione con un determinato grado di accuratezza.

L'informazione discreta è solitamente identificata con l'informazione digitale, che è un caso speciale di informazione simbolica di rappresentazione alfabetica. Un alfabeto è un insieme finito di simboli di qualsiasi natura. Molto spesso in informatica si verifica una situazione in cui i caratteri di un alfabeto devono essere rappresentati dai caratteri di un altro, cioè deve essere eseguita un'operazione di codifica. Se il numero di caratteri dell'alfabeto di codifica è inferiore al numero di caratteri dell'alfabeto di codifica, l'operazione di codifica stessa non è complicata, altrimenti è necessario utilizzare un insieme fisso di caratteri dell'alfabeto di codifica per una codifica corretta e univoca.

Come ha dimostrato la pratica, l'alfabeto più semplice che ti consente di codificare altri alfabeti è binario, composto da due caratteri, che di solito sono indicati da 0 e 1. Usando n caratteri dell'alfabeto binario, puoi codificare 2n caratteri, e questo è sufficiente per codificare qualsiasi alfabeto.

Il valore che può essere rappresentato da un simbolo dell'alfabeto binario è chiamato unità minima di informazione o bit. Sequenza di 8 bit - byte. Un alfabeto contenente 256 diverse sequenze a 8 bit è chiamato alfabeto di byte.

Come standard oggi in informatica, viene adottato un codice in cui ogni carattere è codificato da 1 byte. Ci sono anche altri alfabeti.

2. Sistemi numerici

Un sistema numerico è un insieme di regole per la denominazione e la scrittura di numeri. Esistono sistemi numerici posizionali e non posizionali.

Il sistema numerico è chiamato posizionale se il valore della cifra del numero dipende dalla posizione della cifra nel numero. In caso contrario, è chiamato non posizionale. Il valore di un numero è determinato dalla posizione di queste cifre nel numero.

3. Rappresentazione di numeri in un computer

I processori a 32 bit possono funzionare con un massimo di 232-1 RAM e gli indirizzi possono essere scritti nell'intervallo 00000000 - FFFFFFFF. Tuttavia, in modalità reale, il processore funziona con memoria fino a 220-1 e gli indirizzi rientrano nell'intervallo 00000 - FFFFF. I byte di memoria possono essere combinati in campi di lunghezza sia fissa che variabile. Una parola è un campo di lunghezza fissa composto da 2 byte, una doppia parola è un campo di 4 byte. Gli indirizzi di campo possono essere pari o dispari, con gli indirizzi pari che eseguono operazioni più velocemente.

I numeri a virgola fissa sono rappresentati nei computer come numeri binari interi e la loro dimensione può essere 1, 2 o 4 byte.

Gli interi binari sono rappresentati in complemento a due, mentre i numeri in virgola fissa sono rappresentati in complemento a due. Inoltre, se un numero occupa 2 byte, la struttura del numero viene scritta secondo la seguente regola: la cifra più significativa è assegnata al segno del numero e il resto alle cifre binarie del numero. Il codice complementare di un numero positivo è uguale al numero stesso, mentre il codice complementare di un numero negativo può essere ottenuto utilizzando la seguente formula: x = 10i - \x\, dove n è la capacità in cifre del numero.

Nel sistema dei numeri binari, un codice aggiuntivo si ottiene invertendo i bit, ovvero sostituendo le unità con zeri e viceversa, e aggiungendo uno al bit meno significativo.

Il numero di bit della mantissa determina la precisione della rappresentazione dei numeri, il numero di bit dell'ordine macchina determina l'intervallo di rappresentazione dei numeri in virgola mobile.

4. Concetto formalizzato di algoritmo

Un algoritmo può esistere solo se, allo stesso tempo, esiste un oggetto matematico. Il concetto formalizzato di algoritmo è connesso con il concetto di funzioni ricorsive, normali algoritmi di Markov, macchine di Turing.

In matematica, una funzione è chiamata a valore singolo se, per qualsiasi insieme di argomenti, esiste una legge in base alla quale viene determinato un valore univoco della funzione. Un algoritmo può agire come tale; in questo caso la funzione si dice calcolabile.

Le funzioni ricorsive sono una sottoclasse di funzioni calcolabili e gli algoritmi che definiscono il calcolo sono chiamati algoritmi di funzione ricorsiva complementare. In primo luogo, vengono fissate le funzioni ricorsive di base, per le quali l'algoritmo di accompagnamento è banale, non ambiguo; quindi vengono introdotte tre regole: operatori di sostituzione, ricorsione e minimizzazione, con l'aiuto dei quali si ottengono funzioni ricorsive più complesse sulla base di funzioni di base.

Le funzioni di base e gli algoritmi di accompagnamento possono essere:

1) una funzione di n variabili indipendenti, identicamente uguale a zero. Quindi, se il segno della funzione è φn, allora indipendentemente dal numero di argomenti, il valore della funzione dovrebbe essere posto uguale a zero;

2) la funzione identità di n variabili indipendenti della forma ψni. Quindi, se il segno della funzione è ψni, allora il valore della funzione dovrebbe essere preso come valore dell'i-esimo argomento, contando da sinistra a destra;

3) Λ è una funzione di un argomento indipendente. Quindi, se il segno della funzione è λ, allora il valore della funzione dovrebbe essere preso come il valore che segue il valore dell'argomento. Diversi studiosi hanno proposto i propri approcci al formalizzato

rappresentazione dell'algoritmo. Ad esempio, lo scienziato americano Church ha suggerito che la classe delle funzioni calcolabili è esaurita dalle funzioni ricorsive e, di conseguenza, qualunque sia l'algoritmo che elabora un insieme di interi non negativi in ​​un altro, esiste un algoritmo che accompagna la funzione ricorsiva che è equivalente a quello dato. Pertanto, se è impossibile costruire una funzione ricorsiva per risolvere un determinato problema, non esiste un algoritmo per risolverlo. Un altro scienziato, Turing, ha sviluppato un computer virtuale che ha elaborato una sequenza di input di caratteri in un output. A questo proposito, ha avanzato la tesi che qualsiasi funzione calcolabile è calcolabile di Turing.

CONFERENZA N. 2. Linguaggio Pascal

1. Introduzione al linguaggio Pascal

I simboli di base della lingua - lettere, numeri e caratteri speciali - costituiscono il suo alfabeto. Il linguaggio Pascal include il seguente insieme di simboli di base:

1) 26 lettere minuscole latine e 26 lettere maiuscole latine:

ABCDEFGHIJKLMNOPQRSTUVWXYZ

abcdefghijklmnopqrstuvwxyz;

2) _ (sottolineatura);

3) 10 cifre: 0123456789;

4) segni di operazioni:

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

5) limitatori:

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

6) identificatori: ^ # $;

7) parole di servizio (riservate):

ABSOLUTE, ASSEMBLER E, ARRAY, ASM, BEGIN, CASE, CONST, CONSTRUCTOR, DESTRUCTOR, DIV, DO, DOWNTO, ELSE, END, EXPORT, EXTERNAL, FAR, FILE, FOR, FORWARD, FUNCTION, GOTO, IF, IMPLEMENTATION, IN, INDEX, EREDITO, INLINE, INTERFACCIA, INTERRUZIONE, ETICHETTA, BIBLIOTECA, MOD, NOME, NIL, VICINO, NON, OGGETTO, DI, O, CONFEZIONATO, PRIVATO, PROCEDURA, PROGRAMMA, PUBBLICO, REGISTRAZIONE, RIPETIZIONE, RESIDENTE, IMPOSTAZIONE, SHL, SHR, STRING, THEN, TO, TYPE, UNIT, UNTIL, USI, VAR, VIRTUALE, WHILE, WITH, XOR.

Oltre a quelli elencati, l'insieme dei caratteri di base comprende uno spazio. Gli spazi non possono essere utilizzati all'interno di caratteri doppi e parole riservate.

Digitare il concetto per i dati

È consuetudine in matematica classificare le variabili in base ad alcune caratteristiche importanti. Viene fatta una netta distinzione tra variabili reali, complesse e logiche, tra variabili che rappresentano valori individuali e un insieme di valori, ecc. Quando si elaborano dati su un computer, tale classificazione è ancora più importante. In qualsiasi linguaggio algoritmico, ogni costante, variabile, espressione o funzione è di un tipo particolare.

C'è una regola in Pascal: il tipo è esplicitamente specificato nella dichiarazione di una variabile o di una funzione che ne precede l'uso. Il concetto di tipo Pascal ha le seguenti proprietà principali:

1) qualsiasi tipo di dato definisce un insieme di valori a cui appartiene una costante, che può assumere una variabile o un'espressione, oppure può produrre un'operazione o una funzione;

2) il tipo di valore dato da una costante, variabile o espressione può essere determinato dalla loro forma o descrizione;

3) ogni operazione o funzione richiede argomenti di tipo fisso e produce un risultato di tipo fisso.

Ne consegue che il compilatore può utilizzare le informazioni sul tipo per verificare la computabilità e la correttezza di vari costrutti.

Il tipo definisce:

1) possibili valori di variabili, costanti, funzioni, espressioni appartenenti ad un dato tipo;

2) la forma interna di presentazione dei dati in un computer;

3) operazioni e funzioni eseguibili su valori appartenenti ad una determinata tipologia.

Va notato che la descrizione obbligatoria del tipo porta alla ridondanza nel testo dei programmi, ma tale ridondanza è un importante strumento ausiliario per lo sviluppo di programmi ed è considerata una proprietà necessaria dei moderni linguaggi algoritmici di alto livello.

Esistono tipi di dati scalari e strutturati in Pascal. I tipi scalari includono tipi standard e tipi definiti dall'utente. I tipi standard includono i tipi intero, reale, carattere, booleano e indirizzo.

I tipi interi definiscono costanti, variabili e funzioni i cui valori sono realizzati dall'insieme di numeri interi consentiti in un determinato computer.

I tipi reali definiscono quei dati che sono implementati da un sottoinsieme di numeri reali consentiti in un determinato computer.

I tipi definiti dall'utente sono enum e range. I tipi strutturati sono disponibili in quattro versioni: array, set, record e file.

Oltre a quelli elencati, Pascal include altri due tipi: procedurale e oggetto.

Un'espressione linguistica è costituita da costanti, variabili, puntatori a funzione, segni di operatore e parentesi. Un'espressione definisce una regola per il calcolo di un valore. L'ordine di calcolo è determinato dalla precedenza (priorità) delle operazioni in esso contenute. Pascal ha la seguente precedenza di operatore:

1) calcoli tra parentesi;

2) calcolo dei valori delle funzioni;

3) operazioni unarie;

4) operazioni *, /, div, mod e;

5) operazioni +, -, o, xor;

6) operazioni relazionali =, <>, <, >, <=, >=.

Le espressioni fanno parte di molti operatori del linguaggio Pascal e possono anche essere argomenti per funzioni integrate.

2. Procedure e funzioni standard

Funzioni aritmetiche

1. Funzione Abs(X);

Restituisce il valore assoluto del parametro.

X è un'espressione di tipo reale o intero.

2. Funzione ArcTan(X: Esteso): Esteso;

Restituisce l'arcotangente dell'argomento.

X è un'espressione di tipo reale o intero.

3. Funzione exp(X: Reale): Reale;

Restituisce l'esponente.

X è un'espressione di tipo reale o intero.

4.Frac(X: Reale): Reale;

Restituisce la parte frazionaria dell'argomento.

X è un'espressione di tipo reale. Il risultato è la parte frazionaria di X, cioè

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

5. Funzione Int(X: Reale): Reale;

Restituisce la parte intera dell'argomento.

X è un'espressione di tipo reale. Il risultato è la parte intera di X, cioè X arrotondata per zero.

6. Funzione Ln(X: Reale): Reale;

Restituisce il logaritmo naturale (Ln e = 1) di un'espressione di tipo reale X.

7. Funzione Pi: Estesa;

Restituisce il valore Pi, che è definito come 3.1415926535.

8.Funzione Sin(X: Esteso): Esteso;

Restituisce il seno dell'argomento.

X è un'espressione di tipo reale. Sin restituisce il seno dell'angolo X in radianti.

9. Funzione Sqr(X: Esteso): Esteso;

Restituisce il quadrato dell'argomento.

X è un'espressione in virgola mobile. Il risultato è dello stesso tipo di X.

10.Funzione Sqrt(X: Esteso): Esteso;

Restituisce la radice quadrata dell'argomento.

X è un'espressione in virgola mobile. Il risultato è la radice quadrata di X.

Procedure e funzioni di conversione del valore

1. Procedura Str(X [: Larghezza [: Decimali]]; var S);

Converte il numero X in una rappresentazione di stringa in base a

Opzioni di formattazione Larghezza e Decimali. X è un'espressione di tipo reale o intero. Larghezza e Decimali sono espressioni di tipo intero. S è una variabile di tipo String o una matrice di caratteri con terminazione null se è consentita la sintassi estesa.

2. Funzione Chr(X: Byte): Char;

Restituisce il carattere con X ordinale nella tabella ASCII.

3.Funzione alta (X);

Restituisce il valore più grande nell'intervallo del parametro.

4.FunzioneBasso(X);

Restituisce il valore più piccolo nell'intervallo di parametri.

5 FunctionOrd(X): Longint;

Restituisce il valore ordinale di un'espressione di tipo enumerato. X è un'espressione di tipo enumerato.

6. Funzione Round(X: Esteso): Longint;

Arrotonda un valore reale a un numero intero. X è un'espressione di tipo reale. Round restituisce un valore Longint, che è il valore di X arrotondato al numero intero più vicino. Se X è esattamente a metà strada tra due numeri interi, viene restituito il numero con il valore assoluto più grande. Se il valore arrotondato di X non rientra nell'intervallo Longint, viene generato un errore di runtime che è possibile gestire utilizzando l'eccezione EInvalidOp.

7. Funzione Tronc(X: Esteso): Longint;

Tronca un valore di tipo reale a un numero intero. Se il valore arrotondato di X non rientra nell'intervallo Longint, viene generato un errore di runtime che è possibile gestire utilizzando l'eccezione EInvalidOp.

8. Procedura Val(S; var V; var Codice: Intero);

Converte un numero da un valore stringa S in un numero

rappresentazione V. S - espressione di tipo stringa - una sequenza di caratteri che forma un numero intero o reale. Se l'espressione S non è valida, l'indice del carattere non valido viene memorizzato nella variabile Code. In caso contrario, il codice è impostato su zero.

Procedure e funzioni del valore ordinale

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

Sottrae uno o N dalla variabile X. Dec(X) corrisponde a X:= X - 1, e Dec(X, N) corrisponde a X:= X - N. X è una variabile di tipo enumerato o di tipo PChar se estesa la sintassi è consentita e N è un'espressione di tipo intero. La procedura Dec genera codice ottimale ed è particolarmente utile nei cicli lunghi.

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

Aggiunge uno o N alla variabile X. X è una variabile di tipo enumerato o di tipo PChar se è consentita la sintassi estesa e N è un'espressione di tipo integrale. Inc (X) corrisponde all'istruzione X:= X + 1 e Inc (X, N) corrisponde all'istruzione X:= X + N. La procedura Inc genera codice ottimale ed è particolarmente utile nei cicli lunghi.

3. FunctionOdd(X: LongInt): Booleano;

Restituisce True se X è un numero dispari, False in caso contrario.

4.FunzionePred(X);

Restituisce il valore precedente del parametro. X è un'espressione di tipo enumerato. Il risultato è dello stesso tipo.

5 Funzione Succ(X);

Restituisce il valore del parametro successivo. X è un'espressione di tipo enumerato. Il risultato è dello stesso tipo.

3. Operatori linguistici Pascal

Operatore condizionale

Il formato dell'istruzione condizionale completa è definito come segue: Se B allora SI altrimenti S2; dove B è una condizione di ramificazione (processo decisionale), un'espressione logica o una relazione; SI, S2 - un'istruzione eseguibile, semplice o composta.

Quando si esegue un'istruzione condizionale, prima viene valutata l'espressione B, quindi viene analizzato il suo risultato: se B è vero, viene eseguita l'istruzione S1 - il ramo di allora e l'istruzione S2 viene saltata; se B è falso, allora l'istruzione S2 - il ramo else viene eseguito e l'istruzione S1 viene saltata.

Esiste anche una forma abbreviata dell'operatore condizionale. Si scrive: Se B allora S.

Seleziona istruzione

La struttura dell'operatore è la seguente:

caso S di

c1: istruzione1;

c2: istruzione2;

...

cn: istruzioneN;

altra istruzione

fine;

dove S è un'espressione di tipo ordinale il cui valore viene calcolato;

с1, с2..., сп - costanti di tipo ordinale con cui vengono confrontate le espressioni

S; istruzione1,..., istruzioneN - operatori di cui viene eseguito quello la cui costante corrisponde al valore dell'espressione S;

istruzione - un'istruzione che viene eseguita se il valore dell'espressione Sylq non corrisponde a nessuna delle costanti c1, c2.... cn.

Questo operatore è una generalizzazione dell'operatore condizionale If per un numero arbitrario di alternative. C'è una forma abbreviata della dichiarazione dove non c'è nessun altro ramo.

Istruzione di ciclo con parametro

Le istruzioni del ciclo di parametri che iniziano con la parola per fanno sì che l'istruzione, che può essere un'istruzione composta, venga eseguita ripetutamente mentre alla variabile di controllo viene assegnata una sequenza di valori crescente.

Vista generale della dichiarazione for:

per <contatore loop> := da <valore iniziale> a <valore finale> fare <istruzione>;

Quando l'istruzione for inizia l'esecuzione, i valori di inizio e fine vengono determinati una volta e questi valori vengono mantenuti durante l'esecuzione dell'istruzione for. L'istruzione contenuta nel corpo dell'istruzione for viene eseguita una volta per ogni valore nell'intervallo tra i valori di inizio e di fine. Il contatore di loop viene sempre inizializzato su un valore iniziale. Quando l'istruzione for è in esecuzione, il valore del contatore di loop viene incrementato ad ogni iterazione. Se il valore iniziale è maggiore del valore finale, l'istruzione contenuta nel corpo dell'istruzione for non viene eseguita. Quando la parola chiave downto viene utilizzata in un'istruzione di ciclo, il valore della variabile di controllo viene decrementato di uno ad ogni iterazione. Se il valore iniziale in tale istruzione è inferiore al valore finale, l'istruzione contenuta nel corpo dell'istruzione di ciclo non viene eseguita.

Se l'istruzione contenuta nel corpo dell'istruzione for cambia il valore del contatore di loop, si tratta di un errore. Dopo l'esecuzione dell'istruzione for, il valore della variabile di controllo diventa indefinito, a meno che l'esecuzione dell'istruzione for non sia stata interrotta da un'istruzione jump.

Istruzione di ciclo con precondizione

Un'istruzione di ciclo di precondizione (che inizia con la parola chiave while) contiene un'espressione che controlla l'esecuzione ripetuta dell'istruzione (che può essere un'istruzione composta). Forma del ciclo:

Mentre B fa S;

dove B è una condizione logica, la cui verità è verificata (è una condizione per terminare il ciclo);

S - corpo del ciclo - una dichiarazione.

L'espressione che controlla la ripetizione di un'istruzione deve essere di tipo Boolean. Viene valutato prima dell'esecuzione dell'istruzione interna. L'istruzione inner viene eseguita ripetutamente finché l'espressione restituisce True. Se l'espressione restituisce False dall'inizio, l'istruzione contenuta nell'istruzione del ciclo di precondizione non viene eseguita.

Istruzione di ciclo con postcondizione

In un'istruzione di ciclo con una postcondizione (che inizia con la parola repeat), l'espressione che controlla l'esecuzione ripetuta di una sequenza di istruzioni è contenuta all'interno dell'istruzione repeat. Forma del ciclo:

ripetere S fino a B;

dove B è una condizione logica, la cui verità è verificata (è una condizione per terminare il ciclo);

S - una o più istruzioni del corpo del ciclo.

Il risultato dell'espressione deve essere di tipo booleano. Le istruzioni racchiuse tra le parole chiave repeat e until vengono eseguite in sequenza finché il risultato dell'espressione non restituisce True. La sequenza di istruzioni verrà eseguita almeno una volta, poiché l'espressione viene valutata dopo ogni esecuzione della sequenza di istruzioni.

CONFERENZA № 3. Procedure e funzioni

1. Il concetto di algoritmo ausiliario

L'algoritmo di risoluzione dei problemi è progettato scomponendo l'intero problema in sottoattività separate. In genere, le attività secondarie vengono implementate come subroutine.

Una subroutine è un algoritmo ausiliario che viene utilizzato ripetutamente nell'algoritmo principale con valori diversi di alcune quantità in ingresso, chiamate parametri.

Una subroutine nei linguaggi di programmazione è una sequenza di istruzioni che sono definite e scritte in un solo punto del programma, ma possono essere richiamate per l'esecuzione da uno o più punti del programma. Ogni subroutine è identificata da un nome univoco.

Ci sono due tipi di subroutine in Pascal, procedure e funzioni. Una procedura e una funzione sono una sequenza denominata di dichiarazioni e istruzioni. Quando si utilizzano procedure o funzioni, il programma deve contenere il testo della procedura o funzione e una chiamata alla procedura o funzione. I parametri specificati nella descrizione sono detti formali, quelli specificati nella chiamata al sottoprogramma sono detti effettivi. Tutti i parametri formali possono essere suddivisi nelle seguenti categorie:

1) parametri-variabili;

2) parametri costanti;

3) valori-parametro;

4) parametri di procedura e parametri di funzione, ovvero parametri di tipo procedurale;

5) parametri variabili non tipizzati.

I testi delle procedure e delle funzioni sono inseriti nella sezione delle descrizioni delle procedure e delle funzioni.

Passare nomi di procedure e funzioni come parametri

In molti problemi, specialmente in matematica computazionale, è necessario passare come parametri i nomi di procedure e funzioni. Per fare ciò, TURBO PASCAL ha introdotto un nuovo tipo di dati - procedurali o funzionali, a seconda di quanto descritto. (I tipi procedurali e di funzione sono descritti nella sezione relativa alla dichiarazione del tipo.)

Una funzione e un tipo procedurale sono definiti come l'intestazione di una procedura e una funzione con un elenco di parametri formali ma senza nome. È possibile definire una funzione o un tipo procedurale senza parametri, ad esempio:

Digitare

Proc = procedura;

Dopo aver dichiarato un tipo procedurale o funzionale, può essere utilizzato per descrivere parametri formali: i nomi di procedure e funzioni. Inoltre, è necessario scrivere quelle procedure o funzioni reali i cui nomi verranno passati come parametri effettivi.

2. Procedure in Pascal

Ciascuna descrizione della procedura contiene un'intestazione seguita da un blocco di programma. La forma generale dell'intestazione della procedura è la seguente:

Procedura <nome> [(<elenco dei parametri formali>)];

Viene attivata una procedura con un'istruzione di procedura che contiene il nome della procedura ei parametri richiesti. Le istruzioni da eseguire quando viene eseguita la procedura sono contenute nella parte relativa alle istruzioni del modulo della procedura. Se un identificatore di procedura viene utilizzato in un'istruzione contenuta in una procedura all'interno di un modulo di procedura, la procedura verrà eseguita in modo ricorsivo, ovvero si riferirà a se stessa quando viene eseguita.

3. Funzioni in Pascal

Una dichiarazione di funzione definisce la parte del programma in cui il valore viene calcolato e restituito. La forma generale dell'intestazione della funzione è la seguente:

Funzione <nome> [(<elenco dei parametri formali>)]: <tipo di restituzione>;

La funzione viene attivata quando viene chiamata. Quando viene chiamata una funzione, vengono specificati l'identificatore della funzione e tutti i parametri necessari per la sua valutazione. Una chiamata di funzione può essere inclusa nelle espressioni come operando. Quando l'espressione viene valutata, la funzione viene eseguita e il valore dell'operando diventa il valore restituito dalla funzione.

La parte operatore del blocco funzione specifica le istruzioni che devono essere eseguite quando la funzione viene attivata. Un modulo deve contenere almeno un'istruzione di assegnazione che assegna un valore a un identificatore di funzione. Il risultato della funzione è l'ultimo valore assegnato. Se non esiste una tale istruzione di assegnazione, o se non è stata eseguita, il valore di ritorno della funzione non è definito.

Se viene utilizzato un identificatore di funzione quando si chiama una funzione all'interno di un modulo, la funzione viene eseguita in modo ricorsivo.

4. Descrizioni in avanti e collegamento di subroutine. Direttiva

Un programma può contenere più subroutine, cioè la struttura del programma può essere complicata. Tuttavia, queste subroutine possono trovarsi allo stesso livello di annidamento, quindi deve venire prima la dichiarazione di subroutine e quindi la chiamata ad essa, a meno che non venga utilizzata una speciale dichiarazione di inoltro.

Una dichiarazione di procedura che contiene una direttiva forward invece di un blocco di istruzioni è chiamata dichiarazione forward. Da qualche parte dopo questa dichiarazione, una procedura deve essere definita da una dichiarazione di definizione. Una dichiarazione di definizione è quella che utilizza lo stesso identificatore di procedura ma omette l'elenco dei parametri formali e include un blocco di istruzioni. La dichiarazione anticipata e la dichiarazione di definizione devono comparire nella stessa parte della procedura e nelle dichiarazioni di funzione. Tra di loro possono essere dichiarate altre procedure e funzioni che possono fare riferimento alla procedura di dichiarazione anticipata. Pertanto, è possibile la ricorsione reciproca.

La forward description e la definizione di definizione sono la descrizione completa della procedura. La procedura si considera descritta utilizzando una descrizione anticipata.

Se il programma contiene molte subroutine, il programma cesserà di essere visivo, sarà difficile navigare al suo interno. Per evitare ciò, alcune routine vengono memorizzate come file sorgente su disco e, se necessario, vengono collegate al programma principale in fase di compilazione tramite una direttiva di compilazione.

Una direttiva è un commento speciale che può essere inserito ovunque in un programma, dove può trovarsi un commento normale. Si differenziano però per il fatto che la direttiva ha una notazione speciale: subito dopo la parentesi chiusa senza spazio si scrive il segno S e poi, sempre senza spazio, si indica la direttiva.

esempio

1) {SE+} - emula il coprocessore matematico;

2) {SF+} - forma un tipo distante di procedura e chiamata di funzione;

3) {SN+} - usa il coprocessore matematico;

4) {SR+} - controlla se gli intervalli sono fuori limite.

Alcune opzioni di compilazione possono contenere un parametro, ad esempio:

{$1 file name}: include il file denominato nel testo del programma compilato.

CONFERENZA N. 4. Sottoprogrammi

1. Parametri del sottoprogramma

La descrizione di una procedura o funzione specifica un elenco di parametri formali. Ciascun parametro dichiarato in un elenco di parametri formale è locale alla procedura o funzione dichiarata e può essere referenziato dal suo identificatore nel modulo associato a quella procedura o funzione.

Esistono tre tipi di parametri: valore, variabile e variabile non tipizzata. Sono caratterizzati come segue.

1. Un gruppo di parametri senza una parola chiave precedente è un elenco di parametri di valore.

2. Un gruppo di parametri preceduto dalla parola chiave const e seguito da un tipo è un elenco di parametri costanti.

3. Un gruppo di parametri preceduto dalla parola chiave var e seguito da un tipo è un elenco di parametri variabili non tipizzati.

4. Un gruppo di parametri preceduto dalla parola chiave var o const e non seguito da un tipo è un elenco di parametri variabili non tipizzati.

2. Tipi di parametri di subroutine

Parametri di valore

Un parametro di valore formale viene trattato come una variabile locale alla procedura o funzione, tranne per il fatto che deriva il suo valore iniziale dal parametro effettivo corrispondente quando viene richiamata la procedura o la funzione. Le modifiche che subisce un parametro di valore formale non influiscono sul valore del parametro effettivo. Il valore del parametro del valore effettivo corrispondente deve essere un'espressione e il suo valore non deve essere un tipo di file o un tipo di struttura contenente un tipo di file.

Il parametro effettivo deve essere di un tipo di assegnazione compatibile con il tipo del parametro di valore formale. Se il parametro è di tipo stringa, il parametro formale avrà un attributo size di 255.

Parametri costanti

I parametri costanti formali funzionano in modo simile a una variabile locale di sola lettura che ottiene il suo valore quando una procedura o una funzione viene richiamata dal parametro effettivo corrispondente. Non sono consentite assegnazioni a un parametro costante formale. Inoltre, un parametro costante formale non può essere passato come parametro effettivo a un'altra procedura o funzione. Un parametro costante corrispondente a un parametro effettivo in una procedura o un'istruzione di funzione deve seguire le stesse regole del valore del parametro effettivo.

Nei casi in cui un parametro formale non cambia il suo valore durante l'esecuzione di una procedura o di una funzione, è necessario utilizzare un parametro costante al posto di un parametro valore. I parametri costanti consentono l'implementazione di una procedura o funzione per la protezione da assegnazioni accidentali a un parametro formale. Inoltre, per i parametri di tipo struct e string, il compilatore può generare codice più efficiente se utilizzato al posto dei parametri valore per i parametri costanti.

Parametri variabili

Un parametro variabile viene utilizzato quando un valore deve essere passato da una procedura o funzione al programma chiamante. Il parametro effettivo corrispondente in un'istruzione di chiamata di procedura o funzione deve essere un riferimento a una variabile. Quando viene richiamata una procedura o una funzione, la variabile-parametro formale viene sostituita dalla variabile effettiva, eventuali modifiche nel valore della variabile-parametro formale si riflettono nel parametro effettivo.

All'interno di una procedura o funzione, qualsiasi riferimento a un parametro variabile formale comporta l'accesso al parametro stesso. Il tipo del parametro effettivo deve corrispondere al tipo del parametro variabile formale, ma questa restrizione può essere aggirata utilizzando un parametro variabile non tipizzato).

Parametri non tipizzati

Quando il parametro formale è un parametro variabile non tipizzato, il parametro effettivo corrispondente può essere qualsiasi riferimento a una variabile o costante, indipendentemente dal tipo. Un parametro non tipizzato dichiarato con la parola chiave var può essere modificato, mentre un parametro non tipizzato dichiarato con la parola chiave const è di sola lettura.

In una procedura o funzione, un parametro variabile non tipizzato non ha tipo, ovvero è incompatibile con variabili di tutti i tipi finché non gli viene assegnato un tipo specifico in base all'assegnazione del tipo di variabile.

Sebbene i parametri non tipizzati forniscano maggiore flessibilità, esistono alcuni rischi associati al loro utilizzo. Il compilatore non può verificare la validità delle operazioni su variabili non tipizzate.

Variabili procedurali

Dopo aver definito un tipo procedurale, diventa possibile descrivere variabili di questo tipo. Tali variabili sono dette variabili procedurali. Come una variabile intera a cui può essere assegnato un valore di tipo intero, a una variabile procedurale può essere assegnato un valore di tipo procedurale. Il valore potrebbe, ovviamente, essere un'altra variabile di procedura, ma potrebbe anche essere una procedura o un identificatore di funzione. In questo contesto, la dichiarazione di una procedura o funzione può essere vista come una descrizione di un tipo speciale di costante il cui valore è la procedura o la funzione.

Come per qualsiasi altra assegnazione, i valori della variabile sul lato sinistro e sul lato destro devono essere compatibili con l'assegnazione. I tipi procedurali, per essere compatibili con l'assegnazione, devono avere lo stesso numero di parametri e i parametri nelle posizioni corrispondenti devono essere dello stesso tipo. I nomi dei parametri in una dichiarazione di tipo procedurale non hanno effetto.

Inoltre, per garantire la compatibilità dell'assegnazione, una procedura o funzione, se deve essere assegnata ad una variabile di procedura, deve soddisfare i seguenti requisiti:

1) non dovrebbe essere una procedura o una funzione standard;

2) tale procedura o funzione non può essere nidificata;

3) tale procedura non deve essere una procedura in linea;

4) non deve essere una procedura di interruzione.

Procedure e funzioni standard sono le procedure e le funzioni descritte nel modulo Sistema, come Writeln, Readln, Chr, Ord. Non è possibile utilizzare procedure e funzioni nidificate con variabili procedurali. Una procedura o funzione è considerata nidificata quando viene dichiarata all'interno di un'altra procedura o funzione.

L'uso dei tipi procedurali non si limita alle sole variabili procedurali. Come ogni altro tipo, un tipo procedurale può partecipare alla dichiarazione di un tipo strutturale.

Quando a una variabile di procedura viene assegnato il valore di una procedura, ciò che accade a livello fisico è che l'indirizzo della procedura viene memorizzato nella variabile. In effetti, una variabile di procedura è molto simile a una variabile puntatore, solo che invece di fare riferimento a dati, punta a una procedura o funzione. Come un puntatore, una variabile procedurale occupa 4 byte (due parole) che contiene un indirizzo di memoria. La prima parola memorizza l'offset, la seconda parola memorizza il segmento.

Parametri di tipo procedurale

Poiché i tipi procedurali possono essere utilizzati in qualsiasi contesto, è possibile descrivere procedure o funzioni che prendono procedure e funzioni come parametri. I parametri di tipo procedurale sono particolarmente utili quando è necessario eseguire azioni comuni su più procedure o funzioni.

Se una procedura o una funzione deve essere passata come parametro, deve seguire le stesse regole di compatibilità dei tipi dell'assegnazione. Cioè, tali procedure o funzioni devono essere compilate con la direttiva far, non possono essere funzioni integrate, non possono essere nidificate e non possono essere descritte con gli attributi inline o interrupt.

CONFERENZA 5. Tipo di dati stringa

1. Tipo di stringa in Pascal

Una sequenza di caratteri di una certa lunghezza è chiamata stringa. Le variabili di tipo stringa vengono definite specificando il nome della variabile, la stringa di parole riservate e, facoltativamente, ma non necessariamente, specificando la dimensione massima, ovvero la lunghezza della stringa, tra parentesi quadre. Se non si imposta la dimensione massima della stringa, per impostazione predefinita sarà 255, ovvero la stringa sarà composta da 255 caratteri.

Ogni elemento di una stringa può essere indicato dal suo numero. Tuttavia, le stringhe sono input e output nel loro insieme, non elemento per elemento, come nel caso degli array. Il numero di caratteri immessi non deve superare quello specificato nella dimensione massima della stringa, quindi se si verifica un tale eccesso, i caratteri "extra" verranno ignorati.

2. Procedure e funzioni per variabili di tipo stringa

1. Funzione Copia (S: Stringa; Indice, Conteggio: Intero): Stringa;

Restituisce una sottostringa di una stringa. S è un'espressione di tipo String.

Indice e Conteggio sono espressioni di tipo intero. La funzione restituisce una stringa contenente i caratteri di conteggio che iniziano dalla posizione dell'indice. Se Index è maggiore della lunghezza di S, la funzione restituisce una stringa vuota.

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

Rimuove una sottostringa di caratteri di lunghezza Count dalla stringa S, a partire dalla posizione Index. S è una variabile di tipo String. Indice e Conteggio sono espressioni di tipo intero. Se l'indice è maggiore della lunghezza di S, nessun carattere viene rimosso.

3. Procedura Insert(Source: String; var S: String; Index: Integer);

Concatena una sottostringa in una stringa, a partire da una posizione specificata. Source è un'espressione di tipo String. S è una variabile di tipo String di qualsiasi lunghezza. Index è un'espressione di tipo intero. Insert inserisce Source in S, a partire dalla posizione S[Indice].

4. Lunghezza della funzione (S: stringa): intero;

Restituisce il numero di caratteri effettivamente utilizzato nella stringa S. Si noti che quando si utilizzano stringhe con terminazione null, il numero di caratteri non è necessariamente uguale al numero di byte.

5. Funzione Pos(Substr: String; S: String): Intero;

Cerca una sottostringa in una stringa. Pos cerca Substr all'interno di S e restituisce un valore intero che è l'indice del primo carattere di Substr all'interno di S. Se Substr non viene trovato, Pos restituisce null.

3. Registrazioni

Un record è una raccolta di un numero limitato di componenti logicamente correlati appartenenti a tipi diversi. I componenti di un record sono chiamati campi, ognuno dei quali è identificato da un nome. Un campo record contiene il nome del campo, seguito da due punti per indicare il tipo di campo. I campi dei record possono essere di qualsiasi tipo consentito in Pascal, ad eccezione del tipo di file.

La descrizione di un record in lingua Pascal viene eseguita utilizzando la parola di servizio RECORD, seguita dalla descrizione dei componenti del record. La descrizione della voce termina con la parola di servizio END.

Ad esempio, un taccuino contiene cognomi, iniziali e numeri di telefono, quindi è conveniente rappresentare una riga separata in un taccuino come voce seguente:

digita Riga = Registra

FIO: stringa[20];

TEL: stringa[7];

fine;

var str: riga;

Le descrizioni dei record sono possibili anche senza utilizzare il nome del tipo, ad esempio:

var str : Registra

FIO : Stringa[20];

TEL : Stringa[7];

fine;

Il riferimento a un record nel suo insieme è consentito solo nelle istruzioni di assegnazione in cui i nomi di record dello stesso tipo sono utilizzati a sinistra ea destra del segno di assegnazione. In tutti gli altri casi, vengono gestiti campi separati di record. Per fare riferimento a un singolo componente record, è necessario specificare il nome del record e, separato da un punto, specificare il nome del campo desiderato. Tale nome è chiamato nome composto. Un componente record può anche essere un record, nel qual caso il nome distinto conterrà non due, ma più nomi.

Il riferimento ai componenti del record può essere semplificato utilizzando l'operatore with append. Consente di sostituire i nomi composti che caratterizzano ogni campo con solo nomi di campo e di definire il nome del record nell'istruzione join.

A volte il contenuto di un singolo record dipende dal valore di uno dei suoi campi. Nella lingua Pascal è consentita una descrizione del record, composta da parti comuni e varianti. La parte variante viene specificata utilizzando il caso P di costrutto, dove P è il nome del campo dalla parte comune del record. I possibili valori accettati da questo campo sono elencati allo stesso modo dell'istruzione variante. Tuttavia, invece di specificare l'azione da eseguire, come avviene in un'istruzione variant, i campi delle varianti vengono specificati tra parentesi. La descrizione della parte variante termina con la parola di servizio end. Il tipo di campo P può essere specificato nell'intestazione della parte variante. I record vengono inizializzati utilizzando costanti tipizzate.

4. Insiemi

Il concetto di insieme nel linguaggio Pascal si basa sul concetto matematico di insiemi: è un insieme limitato di elementi diversi. Un tipo di dati enumerato o intervallo viene utilizzato per costruire un tipo di set concreto. Il tipo di elementi che compongono un insieme è chiamato tipo base.

Un tipo multiplo viene descritto utilizzando il Set di parole funzione, ad esempio:

tipo M = Insieme di B;

Qui M è il tipo plurale, B è il tipo base.

L'appartenenza di variabili a un tipo plurale può essere determinata direttamente nella sezione relativa alla dichiarazione delle variabili.

Le costanti di tipo set vengono scritte come una sequenza tra parentesi di elementi o intervalli di tipo base, separati da virgole. Una costante della forma [] indica un sottoinsieme vuoto.

Un insieme include un insieme di elementi del tipo base, tutti i sottoinsiemi dell'insieme specificato e il sottoinsieme vuoto. Se il tipo base su cui è costruito l'insieme ha K elementi, allora il numero di sottoinsiemi inclusi in questo insieme è pari a 2 alla potenza di K. L'ordine di elencare gli elementi del tipo base in costanti è indifferente. Il valore di una variabile di tipo multiplo può essere dato da una costruzione della forma [T], dove T è una variabile di tipo base.

Le operazioni di assegnazione (:=), unione (+), intersezione (*) e sottrazione (-) sono applicabili a variabili e costanti di un tipo impostato. Il risultato di queste operazioni è un valore di tipo plurale:

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

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

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

Le operazioni sono applicabili a più valori: identità (=), non identità (<>), contenuto in (<=), contiene (>=). Il risultato di queste operazioni ha un tipo booleano:

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

2) ['A','B'] <> ['A','C'] darà VERO;

3) ['B'] <= ['B','C'] darà VERO;

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

Oltre a queste operazioni, per lavorare con valori di tipo set, viene utilizzato l'in operation, che controlla se l'elemento del tipo base a sinistra del segno di operazione appartiene all'insieme a destra del segno di operazione . Il risultato di questa operazione è un booleano. Al posto delle operazioni relazionali viene spesso utilizzata l'operazione di verifica dell'appartenenza di un elemento a un insieme.

Quando nei programmi vengono utilizzati più tipi di dati, le operazioni vengono eseguite su stringhe di dati di bit. Ciascun valore di tipo multiplo nella memoria del computer corrisponde a una cifra binaria.

I valori di tipo multiplo non possono essere elementi di un elenco di I/O. In ogni concreta implementazione del compilatore dal linguaggio Pascal, il numero di elementi del tipo base su cui è costruito il set è limitato.

L'inizializzazione di più valori di tipo viene eseguita utilizzando costanti tipizzate.

Ecco alcune procedure per lavorare con i set.

1. Procedura Exclude(var S: Set di T; I:T);

Rimuove l'elemento I dall'insieme S. S è una variabile di tipo "set" e I è un'espressione di un tipo compatibile con il tipo originale di S. Exclude(S, I) è uguale a S : = S - [I] , ma genera codice più efficiente.

2. Procedura Include(var S: Set di T; I:T);

Aggiunge un elemento I all'insieme S. S è una variabile di tipo "set" e I è un'espressione di un tipo compatibile con il tipo S. Il costrutto Include(S, I) è lo stesso di S : = S + [I], ma genera codice più efficiente.

CONFERENZA N. 6. File

1. File. Operazioni sui file

L'introduzione del tipo di file nel linguaggio Pascal è causata dalla necessità di fornire la capacità di lavorare con dispositivi informatici periferici (esterni) progettati per l'input, l'output e l'archiviazione dei dati.

Il tipo di dati file (o file) definisce una raccolta ordinata di un numero arbitrario di componenti dello stesso tipo. La proprietà comune di un array, set e record è che il numero dei loro componenti è determinato nella fase di scrittura del programma, mentre il numero di componenti del file nel testo del programma non è determinato e può essere arbitrario.

Quando si lavora con i file, vengono eseguite operazioni di I/O. Un'operazione di input significa trasferire dati da un dispositivo esterno (da un file di input) alla memoria principale di un computer, un'operazione di output è un trasferimento di dati dalla memoria principale a un dispositivo esterno (ad un file di output). I file su dispositivi esterni sono spesso indicati come file fisici. I loro nomi sono determinati dal sistema operativo.

Nei programmi Pascal, i nomi dei file vengono specificati tramite stringhe. Per lavorare con i file nel programma, è necessario definire una variabile di file. Pascal supporta tre tipi di file: file di testo, file componente, file non tipizzati.

Le variabili di file dichiarate in un programma sono chiamate file logici. Tutte le procedure e le funzioni di base che forniscono dati di I/O funzionano solo con file logici. Il file fisico deve essere associato al file logico prima di poter eseguire le procedure di apertura del file.

File di testo

Un posto speciale nel linguaggio Pascal è occupato dai file di testo, i cui componenti sono di tipo carattere. Per descrivere i file di testo, la lingua definisce il tipo standard Testo:

var TF1, TF2: testo;

I file di testo sono una sequenza di righe e le righe sono una sequenza di caratteri. Le linee sono di lunghezza variabile, ogni linea termina con un terminatore di linea.

File componenti

Un componente o file tipizzato è un file con il tipo dichiarato dei suoi componenti. I file componenti sono costituiti da rappresentazioni macchina di valori variabili; memorizzano i dati nella stessa forma della memoria del computer.

La descrizione dei valori del tipo di file è:

digitare M = File Di T;

dove M è il nome del tipo di file;

T - tipo di componente.

I componenti di file possono essere di tutti i tipi scalari e da tipi strutturati: array, set, record. In quasi tutte le implementazioni specifiche del linguaggio Pascal, il costrutto "file of files" non è consentito.

Tutte le operazioni sui file dei componenti vengono eseguite utilizzando procedure standard.

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

File non digitati

I file non tipizzati consentono di scrivere sezioni arbitrarie della memoria del computer su disco e di leggerle da disco a memoria. I file non tipizzati sono descritti come segue:

var f: file;

Ora elenchiamo le procedure e le funzioni per lavorare con diversi tipi di file.

1. Procedura Assegna(var F; NomeFile: Stringa);

La procedura AssignFile associa un nome di file esterno a una variabile di file.

F è una variabile di file di qualsiasi tipo di file, FileName è un'espressione String o un'espressione PChar se è consentita la sintassi estesa. Tutte le ulteriori operazioni con F vengono eseguite con un file esterno.

Non è possibile utilizzare una procedura con una variabile di file già aperta.

2. Procedura Chiudi(varF);

La procedura interrompe il collegamento tra la variabile file e il file del disco esterno e chiude il file.

F è una variabile di file di qualsiasi tipo, aperta dalle procedure Reimposta, Riscrivi o Aggiungi. Il file esterno associato a F viene completamente modificato e quindi chiuso, liberando il descrittore di file per il riutilizzo.

La direttiva {SI+} consente di gestire gli errori durante l'esecuzione del programma utilizzando la gestione delle eccezioni. Con la direttiva {$1-} disattivata, è necessario utilizzare IOResult per verificare la presenza di errori di I/O.

3.Funzione Eof(var F): Booleano;

{File digitati o non tipizzati}

Funzione Eof[(var F: Text)]: Booleano;

{file di testo}

Verifica se la posizione del file corrente è la fine del file.

Eof(F) restituisce True se la posizione del file corrente è dopo l'ultimo carattere del file o se il file è vuoto; altrimenti Eof(F) restituisce False.

La direttiva {SI+} consente di gestire gli errori durante l'esecuzione del programma utilizzando la gestione delle eccezioni. Con la direttiva {SI-} disattivata, è necessario utilizzare IOResult per verificare la presenza di errori di I/O.

4. Cancellazione procedura(varF);

Elimina il file esterno associato a F.

F è una variabile di file di qualsiasi tipo di file.

Prima di chiamare la procedura di Cancellazione, è necessario chiudere il file.

La direttiva {SI+} consente di gestire gli errori durante l'esecuzione del programma utilizzando la gestione delle eccezioni. Con la direttiva {SI-} disattivata, è necessario utilizzare IOResult per verificare la presenza di errori di I/O.

5. Funzione FileSize(var F): Intero;

Restituisce la dimensione in byte del file F Tuttavia, se F è un file digitato, FileSize restituirà il numero di record nel file. Il file deve essere aperto prima di utilizzare la funzione FileSize. Se il file è vuoto, FileSize(F) restituisce zero. F è una variabile di qualsiasi tipo di file.

6.Funzione FilePos(var F): LongInt;

Restituisce la posizione corrente di un file all'interno di un file.

Prima di utilizzare la funzione FilePos, il file deve essere aperto. La funzione FilePos non viene utilizzata con i file di testo. F è una variabile di qualsiasi tipo di file, ad eccezione del tipo di testo.

7. Ripristino procedura(var F [: File; RecSize: Word]);

Apre un file esistente.

F è una variabile di qualsiasi tipo di file associata a un file esterno tramite AssignFile. RecSize è un'espressione facoltativa che viene utilizzata se F è un file non tipizzato. Se F è un file non tipizzato, RecSize determina la dimensione del record utilizzata durante il trasferimento dei dati. Se RecSize viene omesso, la dimensione del record predefinita è 128 byte.

La procedura Reset apre un file esterno esistente associato alla variabile file F. Se non è presente alcun file esterno con quel nome, si verifica un errore di runtime. Se il file associato a F è già aperto, viene prima chiuso e poi riaperto. La posizione del file corrente è impostata all'inizio del file.

8. Riscrivi procedura(var F: File [; Ridimensiona: Word]);

Crea e apre un nuovo file.

F è una variabile di qualsiasi tipo di file associata a un file esterno tramite AssignFile. RecSize è un'espressione facoltativa che viene utilizzata se F è un file non tipizzato. Se F è un file non tipizzato, RecSize determina la dimensione del record utilizzata durante il trasferimento dei dati. Se RecSize viene omesso, la dimensione del record predefinita è 128 byte.

La procedura Riscrivi crea un nuovo file esterno con il nome associato a F. Se esiste già un file esterno con lo stesso nome, viene eliminato e viene creato un nuovo file vuoto.

9. Procedura Seek(var F; N: LongInt);

Sposta la posizione del file corrente sul componente specificato. È possibile utilizzare la procedura solo con file digitati o non digitati aperti.

La posizione corrente del file F viene spostata al numero N. Il numero del primo componente del file è 0.

L'istruzione Seek(F, FileSize(F)) sposta la posizione del file corrente alla fine del file.

10. Procedura Append(var F: Text);

Apre un file di testo esistente per aggiungere informazioni alla fine del file (append).

Se non esiste un file esterno con il nome specificato, si verifica un errore di runtime. Se il file F è già aperto, si chiude e si riapre. La posizione del file corrente è impostata alla fine del file.

11.Function Eoln[(var F: Text)]: Boolean;

Verifica se la posizione del file corrente è la fine di una riga in un file di testo.

Eoln(F) restituisce True se la posizione del file corrente è alla fine di una riga o di un file; altrimenti Eoln(F) restituisce False.

12. Procedura Lettura(F, V1 [, V2,..., Vn]);

{File digitati e non tipizzati}

Procedura Read([var F: Testo;] V1 [, V2,..., Vn]);

{file di testo}

Per i file digitati, la procedura legge il componente del file in una variabile. Ad ogni lettura, la posizione corrente nel file avanza all'elemento successivo.

Per i file di testo, uno o più valori vengono letti in una o più variabili.

Con variabili di tipo String Read legge tutti i caratteri fino al successivo marker di fine riga (ma escluso) o finché Eof(F) non restituisce True. La stringa di caratteri risultante viene assegnata alla variabile.

Nel caso di una variabile di tipo intero o reale, la procedura attende una sequenza di caratteri che formino un numero secondo le regole della sintassi Pascal. La lettura si interrompe quando si incontra il primo spazio, tabulazione o fine riga o quando Eof(F) restituisce True. Se la stringa numerica non corrisponde al formato previsto, si verifica un errore di I/O.

13. Procedura Readln([var F: Testo;] V1 [, V2..., Vn]);

È un'estensione della procedura di Lettura ed è definita per i file di testo. Legge una stringa di caratteri nel file, incluso l'indicatore di fine riga, e avanza all'inizio della riga successiva. Chiamando la funzione Readln(F) senza parametri si sposta la posizione del file corrente all'inizio della riga successiva, se presente, altrimenti si salta alla fine del file.

14. Funzione SeekEof[(var F: Text)]: Boolean;

Restituisce la fine del file e può essere utilizzato solo per file di testo aperti. Tipicamente utilizzato per leggere valori numerici da file di testo.

15. Funzione SeekEoln[(var F: Text)]: Boolean;

Restituisce il terminatore di riga in un file e può essere utilizzato solo per file di testo aperti. Tipicamente utilizzato per leggere valori numerici da file di testo.

16. Procedura Write([var F: Testo;] P1 [, P2,..., Pn]);

{file di testo}

Scrive uno o più valori in un file di testo.

Ogni parametro di ingresso deve essere di tipo Char, uno dei tipi interi (Byte, ShortInt, Word, Longint, Cardinal), uno dei tipi a virgola mobile (Single, Real, Double, Extended, Currency), uno dei tipi string ( PChar, AisiString , ShortString) o uno dei tipi booleani (Boolean, Bool).

Procedura Scrittura(F, V1,..., Vn);

{File digitati}

Scrive una variabile in un componente file. Le variabili VI...., Vn devono essere dello stesso tipo degli elementi del file. Ogni volta che viene scritta una variabile, la posizione corrente nel file viene spostata all'elemento successivo.

17. Procedura Writeln([var F: Testo;] [P1, P2,..., Pn]);

{file di testo}

Esegue un'operazione di scrittura, quindi inserisce un indicatore di fine riga nel file.

La chiamata di Writeln(F) senza parametri scrive un indicatore di fine riga nel file. Il file deve essere aperto per l'output.

2. Moduli. Tipi di moduli

Un modulo (1Ж1Т) in Pascal è una libreria di subroutine appositamente progettata. Un modulo, a differenza di un programma, non può essere lanciato da solo, può solo partecipare alla creazione di programmi e altri moduli. I moduli consentono di creare librerie personali di procedure e funzioni e di creare programmi di quasi tutte le dimensioni.

Un modulo in Pascal è un'unità di programma memorizzata separatamente e compilata in modo indipendente. In generale, un modulo è una raccolta di risorse software destinate all'uso da parte di altri programmi. Le risorse del programma sono intese come qualsiasi elemento del linguaggio Pascal: costanti, tipi, variabili, subroutine. Il modulo stesso non è un programma eseguibile, i suoi elementi sono usati da altre unità di programma.

Tutti gli elementi del programma del modulo possono essere divisi in due parti:

1) elementi di programma destinati ad essere utilizzati da altri programmi o moduli, tali elementi sono detti visibili all'esterno del modulo;

2) elementi software che sono necessari solo al funzionamento del modulo stesso, sono detti invisibili (o nascosti).

In base a ciò, il modulo, oltre all'intestazione, contiene tre parti principali, dette interfaccia, eseguibili e inizializzate.

In generale, un modulo ha la seguente struttura:

unità <nome modulo>; {titolo del modulo}

interfaccia

{descrizione degli elementi di programma visibili del modulo}

implementazione

{descrizione degli elementi di programmazione nascosti del modulo}

iniziare

{dichiarazioni di inizializzazione dell'elemento del modulo}

fine.

In un caso particolare, il modulo potrebbe non contenere una parte di implementazione e una parte di inizializzazione, quindi la struttura del modulo sarà la seguente:

unità <nome modulo>; {titolo del modulo}

interfaccia

{descrizione degli elementi di programma visibili del modulo}

implementazione

fine.

L'uso di procedure e funzioni nei moduli ha le sue peculiarità. L'intestazione della subroutine contiene tutte le informazioni necessarie per chiamarla: nome, elenco e tipo di parametri, tipo di risultato per le funzioni. Queste informazioni devono essere disponibili per altri programmi e moduli. D'altra parte, il testo di una subroutine che implementa il suo algoritmo non può essere utilizzato da altri programmi e moduli. Pertanto, i titoli delle procedure e delle funzioni sono collocati nella parte interfaccia del modulo e il testo nella parte implementativa.

La parte dell'interfaccia del modulo contiene solo intestazioni visibili (accessibili ad altri programmi e moduli) di procedure e funzioni (senza il servizio word forward). Il testo completo della procedura o della funzione viene inserito nella parte di implementazione e l'intestazione potrebbe non contenere un elenco di parametri formali.

Il codice sorgente del modulo deve essere compilato utilizzando la direttiva Make del sottomenu Compile e scritto su disco. Il risultato della compilazione del modulo è un file con estensione . TPU (Unità Turbo Pascal). Il nome di base del modulo è preso dall'intestazione del modulo.

Per collegare un modulo al programma, è necessario specificarne il nome nella sezione della descrizione del modulo, ad esempio:

utilizza Crt, Grafico;

Nel caso in cui i nomi delle variabili nella parte interfaccia del modulo e nel programma che utilizza questo modulo coincidano, il riferimento sarà alla variabile descritta nel programma. Per fare riferimento a una variabile dichiarata in un modulo, è necessario utilizzare un nome composto composto dal nome del modulo e dal nome della variabile, separati da un punto. L'uso di nomi composti si applica non solo ai nomi delle variabili, ma a tutti i nomi dichiarati nella parte interfaccia del modulo.

È vietato l'uso ricorsivo dei moduli.

Se un modulo ha una sezione di inizializzazione, le istruzioni in quella sezione verranno eseguite prima che il programma che utilizza quel modulo inizi l'esecuzione.

Elenchiamo i tipi di moduli.

1. Modulo SISTEMA.

Il modulo SYSTEM implementa routine di supporto di livello inferiore per tutte le funzionalità integrate come I/O, manipolazione di stringhe, operazioni in virgola mobile e allocazione dinamica della memoria.

Il modulo SYSTEM contiene tutte le routine e le funzioni Pascal standard e integrate. Qualsiasi subroutine Pascal che non fa parte del Pascal standard e non si trova in nessun altro modulo è contenuta nel modulo Sistema. Questa unità viene utilizzata automaticamente in tutti i programmi e non deve essere specificata nella dichiarazione degli usi.

2. Modulo DOS.

Il modulo Dos implementa numerose routine e funzioni Pascal equivalenti alle chiamate DOS più comunemente utilizzate, come GetTime, SetTime, DiskSize e così via.

3. Modulo CRT.

Il modulo CRT implementa una serie di potenti programmi che forniscono il controllo completo sulle funzionalità del PC come il controllo della modalità schermo, codici tastiera estesi, colori, finestre e suoni. Il modulo CRT può essere utilizzato solo in programmi che girano su personal computer IBM PC, PC AT, PS / 2 di IBM e sono completamente compatibili con essi.

Uno dei principali vantaggi dell'utilizzo del modulo CRT è una maggiore velocità e flessibilità nelle operazioni dello schermo. I programmi che non funzionano con il modulo CRT visualizzano le informazioni sullo schermo utilizzando il sistema operativo DOS, che è associato a un sovraccarico aggiuntivo. Quando si utilizza il modulo CRT, le informazioni in uscita vengono inviate direttamente al BIOS (Basic Input/Output System) o, per operazioni ancora più veloci, direttamente alla memoria video.

4. Modulo GRAFICO.

Utilizzando le procedure e le funzioni incluse in questo modulo, è possibile creare vari grafici sullo schermo.

5. Modulo SOVRAPPOSIZIONE.

Il modulo OVERLAY consente di ridurre i requisiti di memoria di un programma DOS in modalità reale. In effetti, è possibile scrivere programmi che superano la quantità totale di memoria disponibile, poiché solo una parte del programma sarà in memoria in un dato momento.

CONFERENZA N. 7. Memoria dinamica

1. Tipo di dati di riferimento. memoria dinamica. Variabili dinamiche

Una variabile statica (allocata staticamente) è una variabile dichiarata esplicitamente nel programma, a cui si fa riferimento per nome. La posizione in memoria per posizionare le variabili statiche viene determinata al momento della compilazione del programma. A differenza di tali variabili statiche, i programmi Pascal possono creare variabili dinamiche. La proprietà principale delle variabili dinamiche è che vengono create e per esse viene allocata memoria durante l'esecuzione del programma.

Le variabili dinamiche sono collocate in un'area di memoria dinamica (heap-area). Una variabile dinamica non è specificata in modo esplicito nelle dichiarazioni di variabile e non può essere definita per nome. È possibile accedere a tali variabili utilizzando puntatori e riferimenti.

Un tipo di riferimento (puntatore) definisce un insieme di valori che puntano a variabili dinamiche di un tipo specifico, chiamato tipo base. Una variabile di tipo riferimento contiene l'indirizzo di una variabile dinamica in memoria. Se il tipo di base è un identificatore non dichiarato, deve essere dichiarato nella stessa parte della dichiarazione del tipo del tipo di puntatore.

La parola riservata nil denota una costante con un valore di puntatore che non punta a nulla.

Facciamo un esempio della descrizione di variabili dinamiche.

var p1, p2 : ^reale;

p3, p4 : ^intero;

2. Lavorare con la memoria dinamica. Puntatori non digitati

Procedure e funzioni di memoria dinamica

1. Procedura Nuovo(var p: Puntatore).

Alloca spazio nell'area di memoria dinamica per ospitare la variabile dinamica pЛ, e ne assegna l'indirizzo al puntatore p.

2. Procedura Dispose(varp: Pointer).

Libera la memoria allocata per l'allocazione dinamica delle variabili dalla procedura New e il valore del puntatore p diventa indefinito.

3. Procedura GetMem(varp: Pointer; size: Word).

Alloca una sezione di memoria nell'area heap, assegna l'indirizzo del suo inizio al puntatore p, la dimensione della sezione in byte è specificata dal parametro size.

4. Procedura FreeMem(var p: Pointer; size: Word).

Libera l'area di memoria, il cui indirizzo iniziale è specificato dal puntatore p e la dimensione è specificata dal parametro size. Il valore del puntatore p diventa indefinito.

5. Procedura Mark(var p: Pointer)

Scrive nel puntatore p l'indirizzo di inizio di una sezione di memoria dinamica libera al momento della sua chiamata.

6. Rilascio della procedura (var p: Pointer)

Rilascia una sezione di memoria dinamica, a partire dall'indirizzo scritto sul puntatore p dalla procedura Mark, ovvero cancella la memoria dinamica che era occupata dopo la chiamata alla procedura Mark.

7. Funzione MaxAvaikLongint

Restituisce la lunghezza, in byte, dell'heap libero più lungo.

8. Funzione MemAvaikLongint

Restituisce la quantità totale di memoria dinamica libera in byte.

9. Funzione di supporto SizeOf(X):Word

Restituisce la quantità di byte occupati da X, dove X può essere un nome di variabile di qualsiasi tipo o un nome di tipo.

Il tipo integrato Pointer denota un puntatore non tipizzato, ovvero un puntatore che non punta a nessun tipo particolare. Le variabili di tipo Pointer possono essere dereferenziate: specificare il carattere ^ dopo tale variabile provoca un errore.

Come il valore indicato da nil, i valori del puntatore sono compatibili con tutti gli altri tipi di puntatore.

CONFERENZA № 8. Strutture dati astratte

1. Strutture dati astratte

I tipi di dati strutturati, come matrici, set e record, sono strutture statiche perché le loro dimensioni non cambiano durante l'intera esecuzione del programma.

Spesso è necessario che le strutture dati cambino le loro dimensioni nel corso della risoluzione di un problema. Tali strutture dati sono dette dinamiche. Questi includono pile, code, elenchi, alberi, ecc.

La descrizione di strutture dinamiche con l'aiuto di array, record e file porta a un uso dispendioso della memoria del computer e aumenta il tempo per la risoluzione dei problemi.

Ogni componente di qualsiasi struttura dinamica è un record contenente almeno due campi: un campo di tipo "puntatore" e il secondo - per il posizionamento dei dati. In generale, un record può contenere non uno, ma più puntatori e più campi di dati. Un campo dati può essere una variabile, un array, un set o un record.

Se la parte di puntamento contiene l'indirizzo di un elemento dell'elenco, l'elenco viene chiamato unidirezionale (o collegato singolarmente). Se contiene due componenti, è doppiamente connesso. È possibile eseguire varie operazioni sugli elenchi, ad esempio:

1) aggiunta di un elemento alla lista;

2) rimuovere un elemento dalla lista con una determinata chiave;

3) cercare un elemento con un dato valore del campo chiave;

4) ordinare gli elementi della lista;

5) suddivisione della lista in due o più liste;

6) combinare due o più liste in una sola;

7) altre operazioni.

Tuttavia, di norma, non si pone la necessità di tutte le operazioni per risolvere vari problemi. Pertanto, a seconda delle operazioni di base che devono essere applicate, esistono diversi tipi di elenchi. I più popolari di questi sono stack e queue.

2. Pile

Uno stack è una struttura di dati dinamica, l'aggiunta di un componente a cui e la rimozione di un componente da cui sono costituiti da un'estremità, chiamata la parte superiore dello stack. Lo stack funziona secondo il principio LIFO (Last-In, First-Out) - "Last in, first out".

Di solito vengono eseguite tre operazioni sugli stack:

1) formazione iniziale della catasta (record del primo componente);

2) aggiunta di un componente allo stack;

3) selezione del componente (cancellazione).

Per formare uno stack e lavorare con esso, è necessario disporre di due variabili del tipo "pointer", la prima delle quali determina la parte superiore dello stack e la seconda è ausiliaria.

Esempio. Scrivete un programma che formi uno stack, vi aggiunga un numero arbitrario di componenti, quindi legga tutti i componenti e li visualizzi sullo schermo. Prendi una stringa di caratteri come dati. Input di dati - dalla tastiera, un segno di fine input - una stringa di caratteri FINE.

Programma STACK;

utilizza Crt;

Digitare

Alfa = Stringa[10];

PComp = ^Comp;

Comp = record

SD: Alfa

pAvanti : PComp

fine;

var

pIn alto:PComp;

sc: Alfa;

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

iniziare

Nuovo(pIn alto);

pInizio^.pAvanti := NIL;

pTop^.sD := sC;

fine;

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

var pAux: PComp;

iniziare

NUOVO(pAux);

pAux^.pAvanti := pTop;

pTop := pAux;

pTop^.sD := sC;

fine;

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

iniziare

sC := pTop^.sD;

pTop := pTop^.pSuccessivo;

fine;

iniziare

clrscr;

writeln(' INSERIRE UNA STRINGA ');

readln(sc);

CreateStack(pTop, sc);

ripetere

writeln(' INSERIRE UNA STRINGA ');

readln(sc);

AddComp(pTop, sc);

fino a sC = 'FINE';

writeln('****** OUTPUT ******');

ripetere

DelComp(pTop, sc);

scriviln(sC);

fino a pTop = NIL;

fine.

3. Code

Una coda è una struttura dati dinamica in cui un componente viene aggiunto a un'estremità e recuperato all'altra estremità. La coda funziona secondo il principio FIFO (First-In, First-Out) - "First in, first serve".

Per formare una coda e lavorare con essa, è necessario disporre di tre variabili del tipo puntatore, la prima delle quali determina l'inizio della coda, la seconda - la fine della coda, la terza - ausiliaria.

Esempio. Scrivete un programma che formi una coda, vi aggiunga un numero arbitrario di componenti, quindi legga tutti i componenti e li visualizzi sullo schermo. Prendi una stringa di caratteri come dati. Input di dati - dalla tastiera, un segno di fine input - una stringa di caratteri FINE.

CODA di programma;

utilizza Crt;

Digitare

Alfa = Stringa[10];

PComp = ^Comp;

Comp = registrazione

SD: Alfa

pAvanti: PComp;

fine;

var

pBegin, pEnd: PComp;

sc: Alfa;

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

iniziare

Nuovo(pInizio);

pInizio^.pSuccessivo := NIL;

pInizio^.sD := sC;

pEnd := pInizio;

fine;

Procedura Aggiungi ProcedureQueue(var pEnd : PComp; var sC : Alfa);

var pAux: PComp;

iniziare

Nuovo(pAux);

pAux^.pNext := NIL;

pFine^.pAvanti := pAux;

pEnd := pAux;

pFine^.sD := sC;

fine;

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

iniziare

sC := pInizio^.sD;

pInizio := pInizio^.pAvanti;

fine;

iniziare

clrscr;

writeln(' INSERIRE UNA STRINGA ');

readln(sc);

CreateQueue(pBegin, pEnd, sc);

ripetere

writeln(' INSERIRE UNA STRINGA ');

readln(sc);

AggiungiCoda(pEnd, sc);

fino a sC = 'FINE';

writeln(' ***** VISUALIZZA RISULTATI *****');

ripetere

DelQueue(pBegin, sc);

scriviln(sC);

fino a pBegin = NIL;

fine.

LEZIONE N. 9. Strutture dati ad albero

1. Strutture dati ad albero

Una struttura dati ad albero è un insieme finito di elementi-nodi tra i quali esistono relazioni: la connessione tra la sorgente e il generato.

Se utilizziamo la definizione ricorsiva proposta da N. Wirth, allora una struttura dati ad albero con tipo base t è una struttura vuota o un nodo di tipo t, con cui un insieme finito di strutture ad albero con tipo base t, detti sottoalberi, è associato.

Successivamente, diamo le definizioni utilizzate quando si opera con le strutture ad albero.

Se il nodo y si trova direttamente sotto il nodo x, allora il nodo y è chiamato discendente immediato del nodo x, e x è l'antenato immediato del nodo y, cioè, se il nodo x è al livello i-esimo, allora il nodo y è di conseguenza situato al (i+1) -esimo livello.

Il livello massimo di un nodo dell'albero è chiamato altezza o profondità dell'albero. Un antenato non ha solo un nodo dell'albero: la sua radice.

I nodi dell'albero che non hanno figli sono chiamati nodi foglia (o foglie dell'albero). Tutti gli altri nodi sono chiamati nodi interni. Il numero di figli immediati di un nodo determina il grado di quel nodo e il grado massimo possibile di un nodo in un dato albero determina il grado dell'albero.

Antenati e discendenti non possono essere scambiati, cioè la connessione tra l'originale e il generato agisce solo in una direzione.

Se si passa dalla radice dell'albero a un nodo particolare, il numero di rami dell'albero che verranno attraversati in questo caso è chiamato lunghezza del percorso per questo nodo. Se tutti i rami (nodi) di un albero sono ordinati, si dice che l'albero è ordinato.

Gli alberi binari sono un caso speciale di strutture ad albero. Questi sono alberi in cui ogni bambino ha al massimo due figli, chiamati sottoalberi sinistro e destro. Pertanto, un albero binario è una struttura ad albero il cui grado è due.

L'ordinamento di un albero binario è determinato dalla seguente regola: ogni nodo ha il proprio campo chiave, e per ogni nodo il valore della chiave è maggiore di tutte le chiavi nel suo sottoalbero di sinistra e minore di tutte le chiavi nel suo sottoalbero di destra.

Un albero il cui grado è maggiore di due è chiamato fortemente ramificato.

2. Operazioni sugli alberi

Inoltre, considereremo tutte le operazioni in relazione agli alberi binari.

I. Costruzione di alberi

Presentiamo un algoritmo per costruire un albero ordinato.

1. Se l'albero è vuoto, i dati vengono trasferiti alla radice dell'albero. Se l'albero non è vuoto, uno dei suoi rami scende in modo tale che l'ordine dell'albero non venga violato. Di conseguenza, il nuovo nodo diventa la foglia successiva dell'albero.

2. Per aggiungere un nodo a un albero già esistente, puoi utilizzare l'algoritmo sopra.

3. Quando si elimina un nodo dall'albero, prestare attenzione. Se il nodo da rimuovere è una foglia, o ha un solo figlio, l'operazione è semplice. Se il nodo da eliminare ha due discendenti, allora sarà necessario trovare un nodo tra i suoi discendenti che possa essere messo al suo posto. Ciò è necessario a causa del requisito che l'albero sia ordinato.

Puoi farlo: scambiare il nodo da rimuovere con il nodo con il valore di chiave più grande nel sottoalbero di sinistra, o con il nodo con il valore di chiave più piccolo nel sottoalbero di destra, quindi eliminare il nodo desiderato come foglia.

II. Trovare un nodo con un dato valore di campo chiave

Quando si esegue questa operazione, è necessario attraversare l'albero. È necessario tenere conto delle diverse forme di scrittura di un albero: prefisso, infisso e suffisso.

Sorge la domanda: come rappresentare i nodi dell'albero in modo che sia più conveniente lavorare con loro? È possibile rappresentare un albero utilizzando un array, in cui ogni nodo è descritto da un valore di tipo combinato, che ha un campo informazioni di tipo carattere e due campi di tipo riferimento. Ma questo non è molto conveniente, poiché gli alberi hanno un gran numero di nodi che non sono predeterminati. Pertanto, è meglio utilizzare variabili dinamiche quando si descrive un albero. Quindi ogni nodo è rappresentato da un valore dello stesso tipo, che contiene la descrizione di un determinato numero di campi di informazioni, e il numero di campi corrispondenti deve essere uguale al grado dell'albero. È logico definire l'assenza di discendenti con zero. Quindi, in Pascal, la descrizione di un albero binario potrebbe assomigliare a questa:

TIPO TreeLink = ^Albero;

albero = record;

Inf: <tipo di dati>;

Sinistra, Destra: TreeLink;

Fine.

3. Esempi di attuazione delle operazioni

1. Costruire un albero di n nodi di altezza minima, o un albero perfettamente bilanciato (il numero di nodi dei sottoalberi sinistro e destro di tale albero non deve differire di più di uno).

Algoritmo di costruzione ricorsivo:

1) il primo nodo viene preso come radice dell'albero.

2) il sottoalbero sinistro di nl nodi è costruito allo stesso modo.

3) il sottoalbero di destra di nr nodi è costruito allo stesso modo;

nr = n - nl - 1. Come campo informativo, prenderemo i numeri di nodo inseriti dalla tastiera. La funzione ricorsiva che implementa questa costruzione sarà simile a questa:

Albero delle funzioni(n : Byte): TreeLink;

Variante : TreeLink; nl,nr,x : byte;

Iniziare

Se n = 0 allora Tree := nil

Altro

Iniziare

nl := n div 2;

nr = n - nl - 1;

writeln('Inserisci numero vertice ');

leggiln(x);

tritone);

t^.inf := x;

t^.sinistra := Albero(nl);

t^.destra := Albero(nr);

Albero := t;

End;

{Albero}

Fine.

2. Nell'albero ordinato binario, trova il nodo con il valore dato del campo chiave. Se non c'è un tale elemento nell'albero, aggiungilo all'albero.

Procedura di ricerca(x : Byte; var t : TreeLink);

Iniziare

Se t = zero allora

Iniziare

Tritone);

t^inf := x;

t^.sinistra := zero;

t^.destra := zero;

Fine

Altrimenti se x < t^.inf allora

Cerca(x, t^.sinistra)

Altrimenti se x > t^.inf allora

Cerca(x, t^.destra)

Altro

Iniziare

{processo trovato elemento}

...

End;

Fine.

3. Scrivere le procedure di attraversamento dell'albero rispettivamente in ordine diretto, simmetrico e inverso.

3.1. Procedura Preordine(t : TreeLink);

Iniziare

Se t <> zero allora

Iniziare

ScriviIn(t^.inf);

Preordine(t^.sinistra);

Preordina(t^.destra);

End;

End;

3.2. Procedura Inorder(t : TreeLink);

Iniziare

Se t <> zero allora

Iniziare

Inordine(t^.sinistra);

ScriviIn(t^.inf);

Inordine(t^.destra);

End;

Fine.

3.3. Postordine di procedura(t : TreeLink);

Iniziare

Se t <> zero allora

Iniziare

postordine(t^.sinistra);

postordine(t^.destra);

ScriviIn(t^.inf);

End;

Fine.

4. Nell'albero ordinato binario, eliminare il nodo con il valore dato del campo chiave.

Descriviamo una procedura ricorsiva che terrà conto della presenza dell'elemento richiesto nell'albero e del numero di discendenti di questo nodo. Se il nodo da eliminare ha due figli, verrà sostituito dal valore della chiave più grande nel sottoalbero sinistro e solo allora verrà eliminato definitivamente.

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

Varia p : TreeLink;

Procedura Delete2(var q : TreeLink);

Iniziare

Se q^.right <> nil allora Delete2(q^.right)

Altro

Iniziare

p^.inf := q^.inf;

p :=q;

q := q^.sinistra;

End;

End;

Iniziare

Se t = zero allora

Writeln('nessun elemento trovato')

Altrimenti se x < t^.inf allora

Elimina1(x, t^.sinistra)

Altrimenti se x > t^.inf allora

Elimina1(x, t^.destra)

Altro

Iniziare

P := t;

Se p^.sinistra = zero allora

t := p^.destra

Altro

Se p^.right = zero allora

t := p^.sinistra

Altro

Elimina2(p^.sinistra);

End;

Fine.

CONFERENZA N. 10. Conta

1. Il concetto di grafico. Modi per rappresentare un grafico

Un grafo è una coppia G = (V,E), dove V è un insieme di oggetti di natura arbitraria, detti vertici, ed E è una famiglia di coppie ei = (vil, vi2), vijOV, detti archi. Nel caso generale, l'insieme V e/o la famiglia E possono contenere un numero infinito di elementi, ma considereremo solo grafi finiti, cioè grafi per i quali sia V che E sono finiti. Se l'ordine degli elementi inclusi in ei conta, il grafo si dice diretto, abbreviato in digrafo, altrimenti si dice non orientato. I bordi di un digrafo sono chiamati archi. In quanto segue, assumiamo che il termine "grafo", usato senza specificazione (diretto o non orientato), denoti un grafo non orientato.

Se e = , allora i vertici v e u sono chiamati le estremità del bordo. Qui diciamo che lo spigolo e è adiacente (incidente) a ciascuno dei vertici v e u. I vertici ve e sono anche chiamati adiacenti (incidente). Nel caso generale, spigoli della forma e = ; tali bordi sono chiamati loop.

Il grado di un vertice in un grafico è il numero di bordi incidenti a quel vertice, con i cicli contati due volte. Poiché ogni spigolo è incidente a due vertici, la somma dei gradi di tutti i vertici nel grafico è uguale al doppio del numero di spigoli: Sum(deg(vi), i=1...|V|) = 2 * | E|.

Il peso di un nodo è un numero (reale, intero o razionale) assegnato a un dato nodo (interpretato come costo, velocità effettiva, ecc.). Peso, lunghezza del bordo: un numero o più numeri che vengono interpretati come lunghezza, larghezza di banda, ecc.

Un percorso in un grafo (o un percorso in un digrafo) è una sequenza alternata di vertici e archi (o archi in un digrafo) della forma v0, (v0,v1), v1..., (vn - 1,vn ), v. Il numero n è chiamato lunghezza del percorso. Un percorso senza bordi ripetuti è chiamato catena; un percorso senza vertici ripetuti è chiamato catena semplice. Il percorso può essere chiuso (v0 = vn). Un percorso chiuso senza bordi ripetuti è chiamato ciclo (o contorno in un digrafo); senza ripetere i vertici (tranne il primo e l'ultimo) - un semplice ciclo.

Un grafo si dice connesso se c'è un percorso tra due dei suoi vertici e disconnesso in caso contrario. Un grafo disconnesso è costituito da diversi componenti collegati (sottografi collegati).

Esistono vari modi per rappresentare i grafici. Consideriamo ciascuno di essi separatamente.

1. Matrice di incidenza.

Questa è una matrice rettangolare di dimensione nx n, dove n è il numero di vertici, am è il numero di spigoli. I valori degli elementi di matrice si determinano come segue: se lo spigolo xi e il vertice vj sono incidenti, allora il valore dell'elemento di matrice corrispondente è uguale a uno, altrimenti il ​​valore è zero. Per i grafi orientati la matrice di incidenza è costruita secondo il seguente principio: il valore dell'elemento è pari a - 1 se lo spigolo xi proviene dal vertice vj, pari a 1 se lo spigolo xi entra nel vertice vj, e pari a XNUMX altrimenti .

2. Matrice di adiacenza.

Questa è una matrice quadrata di dimensione nxn, dove n è il numero di vertici. Se i vertici vi e vj sono adiacenti, cioè se esiste uno spigolo che li collega, allora il corrispondente elemento di matrice è uguale a uno, altrimenti è uguale a zero. Le regole per costruire questa matrice per grafi orientati e non orientati non sono diverse. La matrice di adiacenza è più compatta della matrice di incidenza. Va notato che anche questa matrice è molto sparsa, ma nel caso di un grafo non orientato è simmetrica rispetto alla diagonale principale, quindi è possibile memorizzare non l'intera matrice, ma solo metà di essa (una matrice triangolare ).

3. Elenco delle adiacenze (incidenti).

È una struttura dati che memorizza un elenco di vertici adiacenti per ciascun vertice del grafico. L'elenco è un array di puntatori, il cui i-esimo elemento contiene un puntatore all'elenco di vertici adiacenti all'i-esimo vertice.

Un elenco di adiacenza è più efficiente di una matrice di adiacenza perché elimina la memorizzazione di elementi nulli.

4. Elenco delle liste.

È una struttura dati ad albero in cui un ramo contiene elenchi di vertici adiacenti a ciascuno dei vertici del grafico e il secondo ramo punta al vertice del grafico successivo. Questo modo di rappresentare il grafico è il più ottimale.

2. Rappresentazione di un grafico mediante una lista di incidenza. Algoritmo di attraversamento della profondità del grafico

Per implementare un grafico come elenco di incidenza, è possibile utilizzare il tipo seguente:

Elenco tipi = ^S;

S=record;

inf: byte;

successivo : Elenco;

fine;

Quindi il grafico è definito come segue:

Var Gr : array[1..n] di List;

Passiamo ora alla procedura di attraversamento del grafico. Questo è un algoritmo ausiliario che permette di visualizzare tutti i vertici del grafico, analizzare tutti i campi di informazioni. Se consideriamo in profondità l'attraversamento del grafo, allora ci sono due tipi di algoritmi: ricorsivi e non ricorsivi.

Con l'algoritmo di attraversamento in profondità ricorsivo, prendiamo un vertice arbitrario e troviamo un vertice arbitrario invisibile (nuovo) v adiacente ad esso. Quindi prendiamo il vertice v come non nuovo e troviamo qualsiasi nuovo vertice adiacente ad esso. Se qualche vertice non ha vertici invisibili più recenti, allora consideriamo questo vertice da usare e restituiamo un livello più alto al vertice da cui siamo arrivati ​​al nostro vertice usato. L'attraversamento continua in questo modo finché non ci sono nuovi vertici non scansionati nel grafico.

In Pascal, la procedura di attraversamento in profondità sarebbe simile a questa:

Procedura Obhod(gr : Grafico; k : Byte);

Var g : Grafico; l : Elenco;

Iniziare

nov[k] := falso;

g := g;

Mentre g^.inf <> k lo fa

g := g^.successivo;

l := g^.smeg;

Mentre l <> zero inizia

Se nov[l^.inf] allora Obhod(gr, l^.inf);

l := l^.successivo;

End;

End;

Nota

In questa procedura, quando si descrive il tipo di Grafico, si intende la descrizione di un grafico mediante una lista di liste. Array nov[i] è un array speciale il cui i-esimo elemento è True se l'i-esimo vertice non è visitato, e False in caso contrario.

Viene spesso utilizzato anche un algoritmo di attraversamento non ricorsivo. In questo caso, la ricorsione viene sostituita da uno stack. Una volta che un vertice è stato visualizzato, viene inserito nello stack e viene utilizzato quando non ci sono più nuovi vertici adiacenti ad esso.

3. Rappresentazione di un grafico mediante un elenco di liste. Algoritmo di attraversamento del grafico di ampiezza

Un grafico può essere definito utilizzando un elenco di elenchi come segue:

TypeList = ^Tlist;

tlista=record

inf: byte;

successivo : Elenco;

fine;

Grafico = ^TGPaph;

TPaph = record

inf: byte;

smeg : Elenco;

successivo : Grafico;

fine;

Quando si attraversa il grafico in ampiezza, selezioniamo un vertice arbitrario e osserviamo tutti i vertici adiacenti ad esso contemporaneamente. Viene utilizzata una coda invece di uno stack. L'algoritmo di ricerca in ampiezza è molto utile per trovare il percorso più breve in un grafico.

Ecco una procedura per attraversare un grafico in larghezza in pseudocodice:

Procedura Obhod2(v);

{valori spisok, nov - globale}

Iniziare

coda = O;

coda <= v;

nov[v] = Falso;

Mentre la coda <> O fallo

Iniziare

p <= coda;

Per te in spisok(p) fai

Se nuovo[u] allora

Iniziare

nov[u] := Falso;

coda <= te;

End;

End;

End;

LEZIONE 11. Tipo di dati dell'oggetto

1. Tipo di oggetto in Pascal. Il concetto di un oggetto, la sua descrizione e utilizzo

Storicamente, il primo approccio alla programmazione è stato la programmazione procedurale, altrimenti nota come programmazione bottom-up. Inizialmente sono state create librerie comuni di programmi standard utilizzati in vari campi di applicazione del computer. Quindi, sulla base di questi programmi, sono stati creati programmi più complessi per risolvere problemi specifici.

Tuttavia, la tecnologia informatica era in costante sviluppo, iniziò ad essere utilizzata per risolvere vari problemi di produzione, economia, e quindi divenne necessario elaborare dati di vari formati e risolvere problemi non standard (ad esempio quelli non numerici). Pertanto, durante lo sviluppo di linguaggi di programmazione, hanno iniziato a prestare attenzione alla creazione di vari tipi di dati. Ciò ha contribuito all'emergere di tipi di dati così complessi come combinati, multipli, stringhe, file, ecc. Prima di risolvere il problema, il programmatore ha eseguito la scomposizione, ovvero suddividendo l'attività in più sottoattività, per ciascuna delle quali è stato scritto un modulo separato . La principale tecnologia di programmazione prevedeva tre fasi:

1) design dall'alto verso il basso;

2) programmazione modulare;

3) codificazione strutturale.

Ma a partire dalla metà degli anni '60 del XX secolo, iniziarono a formarsi nuovi concetti e approcci, che costituirono la base della tecnologia della programmazione orientata agli oggetti. In questo approccio, la modellazione e la descrizione del mondo reale vengono effettuate a livello di concetti di una specifica area tematica a cui appartiene il problema da risolvere.

La programmazione orientata agli oggetti è una tecnica di programmazione che ricorda da vicino il nostro comportamento. È una naturale evoluzione delle precedenti innovazioni nella progettazione del linguaggio di programmazione. La programmazione orientata agli oggetti è più strutturale di tutti i precedenti sviluppi riguardanti la programmazione strutturata. È anche più modulare e più astratto rispetto ai precedenti tentativi di astrazione dei dati e dettagli di programmazione internamente. Un linguaggio di programmazione orientato agli oggetti è caratterizzato da tre proprietà principali:

1) Incapsulamento. La combinazione di record con procedure e funzioni che manipolano i campi di questi record forma un nuovo tipo di dati: un oggetto;

2) Ereditarietà. Definizione di un oggetto e suo ulteriore utilizzo per costruire una gerarchia di oggetti figlio con la possibilità per ogni oggetto figlio correlato alla gerarchia di accedere al codice e ai dati di tutti gli oggetti padre;

3) Polimorfismo. Assegnare a un'azione un unico nome, che viene quindi condiviso su e giù nella gerarchia degli oggetti, con ogni oggetto nella gerarchia che esegue quell'azione in un modo che gli si addice.

Parlando dell'oggetto, introduciamo un nuovo tipo di dati: l'oggetto. Un tipo di oggetto è una struttura costituita da un numero fisso di componenti. Ciascun componente è un campo contenente dati di un tipo rigorosamente definito o un metodo che esegue operazioni su un oggetto. Per analogia con la dichiarazione di variabili, la dichiarazione di un campo specifica il tipo di dati di questo campo e l'identificatore che nomina il campo: per analogia con la dichiarazione di una procedura o funzione, la dichiarazione di un metodo specifica il titolo di una procedura, funzione, costruttore o distruttore.

Un tipo di oggetto può ereditare componenti di un altro tipo di oggetto. Se il tipo T2 eredita dal tipo T1, il tipo T2 è un figlio del tipo T1 e il tipo T1 stesso è un genitore del tipo T2. L'ereditarietà è transitiva, cioè se TK eredita da T2 e T2 eredita da T1, allora TK eredita da T1. L'ambito (dominio) di un tipo di oggetto è costituito da se stesso e da tutti i suoi discendenti.

Il codice sorgente seguente è un esempio di una dichiarazione del tipo di oggetto, type

Digitare

punto = oggetto

X, Y: intero;

fine;

Rett = oggetto

A, B: Punto T;

procedura Init(XA, YA, XB, YB: Intero);

procedura Copia(var R: TRectangle);

procedura Sposta(DX, DY: Intero);

procedura Cresci(DX, DY: Intero);

procedura Interseca(var R: TRectangle);

procedura Unione(var R: TRectangle);

funzione Contiene (P: Punto): Booleano;

fine;

StringPtr = ^Stringa;

FieldPtr = ^TFeld;

Campo = oggetto

X, Y, Len: intero;

Nome: StringPtr;

costruttore Copia(var F: TFeld);

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

distruttore Fatto; virtuale;

procedura Display; virtuale;

procedura Modifica; virtuale;

funzione GetStr: Stringa; virtuale;

funzione PutStr(S: String): Booleano; virtuale;

fine;

StrCampoPtr = ^TSrCampo;

StrField = oggetto(TFfield)

Valore: PString;

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

distruttore Fatto; virtuale;

funzione GetStr: Stringa; virtuale;

funzione PutStr(S: String): Booleano;

virtuale;

funzione Ottieni:stringa;

procedura Put(S: String);

fine;

NumCampoPtr = ^TNumCampo;

TNumField = oggetto(TFfield)

un bagno

Valore, Min, Max: Longint;

la percezione

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

FMin, FMax: Longint);

funzione GetStr: Stringa; virtuale;

funzione PutStr(S: String): Booleano; virtuale;

funzione Ottieni: Longint;

funzione Put(N: Longint);

fine;

ZipFieldPtr = ^TZipField;

ZipField = oggetto(TNumField)

funzione GetStr: Stringa; virtuale;

funzione PutStr(S: String): Booleano;

virtuale;

fine.

A differenza di altri tipi, i tipi di oggetto possono essere dichiarati solo nella sezione della dichiarazione del tipo al livello più esterno dell'ambito di un programma o modulo. Pertanto, i tipi di oggetto non possono essere dichiarati in una sezione di dichiarazione di variabile o all'interno di una procedura, una funzione o un blocco di metodi.

Un tipo di componente di tipo file non può avere un tipo di oggetto o qualsiasi tipo di struttura contenente componenti di tipo di oggetto.

2. Eredità

Il processo mediante il quale un tipo eredita le caratteristiche di un altro tipo è chiamato ereditarietà. Il discendente è chiamato tipo derivato (figlio) e il tipo da cui eredita il tipo figlio è chiamato tipo padre (genitore).

I tipi di record Pascal precedentemente noti non possono ereditare. Tuttavia, Borland Pascal estende il linguaggio Pascal per supportare l'ereditarietà. Una di queste estensioni è una nuova categoria di struttura dati relativa ai record, ma molto più potente. I tipi di dati in questa nuova categoria vengono definiti utilizzando la nuova parola riservata "oggetto". Un tipo di oggetto può essere definito come un tipo completo e indipendente nel modo di descrivere le voci Pascal, ma può anche essere definito come discendente di un tipo di oggetto esistente mettendo il tipo padre tra parentesi dopo la parola riservata "oggetto".

3. Istanziare oggetti

Un'istanza di un oggetto viene creata dichiarando una variabile o una costante di un tipo di oggetto, oppure applicando la procedura standard New a una variabile di tipo "puntatore al tipo di oggetto". L'oggetto risultante è chiamato un'istanza del tipo di oggetto;

var

F: Campo;

Z: TZipCampo;

FP: PFfield;

ZP: PZipField;

Date queste dichiarazioni di variabili, F è un'istanza di TField e Z è un'istanza di TZipField. Allo stesso modo, dopo aver applicato New a FP e ZP, FP punterà a un'istanza TField e ZP punterà a un'istanza TZipField.

Se un tipo di oggetto contiene metodi virtuali, le istanze di quel tipo di oggetto devono essere inizializzate chiamando un costruttore prima di chiamare qualsiasi metodo virtuale.

Di seguito è riportato un esempio:

var

S: StrCampo;

inizio

S.Init(1, 1, 25, 'Nome');

S.Put('Vladimir');

S.Display;

...

S Fatto;

fine.

Se S.Init non è stato chiamato, la chiamata di S.Display causerà l'esito negativo di questo esempio.

L'assegnazione di un'istanza di un tipo di oggetto non implica l'inizializzazione dell'istanza. Un oggetto viene inizializzato dal codice generato dal compilatore che viene eseguito tra la chiamata del costruttore e il punto in cui l'esecuzione raggiunge effettivamente la prima istruzione nel blocco di codice del costruttore.

Se l'istanza dell'oggetto non è inizializzata e il controllo dell'intervallo è abilitato (dalla direttiva {SR+}), la prima chiamata al metodo virtuale dell'istanza dell'oggetto genera un errore di runtime. Se il controllo dell'intervallo è disabilitato (dalla direttiva {SR-}), la prima chiamata a un metodo virtuale di un oggetto non inizializzato può portare a un comportamento imprevedibile.

La regola di inizializzazione obbligatoria si applica anche alle istanze che sono componenti di tipi struct. Per esempio:

var

Commento: array [1..5] di TStrField;

io: intero

iniziare

per I := da 1 a 5 fare

Commento [I].Init (1, I + 10, 40, 'first_name');

.

.

.

for I := da 1 a 5 commenta [I].Done;

fine;

Per le istanze dinamiche, l'inizializzazione riguarda in genere il posizionamento e la pulizia riguarda l'eliminazione, che si ottiene attraverso la sintassi estesa delle procedure standard New e Dispose. Per esempio:

var

SP: StrFieldPtr;

iniziare

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

SP^.Put('Vladimir');

SP^.Visualizza;

.

.

.

Smaltire (SP, Fatto);

fine.

Un puntatore a un tipo di oggetto è un'assegnazione compatibile con un puntatore a qualsiasi tipo di oggetto padre, quindi in fase di esecuzione un puntatore a un tipo di oggetto può puntare a un'istanza di quel tipo oa un'istanza di qualsiasi tipo figlio.

Ad esempio, un puntatore di tipo ZipFieldPtr può essere assegnato a puntatori di tipo PZipField, PNumField e PField e, in fase di esecuzione, un puntatore di tipo PField può essere nullo o puntare a un'istanza di TField, TNumField o TZipField o qualsiasi istanza di un tipo figlio di TField. .

Queste regole di compatibilità dei puntatori di assegnazione si applicano anche ai parametri delle variabili di tipo oggetto. Ad esempio, al metodo TField.Cop possono essere passate istanze di TField, TStrField, TNumField, TZipField o qualsiasi altro tipo figlio di TField.

4. Componenti e ambito

L'ambito di un identificatore di bean si estende oltre il tipo di oggetto. Inoltre, l'ambito di un identificatore di bean si estende attraverso i blocchi di procedure, funzioni, costruttori e distruttori che implementano i metodi del tipo di oggetto e dei suoi discendenti. Sulla base di queste considerazioni, l'ortografia dell'identificatore del componente deve essere univoca all'interno del tipo di oggetto e in tutti i suoi discendenti, nonché in tutti i suoi metodi.

L'ambito dell'identificatore del componente descritto nella parte privata della dichiarazione del tipo è limitato al modulo (programma) che contiene la dichiarazione del tipo di oggetto. In altre parole, i bean di identificatore privato agiscono come normali identificatori pubblici all'interno del modulo che contiene la dichiarazione del tipo di oggetto e all'esterno del modulo tutti i bean privati ​​e gli identificatori sono sconosciuti e inaccessibili. Inserendo tipi correlati di oggetti nello stesso modulo, puoi assicurarti che questi oggetti possano accedere ai rispettivi componenti privati ​​e che questi componenti privati ​​saranno sconosciuti agli altri moduli.

In una dichiarazione del tipo di oggetto, un'intestazione di metodo può specificare i parametri del tipo di oggetto descritto, anche se la dichiarazione non è ancora completa.

LEZIONE N. 12. Metodi

1. Metodi

Una dichiarazione di metodo all'interno di un tipo di oggetto corrisponde a una dichiarazione di metodo forward (forward). Pertanto, da qualche parte dopo una dichiarazione del tipo di oggetto, ma all'interno dello stesso ambito della dichiarazione del tipo di oggetto, è necessario implementare un metodo definendone la dichiarazione.

Per i metodi procedurali e funzionali, la dichiarazione di definizione assume la forma di una normale procedura o dichiarazione di funzione, con l'eccezione che in questo caso l'identificatore di procedura o funzione è trattato come identificatore di metodo.

Per i metodi del costruttore e del distruttore, la dichiarazione di definizione assume la forma di una dichiarazione del metodo di procedura, con l'eccezione che la parola riservata procedure è sostituita dal costruttore o dal distruttore di parola riservata.

La dichiarazione del metodo di definizione può, ma non è necessario, ripetere l'elenco dei parametri formali dell'intestazione del metodo nel tipo di oggetto. In questo caso, l'intestazione del metodo deve corrispondere esattamente all'intestazione nel tipo di oggetto nell'ordine, nei tipi e nei nomi dei parametri e nel tipo restituito del risultato della funzione se il metodo è una funzione.

La descrizione di definizione di un metodo contiene sempre un parametro implicito con l'identificatore Self, corrispondente a un parametro variabile formale che ha un tipo di oggetto. All'interno di un blocco di metodo, Self rappresenta l'istanza il cui componente del metodo è stato specificato per richiamare il metodo. Pertanto, qualsiasi modifica ai valori dei campi Self si riflette nell'istanza.

L'ambito di un identificatore di bean di tipo oggetto si estende a blocchi di procedure, funzioni, costruttori e distruttori che implementano metodi di quel tipo di oggetto. L'effetto è lo stesso che si avrebbe se all'inizio del blocco del metodo fosse inserita un'istruzione with della seguente forma:

con sé stessi

iniziare

...

fine;

Sulla base di queste considerazioni, l'ortografia degli identificatori dei componenti, dei parametri formali del metodo, del Self e di qualsiasi identificatore introdotto nella parte eseguibile del metodo deve essere univoco.

Se è richiesto un identificatore di metodo univoco, viene utilizzato l'identificatore di metodo qualificato. Consiste in un identificatore del tipo di oggetto seguito da un punto e da un identificatore di metodo. Come con qualsiasi altro identificatore, un identificatore di metodo qualificato può essere facoltativamente preceduto da un identificatore di pacchetto e da un punto.

Metodi virtuali

I metodi sono statici per impostazione predefinita, ma ad eccezione dei costruttori, possono essere virtuali (includendo la direttiva virtual nella dichiarazione del metodo). Il compilatore risolve i riferimenti alle chiamate di metodi statici durante il processo di compilazione, mentre le chiamate di metodi virtuali vengono risolte in fase di esecuzione. Questo è talvolta chiamato rilegatura tardiva.

Se un tipo di oggetto dichiara o eredita qualsiasi metodo virtuale, le variabili di quel tipo devono essere inizializzate chiamando un costruttore prima di chiamare qualsiasi metodo virtuale. Pertanto, un tipo di oggetto che descrive o eredita un metodo virtuale deve anche descrivere o ereditare almeno un metodo costruttore.

Un tipo di oggetto può sovrascrivere qualsiasi metodo che eredita dai suoi genitori. Se una dichiarazione di metodo in un figlio specifica lo stesso identificatore di metodo di una dichiarazione di metodo nel genitore, la dichiarazione nel figlio sovrascrive la dichiarazione nel genitore. L'ambito di un metodo sovrascritto si espande all'ambito del figlio in cui è stato introdotto il metodo e rimarrà tale fino a quando l'identificatore del metodo non verrà nuovamente sovrascritto.

L'override di un metodo statico è indipendente dalla modifica dell'intestazione del metodo. Al contrario, l'override di un metodo virtuale deve preservare l'ordine, i tipi e i nomi dei parametri e i tipi di risultati delle funzioni, se presenti. Inoltre, la ridefinizione deve includere nuovamente la direttiva virtuale.

Metodi dinamici

Borland Pascal supporta ulteriori metodi di associazione tardiva chiamati metodi dinamici. I metodi dinamici differiscono dai metodi virtuali solo per il modo in cui vengono inviati in fase di esecuzione. Sotto tutti gli altri aspetti, i metodi dinamici sono considerati equivalenti a quelli virtuali.

Una dichiarazione di metodo dinamico equivale a una dichiarazione di metodo virtuale, ma la dichiarazione di metodo dinamico deve includere l'indice del metodo dinamico, che viene specificato immediatamente dopo la parola chiave virtual. L'indice di un metodo dinamico deve essere una costante intera compresa tra 1 e 656535 e deve essere univoco tra gli indici di altri metodi dinamici contenuti nel tipo di oggetto o nei suoi predecessori. Per esempio:

procedura FileOpen(var Msg: TMessage); virtuale 100;

Una sostituzione di un metodo dinamico deve corrispondere all'ordine, ai tipi e ai nomi dei parametri e corrispondere esattamente al tipo di risultato della funzione del metodo padre. L'override deve includere anche una direttiva virtuale seguita dallo stesso indice del metodo dinamico specificato nel tipo di oggetto predecessore.

2. Costruttori e distruttori

Costruttori e distruttori sono forme specializzate di metodi. Utilizzati in connessione con la sintassi estesa delle procedure standard New e Dispose, i costruttori e i distruttori hanno la capacità di posizionare e rimuovere oggetti dinamici. Inoltre, i costruttori hanno la capacità di eseguire l'inizializzazione richiesta di oggetti contenenti metodi virtuali. Come tutti i metodi, costruttori e distruttori possono essere ereditati e gli oggetti possono contenere un numero qualsiasi di costruttori e distruttori.

I costruttori vengono utilizzati per inizializzare gli oggetti appena creati. In genere, l'inizializzazione si basa sui valori passati al costruttore come parametri. Un costruttore non può essere virtuale perché il meccanismo di invio di un metodo virtuale dipende dal costruttore che ha inizializzato per primo l'oggetto.

Ecco alcuni esempi di costruttori:

costruttore Field.Copy(var F: Field);

iniziare

Sé := F;

fine;

costruttore Field.Init(FX, FY, FLen: intero; FName: stringa);

iniziare

X :=FX;

S := anno;

GetMem(Nome, Lunghezza(NomeF) + 1);

Nome^ := FNome;

fine;

costruttore TStrField.Init(FX, FY, FLen: intero; FName: stringa);

iniziare

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

Campo.Init(FX, FY, FLen, FNome);

GetMem(Valore, Len);

Valore^ := '';

fine;

L'azione principale di un costruttore di tipo derivato (figlio), come il campo TStr sopra. Init è quasi sempre una chiamata al costruttore appropriato del suo genitore immediato per inizializzare i campi ereditati dell'oggetto. Dopo aver eseguito questa procedura, il costruttore inizializza i campi dell'oggetto che appartengono solo al tipo derivato.

I distruttori sono l'opposto dei costruttori e vengono utilizzati per ripulire gli oggetti dopo che sono stati utilizzati. Normalmente, la pulizia consiste nella rimozione di tutti i campi del puntatore nell'oggetto.

Nota

Un distruttore può essere virtuale, e spesso lo è. Un distruttore ha raramente parametri.

Ecco alcuni esempi di distruttori:

distruttore Campo Fatto;

iniziare

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

fine;

distruttore StrField.Done;

iniziare

FreeMem(Valore, Len);

campo fatto;

fine;

Il distruttore di un tipo figlio, ad esempio TStrField sopra. Fatto, in genere rimuove prima i campi del puntatore introdotti nel tipo derivato e quindi, come ultimo passaggio, chiama il distruttore di raccolta appropriato del genitore immediato per rimuovere i campi del puntatore ereditati dell'oggetto.

3. Distruttori

Borland Pascal fornisce un tipo speciale di metodo chiamato Garbage Collector (o distruttore) per ripulire ed eliminare un oggetto allocato dinamicamente. Il distruttore combina la fase di eliminazione di un oggetto con qualsiasi altra azione o attività richiesta per quel tipo di oggetto. È possibile definire più distruttori per un singolo tipo di oggetto.

Il distruttore è definito insieme a tutti gli altri metodi oggetto nella definizione del tipo dell'oggetto:

genere

Temployee = oggetto

Nome: stringa[25];

Titolo: stringa[25];

Tariffa: Reale;

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

distruttore Fatto; virtuale;

funzione Ottieni Nome: Stringa;

funzione OttieniTitolo: Stringa;

funzione GetRate: Tasso; virtuale;

funzione GetPayAmount: reale; virtuale;

fine;

I distruttori possono essere ereditati e possono essere statici o virtuali. Poiché finalizzatori diversi tendono a richiedere tipi diversi di oggetti, è generalmente consigliabile che i distruttori siano sempre virtuali in modo che venga eseguito il distruttore corretto per ogni tipo di oggetto.

Non è necessario specificare il distruttore di parole riservate per ogni metodo di pulizia, anche se la definizione del tipo dell'oggetto contiene metodi virtuali. I distruttori funzionano davvero solo su oggetti allocati dinamicamente.

Quando un oggetto allocato dinamicamente viene ripulito, il distruttore svolge una funzione speciale: assicura che il numero corretto di byte venga sempre liberato nell'area di memoria allocata dinamicamente. Non ci può essere alcuna preoccupazione sull'utilizzo di un distruttore con oggetti allocati staticamente; infatti, non passando il tipo dell'oggetto al distruttore, il programmatore priva un oggetto di quel tipo dei pieni benefici della gestione dinamica della memoria in Borland Pascal.

I distruttori diventano effettivamente se stessi quando gli oggetti polimorfici devono essere cancellati e quando la memoria che occupano deve essere deallocata.

Gli oggetti polimorfici sono quegli oggetti che sono stati assegnati a un tipo padre a causa delle regole di compatibilità dei tipi estesi di Borland Pascal. Un'istanza di un oggetto di tipo THourly assegnato a una variabile di tipo TEmployee è un esempio di oggetto polimorfico. Queste regole possono essere applicate anche agli oggetti; un puntatore a THourly può essere assegnato liberamente a un puntatore a TEmployee e l'oggetto puntato da quel puntatore sarà di nuovo un oggetto polimorfico. Il termine "polimorfico" è appropriato perché il codice che elabora un oggetto "non sa" esattamente in fase di compilazione quale tipo di oggetto dovrà eventualmente elaborare. L'unica cosa che sa è che questo oggetto appartiene a una gerarchia di oggetti che sono discendenti del tipo di oggetto specificato.

Ovviamente, le dimensioni dei tipi di oggetto sono diverse. Quindi, quando arriva il momento di ripulire un oggetto polimorfico allocato nell'heap, come fa Dispose a sapere quanti byte di spazio dell'heap liberare? In fase di compilazione, nessuna informazione sulla dimensione dell'oggetto può essere estratta da un oggetto polimorfico.

Il distruttore risolve questo enigma facendo riferimento al luogo in cui sono scritte queste informazioni, nelle variabili di implementazione del TCM. Ogni TBM di un tipo di oggetto contiene la dimensione in byte di quel tipo di oggetto. La tabella dei metodi virtuali di qualsiasi oggetto è disponibile tramite il parametro nascosto Self, inviato al metodo quando il metodo viene chiamato. Un distruttore è solo un tipo di metodo, quindi quando un oggetto lo chiama, il distruttore ottiene una copia di Self nello stack. Pertanto, se un oggetto è polimorfico in fase di compilazione, non lo sarà mai in fase di esecuzione a causa dell'associazione tardiva.

Per eseguire questa deallocazione late-bound, il distruttore deve essere chiamato come parte della sintassi estesa della procedura Dispose:

Smaltire(P, Fatto);

(Una chiamata al distruttore al di fuori della procedura Dispose non rilascia alcuna memoria.) Quello che sta realmente accadendo qui è che il Garbage Collector dell'oggetto indicato da P viene eseguito come un metodo normale. Tuttavia, una volta completata l'ultima azione, il distruttore cerca la dimensione dell'implementazione del suo tipo nel TCM e passa la dimensione alla procedura Elimina. La procedura Dispose termina il processo cancellando il numero corretto di byte dello spazio heap che (lo spazio) apparteneva in precedenza a P^. Il numero di byte da liberare sarà corretto indipendentemente dal fatto che P puntasse a un'istanza di tipo TSalaried o se puntasse a uno dei tipi figlio di tipo TSalaried, ad esempio TCommissioned.

Nota che il metodo distruttore stesso può essere vuoto ed eseguire solo questa funzione:

distruttoreAnObject.Done;

iniziare

fine;

Ciò che è utile in questo distruttore non è la proprietà del suo corpo, tuttavia, il compilatore genera il codice dell'epilogo in risposta alla parola riservata del distruttore. È come un modulo che non esporta nulla, ma fa del lavoro invisibile eseguendo la sua sezione di inizializzazione prima di avviare il programma. Tutta l'azione si svolge dietro le quinte.

4. Metodi virtuali

Un metodo diventa virtuale se la sua dichiarazione del tipo di oggetto è seguita dalla nuova parola riservata virtual. Se un metodo in un tipo padre viene dichiarato virtuale, anche tutti i metodi con lo stesso nome nei tipi figlio devono essere dichiarati virtuali per evitare un errore del compilatore.

Di seguito sono riportati gli oggetti del libro paga di esempio, opportunamente virtualizzati:

genere

PEdipendente = ^TEmployee;

Temployee = oggetto

Nome, Titolo: stringa[25];

Tariffa: Reale;

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

funzione GetPayAmount : Reale; virtuale;

funzione GetName : Stringa;

funzione GetTitle : Stringa;

funzione GetRate : Reale;

procedura Mostra; virtuale;

fine;

POraria = ^TOraria;

TOraria = oggetto(Timpiegato);

Tempo: intero;

costruttore Init(AName, ATitle: String; Arate: Real; Time: Integer);

funzione GetPayAmount : Reale; virtuale;

funzione GetTime : Intero;

fine;

PSlaried = ^TSlaried;

TSalaried = oggetto(TEmployee);

funzione GetPayAmount : Reale; virtuale;

fine;

PCommissionato = ^TCommissioned;

TCommissioned = oggetto(stipendiato);

Commissione : Reale;

Importo delle vendite: reale;

costruttore Init(AName, ATitle: String; ARate,

ACommissione, AImporto di vendita: Reale);

funzione GetPayAmount : Reale; virtuale;

fine;

Un costruttore è un tipo speciale di procedura che esegue alcune operazioni di configurazione per il meccanismo del metodo virtuale. Inoltre, il costruttore deve essere chiamato prima di chiamare qualsiasi metodo virtuale. Chiamare un metodo virtuale senza prima chiamare il costruttore può bloccare il sistema e non c'è modo per il compilatore di controllare l'ordine in cui vengono chiamati i metodi.

Ogni tipo di oggetto che ha metodi virtuali deve avere un costruttore.

Attenzione

Il costruttore deve essere chiamato prima che venga chiamato qualsiasi altro metodo virtuale. La chiamata di un metodo virtuale senza una precedente chiamata al costruttore può causare un blocco del sistema e il compilatore non può controllare l'ordine in cui vengono chiamati i metodi.

Nota

Per i costruttori di oggetti, si suggerisce di utilizzare l'identificatore Init.

Ogni istanza di oggetto distinta deve essere inizializzata con una chiamata al costruttore separata. Non è sufficiente inizializzare un'istanza di un oggetto e quindi assegnare quell'istanza ad altri. Altre istanze, anche se possono contenere dati validi, non verranno inizializzate con un operatore di assegnazione e bloccheranno il sistema su eventuali chiamate ai loro metodi virtuali. Per esempio:

var

FBee, GBee: Ape; {crea due istanze Bee}

iniziare

FBee.Init(5, 9) { chiamata del costruttore per FBee }

GBee := FBee; {Gbee non è valido! }

fine;

Cosa crea esattamente un costruttore? Ogni tipo di oggetto contiene qualcosa chiamato tabella del metodo virtuale (VMT) nel segmento di dati. Il TVM contiene la dimensione del tipo di oggetto e, per ogni metodo virtuale, un puntatore al codice che esegue quel metodo. Un costruttore stabilisce una relazione tra l'implementazione della chiamata dell'oggetto e il tipo di oggetto TCM.

È importante ricordare che esiste una sola TBM per ogni tipo di oggetto. Istanze separate di un tipo di oggetto (ovvero, variabili di questo tipo) contengono solo la connessione alla TBM, ma non la TBM stessa. Il costruttore imposta il valore di questa connessione su TBM. È per questo motivo che da nessuna parte è possibile avviare l'esecuzione prima di chiamare il costruttore.

5. Campi dati oggetto e parametri del metodo formale

L'implicazione del fatto che i metodi ei loro oggetti condividono un ambito comune è che i parametri formali di un metodo non possono essere identici a nessuno dei campi dati dell'oggetto. Questa non è una nuova limitazione imposta dalla programmazione orientata agli oggetti, ma piuttosto le stesse vecchie regole di ambito che Pascal ha sempre avuto. Questo equivale a impedire che i parametri formali di una procedura siano identici alle variabili locali della procedura:

procedura CrunchIt(Crunchee: MyDataRec, Crunchby,

Codice di errore: intero);

var

A, B: carattere;

Codice di errore: intero;

iniziare

.

.

.

Le variabili locali di una procedura ei suoi parametri formali condividono un ambito comune e pertanto non possono essere identici. Otterrai "Errore 4: identificatore duplicato" se provi a compilare qualcosa del genere, lo stesso errore si verifica quando tenti di impostare un parametro del metodo formale sul nome del campo dell'oggetto a cui appartiene il metodo.

Le circostanze sono alquanto diverse, poiché inserire l'intestazione della procedura all'interno di una struttura dati è un cenno a un'innovazione in Turbo Pascal, ma i principi di base dell'ambito Pascal non sono cambiati.

CONFERENZA N. 13. Compatibilità dei tipi di oggetti

1. Incapsulamento

La combinazione di codice e dati in un oggetto è chiamata incapsulamento. In linea di principio, è possibile fornire metodi sufficienti in modo che l'utente di un oggetto non possa mai accedere direttamente ai campi dell'oggetto. Alcuni altri linguaggi orientati agli oggetti, come Smalltalk, richiedono l'incapsulamento obbligatorio, ma Borland Pascal ha una scelta.

Ad esempio, gli oggetti TEmployee e THourly sono scritti in modo tale che non sia assolutamente necessario accedere direttamente ai loro campi dati interni:

Digitare

Temployee = oggetto

Nome, Titolo: stringa[25];

Tariffa: Reale;

procedura Init(ANome, ATitolo: stringa; Arate: Reale);

funzione GetName : Stringa;

funzione GetTitle : Stringa;

funzione GetRate : Reale;

funzione GetPayAmount : Reale;

fine;

TOraria = oggetto(Timpiegato)

Tempo: intero;

procedura Init(ANome, ATitolo: stringa; Arate:

Reale, Tempo: Intero);

funzione GetPayAmount : Reale;

fine;

Ci sono solo quattro campi di dati qui: nome, titolo, tariffa e ora. I metodi GetName e GetTitle mostrano rispettivamente il cognome e la posizione del lavoratore. Il metodo GetPayAmount utilizza Rate, e nel caso di THourly e Time di lavoro per calcolare l'importo dei pagamenti al lavoro. Non è più necessario fare riferimento direttamente a questi campi di dati.

Supponendo l'esistenza di un'istanza AnHourly di tipo THourly, potremmo utilizzare un insieme di metodi per manipolare i campi di dati AnHourly come questo:

con un'ora

iniziare

Init (Aleksandr Petrov, Operatore di carrelli elevatori' 12.95, 62);

{Visualizza il cognome, la posizione e l'importo dei pagamenti}

Mostrare;

fine;

Va notato che l'accesso ai campi di un oggetto viene effettuato solo con l'aiuto dei metodi di questo oggetto.

2. Oggetti in espansione

Sfortunatamente, il Pascal standard non fornisce alcuna funzionalità per la creazione di procedure flessibili che consentono di lavorare con tipi di dati completamente diversi. La programmazione orientata agli oggetti risolve questo problema con l'ereditarietà: se viene definito un tipo derivato, i metodi del tipo padre vengono ereditati, ma possono essere sovrascritti se lo si desidera. Per sovrascrivere un metodo ereditato, è sufficiente dichiarare un nuovo metodo con lo stesso nome del metodo ereditato, ma con un corpo diverso e (se necessario) un diverso insieme di parametri.

Definiamo un tipo figlio di TEmployee che rappresenta un dipendente a cui viene pagata una tariffa oraria nell'esempio seguente:

const

PayPeriods = 26; {periodi di pagamento}

Soglia straordinari = 80; {per il periodo di pagamento}

Fattore straordinari = 1.5; { tariffa oraria }

Digitare

TOraria = oggetto(Timpiegato)

Tempo: intero;

procedura Init(ANome, ATitolo: stringa; Arate:

Reale, Tempo: Intero);

funzione GetPayAmount : Reale;

fine;

procedura THourly.Init(ANome, ATitolo: stringa;

Arate: Reale, Tempo: Intero);

iniziare

TEmployee.Init(ANome, ATitolo, Arate);

Ora := ATime;

fine;

funzione THourly.GetPayAmount: Real;

var

Straordinario: Intero;

iniziare

Straordinario := Tempo - Soglia Straordinario;

se Straordinario > 0 allora

GetPayAmount := RoundPay(Soglia di straordinario * Tariffa +

Tariffa Straordinari * Fattore Straordinari * Tariffa)

altro

GetPayAmount := RoundPay (tempo * tariffa)

fine;

Una persona pagata a tariffa oraria è un lavoratore: ha tutto ciò che serve per definire l'oggetto TEmployee (nome, posizione, tariffa), e solo la quantità di denaro ricevuta dalla persona oraria dipende da quante ore ha lavorato durante il periodo da pagare. Pertanto, THourly richiede anche un campo Time.

Poiché THourly definisce un nuovo campo Time, la sua inizializzazione richiede un nuovo metodo Init che inizializza sia l'ora che i campi ereditati. Invece di assegnare direttamente valori ai campi ereditati come Nome, Titolo e Tasso, perché non riutilizzare il metodo di inizializzazione dell'oggetto TEmployee (illustrato dalla prima istruzione THourly Init).

Chiamare un metodo che viene sovrascritto non è lo stile migliore. In generale, è possibile che TEmployee.Init esegua un'inizializzazione importante ma nascosta.

Quando si chiama un metodo sottoposto a override, è necessario assicurarsi che il tipo di oggetto derivato includa la funzionalità del genitore. Inoltre, qualsiasi modifica al metodo padre influisce automaticamente su tutti i discendenti.

Dopo aver chiamato TEmployee.Init, THourly.Init può quindi eseguire la propria inizializzazione, che in questo caso consiste solo nell'assegnazione del valore passato in ATime.

Un altro esempio di metodo sottoposto a override è la funzione THourly.GetPayAmount, che calcola l'importo del pagamento per un dipendente orario. In effetti, ogni tipo di oggetto TEmployee ha il proprio metodo GetPayAmount, poiché il tipo di lavoratore dipende da come viene effettuato il calcolo. Il metodo THourly.GetPayAmount dovrebbe tenere conto di quante ore ha lavorato il dipendente, se ci sono stati straordinari, qual è stato il fattore di aumento per gli straordinari, ecc.

Metodo salariato. GetPayAmount dovrebbe dividere la tariffa del dipendente solo per il numero di pagamenti in ogni anno (nel nostro esempio).

unità di lavoro;

interfaccia

const

PayPeriods = 26; {nell'anno}

Soglia straordinari = 80; {per ogni periodo di pagamento}

Fattore straordinari=1.5; {aumento rispetto al normale pagamento}

Digitare

Temployee = oggetto

Nome, Titolo: stringa[25];

Tariffa: Reale;

procedura Init(ANome, ATitolo: stringa; Arate: Reale);

funzione GetName : Stringa;

funzione GetTitle : Stringa;

funzione GetRate : Reale;

funzione GetPayAmount : Reale;

fine;

TOraria = oggetto(Timpiegato)

Tempo: intero;

procedura Init(ANome, ATitolo: stringa; Arate:

Reale, Tempo: Intero);

funzione GetPayAmount : Reale;

funzione GetTime: reale;

fine;

TSalaried = oggetto(TEmployee)

funzione GetPayAmount : Reale;

fine;

TCommissioned = oggetto(TSalaried)

Commissione : Reale;

Importo delle vendite: reale;

costruttore Init(AName, ATitle: String; ARate,

ACommissione, AImporto di vendita: Reale);

funzione GetPayAmount : Reale;

fine;

implementazione

funzione RoundPay (salari: reale): reale;

{arrotondare per eccesso i pagamenti per ignorare importi inferiori a

unità monetaria}

iniziare

RoundPay := Tronca(salari * 100) / 100;

.

.

.

TEmployee è la parte superiore della nostra gerarchia di oggetti e contiene il primo metodo GetPayAmount.

funzione TEmployee.GetPayAmount : Real;

iniziare

RunError(211); {fornire errore di runtime}

fine;

Potrebbe sorprendere il fatto che il metodo dia un errore di runtime. Se viene chiamato Employee.GetPayAmount, si verifica un errore nel programma. Come mai? Perché TEmployee è in cima alla nostra gerarchia di oggetti e non definisce un vero lavoratore; pertanto, nessuno dei metodi TEmployee viene chiamato in un modo specifico, sebbene possano essere ereditati. Tutti i nostri dipendenti sono a ore, stipendiati oa cottimo. Un errore di runtime termina l'esecuzione del programma e genera 211, che corrisponde a un messaggio di errore associato a una chiamata di metodo astratta (se il programma chiama TEmployee.GetPayAmount per errore).

Di seguito è riportato il metodo THourly.GetPayAmount, che tiene conto di aspetti come la retribuzione degli straordinari, le ore lavorate, ecc.

funzione THourly.GetPayAMount : Real;

var

Straordinario: Intero;

iniziare

Straordinario := Tempo - Soglia Straordinario;

se Straordinario > 0 allora

GetPayAmount := RoundPay(Soglia di straordinario * Tariffa +

Tariffa Straordinari * Fattore Straordinari * Tariffa)

altro

GetPayAmount := RoundPay (tempo * tariffa)

fine;

Il metodo TSalaried.GetPayAmount è molto più semplice; in esso scommetto

diviso per il numero di pagamenti:

funzione TSalaried.GetPayAmount : Real;

iniziare

GetPayAmount := RoundPay(Tariffa/Periodo di pagamento);

fine;

Se osservi il metodo TCommissioned.GetPayAmount, vedrai che chiama TSalaried.GetPayAmount, calcola la commissione e la aggiunge al valore restituito dal metodo TSalaried. Ottieni ImportoPaga.

funzione TCommissioned.GetPayAmount : Real;

iniziare

OttieniimportoPay := RoundPay(TSalary.GetPayAmount +

Commissione * Importo delle vendite);

fine;

Nota importante: sebbene i metodi possano essere sovrascritti, i campi dati non possono essere sovrascritti. Una volta che un campo dati è stato definito in una gerarchia di oggetti, nessun tipo figlio può definire un campo dati con esattamente lo stesso nome.

3. Compatibilità dei tipi di oggetti

L'ereditarietà modifica in una certa misura le regole di compatibilità dei tipi di Borland Pascal. Tra le altre cose, un tipo derivato eredita la compatibilità di tipo di tutti i suoi tipi padre.

Questa compatibilità di tipo esteso assume tre forme:

1) tra implementazioni di oggetti;

2) tra puntatori a implementazioni di oggetti;

3) tra parametri formali ed effettivi.

Tuttavia, è molto importante ricordare che in tutte e tre le forme, la compatibilità dei tipi si estende solo dal figlio al genitore. In altre parole, i tipi figlio possono essere utilizzati liberamente al posto dei tipi padre, ma non viceversa.

Ad esempio, TSalaried è un figlio di TEmployee e TSosh-missioned è un figlio di TSalaried. Con questo in mente, considera le seguenti descrizioni:

genere

PEdipendente = ^TEmployee;

PSlaried = ^TSlaried;

PCommissionato = ^TCommissioned;

var

Un dipendente: TE dipendente;

Asalato: Tsalato;

PCommissionato: TCommissionato;

TEmployeePtr: PEmployee;

TSsalariedPtr: PSsalaried;

TCommissionedPtr: PCommissioned;

Sotto queste descrizioni, sono validi i seguenti operatori

Compiti:

Un dipendente :=ASalariato;

Asalato := ACommissionato;

TCommissionedPtr := ACommissioned;

Nota

A un oggetto padre può essere assegnata un'istanza di uno qualsiasi dei suoi tipi derivati. Non sono ammessi incarichi arretrati.

Questo concetto è nuovo per Pascal e all'inizio potrebbe essere difficile ricordare quale sia la compatibilità del tipo di ordine. Devi pensare in questo modo: la sorgente deve essere in grado di riempire completamente il ricevitore. I tipi derivati ​​contengono tutto ciò che i loro tipi padre contengono a causa della proprietà dell'ereditarietà. Pertanto, il tipo derivato ha esattamente la stessa dimensione oppure (cosa che accade nella maggior parte dei casi) è più grande del suo genitore, ma mai più piccolo. Assegnare un oggetto genitore (genitore) a un figlio (figlio) potrebbe lasciare indefiniti alcuni campi dell'oggetto figlio, il che è pericoloso e quindi illegale.

Nelle istruzioni di assegnazione, solo i campi comuni a entrambi i tipi verranno copiati dall'origine alla destinazione. Nell'operatore di assegnazione:

Un dipendente:= Un incaricato;

Solo i campi Nome, Titolo e Tasso di ACommissioned verranno copiati in AnEmployee, poiché questi sono gli unici campi comuni a TCommissioned e TEmployee. La compatibilità dei tipi funziona anche tra i puntatori ai tipi di oggetti e segue le stesse regole generali delle implementazioni di oggetti. Un puntatore a un figlio può essere assegnato a un puntatore al genitore. Date le definizioni precedenti, sono valide le seguenti assegnazioni di puntatori:

TsalariedPtr:= TCommissionedPtr;

TEmployeePtr:= TsalariedPtr;

TEmployeePtr:= PCommissionedPtr;

Ricorda che le assegnazioni inverse non sono consentite!

Un parametro formale (un valore o un parametro variabile) di un determinato tipo di oggetto può assumere come parametro effettivo un oggetto del proprio tipo o oggetti di tutti i tipi figlio. Se definisci un'intestazione di procedura come questa:

procedura CalcFedTax(Vittima: Tstipendiato);

quindi i tipi di parametro effettivi possono essere Tsalaried o TCommissioned, ma non TEmployee. La vittima può anche essere un parametro variabile. In questo caso si seguono le stesse regole di compatibilità.

osservazione

C'è una differenza fondamentale tra parametri di valore e parametri variabili. Un parametro valore è un puntatore all'oggetto effettivo passato come parametro, mentre un parametro variabile è solo una copia del parametro effettivo. Inoltre, questa copia include solo quei campi che sono inclusi nel tipo del parametro del valore formale. Ciò significa che il parametro effettivo viene letteralmente convertito nel tipo del parametro formale. Un parametro variabile è più simile al cast di un pattern, nel senso che il parametro effettivo rimane invariato.

Allo stesso modo, se il parametro formale è un puntatore a un tipo di oggetto, il parametro effettivo può essere un puntatore a quel tipo di oggetto oa qualsiasi tipo figlio. Sia dato il titolo della procedura:

procedura Worker.Add(AWorker: PSalared);

I tipi di parametro effettivi validi sarebbero quindi PSalaried o PCommissioned, ma non PEmployee.

CONFERENZA N. 14. Assemblatore

1. Informazioni sull'assemblatore

Un tempo l'assembler era un linguaggio senza sapere quale fosse impossibile far fare a un computer qualcosa di utile. A poco a poco la situazione è cambiata. Apparvero mezzi di comunicazione più convenienti con un computer. Ma a differenza di altri linguaggi, l'assembler non è morto; inoltre, non poteva farlo in linea di principio. Come mai? Alla ricerca di una risposta, cercheremo di capire cosa sia il linguaggio assembly in generale.

In breve, il linguaggio assembly è una rappresentazione simbolica del linguaggio macchina. Tutti i processi nella macchina al livello hardware più basso sono guidati solo da comandi (istruzioni) del linguaggio macchina. Da ciò risulta chiaro che, nonostante il nome comune, il linguaggio assembly per ogni tipo di computer è diverso. Questo vale anche per l'aspetto dei programmi scritti in assembler e per le idee di cui questo linguaggio riflette.

Risolvere davvero problemi relativi all'hardware (o, ancora di più, problemi relativi all'hardware, come accelerare un programma, ad esempio) è impossibile senza la conoscenza dell'assembler.

Un programmatore o qualsiasi altro utente può utilizzare qualsiasi strumento di alto livello fino a programmi per la costruzione di mondi virtuali e, forse, nemmeno sospettare che il computer stia effettivamente eseguendo non i comandi del linguaggio in cui è scritto il suo programma, ma la loro rappresentazione trasformata sotto forma di una sequenza noiosa e noiosa di comandi di un linguaggio completamente diverso: il linguaggio macchina. Ora immagina che un tale utente abbia un problema non standard. Ad esempio, il suo programma deve funzionare con un dispositivo insolito o eseguire altre azioni che richiedono la conoscenza dei principi dell'hardware del computer. Non importa quanto sia buono il linguaggio in cui il programmatore ha scritto il suo programma, non può fare a meno di conoscere l'assemblatore. E non è un caso che quasi tutti i compilatori di linguaggi di alto livello contengano mezzi per connettere i propri moduli con moduli in assembler o supportino l'accesso al livello di programmazione assembler.

Un computer è composto da diversi dispositivi fisici, ognuno dei quali è collegato a un'unità, chiamata unità di sistema. Per comprendere il loro scopo funzionale, osserviamo lo schema a blocchi di un tipico computer (Fig. 1). Non pretende l'accuratezza assoluta e mira solo a mostrare lo scopo, l'interconnessione e la composizione tipica degli elementi di un moderno personal computer.

Riso. 1. Schema strutturale di un personal computer

2. Modello software del microprocessore

Nel mercato dei computer di oggi, esiste un'ampia varietà di diversi tipi di computer. Pertanto, è possibile presumere che il consumatore avrà una domanda su come valutare le capacità di un particolare tipo (o modello) di computer e le sue caratteristiche distintive rispetto a computer di altri tipi (modelli). Per riunire tutti i concetti che caratterizzano un computer in termini di proprietà funzionali controllate dal programma, esiste un termine speciale: architettura del computer. Per la prima volta, con l'avvento delle macchine di terza generazione, si cominciò a menzionare il concetto di architettura del computer per la loro valutazione comparativa.

Ha senso iniziare ad imparare il linguaggio assembly di qualsiasi computer solo dopo aver scoperto quale parte del computer è rimasta visibile e disponibile per la programmazione in questo linguaggio. Questo è il cosiddetto modello di programma per computer, parte del quale è il modello di programma a microprocessore, che contiene trentadue registri, più o meno disponibili per l'uso da parte del programmatore.

Questi registri possono essere divisi in due grandi gruppi:

1) 6 registri utenti;

2) 16 registri di sistema.

3. Registri utenti

Come suggerisce il nome, i registri utente vengono chiamati perché il programmatore può utilizzarli durante la scrittura dei suoi programmi. Questi registri includono (Fig. 2):

1) otto registri a 32 bit che possono essere utilizzati dai programmatori per memorizzare dati e indirizzi (chiamati anche registri di uso generale (RON)):

eax/ax/ah/al;

eb/bx/bh/bl;

edx/dx/dh/dl;

ecx/cx/ch/cl;

bp/bp;

si/si;

edi/di;

spec/sp.

2) registri a sei segmenti: cs, ds, ss, es, fs, gs;

3) registri di stato e di controllo:

flag registro eflags/flag;

registro del puntatore di comando eip/ip.

Riso. 2. Registri utente

Molti di questi registri sono contrassegnati da una barra. Questi non sono registri diversi: sono parti di un grande registro a 32 bit. Possono essere usati nel programma come oggetti separati.

4. Registri generali

Tutti i registri di questo gruppo consentono di accedere alle loro parti "inferiori". Solo le parti inferiori a 16 e 8 bit di questi registri possono essere utilizzate per l'indirizzamento automatico. I 16 bit superiori di questi registri non sono disponibili come oggetti indipendenti.

Elenchiamo i registri appartenenti al gruppo dei registri di uso generale. Poiché questi registri si trovano fisicamente nel microprocessore all'interno dell'unità logica aritmetica (AL>), sono anche chiamati registri ALU:

1) eax/ax/ah/al (registro accumulatore) - batteria. Utilizzato per memorizzare dati intermedi. In alcuni comandi è richiesto l'uso di questo registro;

2) ebx/bx/bh/bl (registro di base) - registro di base. Utilizzato per memorizzare l'indirizzo di base di un oggetto in memoria;

3) ecx/cx/ch/cl (Registro di conteggio) - registro di conteggio. Viene utilizzato nei comandi che eseguono alcune azioni ripetitive. Il suo utilizzo è spesso implicito e nascosto nell'algoritmo del comando corrispondente.

Ad esempio, il comando di organizzazione del ciclo, oltre a trasferire il controllo a un comando situato a un determinato indirizzo, analizza e decrementa di uno il valore del registro esx/cx;

4) edx/dx/dh/dl (Registro dati) - registro dati.

Proprio come il registro eax/ax/ah/al, memorizza i dati intermedi. Alcuni comandi ne richiedono l'uso; per alcuni comandi questo accade implicitamente.

A supporto delle cosiddette operazioni a catena, ovvero operazioni che elaborano sequenzialmente catene di elementi, ciascuna delle quali può avere una lunghezza di 32, 16 o 8 bit, vengono utilizzati i due seguenti registri:

1) esi/si (Source Index register) - indice sorgente.

Questo registro nelle operazioni a catena contiene l'indirizzo corrente dell'elemento nella catena di origine;

2) edi/di (Destination Index register) - indice del destinatario (destinatario). Questo registro nelle operazioni a catena contiene l'indirizzo corrente nella catena di destinazione.

Nell'architettura del microprocessore a livello hardware e software, è supportata una struttura di dati come uno stack. Per lavorare con lo stack nel sistema di istruzioni del microprocessore ci sono comandi speciali e nel modello software del microprocessore ci sono registri speciali per questo:

1) esp/sp (registro del puntatore dello stack) - registro del puntatore dello stack. Contiene un puntatore all'inizio dello stack nel segmento dello stack corrente.

2) ebp/bp (registro del puntatore di base) - registro del puntatore di base dello stack frame. Progettato per organizzare l'accesso casuale ai dati all'interno dello stack.

L'uso dell'hard pinning dei registri per alcune istruzioni consente di codificare la loro rappresentazione della macchina in modo più compatto. Conoscere queste caratteristiche consentirà, se necessario, di risparmiare almeno alcuni byte di memoria occupata dal codice del programma.

5. Registri di settore

Ci sono sei registri di segmento nel modello software del microprocessore: cs, ss, ds, es, gs, fs.

La loro esistenza è dovuta alle specificità dell'organizzazione e dell'uso della RAM da parte dei microprocessori Intel. Sta nel fatto che l'hardware del microprocessore supporta l'organizzazione strutturale del programma sotto forma di tre parti, chiamate segmenti. Di conseguenza, una tale organizzazione della memoria è chiamata segmentata.

Per indicare i segmenti a cui il programma ha accesso in un determinato momento, si intendono i registri di segmento. Infatti (con una leggera correzione) questi registri contengono gli indirizzi di memoria da cui iniziano i segmenti corrispondenti. La logica di elaborazione di un'istruzione macchina è costruita in modo tale che gli indirizzi in registri di segmento ben definiti vengano utilizzati implicitamente durante il recupero di un'istruzione, l'accesso ai dati del programma o l'accesso allo stack.

Il microprocessore supporta i seguenti tipi di segmenti.

1. Segmento di codice. Contiene i comandi del programma. Per accedere a questo segmento, viene utilizzato il registro cs (registro del segmento di codice), il registro del codice del segmento. Contiene l'indirizzo del segmento di istruzione della macchina a cui ha accesso il microprocessore (ovvero, queste istruzioni vengono caricate nella pipeline del microprocessore).

2. Segmento di dati. Contiene i dati elaborati dal programma. Per accedere a questo segmento, viene utilizzato il registro ds (data segment register), un registro di dati di segmento che memorizza l'indirizzo del segmento di dati del programma corrente.

3. Segmento di impilamento. Questo segmento è una regione di memoria chiamata stack. Il microprocessore organizza il lavoro con lo stack secondo il seguente principio: viene selezionato per primo l'ultimo elemento scritto in quest'area. Per accedere a questo segmento viene utilizzato il registro ss (stack segment register), lo stack segment register contenente l'indirizzo del segmento stack.

4. Segmento dati aggiuntivo. Implicitamente, gli algoritmi per l'esecuzione della maggior parte delle istruzioni macchina presuppongono che i dati che elaborano si trovino nel segmento di dati, il cui indirizzo è nel registro del segmento ds. Se il programma non dispone di un segmento di dati sufficiente, ha l'opportunità di utilizzare altri tre segmenti di dati aggiuntivi. Ma a differenza del segmento di dati principale, il cui indirizzo è contenuto nel registro del segmento ds, quando si utilizzano segmenti di dati aggiuntivi, i loro indirizzi devono essere specificati in modo esplicito utilizzando speciali prefissi di ridefinizione del segmento nel comando. Gli indirizzi di segmenti di dati aggiuntivi devono essere contenuti nei registri es, gs, fs (extension data segment registers).

6. Registri di stato e di controllo

Il microprocessore include diversi registri che contengono costantemente informazioni sullo stato sia del microprocessore stesso che del programma le cui istruzioni sono attualmente caricate nella pipeline. Questi registri includono:

1) flag eflags/flags del registro;

2) registro puntatore comando eip/ip.

Utilizzando questi registri è possibile ottenere informazioni sui risultati dell'esecuzione del comando e influenzare lo stato del microprocessore stesso. Consideriamo più in dettaglio lo scopo e il contenuto di questi registri.

1. eflags/flags (registro flag) - registro flag. La profondità di bit di eflags/flags è 32/16 bit. I singoli bit di questo registro hanno uno scopo funzionale specifico e sono chiamati flag. La parte inferiore di questo registro è esattamente la stessa del registro flags per 18086. La Figura 3 mostra il contenuto del registro eflags.

Riso. 3. Il contenuto del registro eflags

A seconda di come vengono utilizzati, i flag del registro eflags/flags possono essere suddivisi in tre gruppi:

1) otto flag di stato.

Questi flag possono cambiare dopo che le istruzioni della macchina sono state eseguite. I flag di stato del registro eflags riflettono le specifiche del risultato dell'esecuzione di operazioni aritmetiche o logiche. Ciò rende possibile analizzare lo stato del processo di calcolo e rispondere ad esso utilizzando comandi di salto condizionale e chiamate di subroutine. La tabella 1 elenca i flag di stato e il loro scopo.

2) un flag di controllo.

Denotato df (flag della directory). Si trova nel bit 10 del registro eflags ed è usato da comandi concatenati. Il valore del flag df determina la direzione dell'elaborazione elemento per elemento in queste operazioni: dall'inizio della stringa alla fine (df = 0) o viceversa, dalla fine della stringa al suo inizio (df = 1). Esistono comandi speciali per lavorare con il flag df: eld (rimuove il flag df) e std (imposta il flag df). L'uso di questi comandi consente di regolare il flag df in base all'algoritmo e garantire che i contatori vengano automaticamente incrementati o decrementati durante l'esecuzione di operazioni sulle stringhe.

3) cinque flag di sistema.

Controllano l'I/O, gli interrupt mascherabili, il debug, il cambio di attività e la modalità virtuale 8086. Non è consigliabile che i programmi applicativi modifichino questi flag inutilmente, poiché ciò causerebbe l'arresto del programma nella maggior parte dei casi. La tabella 2 elenca i flag di sistema e il loro scopo.

Tabella 1. Flag di statoTabella 2. Flag di sistema

2. eip/ip (Instraction Pointer register) - registro del puntatore dell'istruzione. Il registro eip/ip è largo 32/16 bit e contiene l'offset dell'istruzione successiva da eseguire rispetto al contenuto del registro del segmento cs nel segmento dell'istruzione corrente. Questo registro non è direttamente accessibile al programmatore, ma il suo valore viene caricato e modificato da vari comandi di controllo, che includono comandi per salti condizionali e incondizionati, chiamate di procedure e ritorno dalle procedure. Il verificarsi di interrupt modifica anche il registro eip/ip.

LEZIONE N. 15. Registri

1. Registri di sistema a microprocessore

Il nome stesso di questi registri suggerisce che svolgono funzioni specifiche nel sistema. L'uso dei registri di sistema è rigorosamente regolamentato. Sono loro che forniscono la modalità protetta. Possono anche essere considerati come parte dell'architettura del microprocessore, che viene lasciata deliberatamente visibile in modo che un programmatore di sistema qualificato possa eseguire le operazioni di livello più basso.

I registri di sistema possono essere suddivisi in tre gruppi:

1) quattro registri di controllo;

2) quattro registri di indirizzi di sistema;

3) otto registri di debug.

2. Registri di controllo

Il gruppo di registri di controllo comprende quattro registri: cr0, cr1, cr2, cr3. Questi registri servono per il controllo generale del sistema. I registri di controllo sono disponibili solo per i programmi con livello di privilegio 0.

Sebbene il microprocessore abbia quattro registri di controllo, ne sono disponibili solo tre - è escluso cr1, le cui funzioni non sono ancora definite (è riservato per usi futuri).

Il registro cr0 contiene flag di sistema che controllano le modalità di funzionamento del microprocessore e ne riflettono lo stato a livello globale, indipendentemente dai compiti specifici eseguiti.

Scopo dei flag di sistema:

1) pe (Abilitazione protezione), bit 0 - abilita la modalità di funzionamento protetta. Lo stato di questo flag mostra in quale delle due modalità - reale (pe = 0) o protetta (pe = 1) - il microprocessore sta funzionando in un dato momento;

2) mp (Math Present), bit 1 - la presenza di un coprocessore. Sempre 1;

3) ts (Task Switched), bit 3 - cambio attività. Il processore imposta automaticamente questo bit quando passa a un'altra attività;

4) am (Maschera di allineamento), bit 18 - maschera di allineamento. Questo bit abilita (am = 1) o disabilita (am = 0) il controllo dell'allineamento;

5) cd (Cache Disable), bit 30 - disabilita la memoria cache.

Utilizzando questo bit è possibile disabilitare (cd =1) o abilitare (cd = 0) l'utilizzo della cache interna (la cache di primo livello);

6) pg (PaGing), bit 31 - abilita (pg =1) o disabilita (pg = 0) il paging.

Il flag viene utilizzato nel modello di paging dell'organizzazione della memoria.

Il registro cr2 viene utilizzato nella paginazione RAM per registrare la situazione in cui l'istruzione corrente ha avuto accesso all'indirizzo contenuto in una pagina di memoria che attualmente non è in memoria.

In una tale situazione, nel microprocessore si verifica un numero di eccezione 14 e l'indirizzo lineare a 32 bit dell'istruzione che ha causato questa eccezione viene scritto nel registro cr2. Con queste informazioni, il gestore di eccezioni 14 determina la pagina desiderata, la scambia in memoria e riprende il normale funzionamento del programma;

Il registro cr3 viene utilizzato anche per la memoria di paging. Questo è il cosiddetto registro delle directory delle pagine di primo livello. Contiene l'indirizzo di base fisico a 20 bit della directory della pagina dell'attività corrente. Questa directory contiene 1024 descrittori a 32 bit, ognuno dei quali contiene l'indirizzo della tabella delle pagine di secondo livello. A sua volta, ciascuna delle tabelle di pagina di secondo livello contiene 1024 descrittori a 32 bit che indirizzano i frame di pagina in memoria. La dimensione del frame della pagina è di 4 KB.

3. Registri degli indirizzi di sistema

Questi registri sono anche chiamati registri di gestione della memoria.

Sono progettati per proteggere programmi e dati nella modalità multitasking del microprocessore. Quando si opera in modalità protetta da microprocessore, lo spazio degli indirizzi è suddiviso in:

1) globale - comune a tutti i compiti;

2) locale: separato per ogni attività.

Questa separazione spiega la presenza dei seguenti registri di sistema nell'architettura del microprocessore:

1) il registro della tabella dei descrittori globali gdtr (Global Descriptor Table Register), avente una dimensione di 48 bit e contenente un indirizzo di base a 32 bit (bit 16-47) della tabella dei descrittori globali GDT e uno a 16 bit (bit 0-15) valore limite, che è la dimensione in byte della tabella GDT;

2) il registro della tabella dei descrittori locali ldtr (Local Descriptor Table Register), avente una dimensione di 16 bit e contenente il cosiddetto selettore del descrittore della tabella dei descrittori locali LDT Questo selettore è un puntatore nella tabella GDT, che descrive il segmento contenente la tabella dei descrittori locali LDT;

3) il registro della tabella dei descrittori di interrupt idtr (Interrupt Descriptor Table Register), avente una dimensione di 48 bit e contenente un indirizzo di base a 32 bit (bit 16-47) della tabella dei descrittori di interrupt IDT e uno a 16 bit (bit 0-15) valore limite, che è la dimensione in byte della tabella IDT;

4) Registro attività a 16 bit tr (Registro attività), che, come il registro ldtr, contiene un selettore, ovvero un puntatore a un descrittore nella tabella GDT.Questo descrittore descrive l'attuale stato del segmento di attività (TSS). Questo segmento viene creato per ogni attività nel sistema, ha una struttura rigorosamente regolamentata e contiene il contesto (stato corrente) dell'attività. Lo scopo principale dei segmenti TSS è salvare lo stato corrente di un'attività al momento del passaggio a un'altra attività.

4. ​​Registro di debug

Questo è un gruppo molto interessante di registri destinati al debug hardware. Gli strumenti di debug hardware sono apparsi per la prima volta nel microprocessore i486. Nell'hardware, il microprocessore contiene otto registri di debug, ma solo sei di essi vengono effettivamente utilizzati.

I registri dr0, dr1, dr2, dr3 hanno una larghezza di 32 bit e sono progettati per impostare gli indirizzi lineari di quattro breakpoint. Il meccanismo utilizzato in questo caso è il seguente: qualsiasi indirizzo generato dal programma corrente viene confrontato con gli indirizzi nei registri dr0... dr3 e, se c'è corrispondenza, viene generata un'eccezione di debug con il numero 1.

Il registro dr6 è chiamato registro dello stato di debug. I bit in questo registro vengono impostati in base ai motivi che hanno causato il verificarsi dell'ultima eccezione numero 1.

Elenchiamo questi bit e il loro scopo:

1) b0 - se questo bit è impostato a 1, si è verificata l'ultima eccezione (interruzione) a seguito del raggiungimento del checkpoint definito nel registro dr0;

2) b1 - simile a b0, ma per un checkpoint nel registro dr1;

3) b2 - simile a b0, ma per un checkpoint nel registro dr2;

4) bЗ - simile a b0, ma per un checkpoint nel registro dr3;

5) bd (bit 13) - serve a proteggere i registri di debug;

6) bs (bit 14) - impostato a 1 se l'eccezione 1 è stata causata dallo stato del flag tf = 1 nel registro eflags;

7) bt (bit 15) è impostato su 1 se l'eccezione 1 è stata causata da un passaggio a un'attività con il bit trap impostato in TSS t = 1.

Tutti gli altri bit in questo registro sono riempiti con zeri. Il gestore dell'eccezione 1, basato sul contenuto di dr6, deve determinare il motivo dell'eccezione e intraprendere le azioni necessarie.

Il registro dr7 è chiamato registro di controllo di debug. Contiene campi per ciascuno dei quattro registri del punto di interruzione di debug che consentono di specificare le seguenti condizioni in base alle quali deve essere generato un interrupt:

1) posizione di registrazione del checkpoint - solo nell'attività corrente o in qualsiasi attività. Questi bit occupano gli 8 bit inferiori del registro dr7 (2 bit per ogni punto di interruzione (in realtà un punto di interruzione) impostato dai registri dr0, dr1, dr2, dr3, rispettivamente).

Il primo bit di ogni coppia è la cosiddetta risoluzione locale; l'impostazione indica che il punto di interruzione ha effetto se si trova all'interno dello spazio degli indirizzi dell'attività corrente.

Il secondo bit in ogni coppia specifica l'autorizzazione globale, che indica che il punto di interruzione specificato è valido all'interno degli spazi degli indirizzi di tutte le attività nel sistema;

2) il tipo di accesso con cui viene avviato l'interrupt: solo durante il recupero di un comando, durante la scrittura o durante la scrittura/lettura di dati. I bit che determinano questa natura del verificarsi di un interrupt si trovano nella parte superiore di questo registro. La maggior parte dei registri di sistema sono accessibili a livello di codice.

LEZIONE N. 16. Programmi Assemblatori

1. La struttura del programma in assembler

Un programma in linguaggio assembly è una raccolta di blocchi di memoria chiamati segmenti di memoria. Un programma può essere costituito da uno o più di questi segmenti di blocco. Ogni segmento contiene una raccolta di frasi linguistiche, ognuna delle quali occupa una riga separata di codice del programma.

Le dichiarazioni di assemblaggio sono di quattro tipi:

1) comandi o istruzioni, che sono analoghi simbolici dei comandi della macchina. Durante il processo di traduzione, le istruzioni di montaggio vengono convertite nei comandi corrispondenti del set di istruzioni del microprocessore;

2) macro. Si tratta di frasi del testo del programma che vengono formalizzate in un certo modo e vengono sostituite da altre frasi durante la messa in onda;

3) le direttive, che sono un'indicazione al traduttore assembler per eseguire determinate azioni. Le direttive non hanno controparti nella rappresentazione della macchina;

4) righe di commento contenenti qualsiasi carattere, comprese le lettere dell'alfabeto russo. I commenti vengono ignorati dal traduttore.

2. Sintassi dell'Assemblea

Le frasi che compongono un programma possono essere un costrutto sintattico corrispondente a un comando, una macro, una direttiva o un commento. Affinché il traduttore assembler possa riconoscerli, devono essere formati secondo determinate regole sintattiche. Per fare ciò, è meglio utilizzare una descrizione formale della sintassi della lingua, come le regole della grammatica. I modi più comuni per descrivere un linguaggio di programmazione in questo modo sono i diagrammi di sintassi e le forme estese di Backus-Naur. Per un uso pratico, i diagrammi di sintassi sono più convenienti. Ad esempio, la sintassi delle istruzioni in linguaggio assembly può essere descritta utilizzando i diagrammi di sintassi mostrati nelle figure seguenti.

Riso. 4. Formato frase Assembler

Riso. 5. Formato della Direttiva

Riso. 6. Formato dei comandi e delle macro

Su questi disegni:

1) nome dell'etichetta - un identificatore, il cui valore è l'indirizzo del primo byte della frase del codice sorgente del programma che denota;

2) nome - un identificatore che distingue questa direttiva da altre direttive con lo stesso nome. A seguito dell'elaborazione da parte dell'assemblatore di una determinata direttiva, alcune caratteristiche possono essere assegnate a questo nome;

3) un codice operativo (COP) e una direttiva sono designazioni mnemoniche della corrispondente istruzione macchina, macroistruzione o direttiva del traduttore;

4) operandi - parti del comando, direttive macro o assembler, che denotano oggetti su cui vengono eseguite operazioni. Gli operandi Assembler sono descritti da espressioni con costanti numeriche e di testo, etichette di variabili e identificatori che utilizzano segni di operazione e alcune parole riservate.

Come utilizzare i diagrammi di sintassi? È molto semplice: tutto ciò che devi fare è trovare e quindi seguire il percorso dall'input del diagramma (a sinistra) al suo output (a destra). Se esiste un tale percorso, la frase o la costruzione è sintatticamente corretta. Se non esiste un tale percorso, il compilatore non accetterà questa costruzione. Quando si lavora con i diagrammi sintattici, prestare attenzione alla direzione di attraversamento indicata dalle frecce, in quanto tra i percorsi potrebbero esserci quelli che si possono seguire da destra a sinistra. In effetti, i diagrammi sintattici riflettono la logica del traduttore durante l'analisi delle frasi di input del programma.

I caratteri consentiti durante la scrittura del testo dei programmi sono:

1) tutte le lettere latine: A - Z, a - z. In questo caso, le lettere maiuscole e minuscole sono considerate equivalenti;

2) numeri da 0 a 9;

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

4) separatori.

Le frasi Assembler sono formate da lessemi, che sono sequenze sintatticamente inseparabili di simboli linguistici validi che hanno senso per il traduttore.

I token sono i seguenti.

1. Identificatori: sequenze di caratteri validi utilizzati per designare oggetti di programma come codici operazione, nomi di variabili e nomi di etichette. La regola per la scrittura degli identificatori è la seguente: un identificatore può essere composto da uno o più caratteri. Come caratteri, puoi usare lettere dell'alfabeto latino, numeri e alcuni caratteri speciali - _, ?, $, @. Un identificatore non può iniziare con un carattere cifra. La lunghezza dell'identificatore può essere fino a 255 caratteri, sebbene il traduttore accetti solo i primi 32 caratteri e ignori il resto. È possibile regolare la lunghezza dei possibili identificatori utilizzando l'opzione della riga di comando mv. Inoltre, è possibile dire al traduttore di distinguere tra lettere maiuscole e minuscole o ignorare la loro differenza (cosa che viene eseguita per impostazione predefinita). A tale scopo vengono utilizzate le opzioni della riga di comando /mu, /ml, /mx.

2. Catene di caratteri - sequenze di caratteri racchiuse tra virgolette singole o doppie.

3. Interi in uno dei seguenti sistemi numerici: binario, decimale, esadecimale. L'identificazione dei numeri durante la scrittura nei programmi assembler viene eseguita secondo determinate regole:

1) i numeri decimali non richiedono l'identificazione di caratteri aggiuntivi, ad esempio 25 o 139;

2) per identificare i numeri binari nel testo sorgente del programma, è necessario inserire la "b" latina dopo aver scritto gli zeri e gli uno che li compongono, ad esempio 10010101 b;

3) I numeri esadecimali hanno più convenzioni quando si scrive:

a) in primo luogo, sono costituiti dai numeri 0...9, lettere minuscole e maiuscole dell'alfabeto latino a, b, c, d, e, Gili D B, C, D, E, E

b) in secondo luogo, il traduttore potrebbe avere difficoltà a riconoscere i numeri esadecimali perché possono essere costituiti solo da cifre 0...9 (ad esempio 190845) o iniziare con una lettera dell'alfabeto latino (ad esempio efl5 ). Per “spiegare” al traduttore che un determinato token non è un numero decimale o un identificatore, il programmatore deve evidenziare in modo speciale il numero esadecimale. Per fare ciò, scrivi la lettera latina “h” alla fine della sequenza di cifre esadecimali che compongono un numero esadecimale. Questo è un must. Se un numero esadecimale inizia con una lettera, prima viene scritto uno zero iniziale: 0 efl5 h.

Quindi, abbiamo capito come sono costruite le frasi di un programma assembler. Ma questa è solo la visione più superficiale.

Quasi ogni frase contiene una descrizione dell'oggetto su cui o con l'aiuto del quale viene eseguita un'azione. Questi oggetti sono chiamati operandi. Possono essere definiti come segue: gli operandi sono oggetti (alcuni valori, registri o celle di memoria) che sono interessati da istruzioni o direttive, oppure sono oggetti che definiscono o perfezionano l'azione di istruzioni o direttive.

Gli operandi possono essere combinati con operatori aritmetici, logici, bit per bit e di attributo per calcolare un valore o determinare una posizione di memoria che sarà influenzata da un determinato comando o direttiva.

Consideriamo più in dettaglio le caratteristiche degli operandi nella seguente classificazione:

1) operandi costanti o immediati: un numero, una stringa, un nome o un'espressione che ha un valore fisso. Il nome non deve essere rilocabile, cioè non deve dipendere dall'indirizzo del programma da caricare in memoria. Ad esempio, può essere definito con gli operatori uguale o =;

2) operandi indirizzo, specificano la posizione fisica dell'operando in memoria specificando due componenti dell'indirizzo: segmento e offset (Fig. 7);

Riso. 7. Sintassi di descrizione degli operandi di indirizzo

3) operandi rilocabili - qualsiasi nome simbolico che rappresenta alcuni indirizzi di memoria. Questi indirizzi possono indicare la posizione in memoria di alcune istruzioni (se l'operando è un'etichetta) o dati (se l'operando è il nome di una posizione di memoria nel segmento dati).

Gli operandi rilocabili differiscono dagli operandi di indirizzo in quanto non sono legati a uno specifico indirizzo di memoria fisica. Il componente del segmento dell'indirizzo dell'operando spostato è sconosciuto e verrà determinato dopo che il programma è stato caricato in memoria per l'esecuzione.

Il contatore di indirizzi è un tipo specifico di operando. È indicato dal segno S. La specificità di questo operando è che quando il traduttore assembler incontra questo simbolo nel programma sorgente, sostituisce invece il valore corrente del contatore di indirizzi. Il valore del contatore di indirizzi, o contatore di posizionamento, come viene talvolta chiamato, è l'offset dell'istruzione macchina corrente dall'inizio del segmento di codice. Nel formato dell'elenco, la seconda o la terza colonna corrisponde al contatore degli indirizzi (a seconda che la colonna con il livello di annidamento sia presente o meno nell'elenco). Se prendiamo un elenco come esempio, si può vedere che quando il traduttore elabora la successiva istruzione assembler, il contatore di indirizzi aumenta della lunghezza dell'istruzione macchina generata. È importante comprendere correttamente questo punto. Ad esempio, l'elaborazione delle direttive assembler non cambia il contatore. Le direttive, a differenza dei comandi assembler, sono solo istruzioni al compilatore per eseguire determinate azioni per formare la rappresentazione macchina del programma, e per esse il compilatore non genera alcuna struttura in memoria.

Quando si utilizza tale espressione per saltare, prestare attenzione alla lunghezza dell'istruzione stessa in cui viene utilizzata questa espressione, poiché il valore del contatore di indirizzi corrisponde all'offset nel segmento di istruzione di questa istruzione e non all'istruzione che la segue . Nel nostro esempio, il comando jmp richiede 2 byte. Ma attenzione, la lunghezza di un'istruzione dipende dagli operandi che utilizza. Un'istruzione con operandi di registro sarà più breve di un'istruzione con uno dei suoi operandi in memoria. Nella maggior parte dei casi, queste informazioni possono essere ottenute conoscendo il formato dell'istruzione macchina e analizzando la colonna dell'elenco con il codice oggetto dell'istruzione;

4) register operando è solo un nome di registro. In un programma assembler, puoi usare i nomi di tutti i registri di uso generale e della maggior parte dei registri di sistema;

5) operandi di base e di indice. Questo tipo di operando viene utilizzato per implementare l'indirizzamento indiretto di base, indice indiretto o loro combinazioni ed estensioni;

6) Gli operandi strutturali vengono utilizzati per accedere a un elemento specifico di un tipo di dati complesso chiamato struttura.

I record (simili a un tipo struct) vengono utilizzati per accedere a un campo di bit di un record.

Gli operandi sono componenti elementari che fanno parte dell'istruzione macchina, denotando gli oggetti su cui viene eseguita l'operazione. In un caso più generale, gli operandi possono essere inclusi come componenti in formazioni più complesse chiamate espressioni. Le espressioni sono combinazioni di operandi e operatori, considerati nel loro insieme. Il risultato della valutazione dell'espressione può essere l'indirizzo di una cella di memoria o un valore costante (assoluto).

Abbiamo già considerato i possibili tipi di operandi. Elenchiamo ora i possibili tipi di operatori assembler e le regole sintattiche per la formazione di espressioni assembler e diamo una breve descrizione degli operatori.

1. Operatori aritmetici. Questi includono:

1) "+" e "-" unari;

2) binari "+" e "-";

3) moltiplicazione "*";

4) divisione intera "/";

5) ricavare il resto dalla divisione "mod".

Questi operatori si trovano ai livelli di precedenza 6,7,8 nella Tabella 4.

Riso. 8. Sintassi delle operazioni aritmetiche

2. Gli operatori Shift spostano l'espressione del numero di bit specificato (Fig. 9).

Riso. 9. Sintassi degli operatori di turno

3. Gli operatori di confronto (restituiscono il valore "true" o "false") sono destinati alla formazione di espressioni logiche (Fig. 10 e Tabella 3). Il valore logico "true" corrisponde a un'unità digitale e "false" - a zero.

Riso. 10. Sintassi degli operatori di confronto

Tabella 3. Operatori di confronto

4. Gli operatori logici eseguono operazioni bit per bit sulle espressioni (Fig. 11). Le espressioni devono essere assolute, cioè tali, il cui valore numerico può essere calcolato dal traduttore.

Riso. 11. Sintassi degli operatori logici

5. Operatore indice []. Anche le parentesi sono un operatore e il traduttore percepisce la loro presenza come un'istruzione per aggiungere il valore di espressione_1 dietro queste parentesi con espressione_2 racchiusa tra parentesi (Fig. 12).

Riso. 12. Sintassi dell'operatore di indice

Si noti che nella letteratura sull'assembler viene adottata la seguente designazione: quando il testo fa riferimento al contenuto di un registro, il suo nome viene preso tra parentesi. Ci atterremo anche a questa notazione.

6. L'operatore di ridefinizione del tipo ptr viene utilizzato per ridefinire o qualificare il tipo di etichetta o variabile definita da un'espressione (Fig. 13).

Il tipo può assumere uno dei seguenti valori: byte, word, dword, qword, tbyte, near, far.

Riso. 13. Sintassi dell'operatore di ridefinizione del tipo

7. L'operatore di ridefinizione del segmento ":" (due punti) forza il calcolo di un indirizzo fisico relativo ad una specifica componente del segmento: "segment register name", "segment name" dalla corrispondente direttiva SEGMENT, o "group name" (Fig. 14). Quando abbiamo discusso della segmentazione, abbiamo parlato del fatto che il microprocessore a livello hardware supporta tre tipi di segmenti: codice, stack e dati. Che cos'è questo supporto hardware? Ad esempio, per selezionare l'esecuzione del comando successivo, il microprocessore deve necessariamente guardare il contenuto del registro di segmento cs e solo esso. E questo registro, come sappiamo, contiene l'indirizzo fisico (non ancora spostato) dell'inizio del segmento di istruzione. Per ottenere l'indirizzo di una particolare istruzione, il microprocessore deve moltiplicare il contenuto di cs per 16 (che significa uno spostamento di quattro bit) e aggiungere il valore risultante a 20 bit al contenuto a 16 bit del registro ip. Approssimativamente la stessa cosa accade quando il microprocessore elabora gli operandi nell'istruzione macchina. Se vede che l'operando è un indirizzo (un indirizzo effettivo che è solo una parte dell'indirizzo fisico), allora sa in quale segmento cercarlo - per impostazione predefinita, è il segmento il cui indirizzo iniziale è memorizzato nel registro di segmento ds .

Ma per quanto riguarda il segmento dello stack? Nel contesto della nostra considerazione, siamo interessati ai registri sp e bp. Se il microprocessore vede uno di questi registri come un operando (o parte di esso, se l'operando è un'espressione), per impostazione predefinita forma l'indirizzo fisico dell'operando, utilizzando il contenuto del registro ss come componente del segmento. Questo è un insieme di microprogrammi nell'unità di controllo del microprogramma, ognuno dei quali esegue una delle istruzioni nel sistema di istruzioni della macchina a microprocessore. Ogni microprogramma funziona secondo il proprio algoritmo. Ovviamente non puoi cambiarlo, ma puoi correggerlo leggermente. Questa operazione viene eseguita utilizzando il campo opzionale del prefisso del comando della macchina. Se siamo d'accordo su come funziona il comando, allora questo campo è mancante. Se vogliamo apportare una modifica (se, ovviamente, è consentito per un particolare comando) all'algoritmo del comando, è necessario formare il prefisso appropriato.

Un prefisso è un valore di un byte il cui valore numerico ne determina lo scopo. Il microprocessore riconosce dal valore specificato che questo byte è un prefisso e l'ulteriore lavoro del microprogramma viene eseguito tenendo conto dell'istruzione ricevuta per correggere il suo lavoro. Ora siamo interessati a uno di questi: il prefisso di sostituzione del segmento (ridefinizione). Il suo scopo è indicare al microprocessore (e di fatto al firmware) che non vogliamo utilizzare il segmento predefinito. Le possibilità di tale ridefinizione sono, ovviamente, limitate. Il segmento del comando non può essere ridefinito, l'indirizzo del prossimo comando eseguibile è determinato in modo univoco dalla coppia cs: ip. E qui segmenti di uno stack e dati: è possibile. Ecco a cosa serve l'operatore ":". Il traduttore assembler, elaborando questa istruzione, genera il prefisso di sostituzione del segmento di un byte corrispondente.

Riso. 14. Sintassi dell'operatore di ridefinizione del segmento

8. L'operatore di denominazione del tipo di struttura "."(punto) impone inoltre al compilatore di eseguire determinati calcoli se si verifica in un'espressione.

9. L'operatore per ottenere la componente del segmento dell'indirizzo dell'espressione seg restituisce l'indirizzo fisico del segmento per l'espressione (Fig. 15), che può essere un'etichetta, una variabile, un nome di segmento, un nome di gruppo o un nome simbolico .

Riso. 15. Sintassi dell'operatore ricevente della componente di segmento

10. L'operatore per ottenere l'offset dell'espressione offset consente di ottenere il valore dell'offset dell'espressione (Fig. 16) in byte relativi all'inizio del segmento in cui è definita l'espressione.

Riso. 16. Sintassi dell'operatore get offset

Come nei linguaggi di alto livello, l'esecuzione degli operatori assembler durante la valutazione delle espressioni viene eseguita in base alle loro priorità (Tabella 4). Le operazioni con la stessa priorità vengono eseguite in sequenza da sinistra a destra. È possibile modificare l'ordine di esecuzione inserendo le parentesi che hanno la precedenza più alta.

Tabella 4. Operatori e loro precedenza

3. Direttive di segmentazione

Nel corso della discussione precedente, abbiamo scoperto tutte le regole di base per scrivere istruzioni e operandi in un programma in linguaggio assembly. Rimane aperta la questione di come formattare correttamente la sequenza di comandi in modo che il traduttore possa elaborarli e il microprocessore possa eseguirli.

Considerando l'architettura del microprocessore, abbiamo appreso che ha sei registri di segmento, attraverso i quali può funzionare contemporaneamente:

1) con un segmento di codice;

2) con un segmento di pila;

3) con un segmento di dati;

4) con tre segmenti di dati aggiuntivi.

Ricordiamo ancora una volta che un segmento è fisicamente un'area di memoria occupata da comandi e (o) dati i cui indirizzi sono calcolati rispetto al valore nel registro di segmento corrispondente.

La descrizione sintattica di un segmento in assembler è la costruzione mostrata in Figura 17:

Riso. 17. Sintassi della descrizione del segmento

È importante notare che la funzionalità di un segmento è in qualche modo più ampia della semplice suddivisione del programma in blocchi di codice, dati e stack. La segmentazione fa parte di un meccanismo più generale legato al concetto di programmazione modulare. Implica l'unificazione della progettazione dei moduli oggetto creati dal compilatore, compresi quelli provenienti da diversi linguaggi di programmazione. Ciò consente di combinare programmi scritti in diverse lingue. È per l'implementazione di varie opzioni per tale unione che sono destinati gli operandi nella direttiva SEGMENT.

Consideriamoli in modo più dettagliato.

1. L'attributo di allineamento del segmento (tipo di allineamento) indica al linker di assicurarsi che l'inizio del segmento sia posizionato sul limite specificato. Questo è importante perché un corretto allineamento rende più veloce l'accesso ai dati sui processori i80x86. I valori validi per questo attributo sono i seguenti:

1) BYTE - l'allineamento non viene eseguito. Un segmento può iniziare a qualsiasi indirizzo di memoria;

2) WORD - il segmento parte da un indirizzo multiplo di due, ovvero l'ultimo bit (meno significativo) dell'indirizzo fisico è 0 (allineato al limite della parola);

3) DWORD - il segmento parte da un indirizzo multiplo di quattro, ovvero gli ultimi due bit (meno significativi) sono 0 (allineamento del confine della doppia parola);

4) PARA - il segmento inizia da un indirizzo multiplo di 16, ovvero l'ultima cifra esadecimale dell'indirizzo deve essere Oh (allineamento al limite del paragrafo);

5) PAGINA - il segmento inizia con un indirizzo multiplo di 256, ovvero le ultime due cifre esadecimali devono essere 00h (allineate al limite di una pagina di 256 byte);

6) MAMPAGE - il segmento inizia con un indirizzo multiplo di 4 KB, ovvero le ultime tre cifre esadecimali devono essere OOOh (indirizzo della successiva pagina di memoria da 4 KB). Il tipo di allineamento predefinito è PARA.

2. L'attributo combina segmento (tipo combinatorio) indica al linker come combinare segmenti di moduli diversi che hanno lo stesso nome. I valori degli attributi della combinazione di segmenti possono essere:

1) PRIVATO - il segmento non sarà unito ad altri segmenti con lo stesso nome al di fuori di questo modulo;

2) PUBLIC - fa in modo che il linker colleghi tutti i segmenti con lo stesso nome. Il nuovo segmento fuso sarà intero e continuo. Tutti gli indirizzi (offset) degli oggetti, e questo può dipendere dal tipo di comando e dal segmento dati, verranno calcolati rispetto all'inizio di questo nuovo segmento;

3) COMUNE - posiziona tutti i segmenti con lo stesso nome allo stesso indirizzo. Tutti i segmenti con il nome dato si sovrapporranno e condivideranno la memoria. La dimensione del segmento risultante sarà uguale alla dimensione del segmento più grande;

4) AT xxxx - individua il segmento all'indirizzo assoluto del paragrafo (il paragrafo è la quantità di memoria, un multiplo di 16, quindi l'ultima cifra esadecimale dell'indirizzo del paragrafo è 0). L'indirizzo assoluto di un paragrafo è dato da xxx. Il linker posiziona il segmento ad un determinato indirizzo di memoria (questo può essere utilizzato, ad esempio, per accedere alla memoria video o ad un'area ROM>), tenendo conto dell'attributo di combinazione. Fisicamente ciò significa che il segmento, una volta caricato in memoria, si troverà a partire da questo indirizzo assoluto del paragrafo, ma per accedervi è necessario caricare il valore specificato nell'attributo nel registro di segmento corrispondente. Tutte le etichette e gli indirizzi in un segmento così definito sono relativi all'indirizzo assoluto dato;

5) STACK - definizione di un segmento di stack. Fa sì che il linker connetta tutti i segmenti con lo stesso nome e calcoli gli indirizzi in questi segmenti relativi al registro ss. Il tipo combinato STACK (stack) è simile al tipo combinato PUBLIC, tranne per il fatto che il registro ss è il registro del segmento standard per i segmenti dello stack. Il registro sp è impostato alla fine del segmento di stack concatenato. Se non viene specificato alcun segmento dello stack, il linker emetterà un avviso che non è stato trovato alcun segmento dello stack. Se viene creato un segmento di stack e non viene utilizzato il tipo STACK combinato, il programmatore deve caricare esplicitamente l'indirizzo del segmento nel registro ss (simile al registro ds).

L'attributo di combinazione predefinito è PRIVATO.

3. Un attributo di classe di segmento (tipo di classe) è una stringa tra virgolette che aiuta il linker a determinare l'ordine di segmento appropriato quando si assembla un programma da più segmenti di modulo. Il linker combina tutti i segmenti con lo stesso nome di classe insieme in memoria (il nome della classe può generalmente essere qualsiasi cosa, ma è meglio se riflette la funzionalità del segmento). Un uso tipico del nome di una classe consiste nel raggruppare insieme tutti i segmenti di codice di un programma (di solito per questo viene utilizzata la classe "codice"). Utilizzando il meccanismo di tipizzazione della classe, puoi anche raggruppare segmenti di dati inizializzati e non inizializzati.

4. Attributo della dimensione del segmento. Per i processori i80386 e superiori, i segmenti possono essere a 16 o 32 bit. Ciò influisce principalmente sulla dimensione del segmento e sull'ordine in cui l'indirizzo fisico è formato al suo interno. L'attributo può assumere i seguenti valori:

1) USE16 - significa che il segmento consente l'indirizzamento a 16 bit. Quando si forma un indirizzo fisico, è possibile utilizzare solo un offset di 16 bit. Di conseguenza, tale segmento può contenere fino a 64 KB di codice o dati;

2)USE32 - il segmento sarà a 32 bit. Quando si forma un indirizzo fisico, è possibile utilizzare un offset a 32 bit. Pertanto, tale segmento può contenere fino a 4 GB di codice o dati.

Tutti i segmenti sono di per sé uguali, poiché le direttive SEGMENT e ENDS non contengono informazioni sullo scopo funzionale dei segmenti. Per utilizzarli come segmenti di codice, dati o stack, devi prima informarne il traduttore, per il quale viene utilizzata una apposita direttiva ASSUME, che ha il formato mostrato in Fig. 18. Questa direttiva dice al traduttore quale segmento è legato a quale registro di segmento. A sua volta, ciò consentirà al traduttore di legare correttamente i nomi simbolici definiti nei segmenti. L'associazione dei segmenti ai registri dei segmenti viene eseguita utilizzando gli operandi di questa direttiva, in cui il nome_segmento deve essere il nome del segmento definito nel codice sorgente del programma dalla direttiva SEGMENTO o la parola chiave nulla. Se viene utilizzata solo la parola chiave nulla come operando, le precedenti assegnazioni dei registri di segmento vengono annullate e per tutti e sei i registri di segmento contemporaneamente. Ma la parola chiave nulla può essere usata al posto dell'argomento del nome del segmento; in questo caso, verrà interrotta selettivamente la connessione tra il segmento con il nome del segmento nome e il corrispondente registro di segmento (vedi Fig. 18).

Riso. 18. Direttiva ASSUME

Per programmi semplici contenenti un segmento per codice, dati e stack, vorremmo semplificarne la descrizione. Per fare ciò, i traduttori MASM e TASM hanno introdotto la possibilità di utilizzare direttive di segmentazione semplificate. Ma qui è sorto un problema relativo al fatto che era necessario compensare in qualche modo l'incapacità di controllare direttamente il posizionamento e la combinazione dei segmenti. Per fare ciò, insieme alle direttive di segmentazione semplificata, hanno iniziato a utilizzare la direttiva per la specifica del modello di memoria MODEL, che ha iniziato in parte a controllare il posizionamento dei segmenti e a svolgere le funzioni della direttiva ASSUME (quindi, quando si utilizzano le direttive di segmentazione semplificata, il La direttiva ASSUME può essere omessa). Questa direttiva associa i segmenti, che nel caso di utilizzo di direttive di segmentazione semplificate, hanno nomi predefiniti, con registri di segmento (sebbene sia comunque necessario inizializzare in modo esplicito ds).

La sintassi della direttiva MODEL è mostrata in Figura 19.

Riso. 19. Sintassi della direttiva MODELLO

Il parametro obbligatorio della direttiva MODEL è il modello di memoria. Questo parametro definisce il modello di segmentazione della memoria per la POU. Si presume che un modulo di programma possa avere solo determinati tipi di segmenti, che sono definiti dalle direttive di descrizione del segmento semplificata che abbiamo menzionato in precedenza. Queste direttive sono mostrate nella tabella 5.

Tabella 5. Direttive di definizione dei segmenti semplificate

La presenza del parametro [name] in alcune direttive indica che è possibile definire più segmenti di questo tipo. D'altra parte, l'esistenza di diversi tipi di segmenti di dati è dovuta all'esigenza di garantire la compatibilità con alcuni compilatori di linguaggi di alto livello, che creano segmenti di dati diversi per dati inizializzati e non inizializzati, nonché costanti.

Quando si utilizza la direttiva MODEL, il compilatore mette a disposizione diversi identificatori a cui è possibile accedere durante il funzionamento del programma per ottenere informazioni su determinate caratteristiche di un dato modello di memoria (Tabella 7). Elenchiamo questi identificatori e i loro valori (Tabella 6).

Tabella 6. Identificatori creati dalla direttiva MODEL

Possiamo ora completare la nostra discussione sulla direttiva MODEL. Gli operandi della direttiva MODEL vengono utilizzati per specificare un modello di memoria che definisce l'insieme dei segmenti di programma, le dimensioni dei segmenti di dati e di codice e il metodo di collegamento dei segmenti e dei registri di segmento. La tabella 7 riporta alcuni valori del parametro “modello di memoria” della direttiva MODEL.

Tabella 7. Modelli di memoria

Il parametro "modifier" della direttiva MODEL consente di specificare alcune caratteristiche dell'utilizzo del modello di memoria selezionato (Tabella 8).

Tabella 8. Modificatori del modello di memoria

I parametri facoltativi "lingua" e "modificatore di lingua" definiscono alcune caratteristiche delle chiamate di procedura. La necessità di utilizzare questi parametri sorge quando si scrivono e si collegano programmi in vari linguaggi di programmazione.

Le direttive di segmentazione standard e semplificata che abbiamo descritto non si escludono a vicenda. Le direttive standard vengono utilizzate quando il programmatore desidera avere il controllo completo sul posizionamento dei segmenti in memoria e sulla loro combinazione con segmenti di altri moduli.

Le direttive semplificate sono utili per programmi semplici e programmi destinati al collegamento con moduli di programma scritti in linguaggi di alto livello. Ciò consente al linker di collegare in modo efficiente moduli di lingue diverse standardizzando il collegamento e la gestione.

LEZIONE N. 17. Strutture di comando in Assembler

1. Struttura delle istruzioni della macchina

Un comando macchina è un'indicazione al microprocessore, codificato secondo determinate regole, di eseguire qualche operazione o azione. Ogni comando contiene elementi che definiscono:

1) cosa fare? (La risposta a questa domanda è data dall'elemento di comando chiamato codice operazione (COP).);

2) oggetti su cui è necessario fare qualcosa (questi elementi sono chiamati operandi);

3) come fare? (Questi elementi sono chiamati tipi di operandi, di solito specificati in modo implicito.)

Il formato delle istruzioni macchina mostrato nella Figura 20 è il più generale. La lunghezza massima di un'istruzione macchina è 15 byte. Un comando reale può contenere un numero molto più piccolo di campi, fino a uno - solo KOP.

Riso. 20. Formato istruzione macchina

Descriviamo lo scopo dei campi di istruzioni della macchina.

1. Prefissi.

Elementi opzionali dell'istruzione macchina, ciascuno dei quali è di 1 byte o può essere omesso. In memoria, i prefissi precedono il comando. Lo scopo dei prefissi è modificare l'operazione eseguita dal comando. Un'applicazione può utilizzare i seguenti tipi di prefissi:

1) prefisso di sostituzione del segmento. Specifica in modo esplicito quale registro di segmento viene utilizzato in questa istruzione per indirizzare lo stack oi dati. Il prefisso sovrascrive la selezione del registro di segmento predefinito. I prefissi di sostituzione del segmento hanno i seguenti significati:

a) 2eh - sostituzione del segmento c;

b) 36h - sostituzione del segmento ss;

c) 3eh - sostituzione del segmento ds;

d) 26h - sostituzione dei segmenti es;

e) 64h - sostituzione del segmento fs;

e) 65h - sostituzione del segmento gs;

2) il prefisso di bit dell'indirizzo specifica il bit dell'indirizzo (32 o 16 bit). A ciascuna istruzione che utilizza un operando di indirizzo viene assegnata la larghezza di bit dell'indirizzo di tale operando. Questo indirizzo può essere di 16 o 32 bit. Se la larghezza dell'indirizzo per questo comando è 16 bit, significa che il comando contiene un offset di 16 bit (Fig. 20), corrisponde a un offset di 16 bit dell'operando di indirizzo rispetto all'inizio di un segmento. Nel contesto della Figura 21, questo offset è chiamato indirizzo effettivo. Se l'indirizzo è a 32 bit, significa che il comando contiene un offset di 32 bit (Fig. 20), corrisponde a un offset di 32 bit dell'operando di indirizzo relativo all'inizio del segmento e il suo valore forma un 32 -bit offset nel segmento. Il prefisso del bit dell'indirizzo può essere utilizzato per modificare il bitness dell'indirizzo predefinito. Questa modifica riguarderà solo il comando preceduto dal prefisso;

Riso. 21. Il meccanismo di formazione di un indirizzo fisico in modalità reale

3) Il prefisso della larghezza del bit dell'operando è simile al prefisso della larghezza del bit dell'indirizzo, ma indica la lunghezza del bit dell'operando (32 bit o 16 bit) con cui funziona l'istruzione. Quali sono le regole per impostare l'indirizzo e gli attributi di larghezza di bit dell'operando per impostazione predefinita?

In modalità reale e modalità virtuale 18086, i valori di questi attributi sono 16 bit. In modalità protetta, i valori degli attributi dipendono dallo stato del bit D nei descrittori di segmento eseguibili. Se D = 0, i valori degli attributi predefiniti sono 16 bit; se D = 1, allora 32 bit.

Valori di prefisso per larghezza operando 66h e larghezza indirizzo 67h. Con il prefisso di bit dell'indirizzo in modalità reale, puoi utilizzare l'indirizzamento a 32 bit, ma tieni presente il limite di dimensione del segmento di 64 KB. Analogamente al prefisso di larghezza dell'indirizzo, è possibile utilizzare il prefisso di larghezza dell'operando in modalità reale per lavorare con operandi a 32 bit (ad esempio, nelle istruzioni aritmetiche);

4) il prefisso di ripetizione viene utilizzato con i comandi a catena (comandi di elaborazione della riga). Questo prefisso "fa il loop" del comando per elaborare tutti gli elementi della catena. Il sistema di comando supporta due tipi di prefissi:

a) incondizionato (rep - OOh), costringendo a ripetere il comando concatenato un certo numero di volte;

b) condizionale (repe/repz - OOh, repne/repnz - 0f2h), che, durante il loop, controlla alcuni flag e, come risultato del controllo, è possibile l'uscita anticipata dal loop.

2. Codice operazione.

Elemento obbligatorio che descrive l'operazione eseguita dal comando. Molti comandi corrispondono a diversi codici operativi, ognuno dei quali determina le sfumature dell'operazione. I campi successivi dell'istruzione macchina determinano la posizione degli operandi coinvolti nell'operazione e le specifiche del loro utilizzo. La considerazione di questi campi è connessa con le modalità di specificazione degli operandi in un'istruzione macchina e pertanto verrà eseguita in seguito.

3. Modalità di indirizzamento byte modr/m.

Il valore di questo byte determina il modulo di indirizzo dell'operando utilizzato. Gli operandi possono essere in memoria in uno o due registri. Se l'operando è in memoria, il byte modr/m specifica i componenti (registri offset, base e indice) utilizzati per calcolarne l'indirizzo effettivo (Figura 21). In modalità protetta, il byte sib (Scale-Index-Base) può essere utilizzato anche per determinare la posizione dell'operando in memoria. Il byte modr/m è composto da tre campi (Fig. 20):

1) il campo mod determina il numero di byte occupati dall'indirizzo dell'operando nel comando (Fig. 20, il campo offset nel comando). Il campo mod viene utilizzato insieme al campo r/m, che specifica come modificare l'indirizzo dell'operando "offset istruzione". Ad esempio, se mod = 00, significa che non è presente alcun campo di offset nel comando e l'indirizzo dell'operando è determinato dal contenuto del registro di base e (o) dell'indice. Quali registri verranno utilizzati per calcolare l'indirizzo effettivo è determinato dal valore di questo byte. Se mod = 01 significa che il campo offset è presente nel comando, occupa 1 byte ed è modificato dal contenuto del registro base e (o) indice. Se mod = 10 significa che il campo offset è presente nel comando, occupa 2 o 4 byte (a seconda della dimensione dell'indirizzo predefinito o prefissato), ed è modificato dal contenuto del registro di base e/o indice. Se mod = 11 significa che non ci sono operandi in memoria: sono nei registri. Lo stesso valore del byte mod viene utilizzato quando nell'istruzione viene utilizzato un operando immediato;

2) il campo reg/cop determina o il registro che si trova nel comando al posto del primo operando, oppure un'eventuale estensione dell'opcode;

3) il campo r/m viene utilizzato insieme al campo mod e determina o il registro che si trova nel comando al posto del primo operando (se mod = 11), oppure i registri di base e indice utilizzati per calcolare l'indirizzo effettivo (insieme al campo offset nel comando).

4. Scala byte - indice - base (byte sib).

Utilizzato per espandere le possibilità di indirizzare gli operandi. La presenza del byte sib in un'istruzione macchina è indicata dalla combinazione di uno dei valori 01 o 10 del campo mod e il valore del campo r/m = 100. Il byte sib è composto da tre campi:

1) scala campi ss. Questo campo contiene il fattore di scala per l'indice del componente dell'indice, che occupa i 3 bit successivi del byte sib. Il campo ss può contenere uno dei seguenti valori: 1, 2, 4, 8.

Nel calcolo dell'indirizzo effettivo, il contenuto del registro indice sarà moltiplicato per questo valore;

2) campi indice. Utilizzato per memorizzare il numero di registro dell'indice utilizzato per calcolare l'indirizzo effettivo dell'operando;

3) campi base. Utilizzato per memorizzare il numero di registro di base, utilizzato anche per calcolare l'indirizzo effettivo dell'operando. Quasi tutti i registri di uso generale possono essere utilizzati come registri di base e di indice.

5. Campo di offset al comando.

Un intero con segno a 8, 16 o 32 bit che rappresenta, in tutto o in parte (fatte salve le considerazioni precedenti), il valore dell'indirizzo effettivo dell'operando.

6. Il campo dell'operando immediato.

Un campo facoltativo che è un operando immediato a 8 bit, 16 bit o 32 bit. La presenza di questo campo si riflette, ovviamente, nel valore del byte modr/m.

2. Metodi per specificare gli operandi di istruzione

L'operando è impostato implicitamente a livello di firmware

In questo caso, l'istruzione non contiene esplicitamente operandi. L'algoritmo di esecuzione dei comandi utilizza alcuni oggetti predefiniti (registri, flag in eflags, ecc.).

Ad esempio, i comandi cli e sti funzionano implicitamente con il flag if interrupt nel registro eflags e il comando xlat accede implicitamente al registro al e a una riga in memoria all'indirizzo specificato dalla coppia di registri ds:bx.

L'operando è specificato nell'istruzione stessa (operando immediato)

L'operando è nel codice dell'istruzione, cioè ne fa parte. Per memorizzare un tale operando in un comando, viene allocato un campo lungo fino a 32 bit (Figura 20). L'operando immediato può essere solo il secondo operando (sorgente). L'operando di destinazione può essere in memoria o in un registro.

Ad esempio: mov ax,0ffffti sposta la costante esadecimale ffff nel registro ax. Il comando add sum, 2 somma il contenuto del campo all'indirizzo sum con l'intero 2 e scrive il risultato al posto del primo operando, cioè in memoria.

L'operando è in uno dei registri

Gli operandi dei registri sono specificati dai nomi dei registri. I registri possono essere utilizzati:

1) Registri a 32 bit EAX, EBX, ECX, EDX, ESI, EDI, ESP, EUR;

2) registri a 16 bit AX, BX, CX, DX, SI, DI, SP, BP;

3) registri a 8 bit AH, AL, BH, BL, CH, CL, DH, DL;

4) registri di segmento CS, DS, SS, ES, FS, GS.

Ad esempio, l'istruzione add ax,bx aggiunge il contenuto dei registri ax e bx e scrive il risultato in bx. Il comando dec si decrementa il contenuto di si di 1.

L'operando è in memoria

Questo è il modo più complesso e allo stesso tempo più flessibile per specificare gli operandi. Consente di implementare i seguenti due principali tipi di indirizzamento: diretto e indiretto.

A sua volta, l'indirizzamento indiretto ha le seguenti varietà:

1) indirizzamento di base indiretto; l'altro nome è l'indirizzo indiretto del registro;

2) indirizzamento di base indiretto con offset;

3) indirizzamento indiretto di indice con offset;

4) indirizzamento indiretto dell'indice di base;

5) indirizzamento indiretto dell'indice di base con offset.

L'operando è una porta I/O

Oltre allo spazio degli indirizzi della RAM, il microprocessore mantiene uno spazio degli indirizzi I/O, che viene utilizzato per accedere ai dispositivi I/O. Lo spazio degli indirizzi di I/O è 64 KB. Gli indirizzi vengono allocati per qualsiasi dispositivo del computer in questo spazio. Un particolare valore di indirizzo all'interno di questo spazio è chiamato porta I/O. Fisicamente, la porta I/O corrisponde ad un registro hardware (da non confondere con un registro a microprocessore), al quale si accede tramite apposite istruzioni assembler in entrata e in uscita.

Per esempio:

al,60h; inserire un byte dalla porta 60h

I registri indirizzati da una porta I/O possono avere una larghezza di 8,16, 32 o XNUMX bit, ma la larghezza del bit del registro è fissa per una determinata porta. I comandi in e out operano su un intervallo fisso di oggetti. I cosiddetti registri accumulatori EAX, AX, AL vengono utilizzati come fonte di informazioni o come destinatario. La scelta del registro è determinata dal bit della porta. Il numero di porta può essere specificato come operando immediato nelle istruzioni in e out o come valore nel registro DX. L'ultimo metodo consente di determinare dinamicamente il numero di porta nel programma.

L'operando è nello stack

Le istruzioni possono non avere alcun operandi, possono avere uno o due operandi. La maggior parte delle istruzioni richiede due operandi, uno dei quali è l'operando di origine e l'altro è l'operando di destinazione. È importante che un operando possa trovarsi in un registro o in una memoria e il secondo operando deve trovarsi in un registro o direttamente nell'istruzione. Un operando immediato può essere solo un operando sorgente. In un'istruzione macchina a due operandi sono possibili le seguenti combinazioni di operandi:

1) registrarsi - registrarsi;

2) registro - memoria;

3) memoria - registro;

4) operando immediato - registro;

5) operando immediato - memoria.

Esistono eccezioni a questa regola per quanto riguarda:

1) comandi a catena che possono spostare dati da memoria a memoria;

2) comandi stack che possono trasferire dati dalla memoria a uno stack anch'esso in memoria;

3) comandi di tipo moltiplicativo, che, oltre all'operando specificato nel comando, utilizzano anche un secondo operando implicito.

Delle combinazioni elencate di operandi, vengono utilizzate più spesso registro - memoria e memoria - registro. Data la loro importanza, li esamineremo più in dettaglio. Accompagneremo la discussione con esempi di comandi assembler che mostreranno come cambia il formato di un comando assembler quando viene applicato l'uno o l'altro tipo di indirizzamento. A questo proposito, si veda ancora la Figura 21, che mostra il principio di formare un indirizzo fisico sul bus di indirizzi del microprocessore. Si può notare che l'indirizzo dell'operando è formato dalla somma di due componenti: il contenuto del registro di segmento spostato di 4 bit e l'indirizzo effettivo di 16 bit, che è generalmente calcolato come somma di tre componenti: base, offset e indice.

3. Modalità di indirizzamento

Elenchiamo e poi consideriamo le caratteristiche dei principali tipi di operandi di indirizzamento in memoria:

1) indirizzamento diretto;

2) indirizzamento di base indiretto (di registro);

3) indirizzamento di base (di registro) indiretto con offset;

4) indirizzamento indiretto di indice con offset;

5) indirizzamento indiretto dell'indice di base;

6) indirizzamento indiretto dell'indice di base con offset.

Indirizzamento diretto

Questa è la forma più semplice per indirizzare un operando in memoria, poiché l'indirizzo effettivo è contenuto nell'istruzione stessa e non vengono utilizzate fonti o registri aggiuntivi per formarlo. L'indirizzo effettivo viene prelevato direttamente dal campo di offset dell'istruzione macchina (vedere la Figura 20), che può avere una dimensione di 8, 16, 32 bit. Questo valore identifica in modo univoco il byte, la parola o la doppia parola che si trova nel segmento di dati.

L'indirizzamento diretto può essere di due tipi.

Indirizzamento diretto relativo

Utilizzato per istruzioni di salto condizionato per indicare l'indirizzo di salto relativo. La relatività di tale transizione sta nel fatto che il campo di offset dell'istruzione macchina contiene un valore di 8, 16 o 32 bit, che, a seguito del funzionamento dell'istruzione, verrà aggiunto al contenuto di il registro del puntatore dell'istruzione ip/eip. Come risultato di questa aggiunta, si ottiene l'indirizzo, al quale viene effettuata la transizione.

Indirizzamento diretto assoluto

In questo caso, l'indirizzo effettivo fa parte dell'istruzione macchina, ma questo indirizzo è formato solo dal valore del campo offset nell'istruzione. Per formare l'indirizzo fisico dell'operando in memoria, il microprocessore aggiunge questo campo con il valore del registro di segmento spostato di 4 bit. Diverse forme di questo indirizzamento possono essere utilizzate in un'istruzione assembler.

Ma tale indirizzamento è usato raramente: alle celle comunemente usate nel programma vengono assegnati nomi simbolici. Durante la traduzione, l'assemblatore calcola e sostituisce i valori di offset di questi nomi nell'istruzione macchina che genera nel campo "offset istruzione". Ne risulta che l'istruzione macchina indirizza direttamente il suo operando, avendo, infatti, in uno dei suoi campi il valore dell'indirizzo effettivo.

Altri tipi di indirizzamento sono indiretti. La parola "indiretto" nel nome di questi tipi di indirizzamento significa che solo una parte dell'indirizzo effettivo può trovarsi nell'istruzione stessa, e le sue restanti componenti sono in registri, che sono indicati dal loro contenuto dal byte modr/m e, possibilmente, dal byte sib.

Indirizzamento di base indiretto (registro).

Con questo indirizzamento, l'indirizzo effettivo dell'operando può trovarsi in uno qualsiasi dei registri di uso generale, ad eccezione di sp / esp e bp / ebp (questi sono registri specifici per lavorare con un segmento di stack). Sintatticamente in un comando, questa modalità di indirizzamento è espressa racchiudendo il nome del registro tra parentesi quadre []. Ad esempio, l'istruzione mov ax, [ecx] inserisce nei registri ax il contenuto della parola all'indirizzo dal segmento di dati con l'offset memorizzato nel registro esx. Poiché il contenuto del registro può essere facilmente modificato nel corso del programma, questo metodo di indirizzamento consente di assegnare dinamicamente l'indirizzo di un operando ad alcune istruzioni macchina. Questa proprietà è molto utile, ad esempio, per organizzare calcoli ciclici e per lavorare con varie strutture di dati come tabelle o array.

Indirizzamento di base (registro) indiretto con offset

Questo tipo di indirizzamento è un'aggiunta al precedente ed è progettato per accedere a dati con un offset noto rispetto a un indirizzo di base. Questo tipo di indirizzamento è conveniente da utilizzare per accedere agli elementi delle strutture dati, quando l'offset degli elementi è noto in anticipo, in fase di sviluppo del programma, e l'indirizzo di base (di partenza) della struttura deve essere calcolato dinamicamente, a la fase di esecuzione del programma. La modifica del contenuto del registro di base consente di accedere agli elementi con lo stesso nome in istanze diverse dello stesso tipo di strutture dati.

Ad esempio, l'istruzione mov ax,[edx+3h] trasferisce le parole dall'area di memoria ai registri ax all'indirizzo: il contenuto di edx + 3h.

L'istruzione mov ax,mas[dx] sposta una parola nei registri ax all'indirizzo: il contenuto di dx più il valore dell'identificatore mas (ricordiamo che il compilatore assegna ad ogni identificatore un valore uguale all'offset di questo identificatore da l'inizio del segmento di dati).

Indirizzamento indiretto con offset

Questo tipo di indirizzamento è molto simile all'indirizzamento di base indiretto con un offset. Anche in questo caso, uno dei registri di uso generale viene utilizzato per formare l'indirizzo effettivo. Ma l'indirizzamento degli indici ha una caratteristica interessante che è molto comoda per lavorare con gli array. È connesso con la possibilità del cosiddetto ridimensionamento dei contenuti del registro indice. Cos'è?

Guarda la Figura 20. Siamo interessati al byte sib. Discutendo la struttura di questo byte, abbiamo notato che è composto da tre campi. Uno di questi campi è il campo scala ss, per il quale vengono moltiplicati i contenuti del registro indice.

Ad esempio, nell'istruzione mov ax,mas[si*2], il valore dell'indirizzo effettivo del secondo operando viene calcolato dall'espressione mas+(si)*2. A causa del fatto che l'assemblatore non ha i mezzi per organizzare l'indicizzazione dell'array, il programmatore deve organizzarla da solo.

Avere la capacità di scalare aiuta in modo significativo a risolvere questo problema, ma a condizione che la dimensione degli elementi dell'array sia 1, 2, 4 o 8 byte.

Indirizzamento indiretto dell'indice di base

Con questo tipo di indirizzamento, l'indirizzo effettivo è formato dalla somma del contenuto di due registri generici: base e indice. Questi registri possono essere qualsiasi registro generico e viene spesso utilizzato il ridimensionamento del contenuto di un registro indice.

Indirizzamento indiretto dell'indice di base con offset

Questo tipo di indirizzamento è il complemento dell'indirizzamento indiretto. L'indirizzo effettivo è formato dalla somma di tre componenti: il contenuto del registro di base, il contenuto del registro di indice e il valore del campo offset nel comando.

Ad esempio, l'istruzione mov eax,[esi+5] [edx] sposta una doppia parola nel registro eax all'indirizzo: (esi) + 5 + (edx).

Il comando add ax,array[esi] [ebx] aggiunge il contenuto del registro ax al contenuto della parola all'indirizzo: il valore dell'identificatore array + (esi) + (ebx).

LEZIONE N. 18. Squadre

1. Comandi di trasferimento dati

Per comodità di applicazione pratica e riflessione della loro specificità, è più conveniente considerare i comandi di questo gruppo in base al loro scopo funzionale, in base al quale possono essere suddivisi nei seguenti gruppi di comandi:

1) trasferimenti di dati per finalità generali;

2) ingresso-uscita alla porta;

3) lavorare con indirizzi e puntatori;

4) trasformazioni dei dati;

5) lavorare con la pila.

Comandi generali di trasferimento dati

Questo gruppo include i seguenti comandi:

1) mov è il comando di base per il trasferimento dei dati. Implementa un'ampia varietà di opzioni di spedizione. Nota le specifiche di questo comando:

a) il comando mov non può essere utilizzato per trasferire da un'area di memoria all'altra. Se si presenta tale necessità, qualsiasi registro per uso generale attualmente disponibile dovrebbe essere utilizzato come riserva intermedia;

b) è impossibile caricare un valore direttamente dalla memoria in un registro di segmento. Pertanto, per eseguire un tale carico, è necessario utilizzare un oggetto intermedio. Questo può essere un registro generico o uno stack;

c) non è possibile trasferire il contenuto di un registro di segmento in un altro registro di segmento. Questo perché non esiste un codice operativo corrispondente nel sistema di comando. Ma spesso sorge la necessità di un'azione del genere. È possibile eseguire tale trasferimento utilizzando gli stessi registri generici di quelli intermedi;

d) non è possibile utilizzare il registro di segmento CS come operando di destinazione. Il motivo è semplice. Il fatto è che nell'architettura del microprocessore, la cs: ip pair contiene sempre l'indirizzo del comando che deve essere eseguito successivamente. Modificare il contenuto del registro CS con il comando mov significherebbe in realtà un'operazione di salto, non un trasferimento, il che è inaccettabile. 2) xchg - utilizzato per il trasferimento dati bidirezionale. Per questa operazione, ovviamente, è possibile utilizzare una sequenza di diverse istruzioni mov, ma poiché l'operazione di scambio viene utilizzata abbastanza spesso, gli sviluppatori del sistema di istruzioni del microprocessore hanno ritenuto necessario introdurre un'istruzione di scambio xchg separata. Naturalmente gli operandi devono essere dello stesso tipo. Non è consentito (come per tutte le istruzioni assembler) scambiare tra loro il contenuto di due celle di memoria.

Comandi porta I/O

Osserva la Figura 22. Mostra un diagramma concettuale altamente semplificato del controllo hardware del computer.

Riso. 22. Schema concettuale del controllo hardware del computer

Come si può vedere dalla Figura 22, il livello più basso è il livello del BIOS, in cui l'hardware viene gestito direttamente attraverso le porte. Ciò implementa il concetto di indipendenza delle apparecchiature. In caso di sostituzione dell'hardware, sarà solo necessario correggere le corrispondenti funzioni del BIOS, riorientandole verso nuovi indirizzi e la logica delle porte.

Fondamentalmente, gestire i dispositivi direttamente tramite le porte è facile. Le informazioni sui numeri di porta, la loro profondità di bit, il formato delle informazioni di controllo sono fornite nella descrizione tecnica del dispositivo. Hai solo bisogno di conoscere l'obiettivo finale delle tue azioni, l'algoritmo in base al quale funziona un particolare dispositivo e l'ordine di programmazione delle sue porte, cioè, in effetti, devi sapere cosa e in quale sequenza devi inviare la porta (durante la scrittura su di essa) o letta da essa (durante la lettura) e come queste informazioni dovrebbero essere interpretate. Per fare ciò sono sufficienti due soli comandi presenti nel sistema di comando del microprocessore:

1) in accumulatore, port_number - input all'accumulatore dalla porta con il numero port_number;

2) out port, accumulator - invia il contenuto dell'accumulatore alla porta con il numero port_number.

Comandi per lavorare con indirizzi e puntatori di memoria

Quando si scrivono programmi in assembler, viene svolto un lavoro intenso con gli indirizzi degli operandi che sono in memoria. Per supportare questo tipo di operazioni, esiste un gruppo speciale di comandi, che include i seguenti comandi:

1) destinazione lea, fonte - caricamento indirizzo effettivo;

2) Ids destinazione, sorgente - caricamento del puntatore nel segmento dati register ds;

3) les destination, source - caricamento del puntatore nel registro dei segmenti dati aggiuntivi es;

4) lgs destinazione, sorgente - caricamento del puntatore nel registro del segmento dati aggiuntivo gs;

5) lfs destinazione, sorgente - caricamento del puntatore nel registro del segmento dati aggiuntivo fs;

6) destinazione lss, sorgente - puntatore di caricamento nel registro del segmento dello stack ss.

Il comando lea è simile al comando mov in quanto esegue anche una mossa. Tuttavia, l'istruzione lea non trasferisce i dati, ma l'indirizzo effettivo dei dati (cioè l'offset dei dati dall'inizio del segmento di dati) al registro indicato dall'operando di destinazione.

Spesso, per eseguire alcune azioni in un programma, non è sufficiente conoscere il valore dell'indirizzo effettivo dei dati da solo, ma è necessario disporre di un puntatore completo ai dati. Un puntatore dati completo è costituito da un componente segmento e un offset. Tutti gli altri comandi di questo gruppo consentono di ottenere un puntatore così completo a un operando in memoria in una coppia di registri. In questo caso, il nome del registro di segmento, in cui è collocata la componente di segmento dell'indirizzo, è determinato dal codice dell'operazione. Di conseguenza, l'offset viene inserito nel registro generale indicato dall'operando di destinazione.

Ma non tutto è così semplice con l'operando sorgente. Infatti nel comando come sorgente non è possibile specificare direttamente il nome dell'operando in memoria, a cui vorremmo ricevere un puntatore. Innanzitutto, è necessario ottenere il valore del puntatore completo in un'area di memoria e specificare l'indirizzo completo del nome di quest'area nel comando get. Per eseguire questa azione, è necessario ricordare le direttive per la prenotazione e l'inizializzazione della memoria.

Quando si applicano queste direttive, è possibile un caso speciale quando il nome di un'altra direttiva di definizione dei dati (in effetti, il nome di una variabile) è specificato nel campo dell'operando. In questo caso, l'indirizzo di questa variabile viene formato in memoria. Quale indirizzo verrà generato (efficace o completo) dipende dalla direttiva applicata. Se è dw, viene formato in memoria solo il valore a 16 bit dell'indirizzo effettivo; se è dd, viene scritto in memoria l'indirizzo completo. La posizione di questo indirizzo in memoria è la seguente: la parola bassa contiene l'offset, la parola alta contiene la componente del segmento a 16 bit dell'indirizzo.

Ad esempio, quando si organizza il lavoro con una catena di caratteri, è conveniente collocare il suo indirizzo iniziale in un determinato registro e quindi modificare questo valore in un ciclo per l'accesso sequenziale agli elementi della catena.

La necessità di utilizzare comandi per ottenere in memoria un puntatore dati completo, ovvero l'indirizzo del segmento e il valore di offset all'interno del segmento, sorge, in particolare, quando si lavora con le catene.

Comandi di conversione dei dati

Molte istruzioni del microprocessore possono essere attribuite a questo gruppo, ma la maggior parte di esse ha determinate caratteristiche che richiedono che siano attribuite ad altri gruppi funzionali. Pertanto, dell'intero set di comandi del microprocessore, solo un comando può essere attribuito direttamente ai comandi di conversione dei dati: xlat [address_of_transcoding_table]

Questa è una squadra molto interessante e utile. Il suo effetto è che sostituisce il valore nel registro al con un altro byte dalla tabella di memoria situata all'indirizzo specificato dall'operando remap_table_address.

La parola "tabella" è molto condizionale, infatti è solo una stringa di byte. L'indirizzo del byte nella stringa che sostituirà il contenuto del registro al è determinato dalla somma (bx) + (al), ovvero il contenuto di al funge da indice nell'array di byte.

Quando si lavora con il comando xlat, prestare attenzione al seguente punto sottile. Sebbene il comando specifichi l'indirizzo della stringa di byte da cui recuperare il nuovo valore, questo indirizzo deve essere precaricato (ad esempio utilizzando il comando lea) nel registro bx. Pertanto, l'operando lookup_table_address non è realmente necessario (l'opzionalità dell'operando è mostrata racchiudendolo tra parentesi quadre). Per quanto riguarda la stringa di byte (tabella di transcodifica), è un'area di memoria di dimensione compresa tra 1 e 255 byte (l'intervallo di un numero senza segno in un registro a 8 bit).

Comandi in pila

Questo gruppo è un insieme di comandi specializzati incentrati sull'organizzazione di un lavoro flessibile ed efficiente con lo stack.

Lo stack è un'area di memoria appositamente allocata per la memorizzazione temporanea dei dati del programma. L'importanza dello stack è determinata dal fatto che nella struttura del programma è previsto un segmento separato. Nel caso in cui il programmatore abbia dimenticato di dichiarare un segmento di stack nel suo programma, il linker tlink emetterà un messaggio di avviso.

Lo stack ha tre registri:

1) ss - registro del segmento dello stack;

2) sp/esp - registro del puntatore dello stack;

3) bp/ebp - registro del puntatore di base dello stack frame.

La dimensione dello stack dipende dalla modalità operativa del microprocessore ed è limitata a 64 KB (o 4 GB in modalità protetta).

È disponibile un solo stack alla volta, il cui indirizzo di segmento è contenuto nel registro SS. Questo stack è chiamato stack corrente. Per fare riferimento a un altro stack ("switch the stack"), è necessario caricare un altro indirizzo nel registro ss. Il registro SS viene utilizzato automaticamente dal processore per eseguire tutte le istruzioni che funzionano sullo stack.

Elenchiamo alcune altre caratteristiche del lavoro con lo stack:

1) la scrittura e la lettura dei dati sullo stack avviene secondo il principio LIFO,

2) man mano che i dati vengono scritti nello stack, quest'ultimo cresce verso indirizzi più bassi. Questa caratteristica è incorporata nell'algoritmo dei comandi per lavorare con lo stack;

3) quando si utilizzano i registri esp/sp ed ebp/bp per l'indirizzamento della memoria, l'assemblatore considera automaticamente che i valori in esso contenuti sono offset relativi al registro del segmento ss.

In generale, lo stack è organizzato come mostrato nella Figura 23.

Riso. 23. Schema concettuale dell'organizzazione dello stack

I registri SS, ESP/SP e EUR/BP sono progettati per funzionare con lo stack. Questi registri sono utilizzati in modo complesso e ognuno di essi ha un proprio scopo funzionale.

Il registro ESP/SP punta sempre alla parte superiore dello stack, ovvero contiene l'offset in corrispondenza del quale l'ultimo elemento è stato inserito nello stack. Le istruzioni dello stack modificano implicitamente questo registro in modo che punti sempre all'ultimo elemento inserito nello stack. Se lo stack è vuoto, il valore di esp è uguale all'indirizzo dell'ultimo byte del segmento allocato per lo stack. Quando un elemento viene inserito nello stack, il processore diminuisce il valore del registro esp, quindi scrive l'elemento all'indirizzo del nuovo vertice. Quando si estraggono i dati dallo stack, il processore copia l'elemento situato all'indirizzo superiore, quindi incrementa il valore del registro del puntatore dello stack esp. Pertanto, si scopre che lo stack cresce verso il basso, nella direzione di indirizzi decrescenti.

E se avessimo bisogno di accedere agli elementi non in alto, ma all'interno dello stack? A tale scopo, utilizzare il registro EBP.Il registro EBP è il registro del puntatore di base dello stack frame.

Ad esempio, un trucco tipico quando si entra in una subroutine è passare i parametri desiderati inserendoli nello stack. Se anche la subroutine sta lavorando attivamente con lo stack, l'accesso a questi parametri diventa problematico. La via d'uscita è salvare l'indirizzo della parte superiore dello stack nel puntatore del frame (base) dello stack dopo aver scritto i dati necessari nello stack: il registro EUR. Il valore in EUR può essere successivamente utilizzato per accedere ai parametri passati.

L'inizio dello stack si trova in indirizzi di memoria più alti. Nella Figura 23, questo indirizzo è indicato dalla coppia ss: fffF. Lo spostamento di wT è qui condizionato. In realtà, questo valore è determinato dal valore che il programmatore specifica quando descrive il segmento dello stack nel suo programma.

Per organizzare il lavoro con lo stack, ci sono comandi speciali per la scrittura e la lettura.

1. push source - scrivendo il valore della sorgente in cima allo stack.

Interessante è l'algoritmo di questo comando, che include le seguenti azioni (Fig. 24):

1) (sp) = (sp) - 2; il valore di sp è ridotto di 2;

2) il valore dalla sorgente viene scritto all'indirizzo specificato dalla ss: sp pair.

Riso. 24. Come funziona il comando push

2. assegnazione pop: scrive il valore dalla cima dello stack alla posizione specificata dall'operando di destinazione. Il valore viene quindi "rimosso" dalla cima dello stack. L'algoritmo del comando pop è l'inverso dell'algoritmo del comando push (Fig. 25):

1) scrivere il contenuto della parte superiore della pila nella posizione indicata dall'operando di destinazione;

2) (sp) = (sp) + 2; aumentando il valore di sp.

Riso. 25. Come funziona il comando pop

3. pusha - un comando di scrittura di gruppo nello stack. Con questo comando, i registri ax, cx, dx, bx, sp, bp, si, di vengono scritti in sequenza nello stack. Si noti che vengono scritti i contenuti originali di sp, ovvero il contenuto che era prima dell'emissione del comando pusha (Fig. 26).

Riso. 26. Come funziona il comando pusha

4. pushaw è quasi sinonimo del comando pusha Qual è la differenza? L'attributo bitness può essere use16 o use32. Diamo un'occhiata a come funzionano i comandi pusha e pushaw con ciascuno di questi attributi:

1) use16 - l'algoritmo pushaw è simile all'algoritmo pusha;

2) use32 - pushaw non cambia (cioè è insensibile alla larghezza del segmento e funziona sempre con registri di dimensioni word - ax, cx, dx, bx, sp, bp, si, di). Il comando pusha è sensibile alla larghezza del segmento impostata e quando viene specificato un segmento a 32 bit, funziona con i corrispondenti registri a 32 bit, ovvero eax, esx, edx, ebx, esp, ebp, esi, edi.

5. pushad - eseguito in modo simile al comando pusha, ma ci sono alcune particolarità.

I tre comandi seguenti eseguono il contrario dei comandi precedenti:

1) rora;

2) popo;

3) schiocco.

Il gruppo di istruzioni descritto di seguito consente di salvare il registro flag nello stack e di scrivere una parola o una doppia parola nello stack. Si noti che le istruzioni elencate di seguito sono le uniche nel set di istruzioni del microprocessore che consentono (e richiedono) l'accesso all'intero contenuto del registro flag.

1. pushf - salva il registro dei flag nello stack.

Il funzionamento di questo comando dipende dall'attributo della dimensione del segmento:

1) usa 16 - il registro flag di 2 byte viene scritto nello stack;

2) use32 - il registro eflags di 4 byte viene scritto nello stack.

2. pushfw: salva un registro di flag delle dimensioni di una parola sullo stack. Funziona sempre come pushf con l'attributo use16.

3. pushfd - salvataggio dei flag o dei flag eflags registrati sullo stack a seconda dell'attributo di larghezza di bit del segmento (cioè lo stesso di pushf).

Allo stesso modo, i seguenti tre comandi eseguono il contrario delle operazioni discusse sopra:

1) popf;

2) popTV;

3) popfd.

E in conclusione, notiamo le principali tipologie di operazioni quando l'utilizzo dello stack è quasi inevitabile:

1) chiamare subroutine;

2) memorizzazione temporanea dei valori di registro;

3) definizione di variabili locali.

2. Istruzioni aritmetiche

Il microprocessore può eseguire operazioni intere e in virgola mobile. Per fare ciò, la sua architettura ha due blocchi separati:

1) un dispositivo per eseguire operazioni su interi;

2) un dispositivo per eseguire operazioni in virgola mobile.

Ciascuno di questi dispositivi ha il proprio sistema di comando. In linea di principio, un dispositivo intero può assumere molte delle funzioni di un dispositivo a virgola mobile, ma questo sarà computazionalmente costoso. Per la maggior parte dei problemi che utilizzano il linguaggio assembly, è sufficiente l'aritmetica degli interi.

Panoramica di un gruppo di istruzioni e dati aritmetici

Un dispositivo di calcolo intero supporta poco più di una dozzina di istruzioni aritmetiche. La Figura 27 mostra la classificazione dei comandi in questo gruppo.

Riso. 27. Classificazione dei comandi aritmetici

Il gruppo di istruzioni aritmetiche intere funziona con due tipi di numeri:

1) numeri binari interi. I numeri possono avere o meno una cifra con segno, cioè numeri con o senza segno;

2) numeri interi decimali.

Considera i formati macchina in cui sono archiviati questi tipi di dati.

Numeri binari interi

Un intero binario a virgola fissa è un numero codificato nel sistema numerico binario.

La dimensione di un intero binario può essere 8, 16 o 32 bit. Il segno di un numero binario è determinato da come viene interpretato il bit più significativo nella rappresentazione del numero. Questo è 7,15 o 31 bit per i numeri della dimensione corrispondente. Allo stesso tempo, è interessante notare che tra i comandi aritmetici ci sono solo due comandi che tengono davvero conto di questo bit più significativo come uno di segno, questi sono i comandi di moltiplicazione e divisione di interi imul e idiv. Negli altri casi, la responsabilità delle azioni con numeri con segno e, di conseguenza, con un bit di segno spetta al programmatore. L'intervallo di valori di un numero binario dipende dalla sua dimensione e dall'interpretazione del bit più significativo o come bit più significativo del numero o come bit di segno del numero (Tabella 9).

Tabella 9. Intervallo di numeri binari Numeri decimali

I numeri decimali sono un tipo speciale di rappresentazione di informazioni numeriche, che si basa sul principio di codificare ogni cifra decimale di un numero con un gruppo di quattro bit. In questo caso, ogni byte del numero contiene una o due cifre decimali nel cosiddetto codice decimale a codice binario (BCD - Binary-Coded Decimal). Il microprocessore memorizza i numeri BCD in due formati (Fig. 28):

1) formato confezionato. In questo formato, ogni byte contiene due cifre decimali. Una cifra decimale è un valore binario a 0 bit compreso tra 9 e 4. In questo caso, il codice della cifra più alta del numero occupa i 4 bit più alti. Pertanto, il range di rappresentazione di un numero compresso decimale in 1 byte va da 00 a 99;

2) formato non imballato. In questo formato, ogni byte contiene una cifra decimale nei quattro bit meno significativi. I 4 bit superiori sono impostati a zero. Questa è la cosiddetta zona. Pertanto, l'intervallo di rappresentazione di un numero decimale non compresso in 1 byte è compreso tra 0 e 9.

Riso. 28. Rappresentazione dei numeri BCD

Come descrivere i numeri decimali binari in un programma? Per fare ciò, puoi utilizzare solo due istruzioni di descrizione e inizializzazione dei dati: db e dt. La possibilità di utilizzare solo queste direttive per descrivere i numeri BCD è dovuta al fatto che il principio del "byte basso a indirizzo basso" è applicabile anche a tali numeri, il che è molto conveniente per la loro elaborazione. E in generale, quando si utilizza un tipo di dati come i numeri BCD, l'ordine in cui questi numeri sono descritti nel programma e l'algoritmo per elaborarli è una questione di gusti e preferenze personali del programmatore. Questo diventerà chiaro dopo aver esaminato le basi del lavoro con i numeri BCD di seguito.

Operazioni aritmetiche su interi binari

Aggiunta di numeri binari senza segno

Il microprocessore esegue l'addizione di operandi secondo le regole per l'addizione di numeri binari. Non ci sono problemi fintanto che il valore del risultato non supera le dimensioni del campo operando. Ad esempio, quando si aggiungono operandi di dimensioni byte, il risultato non deve superare il numero 255. Se ciò accade, il risultato non è corretto. Consideriamo perché questo accade.

Ad esempio, facciamo l'addizione: 254 + 5 = 259 in binario. 11111110 + 0000101 = 1 00000011. Il risultato è andato oltre gli 8 bit e il suo valore corretto rientra in 9 bit e il valore 8 è rimasto nel campo a 3 bit dell'operando, il che, ovviamente, non è vero. Nel microprocessore, questo risultato dell'aggiunta è previsto e sono previsti mezzi speciali per correggere tali situazioni ed elaborarle. Quindi, per risolvere la situazione di lasciare la griglia di bit del risultato, come in questo caso, si intende il flag di riporto cf. Si trova nel bit 0 del registro flag EFLAGS/FLAGS. È l'impostazione di questo flag che risolve il fatto del trasferimento di uno dall'ordine superiore dell'operando. Naturalmente, il programmatore deve tenere conto della possibilità di un tale esito dell'operazione di addizione e fornire i mezzi per la correzione. Ciò comporta l'inclusione di sezioni di codice dopo l'operazione di aggiunta in cui viene analizzato il flag cf. Questa bandiera può essere analizzata in vari modi.

Il più semplice e accessibile consiste nell'usare il comando jcc conditional branch. Questa istruzione ha come operando il nome dell'etichetta nel segmento di codice corrente. Il passaggio a questa etichetta avviene se, a seguito dell'operazione del comando precedente, il flag cf è impostato su 1. Nel sistema di comando del microprocessore sono presenti tre comandi di addizione binaria:

1) operando inc - operazione di incremento, ovvero aumentare di 1 il valore dell'operando;

2) add operando_1, operando_2 - istruzione di addizione con il principio di funzionamento: operando_1 = operando_1 + operando_2;

3) adc operando_1, operando_2 - istruzione di addizione tenendo conto del flag di riporto cfr. Principio dell'operazione di comando: operando_1 = operando_1 + operando_2 + valore_sG.

Presta attenzione all'ultimo comando: questo è il comando di aggiunta, che tiene conto del trasferimento di uno dall'ordine superiore. Abbiamo già considerato il meccanismo per l'aspetto di una tale unità. Pertanto, l'istruzione adc è uno strumento a microprocessore per aggiungere lunghi numeri binari, le cui dimensioni superano le lunghezze dei campi standard supportati dal microprocessore.

Aggiunta binaria firmata

In effetti, il microprocessore "non è a conoscenza" della differenza tra numeri con segno e senza segno. Invece, ha i mezzi per fissare il verificarsi di situazioni caratteristiche che si sviluppano nel processo di calcolo. Ne abbiamo trattati alcuni quando abbiamo discusso dell'aggiunta non firmata:

1) il flag cf carry, impostandolo a 1 indica che gli operandi erano fuori portata;

2) il comando adc, che tiene conto della possibilità di tale uscita (portare dal bit meno significativo).

Un altro mezzo è registrare lo stato del bit di ordine superiore (segno) dell'operando, che viene eseguito utilizzando il flag di overflow nel registro EFLAGS (bit 11).

Naturalmente, ricordi come i numeri sono rappresentati in un computer: positivo - in binario, negativo - in complemento a due. Considera varie opzioni per aggiungere numeri. Gli esempi hanno lo scopo di mostrare il comportamento dei due bit più significativi degli operandi e la correttezza del risultato dell'operazione di addizione.

esempio

30566 = 0111011101100110

+

00687 = 00000010

=

31253 = 01111010

Monitoriamo i trasferimenti dalla 14° e 15° cifra e la correttezza del risultato: non ci sono trasferimenti, il risultato è corretto.

esempio

30566 = 0111011101100110

+

30566 = 0111011101100110

=

1132 = 11101110

C'è stato un trasferimento dalla 14a categoria; non vi è alcun trasferimento dalla 15a categoria. Il risultato è sbagliato, perché c'è un overflow: il valore del numero si è rivelato maggiore di quello che può avere un numero con segno a 16 bit (+32 767).

esempio

-30566 = 10001000 10011010

+

-04875 = 11101100 11110101

=

-35441 = 01110101 10001111

C'è stato un trasferimento dalla 15a cifra, non c'è nessun trasferimento dalla 14a cifra. Il risultato non è corretto, perché invece di un numero negativo, si è rivelato positivo (il bit più significativo è 0).

esempio

-4875 = 11101100 11110101

+

-4875 = 11101100 11110101

=

09750 = 11011001

Ci sono trasferimenti dal 14° e 15° bit. Il risultato è corretto.

Pertanto, abbiamo esaminato tutti i casi e abbiamo scoperto che la situazione di overflow (impostando il flag OF su 1) si verifica durante il trasferimento:

1) dalla quattordicesima cifra (per numeri positivi con segno);

2) dalla 15a cifra (per i numeri negativi).

Al contrario, non si verifica alcun overflow (ovvero, il flag OF viene reimpostato su 0) se vi è un riporto da entrambi i bit o se non vi è alcun riporto in entrambi i bit.

Quindi l'overflow viene registrato con il flag di overflow di. Oltre al flag di, durante il trasferimento dal bit di ordine superiore, il flag di trasferimento CF è impostato su 1. Poiché il microprocessore non è a conoscenza dell'esistenza di numeri con segno e senza segno, il programmatore è l'unico responsabile delle azioni corrette con i numeri risultanti. È possibile analizzare i flag CF e OF con le istruzioni di salto condizionale JC\JNC e JO\JNO, rispettivamente.

Per quanto riguarda i comandi per sommare numeri con segno, sono gli stessi dei numeri senza segno.

Sottrazione di numeri binari senza segno

Come nell'analisi dell'operazione di addizione, discuteremo l'essenza dei processi che si verificano durante l'esecuzione dell'operazione di sottrazione. Se il minuendo è maggiore del sottraendo, non ci sono problemi: la differenza è positiva, il risultato è corretto. Se il minuendo è minore del sottraendo, c'è un problema: il risultato è minore di 0, e questo è già un numero con segno. In questo caso, il risultato deve essere avvolto. Cosa significa questo? Con la solita sottrazione (in una colonna), fanno un prestito di 1 dall'ordine più alto. Il microprocessore fa lo stesso, cioè prende 1 dalla cifra che segue quella più alta nella griglia di bit dell'operando. Spieghiamo con un esempio.

esempio

05 = 00000000

-10 = 00000000 00001010

Per fare la sottrazione, facciamo

prestito immaginario senior:

100000000/00000101

-

00000000/00001010

=

11111111/11111011

Quindi, in sostanza, l'azione

(65 + 536) - 5 = 10

0 qui è, per così dire, equivalente al numero 65536. Il risultato, ovviamente, non è corretto, ma il microprocessore ritiene che tutto vada bene, sebbene risolva il fatto di prendere in prestito un'unità impostando il flag di riporto cfr. Ma guarda ancora con attenzione il risultato dell'operazione di sottrazione. È -5 in complemento a due! Conduciamo un esperimento: rappresentiamo la differenza come somma di 5 + (-10).

esempio

5 = 00000000

+

(-10)= 11111111 11110110

=

11111111/11111011

cioè abbiamo ottenuto lo stesso risultato dell'esempio precedente.

Pertanto, dopo il comando per sottrarre numeri senza segno, è necessario analizzare lo stato della bandiera CE. Se è impostato su 1, significa che c'è stato un prestito dall'ordine superiore e il risultato è stato ottenuto in un codice aggiuntivo .

Come le istruzioni di addizione, il gruppo di istruzioni di sottrazione è costituito dall'insieme più piccolo possibile. Questi comandi eseguono la sottrazione secondo gli algoritmi che stiamo ora considerando, e le eccezioni devono essere prese in considerazione dal programmatore stesso. I comandi di sottrazione includono:

1) dec operando - operazione di decremento, ovvero diminuire di 1 il valore dell'operando;

2) sub operando_1, operando_2 - comando di sottrazione; il suo principio di funzionamento: operando_1 = operando_1 - operando_2;

3) sbb operando_1, operando_2 - comando di sottrazione tenendo conto del prestito (ci flag): operando_1 = operando_1 - operando_2 - valore_sG.

Come puoi vedere, tra i comandi di sottrazione c'è un comando sbb che tiene conto del carry flag cfr. Questo comando è simile ad adc, ma ora il flag cf funge da indicatore di prendere in prestito 1 dalla cifra più significativa quando si sottraggono i numeri.

Sottrazione binaria con segno

Qui tutto è un po' più complicato. Il microprocessore non ha bisogno di avere due dispositivi: addizione e sottrazione. È sufficiente averne uno solo: il dispositivo aggiuntivo. Ma per la sottrazione mediante l'aggiunta di numeri con un segno in un codice aggiuntivo, è necessario rappresentare entrambi gli operandi, sia il ridotto che il sottratto. Il risultato dovrebbe anche essere considerato come un valore di complemento a due. Ma qui sorgono le difficoltà. Innanzitutto, sono legati al fatto che il bit più significativo dell'operando è considerato un bit di segno. Considera l'esempio della sottrazione di 45 - (-127).

esempio

Sottrazione di numeri con segno 1

45 = 0010

-

-127 = 1000 0001

=

-44 = 1010 1100

A giudicare dal bit di segno, il risultato è risultato negativo, il che, a sua volta, indica che il numero deve essere considerato come un complemento pari a -44. Il risultato corretto dovrebbe essere 172. Qui, come nel caso dell'addizione con segno, abbiamo riscontrato un overflow di mantissa, quando il bit significativo del numero ha cambiato il bit di segno dell'operando. Puoi tenere traccia di questa situazione in base al contenuto del flag di overflow di. L'impostazione su 1 indica che il risultato è fuori dall'intervallo di numeri con segno (ovvero, il bit più significativo è cambiato) per un operando di queste dimensioni e il programmatore deve agire per correggere il risultato.

esempio

Sottrazione di numeri con segno 2

-45-45 = -45 + (-45) = -90.

-45 = 11010011

+

-45 = 11010011

=

-90 = 1010 0110

Qui va tutto bene, il flag di overflow di viene reimpostato su 0 e 1 nel bit del segno indica che il valore del risultato è un numero di complemento a due.

Sottrazione e addizione di grandi operandi

Se noti, le istruzioni di addizione e sottrazione funzionano con operandi di dimensione fissa: 8, 16, 32 bit. Ma cosa succede se è necessario aggiungere numeri di dimensioni maggiori, ad esempio 48 bit, utilizzando operandi a 16 bit? Ad esempio, aggiungiamo due numeri a 48 bit:

Riso. 29. Aggiunta di operandi di grandi dimensioni

La Figura 29 mostra la tecnologia per aggiungere i numeri lunghi passo dopo passo. Si può vedere che il processo di aggiunta di numeri multi-byte avviene allo stesso modo di quando si aggiungono due numeri "in una colonna" - con l'implementazione, se necessario, di trasferire 1 al bit più alto. Se riusciamo a programmare questo processo, amplieremo notevolmente la gamma di numeri binari su cui possiamo eseguire operazioni di addizione e sottrazione.

Il principio della sottrazione di numeri con un intervallo di rappresentazione superiore alle griglie di bit degli operandi standard è lo stesso dell'addizione, ovvero viene utilizzato il flag di riporto cf. Devi solo immaginare il processo di sottrazione in una colonna e combinare correttamente le istruzioni del microprocessore con l'istruzione sbb.

Per concludere la nostra discussione sulle istruzioni di addizione e sottrazione, oltre al cf e ai flag, ci sono alcuni altri flag nel registro eflags che possono essere usati con le istruzioni aritmetiche binarie. Questi sono i seguenti flag:

1) zf - flag zero, che è impostato a 1 se il risultato dell'operazione è 0, ea 1 se il risultato non è uguale a 0;

2) sf - flag di segno, il cui valore dopo operazioni aritmetiche (e non solo) coincide con il valore del bit più significativo del risultato, ovvero con il bit 7, 15 o 31. Pertanto, questo flag può essere utilizzato per le operazioni sui numeri firmati.

Moltiplicazione di numeri senza segno

Il comando per moltiplicare i numeri senza segno è

fattore multi_1

Come puoi vedere, il comando contiene un solo operando moltiplicatore. Il secondo operando factor_2 è specificato in modo implicito. La sua posizione è fissa e dipende dalla dimensione dei fattori. Poiché, in generale, il risultato di una moltiplicazione è maggiore di qualsiasi suo fattore, anche la sua dimensione e posizione devono essere determinate in modo univoco. Le opzioni per le dimensioni dei fattori e il posizionamento del secondo operando e del risultato sono mostrate nella Tabella 10.

Tabella 10. Disposizione degli operandi e risultato nella moltiplicazione

Dalla tabella si evince che il prodotto è composto da due parti e, a seconda della dimensione degli operandi, è posto in due posti - al posto di factor_2 (parte inferiore) e nel registro aggiuntivo ah, dx, edx (parte superiore parte). Come sapere, quindi, dinamicamente (cioè durante l'esecuzione del programma) che il risultato è abbastanza piccolo da stare in un registro, o che ha superato la dimensione del registro e la parte più alta è finita in un altro registro? Per fare ciò, utilizziamo i flag cf e overflow già a noi noti dalla discussione precedente:

1) se la parte iniziale del risultato è zero, allora dopo l'operazione sul prodotto i flag cf = 0 e of = 0;

2) se questi flag sono diversi da zero, significa che il risultato è andato oltre la parte più piccola del prodotto ed è composto da due parti, che dovrebbero essere prese in considerazione in ulteriori lavori.

Moltiplica i numeri con segno

Il comando per moltiplicare i numeri con un segno è

[imul operando_1, operando_2, operando_3]

Questo comando viene eseguito allo stesso modo del comando mul. Una caratteristica distintiva del comando imul è solo la formazione del segno.

Se il risultato è piccolo e si inserisce in un registro (cioè, se cf = di = 0), allora il contenuto dell'altro registro (la parte alta) è un'estensione del segno - tutti i suoi bit sono uguali al bit alto (bit del segno ) della parte bassa del risultato. Altrimenti (se cf = di = 1), il segno del risultato è il bit di segno della parte alta del risultato e il bit di segno della parte bassa è il bit significativo del codice binario del risultato.

Divisione dei numeri senza segno

Il comando per dividere i numeri senza segno è

divisore div

Il divisore può essere in memoria o in un registro e avere una dimensione di 8, 16 o 32 bit. La posizione del dividendo è fissa e, come nell'istruzione di moltiplicazione, dipende dalla dimensione degli operandi. Il risultato del comando di divisione sono i valori del quoziente e del resto.

Le opzioni per la posizione e la dimensione degli operandi dell'operazione di divisione sono mostrate nella Tabella 11.

Tabella 11. Disposizione degli operandi e risultato nella divisione

Dopo che l'istruzione divide è stata eseguita, il contenuto dei flag è indefinito, ma può verificarsi il numero di interrupt 0, chiamato "divide per zero". Questo tipo di interruzione appartiene alle cosiddette eccezioni. Questo tipo di interruzione si verifica all'interno del microprocessore a causa di alcune anomalie durante il processo di elaborazione. L'interruzione O, "divide per zero", durante l'esecuzione del comando div può verificarsi per uno dei seguenti motivi:

1) il divisore è zero;

2) il quoziente non è compreso nella bit grid ad esso assegnata, cosa che può verificarsi nei seguenti casi:

a) quando si divide un dividendo con un valore di una parola per un divisore con un valore di byte e il valore del dividendo è più di 256 volte maggiore del valore del divisore;

b) quando si divide un dividendo con un valore di una parola doppia per un divisore con un valore di una parola, e il valore del dividendo è superiore a 65 volte il valore del divisore;

c) quando si divide il dividendo con valore di quadrupla word per un divisore con valore di doppia word, e il valore del dividendo è superiore a 4 volte il valore del divisore.

Divisione con un segno

Il comando per dividere i numeri con un segno è

divisore idiv

Per questo comando valgono tutte le disposizioni considerate relative a comandi e numeri firmati. Notiamo solo le caratteristiche del verificarsi dell'eccezione 0, "divisione per zero", nel caso di numeri con segno. Si verifica durante l'esecuzione del comando idiv per uno dei seguenti motivi:

1) il divisore è zero;

2) il quoziente non è compreso nella griglia di bit ad esso assegnata.

Quest'ultimo a sua volta può accadere:

1) quando si divide un dividendo con un valore di parola con segno per un divisore con un valore di byte con segno e il valore del dividendo è superiore a 128 volte il valore del divisore (quindi, il quoziente non deve essere al di fuori dell'intervallo da -128 a + 127);

2) quando si divide il dividendo per un valore di doppia parola con segno per il divisore per un valore di parola con segno e il valore del dividendo è superiore a 32 volte il valore del divisore (quindi, il quoziente non deve essere al di fuori dell'intervallo da - da 768 a +32) ;

3) quando si divide il dividendo per un valore di quadword con segno per un divisore di doppia parola con segno e il valore del dividendo è superiore a 2 volte il valore del divisore (quindi, il quoziente non deve essere al di fuori dell'intervallo da -147 a + 483 648 2 147).

Istruzioni ausiliarie per operazioni intere

Ci sono diverse istruzioni nel set di istruzioni del microprocessore che possono semplificare la programmazione di algoritmi che eseguono calcoli aritmetici. In essi possono sorgere vari problemi, per la cui risoluzione gli sviluppatori di microprocessori hanno fornito diversi comandi.

Digita i comandi di conversione

Cosa succede se le dimensioni degli operandi coinvolti nelle operazioni aritmetiche sono diverse? Si supponga, ad esempio, in un'operazione di addizione, che un operando sia una parola e l'altro sia una doppia parola. Si è detto sopra che gli operandi dello stesso formato devono partecipare all'operazione di addizione. Se i numeri sono senza segno, l'output è facile da trovare. In questo caso, sulla base dell'operando originale, se ne può formare uno nuovo (formato double word), i cui bit alti possono essere semplicemente riempiti di zeri. La situazione è più complicata per i numeri con segno: come tenere conto del segno dell'operando in modo dinamico, durante l'esecuzione del programma? Per risolvere tali problemi, il set di istruzioni del microprocessore ha le cosiddette istruzioni di conversione del tipo. Queste istruzioni espandono i byte in parole, le parole in parole doppie e le parole doppie in parole quad (valori a 64 bit). Le istruzioni di conversione del tipo sono particolarmente utili quando si convertono interi con segno, poiché riempiono automaticamente i bit di ordine superiore dell'operando appena costruito con i valori del bit del segno del vecchio oggetto. Questa operazione si traduce in valori interi dello stesso segno e della stessa grandezza dell'originale, ma in un formato più lungo. Tale trasformazione è chiamata operazione di propagazione del segno.

Esistono due tipi di comandi di conversione del tipo.

1. Istruzioni senza operandi. Questi comandi funzionano con registri fissi:

1) cbw (Convert Byte to Word) - un comando per convertire un byte (nel registro al) in una parola (nel registro ah) estendendo il valore del bit alto al a tutti i bit del registro ah;

2) cwd (Convert Word to Double) - un comando per convertire una parola (nel registro ax) in una doppia parola (nei registri dx: ax) estendendo il valore del bit alto ax a tutti i bit del registro dx;

3) cwde (Convert Word to Double) - un comando per convertire una parola (nel registro ax) in una doppia parola (nel registro eax) distribuendo il valore del bit alto ax a tutti i bit della metà superiore del registro eax ;

4) cdq (Convert Double Word to Quarter Word) - un comando per convertire una doppia word (nel registro eax) in una quadrupla word (negli edx: registri eax) diffondendo a tutti il ​​valore del bit più significativo di eax bit del registro edx.

2. Comandi movsx e movzx relativi ai comandi di elaborazione delle stringhe. Questi comandi hanno una proprietà utile nel contesto del nostro problema:

1) movsx operand_1, operand_2 - invia con propagazione del segno. Estende un valore a 8 o 16 bit di operando_2, che può essere un registro o un operando di memoria, a un valore a 16 o 32 bit in uno dei registri, utilizzando il valore del bit del segno per riempire le posizioni più alte di operando_1. Questa istruzione è utile per preparare operandi con segno per operazioni aritmetiche;

2) movzx operand_1, operand_2 - invia con estensione zero. Estende il valore a 8 o 16 bit di operando_2 a 16 o 32 bit, cancellando (riempiendo) le posizioni alte di operando_2 con zeri. Questa istruzione è utile per preparare operandi senza segno per l'aritmetica.

Altri comandi utili

1. xadd destinazione, sorgente - scambio e aggiunta.

Il comando consente di eseguire due azioni in sequenza:

1) scambiare i valori di destinazione e di origine;

2) posizionare l'operando di destinazione al posto della somma: destinazione = destinazione + sorgente.

2. neg operando - negazione con complemento a due.

L'istruzione inverte il valore dell'operando. Fisicamente, il comando esegue un'azione:

operando = 0 - operando, ovvero sottrae l'operando da zero.

Il comando neg operando può essere utilizzato:

1) cambiare il segno;

2) eseguire la sottrazione da una costante.

Operazioni aritmetiche su numeri binari-decimali

In questa sezione, esamineremo le specifiche di ciascuna delle quattro operazioni aritmetiche di base per i numeri BCD compressi e non imballati.

La domanda potrebbe sorgere giustamente: perché abbiamo bisogno dei numeri BCD? La risposta potrebbe essere: i numeri BCD sono necessari nelle applicazioni aziendali, cioè dove i numeri devono essere grandi e precisi. Come abbiamo già visto nell'esempio dei numeri binari, le operazioni con tali numeri sono piuttosto problematiche per il linguaggio assembly. Gli svantaggi dell'utilizzo di numeri binari includono quanto segue:

1) I valori in formato parola e doppia parola hanno un intervallo limitato. Se il programma è progettato per funzionare nel campo della finanza, limitare l'importo in rubli a 65 (per una parola) o anche a 536 (per una doppia parola) ridurrà significativamente l'ambito della sua applicazione;

2) la presenza di errori di arrotondamento. Riuscite a immaginare un programma in esecuzione da qualche parte in una banca che non tiene conto del valore del saldo quando opera con numeri interi binari e opera con miliardi? Non vorrei essere l'autore di un programma del genere. L'uso di numeri in virgola mobile non salverà: esiste lo stesso problema di arrotondamento;

3) presentazione di una grande quantità di risultati in forma simbolica (codice ASCII). I programmi aziendali non fanno solo calcoli; una delle finalità del loro utilizzo è il tempestivo recapito delle informazioni all'utente. Per fare ciò, ovviamente, le informazioni devono essere presentate in forma simbolica. La conversione di numeri da binari ad ASCII richiede un certo sforzo di calcolo. Un numero in virgola mobile è ancora più difficile da tradurre in una forma simbolica. Ma se guardi la rappresentazione esadecimale di una cifra decimale non compressa e il suo carattere corrispondente nella tabella ASCII, puoi vedere che differiscono di 30 ore. Pertanto, la conversione in forma simbolica e viceversa è molto più semplice e veloce.

Probabilmente hai già visto l'importanza di padroneggiare almeno le basi delle azioni con i numeri decimali. Quindi, considera le caratteristiche dell'esecuzione di operazioni aritmetiche di base con numeri decimali. Notiamo immediatamente il fatto che non esistono comandi separati per addizione, sottrazione, moltiplicazione e divisione di numeri BCD. Ciò è stato fatto per ragioni abbastanza comprensibili: la dimensione di tali numeri può essere arbitrariamente grande. I numeri BCD possono essere aggiunti e sottratti, sia compressi che decompressi, ma solo i numeri BCD non compressi possono dividersi e moltiplicarsi. Perché è così sarà visto da ulteriori discussioni.

Aritmetica sui numeri BCD non imballati

Aggiungi i numeri BCD decompressi

Consideriamo due casi di addizione.

esempio

Il risultato dell'addizione non è superiore a 9

6 = 0000

+

3 = 0000

=

9 = 0000

Non vi è alcun trasferimento dalla tetrade junior a quella senior. Il risultato è corretto.

esempio

Il risultato dell'addizione è maggiore di 9:

06 = 0000

+

07 = 0000

=

13 = 0000

Non abbiamo più ricevuto un numero BCD. Il risultato è sbagliato. Il risultato corretto nel formato BCD non compresso dovrebbe essere 0000 0001 0000 0011 in binario (o 13 in decimale).

Dopo aver analizzato questo problema quando si aggiungono numeri BCD (e problemi simili quando si eseguono altre operazioni aritmetiche) e possibili modi per risolverlo, gli sviluppatori del sistema di comando del microprocessore hanno deciso di non introdurre comandi speciali per lavorare con i numeri BCD, ma di introdurre diversi comandi correttivi .

Lo scopo di queste istruzioni è correggere il risultato dell'operazione di ordinarie istruzioni aritmetiche per i casi in cui gli operandi in esse contenuti sono numeri BCD.

Nel caso della sottrazione nell'esempio 10, si può notare che il risultato ottenuto deve essere corretto. Per correggere l'operazione di aggiunta di due numeri BCD non compressi a una cifra nel sistema di comando del microprocessore, esiste un comando speciale - aaa (ASCII Adjust for Addition) - correzione del risultato dell'addizione per la rappresentazione in forma simbolica.

Questa istruzione non ha operandi. Funziona implicitamente solo con il registro al e analizza il valore della sua tetrade inferiore:

1) se tale valore è inferiore a 9, allora il flag cf viene riportato a XNUMX e viene effettuato il passaggio all'istruzione successiva;

2) se questo valore è maggiore di 9, vengono eseguite le seguenti azioni:

a) 6 viene aggiunto al contenuto della tetrad al inferiore (ma non al contenuto dell'intero registro!) Pertanto, il valore del risultato decimale viene corretto nella direzione corretta;

b) il flag cf è posto a 1, fissando così il trasferimento al bit più significativo in modo che possa essere preso in considerazione nelle azioni successive.

Quindi, nell'esempio 10, supponendo che il valore della somma 0000 1101 sia in al, dopo l'istruzione aaa, il registro avrà 1101 + 0110 = 0011, ovvero binario 0000 0011 o decimale 3, e il flag cf sarà impostato su 1, cioè il trasferimento è stato memorizzato nel microprocessore. Successivamente, il programmatore dovrà utilizzare l'istruzione di aggiunta adc, che terrà conto del riporto del bit precedente.

Sottrazione di numeri BCD non imballati

La situazione qui è abbastanza simile all'addizione. Consideriamo gli stessi casi.

esempio

Il risultato della sottrazione non è maggiore di 9:

6 = 0000

-

3 = 0000

=

3 = 0000

Come puoi vedere, non c'è prestito dal taccuino senior. Il risultato è corretto e non necessita di correzione.

esempio

Il risultato della sottrazione è maggiore di 9:

6 = 0000

-

7 = 0000

=

-1 = 1111 1111

La sottrazione viene eseguita secondo le regole dell'aritmetica binaria. Pertanto, il risultato non è un numero BCD.

Il risultato corretto nel formato BCD decompresso dovrebbe essere 9 (0000 1001 in binario). In questo caso si assume un prestito dalla cifra più significativa, come per un normale comando di sottrazione, cioè nel caso di numeri BCD si dovrebbe effettivamente eseguire la sottrazione di 16 - 7. Quindi è chiaro che, come nel in caso di addizione, il risultato della sottrazione deve essere corretto. Per questo, esiste un comando speciale - aas (ASCII Adjust for Substraction) - correzione del risultato della sottrazione per la rappresentazione in forma simbolica.

Anche l'istruzione aas non ha operandi e opera sul registro al, analizzando la sua tetrade di ordine minimo come segue:

1) se il suo valore è inferiore a 9, il flag cf viene riportato a 0 e il controllo viene trasferito al comando successivo;

2) se il valore della tetrade in al è maggiore di 9, il comando aas esegue le seguenti azioni:

a) sottrae 6 dal contenuto della tetrade inferiore del registro al (nota - non dal contenuto dell'intero registro);

b) azzera la tetrade superiore del registro al;

c) imposta il flag cf su 1, fissando così l'immaginario prestito di alto livello.

È chiaro che il comando aas viene utilizzato insieme ai comandi di sottrazione sub e sbb di base. In questo caso, ha senso utilizzare il comando sub solo una volta, sottraendo le cifre più basse degli operandi, quindi dovrebbe essere utilizzato il comando sbb, che terrà conto di un possibile prestito dall'ordine più alto.

Moltiplicazione dei numeri BCD non imballati

Utilizzando l'esempio dell'addizione e sottrazione di numeri non compressi, è diventato chiaro che non esistono algoritmi standard per eseguire queste operazioni sui numeri BCD e il programmatore stesso, in base ai requisiti del suo programma, deve implementare queste operazioni.

L'attuazione delle due restanti operazioni - moltiplicazione e divisione - è ancora più complicata. Nel set di istruzioni del microprocessore, ci sono solo mezzi per produrre la moltiplicazione e la divisione di numeri BCD non compressi a una cifra.

Per moltiplicare numeri di dimensioni arbitrarie, devi implementare tu stesso il processo di moltiplicazione, basato su un algoritmo di moltiplicazione, ad esempio "in una colonna".

Per moltiplicare due numeri BCD a una cifra, devi:

1) inserire uno dei fattori nel registro AL (come previsto dall'istruzione mul);

2) posizionare il secondo operando in un registro o memoria, allocando un byte;

3) moltiplicare i fattori con il comando mul (il risultato, come previsto, sarà in ah);

4) il risultato, ovviamente, sarà in codice binario, quindi deve essere corretto.

Per correggere il risultato dopo la moltiplicazione, viene utilizzato un comando speciale - aam (ASCII Adjust for Multiplication) - correzione del risultato della moltiplicazione per la rappresentazione in forma simbolica.

Non ha operandi e opera sul registro AX come segue:

1) divide al per 10;

2) il risultato della divisione si scrive come segue: quoziente in al, resto in ah. Di conseguenza, dopo aver eseguito l'istruzione aam, i registri AL e ah contengono le cifre BCD corrette del prodotto di due cifre.

Prima di concludere la discussione sul comando aam, dobbiamo notare un altro suo utilizzo. Questo comando può essere utilizzato per convertire un numero binario nel registro AL in un numero BCD spacchettato, che verrà inserito nel registro ah: la cifra più significativa del risultato è in ah, la cifra meno significativa è in al. È chiaro che il numero binario deve essere compreso tra 0 e 99.

Divisione dei numeri BCD non imballati

Il processo di esecuzione dell'operazione di divisione di due numeri BCD non compressi è in qualche modo diverso dalle altre operazioni considerate in precedenza con essi. Anche qui sono necessarie azioni correttive, ma devono essere eseguite prima dell'operazione principale che divide direttamente un numero BCD per un altro numero BCD. Innanzitutto, nel registro ah, devi ottenere due cifre BCD decompresse del dividendo. Questo rende il programmatore a suo agio per lui in un certo senso. Successivamente, è necessario emettere il comando aad - aad (ASCII Adjust for Division) - correzione della divisione per la rappresentazione simbolica.

L'istruzione non ha operandi e converte il numero BCD scompattato a due cifre nel registro ax in un numero binario. Questo numero binario svolgerà successivamente il ruolo di dividendo nell'operazione di divisione. Oltre alla conversione, il comando aad inserisce il numero binario risultante nel registro AL. Il dividendo sarà naturalmente un numero binario compreso tra 0 e 99.

L'algoritmo mediante il quale il comando aad esegue questa conversione è il seguente:

1) moltiplicare la cifra più alta del numero BCD originale in ah (il contenuto di AH) per 10;

2) eseguire l'addizione AH + AL, il cui risultato (numero binario) viene inserito in AL;

3) ripristinare il contenuto di AH.

Successivamente, il programmatore deve emettere un normale comando di divisione div per eseguire la divisione del contenuto di ax per una singola cifra BCD situata in un registro di byte o in una posizione di memoria di byte.

Similmente ad aash, il comando aad può essere utilizzato anche per convertire i numeri BCD non compressi nell'intervallo 0...99 nel loro equivalente binario.

Per dividere numeri di maggiore capacità, così come nel caso della moltiplicazione, è necessario implementare un proprio algoritmo, ad esempio "in una colonna", oppure trovare un modo più ottimale.

Aritmetica sui numeri BCD imballati

Come notato sopra, i numeri BCD compressi possono solo essere aggiunti e sottratti. Per eseguire altre azioni su di essi, devono essere ulteriormente convertiti in un formato non compresso o in una rappresentazione binaria. Poiché i numeri BCD compressi non sono di grande interesse, li considereremo brevemente.

Aggiunta di numeri BCD compressi

Per prima cosa, entriamo nel vivo del problema e proviamo ad aggiungere due numeri BCD compressi a due cifre. Esempio di aggiunta di numeri BCD compressi:

67 = 01100111

+

75 = 01110101

=

142 = 1101 1100 = 220

Come puoi vedere, in binario il risultato è 1101 1100 (o 220 in decimale), che non è corretto. Questo perché il microprocessore non è a conoscenza dell'esistenza di numeri BCD e li aggiunge secondo le regole per l'aggiunta di numeri binari. In realtà, il risultato in BCD dovrebbe essere 0001 0100 0010 (o 142 in decimale).

Si può vedere che, come per i numeri BCD non compressi, per i numeri BCD compressi è necessario correggere in qualche modo i risultati delle operazioni aritmetiche.

Il microprocessore prevede questo comando daa - daa (Decimal Adjust for Addition) - correzione del risultato dell'addizione per la presentazione in forma decimale.

Il comando daa converte il contenuto del registro al in due cifre decimali impacchettate secondo l'algoritmo fornito nella descrizione del comando daa L'unità risultante (se il risultato dell'addizione è maggiore di 99) è memorizzata nel flag cf, tenendo quindi conto del trasferimento al bit più significativo.

Sottrazione di numeri BCD compressi

Simile all'addizione, il microprocessore tratta i numeri BCD compressi come binari e sottrae i numeri BCD come binari di conseguenza.

esempio

Sottrazione di numeri BCD compressi.

Sottraiamo 67-75. Poiché il microprocessore esegue la sottrazione in modo di addizione, seguiremo questo:

67 = 01100111

+

-75 = 10110101

=

-8 = 0001 1100 = 28

Come puoi vedere, il risultato è 28 decimale, il che è assurdo. In BCD, il risultato dovrebbe essere 0000 1000 (o 8 in decimale).

Quando si programma la sottrazione di numeri BCD compressi, il programmatore, così come quando si sottrae numeri BCD non compressi, deve controllare lui stesso il segno. Questo viene fatto usando il flag CF, che risolve il prestito di ordine elevato.

La sottrazione dei numeri BCD stessa viene eseguita da un semplice comando di sottrazione sub o sbb. La correzione del risultato viene eseguita dal comando das - das (Decimal Adjust for Substraction) - correzione del risultato della sottrazione per la rappresentazione in forma decimale.

Il comando das converte il contenuto del registro AL in due cifre decimali impacchettate secondo l'algoritmo fornito nella descrizione del comando das.

CONFERENZA N. 19. Comandi di trasferimento del controllo

1. Comandi logici

Oltre ai mezzi di calcolo aritmetico, il sistema di comando a microprocessore dispone anche di mezzi di conversione logica dei dati. Con mezzi logici tali trasformazioni di dati, che si basano sulle regole della logica formale.

La logica formale opera a livello di affermazioni vere e false. Per un microprocessore, questo di solito significa rispettivamente 1 e 0. Per un computer, la lingua degli zeri e degli uno è nativa, ma l'unità minima di dati con cui funzionano le istruzioni della macchina è un byte. Tuttavia, a livello di sistema, è spesso necessario poter operare al livello più basso possibile, il livello di bit.

Riso. 29. Modalità di elaborazione logica dei dati

I mezzi di trasformazione logica dei dati includono comandi logici e operazioni logiche. L'operando di un'istruzione assembler può generalmente essere un'espressione, che a sua volta è una combinazione di operatori e operandi. Tra questi operatori possono esserci operatori che implementano operazioni logiche su oggetti espressione.

Prima di considerare nel dettaglio questi strumenti, consideriamo quali sono i dati logici stessi e quali operazioni su di essi vengono eseguite.

Dati booleani

La base teorica per l'elaborazione logica dei dati è la logica formale. Esistono diversi sistemi logici. Uno dei più famosi è il calcolo proposizionale. Una proposizione è qualsiasi affermazione che si può dire vera o falsa.

Il calcolo proposizionale è un insieme di regole utilizzate per determinare la verità o la falsità di una combinazione di proposizioni.

Il calcolo proposizionale è molto armoniosamente combinato con i principi del computer ei metodi di base della sua programmazione. Tutti i componenti hardware di un computer sono costruiti su chip logici. Il sistema per rappresentare le informazioni in un computer al livello più basso si basa sul concetto di bit. Un po', avendo solo due stati (0 (falso) e 1 (vero)), rientra naturalmente nel calcolo proposizionale.

Secondo la teoria, le seguenti operazioni logiche possono essere eseguite su istruzioni (su bit).

1. Negazione (NOT logico) - un'operazione logica su un operando, il cui risultato è il reciproco del valore dell'operando originale.

Questa operazione è caratterizzata in modo univoco dalla seguente tavola di verità (Tabella 12).

Tabella 12. Tabella di verità per la negazione logica

2. Addizione logica (OR logico inclusivo): un'operazione logica su due operandi, il cui risultato è "vero" (1) se uno o entrambi gli operandi sono veri (1) e "falso" (0) se entrambi gli operandi sono falso (0).

Questa operazione è descritta utilizzando la seguente tabella di verità (Tabella 13).

Tabella 13. Tabella di verità per OR logico inclusivo

3. Moltiplicazione logica (AND logico) - un'operazione logica su due operandi, il cui risultato è vero (1) solo se entrambi gli operandi sono veri (1). In tutti gli altri casi, il valore dell'operazione è "falso" (0).

Questa operazione è descritta utilizzando la seguente tabella di verità (Tabella 14).

Tabella 14. Tavola di logica E di verità

4. Aggiunta logica esclusiva (OR logico esclusivo) - un'operazione logica su due operandi, il cui risultato è "vero" (1), se solo uno dei due operandi è vero (1) e falso (0), se entrambi gli operandi sono false (0) o true (1). Questa operazione è descritta utilizzando la seguente tabella di verità (Tabella 15).

Tabella 15. Tabella di verità per XOR logico

Il set di istruzioni del microprocessore contiene cinque istruzioni che supportano queste operazioni. Queste istruzioni eseguono operazioni logiche sui bit degli operandi. Le dimensioni degli operandi, ovviamente, devono essere le stesse. Ad esempio, se la dimensione degli operandi è uguale alla parola (16 bit), l'operazione logica viene eseguita prima sugli zero bit degli operandi e il suo risultato viene scritto al posto del bit 0 del risultato. Successivamente, il comando ripete queste azioni in sequenza su tutti i bit dal primo al quindicesimo.

Comandi logici

Il sistema di comando del microprocessore ha la seguente serie di comandi che supportano il lavoro con i dati logici:

1) e operando_1, operando_2 - operazione di moltiplicazione logica. Il comando esegue un'operazione AND logica bit per bit (congiunzione) sui bit degli operandi operando_1 e operando_2. Il risultato viene scritto al posto di operando_1;

2) og operando_1, operando_2 - operazione di addizione logica. Il comando esegue un'operazione OR logico bit per bit (disgiunzione) sui bit degli operandi operando_1 e operando_2. Il risultato viene scritto al posto di operando_1;

3) xor operando_1, operando_2 - operazione di addizione logica esclusiva. Il comando esegue un'operazione XOR logica bit per bit sui bit degli operandi operando_1 e operando_2. Il risultato viene scritto al posto dell'operando;

4) test operando_1, operando_2 - operazione "test" (utilizzando il metodo di moltiplicazione logica). Il comando esegue un'operazione AND logica bit per bit sui bit degli operandi operando_1 e operando_2. Lo stato degli operandi rimane lo stesso, vengono modificati solo i flag zf, sf e pf, il che consente di analizzare lo stato dei singoli bit dell'operando senza cambiarne lo stato;

5) non operando - operazione di negazione logica. Il comando esegue un'inversione bit per bit (sostituendo il valore con il contrario) di ogni bit dell'operando. Il risultato viene scritto al posto dell'operando.

Per comprendere il ruolo dei comandi logici nel set di istruzioni del microprocessore, è molto importante comprendere le aree della loro applicazione ei metodi tipici del loro utilizzo nella programmazione.

Con l'ausilio di comandi logici è possibile selezionare i singoli bit dell'operando allo scopo di impostarli, resettarli, invertirli o semplicemente verificare un determinato valore.

Per organizzare tale lavoro con i bit, operando_2 di solito svolge il ruolo di una maschera. Con l'aiuto dei bit di questa maschera impostati nel bit 1, vengono determinati i bit operand_1 necessari per una determinata operazione. Mostriamo quali comandi logici possono essere utilizzati per questo scopo:

1) per impostare determinate cifre (bit) a 1, viene utilizzato il comando og operand_1, operand_2.

In questa istruzione, operando_2, che funge da maschera, deve contenere 1 bit al posto di quei bit che devono essere impostati a 1 in operando_XNUMX;

2) per reimpostare determinate cifre (bit) su 0, vengono utilizzati il ​​comando e operando_1, operando_2.

In questa istruzione, operando_2, che funge da maschera, deve contenere zero bit al posto di quei bit che devono essere impostati a 0 in operando_1;

3) viene applicato il comando xor operando_1, operando_2:

a) per scoprire quali bit nell'operando_1 e nell'operando differiscono;

b) per invertire lo stato dei bit specificati nell'operando_1.

I bit di maschera di nostro interesse (operando_2) durante l'esecuzione del comando xor devono essere singoli, il resto deve essere zero;

Il comando test operand_1, operand_2 (check operand_1) viene utilizzato per verificare lo stato dei bit specificati.

I bit controllati di operando_1 nella maschera (operando_2) devono essere impostati su uno. L'algoritmo del comando di test è simile all'algoritmo del comando e, ma non modifica il valore di operando_1. Il risultato del comando è impostare il valore del flag zero zf:

1) se zf = 0, allora come risultato della moltiplicazione logica si ottiene un risultato zero, ovvero un bit unitario della maschera, che non corrispondeva al corrispondente bit unitario dell'operando;

2) se zf = 1, allora come risultato della moltiplicazione logica si ottiene un risultato diverso da zero, cioè almeno un bit unitario della maschera coincide con il corrispondente bit unitario di operando_1.

Per reagire al risultato del comando di test, è consigliabile utilizzare il comando di salto jnz label (Salta se non zero) - salta se il flag zero zf è diverso da zero, oppure il comando di azione inversa - jz label (Salta se zero ) - salta se il flag zero zf = 0.

I due comandi seguenti cercano il primo bit dell'operando impostato su 1. La ricerca può essere eseguita sia dall'inizio che dalla fine dell'operando:

1) bsf operand_1, operand_2 (Bit Scanning Forward) - scansione dei bit in avanti. L'istruzione ricerca (scansiona) i bit dell'operando_2 dal meno significativo al più significativo (dal bit 0 al bit più significativo) alla ricerca del primo bit impostato a 1. Se ne trova uno, operando_1 viene riempito con il numero di questo bit come valore intero. Se tutti i bit di operando_2 sono 0, allora il flag zero zf viene impostato a 1, altrimenti il ​​flag zf viene reimpostato a 0;

2) bsr operand_1, operand_2 (Bit Scanning Reset) - scansionano i bit in ordine inverso. L'istruzione ricerca (scansiona) i bit dell'operando_2 dal più significativo al meno significativo (dal bit più significativo al bit 0) alla ricerca del primo bit impostato a 1. Se ne trova uno, operando_1 viene riempito con il numero di questo bit come valore intero. È importante che la posizione del primo bit dell'unità a sinistra sia ancora conteggiata rispetto al bit 0. Se tutti i bit di operando_2 sono 0, il flag zero zf viene impostato su 1, altrimenti il ​​flag zf viene reimpostato su 0.

Negli ultimi modelli di microprocessori Intel, nel gruppo di istruzioni logiche sono apparse alcune istruzioni in più che consentono di accedere a un bit specifico dell'operando. L'operando può essere in memoria o in un registro generale. La posizione del bit è data dall'offset del bit relativo al bit meno significativo dell'operando. Il valore di offset può essere specificato come valore diretto o contenuto in un registro di uso generale. È possibile utilizzare i risultati dei comandi bsr e bsf come valore di offset. Tutte le istruzioni assegnano il valore del bit selezionato al flag CE.

1) operando bt, bit_offset (Bit Test) - bit test. L'istruzione trasferisce il valore del bit al flag cf;

2) operando bts, offset_bit (Bit Test and Set) - verifica e impostazione di un bit. L'istruzione trasferisce il valore del bit al flag CF e quindi imposta il bit da controllare a 1;

3) btr operand, bit_offset (Bit Test and Reset) - verifica e ripristino di un bit. L'istruzione trasferisce il valore del bit al flag CF e quindi imposta questo bit a 0;

4) operando btc, offset_bit (Bit Test and Convert) - verifica e inversione di un bit. L'istruzione racchiude il valore di un bit nel flag cf e quindi inverte il valore di quel bit.

Comandi di spostamento

Le istruzioni in questo gruppo forniscono anche la manipolazione dei singoli bit degli operandi, ma in un modo diverso rispetto alle istruzioni logiche discusse sopra.

Tutte le istruzioni di spostamento spostano i bit nel campo dell'operando a sinistra oa destra a seconda del codice operativo. Tutte le istruzioni di turno hanno la stessa struttura: copia operando, shift_count.

Il numero di bit da spostare - counter_shifts - si trova al posto del secondo operando e può essere impostato in due modi:

1) staticamente, che comporta l'impostazione di un valore fisso tramite un operando diretto;

2) dinamicamente, il che significa inserire il valore del contatore di spostamento nel registro cl prima di eseguire l'istruzione di spostamento.

In base alla dimensione del registro cl, è chiaro che il valore dello shift counter può variare da 0 a 255. Ma in realtà questo non è del tutto vero. Ai fini dell'ottimizzazione, il microprocessore accetta solo il valore dei cinque bit meno significativi del contatore, ovvero il valore è compreso tra 0 e 31.

Tutte le istruzioni di turno impostano la bandiera di trasporto cfr.

Quando i bit si spostano fuori dall'operando, prima colpiscono il flag di riporto, impostandolo uguale al valore del bit successivo al di fuori dell'operando. La posizione successiva di questo bit dipende dal tipo di istruzione di spostamento e dall'algoritmo del programma.

I comandi di spostamento possono essere suddivisi in due tipi in base al principio di funzionamento:

1) comandi di spostamento lineare;

2) comandi di spostamento ciclico.

Comandi di spostamento lineare

I comandi di questo tipo includono comandi che si spostano in base al seguente algoritmo:

1) il prossimo bit che viene premuto imposta il flag CF;

2) il bit inserito nell'operando dall'altra estremità ha il valore 0;

3) quando il bit successivo viene spostato, va nel flag CF, mentre il valore del bit precedente viene perso! I comandi di spostamento lineare sono divisi in due sottotipi:

1) comandi logici di spostamento lineare;

2) istruzioni di spostamento lineare aritmetico.

I comandi logici di spostamento lineare includono quanto segue:

1) shl operando, counter_shifts (Shift Logical Left) - spostamento logico a sinistra. Il contenuto dell'operando viene spostato a sinistra del numero di bit specificato da shift_count. A destra (nella posizione del bit meno significativo) si inseriscono gli zeri;

2) shr operando, shift_count (Shift Logical Right) - spostamento logico a destra. Il contenuto dell'operando viene spostato a destra del numero di bit specificato da shift_count. A sinistra (nella posizione del bit di segno più significativo) vengono inseriti degli zeri.

La Figura 30 mostra come funzionano questi comandi.

Riso. 30. Schema di lavoro dei comandi di spostamento logico lineare

Le istruzioni di spostamento lineare aritmetico differiscono dalle istruzioni di spostamento logico in quanto operano sul bit di segno dell'operando in un modo speciale.

1) sal operando, shift_counter (Shift Arithmetic Left) - spostamento aritmetico a sinistra. Il contenuto dell'operando viene spostato a sinistra del numero di bit specificato da shift_count. A destra (nella posizione del bit meno significativo) vengono inseriti degli zeri. L'istruzione sal non preserva il segno, ma imposta il flag con / in caso di cambio di segno del bit successivo anticipato. In caso contrario, il comando sal è esattamente lo stesso del comando shl;

2) operando sar, shift_count (Shift aritmetica a destra) - spostamento aritmetico a destra. Il contenuto dell'operando viene spostato a destra del numero di bit specificato da shift_count. Gli zeri vengono inseriti nell'operando a sinistra. Il comando sar conserva il segno, ripristinandolo dopo ogni spostamento di bit.

La Figura 31 mostra come funzionano le istruzioni di spostamento aritmetico lineare.

Riso. 31. Schema di funzionamento dei comandi di spostamento aritmetico lineare

Comandi di rotazione

Le istruzioni di spostamento ciclico includono istruzioni che memorizzano i valori dei bit spostati. Esistono due tipi di istruzioni di spostamento ciclico:

1) semplici comandi di spostamento ciclico;

2) comandi di spostamento ciclico tramite il flag di riporto cfr.

Semplici comandi di spostamento ciclico includono:

1) rol operando, shift_counter (Ruota a sinistra) - spostamento ciclico a sinistra. Il contenuto dell'operando viene spostato a sinistra del numero di bit specificato dall'operando shift_count. I bit spostati a sinistra vengono scritti nello stesso operando da destra;

2) gog operando, counter_shifts (Ruota a destra) - spostamento ciclico a destra. Il contenuto dell'operando viene spostato a destra del numero di bit specificato dall'operando shift_count. I bit spostati a destra vengono scritti nello stesso operando a sinistra.

Riso. 32. Schema di funzionamento dei comandi di un semplice spostamento ciclico

Come si può vedere dalla Figura 32, le istruzioni di un semplice spostamento ciclico nel corso del loro lavoro svolgono un'azione utile, ovvero: il bit spostato ciclicamente non solo viene spinto nell'operando dall'altra estremità, ma allo stesso tempo viene value diventa il valore del flag CE.

I comandi di spostamento ciclico tramite il flag di riporto CF differiscono dai semplici comandi di spostamento ciclico in quanto il bit spostato non entra immediatamente nell'operando dall'altra estremità, ma viene prima scritto nel flag di riporto CE Solo la successiva esecuzione di questo comando di spostamento ( a condizione che venga eseguito in loop) fa posizionare il bit precedentemente anticipato all'altra estremità dell'operando (Fig. 33).

Quanto segue è correlato ai comandi di spostamento ciclico tramite il flag di riporto:

1) rcl operando, shift_count (Ruota attraverso Carry Left) - spostamento ciclico a sinistra tramite carry.

Il contenuto dell'operando viene spostato a sinistra del numero di bit specificato dall'operando shift_count. I bit spostati a loro volta diventano il valore del flag di riporto cfr.

2) rsg operando, shift_count (Ruota attraverso Carry Right) - spostamento ciclico a destra attraverso un carry.

Il contenuto dell'operando viene spostato a destra del numero di bit specificato dall'operando shift_count. I bit spostati a loro volta diventano il valore del flag di riporto CF.

Riso. 33. Ruota le istruzioni tramite Carry Flag CF

La figura 33 mostra che quando si passa attraverso il flag di riporto, appare un elemento intermedio, con l'aiuto del quale, in particolare, è possibile sostituire i bit spostati ciclicamente, in particolare il mismatch di sequenze di bit.

Di seguito, mancata corrispondenza di una sequenza di bit indica un'azione che consente in qualche modo di localizzare ed estrarre le sezioni necessarie di questa sequenza e di scriverle in un altro luogo.

Comandi di turno aggiuntivi

Il sistema di comando degli ultimi modelli di microprocessori Intel, a partire dall'i80386, contiene comandi di spostamento aggiuntivi che espandono le capacità di cui abbiamo discusso in precedenza. Questi sono i comandi di cambio a doppia precisione:

1) shld operand_1, operand_2, shift_counter - spostamento a sinistra a doppia precisione. Il comando shld esegue una sostituzione spostando i bit di operando_1 a sinistra, riempiendo i suoi bit di destra con i valori dei bit spostati da operando_2 secondo lo schema di Fig. 34. Il numero di bit da spostare è determinato dal valore shift_counter, che può essere compreso tra 0 e 31. Questo valore può essere specificato come operando immediato o contenuto nel registro cl. Il valore di operando_2 non viene modificato.

Riso. 34. Lo schema del comando shld

2) shrd operand_1, operand_2, shift_counter - spostamento a destra a doppia precisione. L'istruzione esegue la sostituzione spostando i bit dell'operando operando_1 a destra, riempiendo i suoi bit di sinistra con i valori dei bit spostati da operando_2 secondo lo schema di Figura 35. Il numero di bit da spostare è determinato dal valore di shift_counter, che può essere compreso tra 0 e 31. Questo valore può essere specificato dall'operando immediato o contenuto nel registro cl. Il valore di operando_2 non viene modificato.

Riso. 35. Lo schema del comando shrd

Come abbiamo notato, i comandi shld e shrd si spostano fino a 32 bit, ma a causa delle particolarità di specificare gli operandi e l'algoritmo operativo, questi comandi possono essere utilizzati per lavorare con campi lunghi fino a 64 bit.

2. Comandi di trasferimento del controllo

Abbiamo conosciuto alcuni comandi da cui si formano le sezioni lineari del programma. Ciascuno di essi generalmente esegue una conversione o un trasferimento di dati, dopodiché il microprocessore trasferisce il controllo all'istruzione successiva. Ma pochissimi programmi funzionano in modo così coerente. Di solito ci sono punti in un programma in cui è necessario prendere una decisione su quale istruzione verrà eseguita successivamente. Questa soluzione potrebbe essere:

1) incondizionato - a questo punto, è necessario trasferire il controllo non al comando che viene dopo, ma a un altro, che è a una certa distanza dal comando corrente;

2) condizionale: la decisione su quale comando verrà eseguito successivamente viene presa in base all'analisi di alcune condizioni o dati.

Un programma è una sequenza di comandi e dati che occupano una certa quantità di spazio nella RAM. Questo spazio di memoria può essere contiguo o essere costituito da più frammenti.

Quale istruzione di programma deve essere eseguita successivamente, il microprocessore apprende dal contenuto del cs: (e) coppia di registri ip:

1) cs - registro del segmento di codice, che contiene l'indirizzo fisico (di base) del segmento di codice corrente;

2) eip/ip - registro del puntatore dell'istruzione, che contiene un valore che rappresenta l'offset in memoria della successiva istruzione da eseguire rispetto all'inizio del segmento di codice corrente.

Il particolare registro che verrà utilizzato dipende dalla modalità di indirizzamento impostata use16 o use32. Se viene specificato use 16, viene utilizzato ip, se use32, viene utilizzato eip.

Pertanto, le istruzioni di trasferimento del controllo modificano il contenuto dei registri cs ed eip / ip, di conseguenza il microprocessore seleziona per l'esecuzione non l'istruzione del programma successiva nell'ordine, ma l'istruzione in qualche altra sezione del programma. La pipeline all'interno del microprocessore viene ripristinata.

Secondo il principio di funzionamento, i comandi del microprocessore che forniscono l'organizzazione delle transizioni nel programma possono essere suddivisi in 3 gruppi:

1. Trasferimento incondizionato dei comandi di controllo:

1) un comando di diramazione incondizionato;

2) un comando per chiamare una procedura e tornare da una procedura;

3) un comando per chiamare gli interrupt software e tornare dagli interrupt software.

2. Comandi per il passaggio condizionato del controllo:

1) comandi di salto dal risultato del comando di confronto p;

2) comandi di transizione in base allo stato di una determinata bandiera;

3) istruzioni per saltare il contenuto del registro esx/cx.

3. Comandi di controllo del ciclo:

1) un comando per organizzare un ciclo con un contatore ехх/сх;

2) un comando per organizzare un ciclo con un contatore ех/сх con possibilità di uscita anticipata dal ciclo con una condizione aggiuntiva.

Salti incondizionati

La discussione precedente ha rivelato alcuni dettagli del meccanismo di transizione. Le istruzioni di salto modificano il registro del puntatore dell'istruzione eip/ip ed eventualmente il registro del segmento di codice cs. Ciò che deve essere modificato esattamente dipende da:

1) sul tipo di operando nell'istruzione di ramo incondizionato (vicino o lontano);

2) di specificare un modificatore prima dell'indirizzo di salto (nell'istruzione di salto); in questo caso l'indirizzo di salto stesso può trovarsi sia direttamente nell'istruzione (salto diretto), sia in un registro o cella di memoria (salto indiretto).

Il modificatore può assumere i seguenti valori:

1) vicino a ptr - passaggio diretto a un'etichetta all'interno del segmento di codice corrente. Viene modificato solo il registro eip/ip (a seconda del tipo di segmento di codice use16 o use32 specificato) in base all'indirizzo (etichetta) specificato nel comando o in un'espressione che utilizza il simbolo di estrazione del valore - $;

2) far ptr - transizione diretta a un'etichetta in un altro segmento di codice. L'indirizzo di salto è specificato come operando immediato o indirizzo (etichetta) ed è costituito da un selettore a 16 bit e un offset a 16/32 bit, che vengono caricati rispettivamente nei registri cs e ip/eip;

3) parola ptr - transizione indiretta a un'etichetta all'interno del segmento di codice corrente. Viene modificato solo eip/ip (dal valore di offset dalla memoria all'indirizzo specificato nel comando o da un registro). Dimensione offset 16 o 32 bit;

4) dword ptr - transizione indiretta a un'etichetta in un altro segmento di codice. Entrambi i registri - cs ed eip / ip - vengono modificati (di un valore dalla memoria - e solo dalla memoria, da un registro). La prima word/dword di questo indirizzo rappresenta l'offset e viene caricata in ip/eip; la seconda/terza parola viene caricata in cs. jmp istruzione di salto incondizionato

La sintassi del comando per un salto incondizionato è jmp [modifier] jump_address - un salto incondizionato senza salvare le informazioni sul punto di ritorno.

Jump_address è l'indirizzo sotto forma di etichetta o l'indirizzo dell'area di memoria in cui si trova il puntatore di salto.

In totale, nel sistema di istruzioni del microprocessore sono presenti diversi codici di istruzioni macchina per il salto incondizionato jmp.

Le loro differenze sono determinate dalla distanza di transizione e dal modo in cui viene specificato l'indirizzo di destinazione. La distanza di salto è determinata dalla posizione dell'operando jump_address. Questo indirizzo potrebbe trovarsi nel segmento di codice corrente o in un altro segmento. Nel primo caso, la transizione è chiamata intra-segmento, o chiusa, nel secondo - inter-segmento o distante. Un salto all'interno del segmento presuppone che vengano modificati solo i contenuti del registro eip/ip.

Sono disponibili tre opzioni per l'uso all'interno del segmento del comando jmp:

1) dritto corto;

2) dritto;

3) indiretto.

Процедуры

Il linguaggio assembly ha diversi strumenti che risolvono il problema della duplicazione di sezioni di codice. Questi includono:

1) meccanismo delle procedure;

2) macroassemblatore;

3) meccanismo di interruzione.

Una procedura, spesso chiamata anche subroutine, è l'unità funzionale di base per scomporre (dividere in più parti) un compito. Una procedura è un gruppo di comandi per risolvere una sottoattività specifica e ha i mezzi per ricevere il controllo dal punto in cui l'attività viene chiamata a un livello superiore e restituire il controllo a questo punto.

Nel caso più semplice, il programma può consistere in un'unica procedura. In altre parole, una procedura può essere definita come un insieme ben formato di comandi che, essendo descritti una volta, possono essere richiamati in qualsiasi punto del programma, se necessario.

Per descrivere una sequenza di comandi come una procedura in linguaggio assembly, vengono utilizzate due direttive: PROC e ENDP.

La sintassi della descrizione della procedura è la seguente (Fig. 36).

Riso. 36. Sintassi della descrizione della procedura nel programma

La Figura 36 mostra che nell'intestazione della procedura (direttiva PROC), solo il nome della procedura è obbligatorio. Tra il gran numero di operandi della direttiva PROC, va evidenziato [distanza]. Questo attributo può assumere i valori vicini o lontani e caratterizza la possibilità di chiamare la procedura da un altro segmento di codice. Per impostazione predefinita, l'attributo [distanza] è impostato su vicino.

La procedura può essere posizionata in qualsiasi punto del programma, ma in modo tale da non ottenere il controllo in modo casuale. Se la procedura viene semplicemente inserita nel flusso di istruzioni generale, il microprocessore percepirà le istruzioni della procedura come parte di questo flusso e, di conseguenza, eseguirà le istruzioni della procedura.

Salti condizionali

Il microprocessore ha 18 istruzioni di salto condizionale. Questi comandi consentono di controllare:

1) la relazione tra operandi con segno ("maggiore - minore");

2) la relazione tra operandi senza segno ("superiore - inferiore");

3) stati delle bandiere aritmetiche ZF, SF, CF, OF, PF (ma non AF).

I comandi di salto condizionale hanno la stessa sintassi:

jcc jump_label

Come puoi vedere, il codice mnemonico di tutti i comandi inizia con "j" - dalla parola jump (salto), esso - determina la condizione specifica analizzata dal comando.

Come per l'operando jump_label, questa etichetta può trovarsi solo all'interno del segmento di codice corrente; non è consentito il trasferimento del controllo tra segmenti nei salti condizionati. A questo proposito non c'è dubbio sul modificatore, che era presente nella sintassi dei comandi di salto incondizionato. Nei primi modelli di microprocessore (i8086, i80186 e i80286), le istruzioni di diramazione condizionale potevano eseguire solo brevi salti - da -128 a +127 byte dall'istruzione che segue l'istruzione di diramazione condizionale. A partire dal modello di microprocessore 80386, questa restrizione viene rimossa, ma, come puoi vedere, solo all'interno del segmento di codice corrente.

Per prendere una decisione su dove trasferire il controllo al comando di salto condizionale, è necessario prima formare una condizione sulla base della quale verrà presa la decisione di trasferire il controllo.

Le fonti di tale condizione possono essere:

1) qualsiasi comando che modifichi lo stato dei flag aritmetici;

2) l'istruzione di confronto p, che confronta i valori di due operandi;

3) lo stato del registro esx/cx.

comando di confronto cmp

Il comando page compare ha un modo interessante di lavorare. È esattamente lo stesso del comando di sottrazione - sottooperando, operando_2.

L'istruzione p, come l'istruzione sub, sottrae operandi e imposta flag. L'unica cosa che non fa è scrivere il risultato della sottrazione al posto del primo operando.

La sintassi del comando str - str operand_1, operand_2 (confronta) - confronta due operandi e imposta i flag in base ai risultati del confronto.

I flag impostati dal comando p possono essere analizzati da speciali istruzioni di branch condizionali. Prima di esaminarli, prestiamo un po' di attenzione ai mnemonici di queste istruzioni di salto condizionale (Tabella 16). Comprendere la notazione quando si forma il nome dei comandi di salto condizionale (l'elemento nel nome del comando jcc, lo abbiamo designato) ne faciliterà la memorizzazione e un ulteriore utilizzo pratico.

Tabella 16. Significato delle abbreviazioni nel nome del comando jcc Tabella 17. Elenco dei comandi di salto condizionale per il comando p operando_1, operando_2

Non sorprendere il fatto che diversi codici mnemonici di comandi di branch condizionali corrispondano agli stessi valori di flag (sono separati l'uno dall'altro da una barra nella Tabella 17). La differenza di nome è dovuta al desiderio degli sviluppatori di microprocessori di rendere più semplice l'uso delle istruzioni di salto condizionale in combinazione con determinati gruppi di istruzioni. Pertanto, nomi diversi riflettono un orientamento funzionale piuttosto diverso. Tuttavia, il fatto che questi comandi rispondano agli stessi flag li rende assolutamente equivalenti e uguali nel programma. Pertanto, nella Tabella 17 sono raggruppati non per nome, ma per i valori dei flag (condizioni) a cui rispondono.

Istruzioni e flag del ramo condizionale

La designazione mnemonica di alcune istruzioni di salto condizionale riflette il nome della bandiera con cui funzionano e ha la seguente struttura: il primo carattere è "j" (salta, salta), il secondo è la designazione della bandiera o il carattere di negazione " n", seguito dal nome della bandiera . Questa struttura di squadra riflette il suo scopo. Se non è presente il carattere "n", viene verificato lo stato della bandiera, se è uguale a 1 viene eseguita una transizione all'etichetta di salto. Se è presente il carattere "n", lo stato di flag viene verificato per l'uguaglianza a 0 e, in caso di successo, viene eseguito un salto all'etichetta di salto.

I mnemonici dei comandi, i nomi dei flag e le condizioni di salto sono mostrati nella Tabella 18. Questi comandi possono essere utilizzati dopo qualsiasi comando che modifichi i flag specificati.

Tabella 18. Istruzioni di salto condizionale e flag

Se osservi attentamente le tabelle 17 e 18, puoi vedere che molte delle istruzioni di salto condizionale in esse contenute sono equivalenti, poiché entrambe si basano sull'analisi degli stessi flag.

Istruzioni di salto condizionale e registro esx/cx

L'architettura del microprocessore prevede l'uso specifico di molti registri. Ad esempio, il registro EAX / AX / AL viene utilizzato come accumulatore e i registri BP, SP vengono utilizzati per lavorare con lo stack. Il registro ECX / CX ha anche un certo scopo funzionale: funge da contatore nei comandi di controllo del loop e quando si lavora con stringhe di caratteri. È possibile che funzionalmente l'istruzione branch condizionale associata al registro esx/cx venga attribuita più correttamente a questo gruppo di istruzioni.

La sintassi per questa istruzione branch condizionale è:

1) jcxz jump_label (Salta se ex è Zero) - salta se cx è zero;

2) jecxz jump_label (salta uguale a zero) - salta se è zero.

Questi comandi sono molto utili durante il ciclo e quando si lavora con stringhe di caratteri.

Va notato che esiste una limitazione inerente al comando jcxz/jecxz. A differenza di altre istruzioni di trasferimento condizionale, l'istruzione jcxz/jecxz può indirizzare solo salti brevi -128 byte o +127 byte dall'istruzione successiva.

Organizzazione dei cicli

Il ciclo, come sapete, è una struttura algoritmica importante, senza la quale, probabilmente, nessun programma può fare. È possibile organizzare l'esecuzione ciclica di una determinata sezione del programma, ad esempio, utilizzando il trasferimento condizionato dei comandi di controllo o il comando di salto incondizionato jmp. Con una tale organizzazione del ciclo, tutte le operazioni per la sua organizzazione vengono eseguite manualmente. Ma, data l'importanza di un tale elemento algoritmico come un ciclo, gli sviluppatori del microprocessore hanno introdotto un gruppo di tre comandi nel sistema di istruzioni, che facilita la programmazione dei cicli. Queste istruzioni usano anche il registro esx/cx come contatore di loop.

Diamo una breve descrizione di questi comandi:

1) ciclo transizione_etichetta (Loop) - ripetere il ciclo. Il comando permette di organizzare cicli simili ai cicli for nei linguaggi di alto livello con decremento automatico del contatore dei cicli. Il compito del team è quello di fare quanto segue:

a) decremento del registro ECX/CX;

b) confronto del registro ECX/CX con zero: se (ECX/CX) = 0, allora il controllo viene trasferito al comando successivo al loop;

2) loope/loopz jump_label

I comandi loope e loopz sono sinonimi assoluti. Il lavoro dei comandi consiste nell'eseguire le seguenti azioni:

a) decremento del registro ECX/CX;

b) confrontare il registro ECX/CX con zero;

c) analisi dello stato del flag zero ZF se (ECX/CX) = 0 o XF = 0, il controllo viene trasferito al comando successivo dopo il loop.

3) loopne/loopnz jump_label

Anche i comandi loopne e loopnz sono sinonimi assoluti. Il lavoro dei comandi consiste nell'eseguire le seguenti azioni:

a) decremento del registro ECX/CX;

b) confrontare il registro ECX/CX con zero;

c) analisi dello stato del flag zero ZF: se (ECX/CX) = 0 o ZF = 1, il controllo viene trasferito al comando successivo al loop.

I comandi loope/loopz e loopne/loopnz sono reciproci nel loro funzionamento. Estendono l'azione del comando loop analizzando ulteriormente il flag zf, che consente di organizzare un'uscita anticipata dal loop, utilizzando questo flag come indicatore.

Lo svantaggio dei comandi di loop loop, loope/loopz e loopne/loopnz è che implementano solo salti brevi (da -128 a +127 byte). Per lavorare con i loop lunghi, dovrai usare i salti condizionali e l'istruzione jmp, quindi prova a padroneggiare entrambi i modi di organizzare i loop.

Autore: Tsvetkova A.V.

Ti consigliamo articoli interessanti sezione Appunti delle lezioni, cheat sheet:

Economia mondiale. Culla

Attività di ricerca operativa. Culla

Investimenti. Note di lettura

Vedi altri articoli sezione Appunti delle lezioni, cheat sheet.

Leggere e scrivere utile commenti su questo articolo.

<< Indietro

Ultime notizie di scienza e tecnologia, nuova elettronica:

Pelle artificiale per l'emulazione del tocco 15.04.2024

In un mondo tecnologico moderno in cui la distanza sta diventando sempre più comune, mantenere la connessione e un senso di vicinanza è importante. I recenti sviluppi nella pelle artificiale da parte di scienziati tedeschi dell’Università del Saarland rappresentano una nuova era nelle interazioni virtuali. Ricercatori tedeschi dell'Università del Saarland hanno sviluppato pellicole ultrasottili in grado di trasmettere la sensazione del tatto a distanza. Questa tecnologia all’avanguardia offre nuove opportunità di comunicazione virtuale, soprattutto per coloro che si trovano lontani dai propri cari. Le pellicole ultrasottili sviluppate dai ricercatori, spesse appena 50 micrometri, possono essere integrate nei tessuti e indossate come una seconda pelle. Queste pellicole funzionano come sensori che riconoscono i segnali tattili di mamma o papà e come attuatori che trasmettono questi movimenti al bambino. Il tocco dei genitori sul tessuto attiva i sensori che reagiscono alla pressione e deformano la pellicola ultrasottile. Questo ... >>

Lettiera per gatti Petgugu Global 15.04.2024

Prendersi cura degli animali domestici può spesso essere una sfida, soprattutto quando si tratta di mantenere pulita la casa. È stata presentata una nuova interessante soluzione della startup Petgugu Global, che semplificherà la vita ai proprietari di gatti e li aiuterà a mantenere la loro casa perfettamente pulita e in ordine. La startup Petgugu Global ha presentato una toilette per gatti unica nel suo genere in grado di scaricare automaticamente le feci, mantenendo la casa pulita e fresca. Questo dispositivo innovativo è dotato di vari sensori intelligenti che monitorano l'attività della toilette del tuo animale domestico e si attivano per pulirlo automaticamente dopo l'uso. Il dispositivo si collega alla rete fognaria e garantisce un'efficiente rimozione dei rifiuti senza necessità di intervento da parte del proprietario. Inoltre, la toilette ha una grande capacità di stoccaggio degli scarichi, che la rende ideale per le famiglie con più gatti. La ciotola per lettiera per gatti Petgugu è progettata per l'uso con lettiere idrosolubili e offre una gamma di accessori aggiuntivi ... >>

L'attrattiva degli uomini premurosi 14.04.2024

Lo stereotipo secondo cui le donne preferiscono i "cattivi ragazzi" è diffuso da tempo. Tuttavia, una recente ricerca condotta da scienziati britannici della Monash University offre una nuova prospettiva su questo tema. Hanno esaminato il modo in cui le donne hanno risposto alla responsabilità emotiva degli uomini e alla volontà di aiutare gli altri. I risultati dello studio potrebbero cambiare la nostra comprensione di ciò che rende gli uomini attraenti per le donne. Uno studio condotto da scienziati della Monash University porta a nuove scoperte sull'attrattiva degli uomini nei confronti delle donne. Nell'esperimento, alle donne sono state mostrate fotografie di uomini con brevi storie sul loro comportamento in varie situazioni, inclusa la loro reazione all'incontro con un senzatetto. Alcuni uomini hanno ignorato il senzatetto, mentre altri lo hanno aiutato, ad esempio comprandogli del cibo. Uno studio ha scoperto che gli uomini che mostravano empatia e gentilezza erano più attraenti per le donne rispetto agli uomini che mostravano empatia e gentilezza. ... >>

Notizie casuali dall'Archivio

Storia della polvere africana 31.07.2021

Il gruppo di ricerca, guidato da un professore onorario della Scuola di Scienze Marine e dell'Atmosfera. Rosenstiel dell'Università di Miami (UM) di Joseph Prospero, racconta il trasporto di polvere africana, comprese tre "prime" scoperte indipendenti di polvere africana nei Caraibi negli anni '1950 e '1960.

Ogni anno, la polvere ricca di minerali del deserto del Sahara in Nord Africa viene sollevata nell'atmosfera dai venti e trasportata in un viaggio di 5000 miglia attraverso il Nord Atlantico fino alle Americhe. La polvere africana contiene ferro, fosforo e altri importanti nutrienti essenziali per la vita negli ecosistemi marini e terrestri, compreso il bacino amazzonico. Anche la polvere minerale portata dal vento svolge un ruolo importante nel clima modulando la radiazione solare e le proprietà delle nuvole.

I ricercatori discutono anche della scoperta negli anni '1970 e '1980 di un legame tra il trasporto di polvere e il clima africano dopo l'aumento del trasporto di polvere verso i Caraibi a causa dell'inizio di una grave siccità nel Sahel. Gran parte della ricerca sulla polvere di oggi si concentra sul Nord Africa, poiché è la fonte di polvere più grande e persistente sulla Terra.

Oggi Prospero, soprannominato il "Padre della polvere", utilizza un sistema di stazioni di terra e satelliti per studiare l'effetto del trasporto globale dal Sahara sulla composizione atmosferica del Mar dei Caraibi.

Altre notizie interessanti:

▪ Laser da combattimento di terza generazione

▪ Stuffcool Snap Lightning Power Bank per Apple

▪ Motore ionico X-3

▪ Il giorno più pericoloso dell'anno

▪ MFP a colori Konica Minolta bizhub C3, C458 e C558 A658

News feed di scienza e tecnologia, nuova elettronica

 

Materiali interessanti della Biblioteca Tecnica Libera:

▪ sezione del sito Audio Art. Selezione dell'articolo

▪ articolo Gente di buona volontà. Espressione popolare

▪ articolo Cos'è lo Zodiaco? Risposta dettagliata

▪ articolo L'ambiente aereo è la parte più importante dell'ambiente di lavoro che circonda il lavoratore

▪ articolo Equivalenza di antenne elettriche e magnetiche. Enciclopedia dell'elettronica radio e dell'ingegneria elettrica

▪ articolo Matita vivente. Messa a fuoco segreta

Lascia il tuo commento su questo articolo:

Nome:


E-mail (opzionale):


commento:





Tutte le lingue di questa pagina

Homepage | Biblioteca | Articoli | Mappa del sito | Recensioni del sito

www.diagram.com.ua

www.diagram.com.ua
2000-2024