QEMU.js: τώρα σοβαρό και με WASM

Μια φορά κι έναν καιρό αποφάσισα για πλάκα αποδείξει την αναστρεψιμότητα της διαδικασίας και μάθετε πώς να δημιουργείτε JavaScript (ακριβέστερα Asm.js) από κώδικα μηχανής. Το QEMU επιλέχθηκε για το πείραμα και λίγο αργότερα γράφτηκε ένα άρθρο στο Habr. Στα σχόλια με συμβούλεψαν να ξαναφτιάξω το έργο στο WebAssembly και μάλιστα να παραιτηθώ σχεδόν τέλειωσα Κάπως δεν ήθελα το έργο... Η δουλειά συνεχιζόταν, αλλά πολύ αργά, και τώρα, πρόσφατα εμφανίστηκε σε αυτό το άρθρο σχόλιο με θέμα "Λοιπόν πώς τελείωσαν όλα;" Ως απάντηση στη λεπτομερή απάντησή μου, άκουσα "Αυτό ακούγεται σαν άρθρο". Λοιπόν, αν μπορείτε, θα υπάρχει ένα άρθρο. Ίσως κάποιος το βρει χρήσιμο. Από αυτό ο αναγνώστης θα μάθει μερικά στοιχεία σχετικά με το σχεδιασμό των backend δημιουργίας κώδικα QEMU, καθώς και πώς να γράψει έναν μεταγλωττιστή Just-in-Time για μια εφαρμογή Ιστού.

εργασίες

Εφόσον είχα ήδη μάθει πώς να μεταφέρω «κάπως» το QEMU σε JavaScript, αυτή τη φορά αποφασίστηκε να το κάνω με σύνεση και να μην επαναλάβω παλιά λάθη.

Σφάλμα νούμερο ένα: διακλάδωση από το σημείο απελευθέρωσης

Το πρώτο μου λάθος ήταν να διαχωρίσω την έκδοσή μου από την upstream έκδοση 2.4.1. Τότε μου φάνηκε καλή ιδέα: αν υπάρχει απελευθέρωση σημείου, τότε είναι πιθανώς πιο σταθερό από το απλό 2.4, και ακόμη περισσότερο από το κλάδο master. Και επειδή σχεδίαζα να προσθέσω αρκετά δικά μου σφάλματα, δεν χρειαζόμουν καθόλου κανέναν άλλον. Μάλλον έτσι έγινε. Αλλά εδώ είναι το πράγμα: το QEMU δεν μένει ακίνητο, και σε κάποιο σημείο ανακοίνωσαν ακόμη και βελτιστοποίηση του παραγόμενου κώδικα κατά 10 τοις εκατό. «Ναι, τώρα θα παγώσω», σκέφτηκα και χάλασα. Εδώ πρέπει να κάνουμε μια παρέκβαση: λόγω της φύσης ενός νήματος του QEMU.js και του γεγονότος ότι το αρχικό QEMU δεν συνεπάγεται την απουσία πολλαπλών νημάτων (δηλαδή, τη δυνατότητα ταυτόχρονης λειτουργίας πολλών άσχετων διαδρομών κώδικα και όχι μόνο το "χρησιμοποιήστε όλους τους πυρήνες") είναι κρίσιμο γι 'αυτό, οι κύριες λειτουργίες των νημάτων έπρεπε να το "αποδείξω" για να μπορώ να καλώ από έξω. Αυτό δημιούργησε ορισμένα φυσικά προβλήματα κατά τη συγχώνευση. Ωστόσο, το γεγονός ότι ορισμένες από τις αλλαγές από τον κλάδο master, με το οποίο προσπάθησα να συγχωνεύσω τον κώδικά μου, ήταν επίσης επιλεγμένα στο σημείο κυκλοφορίας (και επομένως στο υποκατάστημά μου) επίσης πιθανότατα δεν θα είχαν πρόσθετη ευκολία.

Γενικά, αποφάσισα ότι έχει ακόμα νόημα να πετάξω το πρωτότυπο, να το αποσυναρμολογήσω για εξαρτήματα και να φτιάξω μια νέα έκδοση από την αρχή με βάση κάτι πιο φρέσκο ​​και τώρα από master.

Λάθος δεύτερο: μεθοδολογία TLP

Στην ουσία, αυτό δεν είναι λάθος, γενικά, είναι απλώς ένα χαρακτηριστικό της δημιουργίας ενός έργου σε συνθήκες πλήρους παρανόησης τόσο του "πού και πώς να κινηθούμε;" και γενικά "θα φτάσουμε εκεί;" Σε αυτές τις συνθήκες αδέξιος προγραμματισμός ήταν μια δικαιολογημένη επιλογή, αλλά, φυσικά, δεν ήθελα να το επαναλάβω άσκοπα. Αυτή τη φορά ήθελα να το κάνω με σύνεση: ατομικές δεσμεύσεις, συνειδητές αλλαγές κώδικα (και όχι «συμβολοσειρά τυχαίων χαρακτήρων μέχρι να μεταγλωττιστεί (με προειδοποιήσεις)», όπως είπε κάποτε ο Linus Torvalds για κάποιον, σύμφωνα με το Wikiquote), κ.λπ.

