ΠΕΡΙΕΧΟΜΕΝΑ

 

1.      CALLING CONVENTIONS

2.      ΚΑΝΟΝΕΣ ΚΛΗΣΗΣ ΤΗΣ C

3.      ΔΗΜΙΟΥΡΓΙΑ ΣΥΝΑΡΤΗΣΗΣ ΣΕ ASSEMBLY

4.      Η ΛΕΞΗ asm

 

 

ΚΕΦΑΛΑΙΟ 12ο

 

 

ΕΝΣΩΜΑΤΩΣΗ ΡΟΥΤΙΝΩΝ ASSEMBLY

 

Αν και η C είναι αρκετά δυνατή και αποτελεσματική, πιθανόν να χρειαστεί να γράψουμε κάποιες ρουτίνες κάνοντας χρήση του assembler έτσι ώστε:

 

1.      Να αυξήσουμε την ταχύτητα και αποτελεσματικότητα της ρουτίνας.

2.      Να εκτελέσουμε κάποιες ρουτίνες του Hardware που δεν υποστηρίζονται από την C.

3.      Να χρησιμοποιήσουμε ρουτίνες τρίτων, γραμμένες σε assembly.

 

Παρ’ όλο που η C παράγει αρκετά συμπαγή και γρήγορο κώδικα, κανένα πρόγραμμα δεν είναι τόσο γρήγορο και συμπαγές όσο ένα assembly πρόγραμμα.

 

Τις περισσότερες φορές υπάρχει μικρή διαφορά στο χρόνο εκτέλεσης μεταξύ ενός C και ενός assembly προγράμματος η οποία υπερκαλύπτεται από τον χρόνο που χρειαζόμαστε για να γράψουμε κώδικα σε assembly. Πάντως υπάρχουν περιπτώσεις που μία συνάρτηση γράφεται σε assembly έτσι ώστε να μειώσει το χρόνο εκτέλεσής της.

 

Υπάρχουν δύο τρόποι να συνδυάσουμε την assembly με τη C. Ο πρώτος είναι η δημιουργία ξεχωριστού προγράμματος assembly και η σύνδεσή του (Link) με τις συναρτήσεις της C. Ο δεύτερος τρόπος χρησιμοποιεί την εντολή asm, για την ενσωμάτωση inline assembly κώδικα μέσα σε συναρτήσεις της C.

 

Πάντως, πριν ξεκινήσουμε θα πρέπει να αναφερθούμε στον τρόπο με τον οποίο η C καλεί τις συναρτήσεις.

 

 

i)                    Calling Conventions

 

Calling Convention είναι η μέθοδος που επέλεξαν οι δημιουργοί του compiler της C για να περνούν πληροφορίες σε συναρτήσεις και να επιστρέφουν τιμές. Οι συνήθεις λύσεις χρησιμοποιούν είτε τους εσωτερικούς καταχωρητές της CPU, είτε τη στοίβα του προγράμματος για να περνούν πληροφορίες μεταξύ συναρτήσεων. Γενικά, η C χρησιμοποιεί το stack για να περνά ορίσματα σε συναρτήσεις και καταχωρητές για να κρατά τις επιστρεφόμενες τιμές. Εάν ένα όρισμα είναι ενός από τους βασικούς τύπους η τιμή που περνιέται στο stack. Εάν το όρισμα είναι ένας πίνακας (πιο σύνθετος τύπος δεδομένων), τότε η διεύθυνση μνήμης του πρώτου στοιχείου του περνιέται στη στοίβα. Όταν μία συνάρτηση αρχίζει να εκτελείται, τότε ανακτά και τα ορίσματα από τη στοίβα. Στο τέλος επιστρέφει στο σημείο από το οποίο κλήθηκε τοποθετώντας τις επιστρεφόμενες τιμές στους καταχωρητές της CPU (αν και θεωρητικά είναι εφικτό, η επιστροφή τιμών στη στοίβα συμβαίνει σπανιότερα).

 

