QEMU.js: ตอนนี้จริงจังและมี WASM

กาลครั้งหนึ่งฉันตัดสินใจเพื่อความสนุกสนาน พิสูจน์การย้อนกลับของกระบวนการ และเรียนรู้วิธีสร้าง 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

โดยพื้นฐานแล้วนี่ไม่ใช่ข้อผิดพลาด โดยทั่วไปเป็นเพียงคุณลักษณะของการสร้างโครงการภายใต้เงื่อนไขของความเข้าใจผิดโดยสิ้นเชิงทั้ง "จะย้ายไปที่ไหนและอย่างไร" และโดยทั่วไป "เราจะไปถึงจุดนั้นหรือไม่" ในสภาวะเหล่านี้ การเขียนโปรแกรมเงอะงะ เป็นตัวเลือกที่สมเหตุสมผล แต่โดยธรรมชาติแล้ว ฉันไม่ต้องการทำซ้ำโดยไม่จำเป็น คราวนี้ฉันต้องการทำอย่างชาญฉลาด: การคอมมิตแบบอะตอมมิก การเปลี่ยนแปลงโค้ดอย่างมีสติ (และไม่ใช่ "การร้อยอักขระแบบสุ่มเข้าด้วยกันจนกว่าจะคอมไพล์ (พร้อมคำเตือน)" ดังที่ Linus Torvalds เคยกล่าวไว้เกี่ยวกับใครบางคน ตาม Wikiquote) ฯลฯ

ข้อผิดพลาดข้อที่สาม: ลงน้ำโดยไม่รู้ฟอร์ด

ฉันยังไม่ได้กำจัดสิ่งนี้ไปโดยสิ้นเชิง แต่ตอนนี้ฉันตัดสินใจที่จะไม่ทำตามเส้นทางที่มีการต่อต้านน้อยที่สุด และทำ "ในฐานะผู้ใหญ่" กล่าวคือ เขียนแบ็กเอนด์ TCG ของฉันตั้งแต่เริ่มต้น เพื่อไม่ให้ ที่ต้องพูดทีหลังว่า “ใช่ แน่นอน มันช้า แต่ฉันควบคุมทุกอย่างไม่ได้ - นั่นคือวิธีเขียน TCI...” นอกจากนี้ ในตอนแรกสิ่งนี้ดูเหมือนเป็นวิธีแก้ปัญหาที่ชัดเจนตั้งแต่นั้นเป็นต้นมา ฉันสร้างรหัสไบนารี่. ขณะที่พวกเขาพูดว่า “เกนต์มารวมตัวกันуแต่ไม่ใช่อันนั้น”: แน่นอนว่าโค้ดนั้นเป็นไบนารี่ แต่การควบคุมไม่สามารถถ่ายโอนไปยังโค้ดนั้นได้ - จะต้องถูกพุชลงในเบราว์เซอร์อย่างชัดเจนเพื่อคอมไพล์ ส่งผลให้เกิดวัตถุบางอย่างจากโลก JS ซึ่งยังคงจำเป็นต้อง ได้รับการบันทึกไว้ที่ไหนสักแห่ง อย่างไรก็ตาม สำหรับสถาปัตยกรรม RISC ปกติ เท่าที่ฉันเข้าใจ สถานการณ์ทั่วไปคือความจำเป็นในการรีเซ็ตแคชคำสั่งสำหรับโค้ดที่สร้างใหม่อย่างชัดเจน - หากนี่ไม่ใช่สิ่งที่เราต้องการ ไม่ว่าในกรณีใด ก็ปิดไปแล้ว นอกจากนี้ จากความพยายามครั้งล่าสุดของฉัน ฉันได้เรียนรู้ว่าการควบคุมดูเหมือนจะไม่ถูกถ่ายโอนไปยังตรงกลางของบล็อกการแปล ดังนั้นเราจึงไม่จำเป็นต้องแปลโค้ดไบต์จากออฟเซ็ตใดๆ จริงๆ และเราสามารถสร้างมันได้จากฟังก์ชันบน TB .

พวกเขามาและเตะ

