กาลครั้งหนึ่งฉันตัดสินใจเพื่อความสนุกสนาน พิสูจน์การย้อนกลับของกระบวนการ และเรียนรู้วิธีสร้าง JavaScript (หรือเรียกอีกอย่างว่า Asm.js) จากโค้ดเครื่อง QEMU ได้รับเลือกสำหรับการทดลอง และต่อมาไม่นานก็มีการเขียนบทความเกี่ยวกับ Habr ในความคิดเห็น ฉันได้รับคำแนะนำให้สร้างโปรเจ็กต์ใหม่ใน WebAssembly และแม้แต่ออกจากตัวเองด้วยซ้ำ เกือบเสร็จแล้ว ไม่อยากให้โปรเจ็กต์นี้เลย... งานกำลังดำเนินไปแต่ช้ามาก และตอนนี้ ล่าสุดในบทความนั้นก็ปรากฏขึ้น
งาน
เนื่องจากฉันได้เรียนรู้วิธี "อย่างใด" พอร์ต QEMU เป็น JavaScript แล้วคราวนี้จึงตัดสินใจทำอย่างชาญฉลาดและไม่ทำผิดพลาดเก่าซ้ำ
ข้อผิดพลาดหมายเลขหนึ่ง: แยกสาขาจากการปล่อยจุด
ข้อผิดพลาดแรกของฉันคือการแยกเวอร์ชันของฉันจากอัปสตรีมเวอร์ชัน 2.4.1 สำหรับฉันแล้วดูเหมือนว่าเป็นความคิดที่ดี: หากมีการปล่อยจุดก็อาจมีความเสถียรมากกว่า 2.4 แบบธรรมดาและยิ่งกว่านั้นคือสาขา master
. และเนื่องจากฉันวางแผนที่จะเพิ่มจุดบกพร่องของตัวเองในจำนวนที่พอเหมาะ ฉันจึงไม่ต้องการข้อบกพร่องของใครเลย นั่นอาจเป็นสิ่งที่เกิดขึ้น แต่นี่คือสิ่งที่: QEMU ไม่หยุดนิ่งและเมื่อถึงจุดหนึ่งพวกเขาก็ประกาศเพิ่มประสิทธิภาพของโค้ดที่สร้างขึ้น 10 เปอร์เซ็นต์ “ใช่ ตอนนี้ฉันจะหยุดทำงานแล้ว” ฉันคิดและพังทลายลง ที่นี่เราจำเป็นต้องพูดนอกเรื่อง: เนื่องจากลักษณะเธรดเดียวของ QEMU.js และความจริงที่ว่า QEMU ดั้งเดิมไม่ได้หมายความถึงการขาดมัลติเธรด (นั่นคือความสามารถในการดำเนินการเส้นทางโค้ดที่ไม่เกี่ยวข้องหลายเส้นทางพร้อมกัน และ ไม่ใช่แค่ "ใช้เคอร์เนลทั้งหมด") เป็นสิ่งสำคัญสำหรับมัน ฟังก์ชั่นหลักของเธรดที่ฉันต้อง "เปิดออก" เพื่อให้สามารถโทรจากภายนอกได้ สิ่งนี้สร้างปัญหาธรรมชาติบางอย่างในระหว่างการควบรวมกิจการ อย่างไรก็ตามความจริงแล้วการเปลี่ยนแปลงบางส่วนจากสาขา master
ซึ่งฉันพยายามรวมรหัสของฉัน ก็ถูกเลือกเชอร์รี่ในการเปิดตัวจุด (และดังนั้นในสาขาของฉัน) อาจจะไม่เพิ่มความสะดวกสบาย
โดยทั่วไป ฉันตัดสินใจว่ายังคงสมเหตุสมผลที่จะทิ้งต้นแบบ ถอดแยกชิ้นส่วน และสร้างเวอร์ชันใหม่ตั้งแต่ต้นโดยอิงจากสิ่งที่สดใหม่กว่าและตอนนี้จาก master
.
ข้อผิดพลาดข้อที่สอง: วิธีการ TLP
โดยพื้นฐานแล้วนี่ไม่ใช่ข้อผิดพลาด โดยทั่วไปเป็นเพียงคุณลักษณะของการสร้างโครงการภายใต้เงื่อนไขของความเข้าใจผิดโดยสิ้นเชิงทั้ง "จะย้ายไปที่ไหนและอย่างไร" และโดยทั่วไป "เราจะไปถึงจุดนั้นหรือไม่" ในสภาวะเหล่านี้ การเขียนโปรแกรมเงอะงะ เป็นตัวเลือกที่สมเหตุสมผล แต่โดยธรรมชาติแล้ว ฉันไม่ต้องการทำซ้ำโดยไม่จำเป็น คราวนี้ฉันต้องการทำอย่างชาญฉลาด: การคอมมิตแบบอะตอมมิก การเปลี่ยนแปลงโค้ดอย่างมีสติ (และไม่ใช่ "การร้อยอักขระแบบสุ่มเข้าด้วยกันจนกว่าจะคอมไพล์ (พร้อมคำเตือน)" ดังที่ Linus Torvalds เคยกล่าวไว้เกี่ยวกับใครบางคน ตาม Wikiquote) ฯลฯ
ข้อผิดพลาดข้อที่สาม: ลงน้ำโดยไม่รู้ฟอร์ด
ฉันยังไม่ได้กำจัดสิ่งนี้ไปโดยสิ้นเชิง แต่ตอนนี้ฉันตัดสินใจที่จะไม่ทำตามเส้นทางที่มีการต่อต้านน้อยที่สุด และทำ "ในฐานะผู้ใหญ่" กล่าวคือ เขียนแบ็กเอนด์ TCG ของฉันตั้งแต่เริ่มต้น เพื่อไม่ให้ ที่ต้องพูดทีหลังว่า “ใช่ แน่นอน มันช้า แต่ฉันควบคุมทุกอย่างไม่ได้ - นั่นคือวิธีเขียน TCI...” นอกจากนี้ ในตอนแรกสิ่งนี้ดูเหมือนเป็นวิธีแก้ปัญหาที่ชัดเจนตั้งแต่นั้นเป็นต้นมา ฉันสร้างรหัสไบนารี่. ขณะที่พวกเขาพูดว่า “เกนต์มารวมตัวกันуแต่ไม่ใช่อันนั้น”: แน่นอนว่าโค้ดนั้นเป็นไบนารี่ แต่การควบคุมไม่สามารถถ่ายโอนไปยังโค้ดนั้นได้ - จะต้องถูกพุชลงในเบราว์เซอร์อย่างชัดเจนเพื่อคอมไพล์ ส่งผลให้เกิดวัตถุบางอย่างจากโลก JS ซึ่งยังคงจำเป็นต้อง ได้รับการบันทึกไว้ที่ไหนสักแห่ง อย่างไรก็ตาม สำหรับสถาปัตยกรรม RISC ปกติ เท่าที่ฉันเข้าใจ สถานการณ์ทั่วไปคือความจำเป็นในการรีเซ็ตแคชคำสั่งสำหรับโค้ดที่สร้างใหม่อย่างชัดเจน - หากนี่ไม่ใช่สิ่งที่เราต้องการ ไม่ว่าในกรณีใด ก็ปิดไปแล้ว นอกจากนี้ จากความพยายามครั้งล่าสุดของฉัน ฉันได้เรียนรู้ว่าการควบคุมดูเหมือนจะไม่ถูกถ่ายโอนไปยังตรงกลางของบล็อกการแปล ดังนั้นเราจึงไม่จำเป็นต้องแปลโค้ดไบต์จากออฟเซ็ตใดๆ จริงๆ และเราสามารถสร้างมันได้จากฟังก์ชันบน TB .
พวกเขามาและเตะ
แม้ว่าฉันจะเริ่มเขียนโค้ดใหม่ในเดือนกรกฎาคม แต่ก็เกิดความมหัศจรรย์เพิ่มขึ้นโดยไม่มีใครสังเกตเห็น โดยปกติแล้วจดหมายจาก GitHub จะมาถึงเป็นการแจ้งเตือนเกี่ยวกับการตอบสนองต่อปัญหาและคำขอดึง แต่ที่นี่ ทันใดนั้น กล่าวถึงในกระทู้
จากนั้นก็มีมากขึ้น
- เปิดตัวบางสิ่งบางอย่างเพื่อการศึกษาโดยไม่ต้องติดตั้งใดๆ เลย
- การจำลองเสมือนบน iOS โดยที่ตามข่าวลือแอปพลิเคชันเดียวที่มีสิทธิ์ในการสร้างโค้ดได้ทันทีคือเอ็นจิ้น JS (จริงหรือไม่)
- การสาธิต mini-OS - ฟลอปปี้เดียว, เฟิร์มแวร์ในตัว, เฟิร์มแวร์ทุกชนิด ฯลฯ ...
คุณสมบัติรันไทม์ของเบราว์เซอร์
ดังที่ฉันได้กล่าวไปแล้ว QEMU เชื่อมโยงกับมัลติเธรด แต่เบราว์เซอร์ไม่มี นั่นคือไม่... ในตอนแรกมันไม่มีเลยจากนั้น WebWorkers ก็ปรากฏขึ้น - เท่าที่ฉันเข้าใจนี่คือมัลติเธรดตามการส่งข้อความ โดยไม่มีตัวแปรร่วม. โดยปกติแล้ว สิ่งนี้จะสร้างปัญหาสำคัญเมื่อทำการย้ายโค้ดที่มีอยู่ตามรุ่นหน่วยความจำที่แชร์ จากนั้นภายใต้แรงกดดันสาธารณะ จึงมีการดำเนินการภายใต้ชื่อด้วย SharedArrayBuffers
. มีการเปิดตัวอย่างค่อยเป็นค่อยไป พวกเขาเฉลิมฉลองการเปิดตัวในเบราว์เซอร์ที่แตกต่างกัน จากนั้นพวกเขาก็เฉลิมฉลองปีใหม่ แล้วก็ Meltdown... หลังจากนั้นพวกเขาก็ได้ข้อสรุปว่าการวัดเวลาหยาบหรือหยาบ แต่ด้วยความช่วยเหลือของหน่วยความจำที่ใช้ร่วมกันและ เธรดที่เพิ่มตัวนับก็เหมือนกันทั้งหมด
คุณสมบัติที่สองคือความเป็นไปไม่ได้ของการจัดการระดับต่ำด้วยสแต็ก: คุณไม่สามารถทำได้บันทึกบริบทปัจจุบันและเปลี่ยนไปใช้สแต็กใหม่ด้วยสแต็กใหม่ Call Stack ได้รับการจัดการโดยเครื่องเสมือน JS ดูเหมือนว่าปัญหาคืออะไร เนื่องจากเรายังคงตัดสินใจที่จะจัดการโฟลว์เดิมด้วยตนเองโดยสมบูรณ์ ความจริงก็คือบล็อก I/O ใน QEMU ถูกนำมาใช้ผ่าน coroutines และนี่คือจุดที่การจัดการสแต็กระดับต่ำจะมีประโยชน์ โชคดีที่ Emscipten มีกลไกสำหรับการดำเนินการแบบอะซิงโครนัสอยู่แล้ว แม้แต่สองกลไก:
ในขณะนี้ ฉันยังไม่ได้แยกโค้ดออกเป็นโค้ดเดียวที่คอมไพล์ใน 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 เขียนขึ้นเองและไม่ได้สร้างขึ้นจากสิ่งใดเลย
WebAssembly
แล้วสิ่งนี้เรียกว่า WebAssembly (aka WASM) คืออะไร? นี่เป็นการแทนที่ Asm.js ซึ่งไม่แสร้งทำเป็นโค้ด JavaScript ที่ถูกต้องอีกต่อไป ในทางตรงกันข้ามมันเป็นไบนารี่ล้วนๆและได้รับการปรับให้เหมาะสมและแม้แต่การเขียนจำนวนเต็มลงไปนั้นก็ไม่ง่ายนัก: เพื่อความกะทัดรัดมันจะถูกจัดเก็บไว้ในรูปแบบ
คุณอาจเคยได้ยินเกี่ยวกับอัลกอริธึมการวนซ้ำสำหรับ Asm.js - นี่คือการคืนค่าคำสั่งควบคุมโฟลว์ "ระดับสูง" (นั่นคือ if-then-else, ลูป ฯลฯ ) ซึ่งเอ็นจิ้น JS ได้รับการออกแบบมาจาก LLVM IR ระดับต่ำใกล้กับรหัสเครื่องที่ดำเนินการโดยโปรเซสเซอร์ โดยธรรมชาติแล้ว การแสดงระดับกลางของ QEMU จะใกล้เคียงกับวินาทีมากกว่า ดูเหมือนว่านี่คือ bytecode จุดสิ้นสุดของความทรมาน... แล้วก็มีบล็อก if-then-else และลูป!..
และนี่คืออีกเหตุผลหนึ่งว่าทำไม Binaryen จึงมีประโยชน์: โดยธรรมชาติแล้วมันสามารถยอมรับบล็อกระดับสูงที่ใกล้เคียงกับสิ่งที่จะถูกเก็บไว้ใน WASM ได้ แต่ยังสามารถสร้างโค้ดจากกราฟของบล็อกพื้นฐานและการเปลี่ยนระหว่างบล็อกเหล่านั้นได้ ฉันได้บอกไปแล้วว่ามันซ่อนรูปแบบพื้นที่เก็บข้อมูล WebAssembly ไว้ด้านหลัง C/C++ API ที่สะดวกสบาย
TCG (เครื่องสร้างโค้ดจิ๋ว)
TCG tcg_qemu_tb_exec
มันกลับกลายเป็นว่ามีประโยชน์มากสำหรับฉัน
หากต้องการเพิ่มแบ็กเอนด์ TCG ใหม่ให้กับ 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
แน่นอนว่ามักจะมีขนาดใหญ่กว่ามากและมีฟังก์ชันบังคับหลายประการ:
- การเริ่มต้น รวมถึงข้อจำกัดว่าคำสั่งใดสามารถทำงานได้กับตัวถูกดำเนินการใด ฉันคัดลอกอย่างโจ่งแจ้งจากแบ็กเอนด์อื่น
- ฟังก์ชั่นที่รับคำสั่ง bytecode ภายในหนึ่งคำสั่ง
- คุณยังสามารถใส่ฟังก์ชันเสริมได้ที่นี่ และคุณยังสามารถใช้ฟังก์ชันคงที่จากได้อีกด้วย
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
การใช้ไบนารี่
โมดูลใน WebAssembly มีฟังก์ชันต่างๆ ซึ่งแต่ละโมดูลประกอบด้วยเนื้อหาซึ่งเป็นนิพจน์ นิพจน์คือการดำเนินการแบบเอกนารีและไบนารี บล็อกที่ประกอบด้วยรายการของนิพจน์อื่น โฟลว์การควบคุม ฯลฯ ดังที่ฉันได้กล่าวไปแล้ว โฟลว์การควบคุมที่นี่ได้รับการจัดระเบียบอย่างแม่นยำเป็นสาขาระดับสูง ลูป การเรียกใช้ฟังก์ชัน ฯลฯ อาร์กิวเมนต์ของฟังก์ชันจะไม่ถูกส่งผ่านบนสแต็ก แต่ชัดเจน เช่นเดียวกับใน JS นอกจากนี้ยังมีตัวแปรทั่วโลก แต่ฉันไม่ได้ใช้มัน ดังนั้นฉันจะไม่บอกคุณเกี่ยวกับมัน
ฟังก์ชันยังมีตัวแปรท้องถิ่น ซึ่งมีหมายเลขตั้งแต่ศูนย์ ประเภท: int32 / int64 / float / double ในกรณีนี้ ตัวแปรโลคัล n ตัวแรกคืออาร์กิวเมนต์ที่ส่งไปยังฟังก์ชัน โปรดทราบว่าแม้ว่าทุกสิ่งที่นี่จะไม่ใช่ระดับต่ำทั้งหมดในแง่ของโฟลว์การควบคุม แต่จำนวนเต็มก็ยังไม่มีแอตทริบิวต์ "ลงนาม/ไม่ได้ลงนาม": ลักษณะการทำงานของตัวเลขจะขึ้นอยู่กับโค้ดการดำเนินการ
โดยทั่วไปแล้ว Binaryen จัดให้
อย่างไรก็ตาม หากคุณต้องการตีความโค้ดได้ทันทีโดยไม่ต้องสร้างและลบอินสแตนซ์ล่ามโดยไม่จำเป็น อาจสมเหตุสมผลที่จะใส่ตรรกะนี้ลงในไฟล์ C++ จากนั้นจัดการ C++ API ทั้งหมดของไลบรารีโดยตรง โดยข้ามผ่าน ready- ทำห่อ
ดังนั้นเพื่อสร้างรหัสที่คุณต้องการ
// настроить глобальные параметры (можно поменять потом)
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
.
อนึ่ง
นั่นคือทั้งหมดที่สำหรับตอนนี้. บางทีอาจจะมีบทความอื่นถ้าใครสนใจ กล่าวคือยังมีอย่างน้อย เท่านั้น ทำให้อุปกรณ์บล็อคทำงานได้ นอกจากนี้ยังอาจสมเหตุสมผลที่จะสร้างการคอมไพล์โมดูล WebAssembly แบบอะซิงโครนัส ตามธรรมเนียมในโลก JS เนื่องจากยังมีล่ามที่สามารถทำทั้งหมดนี้ได้จนกว่าโมดูลดั้งเดิมจะพร้อม
ในที่สุดก็มีปริศนา: คุณได้รวบรวมไบนารี่บนสถาปัตยกรรม 32 บิต แต่โค้ดผ่านการทำงานของหน่วยความจำ ไต่ขึ้นจาก Binaryen ที่ใดที่หนึ่งบนสแต็ก หรือที่อื่นใดในพื้นที่ที่อยู่ 2 บิตบน 32 GB ปัญหาคือจากมุมมองของ Binaryen นี่เป็นการเข้าถึงที่อยู่ผลลัพธ์ที่ใหญ่เกินไป จะแก้ไขเรื่องนี้ได้อย่างไร?
ในแบบของแอดมิน
ฉันไม่ได้ทำการทดสอบสิ่งนี้ แต่ความคิดแรกของฉันคือ “จะเกิดอะไรขึ้นถ้าฉันติดตั้ง Linux แบบ 32 บิต?” จากนั้นส่วนบนของพื้นที่ที่อยู่จะถูกครอบครองโดยเคอร์เนล คำถามเดียวคือจะถูกครอบครองเท่าใด: 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 เองก็ผลักทุกคนออกจากที่นั่นได้อย่างมีประสิทธิภาพ :)
บางทีอาจมีบางคนให้คำอธิบายที่ดีกว่าว่ารหัสของฉันทำงานอย่างไร...
ที่มา: will.com