Ανδρέας Μόσχοβος
Αρχιτεκτονική Υπολογιστών Ι
Φθιν. 2003

Θέμα:

Γλώσσα Assembly MIPS και πως τη χρησιμοποιούμε με τον προσομοιωτή SPIM.

Παράδειγμα προγράμματος Assembly

Αντί να γράφουμε τον κώδικα μηχανής απευθείας στο δυαδικό σύστημα θα χρησιμοποιήσουμε μια γλώσσα προγραμματισμού που μας επιτρέπει με συμβολικό τρόπο να καθορίζουμε τα περιεχόμενα της μνήμης είτε αυτά είναι δεδομένα είτε εντολές. Η γλώσσα προγραμματισμού αυτή λέγεται assembly (σύνθεση;) και διαφέρει από τον κώδικα μηχανής. Από assembly και με τη χρήση του κατάλληλων εργαλείων (κυρίως του assembler (συνθέτης;)) μπορούμε τελικά να παράγουμε τον αντίστοιχο κώδικα μηχανής.

Ένα πρόγραμμα σε assembly μπορούμε να θεωρήσουμε πως αποτελείται γραμμές που κάθε μια τους αποτελείται από τρείς στήλες:

ετικέτα:
εντολές/ψευτοεντολές
ορίσματα

Ανάλογα με την περίπτωση η ετικέτα ή τα ορίσματα μπορεί να μην χρειάζονται.
Ας ξεκινήσουμε με ένα παράδειγμα:

1.
# eimai ena sxolio
2.

.data
0x10000000
3.
a:
.word
0x12345678
4.
b:
.word
0x44332211, 0χ11223344
5.



6.



7.

.text
0x00400000 # akoma ena sxolio
8.

.globl
main
9.
main:
lui
$4, 0x1000
10.

lw
$5, 0x4($4)
11.

lw
$6, 0x8($4)
12

add
$5, $5, $6
13.

sw
$5, 0x0($4)

Οι αριθμοί γραμμής στα αριστερά δεν αποτελούν μέρος του προγράμματος και βρίσκονται έκει μόνο για να είναι εύκολη η αναφορά σε κάθε γραμμή του κώδικα από το κείμενο αυτό.

Κατά τη μεταγλώτιση του κώδικα assembly ο assembler χρησιμοποιεί εσωτερικά δύο δείκτες στη μνήμη, έναν για τα δεδομένα (Δδ) και έναν για τις εντολές (Δε) (έχει και άλλους τέτοιους δείκτες αλλά δεν θα μας απασχολήσουν
προς στιγμήν). Θα τους καλούμε ως τους τρέχοντες δείκτες δεδομένων και εντολών αντίστοιχα. Οι δείκτες Δε και Δδ περιέχουν τις διευθύνσεις στις οποίες θα αποθηκευτούν η επόμενη εντολή ή τα επόμενα δεδομένα αντίστοιχα.

Η γραμμή 1 περιέχει ένα σχόλιο. Γενικά, οτιδήποτε ακολουθεί το χαρακτήρα # θεωρείται ως σχόλιο και αγνοείται από τα εργαλεία. Στη γραμμή 7 βλέπουμε ένα σχόλιο που ξεκινά στο ενδιάμεσο μιας γραμμής.
Η γραμμή 2 περιέχει μια ψευτοεντολή, στη συγκεκριμένη περίπτωση μια οδηγία (directive) προς τον assembler που δηλώνει πως από αυτή τη γραμμή και κάτω ότι συναντάμε είναι δεδομένα τα οποία πρέπει να αποθηκευτούν με τη σειρά που εμφανίζονται και ξεκινώντας από τη διεύθυνση 0χ1000000 (το όρισμα της οδηγίας). Στη γενική της μορφή η οδηγία ".data διεύθυνση" θέτει το  Δδ να δείχνει στη διεύθυνση του ορίσματος.

Από σύμβαση τα δεδομένα σε τυπικά προγράμματα MIPS ξεκινάνε από τη διεύθυνση 0χ1000 0000.