Λάθος νούμερο τρία: να μπεις στο νερό χωρίς να ξέρεις το Ford

Ακόμα δεν το έχω ξεφορτωθεί εντελώς, αλλά τώρα αποφάσισα να μην ακολουθήσω καθόλου το μονοπάτι της ελάχιστης αντίστασης και να το κάνω "ως ενήλικας", δηλαδή να γράψω το backend μου στο TCG από την αρχή, για να μην για να πρέπει να πω αργότερα, "Ναι, αυτό είναι φυσικά, αργά, αλλά δεν μπορώ να ελέγξω τα πάντα - έτσι γράφεται το TCI..." Επιπλέον, αυτό φαινόταν αρχικά ως μια προφανής λύση, αφού Δημιουργώ δυαδικό κώδικα. Όπως λένε, «η Γάνδη μαζεύτηκεу, αλλά όχι αυτό»: ο κώδικας είναι, φυσικά, δυαδικός, αλλά ο έλεγχος δεν μπορεί απλώς να μεταφερθεί σε αυτόν - πρέπει να ωθηθεί ρητά στο πρόγραμμα περιήγησης για μεταγλώττιση, με αποτέλεσμα ένα συγκεκριμένο αντικείμενο από τον κόσμο JS, το οποίο πρέπει ακόμα να να σωθεί κάπου. Ωστόσο, σε κανονικές αρχιτεκτονικές RISC, από όσο καταλαβαίνω, μια τυπική κατάσταση είναι η ανάγκη ρητής επαναφοράς της κρυφής μνήμης εντολών για αναδημιουργημένο κώδικα - εάν αυτό δεν είναι αυτό που χρειαζόμαστε, τότε, σε κάθε περίπτωση, είναι κοντά. Επιπλέον, από την τελευταία μου προσπάθεια, έμαθα ότι ο έλεγχος δεν φαίνεται να μεταφέρεται στη μέση του μπλοκ μετάφρασης, επομένως δεν χρειαζόμαστε πραγματικά bytecode που να ερμηνεύεται από οποιαδήποτε μετατόπιση και μπορούμε απλά να τον δημιουργήσουμε από τη συνάρτηση στο TB .

Ήρθαν και κλώτσησαν

Αν και άρχισα να ξαναγράφω τον κώδικα τον Ιούλιο, ένα μαγικό λάκτισμα πέρασε απαρατήρητο: συνήθως γράμματα από το GitHub φτάνουν ως ειδοποιήσεις σχετικά με απαντήσεις σε ζητήματα και αιτήματα έλξης, αλλά εδώ: ξαφνικά αναφορά στο νήμα Binaryen ως backend qemu στο πλαίσιο, «Έκανε κάτι τέτοιο, ίσως πει κάτι». Μιλούσαμε για τη χρήση της σχετικής βιβλιοθήκης του Emscripten Binaryen για να δημιουργήσετε το WASM JIT. Λοιπόν, είπα ότι έχετε μια άδεια Apache 2.0 εκεί και το QEMU στο σύνολό του διανέμεται υπό το GPLv2 και δεν είναι πολύ συμβατά. Ξαφνικά αποδείχθηκε ότι μια άδεια μπορεί να είναι διορθώστε το με κάποιο τρόπο (Δεν ξέρω: ίσως αλλάξει, ίσως διπλή άδεια, ίσως κάτι άλλο...). Αυτό, φυσικά, με έκανε χαρούμενο, γιατί μέχρι τότε το είχα ήδη κοιτάξει προσεκτικά δυαδική μορφή WebAssembly, και ήμουν κάπως λυπημένος και ακατανόητος. Υπήρχε επίσης μια βιβλιοθήκη που θα καταβρόχθιζε τα βασικά μπλοκ με το γράφημα μετάβασης, θα παρήγαγε τον bytecode και ακόμη και θα τον εκτελούσε στον ίδιο τον διερμηνέα, εάν ήταν απαραίτητο.

Μετά ήταν κι άλλα μια επιστολή στη λίστα αλληλογραφίας QEMU, αλλά αυτό αφορά περισσότερο την ερώτηση, "Ποιος το χρειάζεται ούτως ή άλλως;" Και αυτό είναι ξαφνικά, αποδείχθηκε ότι ήταν απαραίτητο. Τουλάχιστον, μπορείτε να ξύσετε μαζί τέτοιες δυνατότητες χρήσης εάν λειτουργεί περισσότερο ή λιγότερο γρήγορα:

  • λανσάροντας κάτι εκπαιδευτικό χωρίς καμία απολύτως εγκατάσταση
  • εικονικοποίηση στο iOS, όπου, σύμφωνα με φήμες, η μόνη εφαρμογή που έχει το δικαίωμα δημιουργίας κώδικα εν κινήσει είναι μια μηχανή JS (αληθεύει αυτό;)
  • επίδειξη mini-OS - single-floppy, ενσωματωμένο, όλα τα είδη υλικολογισμικού κ.λπ...