แม้ว่าฉันจะเริ่มเขียนโค้ดใหม่ในเดือนกรกฎาคม แต่ก็เกิดความมหัศจรรย์เพิ่มขึ้นโดยไม่มีใครสังเกตเห็น โดยปกติแล้วจดหมายจาก GitHub จะมาถึงเป็นการแจ้งเตือนเกี่ยวกับการตอบสนองต่อปัญหาและคำขอดึง แต่ที่นี่ ทันใดนั้น กล่าวถึงในกระทู้ Binaryen เป็นแบ็กเอนด์ qemu ในบริบท “เขาทำแบบนั้น บางทีเขาอาจจะพูดอะไรสักอย่าง” เรากำลังพูดถึงการใช้ไลบรารี่ที่เกี่ยวข้องกับ Emscripten ไบนาเรน เพื่อสร้าง WASM JIT ฉันบอกว่าคุณมีใบอนุญาต Apache 2.0 ที่นั่นและ QEMU โดยรวมได้รับการเผยแพร่ภายใต้ GPLv2 และเข้ากันไม่ได้มากนัก ทันใดนั้นปรากฎว่าสามารถมีใบอนุญาตได้ แก้ไขมันอย่างใด (ฉันไม่รู้: อาจจะเปลี่ยน, อาจเป็นลิขสิทธิ์แบบคู่, อาจเป็นอย่างอื่น...) สิ่งนี้ทำให้ฉันมีความสุขอย่างแน่นอน เพราะเมื่อถึงเวลานั้นฉันก็ได้เฝ้าดูอย่างใกล้ชิดแล้ว รูปแบบไบนารี WebAssembly และฉันก็เศร้าและเข้าใจยาก นอกจากนี้ยังมีห้องสมุดที่จะกินบล็อกพื้นฐานด้วยกราฟการเปลี่ยนแปลง สร้างรหัสไบต์ และแม้แต่เรียกใช้ในล่ามเอง หากจำเป็น

จากนั้นก็มีมากขึ้น จดหมาย ในรายชื่อผู้รับจดหมายของ QEMU แต่นี่เป็นคำถามเกี่ยวกับ "ใครต้องการมันอีกบ้าง" และมันก็เป็น ทันใดนั้นปรากฎว่ามันจำเป็น อย่างน้อยที่สุด คุณสามารถรวบรวมความเป็นไปได้ในการใช้งานต่อไปนี้ได้ ถ้ามันทำงานได้เร็วไม่มากก็น้อย:

  • เปิดตัวบางสิ่งบางอย่างเพื่อการศึกษาโดยไม่ต้องติดตั้งใดๆ เลย
  • การจำลองเสมือนบน iOS โดยที่ตามข่าวลือแอปพลิเคชันเดียวที่มีสิทธิ์ในการสร้างโค้ดได้ทันทีคือเอ็นจิ้น JS (จริงหรือไม่)
  • การสาธิต mini-OS - ฟลอปปี้เดียว, เฟิร์มแวร์ในตัว, เฟิร์มแวร์ทุกชนิด ฯลฯ ...

คุณสมบัติรันไทม์ของเบราว์เซอร์

ดังที่ฉันได้กล่าวไปแล้ว QEMU เชื่อมโยงกับมัลติเธรด แต่เบราว์เซอร์ไม่มี นั่นคือไม่... ในตอนแรกมันไม่มีเลยจากนั้น WebWorkers ก็ปรากฏขึ้น - เท่าที่ฉันเข้าใจนี่คือมัลติเธรดตามการส่งข้อความ โดยไม่มีตัวแปรร่วม. โดยปกติแล้ว สิ่งนี้จะสร้างปัญหาสำคัญเมื่อทำการย้ายโค้ดที่มีอยู่ตามรุ่นหน่วยความจำที่แชร์ จากนั้นภายใต้แรงกดดันสาธารณะ จึงมีการดำเนินการภายใต้ชื่อด้วย SharedArrayBuffers. มีการเปิดตัวอย่างค่อยเป็นค่อยไป พวกเขาเฉลิมฉลองการเปิดตัวในเบราว์เซอร์ที่แตกต่างกัน จากนั้นพวกเขาก็เฉลิมฉลองปีใหม่ แล้วก็ Meltdown... หลังจากนั้นพวกเขาก็ได้ข้อสรุปว่าการวัดเวลาหยาบหรือหยาบ แต่ด้วยความช่วยเหลือของหน่วยความจำที่ใช้ร่วมกันและ เธรดที่เพิ่มตัวนับก็เหมือนกันทั้งหมด มันจะออกมาค่อนข้างแม่นยำ. ดังนั้นเราจึงปิดการใช้งานมัลติเธรดด้วยหน่วยความจำที่ใช้ร่วมกัน ดูเหมือนว่าพวกเขาจะเปิดมันอีกครั้งในภายหลัง แต่เมื่อเห็นได้ชัดเจนจากการทดลองครั้งแรก ยังมีชีวิตอยู่ถ้าไม่มีมัน และถ้าเป็นเช่นนั้น เราจะพยายามทำโดยไม่ต้องพึ่งพามัลติเธรด

