ΠΕΡΙΕΧΟΜΕΝΑ

 

            - Η ΟΙΚΟΓΕΝΕΙΑ 8086/8088

            1. ΥΠΟΛΟΓΙΣΜΟΣ ΔΙΕΥΘΥΝΣΕΩΝ

            2. 16bit ΕΝΑΝΤΙΟΝ 24bit ΔΙΕΥΘΥΝΣΕΩΝ

            3. ΜΟΝΤΕΛΑ ΜΝΗΜΗΣ

                        α. Tiny Model

                        β. Small Model

                        γ. Medium Model

                        δ. Compact Model

                        ε. Large Model

                        στ. Huge Model

            - ΕΠΙΛΕΓΟΝΤΑΣ ΜΟΝΤΕΛΟ

            4. ΥΠΕΡΠΗΔΩΝΤΑΣ ΤΑ ΜΟΝΤΕΛΑ ΜΝΗΜΗΣ

                        α. Ο ΤΡΟΠΟΠΟΙΗΤΗΣ far

                        β. Ο ΤΡΟΠΟΠΟΙΗΤΗΣ near

                        γ. Ο ΤΡΟΠΟΠΟΙΗΤΗΣ huge

            5. ΚΑΘΟΡΙΣΤΕΣ ΤΜΗΜΑΤΟΣ

            6. ΟΛΟΚΛΗΡΩΜΕΝΑ ΠΑΡΑΔΕΙΓΜΑΤΑ

 

 

 

ΚΕΦΑΛΑΙΟ 11ο

 

 

ΜΟΝΤΕΛΑ ΜΝΗΜΗΣ ΤΗΣ TURBO C

 

 

Για αρκετούς λόγους που θα ξεκαθαρίσουμε παρακάτω, μπορούμε να μεταφράσουμε ένα πρόγραμμα C κάνοντας χρήση κάποιου από τα έξι (6) διαφορετικά μοντέλα που ορίζει η οικογένεια 8086. Κάθε μοντέλο οργανώνει τη μνήμη διαφορετικά και ελέγχει το μέγεθος του κώδικα ή των δεδομένων (ή και των δύο) με άλλο τρόπο. Επίσης καθορίζει και την ταχύτητα με την οποία θα εκτελείται ένα πρόγραμμα.

 

Στο κεφάλαιο αυτό θα εξεταστεί η χρήση της C με τους επεξεργαστές της οικογένειας 8086 / 8088. Η θεωρία και τα παραδείγματα δεν έχουν εφαρμογή σε άλλους επεξεργαστές.

 

 

¤           Η Οικογένεια 8086 / 8088

 

Πριν γίνει αναφορά στα μοντέλα μνήμης θα πρέπει να γίνει κατανοητός ο τρόπος με τον οποίο ο 8086 διευθυνσιοδοτεί τη μνήμη (από εδώ και στο εξής η CPU θα ονομάζεται 8086, αλλά οι πληροφορίες που θα δοθούν έχουν εφαρμογή και στους 8088, 80186, 80286, 80386, 80486 και Pentium, αλλά και στους συμβατούς με αυτούς πχ AMD, CYRIX, T.I. κτλ.).

 

O 8086 περιέχει 14 καταχωρητές όπου αποθηκεύονται πληροφορίες για την επεξεργασία ή τον έλεγχο του προγράμματος. Οι καταχωρητές διακρίνονται στις παρακάτω κατηγορίες:

 

-          Γενικής Χρήσεως Καταχωρητές

-          Base Pointer και Index Καταχωρητές

-          Segment Καταχωρητές

-          Ειδικοί Καταχωρητές

 

Όλοι οι καταχωρητές του 8086 έχουν εύρος 16 Bit, δηλαδή 2 Bytes.

 

Οι καταχωρητές γενικής χρήσης είναι οι χαμάληδες της CPU. Τιμές καταχωρούνται σ’ αυτούς τους καταχωρητές για επεξεργασία ή αριθμητικές πράξεις, όπως: προσθέσεις, πολλαπλασιασμοί, συγκρίσεις, λογικές πράξεις, άλματα, κλπ. Κάθε ένας από αυτούς τους καταχωρητές προσπελαύνεται είτε ως ένας 16-bit καταχωρητής, είτε ως δύο 8-bit καταχωρητές.

 