Λειτουργίες χρόνου εκτέλεσης προγράμματος περιήγησης

Όπως είπα ήδη, το QEMU είναι συνδεδεμένο με multithreading, αλλά το πρόγραμμα περιήγησης δεν το έχει. Λοιπόν, δηλαδή, όχι... Στην αρχή δεν υπήρχε καθόλου, μετά εμφανίστηκαν οι WebWorkers - από όσο καταλαβαίνω, αυτό είναι πολυνηματικό που βασίζεται στη μετάδοση μηνυμάτων χωρίς κοινές μεταβλητές. Φυσικά, αυτό δημιουργεί σημαντικά προβλήματα κατά τη μεταφορά υπάρχοντος κώδικα με βάση το μοντέλο κοινής μνήμης. Στη συνέχεια, υπό την πίεση του κοινού, εφαρμόστηκε και με το όνομα SharedArrayBuffers. Εισήχθη σταδιακά, γιόρτασαν το λανσάρισμά του σε διαφορετικά προγράμματα περιήγησης, μετά γιόρτασαν την Πρωτοχρονιά και μετά το Meltdown... Μετά από το οποίο κατέληξαν στο συμπέρασμα ότι η χονδρική ή χονδροειδής μέτρηση του χρόνου, αλλά με τη βοήθεια κοινής μνήμης και νήμα που αυξάνει τον μετρητή, είναι το ίδιο θα λειτουργήσει με μεγάλη ακρίβεια. Έτσι, απενεργοποιήσαμε την πολλαπλή νήμα με κοινόχρηστη μνήμη. Φαίνεται ότι αργότερα το επανενεργοποίησαν, αλλά, όπως έγινε σαφές από το πρώτο πείραμα, υπάρχει ζωή χωρίς αυτό, και αν ναι, θα προσπαθήσουμε να το κάνουμε χωρίς να βασιζόμαστε στην πολυνηματική.

Το δεύτερο χαρακτηριστικό είναι η αδυναμία χειρισμών χαμηλού επιπέδου με τη στοίβα: δεν μπορείτε απλώς να πάρετε, να αποθηκεύσετε το τρέχον πλαίσιο και να μεταβείτε σε ένα νέο με μια νέα στοίβα. Η διαχείριση της στοίβας κλήσεων γίνεται από την εικονική μηχανή JS. Φαίνεται, ποιο είναι το πρόβλημα, αφού ακόμα αποφασίσαμε να διαχειριστούμε τις πρώτες ροές εντελώς χειροκίνητα; Το γεγονός είναι ότι το block I/O στο QEMU υλοποιείται μέσω κορουτινών, και εδώ είναι χρήσιμοι οι χειρισμοί στοίβας χαμηλού επιπέδου. Ευτυχώς, το Emscipten περιέχει ήδη έναν μηχανισμό για ασύγχρονες λειτουργίες, ακόμη και δύο: Ασύγχρονη и Εμπειρογνώμονας. Το πρώτο λειτουργεί μέσω σημαντικού bloat στον κώδικα JavaScript που δημιουργείται και δεν υποστηρίζεται πλέον. Ο δεύτερος είναι ο τρέχων "σωστός τρόπος" και λειτουργεί μέσω δημιουργίας bytecode για τον εγγενή διερμηνέα. Λειτουργεί, φυσικά, αργά, αλλά δεν διογκώνει τον κώδικα. Είναι αλήθεια ότι η υποστήριξη για κορουτίνες για αυτόν τον μηχανισμό έπρεπε να συνεισφέρει ανεξάρτητα (υπήρχαν ήδη γραμμένες κορουτίνες για το Asyncify και υπήρχε μια υλοποίηση περίπου του ίδιου API για το Emterpreter, απλά έπρεπε να τις συνδέσετε).

Προς το παρόν, δεν έχω καταφέρει ακόμα να χωρίσω τον κώδικα σε έναν που έχει μεταγλωττιστεί στο WASM και ερμηνεύεται χρησιμοποιώντας Emterpreter, επομένως οι συσκευές μπλοκ δεν λειτουργούν ακόμα (δείτε στην επόμενη σειρά, όπως λένε...). Δηλαδή, στο τέλος θα πρέπει να πάρετε κάτι σαν αυτό το αστείο πολυεπίπεδο πράγμα:

  • ερμηνευμένο μπλοκ I/O. Λοιπόν, πραγματικά περιμένατε εξομοίωση NVMe με εγγενή απόδοση; 🙂
  • στατικά μεταγλωττισμένος κύριος κώδικας QEMU (μεταφραστής, άλλες προσομοιωμένες συσκευές κ.λπ.)
  • δυναμικά μεταγλωττισμένος κώδικας επισκέπτη στο WASM

Χαρακτηριστικά των πηγών QEMU

