Η μνήμη συνήθως αντιστοιχίζεται για ένα πρόγραμμα από το Λειτουργικό Σύστημα (OS) σε μια γενική διάταξη:
| διεύθυνση | περιοχή μνήμης |
|---|---|
| υψηλή | στοίβα |
| ... | |
| σωρός | |
| τμήμα ανάγνωσης-εγγραφής | |
| τμήμα κώδικα/μόνο για ανάγνωση | |
| χαμηλή | δεσμευμένη |
Η μνήμη σε τμήματα οργανώνεται σε τομείς, με διαφορετικά δικαιώματα.
Οι συναρτήσεις που έχουμε ορίσει μέχρι τώρα ήταν όλες στον τομέα .text. Αυτός ο τομέας περιέχει εκτελέσιμα δεδομένα μόνο για ανάγνωση. Άλλοι τομείς χρησιμοποιούνται για τη δήλωση μεταβλητών δεδομένων, οι οποίες μπορεί να είναι μόνο για ανάγνωση ή για ανάγνωση και εγγραφή, αλλά δεν είναι εκτελέσιμες.
Τα αρχικοποιημένα δεδομένα δηλώνονται στον τομέα .data.
Στο NASM (The Netwide Assembler, ο assembler που χρησιμοποιείται σε αυτό το track), μια αρχικοποιημένη μεταβλητή έχει ένα όνομα, μια οδηγία που δηλώνει το μέγεθος των δεδομένων, και μια λίστα τιμών χωρισμένες με κόμμα.
Κάθε ένα από αυτά χωρίζεται από το επόμενο με ένα κενό, και η ετικέτα μπορεί προαιρετικά να ακολουθείται από :.
Οι κυριότερες οδηγίες και τα σχετικά μεγέθη δεδομένων τους είναι:
| οδηγία | μέγεθος |
|---|---|
| db | 1 byte |
| dw | 2 bytes |
| dd | 4 bytes |
| dq | 8 bytes |
Για παράδειγμα, αυτό δηλώνει μια μεταβλητή ενός byte με το όνομα space και την τιμή 10:
section .data
space db 10
Οι μεταβλητές που δηλώνονται στο section .data είναι τροποποιήσιμες, δηλαδή μπορούν να διαβαστούν και να τροποποιηθούν.
Έχουν επίσης στατική διάρκεια αποθήκευσης, που σημαίνει ότι υπάρχουν για ολόκληρη τη διάρκεια εκτέλεσης του προγράμματος.
Ο τομέας .rodata είναι παρόμοιος με το section .data.
Και οι δύο τομείς περιέχουν αρχικοποιημένα δεδομένα, τα οποία δηλώνονται με τον ίδιο τρόπο και έχουν την ίδια διάρκεια αποθήκευσης.
Η κύρια διαφορά μεταξύ τους είναι ότι τα δεδομένα στο section .rodata είναι μη τροποποιήσιμα, δηλαδή μόνο για ανάγνωση.
Οι σταθερές που ορίζονται με equ είναι διαφορετικές από αυτές που ορίζονται στο section .rodata.
Μια σταθερά που ορίζεται με equ δεν καταλαμβάνει χώρο στη μνήμη και αντικαθίσταται άμεσα από την τιμή της από τον assembler.
Στην πραγματικότητα, είναι απλώς ένα σύμβολο που παίρνει τη θέση αυτής της τιμής.
Από την άλλη, οι σταθερές που ορίζονται στο section .rodata αποθηκεύονται πραγματικά στη μνήμη και έχουν μια διεύθυνση.
Τα δεδομένα που δηλώνονται πρέπει να έχουν ένα όνομα συνδεδεμένο με αυτά. Αυτό το όνομα ονομάζεται ετικέτα.
Μια ετικέτα είναι ένα σύμβολο που κωδικοποιεί τη συγκεκριμένη διεύθυνση των δεδομένων στη μνήμη. Οι διευθύνσεις σε x86-64 είναι τιμές 64 bit.
Στο NASM, αν προσπαθήσεις να προσπελάσεις τα δεδομένα απευθείας με την ετικέτα τους, δεν παίρνεις τη δεσμευμένη μνήμη, αλλά τη διεύθυνσή τους:
section .data
example dq 27 ; this declares a 8-byte variable initialized with 27
section .text
fn:
mov rax, example ; this stores the address of the declared variable in rax, not its contents
...
Για να προσπελάσεις τα περιεχόμενα μιας διεύθυνσης μνήμης, χρειάζεται να κάνεις αποαναφορά σε αυτήν. Αυτό ονομάζεται έμμεση προσπέλαση.
Στο NASM, αυτό γίνεται με []:
section .data
example dq -27 ; this declares a 8-byte variable initialized with -27
section .text
fn:
mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
...
Ωστόσο, υπάρχουν περιπτώσεις όπου μπορεί να υπάρχει ασάφεια σχετικά με το μέγεθος της μνήμης στην οποία γίνεται αποαναφορά. Σε αυτές τις περιπτώσεις, πρέπει να χρησιμοποιηθεί ένα πρόθεμα που προσδιορίζει αυτό το μέγεθος.
Αυτά είναι τα σημαντικότερα προθέματα και τα μεγέθη τους σε ένα τυπικό πρόγραμμα x86-64:
| πρόθεμα | μέγεθος |
|---|---|
| byte | 1 byte |
| word | 2 bytes |
| dword | 4 bytes |
| qword | 8 bytes |
Η ίδια φόρτωση μπορεί να γραφτεί με το μέγεθος να δηλώνεται ρητά:
mov rax, qword [example] ; same dereference, size stated explicitly
Είναι καλή πρακτική να χρησιμοποιείς πάντα ένα πρόθεμα όταν κάνεις αποαναφορά σε μνήμη.
Η εγγραφή στη μνήμη γίνεται με τον ίδιο τρόπο, κάνοντας αποαναφορά σε μια διεύθυνση:
section .data
example1 db 10 ; example1 is a 1-byte memory location initialized with value 10
example2 dq -456 ; example2 is a 8-byte memory location initialized with value -456
example3 dd 54 ; example3 is a 4-byte memory location initialized with value 54
section .text
fn:
mov byte [example1], 20 ; example1 now has value 20
mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
mov dword [example3], eax ; example3 now has value equal to the contents in eax
Σημείωσε ότι μπορείς να χρησιμοποιήσεις τελεστέους μνήμης στις περισσότερες εντολές χωρίς πρώτα να φορτώσεις τα περιεχόμενα σε έναν καταχωρητή. Ωστόσο, συνήθως δεν είναι δυνατό να τους χρησιμοποιήσεις και στον τελεστέο πηγής και στον τελεστέο προορισμού, μόνο σε έναν από τους δύο:
section .data
example4 dw 4
example5 dq -8
example6 dd 15
section .text
fn:
add word [example4], 5 ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
imul rax, qword [example5] ; rax = rax * (-8)
; this is not possible -> sub dword [example6], dword [example6]
Αν και η mov μπορεί να χρησιμοποιηθεί για να αποθηκεύσει τη διεύθυνση μιας μεταβλητής σε έναν καταχωρητή, υπάρχει μια εντολή με αυτόν τον συγκεκριμένο σκοπό: η lea.
Αυτή η εντολή χρησιμοποιεί έναν τελεστέο σε μορφή μνήμης, αλλά δεν διαβάζει τη μνήμη. Αντ' αυτού, υπολογίζει την έκφραση ενεργού διεύθυνσης και γράφει το αποτέλεσμα στον τελεστέο προορισμού:
lea rax, [example] ; this stores the address of 'example' in rax
Είναι πιο ιδιωματικό να χρησιμοποιείς τη lea για να υπολογίζεις και να αποθηκεύεις διευθύνσεις μνήμης σε καταχωρητές.
Κατά την προσπέλαση θέσεων μνήμης, η προεπιλεγμένη συμπεριφορά στο NASM είναι να παράγει απόλυτες διευθύνσεις, δηλαδή σταθερές διευθύνσεις μνήμης.
Για λόγους ασφάλειας, τα εκτελέσιμα συχνά κατασκευάζονται ως PIE (εκτελέσιμο ανεξάρτητο θέσης), όπου οι περιοχές μνήμης τοποθετούνται σε τυχαιοποιημένες θέσεις.
Σε ένα PIE, η τελική διεύθυνση μιας μεταβλητής δεν είναι γνωστή τη στιγμή της σύνδεσης.
Έτσι, ο κώδικας υπολογίζει τις διευθύνσεις ως μια μετατόπιση από την τιμή σε έναν ειδικό καταχωρητή που ονομάζεται rip, ο οποίος δείχνει στην επόμενη εντολή που θα εκτελεστεί.
Αυτό συνήθως ονομάζεται σχετική διευθυνσιοδότηση ως προς το RIP.
Στο NASM μπορείς να ζητήσεις πρόσβαση σχετική ως προς το RIP με τον τελεστή rel:
mov rax, qword [rel variable]
Η σχετική διευθυνσιοδότηση μπορεί επίσης να γίνει η προεπιλογή για ένα αρχείο πηγαίου κώδικα με default rel στην αρχή του.
Όλες οι ασκήσεις σε αυτό το track μεταγλωττίζονται και συνδέονται ως PIE, οπότε θα πρέπει να χρησιμοποιείς το rel για να παράγεις σχετικές διευθύνσεις.
Οι ετικέτες (συναρτήσεις και δεδομένα) που ορίζονται σε οποιονδήποτε τομέα (π.χ. .text, .data, .rodata) είναι ορατές μέσα στο ίδιο αρχείο πηγαίου κώδικα.
Αν δηλωθούν global, είναι ορατές και σε άλλα αρχεία πηγαίου κώδικα.
Αντίστροφα, οι ετικέτες που ορίζονται σε άλλα αρχεία πηγαίου κώδικα είναι ορατές στο τρέχον αρχείο πηγαίου κώδικα αν δηλωθούν extern.
Σε αυτή την περίπτωση, δεν υπάρχει ένδειξη για το μέγεθος των δεδομένων στη assembly, αυτό πρέπει να είναι γνωστό εκ των προτέρων.
default rel
section .data
global number1 ; 'number1' is a variable visible to other source files
number1 db 200
extern number2 ; 'number2' is a variable visible to the current source file, but defined in another
section .text
extern sum ; sum is a function visible to the current source file, but defined in another
fn:
mov dil, byte [number1]
mov sil, byte [number2]
call sum
...
Ο φίλος σου ο José είναι δάσκαλος σε ένα τοπικό σχολείο. Είχε μια ιδέα για μερικά διασκεδαστικά πειράματα που δείχνουν πώς μπορούν να συνδυαστούν χρώματα για να παραχθούν διαφορετικά.
Ζήτησε τη βοήθειά σου για αυτά τα πειράματα.
Σε αυτή την άσκηση, ένα χρώμα αναπαρίσταται από έναν αριθμό 32-bit (4-byte), που κωδικοποιεί την τιμή RGB του.
Μια τιμή RGB αποτελείται από 3 κανάλια, κόκκινο, πράσινο και μπλε, καθένα από τα οποία καταλαμβάνει 8 bit (1 byte).
Το τέταρτο byte συνήθως προορίζεται για το κανάλι άλφα, αλλά σε αυτή την άσκηση η τιμή του θα είναι κενή (0).
RGB για ένα χρώμαΟι τιμές για κάθε χρώμα είναι ήδη αποθηκευμένες σε έναν πίνακα, που ορίζεται σε ένα άλλο πηγαίο αρχείο. Ένα χρώμα αναγνωρίζεται από μια μοναδική διεύθυνση σε αυτόν τον πίνακα.
Όρισε μια συνάρτηση get_color_value που επιστρέφει την τιμή 32-bit για ένα χρώμα.
Αυτή η συνάρτηση παίρνει ως παράμετρο μια έγκυρη διεύθυνση για αυτό το χρώμα στον πίνακα χρωμάτων.
get_color_value(black)
// => 0
Υπόδειξη - Τα 32 bit είναι ισοδύναμα με 4 byte.
Για να αναμείξει διαφορετικά χρώματα, ο José θα καθορίσει πρώτα ένα βασικό και θα αλλάξει μόνο το δευτερεύον χρώμα που συνδυάζεται με αυτό.
Όρισε μια συνάρτηση add_base_color που αποθηκεύει την τιμή 32-bit για ένα χρώμα στη μεταβλητή base_color, ώστε να μπορεί να χρησιμοποιηθεί αργότερα.
Αυτή η συνάρτηση δεν έχει τιμή επιστροφής και παίρνει ως παράμετρο τη διεύθυνση του χρώματος στον πίνακα χρωμάτων.
Η μεταβλητή base_color ορίζεται από εσένα και πρέπει να είναι προσβάσιμη από άλλα πηγαία αρχεία.
Δε θα υπάρχει περισσότερο από 1 βασικό χρώμα την ίδια στιγμή. Αν προστεθεί ένα νέο βασικό χρώμα, το παλιό απορρίπτεται.
Από προεπιλογή, στην αρχή του προγράμματος, το base_color θα πρέπει να αρχικοποιείται με την τιμή 32-bit για το λευκό, που είναι 0xFFFFFF00.
Υπόδειξη - Ο NASM δέχεται αριθμούς που ορίζονται σε δεκαεξαδικό σύστημα χρησιμοποιώντας 0x στην αρχή, όπως στο 0xFFFFFF00.
Ο José περιμένει να κάνει πολλούς συνδυασμούς χρησιμοποιώντας πρωτεύοντα χρώματα, οπότε θέλει να τα έχει χωριστά για γρήγορη πρόσβαση.
Εφόσον χρησιμοποιεί RGB για να αναπαραστήσει χρώματα, τα πρωτεύοντα χρώματα είναι:
RED, με την τιμή 0xFF000000.GREEN, με την τιμή 0x00FF0000.BLUE, με την τιμή 0x0000FF00.Όρισε μία σταθερά για καθένα από αυτά τα χρώματα. Αυτές οι σταθερές πρέπει να είναι προσβάσιμες από άλλα πηγαία αρχεία.
Τα χρώματα θα πρέπει να συνδυάζονται σύμφωνα με μια combining_function που ορίζεται σε ένα άλλο πηγαίο αρχείο.
Αυτή η συνάρτηση παίρνει ως παραμέτρους τις τιμές 32-bit για το base_color και για ένα δευτερεύον χρώμα που θα αναμειχθεί με αυτό.
Επιστρέφει την τιμή 32-bit για το συνδυασμένο χρώμα.
Όρισε μια συνάρτηση make_color_combination που συνδυάζει δύο χρώματα και αποθηκεύει το αποτέλεσμα στη μνήμη.
Αυτή η συνάρτηση δεν έχει τιμή επιστροφής και παίρνει ως παραμέτρους, με αυτή τη σειρά:
Πρόσεξε ότι η combining_function μπορεί να τροποποιήσει τις τιμές στους καταχωρητές που χρησιμοποιείς.
Βεβαιώσου ότι αποθηκεύεις στη μνήμη οποιαδήποτε μεταβλητή χρειάζεσαι πριν καλέσεις τη συνάρτηση.
Γράψου στο Exercism για να μάθεις και να κατακτήσεις x86-64 Assembly με 22 έννοιες130 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.