Η γραμμή 3 περίεχει μια ετικέτα "a" και την οδηγία ".word" με το όρισμα "0x12345678".
Η οδηγία ".word" αποθηκεύει στη τρέχουσα θέση μνήμης όπως αυτή ορίζεται από το Δδ του assembler που βάση της προηγούμενης γραμμής είναι η 0x10000000.
Έτσι η γραμμή 3 έχει σαν αποτέλεσμα να αποθηκευτούν στην διεύθυνση μνήμης 0χ10000000 τα δεδομένα 0χ12345678. Επίσης το Δδ αυξάνεται κατά το μέγεθος των
δεδομένων που μόλις τώρα αποθηκεύτηκαν (δηλαδή κατα 4) και γίνεται 0χ10000004.
Τέλος η ετικέτα "a" από εδώ και πέρα  αντιστοιχεί στη διεύθυνση 0χ10000000 που είναι και αυτή στην οποία έδειχνε ο Δδ στην αρχή της γραμμής.

Η γραμμή 4 αντιστοιχεί την ετικέτα "b" με τη διεύθυνση στην οποία Δδ (0χ10000004) και στη συνέχεια τοποθετεί εκεί μια λέξη με τα περιεχόμενα 0χ44332211. Αυξάνει το Δδ κατα 4 (0χ10000008) τοποθετεί μια λέξη με τα περιεχόμενα 0χ11223344. Τέλος αυξάνει το Δδ κατά 4 (0χ100000c).

Βλέπουμε λοιπόν πως η οδηγία ".word" μπορεί να δεχτεί μια λίστα από λέξεις ως όρισμα οι οποίες και αποθηκεύονται με τη σειρά που εμφανίζονται σε διαδοχικές διευθύνσεις (+4 κάθε φορά).

Στη γραμμή 7 συναντάμε την  οδηγία ".text" η οποία δηλώνει πως από δω και πέρα ότι συναντάμε είναι εντολή και θα πρέπει να αποθηκευτεί στη μνήμη ξεκινώντας από τη διεύθυνση 0χ400000. Θέτει λοιπόν αυτή η οδηγία Δε=0χ400000.
Η γραμμή 8 είναι απαραίτητη για το SPIM. Στην πραγματικότητα η οδηγία ".globl" χρησιμοποιείται από προγράμματα που έχουν γραφτεί σε περισσότερα του ενός αρχεία καθως και για τη δυνατότητα συμβολικού debugging (δηλαδή στον
debugger να μπορούμε να χρησιμοποιούμε το όνομα main και αυτό να μας οδηγεί στο αντίστοιχο κομμάτι του κώδικα).

Η γραμμή 9 ορίζει αρχικά την ετικέτα "main" η οποία και παίρνει την τρέχουσα τιμή του Δε (και όχι του Δδ όπως πριν μια και προηγήθηκε η οδηγία .text) δηλαδή 0χ400000.
Στην συνέχεια έχουμε μια εντολή του MIPS η οποία αποθηκεύεται στη διεύθυνση Δε (0χ400000). Τέλος εφόσον κάθε εντολή είναι 4 bytes, το Δε αυξάνεται κατά 4 και γίνεται 0χ400004.
Αντίστοιχα οι εντολές στις γραμμές 10 έως και 13 αποθηκεύονται στις διευθύνσεις 0χ400008 εως και 0χ400010. Στο τέλος ο Δε δείχνει στη διεύθυνση 0χ400014.

Οι οδηγίες ".text" και ".data" μπορούν να χρησιμοποιηθούν πολλές φορές μέσα στο ίδιο πρόγραμμα. Επίσης μπορούμε να τις χρησιμοποιήσουμε χωρίς το όρισμα διεύθυνση. Σε αυτή τη περίπτωση χρησιμοποιούν την τελευταία τιμή του Δε και Δδ αντίστοιχα. Για παράδειγμα το παρακάτω πρόγραμμα είναι ισοδύναμο με το προηγούμενο.

1.

.globl
main
2.

.text
0x00400000
3
main:
lui
$4, 0x100
4.

.data
0x10000000
5.
a:
.word
0x12345678
6.

.text

7.

lw
$5, 0x4($4)
8.

lw
$6, 0x8($4)
9

add
$5, $5, $6
10.

sw
$5, 0x0($4)
11.

.data

12
b:
.word
0x44332211, 0x11223344


ΟΔΗΓΙΕΣ:

Άλλες οδηγίες (directives) που μας είναι διαθέσιμες είναι οι:

.asciiz "ακολουθία χαρακτήρων με τις συμβάσεις της C" --> αποθηκεύει την ακολουθία χαρακτήρων και την τερματίζει με το 0. Παράδειγμα :
.asciiz "TRALALA" --> θα αποθηκεύσει 8 bytes, 'T', 'R', 'A', 'L', 'A', 'L', 'A', 0. Εφόσον ακουλουθεί τις συμβάσεις της C, οι ακολουθίες π.χ. \n και \t αντιστοιχούν σε ειδικούς χαρακτήρες (αλλαγής γραμμής και μετατόπισης θέσης (tabstop) αντίστοιχα).

