x86-64 असेंबली सीखना
(gpfault.net)- x86-64 असेंबली की शुरुआती पढ़ाई के लिए यह श्रृंखला की पहली पोस्ट है
- आधुनिक 64-बिट सिस्टम के आधार पर टूल इंस्टॉलेशन और बुनियादी संरचना की व्याख्या दी गई है
- Flat Assembler (FASM) और WinDbg को मुख्य development और debugging टूल के रूप में उपयोग करने का मार्गदर्शन दिया गया है
- PE format, DLL import, Windows calling convention जैसी व्यावहारिक काम में ज़रूरी मुख्य जानकारियों का सार शामिल है
- एक साधारण exit program लिखने और debugging प्रक्रिया का अभ्यास करने के अनुभव-केंद्रित विवरण दिए गए हैं
परिचय और महत्व
- x86 असेंबली को पहली बार सीखते समय विश्वविद्यालयों में अक्सर पुराने वातावरण (16-बिट, DOS, segmented memory) पर आधारित तरीकों से पढ़ाया जाता था
- आज 64-बिट प्रोसेसर मुख्यधारा में हैं, इसलिए यह श्रृंखला केवल वास्तव में उपयोग होने वाले x86-64 वातावरण पर केंद्रित है और पुराने तत्वों को पूरी तरह बाहर रखती है
- यह tutorial Windows operating system वातावरण में चलने वाले 64-बिट प्रोग्राम विकसित करने पर केंद्रित है
- शुरुआत libraries का उपयोग किए बिना, OS तक सीधे पहुँचने वाले न्यूनतम code से की जाती है
- यह लेख उन developers के लिए है जो पहली बार असेंबली सीखना चाहते हैं, और यह मानकर चलता है कि उन्हें बुनियादी C/C++ ज्ञान है
विकास टूल तैयार करना
Assembler
- CPU केवल machine code को समझ सकता है, जिसे इंसानों के पढ़ने योग्य रूप में लिखा जाए तो उसे assembly language कहा जाता है
- assembly language को machine code में बदलने वाला program assembler कहलाता है
- x86-64 assembly language का कोई एक तय standard नहीं है, और अलग-अलग assemblers का syntax और काम करने का तरीका अलग होता है
- इस श्रृंखला में Flat Assembler (FASM) का उपयोग किया गया है, जो छोटा है, उपयोग में आसान है, और शक्तिशाली macro system तथा editor प्रदान करता है
Debugger
- लिखे गए assembly code का विश्लेषण करने और execution flow देखने के लिए debugger एक अनिवार्य टूल है
- WinDbg की सिफारिश की गई है, क्योंकि इसमें registers, memory और assembly code को अलग-अलग देखा और बदला जा सकता है
- इसे Windows 10 SDK में केवल ज़रूरी component चुनकर install किया जा सकता है
- debugger के माध्यम से program की आंतरिक स्थिति, memory structure और register में होने वाले बदलाव सीधे देखे जा सकते हैं
असेंबली प्रोग्रामिंग का दृष्टिकोण
CPU संरचना और instruction set
- CPU केवल एक निश्चित instruction set के अनुसार सीमित प्रकार के काम कर सकता है
- Instruction वह बुनियादी कार्य-इकाई है जिसे CPU चला सकता है
- हर instruction अपने parameters के साथ बहुत सरल काम करता है, जैसे मान सहेजना या arithmetic operation करना
- low-level programming और debugging में यह समझना महत्वपूर्ण है कि यही संरचना सभी high-level concepts की नींव है
Registers
- Register CPU के भीतर बना हुआ बहुत तेज़ dedicated memory क्षेत्र होता है
- x86-64 में 16 general-purpose registers होते हैं और सभी 64-बिट आकार के होते हैं
- हर register को byte, word और doubleword स्तर पर आंशिक रूप से access किया जा सकता है
| रजिस्टर | निचला byte | निचला word | निचला doubleword |
|---|---|---|---|
| rax | al | ax | eax |
| rbx | bl | bx | ebx |
| rcx | cl | cx | ecx |
| rdx | dl | dx | edx |
| rsp | spl | sp | esp |
| rsi | sil | si | esi |
| rdi | dil | di | edi |
| rbp | bpl | bp | ebp |
| r8~r15 | r8b~r15b | r8w~r15w | r8d~r15d |
rspstack pointer है,rsi/rdistring processing index के रूप में काम करते हैं; इस तरह कुछ registers को विशेष उद्देश्य दिए गए हैंripinstruction pointer है, औरrflagsएक विशेष register है जो operation results के status flags रखता है
Memory और addresses
- memory, index 0 से शुरू होने वाले लगातार bytes के array की तरह काम करती है
- पुराने x86 architecture में segment-offset तरीका आवश्यक था, लेकिन x86-64 में पूरी memory को flat address space के रूप में संभाला जाता है
- वास्तव में operating system और hardware हर process के लिए virtual address space को physical memory पर dynamically map करते हैं
- यानी एक ही virtual address अलग-अलग processes में अलग physical memory से जुड़ सकता है
- instructions और data एक ही memory में मौजूद होते हैं (von Neumann architecture); यह Arduino में उपयोग होने वाले AVR जैसे Harvard architecture से अलग है, जहाँ data अलग रखा जाता है
पहला assembly program लिखना
- FASM install करने के बाद नीचे दिए गए सरल program code को लिखने और build करने का अभ्यास किया जाता है
format PE64 NX GUI 6.0
entry start
section '.text' code readable executable
start:
int3
ret
Code की व्याख्या
format PE64 NX GUI 6.0: FASM द्वारा बनाए जाने वाले executable file format को निर्दिष्ट करता है; यहाँ यह PE (Portable Executable) 64-बिट GUI हैentry start: program के entry point को परिभाषित करता है; execution इस label (start) से शुरू होती हैsection '.text' code readable executable: यह PE का code section है; यह executable क्षेत्र हैstart:: पहले तय किए गए entry point को नाम देता हैint3: debugger के लिए breakpoint; program को अस्थायी रूप से रोककर उसकी स्थिति जाँचने के लिए उपयोग होता हैret: stack से address निकालकर control को उस स्थान पर भेजने वाला instruction; इस program में यह तुरंत exit प्रतिक्रिया देता है
Debugging अभ्यास
-
WinDbg में ऊपर वाले program की executable file (
.exe) खोलकर disassembly, registers आदि की विभिन्न windows तैयार की जाती हैं -
F5 दबाने पर program breakpoint तक पहुँचता है, और F8 दबाने पर हर बार एक-एक instruction execute होता है
-
registers (
ripआदि) में होने वाले बदलाव real time में देखे जा सकते हैं -
retचलने के बाद control operating system को चला जाता है, और फिरRtlExitUserThreadको call करते हुए thread और process का exit आगे बढ़ता है -
ध्यान दें: केवल
retinstruction से exit करने पर, thread के अलावा अतिरिक्त background execution होने की स्थिति में process बचा रह सकता है; इसलिए सही तरीके से exit करने के लिएExitProcessको call करना बेहतर है
PE format और DLL import
DLL function import संरचना का अवलोकन
- ExitProcess जैसी WinAPI functions KERNEL32.DLL में होती हैं
- ऐसी external functions का उपयोग करने के लिए executable file की import table (
.idatasection) बनानी पड़ती है idatasection की Import Directory Table (IDT) में DLL name, function name, IAT/ILT आदि के address (RVA) की जानकारी होती है- IAT (Import Address Table) को runtime पर OS loader वास्तविक function addresses से overwrite करता है
- Hint/Name Table में हर function का नाम और hint information होती है
FASM में .idata section परिभाषित करने का उदाहरण
section '.idata' import readable writeable
idt:
dd rva kernel32_iat
dd 0
dd 0
dd rva kernel32_name
dd rva kernel32_iat
dd 5 dup(0)
name_table:
_ExitProcess_Name dw 0
db "ExitProcess", 0, 0
kernel32_name: db "KERNEL32.DLL", 0
kernel32_iat:
ExitProcess dq rva _ExitProcess_Name
dq 0
db/dw/dd/dq: byte/word/doubleword/quadword (8-byte) इकाइयों में values insert करते हैंrva: symbol का virtual address (Relative Virtual Address) calculate करता है- IAT और Name Table को manually बनाकर DLL function references का उपयोग किया जा सकता है
64-बिट Windows calling convention (MS x64 Calling Convention)
- function call के समय arguments pass करने और stack उपयोग का तरीका तय करने वाला standard convention
- 64-बिट Windows में Microsoft x64 Calling Convention का उपयोग होता है
- मुख्य विशेषताएँ:
- stack pointer हमेशा 16-byte aligned होना चाहिए
- पहले 4 integer/pointer arguments
rcx,rdx,r8,r9registers में दिए जाते हैं - पहले 4 floating-point arguments
xmm0~xmm3में रखे जाते हैं - अतिरिक्त arguments के लिए stack का उपयोग होता है
- arguments की संख्या से स्वतंत्र होकर stack पर 32-byte shadow space आरक्षित करना होता है
- stack cleanup की ज़िम्मेदारी caller की होती है
ExitProcess call का उदाहरण
format PE64 NX GUI 6.0
entry start
section '.text' code readable executable
start:
int3
sub rsp, 8 * 5
xor rcx, rcx
call [ExitProcess]
section '.idata' import readable writeable
idt:
dd rva kernel32_iat
dd 0
dd 0
dd rva kernel32_name
dd rva kernel32_iat
dd 5 dup(0)
name_table:
_ExitProcess_Name dw 0
db "ExitProcess", 0, 0
kernel32_name db "KERNEL32.DLL", 0
kernel32_iat:
ExitProcess dq rva _ExitProcess_Name
dq 0
नए code का विश्लेषण
-
sub rsp, 8 * 5: stack pointer को adjust करता है (40 bytes reserve), और एक साथ 16-byte alignment तथा shadow space सुनिश्चित करता है -
xor rcx, rcx: पहले argument registerrcxमें 0 सेट करता है (इसे exit code के रूप में उपयोग किया जाता है) -
call [ExitProcess]: import table में वास्तव में लिखे गएExitProcessके function address पर jump करता है -
WinDbg में step-by-step execution के दौरान stack pointer (
rsp) औरrcxregister के बदलाव तथा process exit flow को सीधे देखा जा सकता है
समापन
- यह लेख बुनियादी tool setup से लेकर PE format, DLL import, x64 calling convention, पहला program लिखना और debugging तक x86-64 असेंबली के समग्र flow को hands-on तरीके से समझाता है
- अगले भाग में और विविध features के implementation और वास्तविक code पर चर्चा की जाएगी
1 टिप्पणियां
Hacker News राय
मैं कई वर्षों से विकसित कर रहे एक प्रोजेक्ट को साझा करना चाहता हूँ
https://asm-editor.specy.app
यह एक ऑनलाइन interactive IDE है जो M68K, MIPS, RISC-V, X86 आदि कई assembly भाषाओं को सपोर्ट करता है
इसमें assembly programming सिखाने के लिए कई तरह की सुविधाएँ हैं
इसे दूसरी वेबसाइटों में embed भी किया जा सकता है
मुझे नहीं पता था कि pointer indexing registers के low-address byte तक सीधे पहुँचने की सुविधा होती है (उदाहरण: 16/32-bit में si/esi को sil के रूप में access किया जा सकता है)
यह ax/eax से al को access करने जैसी ही अवधारणा है
मैं जानना चाहता हूँ कि x86_64 में इसके लिए जो नए opcode जोड़े गए, वे वास्तव में मौजूद हैं या नहीं
लग रहा है कि platform specification फिर से देखनी पड़ेगी
यह बस शुद्ध जिज्ञासा से पूछा है
मैं अपने द्वारा लिखी गई assembly की शुरुआती सामग्री साझा कर रहा हूँ
https://www.nayuki.io/page/a-fundamental-introduction-to-x86-assembly-programming
मैं यह जानना चाहता था कि क्या मैं अपने CPU emulator dispatch को C++ से तेज बना सकता हूँ, इसलिए assembly में optimize करने की कोशिश की
मैंने Fibonacci प्रोग्राम चलाकर देखा, लेकिन नतीजा उसके आस-पास भी नहीं पहुँचा
आखिर में मैंने इसे सिर्फ default-disabled option के रूप में merge किया
फिर भी मुझे यक़ीन है कि इसे और तेज करने का कोई तरीका ज़रूर होगा
https://github.com/libriscv/libriscv/blob/master/lib/libriscv/amd64/inaccurate_dispatch.nasm
memory access का तरीका सीखते हुए performance में थोड़ा सुधार किया
jump table को 64-bit से 32-bit तक घटाया और उसे .text section में रखा ताकि वह RIP-relative access बन जाए
Fibonacci प्रोग्राम को ज़्यादा bytecode की ज़रूरत नहीं थी
मैं सच में और सुधार के लिए सुझाव सुनना चाहूँगा
मुझे पूरा context नहीं पता, लेकिन मुझे लगता है कि अंतर dispatch mechanism (instruction fetch का तरीका) की वजह से नहीं, बल्कि वास्तविक instruction implementation के अंतर की वजह से भी हो सकता है
optimization के तौर पर, emulated registers को x86-64 के वास्तविक registers पर map किया जा सकता है और उन्हें memory में जाने ही न दिया जाए
ऐसा करने पर add जैसी operations memory से निकाले बिना सीधे की जा सकती हैं
हालांकि, इस तरीके से emulator लिखना काफ़ी ज़्यादा झंझटभरा हो जाता है
यह browser में अभ्यास किया जा सकने वाला x86 assembly का शुरुआती परिचय है
बिना किसी खास local setup के उदाहरणों को तुरंत चलाया जा सकता है
https://shikaan.github.io/assembly/x86/guide/2024/09/08/x86-64-introduction-hello.html
संदर्भ के लिए, यह सामग्री मैंने ही लिखी है
यह NASM से सीधे assemble करके binary चलाने जैसा लग रहा है, इसलिए security को लेकर जिज्ञासा हुई
सिर्फ profile photo देखकर मुझे लगा यह junferno है
assembly को बस एक बार भी हाथ लगाकर देखने से समग्र समझ गहरी हो जाती है, इसलिए यह हमेशा अच्छा अनुभव होता है
कोई बड़ा प्रोजेक्ट बनाना ज़रूरी नहीं है, इसलिए हिम्मत करके थोड़ा-सा ही सही, खुद करके देखने की सलाह दूँगा
(2020) उस समय की HN discussion link साझा कर रहा हूँ
https://news.ycombinator.com/item?id=24195627
अच्छा लगा कि यह Intel-style assembly syntax है
assembly में कुछ करना तो चाहता हूँ, लेकिन कोई खास idea सूझ नहीं रहा
यह एक तरह का pseudo-assembly puzzle game है
मुझे लगता है ऐसे गेम assembly के प्रति आपकी जिज्ञासा को संतुष्ट कर सकते हैं