Όπως πιθανώς ήδη μαντέψατε, ο κώδικας για την εξομοίωση αρχιτεκτονικών επισκεπτών και ο κώδικας για τη δημιουργία εντολών κεντρικού υπολογιστή διαχωρίζονται στο QEMU. Στην πραγματικότητα, είναι ακόμη λίγο πιο δύσκολο:

  • υπάρχουν φιλοξενούμενες αρχιτεκτονικές
  • υπάρχει επιταχυντές, συγκεκριμένα, KVM για εικονικοποίηση υλικού σε Linux (για φιλοξενούμενα και κεντρικά συστήματα συμβατά μεταξύ τους), TCG για δημιουργία κώδικα JIT οπουδήποτε. Ξεκινώντας με το QEMU 2.9, εμφανίστηκε η υποστήριξη για το πρότυπο εικονικοποίησης υλικού HAXM στα Windows (λεπτομέρειες)
  • εάν χρησιμοποιείται TCG και όχι εικονικοποίηση υλικού, τότε έχει ξεχωριστή υποστήριξη δημιουργίας κώδικα για κάθε αρχιτεκτονική κεντρικού υπολογιστή, καθώς και για τον καθολικό διερμηνέα
  • ... και γύρω από όλα αυτά - εξομοιούμενα περιφερειακά, διεπαφή χρήστη, μετεγκατάσταση, επανάληψη εγγραφής κ.λπ.

Με την ευκαιρία, ξέρατε: Το QEMU μπορεί να μιμηθεί όχι μόνο ολόκληρο τον υπολογιστή, αλλά και τον επεξεργαστή για μια ξεχωριστή διεργασία χρήστη στον πυρήνα του κεντρικού υπολογιστή, ο οποίος χρησιμοποιείται, για παράδειγμα, από το fuzzer AFL για δυαδικά όργανα. Ίσως κάποιος θα ήθελε να μεταφέρει αυτόν τον τρόπο λειτουργίας του QEMU στο JS; 😉

Όπως τα περισσότερα μακροχρόνια δωρεάν λογισμικό, το QEMU δημιουργείται μέσω της κλήσης configure и make. Ας υποθέσουμε ότι αποφασίσατε να προσθέσετε κάτι: ένα backend TCG, εφαρμογή νήματος, κάτι άλλο. Μην βιαστείτε να είστε χαρούμενοι/τρομακωμένοι (υπογραμμίστε ανάλογα) με την προοπτική επικοινωνίας με το Autoconf - στην πραγματικότητα, configure Τα QEMU είναι προφανώς γραμμένα από τον εαυτό τους και δεν δημιουργούνται από τίποτα.

WebAssembly

Τι είναι λοιπόν αυτό το πράγμα που ονομάζεται WebAssembly (γνωστό και ως WASM); Αυτό είναι μια αντικατάσταση του Asm.js, χωρίς πλέον να προσποιείται ότι είναι έγκυρος κώδικας JavaScript. Αντίθετα, είναι καθαρά δυαδικό και βελτιστοποιημένο, και ακόμη και η απλή εγγραφή ενός ακέραιου σε αυτό δεν είναι πολύ απλή: για συμπαγή, αποθηκεύεται στη μορφή LEB128.

Μπορεί να έχετε ακούσει για τον αλγόριθμο επανακυκλοφορίας για το Asm.js - πρόκειται για την επαναφορά των οδηγιών ελέγχου ροής "υψηλού επιπέδου" (δηλαδή, εάν-τότε-άλλο, βρόχοι κ.λπ.), για τον οποίο έχουν σχεδιαστεί οι κινητήρες JS, από το χαμηλού επιπέδου LLVM IR, πιο κοντά στον κώδικα μηχανής που εκτελείται από τον επεξεργαστή. Φυσικά, η ενδιάμεση αναπαράσταση του QEMU είναι πιο κοντά στη δεύτερη. Φαίνεται ότι εδώ είναι, bytecode, το τέλος του μαρτυρίου... Και μετά υπάρχουν μπλοκ, αν-τότε-άλλο και βρόχοι!..

Και αυτός είναι ένας άλλος λόγος για τον οποίο το Binaryen είναι χρήσιμο: μπορεί φυσικά να δέχεται μπλοκ υψηλού επιπέδου κοντά σε αυτά που θα ήταν αποθηκευμένα στο WASM. Αλλά μπορεί επίσης να παράγει κώδικα από ένα γράφημα βασικών μπλοκ και μεταβάσεων μεταξύ τους. Λοιπόν, έχω ήδη πει ότι κρύβει τη μορφή αποθήκευσης WebAssembly πίσω από το βολικό C/C++ API.

TCG (Tiny Code Generator)

