วิธีสร้างเครื่องเสมือน (2022)
(jmeiners.com)- เพื่อทำความเข้าใจการทำงานภายในของคอมพิวเตอร์และวิธีที่ภาษาโปรแกรมถูกรัน ผู้เขียนได้ลงมือสร้าง VM ที่เขียนด้วย C ราว 250 บรรทัด เพื่อรันโปรแกรมแอสเซมบลีบนสถาปัตยกรรมเพื่อการศึกษา LC-3 ด้วยตัวเอง
- สิ่งที่นำมาสร้างคือ โมเดลคอมพิวเตอร์ขนาดเล็ก ที่มีตำแหน่งหน่วยความจำ 16 บิตจำนวน 65,536 ตำแหน่ง, รีจิสเตอร์ 10 ตัว, opcode 16 แบบ, condition flag, trap routine และ memory-mapped register
- ลูปการทำงานจะอ่านคำสั่งที่
PCชี้อยู่แล้วเพิ่มค่า จากนั้นถอดรหัส opcode เพื่อรันคำสั่งอย่างADD,LDI,BR,JMP,TRAPตามโครงสร้างแบบ fetch-decode-execute - การโหลดโปรแกรมจะอ่านค่า origin 16 บิตตัวแรกของ object file แล้ววางลงในหน่วยความจำ พร้อมสลับไบต์ให้เข้ากับรูปแบบ little-endian ที่คอมพิวเตอร์สมัยใหม่ส่วนใหญ่ใช้ จากรูปแบบ big-endian ของ LC-3
- การรับคีย์บอร์ดและแสดงผลคอนโซลจัดการผ่าน trap routine และ memory-mapped register ของ
KBSR/KBDRโดยต้องมีโค้ดจัดการ input buffering ของเทอร์มินัลที่ต่างกันระหว่าง Unix/macOS และ Windows
เป้าหมายและสมมติฐานของบทสอน
- พาเขียน LC-3 virtual machine ด้วยตัวเองและตามดูขั้นตอนการรันโปรแกรมภาษาแอสเซมบลี
- โค้ดสุดท้ายมีขนาดประมาณ 250 บรรทัด ใน C และมีทั้ง
lc3.cสำหรับ Unix กับlc3-win.cสำหรับ Windows - พื้นฐานที่ต้องมีคือการอ่าน C หรือ C++ เบื้องต้น และ เลขคณิตฐานสอง
- โค้ดทั้งหมดอยู่ใน GitHub repo และตัวบทสอนเองอยู่ในรูปแบบ literate program ที่นำบล็อกโค้ดมาประกอบเป็นซอร์สสุดท้าย
เครื่องเสมือนทำหน้าที่อะไร
- VM คือโปรแกรมที่ทำงานคล้าย CPU และฮาร์ดแวร์บางส่วน
- ทำงานคำนวณทางคณิตศาสตร์
- อ่านและเขียนหน่วยความจำ
- โต้ตอบกับอุปกรณ์ I/O
- เข้าใจ ภาษาเครื่อง ของตัวเองและใช้มันรันโปรแกรม
- ขึ้นอยู่กับเป้าหมายของ VM มันอาจจำลองฮาร์ดแวร์จริงอย่างใกล้เคียง หรือสร้างสถาปัตยกรรมเสมือนใหม่เพื่อให้พัฒนาซอฟต์แวร์ได้สะดวกขึ้นก็ได้
- JVM เป็นตัวอย่างเด่นของ VM ที่ทำหน้าที่เป็นแพลตฟอร์มรันมาตรฐาน และบนอุปกรณ์ที่มี JVM ก็สามารถรันโปรแกรม Java, Kotlin, Clojure ได้โดยไม่ต้องแก้ไข
- การรันแบบแยกสภาพแวดล้อมก็เป็นการใช้งานสำคัญของ VM เช่นกัน
- ใน garbage collection นั้น VM สามารถสังเกต stack และการอ้างอิงหน่วยความจำจากภายนอกโปรแกรมที่กำลังรันอยู่ได้
- Ethereum smart contract จะรันอยู่ใน VM ที่ไม่สามารถเข้าถึง file system, network, disk ฯลฯ ได้
องค์ประกอบของสถาปัตยกรรม LC-3
- สิ่งที่จะนำมาสร้างคือ LC-3 ซึ่งใช้สอนโครงสร้างคอมพิวเตอร์และภาษาแอสเซมบลีในมหาวิทยาลัย
- หน่วยความจำของ LC-3 มี 65,536 ตำแหน่ง และแต่ละตำแหน่งเก็บค่า 16 บิต
- ความจุรวมคือ 128KB
- ใน C จะแทนด้วยอาร์เรย์
uint16_t memory[MEMORY_MAX]
- รีจิสเตอร์มีทั้งหมด 10 ตัว
R0~R7: รีจิสเตอร์ใช้งานทั่วไป 8 ตัวPC: ที่อยู่หน่วยความจำของคำสั่งถัดไปที่จะรันCOND: condition flag ของผลการคำนวณครั้งล่าสุด
- คำสั่งของ LC-3 ทุกคำสั่งมีขนาด 16 บิต และ 4 บิตซ้ายสุดคือ opcode
- มีการกำหนด opcode ไว้ 16 แบบ
- รวมถึง
OP_BR,OP_ADD,OP_LD,OP_ST,OP_JSR,OP_AND,OP_LDR,OP_STR,OP_RTI,OP_NOT,OP_LDI,OP_STI,OP_JMP,OP_RES,OP_LEA,OP_TRAP
- condition flag ใช้บอกเครื่องหมายของผลลัพธ์จากการคำนวณล่าสุด
FL_POS: ค่าบวกFL_ZRO: 0FL_NEG: ค่าลบ
แอสเซมบลีกับภาษาเครื่อง
- สิ่งที่ LC-3 VM รันจริง ๆ ไม่ใช่แอสเซมบลีที่มนุษย์อ่านได้ แต่เป็น อาร์เรย์คำสั่งภาษาเครื่อง 16 บิต
- assembler จะเปลี่ยนแอสเซมบลี LC-3 ที่เขียนเป็นข้อความให้กลายเป็นคำสั่งไบนารี 16 บิต
- ตัวอย่าง
Hello Worldมีลำดับการทำงานดังนี้.ORIG x3000: ระบุที่อยู่หน่วยความจำที่จะโหลดโปรแกรมLEA R0, HELLO_STR: โหลดที่อยู่ของสตริงเข้าR0PUTS: แสดงสตริงที่R0ชี้อยู่HALT: หยุดโปรแกรม.STRINGZ "Hello World!": เก็บข้อมูลสตริงไว้ในโปรแกรม
.ORIG,.STRINGZไม่ใช่คำสั่งของ CPU แต่เป็น assembler directive- เงื่อนไขและการวนซ้ำถูกเขียนด้วยคำสั่งกระโดดที่ใกล้เคียงกับ
gotoเช่นBRn LOOP
ขั้นตอนหลักของลูปการรัน
- การทำงานของ VM จะวนซ้ำขั้นตอนเดิม
- อ่านคำสั่งจากตำแหน่งที่รีจิสเตอร์
PCชี้อยู่ - เพิ่มค่า
PC - ดึง opcode จาก 4 บิตบนของคำสั่ง
- รันโค้ดที่ตรงกับ opcode
- แล้วกลับไปอ่านคำสั่งถัดไป
- อ่านคำสั่งจากตำแหน่งที่รีจิสเตอร์
- ที่อยู่เริ่มต้นพื้นฐานคือ
0x3000 - บางคำสั่งสามารถเปลี่ยนค่า
PCโดยตรงเพื่อกระโดดเส้นทางการรันได้- เพราะมีคำสั่ง branch และ jump จึงทำให้โครงสร้างที่ดูเหมือนแค่เพิ่ม
PCไปเรื่อย ๆ ยังรองรับลูปและการรันแบบมีเงื่อนไขได้
- เพราะมีคำสั่ง branch และ jump จึงทำให้โครงสร้างที่ดูเหมือนแค่เพิ่ม
- ลูป
mainใช้switch (op)เพื่อเรียกโค้ดตาม opcode- รองรับ
OP_ADD,OP_AND,OP_NOT,OP_BR,OP_JMP,OP_JSR,OP_LD,OP_LDI,OP_LDR,OP_LEA,OP_ST,OP_STI,OP_STR,OP_TRAP OP_RES,OP_RTIเป็น opcode ที่ไม่ได้ใช้และอาจจัดการด้วยabort()
- รองรับ
วิธี implement คำสั่ง
ADDจะนำสองค่ามาบวกกัน เก็บไว้ในรีจิสเตอร์ปลายทาง และอัปเดต condition flagADDมีสองโหมด- โหมดรีจิสเตอร์: อ่าน operand ตัวที่สองจากรีจิสเตอร์อีกตัว
- โหมด immediate: อ่าน operand ตัวที่สองจาก
imm5ซึ่งอยู่ใน 5 บิตล่างของคำสั่ง
- ค่าที่สั้นกว่า 16 บิตอย่าง
imm5ต้องถูกขยายเป็นค่า 16 บิตด้วย sign extension- ค่าบวกจะเติม 0
- ค่าลบจะเติม 1 เพื่อรักษาค่าเดิมไว้
- คำสั่งที่เขียนค่าลงรีจิสเตอร์จะอัปเดต
R_CONDผ่านupdate_flags- ถ้าค่าเป็น 0 ให้
FL_ZRO - ถ้าบิตสูงสุดเป็น 1 ให้
FL_NEG - นอกนั้นให้
FL_POS
- ถ้าค่าเป็น 0 ให้
LDIคือคำสั่ง “load indirect”- ทำ sign extension ให้
PCoffset9ของคำสั่ง - นำไปบวกกับ
PCปัจจุบันเพื่อได้ที่อยู่หน่วยความจำ - ใช้ค่าที่เก็บอยู่ในตำแหน่งนั้นเป็นที่อยู่อีกชั้น แล้วอ่านข้อมูลปลายทาง
- นำค่าที่อ่านได้ไปเก็บในรีจิสเตอร์ปลายทางและอัปเดต condition flag
- ทำ sign extension ให้
ชุดคำสั่งหลัก
- การคำนวณและบิตไวส์
ADD: บวกAND: บิต ANDNOT: บิต NOT
- การควบคุมลำดับการทำงาน
BR: เทียบ condition flag กับบิตเงื่อนไขของคำสั่งแล้วเปลี่ยนPCJMP: ตั้งค่าPCเป็นค่าของรีจิสเตอร์ที่กำหนดRET: ในสเปกระบุเป็นคีย์เวิร์ดแยก แต่จริง ๆ เป็นกรณีพิเศษของJMPJSR,JSRR: บันทึกPCปัจจุบันไว้ในR7แล้วกระโดดไปยังตำแหน่งของ subroutine
- การอ่านหน่วยความจำ
LD: อ่านจากตำแหน่ง offset อ้างอิงจากPCLDI: อ่านผ่านที่อยู่ทางอ้อมอีกหนึ่งชั้นLDR: อ่านจากที่อยู่ที่คำนวณจาก base register และ offsetLEA: เก็บ effective address เองลงในรีจิสเตอร์
- การเขียนหน่วยความจำ
ST: เก็บลงตำแหน่ง offset อ้างอิงจากPCSTI: เก็บโดยตามที่อยู่ทางอ้อมSTR: เก็บลงตำแหน่งที่คำนวณจาก base register และ offset
Trap routine และ I/O
- LC-3 มี trap routine สำหรับงานทั่วไปและการเข้าถึงอุปกรณ์ I/O
- trap routine สามารถมองได้เหมือนระบบปฏิบัติการหรือ API ของ LC-3
- trap code ถูกกำหนดไว้ดังนี้
TRAP_GETC = 0x20: รับอักขระจากคีย์บอร์ดโดยไม่ echo บนเทอร์มินัลTRAP_OUT = 0x21: แสดงอักขระTRAP_PUTS = 0x22: แสดง word stringTRAP_IN = 0x23: รับอักขระแล้ว echo บนเทอร์มินัลTRAP_PUTSP = 0x24: แสดง byte stringTRAP_HALT = 0x25: หยุดโปรแกรม
- ใน LC-3 simulator อย่างเป็นทางการ trap routine ถูกเขียนด้วยแอสเซมบลี แต่ใน VM นี้จะ implement เป็นฟังก์ชัน C
PUTSจะเริ่มแสดงผลจากที่อยู่ที่เก็บในR0ไปเรื่อย ๆ จนกว่าจะเจอx0000- สตริงของ LC-3 ไม่ได้เก็บตัวอักษรแบบ 1 ไบต์ต่อเนื่องเหมือน C string แต่เก็บ หนึ่งอักขระต่อหนึ่งตำแหน่งหน่วยความจำ
- เนื่องจากแต่ละตำแหน่งเป็น 16 บิต เวลาพิมพ์ใน C จึงต้องแปลงเป็น
char
- trap
HALTจะพิมพ์"HALT"แล้วเปลี่ยน execution flag เป็น 0 เพื่อออกจากลูป VM
การโหลด program image
- เมื่อแปลงโปรแกรมแอสเซมบลี LC-3 เป็นภาษาเครื่องแล้ว จะได้ไฟล์ที่มีอาร์เรย์ของคำสั่งและข้อมูล
- 16 บิต แรกของ object file คือ origin ที่บอกว่าควรวางโปรแกรมไว้ตรงไหนในหน่วยความจำ
- ตัวโหลดจะอ่าน origin ก่อน แล้วคัดลอกข้อมูลที่เหลือไปยังหน่วยความจำตั้งแต่ที่อยู่ origin
- โปรแกรม LC-3 ใช้รูปแบบ big-endian
- คอมพิวเตอร์สมัยใหม่ส่วนใหญ่ใช้ little-endian จึงต้องใช้
swap16กับuint16_tแต่ละตัวที่โหลดมา - บนเครื่อง big-endian อย่าง PPC Mac รุ่นเก่าไม่ควรสลับไบต์
- คอมพิวเตอร์สมัยใหม่ส่วนใหญ่ใช้ little-endian จึงต้องใช้
read_imageจะเปิดไฟล์ในโหมดไบนารี เรียกread_image_fileแล้วปิดไฟล์
Memory-mapped register
- รีจิสเตอร์พิเศษที่ไม่ได้เข้าถึงผ่านตารางรีจิสเตอร์ทั่วไป จะถูกแมปไว้กับที่อยู่หน่วยความจำเฉพาะ
- ใน LC-3 มี memory-mapped register ที่ต้อง implement อยู่สองตัว
MR_KBSR = 0xFE00: keyboard status registerMR_KBDR = 0xFE02: keyboard data register
KBSRบอกว่ามีการกดปุ่มหรือไม่ และKBDRเก็บว่าปุ่มใดถูกกดGETCจะบล็อกการทำงานจนกว่าจะมีอินพุตเข้ามา แต่KBSRและKBDRเปิดทางให้โปรแกรม poll สถานะอุปกรณ์เพื่อยังตอบสนองต่อไปได้ระหว่างรออินพุต- การอ่านหน่วยความจำจะไม่อ่านจากอาร์เรย์โดยตรง แต่ผ่าน
mem_read- ถ้าที่อยู่คือ
MR_KBSRจะเรียกcheck_key()เพื่อตรวจสถานะคีย์บอร์ด - ถ้ามีปุ่มถูกกด จะตั้งบิตสูงสุดของ
KBSRและเก็บค่าgetchar()ลงในKBDR - ถ้าไม่มีปุ่มถูกกด จะตั้ง
KBSRเป็น 0
- ถ้าที่อยู่คือ
การจัดการเทอร์มินัลตามแพลตฟอร์ม
- เพื่อให้จัดการอินพุตคีย์บอร์ดและพฤติกรรมของเทอร์มินัลได้ถูกต้อง จำเป็นต้องตั้งค่า input buffering แยกตามแพลตฟอร์ม
- ฝั่ง Linux/macOS/UNIX ใช้
termios,selectเป็นต้น- ปิด canonical mode และ echo
- ใช้
selectตรวจว่าพร้อมรับอินพุตหรือไม่
- ฝั่ง Windows ใช้
GetStdHandle,GetConsoleMode,SetConsoleMode,_kbhitเป็นต้น- ปรับ echo และ line input
- ใช้
WaitForSingleObjectกับ_kbhitเพื่อตรวจการกดปุ่ม
- ตอนเริ่มโปรแกรมจะเรียก
disable_input_buffering()และตอนจบจะเรียกrestore_input_buffering() - เมื่อได้รับ
SIGINTจะกู้คืนค่าเทอร์มินัล พิมพ์ขึ้นบรรทัดใหม่ แล้วจบการทำงาน
การรัน VM และการดีบัก
- ตัวอย่างการ build VM มีดังนี้
gcc lc3.c -o lc3-vm
- เมื่อต้องการรัน ให้ส่ง object file ของ LC-3 ที่ประกอบแล้วเป็นอาร์กิวเมนต์
lc3-vm path/to/2048.obj
- object file ตัวอย่างที่ให้มาคือ
2048.objและrogue.obj - ตัวอย่าง 2048 ควบคุมด้วยปุ่ม WASD
- ถ้าโปรแกรมทำงานไม่ถูกต้อง มีโอกาสสูงว่าเกิดจากข้อผิดพลาดในการ implement คำสั่ง
- แนะนำให้อ่านซอร์สแอสเซมบลี LC-3 ไปพร้อมกับใช้ดีบักเกอร์รันคำสั่งของ VM ทีละขั้น
- หากพบจุดที่มันไม่กระโดดไปยังคำสั่งที่คาดไว้ ให้ย้อนกลับไปตรวจสเปกและ implementation ของคำสั่งนั้นอีกครั้ง
ตัวเลือกเสริม: implementation แบบ C++ generic
- ยังมีวิธี implement แบบ C++ ที่สั้นกว่าซึ่งกล่าวถึงเป็นตัวเลือกเสริม
- เพราะหลายคำสั่งใช้ขั้นตอนซ้ำกัน เช่น sign extension, offset อิง
PC, การคำนวณที่อยู่ทางอ้อม จึงสามารถมองการรันคำสั่งเป็น pipeline ของขั้นตอนย่อยได้ - ใช้ C++ template และ bit flag เพื่อให้แต่ละ opcode รวมเฉพาะขั้นตอนที่จำเป็นตอน compile
- วิธีนี้ช่วยลดโค้ดซ้ำ และยังใกล้เคียงกับรูปแบบการเดินสายของฮาร์ดแวร์จริงที่แต่ละขั้นตอนกินพื้นที่ทางกายภาพบนชิป
- แนวคิดนี้อ้างอิงจาก Bisqwit’s NES emulator
เอกสารและการมีส่วนร่วม
- atul-g ได้ช่วยทำ reference card ที่สรุปการทำงานของทั้งระบบ
- implementation หลายภาษาถูกรวบรวมไว้ภายใต้ GitHub topic
lc3- รวมถึง C, C++, Go, Haskell, Java, JavaScript, Kotlin, Lua, OCaml, Python, Ruby, Rust, Swift, TypeScript, Zig เป็นต้น
- หากต้องการให้ implementation ของตัวเองแสดงในรายการ ก็เพียงติด GitHub topic
lc3 - การรองรับแพลตฟอร์ม Windows ได้รับการช่วยพัฒนาโดย inkydragon
- ในโปรเจกต์มี good first issue ที่เกี่ยวกับ integration test
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ตอนเป็นวัยรุ่น ในคาบวิชาเบื้องต้นวิทยาการคอมพิวเตอร์ที่ community college ผมได้ออกแบบ ชุดคำสั่ง CPU ง่าย ๆ แล้วสร้าง virtual machine กับ assembler เอง จากนั้นก็เขียนและรันโปรแกรม assembly ดู
มันง่ายอย่างน่าประหลาด และทำให้คอมพิวเตอร์ดูลึกลับน้อยลงมาก
ผมคิดว่าวิธีแบบนี้น่าจะใช้เรียนรู้ทุกชั้นของการคอมพิวติ้งได้ ตั้งแต่การออกแบบ CPU จริงสำหรับ FPGA ไปจนถึงการเขียนระบบปฏิบัติการง่าย ๆ และโปรแกรมที่รันอยู่บนนั้น
ถ้าไม่นับประสิทธิภาพและความปลอดภัยที่คอมพิวติ้งสมัยใหม่ต้องการ แล้วตั้งเป้าแค่ว่า “ขอให้ทำงานได้” สาขานี้ก็เรียบง่ายกว่าที่คิด
ถ้าจำไม่ผิด อย่างน้อยก็มี memory segmentation, protected mode และ MMU เข้ามาเกี่ยวข้อง
เป็นคอมพิวเตอร์/assembler ง่าย ๆ ที่เขียนด้วย BASIC บน PDP และหนึ่งในงานที่ได้รับคือให้ทำการคูณอย่างง่ายโดยใช้ลูปบวกซ้ำ
เพื่อนผมกลับแก้โปรแกรมแทน แล้วสร้าง คำสั่ง MUL ใหม่ขึ้นมา ซึ่งอาจารย์ไม่ชอบเลยแม้แต่น้อย
คนที่มีความอยากรู้อยากเห็นและอยากเรียนรู้สามารถเรียนชั้นพื้นฐานเหล่านี้ได้ไม่ยาก แต่ไม่ใช่สำหรับคนที่ต้องการ “รีบหาเงินและพร้อมเข้าทำงานให้เร็วที่สุด”
หนังสือที่มีคนแนะนำ:
ถ้าใครเคยอ่านหนังสือเหล่านี้แล้วช่วยเพิ่มความคิดเห็น ก็น่าจะเป็นประโยชน์กับทุกคน
emulator ของ Nintendo, hypervisor ที่ใช้ VT-x, ระบบปฏิบัติการ multitasking แบบดั้งเดิม, interpreter ของภาษา scripting ใหม่, SQL query optimizer, regex matcher, security monitor ที่รันโค้ดผู้เล่นที่ไม่น่าเชื่อถือบน game server ฯลฯ ดูแทบไม่มีข้อพิจารณาร่วมกันเลย แต่ทั้งหมดล้วนเป็น virtual machine
แม้แต่ในรูปแบบ terminfo ที่กำหนด escape sequence ของ terminal แบบ character cell ก็ยังมี virtual machine แบบ stack-based อยู่
ถ้ามองลึกลงไป สิ่งที่ทำให้คอมพิวเตอร์เป็นคอมพิวเตอร์ในความหมายปัจจุบันก็คือ virtual machine และบทความ Entscheidungsproblem ปี 1936 ของ Turing ก็ผูกอยู่กับแนวคิดที่ว่า virtual machine สามารถเลียนแบบกันและกันได้
หลังจากดูซีรีส์ breadboard CPU ของ Ben Eater แล้ว ผมก็มีแต่ความอยากจะลอง ออกแบบและ emulate CPU เอง
หวังว่าจะหาเวลานั่งออกแบบมันได้
ผมมองว่า architecture เพื่อการศึกษาอย่าง Brookshear Machine หรือ Little Computer ไม่ได้เหมือน architecture จริงเลย จึงไม่ใช่แค่ไร้ประโยชน์ แต่ถึงขั้นเป็นโทษ
ผมเคยเห็นนักเรียนที่เรียนคาบซึ่งใช้สิ่งเหล่านี้ กลับเข้าใจคอมพิวเตอร์บิดเบี้ยวมากกว่าคนที่ไม่เคยเรียนคาบใดเลย
สำหรับคนส่วนใหญ่ที่อยากเรียนรู้นิดหน่อยว่าคอมพิวเตอร์ของตัวเองทำงานอย่างไร วิชาระบบปฏิบัติการ จะเหมาะกว่า และถ้าในนั้นมีเวลาทำได้แค่ tutorial สั้น ๆ หนึ่งอัน ผมแนะนำ “Writing my own bootloader”
https://dev.to/frosnerd/writing-my-own-boot-loader-3mld
คำพูดนี้ไม่ได้หมายความว่า tutorial “Write your own VM” แย่ แต่จากประสบการณ์ของผม สำหรับคนส่วนใหญ่ที่จะทำมัน หัวข้ออื่นจะเป็นประโยชน์มากกว่า
ช่วยอธิบายเพิ่มเติมได้ไหมว่าทำไม LC-3 ถึงไม่ดีสำหรับการศึกษาสถาปัตยกรรมคอมพิวเตอร์
ผมเข้าใจว่ามันต่างจากฮาร์ดแวร์จริงโดยสิ้นเชิงและเรียบง่ายเกินไป แต่ก็สงสัยว่าแม้มองจากมุมการเขียน CPU emulator มันก็แย่ด้วยหรือไม่
มันเป็นเครื่องเลขฐานสิบที่น่าจะถูกสร้างขึ้นได้ในยุค 1960 แต่หลังทศวรรษ 1970 ก็ไม่มีใครสร้างเครื่องแบบนั้นแล้ว
ระบบแบบนั้นสอนพื้นฐานได้หลายอย่าง แต่เทคนิคใน https://en.wikipedia.org/wiki/Hacker%27s_Delight ส่วนใหญ่พึ่งพาวิธีแทนค่าตัวเลขที่พบได้ทั่วไป จึงเรียนรู้ได้ยาก
ผมไม่ค่อยรู้ เลยลองดู Wikipedia แวบหนึ่ง หลังจากเห็นการ์ตูนแล้วคาดว่าจะเจออะไรแปลก ๆ แต่ดูผ่าน ๆ ก็ไม่ได้ชวนช็อกขนาดนั้น
ให้ความรู้สึกเหมือนผสม s/360, x86 เล็กน้อย, ARM นิดเดียว หรือ architecture ตระกูล RISC อื่น ๆ เข้าด้วยกัน และแม้จะมีส่วนที่ถูกละไว้กับส่วนแปลก ๆ อยู่มาก แต่เป้าหมายดูเหมือนเป็นการไปให้ถึง implementation ที่ทำงานได้เร็ว
อยากรู้ว่าอะไรทำให้มองว่ามัน “ไม่ใช่แค่ไร้ประโยชน์ แต่ถึงขั้นเป็นโทษ” สำหรับการศึกษา
ในวิชาวิทยาการคอมพิวเตอร์จำนวนมากในอินเดีย ดูเหมือนยังใช้ 8086/8088 กันอยู่
โดยเฉพาะสามารถทำ double-indirect load ผ่าน word แบบ PC-relative ที่อยู่ตรงกลางได้
แต่ถึงอย่างนั้น การลบก็ต้องสร้างจากการ negate และการ negate ก็ต้องสร้างจาก NOT กับ ADD ,,#-1
ถ้าคิดถึงพื้นที่ encoding ของคำสั่งที่มีจำกัด ผมว่า NOT d,s = XOR d,s,#-1 น่าจะเป็นการใช้งานที่ดีกว่า
ถ้าจะให้แยกอย่างเคร่งครัด สิ่งนี้ไม่ใช่เครื่องเสมือน แต่เป็น อีมูเลเตอร์
ในเชิงความหมายเชิงอธิบาย คำนี้อาจใช้ได้ และก่อนยุคการทำ hardware virtualization ก็มีความกำกวมอยู่บ้าง แต่ในปัจจุบัน การใช้คำว่า “Virtual Machine” ที่พบกันอย่างท่วมท้นมักหมายถึงสภาพแวดล้อมที่ใช้ฟีเจอร์ hardware virtualization อย่าง VT-x
JVM ถูกเผยแพร่อย่างกว้างขวาง, Ethereum VM ก็เรียกว่า EVM, https://www.linuxfoundation.org/hubfs/LF%20Research/The_Stat... ก็อธิบาย BPF และ eBPF ซ้ำ ๆ ว่าเป็น “virtual machines” และ https://webassembly.org/ ก็เริ่มต้นว่า “WebAssembly (ย่อว่า Wasm) เป็นรูปแบบคำสั่งไบนารีสำหรับเครื่องเสมือนแบบสแตก”
“เครื่องเสมือน” ยังคงเป็นคำที่ใช้เรียกเครื่องจักรเสมือนที่พบได้บ่อยที่สุด
โดยส่วนตัวชอบสำนวนอย่าง “fictive machine”, “fictious machine”, “imaginary computer”, “fantastic automaton” มากกว่า แต่คงไม่น่าจะถูกนำมาใช้กัน
ไม่สามารถใช้คำว่า “อีมูเลเตอร์” แทน “เครื่องเสมือน” ได้เสมอไป
wasmtime อาจเรียกว่าเป็นอีมูเลเตอร์ได้ แต่การเรียก WebAssembly เองว่าอีมูเลเตอร์นั้นไม่ถูกต้อง และ WebAssembly คือเครื่องเสมือนที่ wasmtime อีมูเลต
การเรียกอีมูเลเตอร์ว่าเครื่องเสมือนก็พบได้บ่อย และอินสแตนซ์ของอีมูเลเตอร์ที่กำลังทำงานอยู่ก็เป็นเครื่องเสมือนในอีกความหมายหนึ่งเช่นกัน
การเรียกสภาพแวดล้อม hardware virtualization ว่า “เครื่องเสมือน” ก็สมเหตุสมผล และทับซ้อนกับความหมายสุดท้ายนี้อยู่บ้าง
ในสภาพแวดล้อมปัจจุบัน การใช้คำนั้นอาจพบได้อย่างท่วมท้นก็จริง แต่ไม่ได้แปลว่าที่อื่นจะต้องเป็นเช่นนั้นเสมอไป
ในความหมายที่บริสุทธิ์ที่สุด เครื่องเสมือน ก็เป็นเพียงคอมพิวเตอร์ที่สร้างขึ้นมาเท่านั้น ไม่ได้สื่อโดยนัยว่าจะใช้ทำอะไรหรือทำงานอย่างไร
ในบทความก็ยกตัวอย่างการอีมูเลตคอนโซลคลาสสิก แต่จากนิยามที่ให้มา เห็นได้ชัดว่ายังมีเครื่องเสมือนที่เป็นไปได้อีกมากมาย
ประเด็นสำคัญคือเครื่องเสมือนเป็นแนวคิดเชิงนามธรรม และมีหลายประเภทมาก
ซิมูเลเตอร์ อีมูเลเตอร์ ไฮเปอร์ไวเซอร์ ฯลฯ ล้วนเป็นเครื่องเสมือน และยังมีเครื่องเสมือนรูปแบบแปลก ๆ ที่ยังไม่มีชื่อเรียกด้วย
ไม่ได้ตั้งใจจะพูดจาเสียมารยาท ตรงกันข้ามคือพยายามให้เกียรติ และอยากทำให้คำนี้ชัดเจนสำหรับคนที่กำลังเรียนรู้
“เครื่องเสมือน” เป็นคำที่ใช้กันทั่วไปกับซอฟต์แวร์ใด ๆ ที่รัน machine code หรือ bytecode ไม่ว่าจะด้วยเหตุผลใดก็ตาม
อาจรวมถึง virtualization ได้ แต่ก็มักใช้กับ language runtime ด้วย เช่น JVM ของ Java หรือ YARV (Yet Another Ruby VM) ของ Ruby
ที่จริงแล้ว พื้นที่ที่ไม่ค่อยได้ยินคำนี้บ่อยนักกลับเป็นด้านอีมูเลชัน ส่วนหนึ่งก็เพราะอีมูเลเตอร์สมัยใหม่ส่วนใหญ่เอนเอียงไปใช้เทคนิค dynamic recompilation กับซอฟต์แวร์เป้าหมาย แทนที่จะอีมูเลตทั้งระบบ
ถ้าเป็นระดับ Java ก็น่าจะถือได้ว่าเป็น “การใช้ที่พบกันอย่างท่วมท้น”