Φυσικά, αυτό συνεπάγεται ότι θα πρέπει να έχει προκαθοριστεί ποιοι καταχωρητές είναι δεσμευμένοι και ποιοι μπορούν να χρησιμοποιηθούν ελεύθερα. Τη λειτουργία αυτή αναλαμβάνει το calling convention. Συχνά ο compiler δημιουργεί αντικειμενικό κώδικα ο οποίος χρειάζεται μόνο ένα τμήμα των καταχωρητών πριν τους χρησιμοποιήσουμε κάνοντας push τα περιεχόμενά τους στη στοίβα.

 

Πριν γράψουμε μία συνάρτηση σε assembly για χρήση με την C, θα πρέπει να ακολουθήσουμε όλους τους κανόνες κλήσης συναρτήσεων που ορίζει και χρησιμοποιεί η C. Μόνο έτσι θα είμαστε σίγουροι ότι τα αποτελέσματά μας θα είναι σωστά.

 

ii)                  Κανόνες Κλήσης Της C

 

Όπως όλοι οι C compilers, η Turbo C, περνά παραμέτρους σε συναρτήσεις μέσω της στοίβας. Οι παράμετροι γίνονται push στη στοίβα από δεξιά προς τα αριστερά (C Model). Αυτό σημαίνει ότι στη συνάρτηση func(a, b, c), το c περνά πρώτο στη στοίβα ακολουθούμενο από το b και τέλος το a.

 

Κατά τη διάρκεια εισόδου σε μία διαδικασία assembly, τα περιεχόμενα του ΒΡ σώζονται στη στοίβα και η τιμή του SP τοποθετείται στον ΒΡ. Ίσως επίσης χρειαστεί να σώσουμε και τα περιεχόμενα των SI και DI εάν το απαιτεί η ρουτίνα.

 

Πριν επιστρέψουμε από μία διαδικασία assembly πρέπει να ανακτήσουμε τα περιεχόμενα των BP, SI και DI και να αρχικοποιήσουμε τον SP.

 

Data Type

No_Of_Bytes

char

short

signed char

signed short

unsigned char

unsigned short

int

signed int

unsigned int

long

unsigned long

float

double

(near) pointer

(far) pointer

2

2

2

2

2

2

2

2

2

4

4

4

8

2 (Offset)

4 (Seg : Offs)

 

Αριθμός Bytes που απαιτούνται στη στοίβα ανά τύπο δεδομένων

 

Type

Registers

char

unsigned char

short

unsigned short

int

unsigned int

long

 

unsigned long

 

float

 

double

struct & union

(near) pointer

(far) pointer

AX

AX

AX

AX

AX

AX

Low-Order Word in AX

High-Order Word in AX

Low-Order Word in AX

High-Order Word in AX

Low-Order in AX

High-Order in AX

Return in 8087 stack or TOS

Address to Value

AX

Offset in AX, Segment in DX

 

Χρήση των καταχωρητών κατά την επιστροφή στη συνάρτηση

 

i)                    Δημιουργία Συνάρτησης σε ASSEMBLY

 

Χωρίς αμφιβολία ο πιο εύκολος τρόπος να δημιουργήσουμε assembly ρουτίνες είναι να δούμε πως δημιουργεί η TC assembly κώδικα, κάνοντας χρήση της επιλογής –S του compiler. Η επιλογή φυσικά μπαίνει στην command line έκδοση του compiler κι όχι στο IDE. Η σύνταξή της είναι:

 

                                 C : \ > tccs name.c

 

Ας υποθέσουμε ότι μεταφράζουμε το παρακάτω πρόγραμμα:

 

int sum;

main( )

{

         sum = add (10, 20);

}

add(int a, int b);

{

         int t;

         t = a + b;

         return t ;

}

 

Η μεταβλητή sum είναι δηλωμένη σαν global έτσι ώστε να μπορούμε να δούμε και τις τοπικές και τις συνολικές μεταβλητές. Αν το πρόγραμμα ονομάζεται test.c τότε η παρακάτω εντολή θα δημιουργήσει το TEST.ASM.

 

                                 C : \ > TCCs TEST

 