ΤΕΕ ήταν αρχικά backend για τον μεταγλωττιστή C. Τότε, προφανώς, δεν άντεξε τον ανταγωνισμό με το GCC, αλλά τελικά βρήκε τη θέση του στο QEMU ως μηχανισμός δημιουργίας κώδικα για την πλατφόρμα υποδοχής. Υπάρχει επίσης ένα backend TCG που δημιουργεί έναν αφηρημένο bytecode, ο οποίος εκτελείται αμέσως από τον διερμηνέα, αλλά αποφάσισα να αποφύγω τη χρήση του αυτή τη φορά. Ωστόσο, το γεγονός ότι στο QEMU είναι ήδη δυνατό να ενεργοποιηθεί η μετάβαση στην παραγόμενη φυματίωση μέσω της συνάρτησης tcg_qemu_tb_exec, μου φάνηκε πολύ χρήσιμο.

Για να προσθέσετε ένα νέο backend TCG στο QEMU, πρέπει να δημιουργήσετε έναν υποκατάλογο tcg/<имя архитектуры> (σε αυτήν την περίπτωση, tcg/binaryen), και περιέχει δύο αρχεία: tcg-target.h и tcg-target.inc.c и κανω ΕΓΓΡΑΦΗ πρόκειται για configure. Μπορείτε να βάλετε άλλα αρχεία εκεί, αλλά, όπως μπορείτε να μαντέψετε από τα ονόματα αυτών των δύο, θα συμπεριληφθούν και τα δύο κάπου: το ένα ως κανονικό αρχείο κεφαλίδας (περιλαμβάνεται στο tcg/tcg.h, και αυτό βρίσκεται ήδη σε άλλα αρχεία στους καταλόγους tcg, accel και όχι μόνο), το άλλο - μόνο ως απόσπασμα κώδικα tcg/tcg.c, αλλά έχει πρόσβαση στις στατικές λειτουργίες του.

Αποφασίζοντας ότι θα αφιερώνω πολύ χρόνο σε λεπτομερείς έρευνες για το πώς λειτουργεί, απλώς αντέγραψα τους "σκελετούς" αυτών των δύο αρχείων από μια άλλη υλοποίηση backend, υποδεικνύοντας ειλικρινά αυτό στην κεφαλίδα άδειας χρήσης.

αρχείο tcg-target.h περιέχει κυρίως ρυθμίσεις στη φόρμα #define-μικρό:

  • πόσοι καταχωρητές και τι πλάτος υπάρχουν στην αρχιτεκτονική προορισμού (έχουμε όσες θέλουμε, όσες θέλουμε - το ερώτημα είναι περισσότερο για το τι θα δημιουργηθεί σε πιο αποτελεσματικό κώδικα από το πρόγραμμα περιήγησης στην αρχιτεκτονική "εντελώς στόχος" ...)
  • ευθυγράμμιση εντολών κεντρικού υπολογιστή: στο x86, ακόμη και στο TCI, οι οδηγίες δεν είναι καθόλου ευθυγραμμισμένες, αλλά θα βάλω στο buffer κώδικα όχι καθόλου οδηγίες, αλλά δείκτες στις δομές βιβλιοθήκης Binaryen, οπότε θα πω: 4 byte
  • ποιες προαιρετικές οδηγίες μπορεί να δημιουργήσει το backend - συμπεριλαμβάνουμε ό,τι βρίσκουμε στο Binaryen, αφήνουμε τον επιταχυντή να χωρίσει τα υπόλοιπα σε πιο απλά
  • Ποιο είναι το κατά προσέγγιση μέγεθος της κρυφής μνήμης TLB που ζητείται από το backend. Το γεγονός είναι ότι στο QEMU όλα είναι σοβαρά: αν και υπάρχουν βοηθητικές λειτουργίες που εκτελούν φόρτωση/αποθήκευση λαμβάνοντας υπόψη τον επισκέπτη MMU (πού θα ήμασταν χωρίς αυτό τώρα;), αποθηκεύουν τη μνήμη cache μετάφρασης με τη μορφή μιας δομής, το η επεξεργασία των οποίων είναι βολικό να ενσωματωθεί απευθείας σε μπλοκ εκπομπής. Το ερώτημα είναι, ποια μετατόπιση σε αυτή τη δομή επεξεργάζεται πιο αποτελεσματικά από μια μικρή και γρήγορη ακολουθία εντολών;
  • εδώ μπορείτε να τροποποιήσετε το σκοπό ενός ή δύο δεσμευμένων καταχωρητών, να ενεργοποιήσετε την κλήση TB μέσω μιας συνάρτησης και προαιρετικά να περιγράψετε μερικά μικρά inline-Λειτουργεί όπως flush_icache_range (αλλά δεν είναι αυτή η περίπτωσή μας)

αρχείο tcg-target.inc.c, φυσικά, είναι συνήθως πολύ μεγαλύτερο σε μέγεθος και περιέχει αρκετές υποχρεωτικές λειτουργίες:

  • αρχικοποίηση, συμπεριλαμβανομένων περιορισμών σχετικά με το ποιες εντολές μπορούν να λειτουργήσουν σε ποιους τελεστές. Αντιγράφηκε κατάφωρα από εμένα από άλλο backend
  • συνάρτηση που παίρνει μία εντολή εσωτερικού bytecode
  • Μπορείτε επίσης να βάλετε βοηθητικές λειτουργίες εδώ και μπορείτε επίσης να χρησιμοποιήσετε στατικές λειτουργίες από tcg/tcg.c

