บทนำ
ดีบัก Wasm อยู่หรือเปล่า? โรย Whamm ลงไปหน่อยสิ!
หากคุณกำลังสร้างการวิเคราะห์เชิงพลวัต (dynamic analysis) ใหม่สำหรับ Wasm และกำลังมองหาเฟรมเวิร์กที่จะช่วยสนับสนุนคุณอยู่ มาถูกที่แล้วครับ!
Whamm คือเฟรมเวิร์กสำหรับงาน “Wasm Application Monitoring and Manipulation”1
หนังสือเล่มนี้จะช่วยให้คุณก้าวไปในทิศทางที่ถูกต้องเมื่อต้องทำงานกับภาษานี้
เครื่องมือในเฟรมเวิร์ก Whamm เขียนขึ้นด้วยภาษาจำเพาะโดเมน (domain-specific language หรือ DSL) ของมันเอง
โดยได้แรงบันดาลใจมาจากภาษา D ของ DTrace
DSL ของ Whamm ช่วยให้นักพัฒนาเครื่องมือสำหรับ Wasm แสดง instrumentation ของตนผ่านนามธรรมระดับสูง ไม่ว่าจะเป็นอีเวนต์หรือตำแหน่งในโปรแกรมที่ความละเอียดต่างกันก็ตาม
ต่อไปนี้คือเป้าหมายบางส่วนของ DSL Whamm:
- ระดับสูงและเข้าใจง่าย
- instrumentation สร้างได้ง่าย ทดสอบได้ และดีบักได้
- แสดง instrumentation ในรูปของโพรบที่มีเพรดิเคต
- สามารถทำ instrumentation กับ อีเวนต์ที่ความละเอียดต่างกันได้
- ให้ พฤติกรรมในรูปฟังก์ชัน Wasm แล้วระบุว่าควรเรียกใช้ตรงไหนใน
Whamm - เขียน instrumentation ครั้งเดียว ส่วนที่เหลือ
Whammจัดการกลยุทธ์การอินเจกต์ให้
เครื่องมือที่เขียนด้วยเฟรมเวิร์ก Whamm สามารถนำไปใช้ได้ทั้งผ่านความสามารถด้าน dynamic instrumentation ของเอนจิน หรือผ่านการเขียนไบต์โค้ดใหม่ในกรณีที่เอนจินไม่มีความสามารถดังกล่าว
คุณใช้เครื่องมือเดียวกันกับกลยุทธ์ใดก็ได้ เพราะ Whamm ดูแลการอินเจกต์ให้ทั้งหมด!
ด้วยความสามารถในการพอร์ตเช่นนี้ Whamm จึงขยายขอบเขตการใช้งานของเครื่องมือให้กว้างขึ้น และช่วยลดการแตกกระจายของเครื่องมือ ทั้งในแง่ของภาษาต้นทาง (เพราะ Wasm รองรับได้หลายภาษา) และในแง่ของเอนจิน Wasm (เพราะมีทางเลือกสำรองอย่างการเขียนไบต์โค้ดใหม่ที่รันได้ทุกที่)
ลองหยุดสักครู่เพื่อพิจารณาขนาดของผลกระทบที่ DSL นี้จะมีต่อเครื่องมือของนักพัฒนา โดยดูจากข้อเท็จจริงต่อไปนี้:
- WebAssembly กำลังเติบโตไปสู่การใช้งานนอกเบราว์เซอร์
- หลายภาษาสามารถคอมไพล์เป็น Wasm ได้
- ด้วย
Whammเขียน instrumentation ครั้งเดียว ก็รองรับแอปได้หลากหลายโดเมน- ใช้ความสามารถของเอนจินเท่าที่มี
- ใช้การเขียนไบต์โค้ดใหม่เพื่อรองรับทุกอย่างที่เหลือ
นั่นหมายความว่าเครื่องมือของนักพัฒนาที่เขียนด้วย Whamm จะรองรับแอปพลิเคชันได้อย่างมหาศาล และทำให้ WebAssembly กลายเป็นแพลตฟอร์มเป้าหมายแห่งอนาคตสำหรับการดีบัก
แหล่งข้อมูล
กลยุทธ์การอินเจกต์:
- เปเปอร์ที่อธิบายกลยุทธ์การอินเจกต์แบบไม่รุกล้ำที่
Whammใช้: Flexible Non-intrusive Dynamic Instrumentation for WebAssembly - ไลบรารีที่ใช้ทำกลยุทธ์การอินเจกต์แบบรุกล้ำ (การเขียนไบต์โค้ดใหม่): wirm
คำศัพท์และแนวคิดที่เป็นประโยชน์
- WebAssembly (Wasm): WebAssembly คือรูปแบบคำสั่งไบนารีสำหรับเครื่องเสมือนแบบสแตก ถูกออกแบบให้เป็นเป้าหมายการคอมไพล์ที่พอร์ตได้สำหรับภาษาต่างๆ
- Instrumentation: เมื่อเราบอกว่า “กำลังทำ instrumentation ให้โปรแกรม” ในระดับสูงหมายถึง “กำลังแทรกโค้ดบางอย่าง เข้าไปในโปรแกรม เพื่อทำงานบางอย่าง” นิยามนี้ตั้งใจให้กว้างเช่นนี้ เพราะ instrumentation ทำได้แทบทุกอย่างเท่าที่เราจินตนาการออก! คุณใช้ instrumentation สร้างดีบักเกอร์ การวิเคราะห์เชิงพลวัต ตัวสร้างเทเลเมทรี และอื่นๆ ได้อีกมากมาย
- Dynamic analysis: การวิเคราะห์เชิงพลวัตคือการวิเคราะห์โปรแกรมขณะที่มันกำลังทำงาน (ตรงข้ามกับการวิเคราะห์เชิงสถิตที่วิเคราะห์โปรแกรมซึ่งไม่ได้รันอยู่) การวิเคราะห์ประเภทนี้เปิดโอกาสให้ได้ข้อมูลเชิงลึกที่เป็นประโยชน์เกี่ยวกับโปรแกรม เพราะสามารถเข้าถึงข้อมูลที่ไม่มีในเชิงสถิตได้ (เช่น ตำแหน่งโค้ดที่ทำงานบ่อย การเข้าถึงหน่วยความจำเมื่อเวลาผ่านไป ความครอบคลุมโค้ดของชุดทดสอบ เป็นต้น)
- Bytecode rewriting: นี่คือตัวอย่างหนึ่งของกลยุทธ์การฉีดตรรกะ instrumentation เข้าไปในแอปพลิเคชัน กลยุทธ์นี้ฉีด instrumentation ด้วยการแทรกคำสั่งใหม่เข้าไปในไบต์โค้ดของแอปพลิเคชันโดยตรง
1: ตัว ‘h’ ไม่ต้องออกเสียง
เริ่มต้นใช้งาน
ในส่วนนี้คุณจะได้พบข้อมูลเกี่ยวกับวิธีเริ่มต้นเขียน instrumentation ด้วย DSL Whamm
การติดตั้ง
ปัจจุบัน วิธีติดตั้ง Whamm คือการโคลนรีโพสิทอรี บิลด์ซอร์สโค้ดด้วยตนเอง แล้วเพิ่มไบนารีที่บิลด์ได้ลงในตัวแปร PATH ของคุณ
ในอนาคต ผู้ใช้จะสามารถดาวน์โหลดไบนารีที่บิลด์ไว้ล่วงหน้าจากหน้า releases บน GitHub ได้ เพราะตอนนี้เรามีรีลีสของ Whamm ที่ติดแท็กและเสถียรแล้ว
ขั้นตอนมีดังนี้:
- โคลนรีโพ
Whamm - บิลด์ซอร์สโค้ดด้วย
cargo build --release - เพิ่มไบนารีที่บิลด์ได้ลงใน
PATHของคุณ ไบนารีนี้ควรอยู่ที่target/release/whamm1
การทดสอบเบื้องต้น
การทดสอบเบื้องต้นที่คุณสามารถรันเพื่อยืนยันว่าไบนารี Whamm อยู่ใน path ของคุณและทำงานได้ตามที่คาดหวัง คือการรันคำสั่ง whamm --help
CLI จะแสดงข้อมูลเกี่ยวกับคำสั่งและตัวเลือกต่างๆ ที่พร้อมใช้งานให้คุณ
มอนิเตอร์และตัวจัดการ Wasm
อย่างที่กล่าวไว้ในบทนำ Whamm ใช้ได้ทั้งเพื่อมอนิเตอร์ หรือ ปรับเปลี่ยนการทำงานของโปรแกรม
การมอนิเตอร์การทำงานหมายถึงการเก็บข้อมูลบางอย่างเกี่ยวกับพฤติกรรมเชิงพลวัตของโปรแกรม ซึ่งมักนำไปใช้ในการดีบัก การบันทึกล็อก และการเก็บเมตริก
ส่วนการปรับเปลี่ยนการทำงานหมายถึงการเปลี่ยนพฤติกรรมเชิงพลวัตของโปรแกรมจริงๆ
ลองนึกถึงฟีเจอร์หนึ่งของเครื่องมือดีบักที่เราคุ้นเคยกันดี เมื่อใช้ดีบักเกอร์ นักพัฒนาสามารถตั้งเบรกพอยต์ ตรวจสอบสถานะปัจจุบันของแอปพลิเคชัน และเปลี่ยนค่าของตัวแปรได้
นี่คือตัวอย่างของการปรับเปลี่ยนพฤติกรรมเชิงพลวัตของแอปพลิเคชันผ่านการเปลี่ยนสถานะ และเป็นสิ่งที่เรามุ่งจะรองรับใน Whamm
อ่านเนื้อหาส่วน “เริ่มต้นใช้งาน” ของหนังสือเล่มนี้ต่อไป เพื่อดูวิธีเขียนมอนิเตอร์และตัวจัดการในลักษณะดังกล่าว
โครงสร้างของ Instrumentation ใน Whamm
โครงสร้างหลักของ instrumentation ที่เขียนด้วย Whamm ประกอบด้วยสองส่วน คือ สคริปต์ Whamm (instr.mm) และไลบรารี instrumentation (instr.wasm)
ไฟล์ทั้งสองนี้ร่วมกันบอกคอมไพเลอร์ Whamm ว่าจะแทรก instrumentation ที่ไหน และจะฉีดตรรกะอะไร ณ จุดเหล่านั้น
instr.mm
ที่ไหนที่จะแทรก instrumentation
สคริปต์ Whamm นี้บรรยายหน่วยของ instrumentation โดยระบุจุดที่ต้องการโพรบในแอปพลิเคชันด้วยไวยากรณ์โพรบ
โดยเนื้อโพรบแต่ละอันจะเก็บตรรกะที่จะฉีด ณ จุดที่แมตช์นั้น
DSL มีความสามารถเพียงพอต่อการรองรับกรณีการใช้งานมอนิเตอร์บางประเภท อย่างไรก็ตาม เพื่อไม่ให้ขอบเขตของ DSL บานปลาย เราจึงตัดสินใจผลักการเขียนโปรแกรมแบบอเนกประสงค์ออกไปยังภาษาใดก็ได้ที่คอมไพล์เป็น Wasm ได้
สคริปต์สามารถเรียกไลบรารี instrumentation ของผู้ใช้ ซึ่งจัดเตรียมไว้ในรูป lib.wasm
lib.wasm
ตรรกะอะไรที่จะแทรก ณ จุดเป้าหมายของแอปพลิเคชัน
การจัดหาไลบรารีนี้ให้ Whamm เป็นเรื่องทางเลือก
จำเป็นเฉพาะเมื่อผู้ใช้ต้องการเขียน instrumentation ด้วยภาษาระดับสูงกว่า หรือเมื่อ DSL ไม่มีไวยากรณ์/เครื่องมือบางอย่างที่จำเป็นต่อตรรกะ instrumentation เท่านั้น
ภาษาดังกล่าวต้องคอมไพล์เป็น Wasm ได้ เนื่องจากต้องส่งไลบรารีให้คอมไพเลอร์ Whamm ในรูปไฟล์ Wasm
ผู้ใช้ควรคำนึงไว้เสมอว่า ลายเซ็นของฟังก์ชันในไลบรารีที่จะถูกเรียกใช้ภายในเนื้อโพรบ ต้องมีชนิดข้อมูลที่เข้ากันได้กับชนิดข้อมูลที่ Whamm ให้ไว้ในปัจจุบัน
มิฉะนั้น Whamm จะไม่สามารถคอมไพล์โค้ด instrumentation ของผู้ใช้ได้
เครื่องมือที่เป็นประโยชน์
ต่อไปนี้คือเครื่องมือบางอย่างที่อาจช่วยคุณได้เมื่อทำงานกับ Wasm:
wabtหรือที่รู้จักกันในชื่อ WebAssembly Binary Toolkitwasm-tools
1: เราแนะนำให้เพิ่มไบนารีที่บิลด์ภายใน target/ ลงใน path ของคุณ เพราะจะทำให้คุณดึงการเปลี่ยนแปลงล่าสุดบน master มาบิลด์เวอร์ชันใหม่ และมีไบนารีล่าสุดอยู่ใน PATH โดยอัตโนมัติ
ภาษา
Whamm เปิดทางให้นักพัฒนาเครื่องมือแสดง instrumentation ในรูปของอีเวนต์ในโปรแกรมและการกระทำที่มีเพรดิเคตที่สอดคล้องกัน
กล่าวคือ “เมื่ออีเวนต์นี้เกิดขึ้นระหว่างการทำงานของโปรแกรม ให้ลงมือทำการกระทำเหล่านี้หากเพรดิเคตนี้ (หรือเงื่อนไข) ประเมินเป็น true”
นามธรรมเช่นนี้ทำให้ได้ไวยากรณ์ระดับสูงที่เข้าใจง่าย และกำหนดเป้าหมายอีเวนต์ได้ที่ความละเอียดต่างๆ กันในโปรแกรมเป้าหมาย
อ่านต่อไปเพื่อดูภาพรวมของไวยากรณ์และความหมายของภาษา
แนวคิดของภาษา
- ตัวแปร ใช้สำหรับเก็บข้อมูล
- การดำเนินการเชิงตรรกะ ใช้รวมนิพจน์บูลีนหลายนิพจน์เข้าด้วยกัน
- นิพจน์เทอร์นารี ช่วยกำหนดค่าตัวแปรแบบมีเงื่อนไขได้อย่างกระชับ
- ชนิดพื้นฐาน ได้แก่ ตัวเลข บูลีน และสตริง
- การดำเนินการทางเลขคณิต รูปแบบต่างๆ ใช้ได้กับตัวเลข
- สตริง เป็นหัวใจของการจัดการข้อความและอื่นๆ
- ทูเพิล ช่วยให้ใช้หลายค่าตรงที่ปกติคาดว่าจะมีค่าเดียว
- แมป เป็นหัวใจของการเก็บข้อมูลปริมาณมาก แต่ถูกนำไปใช้ต่างออกไปพอสมควรใน
Whamm - นิยามฟังก์ชัน ช่วยให้ใช้โค้ดบางส่วนซ้ำได้
- คำสั่งเงื่อนไข คือคำสั่ง if/else/elif สำหรับควบคุมโฟลว์อย่างง่าย
- ขอบเขตชนิด ใช้จัดการออปโค้ดที่มีหลายรูปแบบ (polymorphic)
Whammยังมีฟีเจอร์ที่ช่วยเสริมความสามารถในการแสดง instrumentation- ตัวแปร Report เป็นหัวใจของการระบายข้อมูลเชิงพลวัตที่ instrumentation เก็บรวบรวมระหว่างการรันแอปพลิเคชัน
- ตัวแปร Unshared สร้างอินสแตนซ์ของตัวแปรให้ทุกจุดที่แมตช์ของโพรบ โดยค่ายังคงอยู่ข้ามการมาเยือนจุดนั้น
- ตัวแปร Shared สร้างอินสแตนซ์ของตัวแปรที่ทุกจุดที่แมตช์ของโพรบใช้ร่วมกัน
- ตัวแปร Frame สร้างอินสแตนซ์ของตัวแปรที่ถูกเก็บไว้บนเฟรมที่ทำงานอยู่ของฟังก์ชัน
- และสุดท้าย โพรบ ใช้แสดง instrumentation
- ไวยากรณ์ทั้งหมดนี้ใช้เขียนสคริปต์
Whamm
ตัวแปร
ตัวแปรใช้สำหรับเก็บข้อมูล ไม่ว่าจะเป็นตัวเลขหรือสตริง
// Declaring a new variable `<type> <var_name>;`:
var i: i32;
// Assigning a value to a variable `<var_name> = <value>;`:
i = 0;
// Variables can also be set to the result of an expression `<var_name> = <expression>;`:
i = 1 + 2;
i = add(1, 2) + 9; // (assuming that the `add` fn is in scope and returns an `i32`)
สโคป
ตัวแปรแต่ละตัวจะผูกอยู่กับสโคป (scope) บางอย่าง ซึ่งหมายถึงช่วงของโปรแกรมที่ตัวแปรนั้นทำงานอยู่และเข้าถึงได้ เราจะได้เห็นว่ามีสโคปที่ผูกอยู่กับฟังก์ชัน, โพรบ และสคริปต์ ไวยากรณ์สำหรับการประกาศและการกำหนดค่าตัวแปรนั้นสอดคล้องกันในทุกบริบทเหล่านี้
ตัวดำเนินการเชิงตรรกะ
ตัวดำเนินการเชิงตรรกะ (logical operators) ใช้เชื่อมนิพจน์บูลีนหลายนิพจน์เข้าด้วยกัน
เช่นเดียวกับ C/C++ และ Java ตัวดำเนินการ && และ || ให้การดำเนินการ AND เชิงตรรกะและ OR เชิงตรรกะตามลำดับ
ตัวดำเนินการทั้งสองมีการประเมินแบบลัดวงจร (short-circuit evaluation) นั่นคือ จะประเมินนิพจน์ทางขวามือก็ต่อเมื่อทางซ้ายมือประเมินเป็น true หรือ false ตามลำดับเท่านั้น
var a: bool;
a = false && false; // == false
a = false && true; // == false
a = true && false; // == false
a = true && true; // == true
var a: bool;
a = false || false; // == false
a = false || true; // == true
a = true || false; // == true
a = true || true; // == true
นิพจน์เทอร์นารี
Whamm รองรับนิพจน์ “เงื่อนไข” รูปแบบหนึ่งที่เลือกค่าระหว่างสองค่าตามเงื่อนไข
ไวยากรณ์เป็นไปตามแบบแผนของ C, C++ และ Java ซึ่งใช้เครื่องหมาย ? :
// with declared types
var a: i32;
a = 1 > 0 ? 16 : 27; // == 16
a = 1 < 0 ? 17 : 29; // == 29
การประเมินแบบลัดวงจร
นิพจน์เทอร์นารีจะประเมินเฉพาะกิ่งที่ตรงกับค่าของเงื่อนไขเท่านั้น พูดอีกอย่างคือ มันลัดวงจรนั่นเอง
ชนิดพื้นฐาน
Whamm มีชนิดพื้นฐาน (primitive types) ชุดเล็กๆ ที่มีประโยชน์ต่อการคำนวณทางเลขคณิตและการแทนข้อมูล
บูลีน
ด้วยค่าเพียงสองค่า คือ true และ false บูลีนจึงถูกแทนใน Whamm ด้วยชนิด bool
var x: bool; // default == false
x = true;
x = false;
จำนวนเต็ม
ขณะนี้ Whamm รองรับชนิดจำนวนเต็มต่อไปนี้:
| ชนิด | บิต | คำอธิบาย | ค่าต่ำสุด | ค่าสูงสุด |
|---|---|---|---|---|
u8 | 8 | ค่าไม่มีเครื่องหมาย | 0 | 255 (เท่ากับ 2^8 - 1) |
i8 | 8 | ค่ามีเครื่องหมาย | -128 (เท่ากับ -2^7) | 127 (เท่ากับ 2^7 - 1) |
u16 | 16 | ค่าไม่มีเครื่องหมาย | 0 | 65_535 (เท่ากับ 2^16 - 1) |
i16 | 16 | ค่ามีเครื่องหมาย | -32_768 (เท่ากับ -2^15) | 32_767 (เท่ากับ 2^15 - 1) |
u32 | 32 | ค่าไม่มีเครื่องหมาย | 0 | 4_294_967_295 (เท่ากับ 2^32 - 1) |
i32 | 32 | ค่ามีเครื่องหมาย | -2_147_483_648 (เท่ากับ -2^31) | 2_147_483_647 (เท่ากับ 2^31 - 1) |
f32 | 32 | ค่าจุดลอยตัว | -3.40282347E+38 | 3.40282347E+38 |
u64 | 64 | ค่าไม่มีเครื่องหมาย | 0 | 4_294_967_295 (เท่ากับ 2^32 - 1) |
i64 | 64 | ค่ามีเครื่องหมาย | -2_147_483_648 (เท่ากับ -2^31) | 18_446_744_073_709_551_615 (เท่ากับ 2^63 - 1) |
f64 | 64 | ค่าจุดลอยตัว | -1.7976931348623157E+308 | 1.7976931348623157E+308 |
// with declared types
var d: i32; // default == 0
d = 0;
d = 9993;
d = -42;
เลขคณิต
เลขคณิตเป็นพื้นฐานของการคำนวณ
Whamm นิยามตัวดำเนินการทางเลขคณิตหลายตัวอย่างบนชนิดพื้นฐาน
ชนิดจำนวนเต็ม
var a: i32;
a = 0;
a++; // increment == 1
a--; // decrement == 0
a = 9 + 3; // add == 12
a = 8 - 2; // subtract == 6
a = 7 * 4; // multiply == 28
a = 9 / 2; // divide == 4
a = 5 % 3; // modulus == 2
สตริง
Whamm มีการรองรับสตริงแบบพื้นฐานมาก สตริงจะถูกฉีดเข้าไปในโปรแกรม Wasm ที่ทำ instrumentation
และแทนด้วยทูเพิล: (memory_address, length) โดยกำหนดได้เฉพาะสตริงสแตติก (static string)
เช่น "i'm a string" เท่านั้น ยังไม่รองรับการสร้างสตริงแบบพลวัต เช่น "i " + " am " + " groot"
อย่างไรก็ตาม หากต้องการฟังก์ชันนี้ ก็ใช้วิธีหลบเลี่ยงด้วยการดึงสตริงจากไลบรารีของผู้ใช้ได้
var a: str = "";
var b: str = "The quick brown fox";
// var c: string = null; // INVALID -- strings cannot be set to null
การหลบอักขระ (Escapes)
สตริงสามารถใช้ตัวอักษร \ เพื่อหลบลำดับอักขระบางอย่างได้ เช่น:
\nขึ้นบรรทัดใหม่\tแท็บ\"เครื่องหมายอัญประกาศคู่\'เครื่องหมายอัญประกาศเดี่ยว\\แบ็กสแลช\0ไบต์ null\x(HEX_DIGIT{2})แทรกเลขฐานสิบหก\u(HEX_DIGIT+)แทรกยูนิโคด
// encodes the string literal: "hello drop😀😀A"
report var s: str = "hello drop\u{1F600}😀\x41\n";
ยูทิลิตี้
ยูทิลิตี้ต่อไปนี้มีให้ใช้สำหรับการดำเนินการกับสตริง:
var s: str = "test";
/// Returns the length of the string.
/// This length is in bytes, not chars or graphemes. In other words, it might not be
/// what a human considers the length of the string.
var l: u32 = s.len();
/// Returns true if the given pattern matches a prefix of this string slice.
/// Returns false if it does not.
var r0: bool = s.starts_with("te"); // `true`
var r1: bool = s.starts_with("st"); // `false`
/// Returns true if the given pattern matches a suffix of this string slice.
/// Returns false if it does not.
var r2: bool = s.ends_with("te"); // `false`
var r3: bool = s.ends_with("st"); // `true`
/// Returns true if the given pattern matches a sub-slice of this string.
/// Returns false if it does not.
var r4: bool = s.contains("te"); // `true`
var r5: bool = s.contains("NA"); // `false`
สตริงและไลบรารี
สตริงสามารถส่งต่อไปยัง / ดึงกลับจากไลบรารีได้ผ่านการดำเนินการกับหน่วยความจำ เพื่อให้ สิ่งนี้ทำงานได้ในไลบรารีของคุณเอง คุณต้องเปิดเผยฟังก์ชันสำหรับจัดสรร/คืนหน่วยความจำ
เขียนไปยังไลบรารี:
use whamm_core;
wasm:opcode:drop:before {
// initialize the string to pass
var s: str = "hello world!";
var l: u32 = s.len();
// allocate the right number of bytes to store the string
var ptr: i32 = whamm_core.mem_alloc(l as i32);
// write the string to the target library's memory
write_str(memid(whamm_core), ptr, s);
// call a function in the library that uses the passed string
// (this just prints the string)
whamm_core.puts(ptr, l as i32);
// free the allocated memory
whamm_core.mem_free(ptr);
}
ดึงจากไลบรารี:
use alpha;
wasm:opcode:drop:before {
// allocate some space in memory to store the library's string
var MAX: i32 = 100;
var ptr: i32 = alpha.mem_alloc(MAX);
// write a string to memory, return the length of the string written
var l: i32 = alpha.write_alphabet(ptr, MAX);
// read the string from the library's memory
var s: str = read_str(memid(alpha), ptr, l as u32);
// free the memory we've just used
alpha.mem_free(ptr);
}
ทูเพิล
ทูเพิล (tuple) เป็นวิธีที่รวดเร็วและง่ายดายในการรวมค่าตั้งแต่หนึ่งค่าขึ้นไปให้เป็นค่าเชิงประกอบ
var a: (i32, i32);
a = (0, 1);
var b: (bool, i32);
b = (true, 78);
การเข้าถึงสมาชิก
สมาชิกของทูเพิลเข้าถึงได้เสมือนเป็นฟิลด์ของค่าทูเพิล
แทนที่จะใช้ชื่อฟิลด์ สมาชิกจะถูกอ้างด้วยเลขจำนวนเต็ม เริ่มนับจาก 0
// tuple members are accessed with '.' and numbered from 0
var a: (bool, i32);
a = (true, 1);
var b: bool;
b = a.0; // == true
var c: i32;
c = a.1; // == 1
var d: (i32, (i32, i32));
d = (1, (12, 13));
var e: i32;
e = d.1.0; // == 12
แมป
Whamm มีแมป (map) สำหรับจัดเก็บคู่คีย์-ค่า
ซึ่งคล้ายกับชนิด Map ของ java และ dict ของ python
อันที่จริงมันคือชนิด HashMap ของ Rust เป๊ะๆ … เพราะ Whamm นำชนิดของ Rust นี้มาใช้อยู่เบื้องหลัง!
การสร้างอินสแตนซ์
// No need to instantiate a map, it is automatically created as an empty map.
var a: map<i32, i32>;
การอ่านและเขียนสมาชิก
การอ่านและเขียนสมาชิกของแมปใช้ไวยากรณ์ [ ... ] เหมือนแมปในภาษาอื่นๆ หลายภาษา
var a: map<i32, i32>;
a[0] = 3; // map write
var b: i32 = a[0]; // map read
// maps can also contain tuples!
var c: map<(i32, i32, i32), i32>;
c[(0, 0, 0)] = 3; // map write
var b: i32 = c[(0, 0, 0)]; // map read
ขอบเขตและการตรวจ null
การเข้าถึงแมปของ Whamm ถูกตรวจสอบกับขอบเขตแบบพลวัต
var a: map<i32, i32>;
var b: i32 = a[0]; // produces Wasm trap through Rust
ฟังก์ชัน
หมายเหตุ: ฟังก์ชันนี้ยังพัฒนาไม่เสร็จสมบูรณ์! จะมีเอกสารเพิ่มเติมตามมาหลังการพัฒนา!
คำสั่งเงื่อนไข
คำสั่งเงื่อนไขมีประโยชน์สำหรับควบคุมวิธีที่โปรแกรมทำงาน
ไวยากรณ์
Whamm รองรับตัวบ่งชี้ 3 แบบสำหรับคำสั่งเงื่อนไข:
บล็อก if/else ทั้งหมดต้องเริ่มต้นด้วยคำสั่ง if จากนั้นตามด้วยคำสั่ง elif จำนวนเท่าใดก็ได้ และคำสั่ง else อีก 0 หรือ 1 อัน สุดท้ายทั้งเชนต้องปิดด้วย ;
ไวยากรณ์ทางการ
“if” ~ “(” ~ expr ~ “)” ~ “{” ~ statement* ~ “}” ~ (else | elif) ? ~ “;”
โดย elif คือ: “elif” ~ “(” ~ expr ~ “)” ~ “{” ~ statement* ~ “}” ~ (else| elif) ?
และ else คือ: “else” ~ “{” ~ statement* ~ “}”
ตัวอย่างคำสั่งเงื่อนไข
var a: i32 = 5;
if (a == 5) {
a = 3;
};
var a: i32 = 5;
if (a == 4){
a = 3;
}elif (a == 3){
a = 2;
}elif (a == 5){
a = 1;
};
//This is an example in a function that returns a bool
my_fn (param: i32) -> bool {
return (param/10 == 0);
}
var a: i32 = 5;
if(my_fn(a)) {
a = 3;
}else{
a = 2;
};
การประเมินแบบลัดวงจร
นิพจน์เงื่อนไขจะประเมินเฉพาะกิ่งที่ตรงกับค่าของเงื่อนไขเท่านั้น พูดอีกอย่างคือ มันลัดวงจรนั่นเอง
ขอบเขตชนิด
ออปโคดของ Wasm บางตัว เช่น local.set มีอาร์กิวเมนต์แบบหลายรูปแบบ (polymorphic)
ผู้ใช้สามารถกำหนดเงื่อนไขเพิ่มเติมให้กับการแมตช์ตำแหน่งโพรบดังกล่าวตามชนิดได้ด้วยขอบเขตชนิด (type bound)
ในตอนนี้ วิธีนี้ยังช่วยเลี่ยงความจำเป็นในการทำ polymorphism ใน Whamm
โดยผู้ใช้ต้องสร้างโพรบแบบขอบเขตชนิดหนึ่งตัวต่อชนิดที่เป็นไปได้แต่ละชนิด
ตัวอย่างการใช้ขอบเขตชนิดมีดังนี้:
wasm:opcode:call(arg0: i32):before {
report unshared var all_arg0s: map<i32, i32>;
all_arg0s[arg0]++;
}
ตัวแปร report
คีย์เวิร์ด report ใช้ระบุตัวแปรที่ค่าของมันควรถูกระบาย (flush) ออกมา
เมื่อมอนิเตอร์การทำงานของแอปพลิเคชัน ข้อมูลที่บรรยายสิ่งที่สังเกตได้ควรถูกเก็บไว้ในตัวแปร report เพื่อใช้ประโยชน์จากฟีเจอร์การระบายนี้
การใช้ report จริงๆ แล้วเป็นคำย่อของ report unshared ดูเอกสารตัวแปร [unshared]ประกอบ
พฤติกรรมเริ่มต้นของ “การระบาย” นี้คือการพิมพ์ไปยังคอนโซล (ไลบรารีหลักของ Whamm ใช้ WASI ทำสิ่งนี้)
การระบายจะเกิดขึ้นตอนสิ้นสุดการทำงานของโปรแกรมด้วยค่าสุดท้ายของตัวแปร
ตัวอย่าง:
report var count;
wasm:opcode:call:before {
// count the number of times the `call` opcode was used during the application's dynamic execution.
// (a single global count)
count++;
}
ตัวแปร unshared
หากตัวแปรถูกทำเครื่องหมายเป็น unshared อินสแตนซ์ของตัวแปรนี้จะถูกสร้างให้ทุกจุดที่แมตช์ของโพรบ
สโคปของตัวแปรนี้จำกัดอยู่ที่จุดที่แมตช์นั้นๆ
ค่าของตัวแปรนี้จะคงที่ในทุกครั้งที่เข้าไปในตรรกะของโพรบ นั่นหมายความว่ามันจะไม่ถูกเริ่มต้นใหม่ทุกครั้ง
คุณจึงใช้ตัวแปรนี้เก็บข้อมูล ณ จุดหนึ่งของโปรแกรมเมื่อเวลาผ่านไปได้
ตัวอย่าง:
wasm:opcode:call:before {
// collect the number of times each `call` opcode is executed during dynamic execution.
// (as many counts as there are `call` opcodes in the program)
// This variable will not be reinitialized each time this probe's body is executed,
// rather, it will be the value it was the last time it ran!
unshared var count: i32;
count++;
// This variable will be reinitialized to 0 each time this probe's body is executed
var local_variable: i32;
local_variable++;
}
ตัวแปร shared
หมายเหตุ: ฟังก์ชันนี้ยังพัฒนาไม่เสร็จสมบูรณ์! จะมีเอกสารเพิ่มเติมตามมาหลังการพัฒนา!
หากตัวแปรถูกทำเครื่องหมายเป็น shared อินสแตนซ์เดียวของตัวแปรนี้จะถูกใช้ร่วมกันโดยทุกจุดที่แมตช์ของโพรบ
สโคปของตัวแปรนี้จำกัดอยู่ที่ทุกจุดที่แมตช์ของโพรบ
ค่าของตัวแปรนี้จะคงที่ในทุกครั้งที่เข้าไปในตรรกะของโพรบ นั่นหมายความว่ามันจะไม่ถูกเริ่มต้นใหม่ทุกครั้ง
คุณจึงใช้ตัวแปรนี้เก็บข้อมูลสำหรับอีเวนต์ที่โพรบเมื่อเวลาผ่านไปได้
ตัวอย่าง:
wasm:opcode:call:before {
// collect the number of times the `call` opcode is executed during dynamic execution.
// (a single count tied to the wasm:opcode:call event in the program)
// This variable will not be reinitialized each time this probe's body is executed,
// rather, it will be the value it was the last time it ran!
shared var count: i32;
count++;
// This variable will be reinitialized to 0 each time this probe's body is executed
var local_variable: i32;
local_variable++;
}
ตัวแปร frame
หมายเหตุ: ฟังก์ชันนี้ยังพัฒนาไม่เสร็จสมบูรณ์! จะมีเอกสารเพิ่มเติมตามมาหลังการพัฒนา!
หากตัวแปรถูกทำเครื่องหมายเป็น frame ตัวแปรเองจะถูกเก็บไว้บนเฟรมที่ทำงานอยู่ของฟังก์ชัน
ดังนั้นตัวแปรนี้จึงอยู่ในสโคปของทุกโพรบในเฟรมที่ทำงานอยู่นั้น
ค่าของตัวแปรนี้จะคงที่ตลอดการเข้าถึงของโพรบ ตราบใดที่เฟรมของฟังก์ชันนั้นยังทำงานอยู่
คุณจึงใช้ตัวแปรนี้เก็บข้อมูลสำหรับอีเวนต์ที่โพรบระหว่างการทำงานของฟังก์ชันหนึ่งๆได้
ตัวอย่าง:
// Tracks all of the runtimes of each function invocation across program execution.
use time;
wasm:func:entry {
frame var start: u32;
start = time.now();
}
wasm:func:exit {
frame var start: u32;
unshared var elapsed: vec<u32>;
elapsed.push(time.now() - start);
}
โพรบ
<probe_rule> / <predicate> / { <actions> }
เราใช้คำว่า probe (โพรบ) เพื่ออ้างถึงสามส่วนนี้ ได้แก่ probe_rule, predicate และ actions
เมื่อทำการเขียนไบต์โค้ดใหม่ Whamm จะทำงานดังนี้:
- ท่องไปตามโมดูล Wasm ของแอปพลิเคชันเพื่อค้นหาตำแหน่งที่สนใจตามที่ระบุไว้ใน
probe_ruleของแต่ละโพรบ - ตรวจสอบว่า
predicateประเมินเป็นfalseแบบสแตติกหรือไม่- ถ้าประเมินเป็น
falseก็จะเดินหน้าต่อ โดยไม่ฉีด actions ของโพรบ - ถ้าไม่ประเมินเป็น
falseก็จะฉีด actions ของโพรบที่ตำแหน่งนั้น พร้อมกับpredicateที่ถูกพับค่าแล้ว- ถ้า
predicateประเมินเป็นtrueแบบสแตติก ก็จะฉีด actions เข้าไปในโปรแกรมโดยไม่มีเงื่อนไข - ถ้า
predicateไม่พับเป็นค่าbooleanธรรมดา ก็จะฉีด actions ที่มีเงื่อนไข ณ ตำแหน่งนี้ จากนั้น predicate จะถูกประเมินแบบพลวัตเมื่อแอปพลิเคชันรัน เพื่อให้ actions ของโพรบทำงานตามเงื่อนไข
- ถ้า
- ถ้าประเมินเป็น
ตัวช่วย info ใน CLI
whamm info --help
คำสั่ง info ที่ CLI ให้มาเป็นแหล่งข้อมูลชั้นเยี่ยมสำหรับดูว่าอะไรใช้เป็นกฎการแมตช์ของโพรบได้บ้าง
คำสั่งนี้ให้เอกสารที่อธิบายส่วนประกอบของกฎการแมตช์ ตลอดจนตัวแปรและฟังก์ชันที่ผูกไว้ซึ่งอยู่ในสโคป ซึ่งช่วยให้ผู้ใช้เรียนรู้วิธีสร้าง instrumentation ของตนได้เป็นอย่างดี
กฎการแมตช์ของโพรบ
provider:package:event:mode
probe_rule เป็นวิธีแสดง “ตำแหน่ง” ที่คุณต้องการทำ instrumentation ในโปรแกรม
มันเป็นลำดับชั้นตาม provider, package, event และ mode
| ส่วน | คำอธิบาย |
|---|---|
| provider | ชื่อของ provider ที่รองรับความสามารถ instrumentation ที่ใช้ในโพรบนี้ |
| package | ชื่อของ package ภายใน provider ที่ระบุ ซึ่งรองรับความสามารถ instrumentation ที่ใช้ในโพรบนี้ |
| event | ชื่อของ event ที่สัมพันธ์กับตำแหน่งที่จะแทรกโพรบนี้ในโปรแกรมที่ทำ instrumentation |
| mode | ชื่อของ mode ที่ควรใช้เมื่อปล่อย actions ของโพรบ ณ ตำแหน่งของ event เช่น before, after และ alt |
แต่ละส่วนของ probe_rule จะเพิ่มความจำเพาะขึ้นเรื่อยๆ จนกระทั่งถึง mode ของโพรบ
ลองพิจารณากฎการแมตช์ตัวอย่าง: wasm:opcode:br_if:before
กฎนี้ตีความได้ว่า “แทรกโพรบนี้ก่อนแต่ละ ออปโคด br_if ของ Wasm ในโปรแกรมของฉัน”
อ่านเอกสารอีเวนต์ที่ทำ instrumentation ได้ของเราเพื่อดูสิ่งที่รองรับในปัจจุบันและเป้าหมายในอนาคต
เพรดิเคต
/ <predicate> /
predicate เป็นวิธีแสดง “เงื่อนไข” ที่คุณต้องการให้ประเมินเป็น true เพื่อให้ actions ของโพรบถูกทำ
มันช่วยจำกัดกฎการแมตช์ให้แคบลงอีกชั้น
ส่วนนี้ของโพรบเป็นทางเลือก
ถ้าโพรบใดไม่มี predicate actions จะถูกทำเสมอเมื่อถึงตำแหน่งของโพรบระหว่างการทำงานของโปรแกรม
การพับค่าคงที่ของข้อมูลสแตติก
โพรบสามารถมีเพรดิเคตบนข้อมูลทั้งสแตติกและพลวัตได้
เพื่อรองรับสิ่งนี้ เมื่อกำหนดเป้าหมายเป็นการเขียนไบต์โค้ดใหม่ คอมไพเลอร์ whamm จะทำการแพร่ค่าคงที่สำหรับข้อมูลที่นิยามแบบสแตติก
หมายความว่า เนื่องจากข้อมูลสแตติกรู้ได้ตั้งแต่ตอนคอมไพล์ไทม์ ค่าของตัวแปรเหล่านั้นจะถูกแทนที่ และนิพจน์สามารถประเมินบางส่วนได้
ตัวอย่างเช่น ลองพิจารณาโพรบต่อไปนี้:
wasm:opcode:br_if:before / pc == 25 && arg0 == 1 / {..}
อ่านได้ว่า แนบโพรบที่ออปโคด wasm br_if แต่รันตรรกะนี้เฉพาะเมื่อ pc เป็น 25 และ arg0 ประเมินเป็น 1
ขณะที่เราท่องไปตามแอปพลิเคชัน เรารู้แบบสแตติกว่าเรากำลังอยู่ที่ออฟเซต pc เท่ากับ 25 ภายในฟังก์ชัน
แต่เราไม่รู้ค่าของ arg0 จนกว่าโค้ดจะรันจริง!
นี่คือจุดที่การประเมินบางส่วนเข้ามามีบทบาท
สมมติว่าเราอยู่ที่ pc 0 ภายในฟังก์ชันหนึ่ง การแพร่ค่าคงที่และการประเมินบางส่วนจะมีลักษณะดังนี้:
0 == 25 && arg0 == 1
false && arg0 == 1
false
เพรดิเคตประเมินเป็น false แบบสแตติก! นั่นหมายความว่าเราไม่ควรแนบโพรบที่ตำแหน่งนี้ เพราะมันไม่แมตช์!
สมมติว่าเราอยู่ที่ pc 25 ภายในฟังก์ชันหนึ่ง:
25 == 25 && arg0 == 1
true && arg0 == 1
arg0 == 1
อืม … เรายังมีส่วนหนึ่งของเพรดิเคตเหลืออยู่! ควรทำอย่างไรดี … เรายังจำเป็นต้องมีเพรดิเคตบนส่วนนี้ของนิพจน์ เพียงแต่ต้องฉีดนิพจน์ที่ประเมินบางส่วนแล้วนี้ออกไปรันตอนรันไทม์! ดังนั้น เราจะฉีดเนื้อโพรบโดยห่อด้วยเงื่อนไขนี้!
โปรดทราบว่าสิ่งนี้มีลักษณะแตกต่างออกไปเล็กน้อยสำหรับเป้าหมาย wei เพราะ Whamm ต้องปรับแต่งเพรดิเคตเป็นอย่างอื่นตรงจุดนั้น ซึ่งช่วยให้เอนจินรันส่วนที่ถูกต้องของเพรดิเคตในเวลาที่ถูกต้อง (match time กับรันไทม์)
Actions
{ <actions> }
actions คือคำสั่งที่ถูกทำ ณ ตำแหน่งของ probe_rule หาก predicate ประเมินเป็น true
ลำดับการฉีดโพรบ
เมื่อมีโพรบหลายตัวแมตช์ตำแหน่งไบต์โค้ดเดียวกัน whamm รับประกันลำดับการฉีดภายใต้กฎสองข้อ:
กฎข้อ 1: หมวดหมู่โพรบเดียวกัน — ลำดับในสคริปต์ถูกคงไว้
โพรบสองตัวที่อยู่ในหมวดหมู่การฉีดเดียวกัน (เช่น เป็น opcode:call:before ทั้งคู่ หรือเป็น func:entry ทั้งคู่) จะถูกฉีดตามลำดับที่ปรากฏในสคริปต์เสมอ
// order0.mm — func:entry fires first, then the second func:entry
wasm:func:entry { /* A */ }
wasm:func:entry { /* B */ }
// at any function entry: A runs, then B runs
การรับประกันนี้ยังใช้ได้กับสคริปต์หลายไฟล์ที่ส่งให้คอมไพเลอร์ด้วย: โพรบจากสคริปต์แรกจะถูกฉีดก่อนโพรบจากสคริปต์ที่สอง
กฎข้อ 2: หมวดหมู่โพรบต่างกัน — ลำดับถูกกำหนดโดยเป้าหมายการฉีด
เมื่อโพรบสองตัวอยู่ต่างหมวดหมู่การฉีดกัน (เช่น wasm:opcode:call:before กับ wasm:func:entry) ลำดับสัมพัทธ์ ณ ตำแหน่งที่ใช้ร่วมกันไม่ถูกควบคุมด้วยลำดับในสคริปต์
เป้าหมายการเขียนไบต์โค้ดใหม่ (wirm) ใช้โมเดลการฉีดสองรอบ:
- รอบที่ 1 (ปกติ): โพรบ
before,afterและaltถูกฉีดระหว่างการท่องไบต์โค้ด - รอบที่ 2 (โหมดพิเศษ): โพรบเชิงโครงสร้างอย่าง
func:entry,func:exit,block:entry,block:exitและอื่นๆ ที่คล้ายกัน ถูกแก้ไขให้ชัดเจนและฉีดตอน encode
เนื่องจากสองรอบนี้เป็นอิสระต่อกัน โพรบ before จึงถูกฉีดก่อนโพรบ func:entry ณ ตำแหน่งไบต์โค้ดเดียวกัน (คำสั่งแรกของฟังก์ชัน) เสมอ ไม่ว่าโพรบใดจะปรากฏก่อนในสคริปต์
หากคุณต้องการลำดับที่เฉพาะเจาะจงระหว่างโพรบต่างหมวดหมู่ ให้ใส่ตรรกะทั้งหมดไว้ในโพรบเดียวที่อีเวนต์จำเพาะกว่า (เช่น opcode:call:before แทนที่จะผสม func:entry กับ opcode:call:before ที่ตำแหน่งเดียวกัน)
สคริปต์ Whamm
Instrumentation (หรือที่เรียกว่ามอนิเตอร์) ถูกแสดงเป็นชุดของโพรบที่มีเพรดิเคตในสคริปต์ที่ใช้นามสกุล .mm
นี่คือภาพรวมระดับสูงของไวยากรณ์สำหรับสคริปต์ Whamm:
// Statements to initialize the global state of the instrumentation
global_statements;
...
// Function definitions to reuse code snippets
fn_name(fn_args) -> ret_val { fn_body; ... }
...
// An example of what a `probe` would look.
// There can be many of these in a monitor.
provider:package:event:mode / predicate / {
probe_actions;
...
}
การทำ instrumentation ด้วย CLI
whamm instr --help
คำสั่ง instr ที่ CLI ให้มาเปิดทางให้นักพัฒนาทำ instrumentation กับโปรแกรมได้จริง
อีเวนต์ที่ทำ instrumentation ได้
packages ที่มีให้ใช้งานในปัจจุบัน:
wasm:opcode: สำหรับทำ instrumentation กับออปโคดของ Wasmwasm:func: สำหรับทำ instrumentation กับฟังก์ชันของ Wasm- ปัจจุบันรองรับ
entryและexit - อนาคต:
unwind
- ปัจจุบันรองรับ
wasm:block: สำหรับทำ instrumentation กับ basic block ของ Wasm
Packages ที่จะเพิ่มเติมในอนาคต:
- อีเวนต์การทำงานของ
thread - อีเวนต์การทำงานของ
gc - อีเวนต์การเข้าถึง
memory(อ่าน/เขียน) - อีเวนต์การเข้าถึง
table(อ่าน/เขียน) - อีเวนต์การทำงานของ
componentใน WASI เช่นwasi:http:send_req:alt - อีเวนต์
wasm:begin/wasm:end traps- อีเวนต์ throw/rethrow/catch ของ
exception
ไลบรารี
ไลบรารีใช้กำหนดพฤติกรรมของ instrumentation ในกรณีที่มันเกินขอบเขตของไวยากรณ์ DSL หลัก
อันที่จริง Whamm เองก็พึ่งพาไลบรารีหลักของ Whamm ที่ชื่อ whamm_core.wasm เพื่อรองรับการพิมพ์ข้อความและแมป
การบิลด์และใช้งานไลบรารีที่กำหนดเอง
หากต้องการบิลด์ไลบรารีที่กำหนดเอง มีขั้นตอนดังนี้:
- ออกแบบ API ให้โต้ตอบได้ด้วยชนิดพื้นฐานของ Wasm (เช่น i32, f32 ฯลฯ) โปรดทราบว่า Whamm ยังไม่รองรับไลบรารีที่คืนค่าหลายผลลัพธ์ในปัจจุบัน
- เขียนไลบรารีด้วยภาษาที่คอมไพล์เป็น Wasm
ตรวจสอบให้แน่ใจว่าฟังก์ชัน API ถูกส่งออกด้วยชื่อที่เข้ากันได้กับคำสงวนของ DSL
Whamm - คอมไพล์ไลบรารีเป็น Wasm
- ตรวจดูรายการฟังก์ชันที่ส่งออกของไลบรารีด้วย
wasm-tools
เมื่อคุณมีไบนารีของไลบรารีแล้ว ก็สามารถนำมันมาใช้ในสคริปต์ Whamm ได้
โดยนำเข้าไลบรารีเข้าสคริปต์ด้วยคีย์เวิร์ด use
จากนั้นจึงเรียกใช้งานไลบรารีได้ด้วยไวยากรณ์: lib_name.func_name()
ตัวอย่างสคริปต์ต่อไปนี้ใช้ไลบรารีของ Whamm (และยังหลบข้อจำกัดที่ Whamm ไม่รองรับการคืนค่าหลายผลลัพธ์ ด้วยการแพ็กบิตเข้าด้วยกัน):
// import the library that simulates a cache
use cache;
// instrument all load and store opcodes
wasm:opcode:*load*|*store*:before {
report unshared var hit: u32;
report unshared var miss: u32;
// call the library `check_access` function and pass bound variables as parameters
var result: i32 = cache.check_access(effective_addr as i32, data_size as i32);
var num_hits: i32 = (result & 0xFFFF0000) >> 16;
var num_misses: i32 = (result & 0x0000FFFF);
hit = hit + (num_hits as u32);
miss = miss + (num_misses as u32);
}
การใช้ไลบรารีตอน Init Time
ไลบรารีบางตัวอาจจำเป็นต้องเริ่มต้นด้วยสถานะบางอย่างตอนเริ่มการทำงานของโปรแกรม
ซึ่งทำได้ง่ายๆ เพียงเพิ่มแอนโนเทชัน @init ให้กับการเรียกใช้ฟังก์ชันนั้น
หากการเรียกไลบรารีถูกติดแท็กเป็น @init ในสโคปภายในโพรบ การเรียกนั้นรับประกันว่าจะถูกทำตอน initialization ของโพรบ (เวลาที่แน่นอนขึ้นอยู่กับเป้าหมายของแบ็กเอนด์ว่าจะเป็น wei หรือ rewriting)
ในขณะที่การเรียกไลบรารีในสโคปโกลบอลของสคริปต์รับประกันเสมอว่าจะถูกทำในฟังก์ชัน start ของโปรแกรม ถึงอย่างนั้นผู้ใช้ก็ยังติดแท็ก @init ให้มันได้ เพื่อแสดงการรับประกันนี้ในสคริปต์ให้ชัดเจนยิ่งขึ้น
use toggle;
// both of the following will run in a program's start function
@init toggle.get_value();
toggle.get_value();
wasm:opcode:*:before {
// will run at probe initialization time
// rewriting: in the program's start function
// wei: in the probe's initialization function callback
@init toggle.get_value();
}
การใช้ไลบรารีตอน Match Time
ไลบรารียังใช้ขยายความสามารถด้าน match-time ของ DSL Whamm ได้อีกด้วย
เพียงเพิ่มแอนโนเทชัน @static ให้กับการเรียกใช้ฟังก์ชันนั้น
หมายเหตุ: การเรียกเหล่านี้ต้องไม่มีผลข้างเคียงที่จำเป็นต้องคงอยู่ไปจนถึงการทำงานเชิงพลวัต
ผลข้างเคียงดังกล่าวจะยังคงอยู่บน wei แต่ในการเขียนไบต์โค้ดใหม่ สถานะเหล่านั้นจะหายไป
เราวางแผนจะรองรับสถานะแบบคงทนสำหรับ rewriting ในอนาคต ดูรายละเอียดได้ที่: ideas/@static-serialize.md
ทำไมสิ่งนี้ถึงมีประโยชน์?
สมมติว่าคุณต้องการทำ instrumentation กับ i32.load ที่ผลลัพธ์สุดท้ายถูกนำไปใช้โดย br_table ตัวใดตัวหนึ่ง
คุณไม่สามารถแสดงเงื่อนไขเช่นนั้นด้วยความสามารถของกฎการแมตช์ใน Whamm ปัจจุบันได้
อันที่จริง สถานการณ์เฉพาะแบบนี้ต้องใช้ abstract interpretation เข้ามาช่วย!
เพื่อผลักความซับซ้อนเช่นนี้ออกไป การตัดสินใจตอน match-time จึงสามารถใช้ไลบรารีช่วยค้นหาจุดดังกล่าวได้
ยิ่งไปกว่านั้น ในการเขียนไบต์โค้ดใหม่ ไลบรารีเหล่านี้ยังใช้ดึงค่าคงที่และลดปริมาณโค้ดที่ปล่อยออกมาผ่านการแพร่ค่าคงที่ได้
การแพร่ค่าคงที่นี้ลดทอนสวิตช์ if/else ได้ด้วยซ้ำ!
ลองพิจารณาสคริปต์ตัวอย่างต่อไปนี้ ซึ่งใช้สำหรับ instrumentation แบบแก๊ส:
use gas;
use analysis;
TINIT = 0;
TFILL = 0;
TCOPY = 0;
MINIT = 0;
MFILL = 0;
MCOPY = 0;
// Probes for GAS usage
wasm:opcode:*:before / @static analysis.should_inject(fid, pc) && ! @static analysis.linear_cost_at(fid, pc) == 0 / {
var constant_cost: i32 = @static analysis.constant_cost_at(fid, pc);
switch (@static analysis.instr_kind(fid, pc)) {
case CONST => gas.decr_const(constant_cost);
default => unreachable();
}
}
wasm:opcode:*(arg0: i32):before / @static analysis.should_inject(fid, pc) && @static analysis.linear_cost_at(fid, pc) > 0 / {
var linear_cost: i32 = @static analysis.linear_cost_at(fid, pc);
var constant_cost: i32 = @static analysis.constant_cost_at(fid, pc);
switch (@static analysis.instr_kind(fid, pc)) {
case TINIT => gas.finite_wasm_table_init(arg0, linear_cost, constant_cost);
case TFILL => gas.finite_wasm_table_fill(arg0, linear_cost, constant_cost);
case TCOPY => gas.finite_wasm_table_copy(arg0, linear_cost, constant_cost);
case MINIT => gas.finite_wasm_memory_init(arg0, linear_cost, constant_cost);
case MFILL => gas.finite_wasm_memory_fill(arg0, linear_cost, constant_cost);
case MCOPY => gas.finite_wasm_memory_copy(arg0, linear_cost, constant_cost);
default => unreachable();
}
}
สิ่งนี้ทำงานอย่างไร?
ในการเขียนไบต์โค้ดใหม่ เราฝัง wasmtime ไว้เพื่อเรียกไลบรารีดังกล่าว ขณะที่แบ็กเอนด์ท่องไปตามไบต์โค้ดของแอปพลิเคชันเป้าหมาย
ทุกโมดูลที่ถูกเรียกแบบสแตติกจะถูกสร้างอินสแตนซ์ขึ้น โดยตัวอินสแตนซ์และสถานะถูกเก็บไว้ใน WasmRegistry
รีจิสทรีนี้ถูกนิยามไว้ใน src/lang_features/libraries/registry.rs
ใน wei การเรียกเหล่านี้จะถูกทำตอน match time ของเอนจินโดยตรง
ซึ่งรับประกันว่าสถานะใดก็ตามที่ควรคงอยู่ระหว่าง match time กับรันไทม์จะยังอยู่ครบ
นอกจากนี้ยังกำหนดให้การรันโมดูลมอนิเตอร์ wei ที่สร้างจากสคริปต์ซึ่งมีการเรียก @static ไปยังไลบรารี ต้องถูกเชื่อมโยงตอนรันไทม์
ข้อกำหนดนี้ไม่มีสำหรับการเขียนไบต์โค้ดใหม่ เพราะการเรียก @static เหล่านั้นถูกทำไปแล้ว (จึงไม่ต้องนำเข้าไลบรารีนั้นใน Wasm)
การทดสอบ Instrumentation ของคุณ
หมายเหตุ: ฟังก์ชันนี้ยังพัฒนาไม่เสร็จสมบูรณ์! จะมีเอกสารเพิ่มเติมตามมาหลังการพัฒนา! นี่คือเป้าหมายการวิจัยในอนาคตของเรา
กลยุทธ์การอินเจกต์
การดีบักและโปรไฟล์โปรแกรมเป็นส่วนสำคัญของการวิศวกรรมซอฟต์แวร์ โดยทั่วไปทำได้ผ่านการทำ instrumentation กับโปรแกรมที่กำลังสังเกตอยู่ (กล่าวคือ แทรกคำสั่งที่ให้ข้อมูลเชิงลึกเกี่ยวกับการทำงานเชิงพลวัตเข้าไป)
เทคนิค instrumentation ที่พบมากที่สุด เช่น การเขียนไบต์โค้ดใหม่ จะฉีดคำสั่งเข้าไปในโค้ดของแอปพลิเคชันโดยตรง แม้ว่าวิธีนี้จะทำให้ instrumentation รองรับโดเมนของแอปพลิเคชันได้แทบทุกประเภท แต่มันรุกล้ำพื้นที่สถานะของโปรแกรม (ซึ่งอาจนำมาซึ่งบั๊ก) ทำให้การนำไปใช้ซับซ้อนขึ้น จำกัดขอบเขตการสังเกต และไม่สามารถปรับตัวตามพฤติกรรมของโปรแกรมได้แบบพลวัต
อีกทางหนึ่ง เราสามารถแก้ปัญหาเหล่านี้ของการเขียนไบต์โค้ดใหม่ได้ด้วยการเชื่อมต่อกับเอนจินรันไทม์ที่รองรับ instrumentation โดยตรง เทคนิคนี้เปิดโอกาสให้นำความสามารถอันทรงพลังมาใช้ได้ ดังที่เอนจินวิจัยอย่าง Wizard สาธิตให้เห็นในเปเปอร์ ASPLOS เรื่อง Flexible Non-intrusive Dynamic Instrumentation for WebAssembly เปเปอร์นี้อธิบายวิธีสร้างการรองรับ instrumentation ที่ปกป้องแอปพลิเคชันที่กำลังสังเกต ให้การรับประกันความสอดคล้องเพื่อให้เครื่องมือประกอบกันได้ ใช้การปรับให้เหมาะที่สุดของ JIT ที่จำเพาะกับ instrumentation ซึ่งทำให้เครื่องมือบางตัวรันได้เร็วยิ่งกว่าการเขียนไบต์โค้ดใหม่เสียอีก และอื่นๆ อีกมาก อย่างไรก็ตาม เทคนิคนี้ยังไม่เป็นที่นิยมเท่าการเขียนไบต์โค้ดใหม่ เพราะจำกัดขอบเขตของเครื่องมือไว้เฉพาะแอปพลิเคชันที่รันบนเอนจินเหล่านี้ได้เท่านั้น
นี่คือจุดที่ Whamm เข้ามามีบทบาท
DSL นี้เป็นนามธรรมที่อยู่เหนือเทคนิค instrumentation ทำให้เครื่องมือของนักพัฒนารองรับแอปพลิเคชันได้หลากหลายโดเมน พร้อมใช้ความสามารถของรันไทม์เท่าที่มี โดยไม่ต้องนำกลับมาเขียนใหม่
ด้วย Whamm คุณเขียน instrumentation ครั้งเดียวและรองรับแอปได้หลากหลายโดเมน
ใช้ความสามารถ instrumentation ของเอนจินเท่าที่มี
ใช้การเขียนไบต์โค้ดใหม่เพื่อรองรับทุกอย่างที่เหลือ
การเขียนไบต์โค้ดใหม่
เมื่อใช้กลยุทธ์นี้ ต้องจัดหาไบต์โค้ดของแอปพลิเคชันเป้าหมายให้คอมไพเลอร์ Whamm ด้วย
เพื่อทำกลยุทธ์การอินเจกต์แบบเขียนไบต์โค้ดใหม่ Whamm ใช้ไลบรารี Rust ชื่อ wirm
ไลบรารีนี้โหลดโมดูล app.wasm เข้าเป็นตัวแทน AST ซึ่งสามารถท่องไปและปรับแต่งได้ เพื่อฉีด instrumentation เข้าไปในไบต์โค้ดของแอปพลิเคชันโดยตรง
อ่านรายละเอียดระดับต่ำเพิ่มเติมได้ในเอกสารสำหรับนักพัฒนา
แอปพลิเคชันที่ทำ instrumentation แล้วจะถูกส่งออกเป็นโมดูล Wasm ใหม่ ดังที่แสดงด้านบน (app.out.wasm)
การรองรับเอนจินโดยตรง (wei)##
ผู้ใช้ยังสามารถกำหนดเป้าหมายเป็นเอนจินที่รองรับ instrumentation โดยตรงผ่านส่วนต่อประสานที่ Whamm ใช้ได้
ปัจจุบันมีเอนจิน Wasm เพียงตัวเดียวที่ทำเช่นนี้ คือ Wizard
คุณอ่านเกี่ยวกับความสามารถด้าน instrumentation ของ Wizard ได้ในเปเปอร์ ASPLOS: Flexible Non-intrusive Dynamic Instrumentation for WebAssembly
คอมไพเลอร์ Whamm สร้างโมดูล Wasm ที่เข้ารหัส instrumentation ไว้ในแบบทั่วไป โดยไม่ผูกติดกับไบต์โค้ดของแอปพลิเคชันโดยตรง
หากสนใจ อ่านเอกสารเป้าหมายเอนจินของนักพัฒนาเพื่อดูรายละเอียดว่ามันทำงานอย่างไร
โปรดทราบว่า เมื่อใช้กลยุทธ์นี้ ไบต์โค้ดของแอปพลิเคชันเป้าหมายจะไม่ถูกจัดหาให้คอมไพเลอร์ Whamm
เนื่องจากเอนจินจะเป็นผู้ค้นหาจุดฉีด instrumentation ที่เหมาะสมในแอปพลิเคชัน และแนบคอลแบ็กที่เหมาะสมตอนรันไทม์ของแอปพลิเคชัน
ด้วยเหตุนี้ app.wasm จึงถูกจัดหาให้เอนจินตอนรันไทม์ในฐานะ “co-module” (โมดูล Wasm ที่เชื่อมต่อโดยตรงกับ instr.wasm ที่สนับสนุนมัน)
ตัวอย่าง
ต่อไปนี้คือตัวอย่างโพรบที่เขียนด้วยภาษานี้ พร้อมคำอธิบายประกอบ
Branch Monitor
ต่อไปนี้คือตัวอย่างมอนิเตอร์ที่เขียนด้วย Whamm โดยไม่จำเป็นต้องใช้ไลบรารี instrumentation เลย
แต่ใช้ DSL แสดงตรรกะการมอนิเตอร์ทั้งหมดแทน
// Matches _if and br_if events
wasm::*if:before {
report unshared var taken: i32;
report unshared var total: i32;
// which branch was taken?
var was_taken: bool = arg0 != 0;
taken = taken + (was_taken as i32);
total++;
}
wasm::br_table:before {
report unshared var taken_branches: map<u32, u32>;
// which branch was taken?
// default branch is at 'num_targets' in the map
var index: u32 = arg0 <= (num_targets - 1) ? arg0 : num_targets;
// count stores an array of counters
taken_branches[index]++;
}
wasm::select(arg0: i32):before {
report unshared var selected_first: u32;
report unshared var total: u32;
// which branch was taken?
var was_taken: bool = arg0 != 0;
selected_first = selected_first + (was_taken as u32);
total++;
}
Cache Simulator
ต่อไปนี้คือตัวอย่างมอนิเตอร์ที่เขียนด้วย Whamm โดยเรียกใช้การนำแคชไปใช้ (cache implementation) ที่จัดหาไว้ เพื่อจำลองการค้นหาแคชบนการดำเนินการกับหน่วยความจำ
// CacheSim: instruments memory accesses and simulates a cache attached to the memory.
use cache;
wasm:opcode:*load*|*store*:before {
report var hit: u32;
report var miss: u32;
var result: i32 = cache.check_access(effective_addr as i32, data_size as i32);
var num_hits: i32 = (result & 0xFFFF0000) >> 16;
var num_misses: i32 = (result & 0x0000FFFF);
hit = hit + (num_hits as u32);
miss = miss + (num_misses as u32);
}
Call Graph
ต่อไปนี้คือตัวอย่างมอนิเตอร์ที่เขียนด้วย Whamm โดยสร้างกราฟการเรียก (call graph) ของทั้งการเรียกโดยตรงและโดยอ้อม
use whamm_core;
// (from, to) -> count
report var call_graph: map<(u32, u32), u32>;
var tracking_target: bool;
var caller: u32;
wasm:opcode:call|return_call:before {
call_graph[(fid, imm0)]++;
}
wasm:opcode:*call_indirect|*call_ref:before {
tracking_target = true;
caller = fid;
}
wasm:func:entry {
if (tracking_target) {
call_graph[(caller, fid)]++;
tracking_target = false;
}
}
wasm:report {
// this is the ID of the map above...need to make this less hard-coded.
// maybe: `call_graph.id()`
whamm_core.print_map_as_csv(0);
}
สำหรับนักพัฒนา Whamm
คุณอยากมีส่วนร่วมพัฒนา Whamm หรือแค่อยากศึกษารายละเอียดระดับต่ำเพื่อความสนุก?
ถ้าอย่างนั้นคุณมาถูกที่แล้ว
แหล่งข้อมูล
การแยกวิเคราะห์:
แนวคิดการนำ Whamm ไปใช้
- แยกวิเคราะห์
- ตรวจสอบ
- แปลง AST ให้เป็นตัวแทนของกลยุทธ์การอินเจกต์
- ปล่อยโค้ด
แนวคิดอื่นๆ ที่เป็นประโยชน์:
- CLI ของ
Whamm - การทดสอบ
- การจัดการข้อผิดพลาด
สี่เฟสของการคอมไพล์
ก่อนอื่น ความหมายของคำว่า “การคอมไพล์” ขึ้นอยู่กับกลยุทธ์การอินเจกต์ที่เลือกไว้
สำหรับการเขียนไบต์โค้ดใหม่ การคอมไพล์หมายถึงการสร้างไบต์โค้ดของแอปพลิเคชันรูปแบบใหม่ที่ทำ instrumentation แล้ว
สำหรับการรองรับเอนจินโดยตรง การคอมไพล์หมายถึงการคอมไพล์สคริปต์ .mm ให้เป็นโมดูล Wasm ใหม่ที่เชื่อมต่อกับเอนจินเพื่อทำ instrumentation กับโปรแกรมแบบพลวัต
เมื่อใช้กลยุทธ์นี้ โปรแกรมต้นฉบับไม่ถูกแตะต้อง และ ไม่ถูกจัดหาให้กับคอมไพเลอร์
สามเฟสแรกของการคอมไพล์ Whamm เหมือนกันทั้งสองกลยุทธ์
ส่วนเฟส translate และ emit จะแตกต่างกันไปตามกลยุทธ์การอินเจกต์
ทั้งนี้เพราะ “การปล่อยโค้ด” สำหรับการเขียนไบต์โค้ดใหม่หมายถึงการใช้ไลบรารี wirm เพื่อแทรกคำสั่งใหม่เข้าไปในโปรแกรม
ในขณะที่ “การปล่อยโค้ด” สำหรับการรองรับเอนจินโดยตรงหมายถึงการปล่อยโมดูล Wasm ที่เข้ารหัสว่าจะทำ instrumentation ที่ไหนและคอลแบ็กที่จะแนบ ณ จุดที่โพรบ โดยเชื่อมต่อกับเอนจินตอนรันไทม์ของแอปพลิเคชัน
สี่เฟสของการคอมไพล์มีดังนี้:
- แยกวิเคราะห์
- ตรวจสอบ
- แปลง AST ให้เป็นตัวแทนของกลยุทธ์การอินเจกต์
- ปล่อยโค้ด
เฟสที่ 1: การแยกวิเคราะห์
นี่คือเอกสารที่อธิบายวิธีแยกวิเคราะห์สคริปต์ .mm
ไวยากรณ์
ไวยากรณ์ของ Whamm เขียนขึ้นด้วยไลบรารี Rust ชื่อ Pest ซึ่งเป็นตัวสร้าง parser ที่ใช้ Parsing Expression Grammars (PEG) เป็นอินพุต
การอ่านหนังสือ Pest ก่อนจะช่วยให้คุณเข้าใจวิธีอ่านไวยากรณ์ใน whamm.pest
Pest ทำการแยกวิเคราะห์สคริปต์ .mm ที่ส่งเข้ามา และสร้างชุด Rule ที่แมตช์ได้ ซึ่งจากนั้นจะถูกท่องไปใน whamm_parser.rs เพื่อสร้าง Abstract Syntax Tree (AST) ของ Whamm
ชื่อของ Rule เหล่านี้สอดคล้องกับชื่อที่ใช้ในไวยากรณ์ whamm.pest
คุณติดตามตรรกะการสร้าง AST จาก Pest Rule ได้โดยเริ่มจากจุดเข้าของการแยกวิเคราะห์ นั่นคือฟังก์ชัน parse_script ที่อยู่ในไฟล์ whamm_parser.rs
Abstract Syntax Tree (AST)
เราใช้ AST เป็นตัวแทนของสคริปต์ .mm หลังการแยกวิเคราะห์
AST นี้ถูกนำไปใช้ต่างกันในแต่ละเฟสของคอมไพเลอร์ที่ตามมา
ระหว่าง การตรวจสอบ AST ถูกใช้สร้าง SymbolTable และทำการตรวจชนิด
ขณะ แปลง AST ให้เป็นตัวแทนของกลยุทธ์การอินเจกต์ AST จะถูกเยี่ยมชมและจัดโครงสร้างใหม่ให้คอมไพล์ได้ง่ายขึ้นสำหรับแต่ละกลยุทธ์ แต่ละโหนดจะมีข้อมูลใหม่ที่จำเพาะต่อกลยุทธ์ ซึ่งมีประโยชน์ระหว่างการปล่อยโค้ด
ขณะ ปล่อยโค้ด จะใช้รูปแบบ AST ที่เรียบง่ายกว่าที่กล่าวถึงข้างต้น เพื่อค้นหาคำสั่งโกลบอลและวนซ้ำนิยามของโพรบเพื่อฉีดเข้าไปยังตำแหน่งที่สนใจในโปรแกรม Wasm
เฟสที่ 2: การตรวจสอบ
นี่คือเอกสารที่อธิบายวิธีตรวจสอบสคริปต์ .mm
SymbolTable
ระหว่างการตรวจสอบ ขั้นแรก SymbolTable จะถูกสร้างขึ้นจาก AST
มีแหล่งข้อมูลออนไลน์ที่ดีมากมายที่สอนเรื่องตารางสัญลักษณ์ หากจะเป็นประโยชน์กับผู้อ่าน
ในระดับสูง SymbolTable เก็บเมตาดาต้าเกี่ยวกับสัญลักษณ์ของซอร์สโค้ด
สำหรับสคริปต์ .mm สัญลักษณ์อาจเป็นส่วนของกฎการแมตช์ของโพรบ (เช่น provider, package, event และ mode) ชื่อฟังก์ชัน และตัวแปร (โลคัลหรือโกลบอล)
enum ชื่อ Record ในไฟล์ verifier/types.rs นิยามสัญลักษณ์เหล่านี้และเมตาดาต้าที่สนใจของแต่ละตัว
เมตาดาต้าที่สนใจมักเป็นข้อมูลชนิดและการระบุที่อยู่ ซึ่งสอดคล้องกับ ID ที่กำหนดให้สัญลักษณ์หลังถูกปล่อยเข้าไปในโปรแกรม Wasm
ข้อมูลชนิดถูกใช้ทั้งตอนตรวจชนิดสคริปต์และตอนปล่อย instrumentation (เพื่อให้รู้ชนิดของแต่ละรายการที่ปล่อยออกมา)
ตัวอย่างหนึ่งของการใช้เมตาดาต้าการระบุที่อยู่ คือการปล่อยและการเรียกฟังก์ชัน
หลังจากฟังก์ชันที่นิยามโดย instrumentation ถูกฉีดเข้าไปแล้ว ID ของฟังก์ชันนี้ต้องถูกเก็บไว้ใน SymbolTable เพื่อค้นหาและนำไปใช้เมื่อมีการปล่อยการเรียกฟังก์ชันในอนาคต (ดูเอกสาร InitGenerator)
สัญลักษณ์เหล่านี้อยู่ภายในสโคปบางอย่าง
ชนิดของสโคปที่ปรากฏในสคริปต์ .mm หาได้ใน enum ชื่อ ScopeType ในไฟล์ verifier/types.rs
แนวคิดเรื่องสโคปในบริบทนี้เหมือนกับภาษาโปรแกรมอื่นๆ
สโคปนิยามว่าตัวแปรและฟังก์ชันเข้าถึงได้ที่ไหนตามตำแหน่งในโปรแกรม
ในบริบทของ Whamm มีสโคปบางอย่างที่ดำรงอยู่แต่ผู้ใช้ปลายทางเข้าถึงไม่ได้
ลองพิจารณากฎการแมตช์ของโพรบ: provider:package:event:mode
แต่ละส่วนของกฎการแมตช์นี้มีสโคปของตัวเองจริงๆ
สิ่งนี้ทำให้แต่ละส่วนแนะนำตัวแปรและฟังก์ชันที่ผูกไว้ซึ่งเป็นประโยชน์ของตัวเอง เพื่อให้ผู้ใช้เขียน instrumentation ที่แสดงออกได้หลากหลายขึ้น!
ตัวแปรและฟังก์ชันที่ผูกไว้เหล่านี้ถูกเพิ่มเข้า AST ในไฟล์ whamm_parser.rs โดยเรียกฟังก์ชัน get_matches ของ provider_handler.rs
ดูเอกสารไวยากรณ์โพรบสำหรับเครื่องมือ CLI ที่เป็นประโยชน์ ซึ่งช่วยให้ผู้ใช้เห็นว่าอะไรอยู่ในสโคปของกฎการแมตช์ของโพรบใดๆ
ดูเอกสารการขยาย providerสำหรับวิธีขยาย provider และฟังก์ชัน/ตัวแปรที่ผูกไว้
ปัญหา / ทางแก้ชั่วคราว
1. ความเป็นเจ้าของของ Record และ Scope
ตอนเขียนโครงสร้าง SymbolTable มีปัญหาเกี่ยวกับการผลักความเป็นเจ้าของของ Record และ Scope ลงไปในแต่ละพาเรนต์
ทางแก้ชั่วคราวคือถือ Vec ของ Record และ Scope ทั้งหมด ของทั้งโปรแกรมไว้ใน struct SymbolTable แล้วถือชนิด usize ที่ชี้เข้าไปใน Vec เหล่านี้ใน Record และ Scope
เป็นไปได้ว่าปัญหานี้เลี่ยงได้ด้วยการ box ค่าใน Record และ Scope แต่ต้องทดลองเพิ่มเติม
การสร้าง SymbolTable
ไฟล์ builder_visitor.rs สร้าง SymbolTable จาก AST ของสคริปต์
ฟังก์ชัน visit_whamm เป็นจุดเข้าของพฤติกรรมนี้
ซึ่งทำตามดีไซน์แพตเทิร์น visitor
มีแหล่งข้อมูลออนไลน์ที่ดีมากมายที่สอนเรื่อง visitor pattern หากจะเป็นประโยชน์กับผู้อ่าน
TypeChecker
จากนั้นตัวตรวจชนิดจะท่อง AST และใช้ SymbolTable เพื่อตรวจสอบว่าการใช้ตัวแปรเหมาะสมหรือไม่
มันค้นหาได้ทั้งการใช้งานนอกสโคป การเรียกเมธอดที่ไม่ถูกต้อง การใช้ชนิดผิด ฯลฯ
เฟสที่ 3: การแปลง AST
ขั้นตอนนี้เริ่มจากการเก็บเมตาดาต้าเกี่ยวกับแต่ละโพรบผ่านการท่อง AST ด้วย MetadataCollector
ตัวเก็บนี้ติดตามข้อมูลต่อไปนี้ (รายการนี้อาจขยายเพิ่มในอนาคต):
- ฟังก์ชันไลบรารีของผู้ใช้ที่ถูกเรียกใช้จริงในสคริปต์
- ฟังก์ชันที่ผูกไว้ (bound functions) ที่ถูกเรียกใช้จริงในสคริปต์
- ชนิดข้อมูลแต่ละชนิดที่ต้องถูกระบายตอน report
- สตริงที่จำเป็นต้องมีเพื่อให้สคริปต์ทำงานได้
- ว่าโพรบใดมีสถานะที่ต้องเริ่มต้นหรือไม่
การติดตามสถานะทั้งหมดนี้ช่วยให้ปล่อยโค้ดได้ดีขึ้นในแบ็กเอนด์ของคอมไพเลอร์
อีกทั้งยังสรุปตัวแปรที่ผูกแบบสแตติกและแบบพลวัตของแต่ละโพรบให้ด้วย
รายการนี้ถูกใช้ในการเขียนไบต์โค้ดใหม่เพื่อนิยามข้อมูลสแตติกที่จำเป็นตอนคอมไพล์ไทม์ และเพื่อปล่อยโค้ดที่จำเป็นสำหรับนิยามข้อมูลพลวัตตอนรันไทม์
สำหรับแบ็กเอนด์ wei ตัวแปรที่ผูกแต่ละตัวจะถูกขอจากเอนจิน เพื่อให้เอนจินส่งค่าของมันให้ฟังก์ชันคอลแบ็กของโพรบหรือเพรดิเคต
เมื่อทำการเขียนไบต์โค้ดใหม่ AST ที่เป็นผลจากขั้นตอนเก็บเมตาดาต้าจะถูกแปลงให้เป็น SimpleAst
เฟสที่ 5: การปล่อยโค้ด
นี่คือเอกสารที่อธิบายวิธีปล่อยโค้ดสคริปต์ .mm
แนวคิดที่เป็นประโยชน์
generator คืออะไร?
generator ใช้ท่องตัวแทนของตรรกะบางอย่างในเชิงนามธรรม
จากนั้นจึงเรียก emitter เมื่อเหมาะสม เพื่อปล่อยโค้ดในตัวแทนเป้าหมายจริง
emitter คืออะไร?
emitter เปิด API ที่สามารถเรียกได้เพื่อปล่อยโค้ดในตัวแทนเป้าหมาย
โดยจะมี emitter มากเท่ากับจำนวนตัวแทนเป้าหมายที่ภาษารองรับ
กลยุทธ์การอินเจกต์
โค้ดที่ถูกปล่อย และวิธีการปล่อย ขึ้นอยู่กับกลยุทธ์การอินเจกต์ที่ผู้ใช้ระบุ
ปัจจุบันมีกลยุทธ์การอินเจกต์ที่รองรับสองแบบ:
- การเขียนไบต์โค้ดใหม่
- การเชื่อมต่อกับเอนจิน
การเขียนไบต์โค้ดใหม่
สำหรับการเขียนไบต์โค้ดใหม่ มี generator สองตัว
generator แต่ละตัวถูกใช้ด้วยเหตุผลเฉพาะขณะปล่อย instrumentation
InitGenerator จะถูกรันเป็นตัวแรก เพื่อปล่อยส่วนของสคริปต์ .mm ที่ต้องดำรงอยู่ก่อน actions ของโพรบใดๆ จะถูกปล่อย เช่น ฟังก์ชันและสถานะโกลบอล
จากนั้น InstrGenerator จะถูกรันเป็นตัวที่สอง เพื่อปล่อยโพรบขณะท่องไบต์โค้ด app.wasm (ซึ่งแทนด้วย IR ในหน่วยความจำ)
generator ทั้งสองใช้ emitter ที่ปล่อยโค้ด Wasm
โดย emitter ใช้ยูทิลิตี้ที่รวมตรรกะการปล่อย Wasm ไว้ที่ utils.rs
1. InitGenerator
init_generator.rs ท่อง AST เพื่อปล่อยฟังก์ชันและตัวแปรที่ต้องดำรงอยู่ก่อนปล่อยโพรบ
ฟังก์ชัน run เป็นจุดเข้าของ generator นี้
ซึ่งทำตามดีไซน์แพตเทิร์น visitor
มีแหล่งข้อมูลออนไลน์ที่ดีมากมายที่สอนเรื่อง visitor pattern หากจะเป็นประโยชน์กับผู้อ่าน
generator นี้ปล่อยฟังก์ชันและตัวแปร Wasm ใหม่เข้าไปในโปรแกรม พร้อม ID ของ Wasm ที่เกี่ยวข้อง
ID เหล่านี้ถูกเก็บไว้ใน SymbolTable เพื่อใช้ขณะรัน InstrGenerator
เมื่อปล่อยคำสั่งที่เรียกฟังก์ชันที่ปล่อยแล้ว หรือทำการดำเนินการบางอย่างกับโกลบอลที่ปล่อยแล้ว ชื่อของสัญลักษณ์นั้นจะถูกค้นหาใน SymbolTable เพื่อนำ ID ที่บันทึกไว้มาใช้ในคำสั่งที่ถูกปล่อย
2. InstrGenerator
instr_generator.rs เรียกเข้า emitter เพื่อท่องแอปพลิเคชันทีละขั้นเพื่อค้นหาตำแหน่งที่สอดคล้องกับอีเวนต์ของโพรบใน AST ของ .mm
เมื่อพบตำแหน่งที่ถูกโพรบ generator จะปล่อยโค้ด Wasm เข้าไปในแอปพลิเคชัน ณ จุดนั้นผ่านยูทิลิตี้ของ emitter
ลำดับการฉีดโพรบ
เมื่อมีโพรบหลายตัวแมตช์ตำแหน่งไบต์โค้ดเดียวกัน ลำดับการฉีดถูกกำหนดโดยปัจจัยอิสระสองอย่าง
หมวดหมู่การฉีดเดียวกัน: ลำดับในสคริปต์
โพรบที่อยู่ในหมวดหมู่การฉีดเดียวกัน (เช่น โพรบ opcode:call:before สองตัว หรือโพรบ func:entry สองตัว) จะถูกฉีดตามลำดับที่ปรากฏในสคริปต์
สิ่งนี้ถูกรับประกันโดยใช้ IndexMap แทน HashMap ในทุกระดับของลำดับชั้นโพรบ — Provider.packages, Package.events, Event.probes ใน provider_handler.rs และ SimpleProv.pkgs, SimplePkg.evts, SimpleEvt.modes ที่สอดคล้องกันใน simple_ast.rs
IndexMap รักษาลำดับการแทรก ซึ่งเท่ากับลำดับที่โพรบถูกแยกวิเคราะห์จากสคริปต์
หากใช้ HashMap ธรรมดาแทน ลำดับการวนซ้ำจะไม่แน่นอน ทำให้การรับประกันลำดับนี้เสียไปอย่างเงียบๆ
หมวดหมู่การฉีดต่างกัน: โมเดลสองรอบของ wirm
wirm (ไลบรารี Wasm IR ที่อยู่เบื้องหลัง) ใช้โมเดลการฉีดสองรอบ:
- รอบที่ 1 — โหมดปกติ (
before,after,alt): ถูกฉีดระหว่างการท่องไบต์โค้ด ด้วยการเรียกapp_iter.before()/.after()/.alternate() - รอบที่ 2 — โหมดพิเศษ/เชิงโครงสร้าง (
func_entry,func_exit,block_entry,block_exit, เป้าหมายการแตกกิ่ง ฯลฯ): ถูกแก้ไขให้ชัดเจนและฉีดตอน encode โดยรอบที่สองของ wirm เอง
สองรอบนี้เป็นอิสระต่อกัน wirm จะรวมผลลัพธ์ของทั้งสองรอบตอน encode โดยโพรบจากรอบที่ 1 มาก่อนโพรบจากรอบที่ 2 ณ คำสั่งเดียวกัน
นั่นหมายความว่าลำดับสัมพัทธ์ระหว่างโพรบ wasm:opcode:call:before กับโพรบ wasm:func:entry ณ ตำแหน่งเดียวกัน (เช่น call ที่เป็นคำสั่งแรกของฟังก์ชันด้วย) ถูกกำหนดตายตัวโดย wirm ไม่ใช่โดยลำดับในสคริปต์:
โพรบ before จะรันก่อนโพรบ func_entry ณ ตำแหน่งนั้นเสมอ
ดู probe_mode_for_package ใน instr_generator.rs สำหรับจุดที่ whamm แมปกฎโพรบเข้ากับ wirm mode
การแพร่ค่าคงที่และการพับค่า!!
การแพร่ค่าคงที่และการพับค่าเป็น optimization ของคอมไพเลอร์ที่มีจุดประสงค์พิเศษใน Whamm
มีแหล่งข้อมูลออนไลน์มากมายที่อธิบายแนวคิดเหล่านี้ หากจะเป็นประโยชน์กับผู้อ่าน
คำสั่ง whamm info ช่วยให้ผู้ใช้เห็นตัวแปรต่างๆ ที่อยู่ในสโคปเมื่อใช้กฎการแมตช์ของโพรบแบบต่างๆ
ตัวแปรโกลบอลทั้งหมดนี้ถูกนิยามโดยคอมไพเลอร์ของ Whamm และควรถูกปล่อยเป็นค่าคงที่เท่านั้น
หากตัวแปรถูกปล่อยเข้าไปในโปรแกรมที่ทำ instrumentation โดยตรง โดยไม่มีนิยามที่คอมไพเลอร์จัดให้ โปรแกรมจะรันไม่สำเร็จ เพราะตัวแปรนั้นจะไม่ถูกนิยาม
Whamm ใช้การแพร่ค่าคงที่และการพับค่าเพื่อแก้สถานการณ์นี้!
ฟังก์ชัน define ใน visiting_emitter.rs คือวิธีที่ค่าคงที่ของคอมไพเลอร์ถูกนิยาม ขณะท่องไบต์โค้ดของแอปพลิเคชัน
ตัวแปรเหล่านี้ถูกนิยามใน emitter เพราะนิยามของมันผูกอยู่กับตำแหน่งในโปรแกรม Wasm ที่กำลังทำ instrumentation
ExprFolder ใน folding.rs ทำการแพร่ค่าคงที่และการพับค่าบนนิพจน์
เมื่อพิจารณาโพรบที่มีเพรดิเคต พฤติกรรมนี้อาจน่าสนใจทีเดียว ลองดูนิยามโพรบต่อไปนี้เป็นตัวอย่าง:
wasm:bytecode:call:alt /
target_fn_type == "import" &&
target_imp_module == "ic0" &&
target_fn_name == "call_perform"
/ { ... }
ตัวแปรที่ผูกทั้งสามตัวที่ใช้ใน predicate ถูกนิยามแบบสแตติกโดยคอมไพเลอร์ และถูกจัดหาโดยอีเวนต์ call
หมายความว่าทุกการใช้ตัวแปรเหล่านี้จะถูกแทนที่ด้วยค่าคงที่ และ predicate จะพับเป็น true หรือ false
ถ้า predicate พับเป็น true actions ของโพรบสามารถถูกปล่อยที่ตำแหน่งที่พบโดยไม่มีเงื่อนไข
ถ้า predicate พับเป็น false โพรบไม่ควรถูกปล่อย
ทีนี้ ลองดูนิยามโพรบตัวอย่างถัดไป:
wasm:bytecode:call:alt /
target_fn_type == "import" &&
target_imp_module == "ic0" &&
target_fn_name == "call_new" &&
strcmp((arg0, arg1), "bookings") &&
strcmp((arg2, arg3), "record")
/ { ... }
predicate ของโพรบนี้ตอนนี้มีทั้งตัวแปรที่นิยามแบบสแตติกและตัวแปรที่นิยามแบบพลวัต ซึ่งถูกต้องตามความหมายทั้งหมด!
แล้วเกิดอะไรขึ้นตรงนี้?
ตัวแปรที่ผูกสามตัวแรกจะถูกแพร่กลายเป็นค่าคงที่ นิพจน์จะถูกพับค่า และการตรวจความเท่าเทียมของค่าคงที่เหล่านั้นจะประเมินเป็น true หรือ false
ค่าที่ลดรูปแล้วนี้จะถูกandเข้ากับส่วนที่นิยามแบบพลวัตของนิพจน์ที่ตามมา
เป้าหมายเดียวกันกับตัวอย่างก่อนหน้าจึงสำเร็จได้เช่นกัน (โพรบจะถูกปล่อยหรือไม่ถูกปล่อยที่ตำแหน่งไบต์โค้ดนั้นตามข้อมูลที่กำหนดได้แบบสแตติก)
แต่ครั้งนี้ actions ที่ปล่อยออกมาจะยังมีเงื่อนไข แต่เป็นเงื่อนไขที่พับค่าแล้วซึ่งมีเฉพาะส่วนพลวัตของ predicate เดิมเท่านั้น
เจ๋งใช่ไหมล่ะ??
ส่วนต่อประสานเอนจินของ Whamm (wei)
สำหรับ wei มี generator หนึ่งตัวชื่อ WeiGenerator อยู่ใน generator/mod.rs
generator นี้ท่อง AST ของ MetadataCollector และปล่อยมอนิเตอร์ที่พกพาได้และครบในตัวเองสำหรับ wei
generator นี้ใช้ emitter ที่ปล่อยโค้ด Wasm
โดย emitter ใช้ยูทิลิตี้ที่รวมตรรกะการปล่อย Wasm ไว้ที่ utils.rs
WeiGenerator
generator ท่อง AST เพื่อปล่อยฟังก์ชันและตัวแปรที่ต้องดำรงอยู่ก่อนปล่อยโพรบ
ฟังก์ชัน run เป็นจุดเข้าของ generator นี้
ซึ่งทำตามดีไซน์แพตเทิร์น visitor
มีแหล่งข้อมูลออนไลน์ที่ดีมากมายที่สอนเรื่อง visitor pattern หากจะเป็นประโยชน์กับผู้อ่าน
generator นี้ปล่อยฟังก์ชันและตัวแปร Wasm ใหม่เข้าไปในโปรแกรม พร้อม ID ของ Wasm ที่เกี่ยวข้อง
ID เหล่านี้ถูกเก็บไว้ใน SymbolTable เพื่อใช้ขณะรัน InstrGenerator
เมื่อปล่อยคำสั่งที่เรียกฟังก์ชันที่ปล่อยแล้ว หรือทำการดำเนินการบางอย่างกับโกลบอลที่ปล่อยแล้ว ชื่อของสัญลักษณ์นั้นจะถูกค้นหาใน SymbolTable เพื่อนำ ID ที่บันทึกไว้มาใช้ในคำสั่งที่ถูกปล่อย
โปรดจำไว้ว่าเมื่อกำหนดเป้าหมายเป็น wei Whamm ไม่มีสิทธิ์เข้าถึงแอปพลิเคชันระหว่างการคอมไพล์
แต่ Whamm ต้องเข้ารหัสคำขอสำหรับสถานะของแอปพลิเคชันผ่านส่วนต่อประสานแทน
generator เข้ารหัสคำขอเหล่านี้ไว้ในชื่อที่ส่งออกของฟังก์ชัน Wasm
จากนั้นสถานะที่ขอจะถูกส่งต่อโดยเอนจินไปยังมอนิเตอร์ผ่านอาร์กิวเมนต์ของฟังก์ชัน
การส่งออกของ wei
เพรดิเคตและเนื้อโพรบถูกคอมไพล์เป็น Wasm และใช้เป็นคอลแบ็กของเอนจิน เอนจินจะรับรู้ถึงฟังก์ชันพิเศษเหล่านี้ผ่านเมตาดาต้าที่เข้ารหัสไว้ในชื่อที่ส่งออก เมตาดาต้านี้เป็นไปตามไวยากรณ์ง่ายๆ ดังนี้:
⟨export-name⟩ ::= ⟨match-rule⟩⟨predicate⟩? ⟨params⟩?
⟨match-rule⟩ ::= 'wasm':'opcode':('OPCODE'|'*')
'wasm':'func':('entry'|'exit')
'wasm':'block':('start'|'end')
'wasm':'exit'
⟨predicate⟩ ::= ‘/’ ⟨call⟩‘/’
⟨call⟩ ::= ‘$’ ID ⟨params⟩?
⟨params⟩ ::= ‘(’ ‘)’
| ‘(’ ⟨param⟩( ‘,’ ⟨param⟩)* ‘)’
⟨param⟩ ::= ‘argN’ | ‘immN’ | ‘localN’ | ⟨call⟩| ‘pc’ | ‘fid’ | ‘frame’
เอนจินจะระบุฟังก์ชันพิเศษที่ส่งออกเหล่านี้ และหาตำแหน่งที่แมตช์ของมันในแอปพลิเคชันเป้าหมาย หากการแมตช์นั้นมีเพรดิเคตเพิ่มเติม มันจะเรียกฟังก์ชันเพรดิเคตที่ส่งออกและส่งข้อมูลที่ขอไปให้ ถ้าเพรดิเคตประเมินเป็นจริง (ผลลัพธ์ไม่เป็นศูนย์) โพรบจะถูกแทรกแบบพลวัต ณ ตำแหน่งนั้นของแอปพลิเคชัน ถ้าเพรดิเคตประเมินเป็นเท็จ (ผลลัพธ์เป็นศูนย์) โพรบจะไม่ถูกแทรกที่ตำแหน่งนั้น
ตัวแปรที่ผูกหลายตัวต้องถูกขอจากเอนจินอย่างชัดเจน
อย่างไรก็ตาม ตัวแปรบางตัวได้มาจากตัวแปรที่ผูกตัวอื่นๆ โดยตัวแปรเหล่านี้จะขอข้อมูลที่ตนได้มา เพื่อไม่ให้เอนจินต้องรองรับสถานะมากขึ้นเรื่อยๆ
ตัวอย่างเช่น effective_addr ของการโหลดได้มาจากการบวก arg0 กับ offset
ดังนั้นคอมไพเลอร์จะขอเพียง arg0 กับสถานะ offset ที่สแตติก แล้วคำนวณ effective_addr ขึ้นมาด้วยการบวกสองค่านั้น
นี่คือตัวอย่างการส่งออกฟังก์ชันของโพรบ:
(export "wasm:opcode:array.new / $20(is_func_end, opname) / ($21(fname), fid, pc)" (func 22))
เมื่อเห็นเช่นนี้ เอนจินจะหาออปโคด array.new ทั้งหมดในแอปพลิเคชันเป้าหมาย
มันจะประเมินฟังก์ชันเพรดิเคตที่ส่งออกด้วยชื่อ “$20” และส่งสถานะ is_func_end กับ op_name ไปให้
ถ้าประเมินเป็นจริง เอนจินจะรันฟังก์ชัน “$21” โดยส่งสถานะที่ขอไป และจำผลลัพธ์ของฟังก์ชันนี้ไว้
โพรบจะถูกแนบที่ตำแหน่ง array.new นี้ และเอนจินจะส่งผลลัพธ์ของการรัน “$21” รวมทั้ง fid และ pc ตามลำดับนั้น
มี optimization หลายอย่างที่เอนจินทำได้เพื่อให้ instrumentation รันเร็วเมื่อใช้ wei
optimization เหล่านี้ถูกอภิปรายไว้ในเปเปอร์นี้: https://doi.org/10.1145/3763124
คุณอาจสังเกตด้วยว่า wei ไม่รองรับการจับคู่แบบ glob ทั่วไป
เพื่อไม่ให้เอนจินต้องนำฟีเจอร์นี้ไปใช้ Whamm จึงขยาย glob ไปสู่อีเวนต์ที่แมตช์ต่ำสุดแทน
โพรบที่ทับซ้อนกัน
โพรบหลายตัวในสคริปต์อาจกำหนดเป้าหมายอีเวนต์เดียวกัน (เช่น โพรบ wasm:opcode:call:before สองตัว)
สำหรับการเขียนไบต์โค้ดใหม่ เรื่องนี้ไม่เป็นปัญหา — เนื้อของแต่ละโพรบถูกฉีดแยกกันที่จุดเรียก
แต่สำหรับ wei ฟังก์ชันโพรบที่ส่งออกแต่ละตัวถูกระบุด้วยชื่อที่ส่งออก
ถ้าโพรบสองตัวสร้างชื่อที่ส่งออกเหมือนกัน เอนจินจะเห็นการส่งออกซ้ำกัน และพฤติกรรมจะไม่ถูกนิยาม
เพื่อจัดการเรื่องนี้ WeiGenerator จะรวมโพรบที่มีลายเซ็นชื่อที่ส่งออกเหมือนกันก่อนปล่อย
คีย์ของการรวมเข้ารหัสทุกอย่างที่กำหนดรูปร่างของชื่อที่ส่งออก:
- สตริงกฎ (เช่น
wasm:opcode:call:before) - ชนิดพารามิเตอร์ของฟังก์ชันเนื้อโพรบ — ได้มาจาก
metadata.body_args; โพรบที่ขอสถานะแอปพลิเคชันต่างกัน (เช่นlocalNคนละชนิด หรือตัวแปรเอนจินต่างกัน) ให้ลายเซ็นเนื้อโพรบต่างกัน จึงถูกแยกไว้ - ชนิดพารามิเตอร์ของฟังก์ชันเพรดิเคต — จาก
metadata.pred_args; รวมเฉพาะเมื่อฟังก์ชันเพรดิเคตแยกต่างหากจะถูกปล่อย (เพรดิเคตแบบสแตติก ไม่ใช่พลวัต) - ชนิดพารามิเตอร์ของฟังก์ชัน init/alloc — จาก
metadata.init_args - ว่ามีฟังก์ชัน alloc หรือไม่ (คือโพรบมีตัวแปร unshared)
- จำนวนฟังก์ชันการเรียกไลบรารีแบบสแตติก
โพรบสองตัวที่มีคีย์ตรงกันจะถูกรวมเป็นโพรบเดียวที่เนื้อเป็นผลต่อกันของเนื้อทั้งสอง
ถ้าโพรบองค์ประกอบมีเพรดิเคต เนื้อของมันจะถูกห่อเป็น if <pred> { <body> } เพื่อให้เพรดิเคตถูกประเมินตอนรันไทม์ภายในฟังก์ชันที่รวมแล้ว แทนที่จะเป็นการส่งออกตรวจล่วงหน้าแยกต่างหาก
สิ่งนี้ทำให้เพรดิเคตที่ถูก inline ทั้งหมดกลายเป็น “พลวัต” ในแง่ว่าถูกตรวจภายในเนื้อ ไม่ได้ส่งออกเป็นฟังก์ชันเพรดิเคตแยก
การจัดการเพิ่มเติมที่ทำระหว่างการรวม:
- การแมปนามแฝง
@staticใหม่ — การอ้างอิงตัวแปร@staticNของแต่ละโพรบถูกเปลี่ยนเลขตามออฟเซตของการเรียกไลบรารีแบบสแตติกของโพรบนั้นในรายการที่รวมแล้ว (เช่น@static0ของโพรบ B กลายเป็น@static2ถ้าโพรบ A มีส่วนทำให้เกิดการเรียกแบบสแตติก 2 ครั้ง) - การแยกตัวแปร unshared ที่ซ้ำกัน — ถ้าโพรบสองตัวประกาศตัวแปร unshared ชื่อเดียวกัน ตัวแปรของโพรบที่มาทีหลังจะถูกเปลี่ยนชื่อด้วยคำต่อท้ายเป็นตัวเลข (
foo→foo_1ฯลฯ) และการอ้างอิงทั้งหมดในเนื้อและตรรกะ init ของมันถูกเขียนใหม่ให้สอดคล้องกัน
CLI ของ Whamm
CLI ถูกนิยามขึ้นด้วยเครต Rust ชื่อ clap
การขยายนิยาม Provider
(ตอนนี้ขั้นตอนนี้ค่อนข้างน่าเบื่อ เราวางแผนจะเปลี่ยนวิธีทำในอนาคต)
provider ของ Whamm ถูกนิยามผ่านไฟล์ .yaml ที่อยู่ในโฟลเดอร์ providers/
ไฟล์เหล่านี้ถูกประมวลผลโดย provider_handler.rs เพื่อค้นหาเหตุการณ์ที่แมตช์ตามกฎการแมตช์ของโพรบ
เหตุผลที่มันน่าเบื่อมากคือ ไฟล์ .yaml ทั้งหมดที่ส่งให้ Whamm ถูกต่อรวมเป็นไฟล์ .yaml ใหญ่ไฟล์เดียว เพื่อให้เข้ากับไลบรารีแยกวิเคราะห์ yaml ของ Rust
ยิ่งไปกว่านั้น ไลบรารีนี้ยังมีบั๊กในการจัดการ anchor ของ yaml ทำให้สิ่งที่ต้องเขียนซ้ำซ้อนมาก
และเนื่องจากไลบรารีนี้เพิ่งถูก archive ไป เราจึงจำเป็นต้องย้ายไปใช้อะไรที่เสถียรกว่านี้จริงๆ
อย่างไรก็ตาม การขยายสิ่งที่เรามีตอนนี้ทำได้โดยแก้ไฟล์ที่พบในโฟลเดอร์ providers/
หากต้องการทดสอบการเปลี่ยนแปลง ให้ใช้ยูทิลิตี้ whamm info ซึ่งควรแสดงการเปลี่ยนแปลงให้เห็นทันที
ถ้าคุณเจอข้อผิดพลาดตอนแยกวิเคราะห์ yaml แสดงว่าการเปลี่ยนแปลงของคุณมีปัญหา
นอกจากนี้ จำไว้ว่า CLI ของ whamm รองรับการเปลี่ยนเส้นทางที่ใช้ค้นหาโฟลเดอร์ providers/
โดยใช้ตัวเลือก --defs-path
การผูกข้อมูลสำหรับ Rewriting
หากคุณต้องการรองรับการเปลี่ยนแปลง provider ของคุณในแบ็กเอนด์ rewriting ของ Whamm คุณต้องนิยามก่อนว่าสถานะจริงๆ คืออะไรในคอมไพเลอร์
ทั้งหมดนี้จัดการอยู่ใน emitter/rewriting/rules/mod.rs
ไฟล์นี้แมตช์กับกฎการแมตช์ของโพรบ (เช่น “wasm:opcode:call:before”) แล้วดูว่าสถานะของแอปพลิเคชันใดที่โพรบต้องใช้ในการทำงาน
จากนั้นจึงนิยามสถานะนี้และให้คอมไพเลอร์ดำเนินการแทรกโพรบต่อไป
นิยาม Provider + wei = เจ๋ง
ฟังก์ชันนี้ถึงขั้นให้คุณนิยาม provider ใหม่ทั้งหมดสำหรับ Whamm ได้ ทำให้คุณทำ instrumentation กับอะไรก็ได้ด้วยโค้ด Wasm!
บางทีคุณอาจอยากทำ instrumentation กับ syscall ของลินุกซ์ด้วย Wasm (ระวังตัวไว้ eBPF)
หากต้องการทำเช่นนี้ คุณต้องสร้างนิยาม provider สำหรับสิ่งนั้น แล้วสร้างสิ่งที่รองรับ wei
เจ้า wei นี้จะทำแบบเดียวกับที่เอนจิน Wasm ทำกับโมดูลมอนิเตอร์ของคุณ คือหาเมธอดที่ส่งออกที่เกี่ยวข้อง ทำเพรดิเคชัน และจำสถานะไว้ส่งต่อเมื่อเรียกคอลแบ็ก
จากนั้นมันต้องแทรกตรรกะเข้าไปในเคอร์เนลให้เรียกคอลแบ็กและส่งสถานะที่เกี่ยวข้องไปยังโมดูล Wasm ด้วย
สิ่งนี้ช่วยให้คุณทำ instrumentation กับอะไรก็ได้ด้วย Wasm ผ่านส่วนต่อประสานที่เรียบง่ายและขยายได้: wei
ฮาร์เนสทดสอบ .wast
ไฟล์ .wast ถูกใช้เพื่อการทดสอบและช่วยให้นักพัฒนาเขียนเทสต์ได้ง่ายขึ้น
เราใช้ไฟล์ .wast เพื่อเข้ารหัสแอสเซอร์ชันที่ควรผ่านเมื่อรันโมดูล wasm ที่ทำ instrumentation แล้ว
การเขียนเทสต์ .wast
โครงสร้างระดับสูงมีหน้าตาดังนี้:
<module_in_wat>
;; WHAMM --> <some_oneline_whamm_script>
<whamm0_assertion0> ;; The first assertion for the first whamm script
<whamm0_assertion1> ;; The second assertion for the first whamm script
;; WHAMM --> <some_oneline_whamm_script>
<whamm1_assertion0> ;; The first assertion for the second whamm script
<whamm1_assertion1> ;; The second assertion for the second whamm script
<whamm2_assertion1> ;; The third assertion for the second whamm script
โมดูลที่เข้ารหัสในสคริปต์ด้านบนจะถูกใช้กับทุกกลุ่ม whamm/แอสเซอร์ชันที่ตามมา
เพื่อยืนยันว่าแอสเซอร์ชันทั้งหมดล้มเหลวก่อนทำ instrumentation ไฟล์ .wast ใหม่ 5 ไฟล์จะถูกสร้างขึ้นด้วยโมดูลต้นฉบับและรันบนอินเทอร์พรีเตอร์ที่กำหนดค่าไว้
และเพื่อยืนยันว่าแอสเซอร์ชันทั้งหมดผ่านหลังทำ instrumentation ไฟล์ .wast ใหม่ 2 ไฟล์จะถูกสร้างขึ้น หนึ่งไฟล์ต่อสคริปต์ whamm ที่ระบุ โดยรวมแอสเซอร์ชันไว้ใต้สคริปต์ whamm ที่เกี่ยวข้อง
ด้านล่างคือตัวอย่างเทสต์ .wast:
;; Test `wasm:opcode:call` event
;; @instrument
(module
;; Auxiliary definitions
(func $other (param i32) (result i32) (local.get 1))
(func $dummy (param i32) (result i32) (local.get 0))
;; Test case functions
(func (export "instrument_me") (result i32)
(call $dummy (i32.const 0))
)
)
;; WHAMM --> wasm:opcode:call:before { arg0 = 1; }
(assert_return (invoke "instrument_me") (i32.const 1)) ;; will be run with the above WHAMM instrumentation
;; WHAMM --> wasm:opcode:call:alt { alt_call_by_name("other"); }
(assert_return (invoke "instrument_me") (i32.const 1)) ;; will be run with the above WHAMM instrumentation
ด้านล่างคือตัวอย่างเทสต์ .wast ที่ใช้การนำเข้า:
(module
(func (export "dummy") (param i32) (result i32)
local.get 0
)
)
(register "test")
;; @instrument
(module
;; Imports
(type (;0;) (func (param i32) (result i32)))
(import "test" "dummy" (func $dummy (type 0)))
;; Globals
(global $var (mut i32) (i32.const 0))
;; Global getters
(func $get_global_var (result i32)
(global.get $var)
)
;; Test case functions
(func $foo
(call $dummy (i32.const 0))
global.set $var
)
(start $foo)
(export "foo" (func $foo))
(export "get_global_var" (func $get_global_var))
(memory (;0;) 1)
)
;; WHAMM --> var count: i32; wasm:opcode:call:alt / arg0 == 0 / { count = 5; return 1; }
(assert_return (invoke "get_global_var") (i32.const 1)) ;; alt, so global should be return value
(assert_return (invoke "get_count") (i32.const 5))
มีธรรมเนียมปฏิบัติหลายอย่างที่ควรทำตามเมื่อเขียนเคสเทสต์ .wast สำหรับ whamm
- มี
moduleที่จะทำ instrumentation เพียงหนึ่งโมดูลต่อไฟล์.wast- การตั้งค่าเทสต์อยู่ด้านบน (ซึ่งรวมหลายโมดูลได้เมื่อพิจารณาการทดสอบการนำเข้า)
moduleที่จะทำ instrumentation เป็นส่วนสุดท้ายของการตั้งค่า และมีเครื่องหมาย;; @instrumentกำกับเหนือโมดูล
- ใช้คอมเมนต์ระบุสคริปต์
Whammไวยากรณ์:;; WHAMM --> <whamm_script>- สคริปต์ถูกรันบน
moduleในไฟล์.wast - ถ้ามี
assertsหลายอันใต้คอมเมนต์Whammอันหนึ่ง พวกมันทั้งหมดจะถูกรันกับmoduleที่ทำ instrumentation แล้วซึ่งได้จากสคริปต์Whammนั้น
- สคริปต์ถูกรันบน
- แอสเซิร์ตทั้งหมดควร ล้มเหลว หากถูกรันโดยไม่มีการทำ instrumentation
หมายเหตุ: สำหรับ wei อย่าทำการปรับเปลี่ยนที่เปลี่ยน arg* (ซึ่งต้องใช้ตัวเข้าถึงเฟรม) ให้เปลี่ยนสถานะโกลบอลแทนไปก่อน?
โค้ดของฮาร์เนส
ฮาร์เนสอยู่ใน tests/common/wast_harness.rs โดยมีฟังก์ชัน main เป็นจุดเข้า
เราเรียกฮาร์เนสนี้ผ่านการเรียกจุดเข้า main ในเคสเทสต์ run_wast_tests ที่อยู่ใน tests/integration_test.rs
ใครก็อ่านโค้ดของฮาร์เนสได้ และจะเห็นว่ามันทำตรรกะต่อไปนี้:
- แยกส่วนประกอบของเทสต์ จากแต่ละไฟล์
.wastที่พบใต้tests/wast_suiteเป็นWastTestCaseแต่ละรายการ- โมดูล
wasm - สคริปต์
whamm - รายการแอสเซอร์ชันที่ควรเป็นจริงหลังทำ instrumentation
- โมดูล
- ยืนยันว่าแอสเซอร์ชันทั้งหมดล้มเหลวก่อนทำ instrumentation ด้วย
whammเราทำเช่นนี้เพื่อยืนยันได้ว่าความถูกต้องของ instrumentation เป็นเหตุผลเดียวที่ทำให้เทสต์บางตัวผ่าน เรายืนยันคุณสมบัตินี้โดยสร้างไฟล์.wastใหม่ที่มีแอสเซอร์ชันเดียวต่อไฟล์ก่อน แล้วตรวจว่ามันล้มเหลวเมื่อรันบนอินเทอร์พรีเตอร์ที่รองรับ- เราสร้างไฟล์
.wastใหม่แบบนี้เพราะอินเทอร์พรีเตอร์ที่เราใช้จะออกเมื่อเจอแอสเซอร์ชันแรกที่ล้มเหลวต่อไฟล์.wastแต่เราต้องการรับประกันคุณสมบัตินี้กับทุกแอสเซอร์ชัน
- เราสร้างไฟล์
- ยืนยันว่าแอสเซอร์ชันทั้งหมดผ่านหลังทำ instrumentation ด้วย
whamm- รันสคริปต์
whammที่ระบุบนโมดูลต่อชุดแอสเซอร์ชัน - ส่งออกไฟล์
.wastใหม่ที่มีโมดูลที่ทำ instrumentation แล้วพร้อมแอสเซอร์ชันที่เกี่ยวข้อง
- รันสคริปต์
อินเทอร์พรีเตอร์ที่รองรับ
ฮาร์เนสสร้างไฟล์ *.bin.wast เพื่อรันบนรายการเอนจิน เช่น wizeng และ spec interpreter
ดู README.md ของรีโพสำหรับวิธีตั้งค่าอินเทอร์พรีเตอร์ให้รันกับฮาร์เนสทดสอบของเรา
ไอเดียสำหรับการปรับปรุงในอนาคต
ตัวแปร Report
;; Use something like below to assert on the values of some report variable dynamically.
;; REPORT_TRACE(ID) --> 1, 3, 5, 6, 7
;; Use something like below to assert on report variable values!
;; WITH_WHAMM --> (assert_return (invoke "get_report_var" (i32.const 1)) (i32.const 7))
การจัดการข้อผิดพลาด
ข้อผิดพลาดจะถูกเพิ่มเข้าใน ErrorGen ซึ่งนิยามไว้ใน error.rs และถูกรายงานระหว่างเฟสของคอมไพเลอร์
หากเกิดข้อผิดพลาดขึ้นระหว่างเฟสการคอมไพล์ ข้อผิดพลาดจะถูกรายงานและยกเลิกการคอมไพล์ทันที
ผู้มีส่วนร่วม
คนเหล่านี้คือผู้ที่คุณควรขอบคุณเมื่อใช้ Whamm … จะจริงใจหรือประชดก็ตาม … หากคุณพบปัญหา ควรแจ้งให้พวกเขาทราบใน GH issue ;)
Elizabeth Gilbert นักศึกษาปริญญาเอกที่ Carnegie Mellon University (CMU)
Alex Bai นักศึกษาปริญญาตรีที่ Tufts University
Wavid Bowman, นักศึกษาปริญญาตรีที่ University of Florida