เรื่องแปลกที่ได้เรียนรู้ระหว่างเขียน x86 emulator
(timdbg.com)- ระหว่างเขียน CPU emulator สำหรับ Time Travel Debugging ขึ้นใหม่ด้วย C++ พบว่า x86/amd64 ต้องจัดการแม้แต่พฤติกรรมเดียวกันให้ต่างกันตาม encoding, prefix และโหมดการทำงาน
- มีรูปแบบแทนคำสั่งจำนวนมากที่ส่งผลต่อประสิทธิภาพและการดีบัก เช่น encoding แบบ single-byte
CCของint 3, รูปแบบสั้นของADD EAX, imm, และ REX prefix ที่ไม่มีผลใด ๆ - คำสั่ง
INC/DEC,CMPXCHG8B/CMPXCHG16B, และคำสั่ง shift/rotate จัดการ flags แบบไม่ตรงกับที่คาด ทำให้เกิด บั๊กใน emulator ได้ง่าย - shift count ไม่ได้ถูกใช้ตามขนาด operand ตรง ๆ แต่ถูก mask ไว้ ดังนั้น
shr eax,20hจึงไม่ได้ทำให้รีจิสเตอร์ 32 บิตกลายเป็น 0 แต่กลับ คงค่าเดิมไว้ - segment ยังถูกใช้ในการเข้าถึง TEB บน Windows แบบ 32 บิตและ 64 บิต และความต่างของความหมาย
FS/GSรวมถึงวิธีกำหนด base มีผลโดยตรงต่อการทำ disassembler และ emulator
กฎย่อยของ x86 ที่โผล่มาระหว่างการเขียน TTD emulator ใหม่
- องค์ประกอบหนึ่งของ Time Travel Debugging คือ CPU emulator ที่บันทึกการทำงานทั้งหมดของโปรเซสในระดับคำสั่ง
- emulator ของ iDNA รุ่นแรกเขียนเกือบทั้งหมดด้วยแอสเซมบลี จึงเร็วมาก แต่ดูแลรักษาและขยายต่อได้ยาก
- ในรุ่นที่สอง ส่วนของ emulation และส่วนอื่น ๆ เกือบทั้งหมดหลังจากนั้นถูกเขียนใหม่ด้วย C++ โดยตั้งเป้าจะคงประสิทธิภาพส่วนใหญ่ของเวอร์ชันแอสเซมบลีไว้ พร้อมกับทำให้ฐานโค้ดดูแลง่ายขึ้น
- การสร้าง CPU emulator ต้องทำรายละเอียดพฤติกรรมของ CPU ให้ตรงทุกจุด และแม้แต่กฎที่คนคุ้นเคยกับ x86 รู้สึกว่าคุ้นตาอยู่แล้ว ก็ต้องกลับมาตรวจสอบอีกครั้งเมื่อทำของจริง
x86 encoding ที่แทนคำสั่งเดียวกันได้หลายแบบ
- x86 สามารถแทนคำสั่งเดียวกันด้วยลำดับไบต์ได้หลายแบบ
int 3เข้ารหัสเป็นCD 03ได้ แต่ก็เข้ารหัสเป็นCCแบบ ไบต์เดียว ได้เช่นกัน- เพราะมันถูกใช้เป็น software breakpoint จึงสามารถวาง breakpoint ที่ตำแหน่งคำสั่งตรงท้ายหน้า memory ซึ่งหน้าถัดไปยังไม่ได้ถูกแมปได้
- ยังมี encoding ทางเลือกที่ทำให้กรณีที่พบบ่อยสั้นลง
add eax, immเขียนแบบสั้นได้เป็น05cccccccc- ถ้าจะบวกค่าเดียวกันให้
ECXจะต้องใช้81c1ccccccccซึ่งยาวกว่า 1 ไบต์
- การที่
EAXถูกเรียกว่า “Accumulator register” ไม่ใช่แค่ขนบ แต่ทำให้เกิดความต่างของ encoding จริง และคำสั่งที่สั้นลงอาจช่วยด้านประสิทธิภาพได้เพราะลดทั้งข้อมูลที่ต้องย้ายจากหน่วยความจำหลักและการใช้ instruction cache - คอมไพเลอร์สามารถใช้ encoding แบบสั้น เหล่านี้ได้เมื่อทำได้
prefix และข้อจำกัดความยาวคำสั่ง 15 ไบต์
- คำสั่ง x86 สามารถมี prefix byte ที่เปลี่ยนพฤติกรรมได้
- REX prefix ซึ่งใช้บ่อยในโค้ด 64 บิต ใช้เพื่อเข้าถึงช่วงรีจิสเตอร์ที่กว้างกว่าโค้ด 32 บิต
- CPU ยอมรับ REX prefix ที่ไม่มีผลใด ๆ ได้ด้วย
4004ccคือรูปแบบที่มี REX byte นำหน้าadd al,0CChแบบ 8 บิต แต่ในกรณีนี้ REX ไม่มีผลอะไร- แม้จะใส่ REX prefix สองตัว CPU ก็ยังรันได้ และ disassembler หลายตัวรวมถึง WinDbg อาจสับสนกับกรณีนี้
- บน CPU ที่เข้ากันได้กับ x86 ความยาวคำสั่งปัจจุบันมี hard limit อยู่ที่ 15 ไบต์
- คำสั่งที่ยาวเกิน 15 ไบต์จะถือเป็นคำสั่งไม่ถูกต้องและทำให้เกิด exception
- CPU รุ่นเก่ายังมีข้อจำกัดอื่นเกี่ยวกับ prefix ด้วย และบาง prefix เช่น
LOCKก็มีเงื่อนไขการใช้ที่เข้มงวดกว่า
การตีความที่เปลี่ยนไปตามขนาดแอดเดรสและโหมด
- Address override prefix สามารถทำให้ในโหมด 64 บิตอ้างถึง แอดเดรส 32 บิต ได้
488d0424คือlea rax,[rsp]67488d0424คือlea rax,[esp]เพราะมี prefix0x67
- ในโค้ด 32 บิต Address override จะเปลี่ยนโหมดแอดเดรสเป็น แอดเดรส 16 บิต
- แม้จะเป็นลำดับไบต์เดียวกัน ก็ต้องรู้ขนาด operand เริ่มต้นและขนาดแอดเดรสของ code segment จึงจะ disassemble หรือตีความได้ถูกต้อง
8b0424ในโหมด 32 บิตคือmov eax,dword ptr [esp]8b0424ในโหมด 64 บิตคือmov eax,dword ptr [rsp]
- ช่วง
40~4Fที่เคยใช้กับINC regและDEC regบน x86 ถูกนำไปใช้เป็น REX prefix bytes บน x64- ในโหมด 32 บิต
48 03 04 24ถูกตีความเป็นสองคำสั่งคือdec eaxและadd eax,dword ptr [esp] - ในโหมด 64 บิต
48030424ถูกตีความเป็นคำสั่งเดียวคือadd rax,qword ptr [rsp]
- ในโหมด 32 บิต
- ผู้ออกแบบ AMD64 นำพื้นที่ encoding ขนาดใหญ่ของ
INC/DECไปใช้กับ prefix ใหม่เพื่อขยายชุดรีจิสเตอร์ในโหมด 64 บิต โดยคำสั่งเหล่านั้นมี encoding แบบอื่นที่รองรับทั้งรีจิสเตอร์และหน่วยความจำอยู่แล้ว
กับดักของ WinDbg และ INC reg ในโหมด 64 บิต
- WinDbg ประกอบคำสั่งราวกับอยู่ในโหมด 32 บิตเสมอ ดังนั้นถ้าพยายาม assemble
INC regในโค้ด 64 บิต อาจได้ผลลัพธ์ไม่ตรงเจตนา - ในตัวอย่าง
inc eaxไม่ได้กลายเป็นคำสั่งเพิ่มค่าจริง แต่กลายเป็น REX prefix ที่ไร้ประโยชน์ ซึ่งไปขยายคำสั่งถัดไปแทน - ผลคือชุดไบต์นั้นไม่ได้ถูกตีความเป็น
incแต่เป็น prefix ที่อยู่หน้าคำสั่งjmp
ข้อยกเว้นของพฤติกรรม flags
INC EAXดูคล้ายADD EAX, 1แต่ไม่เหมือนกันทั้งหมดADDจะอัปเดต carry flagINCจะ ไม่อัปเดต carry flag
- ระหว่างทำ TTD emulator ผู้เขียนเคย implement จุดนี้ผิดในตอนแรก และเจอจาก unit test
- การคำนวณเชิงเลขและตรรกะส่วนใหญ่จะตั้งค่า overflow, sign, zero, auxiliary carry, parity และ carry flag
CMPXCHGก็ตั้งค่า flags เหล่านี้เช่นกัน แต่CMPXCHG8BและCMPXCHG16Bจะเปลี่ยนเฉพาะ zero flag เท่านั้น- คำสั่งบางตัวปล่อยบาง flags ให้อยู่ในสถานะไม่กำหนด
- คำสั่ง shift และ rotate จะปล่อย overflow flag เป็นสถานะไม่กำหนดเมื่อ shift amount มากกว่า 1
- พฤติกรรมจริงของ flags ที่ไม่กำหนดเกี่ยวข้องกับการ implement ภายในของการทำ shift และอาจต่างกันตามสถาปัตยกรรม
- มีเรื่องเล่าว่า CPU ตระกูล Atom ทำ bit shift ใน ALU ด้วยวิธีที่ถูกกว่าและช้ากว่า จึงทำให้ค่า flags ที่ไม่กำหนดออกมาต่างกัน แต่ผู้เขียนไม่ได้ทดสอบเอง
การ mask count ของคำสั่ง shift
66c1e810คือshr ax,10hและทำการ shiftAXไปทางขวา 16 บิต- เนื่องจาก
AXเป็นรีจิสเตอร์ 16 บิต ผลลัพธ์จึงกลายเป็น 0
- เนื่องจาก
c1e820คือshr eax,20hซึ่งดูเผิน ๆ เหมือนเป็นคำสั่ง shiftEAXไปทางขวา 32 บิต- แต่ความจริงแล้วค่า
EAXไม่เปลี่ยนเลย- ตาม Intel SDM ค่า count จะถูก mask ด้วย
1Fhทำให้ใช้เฉพาะ 5 บิตล่างของค่าที่ใช้ rotate/shift - ถ้าใช้ prefix
REX.Wmask จะกลายเป็น3Fhทำให้ค่า shift สูงสุดเป็น 63 บิต
- ตาม Intel SDM ค่า count จะถูก mask ด้วย
- ในการสัมภาษณ์งานที่ Microsoft เคยมีคำถามว่า “มีวิธีใดบ้างในการ clear รีจิสเตอร์ 32 บิตด้วยคำสั่งเดียว” และพฤติกรรมนี้เคยเป็นประเด็นจริง
- ผู้สัมภาษณ์คิดว่าน่าจะใช้ shift ได้ แต่คำตอบคือทำไม่ได้สำหรับรีจิสเตอร์ 32 บิต
segment ที่ยังมีชีวิตอยู่ในโค้ด 32 บิตและ 64 บิต
- หน่วยความจำแบบ segment อาจดูเหมือนของตกค้างจากโค้ด 16 บิต แต่ในโค้ด 32 บิตและ 64 บิตก็ยังมีผลจริง
- OS ส่วนใหญ่ใช้โมเดลหน่วยความจำที่เกือบเป็น flat และตั้ง segment base address เป็น 0 จึงมักไม่รู้สึกถึงมันในชีวิตประจำวัน
- ในโหมด 64 บิต CPU จะถือว่า segment base ของ
CS,DS,ES,SSเป็น 0 เสมอ
- ในโหมด 64 บิต CPU จะถือว่า segment base ของ
- ข้อยกเว้นคือ thread local storage ที่ใช้ extra segment register อย่าง
FSหรือGS - มีการแก้ไขเพิ่มเติมว่า base ของ segment
FS/GSสามารถอ่านได้จากโค้ดที่ไม่มีสิทธิพิเศษด้วยคำสั่งrdfsbase,wrfsbase,rdgsbase,wrgsbase- คำสั่งเหล่านี้ใช้ได้ตั้งแต่ Ivy Bridge หรือปี 2012 เป็นต้นมา
การเข้าถึง Windows TEB และ FS/GS
- บน Windows นั้น
FSและGSถูกใช้เพื่ออ้างถึง TEB (Thread Execution Block) - โครงสร้าง TEB มี self pointer ที่ชี้ไปยัง flat address ของจุดเริ่มต้นโครงสร้าง และแอดเดรสนี้ก็คือ base ของ segment นั้นด้วย
- ในโปรเซส 32 บิต TEB อยู่ที่
FSGetLastErrorจะดึงTEB.NtTib.Selfจากfs:[00000018h]แล้วอ่านLastErrorValueจาก[eax+34h]
- ในโปรเซส 64 บิต TEB อยู่ที่
GSGetLastErrorจะอ่าน pointer จากgs:[30h]แล้วดึงค่าจาก[rax+68h]
- โปรเซส 32 บิตที่รันบน OS 64 บิตจะมีทั้ง TEB แบบ 32 บิตและ 64 บิต และมีบริบทที่การเข้าถึงทั้งสองอันมีประโยชน์ เช่น WOW code แบบ 64 บิตที่รันอยู่ภายในโปรเซส 32 บิต
วิธีตัดสิน segment base ก็แตกต่างกันตามโหมด
- การตั้งค่าของ CPU ที่ใช้กำหนด base address ของ
FSและGSต่างกันระหว่างโหมด 32 บิตกับโหมด 64 บิต - ในโหมด 32 บิต ค่าจริงของ segment register จะอ้างถึง segment descriptor ที่นิยามไว้ใน Global Descriptor Table และ Local Descriptor Table
- ในโหมด 64 บิต base จะถูกควบคุมด้วย MSR สองตัว
FS Base, หรือIA32_FS_BASEตาม Intel SDMGS Base, หรือIA32_GS_BASEตาม Intel SDM
- ด้วยโครงสร้างนี้ ค่าของรีจิสเตอร์
FSและGSเองจึงไม่สำคัญในโหมด 64 บิต- สิ่งที่สำคัญคือ segment override prefix
- ระหว่างดีบักโปรเซส 32 บิตใน WinDbg สามารถใช้ค่ารีจิสเตอร์
FSเพื่อ dump เนื้อหาของ “FS segment” ได้ - แต่ในโปรเซส 64 บิต วิธีเดียวกันใช้ไม่ได้ และ segment override prefix มีความหมายมากกว่าค่า segment เอง
บทเรียนเชิงปฏิบัติสำหรับคนทำ emulator
- การสร้าง x86 emulator ทำให้ต้องรับมืออย่างละเอียดกับ พฤติกรรมจริงของ CPU เช่น instruction encoding, prefix, flags, shift count และ segment
- กฎเหล่านี้จำนวนมากแทบไม่มีประโยชน์กับการเขียนโค้ดทั่วไป แต่เป็นข้อกำหนดโดยตรงสำหรับการ implement emulator
- ผู้เขียนได้เรียนรู้หลายอย่างจากการลองผิดลองถูกและการมี mentor รวมถึงแนะนำ Darek Mihocka กับ emulators.com ซึ่งมีประสบการณ์กับ emulator รุ่นเก่าอย่างมาก
- หากสนใจการ optimize บน x86 และพฤติกรรมระดับล่างของระบบ เอกสารใน Agner Fog’s website มีประโยชน์มาก
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
แถม BSF/BSR ก็มีจุดแปลกด้วย Intel SDM ระบุว่าถ้าอินพุตเป็น 0 ค่าปลายทางจะไม่ถูกกำหนด แต่ AMD ระบุไว้ในเอกสารว่าในกรณีนั้นปลายทางจะไม่ถูกแก้ไข
แต่ glibc ใช้ข้อเท็จจริงที่ไม่ได้ระบุในเอกสารว่าแม้บน Intel ปลายทางก็ไม่ถูกแก้ไขเช่นกัน [1] เรื่องนี้ทำให้ผมใช้เวลาค่อนข้างนานในการหาสาเหตุปัญหาใน binary translator ของผม
อีกอย่าง TZCNT/LZCNT เป็น encoding ของ BSF/BSR ที่มี prefix F3 แต่บนโปรเซสเซอร์รุ่นเก่าที่ไม่รองรับส่วนขยายนี้ prefix จะถูกเพิกเฉยไปเงียบ ๆ ดังนั้นโค้ดเดียวกันจึงทำงานต่างกันตาม CPU แต่ อย่างน้อยเรื่องนี้ก็มีระบุในเอกสาร
ในด้าน encoding คนมักด่า prefix กันบ่อย แต่ส่วนตัวผมคิดว่านั่นยังไม่ใช่ส่วนที่แย่ที่สุด มันเป็นที่รู้กันดีและมีเอกสารอยู่พอสมควร ยังมีจุดแปลกที่แย่กว่านั้นอีก เช่น บิตขยาย REX/VEX/EVEX.RXB ถ้าไม่เกี่ยวกับสิ่งที่จะใช้ก็จะถูกเพิกเฉย แต่กับ mask register k0-k7 จะทำให้เกิด #UD ทว่าเมื่อรีจิสเตอร์ถูก encode อยู่ใน ModRM.rm บิตขยายกลับถูกเพิกเฉยอีก
APX ยกระดับความแปลกขึ้นไปอีกขั้น prefix REX2 สามารถ encode รีจิสเตอร์ทั่วไป r16-r31 ได้ แต่ encode xmm16-xmm31 ไม่ได้ ส่วน prefix EVEX มี layout หลายแบบขึ้นกับ opcode และบิตขยายที่ใช้กับรีจิสเตอร์ก็แตกต่างกันตามชนิดของรีจิสเตอร์ด้วย รีจิสเตอร์ XMM ใช้ X3:B3:rm และ V4:X3:idx ส่วนรีจิสเตอร์ทั่วไปใช้ B4:B3:rm และ X4:X3:idx ผ่านมา 1 ปีแล้วแต่ผมยังทำ APX decoder ไม่เสร็จ จึงให้รายการทั้งหมดไม่ได้
[1]: https://sourceware.org/bugzilla/show_bug.cgi?id=31748
สำหรับ EVEX ผมตั้งใจจะเก็บบิตดิบไว้จนกว่าจะอ่าน opcode และระบุคลาสของ EVEX ได้ กล่าวคือวางแผนจะเก็บไว้ถึงขั้นก่อน immediate และน่าจะก่อน ModRM
decoder ของผมโดยรวมอิงจากตารางในคู่มือ และโค้ดก็ถือว่าโอเคเป็นส่วนใหญ่ การเยื้องไม่ได้มากเกินไป และขั้นตอนต่าง ๆ ส่วนใหญ่ก็แยกกันหรือระบุได้ง่าย เนื่องจากเอาต์พุตเป็นโค้ด JIT จึงไม่จำเป็นต้องมีประสิทธิภาพสูงมาก อ่านง่ายไว้ก็พอ และนี่ก็ไม่ใช่จุดที่ใช้เวลาส่วนใหญ่ด้วย
ถึงอย่างนั้นก็มีหลายกรณีที่คู่มือผิดหรือไม่ได้บอกเนื้อหาทั้งหมด ตารางก็ไม่ได้อัปเดตมาหลายปีแล้ว เช่น ไม่มีคำสั่งของ K register ด้วยซ้ำ ต่อไปคงต้องทำด้วยมือมากขึ้น
คอมเมนต์ด้านบนสุดอธิบายสถานการณ์ไว้นิดหน่อย: https://github.com/qemu/qemu/blob/59084feb256c617063e0dbe7e6...
อย่างที่พูดไปข้างต้น ยังมีคำสั่งอีกไม่กี่ตัวที่โค้ดเก่ายังเป็นผู้จัดการ โดยเฉพาะ BT/BTS/BTR/BTC โค้ดเขียนไว้แล้วแต่ยังไม่ได้ merge
แต่มี prefix 0x66 ที่ใช้สลับโหมด 16 บิตกับ 32 บิตอยู่ ถ้านำมันไปใช้กับ BSWAP EAX จะเกิดเรื่องแปลกที่ไม่ได้ถูกกำหนดไว้
ในสถาปัตยกรรม CPU บางแบบ prefix จะถูกเพิกเฉยไปเหมือนความต่างระหว่าง Intel กับ AMD ส่วนบางแบบจะทำพฤติกรรมที่ผมเรียกว่า “internal swap” เช่น ในสี่ไบต์ที่เก็บอยู่ใน EAX ไบต์ที่ 1 กับไบต์ที่ 2 จะสลับกัน
0x11223344 จะกลายเป็น 0x11332244
เมื่อก่อน x86 เคยเป็นคูเมืองของ Intel แต่ตอนนี้ดูเหมือนภาระฝันร้ายที่ต้องแบกต่อไป
มีฟังก์ชันชื่อ clz() อยู่ก็จริง แต่ถ้า LZCNT เป็นแค่ BSR ที่ต่างกันเฉพาะ semantics ของอินพุต 0 การเพิ่มการลบอีกครั้งในการ implementation เพื่อให้ได้ compatibility ก็น่าจะเป็นต้นทุนเล็กน้อย
ถ้านำโปรแกรมเดียวกันไปรันบน CPU ที่ emulate ด้วย แล้วตรวจสอบว่าทุกคำสั่งมีสถานะเหมือนกันหรือไม่ ก็น่าจะทดสอบได้ว่า emulator จำลองฮาร์ดแวร์ได้สมบูรณ์แบบหรือเปล่า
เป็นคนที่เจ๋งมาก การเขียน assembly ให้ความรู้สึกเรียบง่าย และผมก็ชอบสุนทรียะของมันที่เรียงลงมาเป็นแนวตั้ง
ประสบการณ์ที่ใกล้เคียงกับงานแบบ OP แม้เพียงเล็กน้อยของผม คือการพยายามอธิบาย stack ให้เพื่อนที่ทำ JS เข้าใจ จนลงเอยด้วยการช่วยกันสร้าง mini VM ที่มี ISA เล็ก ๆ: https://gist.github.com/darighost/2d880fe27510e0c90f75680bfe...
ตอนนั้นเราน่าจะขุดลึกลงไปได้อีก และผมก็อยากให้เป็นอย่างนั้น แต่คงจะหลุดจากเป้าหมายการสอนเดิมไป คงต้องลองติดต่อเพื่อนคนนั้นดูว่ายังอยากเรียนด้วยกันอยู่ไหม เพื่อนหาเงินได้เยอะจากงานเว็บสุดเจ๋งเลยไม่มีเวลาลงลึก ส่วนผมเป็นคนว่างงาน เวลาและพลังงานจึงแทบเหมือนทะเลที่ไม่มีที่สิ้นสุด เลยไม่ง่ายนัก
Justine Tunney กับ emulator ของเธอก็น่าดูเหมือนกัน https://justine.lol/blinkenlights/
เอกสารพาให้ได้สำรวจอย่างยอดเยี่ยมว่า CPU ทำงานอย่างไร
การถกเถียงก่อนหน้านี้อยู่ที่นี่: https://news.ycombinator.com/item?id=34636699
ไม่น่าเชื่อว่าเวลาผ่านไป 16 เดือนแล้ว เวลาผ่านไปเร็วจริง ๆ
ไม่เห็นด้วยอย่างแรงกับคำกล่าวที่ว่า “การเขียน CPU emulator คือวิธีที่ดีที่สุดในการเข้าใจการทำงานของ CPU อย่างแท้จริง”
วิธีที่ดีที่สุดคือ สร้าง CPU ในระดับเกต เหมือนที่ทำกันในวิชาวิทยาการคอมพิวเตอร์ดี ๆ การสร้าง ARM แบบย่อส่วนตั้งแต่ต้นสนุกมากจริง ๆ
การสร้าง emulator ของ CPU สมัยใหม่เป็นความท้าทายที่เข้าถึงได้มากกว่า และแม้ทำงานได้แค่บางส่วนก็ยังให้ผลด้านการเรียนรู้สูงมาก
วิศวกรซอฟต์แวร์ส่วนใหญ่มีความเข้าใจเกี่ยวกับ data hazard, การทำให้แคชเป็นโมฆะ และ pipeline stall ที่ไม่สมบูรณ์
แต่เมื่อเริ่มสร้าง CPU แบบนั้นเพื่อใช้กับงานที่มีความหมาย รายละเอียดพวกนั้นก็เริ่มน่าสนใจน้อยลง ตอนนั้นความซับซ้อนของพฤติกรรมและสเปกจะน่าสนใจกว่า และแนวทาง emulator ก็จัดการได้ง่ายกว่าและครอบคลุมชนิดของพฤติกรรมได้มากกว่า
เคยเขียน emulator ความเร็วสูงให้สถาปัตยกรรมที่ไม่ใช่ของเล่นมาราว 12 แบบ และยังทำ JIT translator อยู่บ้าง แต่ x86 ยังทำให้ PTSD อยู่ดี ไม่เคยเห็นสถาปัตยกรรมที่รกขนาดนี้มาก่อน มันมีประวัติศาสตร์และเหตุผลของมัน แต่ก็ยังหนักมากอยู่ดี
ช่วงหลังผมทำส่วนใหญ่ของ x86-64 decoder เป็นโปรเจกต์ข้างเคียง [1] และค่อนข้างประหลาดใจว่ามันซับซ้อนขึ้นมากในยุคนี้ สำหรับเป้าหมายของผม Sandpile.org [2] มีประโยชน์มากจริง ๆ
[1] พูดให้ตรงคือเป็นเวอร์ชัน x86-64 ของ disfilter ของ Fabian Giesen ที่ทำไว้เพื่อโปรเจกต์ข้างเคียงอีกตัวซึ่งยังไม่ได้เปิดเผย: https://gist.github.com/lifthrasiir/df47509caac2f065032ef72e...
[2] https://sandpile.org/
68k disassembler ที่ทำตอนมหาวิทยาลัยให้ความรู้สึกเหมือนฉากที่ Neo พูดว่า “ผมรู้กังฟูแล้ว” มันเป็นชิ้นส่วนเชื่อมโยงที่ขาดหายไป ตั้งแต่ภาษาระดับสูงลงไปถึงทรานซิสเตอร์ แล้วกลับมาย้อนคิดเหตุผลเกี่ยวกับโค้ดได้อีกครั้ง
ถ้าเขียน emulator เต็มตัว น่าจะให้ผลมากกว่านั้นอีกหนึ่งลำดับขั้น บทความดีมาก
ดูเหมือนว่าความจำผมจะผิด ผมจำได้ว่า variant ของ salsa20 กับ machine code เดิมอยู่ที่ cryp.to แต่เว็บไซต์ของ Dan Bernstein คือ https://cr.yp.to/
สมัยที่ตรวจสอบการเข้ารหัสข้อมูลที่จัดเก็บ, การเข้ารหัสแบบสตรีมมิง ฯลฯ ในสตาร์ตอัป หน้าของ Dan มี implementation หลายตัวแยกตามชิปเซ็ตเป้าหมายและชุดคำสั่ง สิ่งเหล่านั้นถูก cross-compile มาจากรูปแบบ assembler ของเขา
ตอนลองใช้ VM ก็น่าสนใจที่ได้เห็นว่าชุดคำสั่งใดบ้างที่รองรับในช่วงต้นถึงกลางทศวรรษ 2000 ระหว่างทดสอบ บางครั้งก็เกิดปัญหาเพราะ VM อ้างว่ารองรับ แต่ implementation ยังไม่รองรับครบจริง ๆ
น่าสนใจที่ตรงนี้มีคนพูดกันเยอะว่า x86 assembly เจ็บปวดแค่ไหนเมื่อเทียบกับ RISC แต่ผมมีปัญหาตรงข้ามตอนแยกโค้ดกลับเป็น object file
สำหรับงานแบบนี้ การวิเคราะห์ x86 ง่ายมาก ส่วน MIPS นั้นเหมือนฝันร้าย เหตุผลหลักคือสิ่งที่ต้องสนใจคือ reference ไปยังโค้ดและข้อมูล x86 มี immediate constant ขนาดพอยน์เตอร์ ส่วน MIPS มีคู่ relocation HI16/LO16 ซึ่งไปพันกับกราฟการใช้รีจิสเตอร์, control flow ของโค้ด และคำสั่ง branch delay จนเกิดปัญหาสารพัด
แต่นั่นไม่ได้แปลว่าผมกำลังชม x86 นะ
สิ่งสำคัญที่สุดที่ได้เรียนจากการเปรียบเทียบ x86 assembly กับ C คือ signed/unsigned ไม่ได้เป็นชนิดข้อมูล แต่เป็นคุณสมบัติของ operation
ถ้าใช้ประโยชน์จาก flag ได้ก็คงดี และในสถาปัตยกรรมบางตัวอย่าง PPC หรือ armv7 ก็ทำได้ง่ายกว่า แต่ x86 เขียนทับ flag ง่ายเกินไปจนยากมากที่จะเอาค่าเหล่านั้นมาใช้