τα περιεχόμενα του οποίου θα είναι:

 


 

                                 name               test

        

_text                segment          byte public code

dgroup             group               _bss, _data

                        assume           cs: _text, ds: dgroup, ss: dgroup

_text                ends

_data               segment          word public data

_d@                label                 byte

_data               ends

_bss                segment          word public ‘bss’

_b@                label                 byte

_bss                ends

_text                segment          byte public ‘code’

_main              proc                 near

                        mov                 ax, 20

                        push                ax

                        mov                 ax, 10

                        push                ax

                        call                   near ptr _add

                        pop                  cx

                        pop                  cx

                        mov                 word ptr dgroup: _sum, ax

@1:

                        ret

_main              endp

_add                proc                 near

                                 push                si

                                 push                bp

                                 mov                 bp, sp

                                 mov                 si, word ptr [bp+6]

                                 add                  si, word ptr [bp+8]

                                 mov                 ax, si

         @2:                 pop                  bp

                                 pop                  si

                                 ret

         _add                endp

         _text                ends

         _bss                segment          word public ‘bss’

                                 public               _sum

         _sum               label                 word

                                 db                    2 (dup) (?)

         _bss                ends

         _data               segment          word public ‘data’

         _s@                label                 byte

         _data               ends

         _text                segment          byte public ‘code’

                                 public               _add

                                 public               _main

         _text                ends

                                 end

 

 

Το πρόγραμμα ξεκινά καθορίζοντας τα διάφορα τμήματα που απαιτούνται από την Turbo C. Αυτά φυσικά διαφέρουν ανάμεσα στα διάφορα μοντέλα μνήμης (το αρχείο TEST.ASM δημιουργήθηκε μέσω του small μοντέλου).

 

Σημειώνεται ότι δύο bytes δεσμεύονται στο τμήμα bss για τη συνολική μεταβλητή sum. Το underscore ( _ ) τοποθετείται από τον compiler έτσι ώστε να αποφευχθούν τυχόν συγχύσεις με ήδη δηλωμένες μεταβλητές του compiler. Βασικά προστίθεται σε όλα τα ονόματα συναρτήσεων και συνολικών μεταβλητών. Μετά από αυτό ξεκινά το κυρίως πρόγραμμα. Στην Turbo C το code segment ονομάζεται _text.

 

Το πρώτο πράγμα που συμβαίνει στη συνάρτηση _main είναι ότι τα δύο ορίσματα της _add τοποθετούνται στη στοίβα και κατόπιν η _add καλείται. Κατά την επιστροφή εκτελούνται δύο εντολές pop cx και η στοίβα επανέρχεται στην αρχική της κατάσταση. Η επόμενη εντολή μετακινεί την επιστρεφόμενη τιμή από την _add στην _sum. Τέλος, η main επιστρέφει.

 

Η συνάρτηση _add ξεκινά σώνοντας τις τιμές των SI και BP στη στοίβα και κατόπιν τοποθετεί την τιμή του SP στον BP. Οι υπόλοιπες τρεις εντολές προσθέτουν τοις αριθμούς. Σημειώνεται ότι η Turbo C χρησιμοποιεί τον SI για να κρατήσεις την τιμή της τοπικής μεταβλητής t. Αν και η μεταβλητή t δεν έχει δηλωθεί σαν register, η Turbo C, αυτομάτως την μετατρέπει σε register, σαν τμήμα των βελτιστοποιήσεων του compiler. Εάν το πρόγραμμα χρησιμοποιούσε περισσότερες από δύο register μεταβλητές, θα είχε δεσμευτεί χώρος γι’ αυτές στη στοίβα. Τέλος, το αποτέλεσμα αποθηκεύεται στον ΑΧ, οι ΒΡ και SI ανακτούν τις τιμές τους και η _add επιστρέφει.

 