Για τον εαυτό μου, επέλεξα την ακόλουθη στρατηγική: στις πρώτες λέξεις του επόμενου μπλοκ μετάφρασης, έγραψα τέσσερις δείκτες: ένα σημάδι έναρξης (μια ορισμένη τιμή στην περιοχή 0xFFFFFFFF, το οποίο καθόρισε την τρέχουσα κατάσταση του TB), το πλαίσιο, την παραγόμενη μονάδα και τον μαγικό αριθμό για εντοπισμό σφαλμάτων. Στην αρχή τοποθετήθηκε το σήμα 0xFFFFFFFF - nΌπου n - ένας μικρός θετικός αριθμός, και κάθε φορά που εκτελούνταν μέσω του διερμηνέα αυξανόταν κατά 1. Όταν έφτασε 0xFFFFFFFE, πραγματοποιήθηκε η μεταγλώττιση, η μονάδα αποθηκεύτηκε στον πίνακα συναρτήσεων, εισήχθη σε μια μικρή "εκκίνηση", στην οποία η εκτέλεση έγινε από tcg_qemu_tb_execκαι η μονάδα αφαιρέθηκε από τη μνήμη QEMU.

Για να παραφράσω τα κλασικά, «Δεκατρίνι, πόσο είναι συνυφασμένο σε αυτόν τον ήχο για την καρδιά του προέργου…». Ωστόσο, κάπου διέρρεε η μνήμη. Επιπλέον, η διαχείριση της μνήμης ήταν από την QEMU! Είχα έναν κωδικό που, όταν έγραφα την επόμενη οδηγία (καλά, δηλαδή έναν δείκτη), διέγραψε αυτόν του οποίου ο σύνδεσμος βρισκόταν σε αυτό το μέρος νωρίτερα, αλλά αυτό δεν βοήθησε. Στην πραγματικότητα, στην απλούστερη περίπτωση, το QEMU εκχωρεί μνήμη κατά την εκκίνηση και γράφει εκεί τον κώδικα που δημιουργείται. Όταν τελειώσει το buffer, ο κώδικας πετιέται έξω και ο επόμενος αρχίζει να γράφεται στη θέση του.

Αφού μελέτησα τον κώδικα, συνειδητοποίησα ότι το κόλπο με τον μαγικό αριθμό μου επέτρεψε να μην αποτύχω στην καταστροφή σωρού, ελευθερώνοντας κάτι λάθος σε ένα μη αρχικοποιημένο buffer στο πρώτο πέρασμα. Αλλά ποιος ξαναγράφει το buffer για να παρακάμψει τη λειτουργία μου αργότερα; Όπως συμβουλεύουν οι προγραμματιστές του Emscripten, όταν αντιμετώπισα πρόβλημα, μετέφεραν τον κώδικα που προέκυψε πίσω στην εγγενή εφαρμογή, ρύθμισα το Mozilla Record-Replay σε αυτό... Γενικά, στο τέλος συνειδητοποίησα ένα απλό πράγμα: για κάθε μπλοκ, ένα struct TranslationBlock με την περιγραφή του. Μαντέψτε πού... Σωστά, λίγο πριν το μπλοκ ακριβώς στο buffer. Συνειδητοποιώντας αυτό, αποφάσισα να σταματήσω να χρησιμοποιώ πατερίτσες (τουλάχιστον μερικές) και απλά πέταξα τον μαγικό αριθμό και μετέφερα τις υπόλοιπες λέξεις στο struct TranslationBlock, δημιουργώντας μια μεμονωμένη συνδεδεμένη λίστα που μπορεί να διασχιστεί γρήγορα όταν επαναφέρεται η προσωρινή μνήμη μετάφρασης και να ελευθερωθεί μνήμη.

Μερικά δεκανίκια παραμένουν: για παράδειγμα, σημειωμένοι δείκτες στο buffer κώδικα - μερικά από αυτά είναι απλά BinaryenExpressionRef, δηλαδή, εξετάζουν τις εκφράσεις που πρέπει να τεθούν γραμμικά στο βασικό μπλοκ που δημιουργείται, μέρος είναι η προϋπόθεση για τη μετάβαση μεταξύ BB, μέρος είναι πού να πάει. Λοιπόν, υπάρχουν ήδη έτοιμα μπλοκ για το Relooper που πρέπει να συνδεθούν σύμφωνα με τις συνθήκες. Για να τα διακρίνουμε, χρησιμοποιείται η υπόθεση ότι είναι όλα ευθυγραμμισμένα κατά τουλάχιστον τέσσερα byte, ώστε να μπορείτε να χρησιμοποιήσετε με ασφάλεια τα λιγότερο σημαντικά δύο bit για την ετικέτα, απλά πρέπει να θυμάστε να την αφαιρέσετε εάν είναι απαραίτητο. Παρεμπιπτόντως, τέτοιες ετικέτες χρησιμοποιούνται ήδη στο QEMU για να υποδείξουν τον λόγο εξόδου από τον βρόχο TCG.

