x86 एमुलेटर लिखते समय सीखी गई अजीब बातें
(timdbg.com)- Time Travel Debugging के लिए CPU एमुलेटर को C++ में दोबारा लिखने की प्रक्रिया में, x86/amd64 में एक ही व्यवहार को भी encoding, prefix और execution mode के अनुसार अलग-अलग तरीके से handle करना पड़ता है
int 3कीCCsingle-byte encoding,ADD EAX, immका छोटा रूप, और असर न डालने वाले REX prefix जैसी कई वैकल्पिक representations हैं, जो performance और debugging को प्रभावित करती हैंINC/DEC,CMPXCHG8B/CMPXCHG16B, shift/rotate instructions में flags handling intuition से अलग होती है, इसलिए ये आसानी से एमुलेटर bugs तक ले जा सकती हैं- shift count को operand size के हिसाब से सीधे लागू नहीं किया जाता, बल्कि mask किया जाता है; इसलिए
shr eax,20h32-bit register को 0 नहीं बनाता, बल्कि value बनाए रखता है - segments अब भी 32-bit और 64-bit Windows में TEB access के लिए इस्तेमाल होते हैं, और
FS/GSके meaning व base तय करने के तरीके का फर्क disassembler और emulator implementation पर सीधा असर डालता है
TTD एमुलेटर को दोबारा लिखते समय सामने आए x86 के सूक्ष्म नियम
- Time Travel Debugging का एक component CPU एमुलेटर है, जो process execution को पूरी तरह instruction level पर record करता है
- पहले version iDNA का emulator लगभग पूरी तरह assembly में लिखा गया था, इसलिए तेज था, लेकिन maintain और extend करना मुश्किल था
- दूसरे version में emulation वाले हिस्से और बाद में अधिकांश अन्य हिस्सों को C++ में फिर से लिखा गया, लक्ष्य था assembly version की अधिकतर performance बनाए रखते हुए ऐसा codebase बनाना जिसे manage करना आसान हो
- CPU emulator बनाने के लिए CPU behavior की हर detail को सही से match करना पड़ता है, और x86 का अनुभव रखने वालों को familiar rules भी actual implementation में फिर से verify करने पड़ते हैं
एक ही instruction को कई तरीकों से व्यक्त करने वाली x86 encoding
- x86 में एक ही instruction को कई byte sequences से व्यक्त किया जा सकता है
int 3कोCD 03के रूप में भी encode किया जा सकता है, लेकिन single byteCCके रूप में भी encode किया जा सकता है- क्योंकि इसका उपयोग software breakpoint के तौर पर होता है, इसलिए memory page के आखिर में मौजूद instruction position पर भी breakpoint लगाया जा सकता है, भले ही अगला page mapped न हो
- आम cases को छोटा बनाने के लिए alternative encodings भी हैं
add eax, immको05ccccccccजैसे छोटे रूप में व्यक्त किया जा सकता है- वही value
ECXमें जोड़ने के लिए81c1ccccccccजैसा 1 byte ज्यादा चाहिए
EAXको “Accumulator register” कहा जाना सिर्फ convention नहीं है; इससे वास्तविक encoding difference बनता है, और छोटे instructions main memory से लाए जाने वाले data व instruction cache usage को घटाकर performance में मदद कर सकते हैं- compiler संभव होने पर ऐसी short encodings का इस्तेमाल कर सकता है
prefix और 15-byte instruction length limit
- x86 instructions में behavior बदलने वाले prefix bytes हो सकते हैं
- 64-bit code में आम तौर पर इस्तेमाल होने वाला REX prefix 32-bit code की तुलना में बड़े register range तक access के लिए इस्तेमाल होता है
- CPU ऐसे REX prefix भी स्वीकार करता है जिनका कोई असर नहीं होता
4004cc, 8-bitadd al,0CChके आगे REX byte लगा हुआ रूप है, लेकिन इस case में REX का कोई effect नहीं है- दो REX prefix लगाने पर भी CPU execute कर सकता है, और WinDbg सहित कई disassemblers इससे confuse हो सकते हैं
- x86-compatible CPU में current instruction length की 15 bytes hard limit है
- 15 bytes से लंबे instructions को invalid instruction माना जाता है और वे exception raise करते हैं
- पुराने CPUs में prefixes को लेकर अन्य limits भी हैं, और
LOCKprefix जैसे कुछ prefixes पर इस्तेमाल की conditions ज्यादा strict होती हैं
address size और mode के अनुसार बदलती interpretation
- Address override prefix, 64-bit mode में 32-bit address refer करवाने के लिए इस्तेमाल हो सकता है
488d0424हैlea rax,[rsp]67488d0424,0x67prefix की वजह सेlea rax,[esp]है
- 32-bit code में Address override, address mode को 16-bit address में बदल देता है
- एक ही byte sequence को सही से disassemble या interpret करने के लिए code segment का default operand size और address size जानना जरूरी है
- 32-bit mode में
8b0424हैmov eax,dword ptr [esp] - 64-bit mode में
8b0424हैmov eax,dword ptr [rsp]
- 32-bit mode में
- x86 में
INC regऔरDEC regके लिए इस्तेमाल होने वाली40~4Frange x64 में REX prefix bytes के रूप में इस्तेमाल होती है- 32-bit mode में
48 03 04 24,dec eaxऔरadd eax,dword ptr [esp]दो instructions के रूप में interpret होता है - 64-bit mode में
48030424,add rax,qword ptr [rsp]एक instruction के रूप में interpret होता है
- 32-bit mode में
- AMD64 designers ने 64-bit mode में register set expand करने के लिए
INC/DECकी बड़ी encoding space को नए prefix के लिए इस्तेमाल किया, और उन instructions के लिए पहले से दूसरी encoding मौजूद थी जो registers और memory दोनों को support करती थी
WinDbg और 64-bit mode में INC reg का trap
- WinDbg instructions को हमेशा 32-bit mode की तरह assemble करता है, इसलिए 64-bit code में
INC regassemble करने की कोशिश करने पर intended result से अलग output आ सकता है - example में
inc eaxactual increment instruction नहीं रहता, बल्कि अगली instruction को modify करने वाला बेकार REX prefix बन जाता है - परिणामस्वरूप वह byte sequence
incनहीं, बल्किjmpinstruction के आगे लगे prefix के रूप में interpret होता है
flags behavior के exceptions
INC EAX,ADD EAX, 1जैसा दिखता है, लेकिन पूरी तरह समान नहीं हैADDcarry flag को update करता हैINCcarry flag को update नहीं करता
- TTD emulator implementation के दौरान इस difference को शुरुआत में गलत implement किया गया था, और unit test ने इसे पकड़ लिया
- ज्यादातर arithmetic/logical operations overflow, sign, zero, auxiliary carry, parity, carry flag set करते हैं
CMPXCHGभी ये flags set करता है, लेकिनCMPXCHG8BऔरCMPXCHG16Bसिर्फ zero flag modify करते हैं- कुछ instructions flags के कुछ हिस्से undefined state में छोड़ देते हैं
- shift और rotate instructions में shift amount 1 से ज्यादा हो तो overflow flag undefined state में रहता है
- undefined flags का actual behavior shift operation की internal implementation से जुड़ा होता है, और architecture के हिसाब से अलग हो सकता है
- ऐसा कहा जाता है कि Atom family CPU, ALU में bit shift को सस्ते और धीमे तरीके से perform करते हैं, जिससे undefined flag values अलग हो जाती हैं, लेकिन इसे सीधे test नहीं किया गया
shift instruction की count masking
66c1e810हैshr ax,10h, और यहAXको 16 bits right shift करता हैAX16-bit register है, इसलिए result 0 हो जाता है
c1e820हैshr eax,20h, और सतह पर यहEAXको 32 bits right shift करने वाली instruction जैसा दिखता है- असल में
EAXvalue बदलती नहीं है- Intel SDM के अनुसार count को
1Fhसे mask किया जाता है, और rotation के lower 5 bits ही इस्तेमाल होते हैं REX.Wprefix इस्तेमाल करने पर mask3Fhहो जाता है और maximum shift value 63 bits हो जाती है
- Intel SDM के अनुसार count को
- Microsoft interview में “32-bit register को single instruction से clear करने के सभी तरीके” पूछने वाले सवाल में यह behavior सचमुच मुद्दा बना था
- interviewer को लगता था कि shift से यह संभव है, लेकिन जवाब दिया गया कि 32-bit register के लिए यह संभव नहीं है
32-bit और 64-bit code में अब भी जीवित segments
- segment memory 16-bit code की विरासत जैसी लग सकती है, लेकिन 32-bit और 64-bit code में भी इसका real effect है
- ज्यादातर OS लगभग flat memory model इस्तेमाल करते हैं और segment base address को 0 रखते हैं, इसलिए सामान्यतः हम इसे notice नहीं करते
- 64-bit mode में CPU
CS,DS,ES,SSके segment base को हमेशा 0 मानता है
- 64-bit mode में CPU
- अपवाद के रूप में thread local storage के लिए
FSयाGSजैसे extra segment registers इस्तेमाल होते हैं - correction के तौर पर,
FS/GSsegment का base non-privileged code में भीrdfsbase,wrfsbase,rdgsbase,wrgsbaseinstructions से पढ़ा जा सकता है- ये instructions Ivy Bridge यानी 2012 से उपलब्ध हैं
Windows TEB access और FS/GS
- Windows में
FSऔरGSका उपयोग TEB(Thread Execution Block) को refer करने के लिए होता है - TEB struct में एक self pointer होता है, जो struct की शुरुआत के flat address की ओर point करता है, और यह address उस segment का base भी होता है
- 32-bit process में TEB
FSसे locate होता हैGetLastError,fs:[00000018h]सेTEB.NtTib.Selfलाता है, फिर[eax+34h]सेLastErrorValueपढ़ता है
- 64-bit process में TEB
GSसे locate होता हैGetLastError,gs:[30h]से pointer पढ़ता है, और[rax+68h]से value लाता है
- 64-bit OS पर चलने वाले 32-bit process के पास 32-bit TEB और 64-bit TEB दोनों होते हैं, और 32-bit process के अंदर चलने वाले 64-bit WOW code जैसे cases में दोनों TEB तक access करना उपयोगी context हो सकता है
segment base तय करने का तरीका भी mode के हिसाब से अलग
FSऔरGSका base address तय करने वाली CPU settings 32-bit mode और 64-bit mode में अलग होती हैं- 32-bit mode में segment register की actual value Global Descriptor Table और Local Descriptor Table में defined segment descriptor को refer करती है
- 64-bit mode में base दो MSR से control होता है
FS Base, Intel SDM काIA32_FS_BASEGS Base, Intel SDM काIA32_GS_BASE
- इस structure की वजह से 64-bit mode में
FSऔरGSकी actual register value खुद महत्वपूर्ण नहीं होती- महत्वपूर्ण चीज segment override prefix है
- WinDbg में 32-bit process debug करते समय
FSregister value का उपयोग करके “FS segment” की contents dump की जा सकती हैं - 64-bit process में यही तरीका काम नहीं करता, और segment value की तुलना में segment override prefix मायने रखता है
emulator implementer के लिए व्यावहारिक सबक
- x86 emulator बनाते समय instruction encoding, prefixes, flags, shift count और segments जैसे CPU के वास्तविक behavior को बहुत बारीकी से handle करना पड़ता है
- इन rules में से काफी कुछ normal code लिखने में लगभग बेकार है, लेकिन emulator implementation के लिए ये direct requirements बन जाते हैं
- trial-and-error और mentoring से बहुत कुछ सीखा गया, और पुराने emulator experience वाले Darek Mihocka व emulators.com का भी उल्लेख है
- x86 optimization और low-level behavior में रुचि हो तो Agner Fog’s website के resources उपयोगी हैं
1 टिप्पणियां
Hacker News की राय
बोनस के तौर पर BSF/BSR में भी अजीब बातें हैं। Intel SDM कहता है कि अगर input 0 हो तो destination value undefined होती है, लेकिन AMD ने दस्तावेज़ में लिखा है कि उस स्थिति में destination बदला नहीं जाता
लेकिन glibc Intel पर भी इस undocumented तथ्य को ज्यों का त्यों इस्तेमाल करता है कि destination बदला नहीं जाता [1]। इसकी वजह से मेरे binary translator में समस्या की जड़ ढूँढने में काफी समय लगा
साथ ही TZCNT/LZCNT, F3 prefix के साथ BSF/BSR encoding हैं, और पुराने processors पर जो इस extension को support नहीं करते, prefix चुपचाप ignore हो जाता है। इसलिए वही code CPU के हिसाब से अलग तरह से behave करता है, हालांकि कम से कम यह दस्तावेज़ में लिखा हुआ है
encoding में लोग prefixes को अक्सर कोसते हैं, लेकिन निजी तौर पर मुझे वह सबसे बुरा नहीं लगता। वे जाने-पहचाने हैं और कुछ हद तक documented भी हैं। इससे भी खराब अजीब बातें हैं। उदाहरण के लिए REX/VEX/EVEX.RXB extension bits अगर लागू न हों तो ignore हो जाते हैं, लेकिन mask registers k0-k7 में #UD trigger करते हैं। लेकिन अगर register ModRM.rm में encoded हो तो extension bits फिर ignore हो जाते हैं
APX अजीबपन का स्तर एक और पायदान ऊपर ले जाता है। REX2 prefix general-purpose registers r16-r31 को encode कर सकता है, लेकिन xmm16-xmm31 को नहीं, और EVEX prefix में opcode के हिसाब से कई layouts हैं, तथा registers के लिए इस्तेमाल होने वाले extension bits भी register के type के हिसाब से बदलते हैं। XMM registers X3:B3:rm और V4:X3:idx का इस्तेमाल करते हैं, और general-purpose registers B4:B3:rm और X4:X3:idx का इस्तेमाल करते हैं। 1 साल हो गया, फिर भी मैं अभी तक APX decoder पूरा नहीं कर पाया हूँ, इसलिए पूरी सूची नहीं दे सकता
[1]: https://sourceware.org/bugzilla/show_bug.cgi?id=31748
EVEX के लिए मेरा plan है कि opcode पढ़कर EVEX class identify करने तक raw bits बनाए रखे जाएँ। यानी immediate value से पहले, शायद ModRM से पहले तक preserve करने की योजना है
मेरा decoder ज्यादातर manual की tables पर आधारित है, और code कुल मिलाकर ठीक है। indentation बहुत ज्यादा नहीं है और stages भी अधिकतर अलग हैं या identify करना आसान है। output JIT code है, इसलिए बहुत efficient होना जरूरी नहीं; readable होना ठीक है। ज्यादा समय भी यहाँ खर्च नहीं होता
फिर भी कई बार manual गलत होता है या पूरी बात नहीं बताता। tables भी कई सालों से update नहीं हुई हैं, उदाहरण के लिए K register instructions भी नहीं हैं। आगे manual work ज्यादा करना पड़ेगा लगता है
सबसे ऊपर की comment में स्थिति थोड़ी समझाई गई है: https://github.com/qemu/qemu/blob/59084feb256c617063e0dbe7e6...
ऊपर जैसा कहा, अभी भी कुछ instructions हैं जिन्हें पुराना code handle करता है, खासकर BT/BTS/BTR/BTC। code लिखा जा चुका है, लेकिन अभी merge नहीं किया है
लेकिन 16-bit और 32-bit modes के बीच switch करने वाला 0x66 prefix है। इसे BSWAP EAX पर apply करने से undefined अजीब चीजें होती हैं
कुछ CPU architectures में, Intel और AMD के फर्क की तरह, prefix बस ignore हो गया, और कुछ जगहों पर वह behavior हुआ जिसे मैं “internal swap” कहता हूँ। उदाहरण के लिए EAX में stored चार bytes में से byte 1 और byte 2 आपस में बदल जाते हैं
0x11223344, 0x11332244 बन जाता है
पहले x86 Intel की moat था, लेकिन अब यह ढोने लायक किसी nightmare burden जैसा लगता है
clz() नाम का function मौजूद तो है, लेकिन अगर LZCNT बस 0 input semantics में अलग BSR होता, तो compatibility पाने के लिए implementation में extra subtraction करना शायद छोटी कीमत होती
उसी program को emulated CPU पर भी चलाकर हर instruction पर state समान है या नहीं verify करें, तो test किया जा सकता है कि emulator hardware की हूबहू नकल करता है या नहीं
कमाल का इंसान है। assembly लिखना सरल लगता है, और मुझे उसकी vertically arranged aesthetics भी पसंद है
OP जैसे काम के थोड़ा भी करीब मेरा अनुभव बस इतना था कि JS करने वाले एक दोस्त को stack समझाने की कोशिश में हमने साथ मिलकर एक छोटी ISA वाली mini VM बनाई थी: https://gist.github.com/darighost/2d880fe27510e0c90f75680bfe...
और गहराई में जा सकते थे और काश मैं गया होता, लेकिन तब शायद मूल educational purpose से भटक जाते। उस दोस्त से पूछना चाहिए कि क्या वह अभी भी साथ में पढ़ना चाहता है। वह शानदार web development से खूब पैसा कमा रहा है, इसलिए उसके पास गहराई में जाने का समय नहीं है, और मैं बेरोजगार हूँ, इसलिए मेरे पास समय और energy लगभग अनंत समुद्र जैसी है—इसलिए आसान नहीं है
Justine Tunney और उनका emulator भी देखने लायक है। https://justine.lol/blinkenlights/
documentation CPU कैसे काम करता है, इसका बेहतरीन tour कराती है
पिछली चर्चा यहाँ है: https://news.ycombinator.com/item?id=34636699
यकीन नहीं होता कि 16 महीने गुजर चुके हैं। समय सचमुच बहुत तेज़ भागता है
इस बात से मैं काफ़ी असहमत हूँ कि “CPU emulator लिखना CPU के कामकाज को सच में समझने का सबसे अच्छा तरीका है”
सबसे अच्छा तरीका वही है जो किसी अच्छी computer science class में कराया जाता है: gate level पर CPU बनाना। शुरू से एक छोटा ARM बनाना वाकई मज़ेदार काम था
आधुनिक CPU emulator बनाना थोड़ा ज़्यादा accessible challenge है, और उसका कुछ हिस्सा भी चल जाए तो भी सीखने का असर बहुत बड़ा होता है
ज़्यादातर software engineers को data hazards, cache invalidation, और pipeline stalls की समझ अधूरी होती है
लेकिन जब आप उसी तरह ऐसा CPU बनाना शुरू करते हैं जिसे किसी meaningful काम में इस्तेमाल करना चाहें, तो उन details में रुचि कम होने लगती है। तब behavior और specification की complexity ज़्यादा दिलचस्प लगती है, और emulator approach ज़्यादा manageable होती है और ज़्यादा तरह के behavior cover कर सकती है
मैंने non-toy architectures के करीब 12 fast emulators लिखे हैं, और कुछ JIT translators भी बनाए हैं, लेकिन x86 अभी भी PTSD देता है। इतनी messy architecture मैंने कभी नहीं देखी। इसका इतिहास है और वजहें भी हैं, फिर भी हालत बहुत खराब है
हाल ही में एक side project के तौर पर मैंने x86-64 decoder का बड़ा हिस्सा implement किया [1], और यह देखकर काफ़ी हैरानी हुई कि आजकल यह और भी complex हो गया है। मेरे उद्देश्य के लिए Sandpile.org [2] बहुत उपयोगी रहा
[1] सटीक तौर पर, Fabian Giesen के disfilter का x86-64 version, जो अभी public न हुए एक और side project के लिए बनाया था: https://gist.github.com/lifthrasiir/df47509caac2f065032ef72e...
[2] https://sandpile.org/
University में बनाया गया 68k disassembler मेरे लिए Neo के “मुझे kung fu आती है” कहने वाले पल जैसा था। high-level language से transistors तक, और फिर उल्टा code के बारे में reason करने तक, यह missing link था
पूरा emulator लिखना उससे एक order of magnitude ज़्यादा असरदार होगा। अच्छा लेख है
लगता है मेरी याददाश्त गलत थी। मुझे याद था कि salsa20 variant और machine code मूल रूप से cryp.to पर थे, लेकिन Dan Bernstein की site https://cr.yp.to/ थी
जब हम startup में data-at-rest encryption, streaming encryption वगैरह evaluate कर रहे थे, तब Dan के pages पर target chipsets और instruction sets के हिसाब से कई implementations थीं। वे उसके assembler representation से cross-compiled थीं
VM इस्तेमाल करके यह देखना दिलचस्प था कि 2000s के शुरुआती-मध्य दौर में कौन-कौन से instruction sets supported थे। Testing के दौरान कभी-कभी समस्या भी आती थी, क्योंकि VM support का दावा करता था लेकिन implementation पूरी तरह supported नहीं होती थी
यहाँ RISC की तुलना में x86 assembly कितनी painful है, इस पर इतनी बात होना दिलचस्प है। जब मैं code को वापस object files में अलग करता हूँ, तो मेरे सामने ठीक उलटी समस्या होती है
इस use case में x86 analysis सचमुच आसान है, और MIPS nightmare था। वजह यह है कि मेरी मुख्य चिंता code और data के references होते हैं। x86 में pointer-size immediate constants होते हैं, और MIPS में HI16/LO16 relocation pairs होते हैं, जो register use graph, code flow, और branch delay instructions के साथ उलझकर तरह-तरह की समस्याएँ पैदा करते हैं
इसका मतलब यह नहीं कि मैं x86 की तारीफ़ कर रहा हूँ
x86 assembly और C की तुलना करते हुए सीखने वाली सबसे बड़ी बात यह है कि signed/unsigned type नहीं, बल्कि operation की property बन जाते हैं
Flags का इस्तेमाल कर पाना अच्छा होगा, और PPC या armv7 जैसी कुछ architectures में यह आसान भी है, लेकिन x86 flags को इतनी आसानी से overwrite कर देता है कि values का इस्तेमाल करना बहुत मुश्किल हो जाता है