Θα μπορούσαμε να μεταφράσουμε αυτό το assembly αρχείο (TEST.ASM) με τον TASM ή τον MASM και να το συνδέσουμε με τον TLINK. Έπειτα θα μπορούσαμε να το τρέξουμε παίρνοντας τα ίδια αποτελέσματα. Αυτό που είναι ιδιαίτερα σημαντικό είναι ότι θα μπορούσαμε να προκαλέσουμε αλλαγές στον assembly κώδικα οι οποίες θα έκαναν το πρόγραμμα γρηγορότερο. Για παράδειγμα, οι εντολές push SI και pop SI μέσα στη συνάρτηση _add θα μπορούσαν να αφαιρεθούν μιας και ο SI δε χρησιμοποιείται πουθενά στο υπόλοιπο πρόγραμμα. Αυτό ονομάζεται χειροποίητη βελτιστοποίηση (Hand Optimization).

 

 

Ένας από τους πιο εύκολους τρόπους για να φτιάξουμε assembly ρουτίνες είναι να αφήσουμε τον compiler να δημιουργήσει ένα σκελετό για μας. Από τη στιγμή που έχουμε το σκελετό μας μένει να τοποθετήσουμε μόνο τις λεπτομέρειες. Για παράδειγμα, ας υποθέσουμε ότι πρέπει να φτιάξουμε μία ρουτίνα η οποία πολλαπλασιάζει δύο ακεραίους. Θα αναγκάσουμε τον compiler να δημιουργήσει ο ίδιος τον assembly σκελετό μεταφράζοντας τον παρακάτω κώδικα:

 

mul (int a, b)

{

}

 

Εάν το τμήμα αυτό μεταφραστεί με την παράμετρο –s θα δώσει το εξής αποτέλεσμα:

 

 

 

                                    name                                 mul

 

_text                            segment                            byte public ‘code’

dgroup                         group                                 _bss, _data

                                    assume                             cs:_text, ds: dgroup, ss:dgroup

_text                            ends                                 

_data                           segment                            word public ‘data’

_d@                            label                                   byte

_data                           ends

_bss                            segment                            word public ‘bss’

_b@                            label                                   byte

_bss                            ends

_text                            segment                            byte public ‘code’

_mul                            proc                                   near

                                    push                                  bp

                                    mov                                   bp, sp

@1:

                                    pop                                    bp

                                    ret

_mul                            endp

_text                            ends                                 

_data                           segment                            word public ‘data’

_s@                            label                                   byte

_data                           ends

_text                            segment                            byte public ‘code’

                                    public                                 _mul

_text                            ends

                                    end

 

Φτιάχνοντας αυτό το σκελετό ο compiler έχει κάνει όλη την απαραίτητη δουλειά δηλώνοντας τα κατάλληλα τμήματα και αρχικοποιώντας τη στοίβα και τους καταχωρητές. Το μόνο που έχουμε να κάνουμε είναι να τοποθετήσουμε τις λεπτομέρειες:

 

                                    name                                 _mul

 

_text                            segment                            byte public ‘code’

dgroup                         group                                 _bss, _data

                                    assume                             css:_text, ds:dgroup, ss:dgroup

_text                            ends

_data                           segment                            word public ‘data’

_d@                            label                                   byte

_data                           ends                                 

_bss                            segment                            word public ‘bss’

_b@                            label                                   byte

_bss                            ends

_text                            segment                            byte public ‘code’

                                    public                                 _mul

_mul                            proc                                   near

                                    push                                  bp

                                    mov                                   bp, sp

                                    mov                                   ax, [bp + 4]

                                    imul                                   word ptr [bp +6]

@1:

                                    pop                                    bp

                                    ret

_mul                            endp

_text                            ends

_data                           segment                            word public ‘data’

_s@                            label                                   byte

_data                           ends

_text                            segment                            byte public ‘code’

                                    public                                 _mul

_text                            ends

                                    end

 

