Magaz, The Greek Linux Magazine
Magaz Logo

Επόμενο  Προηγούμενο  Περιεχόμενα

2. Εικονικές Μηχανές

2.1 Εισαγωγή

Η εικονική μηχανή (Virtual Μachine), όπως δηλώνει και το όνομα της, είναι μια μηχανή που υφίσταται μόνο στο βασίλειο του αφηρημένου. Πρόκειται για επεξεργαστή υλοποιημένο μόνο με λογισμικό, με δικό του σετ εντολών και ιδιαίτερων χαρακτηριστικών.

Μια προφανής και σημαντική χρήση των εικονικών μηχανών είναι η εξομοίωση και η μελέτη υπαρχόντων ή και υπό σχεδίαση hardware συστημάτων. Για αυτή την κατηγορία έχει επικρατήσει η ονομασία εξομοιωτής (emulator) και σήμερα για σχεδόν όλα τα υπολογιστικά συστήματα περασμένων εποχών υπάρχει ένας εξομοιωτής.

Ένας δεύτερος πολύ σημαντικός λόγος για την ύπαρξη VMs είναι η χρήση τους ως ένα "μονωτικό στρώμα" ανάμεσα στις εφαρμογές και το υλικό. Μια εφαρμογή που είναι σχεδιασμένη για μια VM μπορεί να εκτελεστεί σε οποιοδήποτε επεξεργαστή για τον οποίο υπάρχει ένας διερμηνευτής για τον κώδικα της VM. Αυτή είναι η νοοτροπία του "Προγραμμάτισε μια φορά, τρέξε παντού" (Code Once, Run Everywhere).

Πολύ συχνά οι εικονικές μηχανές χρησιμοποιούνται για να δώσουν στο προγραμματιστή την ψευδαίσθηση πως δουλεύει σε ένα ιδιαίτερα εξελιγμένο σύστημα, με χαρακτηριστικά ειδικά σχεδιασμένα για την εργασία του. Τέτοιες μηχανές χρησιμοποιούνται σε παιχνίδια όπου βασικές εντολές του εικονικού επεξεργαστή μπορεί να είναι για παράδειγμα "μετακίνησε το sprite A από τη θέση x στη θέση y".

Οι εικονικές μηχανές, αν και είναι πολύ στη μόδα σήμερα, δεν αποτελούν καινούργιο φαινόμενο. Ήδη από το 1970 η ανάγκη διαχωρισμού των διάφορων φάσεων της μεταγλώττισης ενός προγράμματος οδήγησε τους δημιουργούς μεταγλωττιστών στην εισαγωγή και χρήση ενδιάμεσων μορφών κώδικα. Μια πολύ γνωστή ενδιάμεση μορφή είναι η P-Code που χρησιμοποιήθηκε για τους μεταγλωττιστές της γλώσσας Pascal. Πολύ σύντομα η P-Code έπαψε να χρησιμοποιείται μόνο στους μεταγλωττιστές και έγινε η βάση μιας εικονική μηχανής για το σύστημα της UCSD Pascal.

Παρ' όλο που που η δεκαετία του '70 μοιάζει μακρινή, τα πράγματα δεν έχουν αλλάξει πολύ στον συγκεκριμένο τομέα. Οι σύγχρονες γλώσσες προγραμματισμού χρησιμοποιούν το ίδιο μοντέλο με μια μικρή αλλά σημαντική προσθήκη. Για λόγους ταχύτητας, οι πιο εξελιγμένοι διερμηνευτές δεν εκτελούν τον κώδικα σε ενδιάμεση μορφή αλλά τον μετατρέπουν στη γλώσσα μηχανής του επεξεργαστή στον οποίο τρέχουν (native code). Η μετατροπή αυτή γίνεται την ώρα της εκτέλεσης και ονομάζεται Just-In-Time (JIT) μεταγλώττιση, ενώ η κλασική μεταγλώττιση έχει την ονομασία Ahead-Of-Time (AOT).

2.2 Ενδιάμεσες Μορφές Κώδικα

Οι ενδιάμεσες μορφές κώδικα αλλά και γενικά τα σετ εντολών μπορούν να χωριστούν σε δύο μεγάλες κατηγορίες, ανάλογα με το πως τροφοδοτούνται οι εντολές με δεδομένα.

Στην πρώτη κατηγορία ανήκουν οι μορφές κώδικα στις οποίες τα δεδομένα ανταλλάσσονται μέσω καταχωρητών. Οι μηχανές που υλοποιούν αυτό το μοντέλο ονομάζονται register-based και αποτελούν τη πλειοψηφία των hardware επεξεργαστών. Η πράξη a=a+b*c σε ένα τέτοιο σύστημα έχει τη μορφή:

move t1, b          ;; t1=b
multiply t1, t1, c  ;; t1=t1*c
add a, a, t1        ;; a=a+t1

Αντίθετα, τα συστήματα όπου το μέσο ανταλλαγής δεδομένων είναι ο σωρός ονομάζονται stack-based. Ο σωρός αυτός έχει την ειδική ονομασία σωρός τελεστέων (operand stack). Οι περισσότερες εικονικές μηχανές ακολουθούν αυτό το μοντέλο διότι έχει αποδειχθεί ότι προσφέρει ταχύτερη εκτέλεση σε λογισμικό και είναι πιο απλή και συμπαγής. Η πράξη a=a+b*c εδώ έχει την εξής μορφή (στα σχόλια φαίνεται η κατάσταση του σωρού μετά την εκτέλεσης της εντολής):