Χρήση Binaryen

Οι μονάδες στο WebAssembly περιέχουν συναρτήσεις, καθεμία από τις οποίες περιέχει ένα σώμα, το οποίο είναι μια έκφραση. Οι εκφράσεις είναι μονομερείς και δυαδικές πράξεις, μπλοκ που αποτελούνται από λίστες άλλων παραστάσεων, ροή ελέγχου κ.λπ. Όπως είπα ήδη, η ροή ελέγχου εδώ οργανώνεται ακριβώς ως κλάδοι υψηλού επιπέδου, βρόχοι, κλήσεις συναρτήσεων κ.λπ. Τα επιχειρήματα στις συναρτήσεις δεν μεταβιβάζονται στη στοίβα, αλλά ρητά, όπως και στο JS. Υπάρχουν επίσης καθολικές μεταβλητές, αλλά δεν τις έχω χρησιμοποιήσει, επομένως δεν θα σας πω για αυτές.

Οι συναρτήσεις έχουν επίσης τοπικές μεταβλητές, αριθμημένες από το μηδέν, τύπου: int32 / int64 / float / double. Σε αυτήν την περίπτωση, οι πρώτες n τοπικές μεταβλητές είναι τα ορίσματα που μεταβιβάζονται στη συνάρτηση. Λάβετε υπόψη ότι παρόλο που όλα εδώ δεν είναι εντελώς χαμηλού επιπέδου όσον αφορά τη ροή ελέγχου, οι ακέραιοι αριθμοί εξακολουθούν να μην φέρουν το χαρακτηριστικό "signed/unsigned": πώς συμπεριφέρεται ο αριθμός εξαρτάται από τον κωδικό λειτουργίας.

Σε γενικές γραμμές, το Binaryen παρέχει απλό C-API: δημιουργείτε μια ενότητα, σε αυτόν δημιουργήστε εκφράσεις - μονομερείς, δυαδικές, μπλοκ από άλλες εκφράσεις, έλεγχος ροής κ.λπ. Στη συνέχεια δημιουργείτε μια συνάρτηση με σώμα μια έκφραση. Εάν, όπως εγώ, έχετε ένα γράφημα μετάβασης χαμηλού επιπέδου, το στοιχείο relooper θα σας βοηθήσει. Από όσο καταλαβαίνω, είναι δυνατός ο έλεγχος υψηλού επιπέδου της ροής εκτέλεσης σε ένα μπλοκ, αρκεί να μην υπερβαίνει τα όρια του μπλοκ - δηλαδή είναι δυνατό να γίνει εσωτερική γρήγορη διαδρομή / αργή διακλάδωση διαδρομής μέσα στον ενσωματωμένο κώδικα επεξεργασίας κρυφής μνήμης TLB, αλλά όχι για παρεμβολή στη ροή "εξωτερικού" ελέγχου . Όταν ελευθερώνετε ένα relooper, τα μπλοκ του ελευθερώνονται, όταν ελευθερώνετε ένα module, οι εκφράσεις, οι συναρτήσεις κ.λπ. που έχουν εκχωρηθεί σε αυτό εξαφανίζονται αρένα.

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

Έτσι, για να δημιουργήσετε τον κώδικα που χρειάζεστε

// настроить глобальные параметры (можно поменять потом)
BinaryenSetAPITracing(0);

BinaryenSetOptimizeLevel(3);
BinaryenSetShrinkLevel(2);

// создать модуль
BinaryenModuleRef MODULE = BinaryenModuleCreate();

// описать типы функций (как создаваемых, так и вызываемых)
helper_type  BinaryenAddFunctionType(MODULE, "helper-func", BinaryenTypeInt32(), int32_helper_args, ARRAY_SIZE(int32_helper_args));
// (int23_helper_args приоб^Wсоздаются отдельно)

// сконструировать супер-мега выражение
// ... ну тут уж вы как-нибудь сами :)