.ascii "ακολουθία χαρακτήρων με τις συμβάσεις της C", Όπως η .asciiz  αλλά δεν αποθηκεύει το τελικό 0.
 
.byte b1, ..., bn αποθηκεύει τις τιμές b1...bn ως bytes διαδοχικά στη μνήμη. Παράδειγμα .byte 0x22, 0x23, 10, 55, 255, -1

.half h1, ..., hn αποθηκεύει τις τιμές h1, ..., hn ως half-words (16 bits ή 2 bytes) διαδοχικά στη μνήμη. Παράδειγμα .half 0x1234, 16385, -10

.word w1, ..., wn αποθηκεύει τις τιμές w1, ..., wn ως words (32 bits ή 4 bytes) διαδοχικά στη μνήμνη. Παράδειγμα .word 0x12345678, 102034, -20

.space n "δεσμεύει" n bytes μνήμης τα οποία και δεν παίρνουν κάποια αρχική τιμή. (πρακτικά αυξάνει το Δδ κατά n).

.align n δηλώνει πως τα δεδομένα που ακολουθούν πρέπει να αποθηκευτούν στην επόμενη διεύθυνση που διαιρείται ακριβώς με το 2^n. π.χ. αν Δδ 0χ10000003 και μετά γράψουμε .align 4 τότε τα επόμενα δεδομένα θα αποθηκευτούν ξεκινώντας από τη διεύθυνση 0χ10000010 (επόμενη διεύθυνση που είναι διαιρέσιμη με το 16=2^4). Η οδηγία αυτή χρησιμοποιείται για να σιγουρευτούμε πως συγκεκριμένα δεδομένα είναι ισογραμμισμένα σε κατάλληλες διευθύνσεις για να αποφύγουμε unalined προσπελάσεις (όλες οι προσπελάσεις μνήμης στο MIPS πρέπει να είναι aligned).


ψευτο-ΕΝΤΟΛΕΣ

Για ευκολία ο assembler που είναι ενσωματωμένος στο SPIM υλοποιεί ορισμένες ψευτοεντολές. Οι ψευτοεντολές εμφανίζονται ως μία εντολή στον
κώδικα assembly αλλά μετατρέπονται σε μια μικρή ακουλουθία εντολών σε επίπεδο κώδικα μηχανής. Υπάρχουν για να κάνουν πιο εύκολο το προγραμματισμό. Παράδειγμα:

li $5, 0x11223344 -> ο καταχωρητής $5 παίρνει την τιμή 0χ11223344 (32 bit). Όπως έχουμε εξηγήσει στην πραγματικότητα χρειάζονται δύο εντολές MIPS για να δημιουργήσουμε μια 32bit σταθερά: lui $5, 0x1122 και ori $5, $5, 0x3344 (γίνεται και με lui και  addi ή subi).

la $20, ετικέτα ->
ο κατασωρητής $10 παίρνει την τιμή που αντιστοιχεί στη διεύθυνση στην οποία ορίσαμε την ετικέτα.

Λόγω ιδιομορφίας του SPIM δε μπορούμε να χρησιμοποιήσουμε ετικέτες με τη li αλλά μόνο με τη la.


ΠΡΟΣΟΧΗ: Κατά την υλοποίηση των ψευτοεντολών ο assembler μπορεί να χρησιμοποιήσει τον καταχωρητή $1 (ο οποίος επίσης αναφέρεται και ως $at).

abs Rd, Rs --> [Rd] = απόλυτη τιμή ([Rs])
neg Rd, Rs--> [Rd] = -[Rs]