load b    ;; Σωρός: [b]
load c    ;; Σωρός: [b] [c]
multiply  ;; Σωρός: [b*c] 
load a    ;; Σωρός: [b*c] [a]
add       ;; Σωρός: [a+b*c]
store a   ;; Σωρός: - , a=a+b*c 
Να σημειωθεί ότι και στις αρχιτεκτονικές βασισμένες σε καταχωρητές υπάρχει η έννοια του σωρού αλλά δεν έχει τον ίδιο σκοπό, δηλαδή να είναι το βασικό κανάλι δεδομένων μεταξύ εντολών.

Σήμερα, η Java της Sun και το .NET της Microsoft είναι ίσως τα δύο πιο διάσημα περιβάλλοντα που χρησιμοποιούν εικονικές μηχανές. Και τα δύο είναι βασισμένα στη stack-based αρχιτεκτονική και τα σετ εντολών τους είναι παρεμφερή. Τα standards και για τις δύο περιβάλλοντα υπάρχουν ανοικτά στο internet. Στο Linux, υλοποιήσεις της Java Virtual Machine προσφέρονται από τη Sun, την ΙΒΜ και επίσης υπάρχουν μερικές open source προτάσεις όπως το kaffe ( http://www.kaffe.org). Για το .NET στο Linux υπάρχει το open source Mono Project ( http://www.mono-project.com).

2.3 Java Virtual Machine

Η ενδιάμεση μορφή στην οποία αποθηκεύονται τα προγράμματα σε Java είναι τα Java Bytecodes. Κατά την εκτέλεση της Java Virtual Machine (JVM) κάθε thread αποτελείται από τα παρακάτω στοιχεία:

  • PC (μετρητή προγράμματος): κάθε στιγμή δείχνει τη διεύθυνση της τρέχουσας εντολής.
  • Normal Stack: περιέχει κυρίως τα πλαίσια (frames) των συναρτήσεων και ενδιάμεσες τιμές.
  • Heap: περιοχή από την οποία μοιράζεται η μνήμη στα αντικείμενα.
  • Method Area: περιοχή η οποία περιέχει τα bytecode των μεθόδων και τις σταθερές των κλάσεων.
Κάθε στιγμή το πρόγραμμα βρίσκεται μέσα σε μία συνάρτηση και αποθηκεύει τις τοπικές πληροφορίες σε ένα πλαίσιο στον κανονικό σωρό. Το πλαίσιο περιέχει το σωρό τελεστέων, έναν πίνακα τοπικών μεταβλητών και κάποιες άλλες πληροφορίες που σχετίζονται με τα δεδομένα της κλάσης στην οποία ανήκει η μέθοδος. Η πρώτη τοπική μεταβλητή (index 0) περιέχει την αναφορά στο τρέχον instance της κλάσης. Πρόκειται ουσιαστικά για το this που σίγουρα έχουν χρησιμοποιήσει όσοι έχουν ασχοληθεί με Java. Από εκεί και πέρα (index 1) οι τοπικές μεταβλητές περιέχουν τις παραμέτρους της συνάρτησης. Η έννοια των τοπικών μεταβλητών είναι πιο ευρεία από αυτή που έχουμε συνηθίσει από άλλες γλώσσες (πχ C).

Ένα μικρό παράδειγμα:

public int alf(int x)
{
        if (x > 3)
                x++;
        else
                x--;
                
        return x;
}
παράγει τα εξής bytecodes:
||  .. ! ;----------------------------------------------
||  .. ! ; public int test::alf(int)
||  .. ! ;----------------------------------------------
||  .. ! alf_fd:                                                            
||  .. !   iload_1              ;; Φόρτωσε στο σωρό τελεστέων τη δεύτερη τοπική 
                                ;; μεταβλητή (την πρώτη παράμετρο της συνάρτησης).
||  fe !   iconst_3             ;; Φόρτωσε στο σωρό τελεστέων τη σταθερά 3.
||  ff !   if_icmpge loc_108    ;; Αν η κορυφαία τιμή στο σωρό τελεστέων είναι μεγαλύτερη ή ίση 
                                ;; με την αμέσως προηγούμενη, πήγαινε στη διεύθυνση 108.
|| 102 !   iinc      1, 1       ;; Πρόσθεσε στη δεύτερη τοπική μεταβλητή την τιμή 1.
|| 105 !   goto      loc_10b    ;; Πήγαινε στη διεύθυνση 10b.
|| 108 !                                                                    
|| ... ! loc_108:                       
|| ... !   iinc      1, 0ffh    ;; Πρόσθεσε στη δεύτερη τοπική μεταβλητή την τιμή -1.
|| 10b !                                                                    
|| ... ! loc_10b:                       
|| ... !   iload_1              ;; Φόρτωσε στο σωρό τελεστέων τη δεύτερη τοπική μεταβλητή.
|| 10c !   ireturn              ;; Πάρε την κορυφαία τιμή από τον τρέχον σωρό τελεστέων και
                                ;; τοποθέτησέ την στην κορυφή του σωρού τελεστέων του κώδικα
                                ;; που κάλεσε την τρέχουσα συνάρτηση.
Για να κάνουμε disassemble κάποιο class αρχείο στις εντολές της JVM (όπως παραπάνω) μπορούμε να χρησιμοποιήσουμε τον ΗΤ editor ( http://hte.sourceforge.net). Όμως, μπορούμε να πάμε ακόμα πιο πέρα, χρησιμοποιώντας κάποιον decompiler για Java ο οποίος θα προσπαθήσει να μας δώσει τον αρχικό πηγαίο κώδικα! Δυο πολύ γνωστοί Java decompilers είναι ο MOCHA ( http://www.brouhaha.com/~eric/computers/mocha.html) και ο JAD ( http://kpdus.tripod.com/jad.html).

Επόμενο  Προηγούμενο  Περιεχόμενα


Valid HTML 4.01!   Valid CSS!