ΠΕΡΙΕΧΟΜΕΝΑ
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 καλεί τις
συναρτήσεις.
Calling Convention είναι η μέθοδος που επέλεξαν οι δημιουργοί του compiler της C για να περνούν πληροφορίες
σε συναρτήσεις και να επιστρέφουν τιμές. Οι συνήθεις λύσεις χρησιμοποιούν είτε
τους εσωτερικούς καταχωρητές της CPU, είτε τη
στοίβα του προγράμματος για να περνούν πληροφορίες μεταξύ συναρτήσεων. Γενικά,
η C χρησιμοποιεί το stack για να
περνά ορίσματα σε συναρτήσεις και καταχωρητές για να κρατά τις επιστρεφόμενες
τιμές. Εάν ένα όρισμα είναι ενός από τους βασικούς τύπους η τιμή που περνιέται
στο stack. Εάν το όρισμα είναι ένας
πίνακας (πιο σύνθετος τύπος δεδομένων), τότε η διεύθυνση μνήμης του πρώτου
στοιχείου του περνιέται στη στοίβα. Όταν μία συνάρτηση αρχίζει να εκτελείται,
τότε ανακτά και τα ορίσματα από τη στοίβα. Στο τέλος επιστρέφει στο σημείο από
το οποίο κλήθηκε τοποθετώντας τις επιστρεφόμενες τιμές στους καταχωρητές της CPU (αν και θεωρητικά είναι εφικτό, η επιστροφή τιμών
στη στοίβα συμβαίνει σπανιότερα).
Φυσικά,
αυτό συνεπάγεται ότι θα πρέπει να έχει προκαθοριστεί ποιοι καταχωρητές είναι
δεσμευμένοι και ποιοι μπορούν να χρησιμοποιηθούν ελεύθερα. Τη λειτουργία αυτή
αναλαμβάνει το calling
convention. Συχνά ο compiler δημιουργεί αντικειμενικό κώδικα ο
οποίος χρειάζεται μόνο ένα τμήμα των καταχωρητών πριν τους χρησιμοποιήσουμε
κάνοντας push τα
περιεχόμενά τους στη στοίβα.
Πριν γράψουμε
μία συνάρτηση σε assembly για χρήση
με την C, θα πρέπει να ακολουθήσουμε όλους
τους κανόνες κλήσης συναρτήσεων που ορίζει και χρησιμοποιεί η 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 : \ > tcc –s 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 : \ > TCC –s 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 αν ακολουθήσουμε τους κανόνες
ακριβώς.
Η λέξη 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.