seq Rd, Rs, Rt --> [Rd] = ([Rs] == [Rt]) ? 1 : 0
sge Rd, Rs, Rt --> [Rd] = ([Rs] >= [Rt]) ? 1 : 0
sgeu Rd, Rs, Rt --> [Rd] = ([Rs] >= [Rt]) ? 1 : 0 όπου οι αριθμοί ερμηνεύονται ως μη προσημασμένοι (φυσικοί)
sgt Rd, Rs, Rt --> [Rd] = ([Rs] > [Rt]) ? 1 : 0
sgtu Rd, Rs, Rt --> [Rd] = ([Rs] > [Rt]) ? 1 : 0 όπου οι αριθμοί ερμηνεύονται ως μη προσημασμένοι (φυσικοί)
sle Rd, Rs, Rt --> [Rd] = ([Rs] <= [Rt]) ? 1 : 0
sleu Rd, Rs, Rt --> [Rd] = ([Rs] <= [Rt]) ? 1 : 0 όπου οι αριθμοί ερμηνεύονται ως μη προσημασμένοι (φυσικοί)
sne Rd, Rs, Rt --> [Rd] = ([Rs] != [Rt]) ? 1 : 0

b ετικέτα --> [PC] = ετικέτα
beqz Rs, ετικέτα --> if ([Rs] == 0) then [PC] = ετικέτα
bge Rs, Rt, ετικέτα --> if ([Rs] >= [Rt]) then [PC] = ετικέτα
bgt Rs, Rt, ετικέτα --> if ([Rs] > [Rt]) then [PC] = ετικέτα
bgtu Rs, Rt, ετικέτα --> if ([Rs] > [Rt]) then [PC] = ετικέτα, όπου οι αριθμοί ερμηνεύονται ως μη προσημασμένοι (φυσικοί)
ble Rs, Rt, ετικέτα --> if ([Rs] <= [Rt]) then [PC] = ετικέτα
bleu Rs, Rt, ετικέτα --> if ([Rs] <= [Rt]) then [PC] = ετικέτα, όπου οι αριθμοί ερμηνεύονται ως μη προσημασμένοι (φυσικοί)
bnez Rs, ετικέτα --> if ([Rs] != 0) then [PC] = ετικέτα

move Rd, Rs --> [Rs] = [Rd]

nop --> "τίποτε" π.χ., add $0, $0, $0

Υπάρχουν και άλλες ψευτοεντολές που θα τις αναφέρουμε εν καιρώ.


Συνώνυμα Καταχωρητών

Από σύμβαση πέρα των ονομάτων $ν (όπου ν=0..31) χρησιμοποιούνται και συνώνυμα για τους καταχωρητές:
$t0,..,$t9  =  $8,..,$15,$24,$25
$s0,...,$s7 = $16,...,$23
$zero = $0
$at = $1
$v0,$v1 = $2,$3
$a0,...,$a3 = $4,...,$7
$k0,$k1 = $26,$27
$gp = $28
$sp = $29
$fp = $30
$ra = $31

Η χρήση των καταχωρητών θα εξηγηθεί πλήρως όταν καλύψουμε και την υλοποίηση ρουτινών (functions). Προς στιγμή καλό θα είναι να χρησιμοποιείτε πρώτα τους καταχωρητές $s0...$s7 και μετά τους $t0...t$9.


ΠΑΡΑΔΕΙΓΜΑ:

int a[4] = { 0x1, 0x2, 0x8, 10};
int n = 4;
int sum;

main (void)
{
   int i;

    sum = 0;
    for (i = 0; i < n; i++)
       sum = sum + a[i];
}

Υλοποίηση σε assembly:

          .data
a:       .word    0x1, 0x2, 0x4, 10
n:       .word    0x4
sum:     .space   4

         .text
         .globl main

main:   la $t0, sum
        sw $0, 0($t0)               # sum = 0
        la  $t1, n
        lw $t1, 0($t1)              # $t1 = n
        add $t2, $0, $0             # $t2 = i = 0
        la $t3, a                   # t3 = &[a0], dieythinsi toy prvtoy stoixeioy toy pinaka a

brox:   bge     $t2, $t1, telos    # an i >= n tote teleiwsame
               
        # prospelasi toy a[i]
    `   add    $t4, $t2, $t2      # $t4 = i + i = i*2
        add    $t4, $t4, $t4     # $t4 = (i*2) + (i*2) = i*4
        add    $t4, $t3, $t4     # $t4 = dieythinsi toy a[i]
        lw       $t4, 0x0($t4)    # $t4 = timi a[i]

        lw      $t5, 0x0($t0)    # sum = sum + a[i]    
        add    $t5, $t5, $t4   
        sw     $t5, 0x0($t0)

        addi $t2, $t2, 1            # i = i + 1
        beq    $0, $0, brox

telos:  li    $v0, 10    # η ακολουθία αυτων των δύο εντολών "τερματίζει" το πρόγραμμα στον προσομοιωτή SPIM.
        syscall