ตามที่สัญญาไว้เป็นการติดตาม บันทึกย่อของผู้เริ่มต้นของฉันเกี่ยวกับ WebAssembly ต่อไป นี้เป็นบันทึกย่อบางส่วนเกี่ยวกับ WebAssembly โดยเฉพาะเกี่ยวกับ C/C++
เอ็มสคริปเทน เสียงดังกราวมีการสนับสนุนในตัวสำหรับการสร้างรหัส Wasm แต่ยังมีอีกมากในการสร้างรหัสที่มากกว่าแค่การแปล C++ เป็นรหัสเครื่องที่เหมาะสม Emscripten เป็น C++ ไปยังเบราว์เซอร์ Wasm toolchain ที่จัดการสิ่งนี้ ให้คุณมีคอมไพเลอร์ emcc
ที่เปลี่ยนจากแหล่ง C ไปเป็นเอาต์พุต . .wasm
สำหรับตัวอย่างบางส่วน emscripten ได้จัดเตรียมตัวเชื่อมโยงและการใช้งาน malloc
เช่นเดียวกับการสนับสนุนไฟล์ .js ที่จำเป็นสำหรับการโหลด wasm และมันยังไปไกลถึงการสร้างไฟล์ HTML อีกด้วย
สิ่งที่ยิ่งใหญ่อื่น ๆ ที่ Emscripten จัดการคือการทำให้รหัส C ++ ที่มีอยู่ทำงานโดยทำสิ่งต่าง ๆ เช่น shimming การโทร C++ เช่น puts()
ในการเรียก JS console.log()
อีกตัวอย่างหนึ่ง เมื่อคุณเขียนโค้ด C++ GL แสดงว่า C++ GL API บนเบราว์เซอร์ WebGL เรียก และในทำนองเดียวกันสำหรับ C++ API อื่นๆ จำนวนมาก ดู ส่วนเอกสารของพวกเขาใน Porting แต่สำหรับวิธีที่ Figma ใช้ C++ เราไม่ค่อยได้ใช้สิ่งนี้มากนัก เนื่องจากโดยทั่วไปแล้วเราจะเขียนโค้ดตั้งแต่เริ่มต้นจนถึงเบราว์เซอร์เป้าหมาย
โดยรวมแล้ว Emscripten ทำงานได้ดี แต่ก็ค่อนข้างเกะกะ ฉันไม่ได้หมายถึงแค่วิพากษ์วิจารณ์มัน — ในฐานะนักอดิเรก ฉันรู้ว่า การทำสิ่งต่างๆ เป็นเรื่องยากมาก — แต่ให้สังเกตว่ามีพื้นที่ผิวอยู่ในนั้นจำนวนมากอย่างน่าประหลาดใจ เช่น API หลายตัวสำหรับการโต้ตอบกับ JavaScript และแฟล็กของคอมไพเลอร์ ด้วยชื่อเช่น EMULATE_FUNCTION_POINTER_CASTS
มีการประชดว่า Emscripten เป็นเครื่องมือ JS อย่างไร แต่ ไลบรารี JS ของมันมีโครงสร้างเป็นกลุ่มของ globals
พอยน์เตอร์ Null นั้นน่าประหลาดใจ ในสภาพแวดล้อม C ทั่วไป dereference ตัวชี้ null ขัดข้อง ใน Wasm ตัวชี้ค่าว่างหมายถึง memory[0]
และเป็นที่อยู่ทางกฎหมายสำหรับอ่านและเขียน รหัส C ที่อ่าน/เขียนตัวชี้ null เป็นสิ่งที่เราพยายามหลีกเลี่ยงอยู่แล้ว แต่ในสภาพแวดล้อม Wasm ตัวชี้ null รู้สึกเหมือน On Error Resume Next
สำหรับผู้ที่ไม่ใช่ผู้เชี่ยวชาญ ฉันสงสัยว่าคุณสามารถกู้คืน C semantics เกี่ยวกับการจัดการตัวชี้ null เมื่อรวบรวม C เป็น Wasm ได้หรือไม่ ถ้าคุณสามารถสร้างตัวชี้ null แทนที่อยู่หน่วยความจำบางตัวที่สูงถึงกับดัก ตัวอย่างเช่น ฉันจำได้ว่ามีกฎทางกฎหมายเกี่ยวกับ ภาษาว่า NULL ของ C เป็น 0 จริง หรือไม่ อีกทางหนึ่ง ฉันสงสัยว่าคุณสามารถเข้ารหัสทุกหน่วยความจำที่อ้างอิงถึงแทนการอ้างถึง theAddress-0x1000
ได้หรือไม่ โดยเปลี่ยนหน่วยความจำทั้งหมดลงอย่างมีประสิทธิภาพขณะรันไทม์ ซึ่งจะทำให้พอยน์เตอร์ null มีค่าต่ำกว่าที่อยู่สูงที่ผิดกฎหมายในทำนองเดียวกัน (ฉันคิดว่ามันอาจจะไม่ได้แพงเกินไปด้วยซ้ำ — คำแนะนำหน่วยความจำ Wasm ส่วนใหญ่ใช้พารามิเตอร์ออฟเซ็ตคงที่อยู่แล้ว…)
ความปลอดภัย. ในแง่หนึ่ง nullพอยน์เตอร์ที่ไม่ขัดข้องนั้นค่อนข้างแย่สำหรับการรัน C แต่ในอีกด้านหนึ่ง มันน่าสนใจที่จะพิจารณาว่า C ได้รับการสนับสนุนจากฮาร์ดแวร์และระบบปฏิบัติการที่ทันสมัยมากเพียงใด ไม่ใช่แค่หน้าป้องกันเพื่อทำให้ตัวชี้ null เกิดความผิดพลาด แต่ยังรวมถึง overcommit และหน้า W^X และ ASLR และ CFI เป็นต้น ฯลฯ ทั้งหมดในแง่หนึ่งเพื่อบรรเทาข้อบกพร่องที่แก้ไขโดยคอมไพเลอร์ / รันไทม์ที่อื่น (ตัวอย่างเช่น การจัดการค่า null ในทุกภาษาที่ไม่ใช่ C นั้นไม่ได้อาศัยความช่วยเหลือจาก CPU!)
สภาพแวดล้อม Wasm ไม่ได้รับการสนับสนุนแบบเดียวกัน ซึ่งหมายความว่าความปลอดภัยระดับภาษาอาจมีความสำคัญมากกว่าภายใต้ Wasm ในทางกลับกัน สถาปัตยกรรมฮาร์วาร์ดของ Wasm ยังไม่รวมการสร้างปัญหาของ C เช่น ROP; ยิ่งฉันได้ทำงานกับมันมากเท่าไหร่ ก็ยิ่งรู้สึกเหมือนเป็นตัวเลือกที่ “ถูกต้อง” มากขึ้นเท่านั้น และผลที่ตามมาของข้อบกพร่องก็อาจลดลงเช่นกัน เนื่องจากบัฟเฟอร์ล้นไม่ได้นำไปสู่การหลบหนีจากแซนด์บ็อกซ์ Wasm โดยตรง ต่อไปนี้คือการวิเคราะห์ที่ดีของบทความล่าสุดเกี่ยวกับความสมดุลระหว่างปัจจัยเหล่านี้ จากหนึ่งในบล็อกที่ฉันโปรดปราน
สำหรับฉันดูเหมือนว่าเราจะเห็น “การหลีกเลี่ยง Wasm sandbox เข้าสู่การประเมินเบราว์เซอร์” ของข้อบกพร่อง XSS ในอนาคตเพราะนั่นคือวิธีการรักษาความปลอดภัยที่ทำงานได้ทุกที่: สำหรับคุณสมบัติความปลอดภัยเฉพาะของ Wasm นั่นหมายความว่าบั๊กจะเป็น ที่ขอบเขตระหว่าง Wasm และระบบโฮสต์ แม้ว่า XSS ที่เกิดจาก Wasm จะเป็นการประนีประนอมที่แตกต่างจากการโจมตีตัวเบราว์เซอร์เองมาก แต่ XSS ที่ใช้ Wasm นั้นเทียบเท่ากับการกำหนดเป้าหมายไปยังโหนด nodejs สามารถเพิ่มการเรียกใช้โค้ดตามอำเภอใจได้อย่างง่ายดาย (ดูเอกสารด้านบน)
เค้าโครงหน่วยความจำ หน่วยความจำ Wasm เป็นบัฟเฟอร์แบนขนาดใหญ่ตัวหนึ่ง ในการแมป C กับสิ่งนี้ emscripten วางสแต็กไว้ที่ออฟเซ็ตคงที่และขยายลงมาและให้ฮีปเริ่มต้นที่จุดเดียวกันและเติบโตขึ้นไป หากคุณดู ไฟล์ C++ ในรูปแบบ weave คุณจะเห็นตัวชี้สแต็กเริ่มต้นที่กำหนดไว้ในส่วน “สากล” (บทความที่เชื่อมโยงจากโพสต์บล็อกที่เชื่อมโยงในส่วนก่อนหน้ามีเนื้อหามากกว่านี้)
เสมือน ใน โพสต์อื่นที่ ฉันพูดถึงกับดัก ซึ่งเป็นกรณีที่เครื่อง Wasm หยุดโปรแกรมของคุณ ที่ Figma เราพบกับดักในลักษณะที่น่าสนใจที่เกี่ยวข้องกับการโทรเสมือน
ใน C ++ กำหนด struct Iface { void foo(); }
หากคุณเรียก ptr->foo()
เมื่อ ptr
เป็นโมฆะ จะไม่มีอะไรผิดพลาดในทันที มันเพิ่งเรียกใช้ foo
ด้วย this == nullptr
และ Dereferences เช่น this->someMember
เพิ่งอ่านที่อยู่หน่วยความจำเหลือน้อยซึ่งทั้งหมดถูกกฎหมายใน Wasm
แต่ถ้า foo
เป็นวิธีการ เสมือน สิ่งต่างๆ จะซับซ้อนมากขึ้น ดู ผลลัพธ์ที่สร้างขึ้นที่นี่ ตามความหมายของ C++ การโทรเสมือนจะค้นหา vtable ก่อนโดยยกเลิกการอ้างอิงตัวชี้ จากนั้นจึงรับที่อยู่ของฟังก์ชันเป้าหมายจากตารางนั้น (คุณสามารถเห็นสิ่งนี้ในเอาต์พุต godbolt โดยคู่ของคำสั่ง i32.load
) หากมีค่าว่างที่เกี่ยวข้องที่นี่ ก็ไม่มีปัญหาสำหรับ Wasm เพราะคุณเพิ่งได้รับดัชนีขยะกลับมา ในที่สุดก็มี Wasm opcode call_indirect
ซึ่งเรียกใช้ฟังก์ชันที่เลือกโดยดัชนีคำนวณรันไทม์
หากดัชนีนั้นอ้างถึงฟังก์ชันที่อยู่นอกอาร์เรย์ฟังก์ชันที่โปรแกรมของคุณประกาศไว้ ฟังก์ชันนั้นจะดักจับ แต่ถ้าหลังจาก dereference nulls คุณบังเอิญอ้างถึงฟังก์ชันที่มีอยู่บางฟังก์ชัน ในเอาต์พุตด้านบน ให้สังเกตว่า ประเภท ของฟังก์ชันเป้าหมายที่คาดไว้นั้นรวมอยู่ในคำสั่ง call_indirect
อย่างไร
เหตุผลที่ประเภทฟังก์ชันปรากฏขึ้นในความหมายของ Wasm เป็นบางอย่างเกี่ยวกับคุณสมบัติความสมบูรณ์ของ Wasm โดยที่ฉันเข้าใจ มันจำเป็นต้องรู้ว่าค่าประเภทใดที่อยู่ในสแต็ก ณ จุดใดก็ได้ แต่ความหมายในกรณีนี้คือคุณมีดัชนีฟังก์ชันขยะ และหากดัชนีขยะนั้นอ้างอิงถึงฟังก์ชันที่มีประเภทแตกต่างจากที่คุณคาดไว้ Wasm จะดักจับ
เพื่อสรุปส่วนนี้: nullพอยน์เตอร์ไม่ขัดข้องโดยทั่วไป แต่พอยน์เตอร์ null ที่เสมือนสามารถเกิดขึ้นได้ หากบังเอิญอ้างถึงฟังก์ชันที่มีประเภทระดับ Wasm อื่นโดยไม่ได้ตั้งใจ สิ่งที่น่าสยดสยองเกี่ยวกับเรื่องนี้คือคุณสามารถมีโค้ดบางส่วนที่เกี่ยวข้องกับตัวชี้ null สามารถเขียนลวก ๆ ทั่วทั้งหน่วยความจำได้อย่างมีความสุขและทำงานต่อไปได้และวิธีเดียวที่คุณจะสังเกตเห็นได้ก็คือถ้าในที่สุดมันจะดักจับในการโทรเสมือน
บัฟเฟอร์ทางอ้อม รูปแบบที่น่าสนใจอย่างหนึ่งที่เราไว้วางใจที่ Figma เรียกว่า IndirectBuffer แนวคิดพื้นฐานคือคุณสามารถจัดสรรอาร์เรย์ใน JavaScript และยังคงจัดการจาก C ++ โดยเปิดเผยการเรียกใช้แม้ว่าไบต์ของอาร์เรย์จะไม่เคยอยู่ในหน่วยความจำ C ++ สิ่งนี้มีความสำคัญเนื่องจากหน่วยความจำ Wasm ถูก จำกัด ไว้ที่ ~ 4gb แต่หน่วยความจำ JS ไม่ใช่
โดยเฉพาะอย่างยิ่ง เอกสาร Figma จัดการกับรูปภาพจำนวนมาก ดังนั้นเราจึงพยายามเก็บพิกเซลของรูปภาพไว้ในหน่วยความจำ JS หรือ GPU ในขณะที่ยังคงแสดงฉากจาก C++ ตัวอย่างหนึ่งของสิ่งนี้ โปรเจ็กต์หนึ่งที่ฉันทำงานอยู่ที่ Figma เกี่ยวข้องกับฟังก์ชัน “บันทึกเอกสารปัจจุบันเป็นไฟล์” ซึ่งจำเป็นต้องจัดลำดับเนื้อหาทั้งหมดของเอกสารปัจจุบัน (รวมถึงพิกเซลของรูปภาพ) ลงในบัฟเฟอร์ขนาดใหญ่เพียงอันเดียว เอกสารที่โหลดนั้นกินพื้นที่ Wasm ส่วนใหญ่ของคุณไปแล้ว ดังนั้นเราจึงย้ายเอาต์พุตการทำให้เป็นอนุกรมจำนวนมากไปยังบัฟเฟอร์ระดับ JS แม้ว่ารหัสการทำให้เป็นอนุกรมทั้งหมดยังคงอยู่ใน C++
ตอนเป็นเด็ก ฉันได้เริ่มเขียนโปรแกรมในยุค DOS และฉันยังจำการลงทะเบียนเซกเมนต์และพ อยน์เตอร์ที่อยู่ห่างไกล ได้ ทั้งหมดนี้ทำให้ฉันมีความทรงจำที่สนุกสนานในช่วงเวลานั้น แต่ปัญหานี้อาจค่อนข้างเฉพาะเจาะจงสำหรับ Figma ซึ่งจะกิน RAM ทั้งหมดที่คุณให้ได้อย่างมีความสุข
ยังเช้าอยู่ งานกำลังดำเนินการเพื่อขอข้อมูลการดีบัก DWARF ลงใน devtools ของเบราว์เซอร์ ดูเหมือนว่าจะมีแนวโน้มดี แต่เครื่องมือยังค่อนข้างเร็ว มันอยู่นอกขอบเขตสำหรับโพสต์นี้ แต่ Figma มีการตั้งค่าที่น่าทึ่ง/ผิดพลาดที่เราสามารถสร้างและดีบักส่วน C ++ ของแอปโดยใช้ toolchain ดั้งเดิมแม้ว่าแอปส่วนใหญ่จะถูกเขียนด้วย HTML ซึ่งส่วนใหญ่เรายังคงมีชีวิตอยู่เพราะประสบการณ์การพัฒนาดั้งเดิมคือ ยังดีขึ้นมาก
นอกจากนั้น หากคุณมี C++ call stack ที่เรียกใช้ JS แล้วเรียก new Error()
วัตถุที่ได้จะมีเฟรม C++ อยู่ และมีเครื่องจักรเพียงพอที่คุณจะได้รับสัญลักษณ์ ฉันพูดถึงการดีบักเป็นส่วนใหญ่เพื่อบอกว่ามีการปรับปรุงอย่างรวดเร็ว (มันค่อนข้างเรียบร้อยสำหรับขั้นตอนเดียวในเบราว์เซอร์ผ่าน C++ stack!) และยังเชื่อมโยงคุณ กับโพสต์บล็อกอื่นอย่างละเอียด อีกด้วย
อีกตัวอย่างหนึ่ง ใน emscripten toolchain สแต็คของเครื่องมือที่ใช้ในการเชื่อมโยงคือ Wasm/emscripten-specific ซึ่งหมายความว่าพวกเขาไม่เคยเห็นเวลาหลายปีของการปรับแต่งเพื่อให้ทั้งสองทำงานได้อย่างรวดเร็วและสร้างโค้ดที่แน่นหนาที่คุณได้รับบนแพลตฟอร์มอื่น (โดยเฉพาะ Linux) .
อีกตัวอย่างหนึ่ง “มันเร็ว” แม้ว่าฉันเดาว่าตอนนี้ฉันอยู่ในหัวข้อ ฉันรู้ว่าสิ่งนี้ไม่มีอะไรพิเศษเกี่ยวกับ C++: เราเพิ่งพบจุดบกพร่องในการสร้างโค้ดในตัวเพิ่มประสิทธิภาพ Wasm ของ Firefox ฉันยังค่อนข้างแปลกใจที่เพื่อนร่วมงานของฉันสามารถกลั่นกรองข้อผิดพลาดรันไทม์ของ Figma ให้กลายเป็น repro ขนาดเล็กได้ และประทับใจเพิ่มเติมว่าวิศวกรของ Firefox สามารถเขียนโปรแกรมแก้ไขได้ 90 นาทีหลังจากรายงาน (?!)
โดยรวมแล้ว การพัฒนา C++ บน Wasm ให้ความรู้สึกเหมือนกับที่ฉันจินตนาการว่าการพัฒนา C++ สำหรับบางสิ่งเช่นระบบฝังตัวอาจเป็นเช่น: ยังคงเป็น C++ อย่างแน่นอน แต่เครื่องมือนั้นอ่อนแอกว่าเล็กน้อยและแตกต่างไปจากเครื่องมือที่คุณคุ้นเคย
ชะตากรรมของ C++ ในช่วงฤดูร้อนนี้ เป็นเวลา 25 ปีแล้วที่ฉันมีงานเขียนโปรแกรมครั้งแรก เขียน C++ สำหรับการเริ่มต้นดอทคอม และวันนี้ฉันยังคงเขียน C++ สำหรับการเริ่มต้นดอทคอม จากมุมมองนั้น การเรียนรู้ C++ เป็นการลงทุนที่ชาญฉลาด แต่ฉันคิดว่าหลายคนที่มีประสบการณ์ C ++ คล้ายคลึงกันจะเห็นด้วยกับฉันว่ามันค่อนข้างหายากที่จะเป็นเครื่องมือที่เหมาะสมสำหรับซอฟต์แวร์ในปัจจุบัน
โดยเฉพาะอย่างยิ่ง ฉันคิดว่า C ++ เป็นเครื่องมือระดับผู้เชี่ยวชาญในแง่ที่ไม่ดี ซึ่งหากคุณทำผิดพลาด มันก็จะล้มเหลวอย่างมีเจตนาร้าย: ด้วยความเสียหายของหน่วยความจำแบบเงียบหรือประสิทธิภาพที่ไม่ดีอย่างเงียบๆ เนื่องจากการคัดลอกโดยไม่ได้ตั้งใจ ในขณะเดียวกัน ดังที่ Zaplib post-mortem ได้ค้นพบ มันไม่ชัดเจนว่าภาษาที่มีประสิทธิภาพดีกว่านั้นแปลเป็นประสิทธิภาพได้อย่างง่ายดาย และหากไม่ใช่เพื่อประสิทธิภาพ ก็ยังมีเหตุผลน้อยกว่าในการเขียน C++
ด้วยเหตุนี้ ฉันจึงเห็นอนาคตของ C++ และ Wasm อยู่ที่การรวมโค้ด C++ แบบเก่าเป็นส่วนใหญ่ ไม่ใช่การเขียนโค้ดใหม่ ส่วนใหญ่เป็นเพียงคำแถลงเกี่ยวกับความรู้สึกของฉันเกี่ยวกับ C ++ โดยทั่วไป แต่โดยเฉพาะอย่างยิ่ง Wasm ทำให้ชีวิตใหม่สามารถเชื่อมโยงรหัส C ++ เก่าเข้ากับบริบทใหม่ได้ ตัวอย่างเช่น Mozilla ส่ง แฮ็คที่น่าทึ่ง ซึ่งใช้ Wasm กับโค้ด C++ แบบแซนด์บ็อกซ์ที่ใช้ใน Firefox เอง: พวกเขารวบรวม C++ ของตนไปยัง Wasm แล้วเชื่อมโยงกลับเข้าไปในแอป C ++ ที่ใหญ่กว่า โดยมีเอฟเฟกต์รันไทม์เกือบจะเหมือนกับการเรียกใช้โมดูลย่อย C++ ภายใน โปรแกรมจำลอง