Ο Base Pointer και οι Intex Registers χρησιμοποιούνται σε καταστάσεις που έχουν σχέση με σχετική διευθυνσιοδότηση, τη στοίβα ή εντολές μετακίνησης block.

 

Οι Segment Registers χρησιμοποιούνται για υποστήριξη στο μοντέλο τμηματοποίησης της μνήμης από τον 8086. Ο CS κατά το τρέχον Code Segment, ο DS κατά το τρέχον Data Segment, ο ES κατά το τρέχον Extra Segment και ο SS το Stack Segment.

 

Ειδικός καταχωρητής είναι ο Flag Register που δείχνει ποια είναι η επόμενη προς εκτέλεση εντολή.

 

 

AX

BX

CX

DX

Accumulator

Base

Counter

Data

Συσσωρευτής

Βάση

Μετρητής

Δεδομένα

 

ΓΕΝΙΚΟΥ

ΣΚΟΠΟΥ

SI

DI

Source Index

Dest. Index

Πηγαίο Ευρετήριο

Ευρ. Προορισμού

ΕΥΡΕΤΗΡΙΟΥ

SP

BP

Stack Pointer

Base Pointer

Δείκτης Σωρού

Δείκτης Βάσης

ΣΩΡΟΥ

CS

DS

SS

ES

Code Segment

Data Segment

Stack Segment

Extra Segment

Τμήμα Κώδικα

Τμήμα Δεδομένων

Τμήμα Σωρού

Τμήμα Extra

ΤΜΗΜΑΤΟΣ

IP

FLAGS

Instruction Register

Flag Register

Δείκτης Εντολών

Σημάνσεις Λειτουργ.

ΕΙΔΙΚΟΙ

 

Οι καταχωρητές (Registers) του 8086

 

Bit

Ονομασία

Λειτουργία

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

CF

Unused

PF

Unused

AF

Unused

ZF

SF

TF

IF

PF

OF

Unused

Unused

Unused

Σήμανση Υπολοίπου

 

Σήμανση Ισοτιμίας

 

Βοηθητική Σήμανση Υπολοίπου

 

Σήμανση Μηδενός

Σήμανση Πρόσημου

Σήμανση Παγίδευσης

Σήμανση Διακοπής

Σήμανση Κατεύθυνσης

Σήμανση Υπερχείλισης

 

 

 

 

 

   Οι σημαίες (FLAGS) του 8086

 

 

i)                    Υπολογισμός Διευθύνσεων

 

Ο 8086 έχει συνολικό μέγεθος διευθύνσεων μνήμης 1ΜΒ. Για να επιτευχθεί προσπέλαση 1ΜΒ RAM απαιτείται δίαυλος 20-bit. Πάντως, στον 8086 δεν υπάρχει καταχωρητής μεγαλύτερος από 16 bit. Αυτό σημαίνει ότι η 20-bit διεύθυνση διαιρείται μεταξύ δύο καταχωρητών.

 

Στον 8086 όλες οι διευθύνσεις αποτελούνται από ένα segment και ένα offset. Στην πραγματικότητα ο τρόπος διευθυνσιοδότησης του 8086 καλείται μοντέλο segment : offset. Ένα segment είναι ένα τμήμα της RAM, μεγέθους 64Κ το οποίο πρέπει να ξεκινά από ένα άρτιο πολλαπλάσιο του 16.

 

Στη γλώσσα του 8086, 16 bytes ονομάζονται paragraph. Έτσι, ο όρος paragraph boundary χρησιμοποιείται πολλές φορές για αναφορά σε τέτοια πολλαπλάσια των 16 bytes.

 

Ο 8086 έχει 4 τμήματα. Ένα για τον κώδικα, ένα για τα δεδομένα, ένα για τη στοίβα, και ένα έξτρα. Η τοποθεσία ενός byte ανάμεσα στο τμήμα λέγεται offset. H απόλυτη (πραγματική διεύθυνση) ενός συγκεκριμένου byte μέσα στον υπολογιστή είναι ο συνδυασμός του segment και του offset.

 