คุณสมบัติที่สองคือความเป็นไปไม่ได้ของการจัดการระดับต่ำด้วยสแต็ก: คุณไม่สามารถทำได้บันทึกบริบทปัจจุบันและเปลี่ยนไปใช้สแต็กใหม่ด้วยสแต็กใหม่ Call Stack ได้รับการจัดการโดยเครื่องเสมือน JS ดูเหมือนว่าปัญหาคืออะไร เนื่องจากเรายังคงตัดสินใจที่จะจัดการโฟลว์เดิมด้วยตนเองโดยสมบูรณ์ ความจริงก็คือบล็อก I/O ใน QEMU ถูกนำมาใช้ผ่าน coroutines และนี่คือจุดที่การจัดการสแต็กระดับต่ำจะมีประโยชน์ โชคดีที่ Emscipten มีกลไกสำหรับการดำเนินการแบบอะซิงโครนัสอยู่แล้ว แม้แต่สองกลไก: ไม่ตรงกัน и ล่าม. อันแรกทำงานผ่านการบวมที่สำคัญในโค้ด JavaScript ที่สร้างขึ้นและไม่ได้รับการสนับสนุนอีกต่อไป วิธีที่สองคือ "วิธีที่ถูกต้อง" ในปัจจุบันและทำงานผ่านการสร้างโค้ดไบต์สำหรับล่ามเจ้าของภาษา แน่นอนว่ามันใช้งานได้ช้า แต่ก็ไม่ได้ทำให้โค้ดขยายใหญ่ขึ้น จริงอยู่ที่การสนับสนุน coroutines สำหรับกลไกนี้ต้องได้รับการสนับสนุนอย่างอิสระ (มี 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 เขียนขึ้นเองและไม่ได้สร้างขึ้นจากสิ่งใดเลย

WebAssembly

แล้วสิ่งนี้เรียกว่า WebAssembly (aka WASM) คืออะไร? นี่เป็นการแทนที่ Asm.js ซึ่งไม่แสร้งทำเป็นโค้ด JavaScript ที่ถูกต้องอีกต่อไป ในทางตรงกันข้ามมันเป็นไบนารี่ล้วนๆและได้รับการปรับให้เหมาะสมและแม้แต่การเขียนจำนวนเต็มลงไปนั้นก็ไม่ง่ายนัก: เพื่อความกะทัดรัดมันจะถูกจัดเก็บไว้ในรูปแบบ LEB128.

คุณอาจเคยได้ยินเกี่ยวกับอัลกอริธึมการวนซ้ำสำหรับ Asm.js - นี่คือการคืนค่าคำสั่งควบคุมโฟลว์ "ระดับสูง" (นั่นคือ if-then-else, ลูป ฯลฯ ) ซึ่งเอ็นจิ้น JS ได้รับการออกแบบมาจาก LLVM IR ระดับต่ำใกล้กับรหัสเครื่องที่ดำเนินการโดยโปรเซสเซอร์ โดยธรรมชาติแล้ว การแสดงระดับกลางของ QEMU จะใกล้เคียงกับวินาทีมากกว่า ดูเหมือนว่านี่คือ bytecode จุดสิ้นสุดของความทรมาน... แล้วก็มีบล็อก if-then-else และลูป!..

และนี่คืออีกเหตุผลหนึ่งว่าทำไม Binaryen จึงมีประโยชน์: โดยธรรมชาติแล้วมันสามารถยอมรับบล็อกระดับสูงที่ใกล้เคียงกับสิ่งที่จะถูกเก็บไว้ใน WASM ได้ แต่ยังสามารถสร้างโค้ดจากกราฟของบล็อกพื้นฐานและการเปลี่ยนระหว่างบล็อกเหล่านั้นได้ ฉันได้บอกไปแล้วว่ามันซ่อนรูปแบบพื้นที่เก็บข้อมูล WebAssembly ไว้ด้านหลัง C/C++ API ที่สะดวกสบาย

TCG (เครื่องสร้างโค้ดจิ๋ว)

TCG เดิมที แบ็กเอนด์สำหรับคอมไพเลอร์ C เห็นได้ชัดว่าไม่สามารถทนต่อการแข่งขันกับ GCC ได้ แต่ท้ายที่สุดก็พบว่ามีตำแหน่งใน QEMU เป็นกลไกการสร้างโค้ดสำหรับแพลตฟอร์มโฮสต์ นอกจากนี้ยังมีแบ็กเอนด์ TCG ที่สร้างโค้ดไบต์นามธรรมซึ่งจะถูกเรียกใช้งานโดยล่ามทันที แต่ฉันตัดสินใจหลีกเลี่ยงการใช้มันในครั้งนี้ อย่างไรก็ตาม ความจริงที่ว่าใน QEMU เป็นไปได้ที่จะเปิดใช้งานการเปลี่ยนไปใช้วัณโรคที่สร้างขึ้นผ่านฟังก์ชันอยู่แล้ว 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-API อย่างง่าย: คุณสร้างโมดูล ในตัวเขา สร้างนิพจน์ - เอกนารี ไบนารี บล็อกจากนิพจน์อื่น โฟลว์การควบคุม ฯลฯ จากนั้นคุณสร้างฟังก์ชันที่มีนิพจน์เป็นส่วนเนื้อความ หากคุณมีกราฟการเปลี่ยนแปลงระดับต่ำเช่นเดียวกับฉัน องค์ประกอบ relooper จะช่วยคุณได้ เท่าที่ฉันเข้าใจ คุณสามารถใช้การควบคุมระดับสูงของโฟลว์การดำเนินการในบล็อกได้ ตราบใดที่มันไม่เกินขอบเขตของบล็อก นั่นคือ เป็นไปได้ที่จะสร้างเส้นทางด่วน / ช้าภายใน การแตกแขนงเส้นทางภายในโค้ดประมวลผลแคช TLB ในตัว แต่ไม่รบกวนโฟลว์การควบคุม "ภายนอก" เมื่อคุณปลดปล่อย relooper บล็อกของมันจะถูกปลดปล่อย เมื่อคุณปลดปล่อยโมดูล นิพจน์ ฟังก์ชัน ฯลฯ ที่จัดสรรให้กับมันจะหายไป อารีน่า.

อย่างไรก็ตาม หากคุณต้องการตีความโค้ดได้ทันทีโดยไม่ต้องสร้างและลบอินสแตนซ์ล่ามโดยไม่จำเป็น อาจสมเหตุสมผลที่จะใส่ตรรกะนี้ลงในไฟล์ 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.

อนึ่ง การสาธิต (ปัจจุบันมีใบอนุญาตที่คลุมเครือ) ทำงานได้ดีใน Firefox เท่านั้น นักพัฒนา Chrome เคยเป็น ยังไม่พร้อม เนื่องจากมีคนต้องการสร้างโมดูล WebAssembly มากกว่าหนึ่งพันอินสแตนซ์ ดังนั้นพวกเขาจึงจัดสรรพื้นที่ที่อยู่เสมือนหนึ่งกิกะไบต์สำหรับแต่ละ...

นั่นคือทั้งหมดที่สำหรับตอนนี้. บางทีอาจจะมีบทความอื่นถ้าใครสนใจ กล่าวคือยังมีอย่างน้อย เท่านั้น ทำให้อุปกรณ์บล็อคทำงานได้ นอกจากนี้ยังอาจสมเหตุสมผลที่จะสร้างการคอมไพล์โมดูล 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

เพิ่มความคิดเห็น