Παρατηρούμε ότι μία γραμμή κώδικα προστέθηκε πριν την πρώτη εντολή της συνάρτησης _mul. Είναι η γραμμή public _mul. H δήλωση αυτή λέει στον assembler ότι η ταυτότητα _mul θα πρέπει να είναι διαθέσιμη σε οποιαδήποτε ρουτίνα τη ζητήσει. Αυτό επιτρέπει σε όλα τα C προγράμματα να καλούν τη συνάρτηση mul( ). Π.χ.:

 

main( )

{

   printf ( “%d”, mul (2, 5) );

}

 

Αυτό πρέπει να γίνεται με όλες τις συναρτήσεις που θέλουμε να καλούμε από τη C. Εάν υπάρχουν και άλλα δεδομένα για τα οποία θα πρέπει να γνωρίζει το C πρόγραμμα θα πρέπει και αυτά να έχουν χαρακτηριστεί public.

 

Ο κανόνας είναι απλός. Τοποθετούμε τα ονόματα των συναρτήσεων που θέλουμε να είναι public στο CODE SEGMENT, και τα ονόματα των μεταβλητών στο DATA SEGMENT.

Όταν θέλουμε να καλέσουμε μία συνάρτηση της C ή να προσπελάσουμε μία μεταβλητή δηλωμένη σε ένα πρόγραμμα C μέσω μιας assembly συνάρτησης, συμβαίνει το αντίθετο. Στην περίπτωση αυτή πρέπει να δηλώσουμε εξωτερικά τα αντικείμενα που χρειάζεται η assembly ρουτίνα κάνοντας χρήση της εντολής extrn του assembler. Η γενική σύνταξη είναι:

 

                           extrn  <object> : <attribute>

 

Για τις μεταβλητές το attribute μπορεί να πάρει τις παρακάτω τιμές:

 

Τιμή

 

byte

word

dword

qword

tbyte

Μέγεθος Σε Bytes

 

1

2

4

8

10

 

Για παράδειγμα, αν μία συνάρτηση assembly χρειαστεί να προσπελάσει τη συνολική ακέραια μεταβλητή count και τη συνάρτηση search( ) θα πρέπει να τοποθετήσουμε τις παρακάτω δηλώσεις στην αρχή του assembly αρχείου:

 

                                             extrn _count  :  word

                                             extrn_search :  near

 

Θα πρέπει να θυμόμαστε πως το όνομα κάθε συνάρτησης assembly ή εξωτερικών δεδομένων που καλείται από ένα πρόγραμμα C πρέπει να έχει ένα underscore ( _ ) μπροστά του.

 

Στην περίπτωση που pointers περνιούνται σε μία συνάρτηση για να προσπελαστεί και να αλλαχτεί η τιμή του ορίσματος απαιτούνται πλάγιες μέθοδοι διευθυνσιοδότησης. Για παράδειγμα, ας θεωρήσουμε ότι πρέπει να δημιουργήσουμε μία συνάρτηση assembly η οποία μετατρέπει σε αρνητικό τον ακέραιο που δείχνεται από το όρισμα της συνάρτησης.

 

x=10;

neg(&x)

printf(“%d”, neg);      / * Τυπώνει –10 * /

 

Στην C η συνάρτηση neg(), θα έμοιαζε ως εξής:

 

neg(a)

int *a;

{

         *a= -*a;

}

 

Σε assembly η συνάρτηση neg( ) είναι η παρακάτω, κάνοντας χρήση small μοντέλου.

 

                                    name                        neg

_text                            segment                   byte public ‘code’

dgroup                         group                        _bss, _data

                                    assume                    214_text, ds : dgroup, ss : dgroup

_text                            ends                        

_data                           segment                   word public ‘data’

_d@                            label                          byte

_bss                            segment                   word public ‘bss’

_b@                            label                          byte

_bss                            ends

_text                            segment                   byte public ‘code’

                                    public                        _neg

_neg                            proc                          near

                                    push                         bp

                                    mov                          bp, sp

;

; Ο κώδικας μετατροπής σε αρνητικό

                                    mov                          bx, word ptr [bp + 4]          ;Λήψη Διεύθυνσης

                                    mov                          ax, word ptr [bx]                ; Φόρτωση Ορίσματος

                                    neg                           ax                                      ; Αρνητικό του ορίσματος

                                    mov                          word ptr [bx], ax                ; Αποθήκευση

;

@1:

                                    pop                           bp

                                    ret

_neg                            endp

_text                            ends

_data                           segment                   word public ‘data’

_s@                            label                          byte

_data                           ends

_text                            segment                   byte public ‘code’

                                    public                        _neg

_text                            ends

                                    end

 

Ο κώδικας που μετατρέπει σε αρνητικό αριθμό είναι:

 

mov bx, word ptr [bp + 4] ;  Παίρνει τη Διεύθυνση

mov ax, word ptr [bx] ;        Φορτώνει το όρισμα

neg   ax ;                              Μετατροπή σε αρνητικό

mov word ptr [bx], ax ;        Αποθήκευση

 

Πρώτα η διεύθυνση του ορίσματος φορτώνεται από τη στοίβα. Αμέσως μετά χρησιμοποιείται ο σχετικός τρόπος διευθυνσιοδότησης του 8086 για να φορτωθεί ο ακέραιος που θα μετατραπεί σε αρνητικό. Η εντολή neg αντιστρέφει το πρόσημο και η τελευταία εντολή τοποθετεί την τιμή στη θέση που δείχνει ο BX.

 

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

 

Όπως είδαμε είναι εύκολο να γράψουμε συναρτήσεις assembly με τον κώδικα της Turbo C αν ακολουθήσουμε τους κανόνες ακριβώς.

 

 

ii)                  Η Λέξη asm

 

Η λέξη asm μας επιτρέπει να ενσωματώνουμε in-line assembly κώδικα μέσα σε ένα Turbo C χωρίς να χρησιμοποιούμε τελείως ξεχωριστά assembly αρχεία. Για να τοποθετήσουμε in-line assembly κώδικα σε ένα πρόγραμμα Turbo C βάζουμε τη λέξη asm στην αρχή κάθε γραμμής assembly και στη συνέχεια τοποθετούμε την assembly δήλωση. Ο κώδικας που ακολουθεί τη λέξη asm θα πρέπει να είναι αποδεκτός assembly κώδικας για τον υπολογιστή που χρησιμοποιούμε. Η Turbo C απλά περνά τον κώδικα αυτόν, ανέπαφο, στον assembler. Θα πρέπει πάντως, να χρησιμοποιήσουμε την επιλογή –Β του compiler η οποία πληροφορεί την Turbo C ότι υπάρχει in-line assembly κώδικας στο πρόγραμμα. Π.χ.:

 

inti_port( )

{

         printf (“Initializ;ing Port …\n”);

         asm out 26, 255

         asm out 26, 0

}

 

Παρατηρούμε ότι οι δηλώσεις asm δεν χρειάζονται ( ; ) για να τελειώσουν. Μία δήλωση assembly τελειώνει με το τέλος της τιμής.

 

Μπορούμε να χρησιμοποιήσουμε in-line assembly κώδικα για να δημιουργήσουμε τη συνάρτηση mul( ) της προηγούμενης παραγράφου χωρίς τη χρήση ξεχωριστού assembly αρχείου. Χρησιμοποιώντας αυτή την προσέγγιση, η mul( ) θα είναι η εξής:

 

mul(int a,b)

{

         asm mov ax, word ptr 4[bp]

         asm word ptr 6[bp]

}

 

Εάν θέλουμε να χρησιμοποιήσουμε σχόλια σε δηλώσεις asm πρέπει να χρησιμοποιούμε τους τελεστές / * και * /. Δεν χρησιμοποιούμε τα ερωτηματικά.

 

Οι δηλώσεις assembly μιας συνάρτησης τοποθετούνται στο CODE Segment. Αυτές που βρίσκονται έξω από όλες τις συναρτήσεις τοποθετούνται στο DATA Segment.