Για να υπολογίσουμε το πραγματικό byte που αναφέρεται από τον συνδυασμό segment : offset πρέπει να εκτελέσουμε κύλιση της τιμής στον segment register κατά 4 bit αριστερά και κατόπιν να προσθέσουμε το offset. Αυτό μας επιστρέφει μία τιμή 20 bit. Π.χ. αν ο καταχωρητής τμήματος περιλαμβάνει την τιμή 0020h και το offset έχει την τιμή 0100h η απόλυτη τιμή στον δίαυλο 20 bit θα είναι 00300h.

 

Segment Register    :                                   0000  0000  0010 0000

Segment Shifted      :                         0000  0000  0010  0000

Offset                       :                                   0000  0001  0000  0000

Segment  +  Offset  :                         0000  0000  0011  0000  0000

 

Πάντως, στην περίπτωση που προσπελαύνουμε διευθύνσεις μόνο μέσα στο τρέχον τμήμα, τότε θα χρειαστεί να δουλέψουμε μόνο με το offset, οπότε συνήθως το segment δε μας απασχολεί. Σε γενικές γραμμές, μια διεύθυνση του τύπου segment  :  offset ενδέχεται να έχει την ίδια τιμή με μία άλλη, μια και τα τμήματα αλληλοκαλύπτονται μεταξύ τους. Π.χ. η διεύθυνση 0000:0010 έχει την ίδια τιμή με την 0001:0000.

 

 

i)                    16-Bit Εναντίον 20-Bit Pointers

 

Όπως ήδη έχει αναφερθεί θα χρειαστούμε μόνο 16-bit δ/νση για να προσπελάσουμε μνήμη μέσα στο ίδιο τμήμα. Πάντως, εάν θέλουμε να αναφερθούμε σε μνήμη έξω από το τρέχουν τμήμα, θα πρέπει να χρησιμοποιήσουμε και το segment και το offset με τις επιθυμητές τιμές στους αντίστοιχους καταχωρητές. Αυτό σημαίνει ότι θα χρειαστούμε μία τιμή εύρους 20-bit. Η χρήση μιας 20-bit δ/νσης συνεπάγεται τη φόρτωση και του 16-Bit Segment και του 16-Bit Offset. Σε γενικές γραμμές οι 20-Βit δ/νσεις κάνουν τα προγράμματα αργά, επιτρέπουν όμως τη δημιουργία μεγάλων προγραμμάτων ή τη χρήση πολλών δεδομένων.

 

 

ii)                  Μοντέλα Μνήμης

 

α)  Tiny Model

 

To Tiny Model μεταφράζει ένα πρόγραμμα θέτοντας όλους τους καταχωρητές τμήματος στην ίδια τιμή κάνοντας χρήση διευθύνσεων μόνο 16-Βit. Αυτό σημαίνει ότι ο κώδικας, τα δεδομένα και η στοίβα, θα βρίσκονται μέσα στο ίδιο τμήμα 64Κ. Η μέθοδος αυτή παράγει τα γρηγορότερα και μικρότερα προγράμματα. Γενικά, προγράμματα μεταφρασμένα με αυτή τη μέθοδο μπορούν να μετατραπούν σε .COM χρησιμοποιώντας την εντολή EXE2BIN του DOS. Υπενθυμίζεται ότι τα .COM προγράμματα χρησιμοποιούν μόνο ένα segment για όλες τις λειτουργίες τους.

Η εντολή EXE2BIN συντάσσεται:

 

                                 ΕΧΕ2ΒΙΝ   ProgName.EXE   ProgName.COM

 

β)  Small Model

 

Το Small Model είναι το μοντέλο που εξ’ ορισμού χρησιμοποιείται από τη C και είναι χρήσιμο σε μία μεγάλη γκάμα προβλημάτων. Παρ’ ότι η διευθυνσιοδότηση γίνεται με χρήση διευθύνσεων μεγέθους 16-Bit (Offset), το τμήμα κώδικα είναι ξεχωριστό από το τμήμα δεδομένων, της στοίβας, και το έξτρα (που βρίσκονται στο δικό τους τμήμα). Το συνολικό μέγεθος ενός τέτοιου προγράμματος είναι 128Κ, σπασμένο σε κώδικα και δεδομένα.

Ο χρόνος διευθυνσιοδότησης παραμένει ίδιος όπως και στο Tiny Model, αλλά ο κώδικας μπορεί να είναι διπλάσιος. Τα περισσότερα προγράμματα που γράφουμε, χωρούν σ’ αυτό το μοντέλο. Το Small Model παράγει Run-Times που πλησιάζουν σε ταχύτητα αυτά του Tiny.

 

 

γ) Medium Model

 

Το Medium Model προορίζεται για μεγάλα προγράμματα όπου ο κώδικας ξεπερνά τον περιορισμό του ενός segment. Ο κώδικας μπορεί να χρησιμοποιεί πολλά segments και απαιτεί χρήση 20-bit Pointers, αλλά η στοίβα, τα δεδομένα και το έξτρα, βρίσκονται στο δικό τους segment, κάνοντας χρήση 16-Bit δ/σεων. Αυτό είναι καλό για μεγάλα προγράμματα τα οποία χρησιμοποιούν λίγα δεδομένα. Προγράμματα μεταφρασμένα σ’ αυτό το μοντέλο είναι σχετικά αργά όταν αναφέρονται σε κώδικα και συναρτήσεις, αλλά σχετικά γρήγορα (όσο και στο Small Model) όταν αναφέρονται σε δεδομένα.

 

 

δ)  Compact Model

 

Το συμπαγές μοντέλο αποτελεί συμπλήρωμα του μεσαίου. Σ’ αυτή την περίπτωση ο κώδικας περιορίζεται στο ένα τμήμα, αλλά τα δεδομένα μπορεί να καταλαμβάνουν πολλά segments. Αυτό σημαίνει ότι όλες οι διευθύνσεις σε δεδομένα απαιτούν 20-Bit αλλά ο κώδικας χρησιμοποιεί 16-Bit διευθυνσιοδότηση. Αυτό είναι καλό για προγράμματα τα οποία απαιτούν μεγάλες ποσότητες δεδομένων αλλά λίγο κώδικα. Το πρόγραμμα θα είναι το ίδιο γρήγορο με το small model, αλλά θα γίνεται αργό όταν αναφέρεται σε δεδομένα.

 

ε)  Large Model

 

Το μεγάλο μοντέλο επιτρέπει στον κώδικα και τα δεδομένα να χρησιμοποιούν πολλά segments. Πάντως, τα συνολικά στατικά δεδομένα όπως οι πίνακες, περιορίζονται στα 64Κ. Το μοντέλο αυτό χρησιμοποιείται όταν έχουμε μεγάλες ποσότητες κώδικα και δεδομένων. Φυσικά τα προγράμματα αυτού του είδους είναι αρκετά πιο αργά από τα υπόλοιπα μοντέλα.

 

στ)  Huge Model

 

Το τεράστιο μοντέλο είναι το ίδιο με το Large με μόνη διαφορά ότι δεν υπάρχει περιορισμός στο σύνολο των στατικών δεδομένων. Αυτό κάνει τα Run-Time ακόμη πιο αργά.

 

 

 

¤            Επιλέγοντας Μοντέλο

 

Γενικά, θα χρησιμοποιήσουμε το small model αν υπάρχει λόγος να συμβεί το αντίθετο. Χρησιμοποιούμε το Medium αν έχουμε λίγα δεδομένα και πολύ κώδικα. Το Compact αν έχουμε πολλά δεδομένα και λίγο κώδικα. To Large αν έχουμε πολλά δεδομένα και κώδικα και τα στατικά μας δεδομένα δεν ξεπερνούν τα 64Κ. Αν ξεπερνούν τα 64Κ επιλέγουμε το Huge. Υπενθυμίζεται πάντως ότι το Large και το Huge παράγουν αργό κώδικα.

 

 

 

iii)                Υπερπηδώντας τα μοντέλα μνήμης

 

Θα ήταν ατυχές να αναγκαστούμε να χρησιμοποιήσουμε το compact μοντέλο (το οποίο θεωρείται αργότερο του small) σε περίπτωση που θέλαμε να κάνουμε μία μόνο αναφορά σε δεδομένα με 20-Bit διεύθυνση (π.χ. Video RAM). Η λύση σε τέτοιου είδους προβλήματα παρέχεται από την Turbo C κάνοντας χρήση των Segment Override Modifiers (Τροποποιητές Υπερπήδησης Τμήματος). Αυτοί είναι:

 

                                 far, near, huge

 

Οι τροποποιητές αυτοί προσαρμόζονται μόνο σε pointers ή συναρτήσεις. Όταν εφαρμόζονται σε pointers, επηρεάζουν τον τρόπο με τον οποίο προσπελαύνονται τα δεδομένα. Όταν εφαρμόζονται σε συναρτήσεις επηρεάζουν τον τρόπο κλήσης ή περιστροφής από συνάρτηση.

 

Οι τροποποιητές ακολουθούν το βασικό τύπο και προηγούνται του ονόματος της μεταβλητής. Π.χ.:

 

                                             char far  *fp;

 

Ορίζει ένα far pointer με όνομα fp.

 

α)  far

 

Ο κυριότερος τροποποιητής αυτού του είδους είναι ο far, επειδή είναι κοινότυπο να θέλουμε να προσπελάσουμε ένα τμήμα της μνήμης που βρίσκεται έξω από το τμήμα των δεδομένων. Φυσικά, όταν η μετάφραση έχει γίνει για large ή huge μοντέλο η προσπέλαση δεδομένων είναι αργή. Η λύση σ’ αυτό το πρόβλημα είναι να δηλώσουμε pointers ακριβώς στο σημείο που θέλουμε σαν far να μεταφράσουμε χρησιμοποιώντας το small μοντέλο. Μ’ αυτόν τον τρόπο μόνο αναφορές σε αντικείμενα έξω από το ορισμένο data segment χρησιμοποιούν την υπερπήδηση.

Η χρήση far συναρτήσεων είναι λιγότερο κοινή και γενικά απαγορεύεται σε ειδικές προγραμματιστικές περιπτώσεις. Συνήθως, χρησιμοποιείται για κλήση συναρτήσεων έξω από το τρέχον code segment, όπως ROM-Based ρουτίνες, κλπ. Σε τέτοιες περιπτώσεις η χρήση του far σιγουρεύει τη σωστή κλήση και επιστροφή.

 

Υπάρχουν δύο σοβαρά πράγματα που πρέπει να θυμόμαστε γύρω από τους far pointers στη C.

 

1)                                 Η αριθμητική των pointers επηρεάζει μόνο το offset. Αυτό σημαίνει ότι αν ένας pointer με τιμή 0000:FFFF αυξηθεί, η νέα του τιμή θα είναι 0000:0000 και όχι 0000:FFF0. Η τιμή του segment δεν αλλάζει ποτέ.

2)                                 Δύο far pointers δεν πρέπει να χρησιμοποιούνται σε μία σχεσιακή έκφραση γιατί μόνο τα offset θα ελεγχθούν. Είναι πιθανό να έχουμε δύο διαφορετικούς pointer που θα δείχνουν την ίδια φυσική διεύθυνση (στο δίαυλο) αλλά θα έχουν διαφορετικό segment και offset. Αν θέλουμε να συγκρίνουμε 20-Bit pointers θα πρέπει να χρησιμοποιήσουμε huge pointers. Πάντως, οι τελεστές == και != μπορούν να χρησιμοποιηθούν σε 20-Bit διευθύνσεις οπότε και ένας null pointer μπορεί να ανιχνευθεί.

 

β) near

 

Ένας near pointer είναι ένα 16-bit offset που χρησιμοποιεί την τιμή του κατάλληλου segment για να καθορίσει μία ακριβή διεύθυνση μνήμης.

 

Ο τροποποιητής near αναγκάζει την C να χειριστεί ένα pointer σαν ένα 16-bit offsset στο segment που δείχνει ο DS. Χρησιμοποιούμε τον τροποποιητή near όταν το πρόγραμμα έχει μεταφραστεί σε medium, large ή huge model και θέλουμε να το υπερπηδήσουμε. Εάν χρησιμοποιήσουμε τον τροποποιητή near σε συνάρτηση, αναγκάζουμε τη συνάρτηση να συμπεριφερθεί σα να είχαμε χρησιμοποιήσει το small model. Αν η συνάρτηση έχει μεταφραστεί κάνοντας χρήση του Tiny, Small ή Compact μοντέλου, όλες οι κλήσεις τοποθετούν μία τιμή 16-bit στη στοίβα. Μεταφράζοντας με Large μοντέλο, μία δ/νση του τύπου segment : offset εύρους 20-bit, θα γίνει push στη στοίβα.

 

γ) huge

 

O huge pointer είναι σαν τον far με δύο διαφορές:

 

1.                                 To segment του έχει ομαλοποιηθεί έτσι ώστε οι συγκρίσεις μεταξύ huge pointers να έχουν νόημα.

2.                                 Όταν ένας huge pointer αυξάνεται και το segment και το offset αλλάζουν. Οι huge pointers δεν υποφέρουν από την ανακύκλωση που εμφανίζεται στους far.

 

 

iv)               Καθοριστές Τμήματος

 

Η Turbo C υποστηρίζει τέσσερις τροποποιητές διευθύνσεων, τους: _cs, _ds, _ss, και _es. Όταν ένας από αυτούς δηλωθεί μπροστά από έναν pointer τότε ο pointer γίνεται ένα 16-bit offset στο συγκεκριμένο segment. Π.χ.:

 

                                             int_es  *ptr;

 

Ο pointer ptr θα δείχνει ένα offset στο extra segment. Γενικά, δε χρησιμοποιούνται και πολύ, παρά μόνο στις πολύ “εξωτικές” εφαρμογές.

 

 

v)                 Ολοκληρωμένα Παραδείγματα

 

 

H παρακάτω εφαρμογή εμφανίζει ένα τμήμα της μνήμης RAM και μας επιτρέπει να το δούμε ή να το αλλάξουμε. Το πρόγραμμα θα χρησιμοποιεί τον τροποποιητή far οπότε μπορούμε να αναφερθούμε σε οποιοδήποτε σημείο της μνήμης.

 

Η πρώτη συνάρτηση που θα δημιουργήσουμε είναι η display_mem( ) την οποία θα χρησιμοποιήσουμε για να εμφανίσουμε τα περιεχόμενα της μνήμης στη δ/νση που θα ζητήσει ο χρήστης. Στην αρχή ζητά να εισάγουμε μία τιμή 20-bit και εμφανίζει τα επόμενα 256 bytes.

 

Το αποτέλεσμα δείχνει 16 γραμμές με 16 bytes η καθεμία. Η διεύθυνση της κάθε γραμμής εμφανίζεται στα αριστερά της.

 

Η δεύτερη συνάρτηση είναι η change_mem( ). Χρησιμοποιείται για να επιτευχθεί η αλλαγή του περιεχομένου ενός συγκεκριμένου byte. Λειτουργεί ζητώντας της 20-bit διεύθυνση του byte και στη συνέχεια ζητά τη νέα τιμή του.

 

Το κυρίως πρόγραμμα ζητά ή την τιμή D ή την τιμή C εμφανίζοντας και το promt   <. Εάν πατηθεί Q η λειτουργία τερματίζεται.

 

 

/ * Display And / Or Change Memory Program * /

#include <stdlib.h>

#include <stdio.h>

void display_mem( ), change_mem( )

main( )

{

         char ch;

         for (; ;)   {

                  printf (“ < “);  / * Display Prompt  * /

                  ch = getche ( );   / * Read Command * /

                  printf (“ \ n”);

                  switch (tolower (ch) )   {

                           case ‘d’ : display_mem( );

                                          break;

                           case ‘c’ : change_mem( );

                                          break;

                           case ‘q’ : exit(0);

                  }

         }

}

void display_mem( )

{

         register int i;

         unsigned char ch;

         unsigned char far  *p;

         char s[80];

         printf(“Beginning Address (in HEX) : “);

         scanf (“%p%*c”, &p);

         scanf (“%p : “, p);

         for (i=1 ; i<=256 ; i++)   {

                  ch = *p;

                  printf (“%02x”, ch);

                  p++;

                  if (! ( i%16) )   {

                           printf (“ \ n “);

                           if (i! = 256) printf(“%p :”, p);

                  }

         }

}

void change_mem( )

{

         unsigned char far  *p;

         char s [80];

         char value;

         printf (“Enter Addres To Change (in HEX) : “);

         scanf ( “ %x”, &value);

         *p = (unsigned char) value;

}