פעם החלטתי בשביל הכיף להוכיח את הפיכות התהליך ולמד כיצד ליצור JavaScript (ליתר דיוק, Asm.js) מקוד מכונה. QEMU נבחר לניסוי, וכעבור זמן מה נכתב מאמר על Habr. בהערות המליצו לי לעשות מחדש את הפרויקט ב-WebAssembly, ואפילו לפרוש בעצמי כמעט סיימתי איכשהו לא רציתי את הפרויקט... העבודה נמשכה, אבל לאט מאוד, ועכשיו, לאחרונה הופיעה הכתבה ההיא על הנושא "אז איך הכל נגמר?" בתגובה לתשובתי המפורטת, שמעתי "זה נשמע כמו מאמר". ובכן, אם אתה יכול, תהיה מאמר. אולי מישהו ימצא את זה שימושי. ממנו ילמד הקורא כמה עובדות על העיצוב של קצה גבוי ליצירת קוד QEMU, כמו גם כיצד לכתוב מהדר Just-in-Time עבור יישום אינטרנט.
משימות
מכיוון שכבר למדתי איך "איכשהו" להעביר QEMU ל-JavaScript, הפעם הוחלט לעשות זאת בחוכמה ולא לחזור על טעויות ישנות.
שגיאה מספר אחת: הסתעף משחרור נקודה
הטעות הראשונה שלי הייתה לחלק את הגרסה שלי מגרסה 2.4.1 במעלה הזרם. ואז זה נראה לי רעיון טוב: אם קיים שחרור נקודתי, אז הוא כנראה יציב יותר מ-2.4 פשוט, ועוד יותר הענף master. ומכיוון שתכננתי להוסיף כמות נכבדה של באגים משלי, לא הייתי צריך בכלל של אף אחד אחר. כנראה שכך זה יצא. אבל זה העניין: QEMU לא עומד במקום, ובשלב מסוים הם אפילו הכריזו על אופטימיזציה של הקוד שנוצר ב-10 אחוז. "כן, עכשיו אני הולך להקפיא", חשבתי ונשברתי. כאן אנחנו צריכים לעשות סטייה: בשל האופי החד-פתיל של QEMU.js והעובדה שה-QEMU המקורי אינו מרמז על היעדר ריבוי-שרשורים (כלומר, היכולת להפעיל בו-זמנית מספר נתיבי קוד לא קשורים, וכן לא רק "השתמש בכל הגרעינים") הוא קריטי עבורו, הפונקציות העיקריות של שרשורים הייתי צריך "להפוך את זה" כדי להיות מסוגל להתקשר מבחוץ. זה יצר כמה בעיות טבעיות במהלך המיזוג. עם זאת, העובדה שחלק מהשינויים מהסניף master, שאיתם ניסיתי למזג את הקוד שלי, גם נבחרו דובדבן בשחרור הנקודות (ולכן בסניף שלי) גם כנראה לא היו מוסיפים נוחות.
באופן כללי, החלטתי שעדיין הגיוני לזרוק את אב הטיפוס, לפרק אותו לחלקים ולבנות גרסה חדשה מאפס המבוססת על משהו רענן יותר ועכשיו מ- master.
טעות מספר שתיים: מתודולוגיית TLP
בעיקרו של דבר, זו לא טעות, באופן כללי, זה רק תכונה של יצירת פרויקט בתנאים של אי הבנה מוחלטת של "לאן ואיך לעבור?" ובאופן כללי "האם נגיע לשם?" בתנאים האלה תכנות מגושם הייתה אפשרות מוצדקת, אבל, באופן טבעי, לא רציתי לחזור עליה שלא לצורך. הפעם רציתי לעשות את זה בחוכמה: מחויבות אטומיות, שינויים בקוד מודע (ולא "מחרוזת תווים אקראיים עד להידור (עם אזהרות)", כפי שאמר פעם לינוס טורוואלדס על מישהו, לפי ויקיציטוט) וכו'.
טעות מספר שלוש: להיכנס למים בלי להכיר את הפור
עדיין לא נפטרתי מזה לגמרי, אבל עכשיו החלטתי בכלל לא ללכת בדרך של ההתנגדות הקטנה ביותר, ולעשות את זה "כמבוגר", כלומר לכתוב את ה-TCG האחורי שלי מאפס, כדי לא צריך לומר מאוחר יותר, "כן, זה כמובן, לאט, אבל אני לא יכול לשלוט על הכל - ככה כתוב TCI..." יתר על כן, זה נראה בהתחלה כמו פתרון ברור, שכן אני מייצר קוד בינארי. כמו שאומרים, "גנט התאספהу, אבל לא ההוא": הקוד הוא, כמובן, בינארי, אבל אי אפשר להעביר אליו שליטה פשוט - יש לדחוף אותו במפורש לדפדפן לצורך הידור, וכתוצאה מכך אובייקט מסוים מעולם JS, שעדיין צריך להישמר איפשהו. עם זאת, בארכיטקטורות RISC רגילות, למיטב הבנתי, מצב טיפוסי הוא הצורך לאפס במפורש את מטמון ההוראות עבור קוד משוחזר - אם זה לא מה שאנחנו צריכים, אז בכל מקרה, זה קרוב. בנוסף, מהניסיון האחרון שלי, למדתי שהשליטה לא מועברת לאמצע בלוק התרגום, אז אנחנו לא באמת צריכים קוד בייט שמפורש מכל אופסט, ואנחנו יכולים פשוט ליצור אותו מהפונקציה ב-TB .
הם באו ובעטו
למרות שהתחלתי לשכתב את הקוד ביולי, בעיטת קסם התגנבה בלי לשים לב: בדרך כלל מכתבים מ-GitHub מגיעים כהתראות לגבי תגובות לבעיות ולבקשות משיכה, אבל כאן, פִּתְאוֹם להזכיר בשרשור בהקשר, "הוא עשה משהו כזה, אולי הוא יגיד משהו." דיברנו על שימוש בספרייה הקשורה של Emscripten כדי ליצור WASM JIT. ובכן, אמרתי שיש לך שם רישיון Apache 2.0, ו-QEMU בכללותו מופץ תחת GPLv2, והם לא מאוד תואמים. פתאום התברר שרשיון יכול להיות לתקן את זה איכשהו (אני לא יודע: אולי לשנות את זה, אולי רישוי כפול, אולי משהו אחר...). זה כמובן שימח אותי, כי אז כבר הסתכלתי מקרוב WebAssembly, והייתי איכשהו עצוב ולא מובן. הייתה גם ספרייה שהייתה זוללת את הבלוקים הבסיסיים עם גרף המעבר, מייצרת את ה-bytecode, ואפילו תפעיל אותו במתורגמן עצמו, במידת הצורך.
אחר כך היה עוד ברשימת התפוצה של QEMU, אבל זה יותר על השאלה, "מי צריך את זה בכלל?" וזה פִּתְאוֹם, התברר שזה הכרחי. לכל הפחות, אתה יכול לגרד יחד את אפשרויות השימוש הבאות, אם זה עובד יותר או פחות מהר:
- השקת משהו חינוכי ללא התקנה כלל
- וירטואליזציה ב-iOS, שבה, לפי השמועות, האפליקציה היחידה שיש לה את הזכות ליצור קוד תוך כדי תנועה היא מנוע JS (האם זה נכון?)
- הדגמה של מיני מערכת הפעלה - תקליט בודד, מובנה, כל סוגי הקושחה וכו'...
תכונות זמן ריצה של דפדפן
כפי שכבר אמרתי, QEMU קשור ל-multithreading, אבל לדפדפן אין את זה. ובכן, כלומר, לא... בהתחלה זה לא היה קיים בכלל, ואז הופיעו WebWorkers - למיטב הבנתי, זה ריבוי השרשורים המבוססים על העברת הודעות ללא משתנים משותפים. באופן טבעי, זה יוצר בעיות משמעותיות בעת העברת קוד קיים על בסיס מודל הזיכרון המשותף. אחר כך, בלחץ ציבורי, הוא יושם גם בשם SharedArrayBuffers. זה הוצג בהדרגה, הם חגגו את ההשקה שלו בדפדפנים שונים, אחר כך חגגו את השנה החדשה, ואחר כך Meltdown... לאחר מכן הגיעו למסקנה שגס או גס את מדידת הזמן, אבל בעזרת זיכרון משותף ו- חוט מגדיל את המונה, הכל אותו דבר . אז השבתנו ריבוי שרשורים עם זיכרון משותף. נראה שמאוחר יותר הפעילו אותו מחדש, אבל כפי שהתברר מהניסוי הראשון, יש חיים בלעדיו, ואם כן, ננסה לעשות זאת מבלי להסתמך על ריבוי הליכים.
התכונה השנייה היא חוסר האפשרות של מניפולציות ברמה נמוכה עם הערימה: אתה לא יכול פשוט לקחת, לשמור את ההקשר הנוכחי ולעבור להקשר חדש עם מחסנית חדשה. מחסנית השיחות מנוהלת על ידי המכונה הוירטואלית JS. נראה, מה הבעיה, מכיוון שבכל זאת החלטנו לנהל את הזרמים הקודמים באופן ידני לחלוטין? העובדה היא שבלוק I/O ב-QEMU מיושם באמצעות קורוטינים, וכאן מניפולציות מחסניות ברמה נמוכה יהיו שימושיות. למרבה המזל, Emscipten כבר מכיל מנגנון לפעולות אסינכרוניות, אפילו שניים: и . הראשון פועל באמצעות נפיחות משמעותית בקוד ה-JavaScript שנוצר ואינו נתמך עוד. השני הוא "הדרך הנכונה" הנוכחית ופועל באמצעות יצירת קוד בתים עבור המתורגמן המקומי. זה עובד, כמובן, לאט, אבל זה לא מנפח את הקוד. נכון, תמיכה ב-coroutines למנגנון הזה הייתה צריכה להיתרם באופן עצמאי (כבר היו קורוטיינים שנכתבו עבור 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. נניח שאתה מחליט להוסיף משהו: קצה אחורי של TCG, יישום שרשור, משהו אחר. אל תמהרו להיות שמחים/מבועשים (הדגשות בהתאם) מהסיכוי לתקשר עם Autoconf - למעשה, configure ה-QEMU's כנראה נכתב בעצמו ואינו נוצר מכלום.
WebAssembly
אז מה זה הדבר הזה שנקרא WebAssembly (aka WASM)? זהו תחליף ל-Asm.js, שאינו מתיימר עוד להיות קוד JavaScript חוקי. להיפך, הוא בינארי ומיוטב בלבד, ואפילו כתיבת מספר שלם לתוכו היא לא פשוטה במיוחד: בשביל הקומפקטיות, הוא מאוחסן בפורמט .
אולי שמעתם על אלגוריתם ה-relooping עבור Asm.js - זהו שחזור של הוראות בקרת זרימה "ברמה גבוהה" (כלומר, אם-אז-אחר, לולאות וכו'), שעבורן מיועדים מנועי JS, מ- ה-LLVM IR ברמה נמוכה, קרוב יותר לקוד המכונה שמבוצע על ידי המעבד. מטבע הדברים, ייצוג הביניים של QEMU קרוב יותר לשני. נראה שהנה זה, bytecode, סוף הייסורים... ואז יש בלוקים, אם-אז-אחר ולולאות!..
וזו סיבה נוספת לכך ש-Binaryen שימושי: באופן טבעי הוא יכול לקבל בלוקים ברמה גבוהה הקרובים למה שהיה מאוחסן ב-WASM. אבל הוא יכול גם לייצר קוד מגרף של בלוקים בסיסיים ומעברים ביניהם. ובכן, כבר אמרתי שהוא מסתיר את פורמט האחסון של WebAssembly מאחורי ה-API הנוח של C/C++.
TCG (מחולל קוד זעיר)
TCG אחורי עבור מהדר C. לאחר מכן, ככל הנראה, הוא לא יכול היה לעמוד בתחרות עם GCC, אבל בסופו של דבר הוא מצא את מקומו ב-QEMU כמנגנון יצירת קוד עבור הפלטפורמה המארחת. יש גם קצה אחורי של TCG שמייצר קוד בתים אבסטרקטי כלשהו, שמבוצע מיד על ידי המתורגמן, אבל החלטתי להימנע משימוש בו הפעם. עם זאת, העובדה שב-QEMU כבר ניתן לאפשר את המעבר לשחפת שנוצרה באמצעות הפונקציה tcg_qemu_tb_exec, התברר שזה מאוד שימושי עבורי.
כדי להוסיף TCG backend חדש ל-QEMU, עליך ליצור ספריית משנה tcg/<имя архитектуры> (במקרה הזה, tcg/binaryen), והוא מכיל שני קבצים: tcg-target.h и tcg-target.inc.c и זה הכל על configure. אתה יכול לשים שם קבצים אחרים, אבל כפי שאתה יכול לנחש מהשמות של שני אלה, שניהם ייכללו איפשהו: אחד כקובץ כותרת רגיל (הוא כלול ב- tcg/tcg.h, וזה כבר נמצא בקבצים אחרים בספריות tcg, accel ולא רק), השני - רק כקטע קוד פנימה tcg/tcg.c, אבל יש לו גישה לפונקציות הסטטיות שלו.
החלטתי שאבזבז יותר מדי זמן על חקירות מפורטות של איך זה עובד, פשוט העתקתי את ה"שלדים" של שני הקבצים האלה מיישום אחורי אחר, וציינת זאת בכנות בכותרת הרישיון.
קובץ tcg-target.h מכיל בעיקר הגדרות בטופס #define-s:
- כמה רגיסטרים ואיזה רוחב יש בארכיטקטורת היעד (יש לנו כמה שאנחנו רוצים, כמה שאנחנו רוצים - השאלה היא יותר מה יווצר לקוד יעיל יותר על ידי הדפדפן בארכיטקטורת "המטרה לחלוטין" ...)
- יישור הוראות מארח: ב-x86, ואפילו ב-TCI, הוראות אינן מיושרות כלל, אבל אני הולך לשים במאגר הקוד לא הוראות בכלל, אלא מצביעים למבני ספריית Binaryen, אז אני אגיד: 4 בתים
- אילו הוראות אופציונליות הקצה האחורי יכול ליצור - אנחנו כוללים את כל מה שאנחנו מוצאים ב-Binaryen, נותנים למאיץ לחלק את השאר לפשוטים יותר בעצמו
- מהו הגודל המשוער של מטמון TLB המבוקש על ידי הקצה האחורי. העובדה היא שב-QEMU הכל רציני: למרות שיש פונקציות עוזר שמבצעות טעינה/אחסון תוך התחשבות ב-MMU האורח (איפה היינו בלעדיה עכשיו?), הן שומרות את מטמון התרגום שלהן בצורה של מבנה, ה- עיבוד שלו נוח להטמעה ישירות בלוקי שידור. השאלה היא, איזה היסט במבנה הזה מעובד בצורה היעילה ביותר על ידי רצף קטן ומהיר של פקודות?
- כאן אתה יכול להתאים את המטרה של אוגרים שמורים אחד או שניים, לאפשר קריאת TB באמצעות פונקציה ולתאר אופציונלי כמה קטנים
inline-פונקציות כמוflush_icache_range(אבל זה לא המקרה שלנו)
קובץ tcg-target.inc.c, כמובן, הוא בדרך כלל הרבה יותר בגודלו ומכיל מספר פונקציות חובה:
- אתחול, כולל הגבלות על אילו הוראות יכולות לפעול על אילו אופרנדים. הועתק על ידי באופן בוטה מקצה אחורי אחר
- פונקציה שלוקחת הוראת קוד בתים פנימית אחת
- אתה יכול גם לשים כאן פונקציות עזר, ואפשר גם להשתמש בפונקציות סטטיות מ
tcg/tcg.c
לעצמי בחרתי באסטרטגיה הבאה: במילים הראשונות של בלוק התרגום הבא, רשמתי ארבעה מצביעים: סימן התחלה (ערך מסוים בסביבה 0xFFFFFFFF, שקבע את המצב הנוכחי של ה-TB), ההקשר, המודול שנוצר ומספר קסם לאיפוי באגים. בהתחלה הסימן הוכנס פנימה 0xFFFFFFFF - nאיפה n - מספר חיובי קטן, ובכל פעם שהוא בוצע דרך המתורגמן הוא גדל ב-1. כשהגיע 0xFFFFFFFE, הקומפילציה התבצעה, המודול נשמר בטבלת הפונקציות, יובא לתוך "משגר" קטן שאליו עבר הביצוע מ tcg_qemu_tb_exec, והמודול הוסר מזיכרון QEMU.
לפרפראזה על הקלאסיקה, "קביים, כמה שזור בצליל הזה ללב הפרוג'ר...". עם זאת, הזיכרון דלף איפשהו. יתר על כן, זה היה זיכרון המנוהל על ידי QEMU! היה לי קוד שבכתיבת ההוראה הבאה (טוב, כלומר מצביע), מחק את זה שהקישור שלו היה במקום הזה קודם לכן, אבל זה לא עזר. למעשה, במקרה הפשוט ביותר, QEMU מקצה זיכרון בעת האתחול וכותב שם את הקוד שנוצר. כאשר המאגר נגמר, הקוד נזרק החוצה והבא הבא מתחיל להיכתב במקומו.
לאחר שלמדתי את הקוד, הבנתי שהטריק עם מספר הקסם אפשר לי לא להיכשל בהשמדת ערמות על ידי שחרור משהו שגוי על מאגר לא מאותחל במעבר הראשון. אבל מי משכתב את החיץ כדי לעקוף את הפונקציה שלי מאוחר יותר? כפי שהמפתחים של Emscripten מייעצים, כשנתקלתי בבעיה, העברתי את הקוד שהתקבל בחזרה לאפליקציה המקורית, הגדרתי עליו Mozilla Record-Replay... באופן כללי, בסופו של דבר הבנתי דבר פשוט: עבור כל בלוק, א struct TranslationBlock עם התיאור שלו. נחשו איפה... זה נכון, ממש לפני הבלוק ממש במאגר. כשהבנתי זאת, החלטתי להפסיק להשתמש בקביים (לפחות חלקם), ופשוט זרקתי את מספר הקסם, והעברתי את המילים הנותרות ל struct TranslationBlock, יצירת רשימה מקושרת בודדת שניתן לעבור במהירות כאשר מטמון התרגום מאופס, ולפנות זיכרון.
נשארו כמה קביים: למשל מצביעים מסומנים במאגר הקוד - חלקם פשוטים BinaryenExpressionRef, כלומר, הם מסתכלים על הביטויים שצריך להכניס באופן ליניארי לבלוק הבסיסי שנוצר, חלק הוא התנאי למעבר בין BBs, חלק הוא לאן ללכת. ובכן, יש כבר בלוקים מוכנים עבור Relooper שצריך לחבר לפי התנאים. כדי להבדיל ביניהם, נעשה שימוש בהנחה שכולם מיושרים לפי לפחות ארבעה בתים, כך שתוכל להשתמש בבטחה בשני הביטים הפחות משמעותיים עבור התווית, אתה רק צריך לזכור להסיר אותו במידת הצורך. אגב, תוויות כאלה כבר נמצאות בשימוש ב-QEMU כדי לציין את הסיבה ליציאה מלולאת TCG.
באמצעות Binaryen
מודולים ב-WebAssembly מכילים פונקציות שכל אחת מהן מכילה גוף, שהוא ביטוי. ביטויים הם פעולות אונאריות ובינאריות, בלוקים המורכבים מרשימות של ביטויים אחרים, זרימת בקרה וכו'. כפי שכבר אמרתי, זרימת הבקרה כאן מאורגנת בדיוק כמו ענפים ברמה גבוהה, לולאות, קריאות פונקציות וכו'. טיעונים לפונקציות לא מועברים בערימה, אלא במפורש, בדיוק כמו ב-JS. יש גם משתנים גלובליים, אבל לא השתמשתי בהם, אז אני לא אספר לכם עליהם.
לפונקציות יש גם משתנים מקומיים, ממוספרים מאפס, מסוג: int32 / int64 / float / double. במקרה זה, n המשתנים המקומיים הראשונים הם הארגומנטים המועברים לפונקציה. שימו לב שלמרות שהכל כאן אינו ברמה נמוכה לחלוטין מבחינת זרימת בקרה, מספרים שלמים עדיין אינם נושאים את התכונה "חתום/לא חתום": איך המספר מתנהג תלוי בקוד הפעולה.
באופן כללי, Binaryen מספק : אתה יוצר מודול, בו ליצור ביטויים - unary, בינארי, בלוקים מביטויים אחרים, זרימת בקרה וכו'. ואז אתה יוצר פונקציה עם ביטוי כגוף שלה. אם יש לך, כמוני, גרף מעבר ברמה נמוכה, רכיב ה-relooper יעזור לך. למיטב הבנתי, אפשר להשתמש בשליטה ברמה גבוהה על זרימת הביצוע בבלוק, כל עוד זה לא עובר את גבולות הבלוק - כלומר, אפשר לעשות נתיב מהיר / איטי פנימי נתיב מסתעף בתוך קוד עיבוד מטמון TLB המובנה, אך לא כדי להפריע לזרימת הבקרה ה"חיצונית". כאשר אתה משחרר רלוופר, הבלוקים שלו משתחררים; כאשר אתה משחרר מודול, הביטויים, הפונקציות וכו' שהוקצו לו נעלמים זִירָה.
עם זאת, אם אתה רוצה לפרש קוד תוך כדי תנועה ללא יצירה ומחיקה מיותרת של מופע מתורגמן, אולי הגיוני להכניס את ההיגיון הזה לקובץ C++, ומשם לנהל ישירות את כל ה-C++ API של הספרייה, תוך עקיפת מוכן- עשו עטיפות.
אז כדי ליצור את הקוד שאתה צריך
// настроить глобальные параметры (можно поменять потом)
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 ובמקביל לגשת במהירות לפונקציות הקומפילציה, נוצר מערך (טבלת פונקציות לייבוא ל-Launcher), ושם הוצבו הפונקציות שנוצרו. כדי לחשב במהירות את האינדקס, האינדקס של בלוק התרגום של מילה אפס שימש בתחילה כפי שהוא, אבל אז האינדקס שחושב באמצעות נוסחה זו התחיל פשוט להשתלב בשדה ב struct TranslationBlock.
אגב, (כרגע עם רישיון עכור) עובד מצוין רק בפיירפוקס. מפתחי Chrome היו איכשהו לא מוכן לעובדה שמישהו ירצה ליצור יותר מאלף מופעים של מודולי WebAssembly, אז הם פשוט הקצו גיגה-בייט של שטח כתובות וירטואלי לכל...
זה הכל לעת עתה. אולי יהיה מאמר נוסף אם מישהו מעוניין. כלומר, נשארים לפחות פשוט לגרום למכשירי חסימה לעבוד. אולי גם הגיוני להפוך את הקומפילציה של מודולי WebAssembly לא-סינכרונית, כפי שנהוג בעולם JS, מכיוון שעדיין יש מתורגמן שיכול לעשות את כל זה עד שהמודול המקורי יהיה מוכן.
סוף סוף חידה: הידור בינארי על ארכיטקטורת 32 סיביות, אבל הקוד, באמצעות פעולות זיכרון, מטפס מ-Binaryen, איפשהו על הערימה, או במקום אחר ב-2 GB העליונים של מרחב הכתובות של 32 סיביות. הבעיה היא שמנקודת המבט של Binaryen מדובר בגישה לכתובת גדולה מדי. איך לעקוף את זה?
בדרך של מנהל
בסופו של דבר לא בדקתי את זה, אבל המחשבה הראשונה שלי הייתה, "מה אם אני מתקין גרסה של 32 סיביות?" Linux"אז החלק העליון של מרחב הכתובות יתפוס על ידי הליבה. השאלה היחידה היא כמה יתפוס: 1 או 2 ג'יגה-בייט."
בדרך של מתכנת (אפשרות למתרגלים)
בואו נפוצץ בועה בחלק העליון של חלל הכתובות. אני בעצמי לא מבין למה זה עובד - שם כבר חייבת להיות ערימה. אבל "אנחנו מתרגלים: הכל עובד בשבילנו, אבל אף אחד לא יודע למה..."
// 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));
}... זה נכון שזה לא תואם ל-Valgriind, אבל, למרבה המזל, ולגרינד עצמו דוחף את כולם ביעילות רבה משם :)
אולי מישהו ייתן הסבר טוב יותר איך הקוד הזה שלי עובד...
מקור: www.habr.com