// потом создать функцию
BinaryenAddFunction(MODULE, "tb_fun", tb_func_type, func_locals, FUNC_LOCALS_COUNT, expr);
BinaryenAddFunctionExport(MODULE, "tb_fun", "tb_fun");
...
BinaryenSetMemory(MODULE, (1 << 15) - 1, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
BinaryenAddMemoryImport(MODULE, NULL, "env", "memory", 0);
BinaryenAddTableImport(MODULE, NULL, "env", "tb_funcs");

// запросить валидацию и оптимизацию при желании
assert (BinaryenModuleValidate(MODULE));
BinaryenModuleOptimize(MODULE);

... αν ξέχασα κάτι, συγγνώμη, αυτό είναι απλώς για να αναπαραστήσω την κλίμακα και οι λεπτομέρειες βρίσκονται στην τεκμηρίωση.

Και τώρα ξεκινά το crack-fex-pex, κάπως έτσι:

static char buf[1 << 20];
BinaryenModuleOptimize(MODULE);
BinaryenSetMemory(MODULE, 0, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
int sz = BinaryenModuleWrite(MODULE, buf, sizeof(buf));
BinaryenModuleDispose(MODULE);
EM_ASM({
  var module = new WebAssembly.Module(new Uint8Array(wasmMemory.buffer, $0, $1));
  var fptr = $2;
  var instance = new WebAssembly.Instance(module, {
      'env': {
          'memory': wasmMemory,
          // ...
      }
  );
  // и вот уже у вас есть instance!
}, buf, sz);

Προκειμένου να συνδεθούν με κάποιο τρόπο οι κόσμοι του QEMU και του JS και ταυτόχρονα να έχετε γρήγορη πρόσβαση στις μεταγλωττισμένες συναρτήσεις, δημιουργήθηκε ένας πίνακας (πίνακας συναρτήσεων για εισαγωγή στον εκκινητή) και τοποθετήθηκαν εκεί οι δημιουργημένες συναρτήσεις. Για τον γρήγορο υπολογισμό του ευρετηρίου, αρχικά χρησιμοποιήθηκε ως αυτό το ευρετήριο του μπλοκ μετάφρασης μηδενικών λέξεων, αλλά στη συνέχεια ο δείκτης που υπολογίστηκε χρησιμοποιώντας αυτόν τον τύπο άρχισε απλώς να ταιριάζει στο πεδίο στο struct TranslationBlock.

παρεμπιπτόντως, διαδήλωση (επί του παρόντος με θολή άδεια) λειτουργεί καλά μόνο στον Firefox. Οι προγραμματιστές του Chrome ήταν κατά κάποιο τρόπο δεν είναι έτοιμο στο γεγονός ότι κάποιος θα ήθελε να δημιουργήσει περισσότερες από χίλιες παρουσίες λειτουργικών μονάδων WebAssembly, έτσι απλά διέθεσε ένα gigabyte εικονικού χώρου διευθύνσεων για κάθε...

Αυτα για τωρα. Ίσως υπάρξει άλλο άρθρο αν ενδιαφέρεται κάποιος. Δηλαδή, μένει τουλάχιστον απλά να λειτουργούν οι συσκευές μπλοκ. Μπορεί επίσης να έχει νόημα να γίνει η μεταγλώττιση των λειτουργικών μονάδων WebAssembly ασύγχρονη, όπως συνηθίζεται στον κόσμο του JS, καθώς υπάρχει ακόμα ένας διερμηνέας που μπορεί να τα κάνει όλα αυτά μέχρι να είναι έτοιμη η εγγενής λειτουργική μονάδα.

Επιτέλους ένας γρίφος: έχετε μεταγλωττίσει ένα δυαδικό σε μια αρχιτεκτονική 32 bit, αλλά ο κώδικας, μέσω λειτουργιών μνήμης, ανεβαίνει από το Binaryen, κάπου στη στοίβα ή κάπου αλλού στα άνω 2 GB του χώρου διευθύνσεων 32 bit. Το πρόβλημα είναι ότι από τη σκοπιά του Binaryen αυτό είναι η πρόσβαση σε μια πολύ μεγάλη διεύθυνση που προκύπτει. Πώς να το ξεπεράσετε αυτό;

Με τον τρόπο του διαχειριστή

Δεν κατέληξα να το δοκιμάσω, αλλά η πρώτη μου σκέψη ήταν "Τι θα γινόταν αν εγκαταστήσω Linux 32-bit;" Στη συνέχεια, το πάνω μέρος του χώρου διευθύνσεων θα καταληφθεί από τον πυρήνα. Το μόνο ερώτημα είναι πόσο θα είναι κατειλημμένο: 1 ή 2 Gb.

Με τον τρόπο του προγραμματιστή (επιλογή για επαγγελματίες)

Ας φυσήξουμε μια φούσκα στην κορυφή του χώρου διευθύνσεων. Εγώ ο ίδιος δεν καταλαβαίνω γιατί λειτουργεί - εκεί ήδη πρέπει να υπάρχει μια στοίβα. Αλλά "είμαστε ασκούμενοι: όλα λειτουργούν για εμάς, αλλά κανείς δεν ξέρει γιατί..."

// 2gbubble.c
// Usage: LD_PRELOAD=2gbubble.so <program>

#include <sys/mman.h>
#include <assert.h>

void __attribute__((constructor)) constr(void)
{
  assert(MAP_FAILED != mmap(1u >> 31, (1u >> 31) - (1u >> 20), PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
}

... είναι αλήθεια ότι δεν είναι συμβατό με το Valgrind, αλλά, ευτυχώς, το ίδιο το Valgrind σπρώχνει πολύ αποτελεσματικά τους πάντες από εκεί :)

Ίσως κάποιος να δώσει μια καλύτερη εξήγηση για το πώς λειτουργεί αυτός ο κώδικας μου...

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο