- Rust बाइनरी
fn main()से पहले runtime initialization चरण से गुजरती है, और इस चरण में panic·unwinding हैंडलिंग तथा program arguments conversion जैसे काम किए जाते हैं - जब operating system loader control को entry point पर सौंपता है, तब C runtime और Rust runtime initialization functions चलाते हैं, और
#[unsafe(link_section = "...")]तथा constructor तरीके से pre-main code को रखा जा सकता है - Linker section कई crates द्वारा दिए गए data को binary build समय पर एक जगह इकट्ठा करता है, और
link-sectionइसे Rust slice की तरह संभालने देता है ctorऔरlink-sectionको साथ इस्तेमाल करने पर CLI subcommand registration, string interning pool sorting जैसे patterns कोmainसे पहले तैयार किया जा सकता है और बाद में lock के बिना पढ़ा जा सकता है- यह तरीका allocation-free aggregation और inversion of control देता है, लेकिन dead code removal की कठिनाई, constructor constraints, platform differences, और Miri compatibility limits की वजह से इसका उपयोग सोच-समझकर करना चाहिए
Rust बाइनरी में main से पहले का चरण
- हर Rust बाइनरी में
fn main()होता है, लेकिन वास्तविक execution flow operating system loader और runtime initialization से गुजरने के बादmainतक पहुँचता है - C में
libcके रूप में पहचाना जाने वाला C runtime होता है, और Rust standard library के जरिए अपना runtime रखता है, जो C runtime के ऊपर higher-level abstraction बनाता है - Runtime का उद्देश्य developer code और platform operating system को एकीकृत करना है
- C runtime
mainसे पहले allocation, file access, thread-local storage जैसी runtime services को सेटअप करता है - Rust इसी समय panic और unwinding handling को तैयार करता है, और C-style program arguments को
std::env::argsinterface में बदलता है - Pre-main चरण user code से पहले चलता है, single-threaded होता है, और predictable ordering वाला environment देता है, इसलिए यह deterministic initialization के लिए उपयुक्त है
Entry point
- बाइनरी की शुरुआत तब होती है जब operating system loader binary को memory में लोड करता है, environment सेट करता है, और control सौंपता है
- Linux में entry point ELF header के
e_entryfield में रखा जाता है, और default रूप से linker_startनाम के symbol का address वहाँ रखता है - Windows में भी ऐसा ही hook होता है, और executable
_WinMainCRTStartupfunction से शुरू होता है - शुरुआती runtime bootstrapping file I/O initialization, allocator initialization जैसे static function call tree के रूप में थी
- Runtime जटिल होने के साथ static initialization call tree भी बड़ी होती गई, और binaries में C runtime की ऐसी features भी शामिल होने लगीं जिनकी ज़रूरत हो भी सकती थी और नहीं भी
- जब linker को binary build से पहले unused code हटाने की क्षमता मिली, तब static initialization call tree का विकल्प चाहिए था
- GCC का
__attribute__((constructor))तरीका initialization function pointers की सूची को binary के contiguous region में रखता था, और C runtime startup पर इन्हें iterate करके call करता था - Constructors को priority दी जा सकती थी; उदाहरण के लिए buffered file I/O से पहले
mallocinitialization की ज़रूरत हो सकती है - Linux के आधुनिक
glibcruntime में function pointers.init_arrayमें रखे जाते हैं, और numeric suffix से execution order तय किया जा सकता है - Priority 100 या उससे कम runtime के लिए reserved है, इसलिए C runtime इस्तेमाल करने वाले code को 101 या उससे ऊपर का उपयोग करना चाहिए
- Rust में
#[used]और#[unsafe(link_section = ".init_array.101")]जैसे attributes से initialization function pointers रखे जा सकते हैं
linktime: ctor, link-section आदि
- उदाहरण Linux और कई BSD पर काम करते हैं, लेकिन इन्हें cross-platform examples के रूप में डिज़ाइन नहीं किया गया है
- macOS
startऔरstopsymbols को support करता है, लेकिन उनके नाम अलग हैं; Windowsstartऔरstopsymbols को support नहीं करता, लेकिन व्यवहार में उसके पास equivalent section ordering rules हैं ctorऔरlink-sectionlinktimeproject की crates हैं, जो platform-specific differences और linker work की जटिलता को abstract करती हैंinventoryऔरlinkmeभी इसी सिद्धांत पर बनी widely used crates हैं, लेकिन उदाहरणों में उनकी सीमाएँ हैंctorcrate cross-platform तरीके से constructors register करने का boilerplate संभालती है#[ctor(unsafe, priority = 101)]जैसे attribute वाली functions को code में सीधे call न किया जाए तब भी linker के organize करने के बाद C runtime उन्हें call करता है
Sections और linker script
- Compiler data या code को binary के भीतर किसी खास location पर रख सकता है, जिसे अधिकतर platforms पर section कहा जाता है
- Rust भी
link_sectionattribute के जरिए यही organization capability देता है - कई linkers developer को linker script देने की सुविधा देते हैं; यह text file linker को बताती है कि object files को कैसे assemble किया जाए
- Linker script की मदद से एक C file Linux executable बन सकती है, या hard disk boot sector में रखे जाने वाला raw assembly block भी बन सकती है
- Linker script ऐसे virtual symbols define कर सकती है जो source file में नहीं होते, लेकिन C code से loaded binary के base data pointers तक पहुँचने के काम आते हैं
- उदाहरण linker script में
_TEXT_START_और_TEXT_END_को.textsection की शुरुआत और अंत की ओर point करने के लिए define किया गया है _TEXT_START_ = .;में dot position counter को दर्शाता है, जिसे binary के current output address के करीब मान के रूप में समझा जाता है
Linker symbols
- Linker start·end symbol की value को pointer के रूप में सेट नहीं करता, बल्कि उसी नाम के
staticको जिस address पर रखा जाएगा उसे सेट करता है - Start·end symbols
*const Typepointers नहीं होते; उनके पास अपना data नहीं होता, वे सिर्फ address का अर्थ रखते हैं - Section उस data से बनता है जो start symbol को शामिल करते हुए और end symbol को छोड़ते हुए range में आता है
- कई linkers executable की सभी section boundaries को automatically define करने की सुविधा रखते हैं
- GNU toolchain में
MY_SECTIONनाम के section के लिए__start_MY_SECTIONऔर__stop_MY_SECTIONsymbols अपने आप define हो जाते हैं - macOS में हर section के लिए
section$startऔरsection$endsymbols synthesize करने का समान pattern है - GNU linker में linker script में explicitly न बताए गए sections को orphan sections कहा जाता है
- Linker
_start·_stopprefixed symbols तभी automatically define करता है जब section name C symbol names के compatible हो our_stringsकाम करता है, लेकिनour.stringsया.our_stringsउसी तरह काम नहीं करते- Boundary symbols में data नहीं होता और सिर्फ address महत्वपूर्ण होता है, इसलिए उदाहरण में इन्हें
MaybeUninit<()>से व्यक्त किया गया है - Stable Rust में आदर्श “opaque external type” अभी implement नहीं हुआ है, इसलिए
MaybeUninitउसका विकल्प बनता है staticitem के लिए&raw constpointer बनाना हमेशा valid है, इसलिए value पढ़े बिना सिर्फ address को safely लिया जा सकता हैlink-sectionlinker section की इन details को abstract करके इसे ऐसे Rust slice में बदल देता है जिस पर standard slice operations इस्तेमाल की जा सकें- Link sections की ताकत यह है कि binary को code देने वाली कोई भी crate उसी section में items submit कर सकती है, और final binary build से ठीक पहले linker उन्हें एक साथ जमा कर देता है
Dependency injection
- Section-based registration pattern dependency injection जैसे सिद्धांत पर काम करता है
- Dagger और Spring जैसे frameworks भी इसी विचार पर बने हैं कि registration data का consumer provider से tightly coupled नहीं होना चाहिए
- Provider अपनी definition location पर data register करता है, और consumer registry को पढ़ता है
- Traditional dependency injection में framework को startup पर module graph walk करना पड़ सकता है या loaded classes scan करनी पड़ सकती हैं ताकि providers और consumers मिल सकें
- Linker sections में binary build होते समय linker provider data इकट्ठा कर देता है और consumer के लिए उसे पढ़ना आसान बना देता है
- CLI subcommand registration का उदाहरण
link_section::sectionके जरिए subcommands register करने वाले इसी pattern का एक मामला है - Turbopack string pool constants, serialization·deserialization registration machinery, और turbotask incremental compilation functions के registration में इस pattern का उपयोग करता है
- एक hypothetical web server भी routes और middleware को build time पर automatically collect करने के लिए इस pattern का उपयोग कर सकता है
Registration के लिए sections का उपयोग
mainसे पहले काम करने का फायदा यह है कि जब तक explicitly start न किया जाए, threads नहीं चलते- इस environment में कई मामलों में locks या synchronization primitives की जटिलता से बचा जा सकता है
- Data lifecycle को साफ़ तौर पर
mainसे पहले writable phase औरmainके बाद immutable phase में बाँटा जा सकता है - Running program में data access करते समय lock acquire और release से बचने पर structure सरल और efficient हो सकता है
- उदाहरण
CliSubcommandstruct,constconstructor function, और#[section]से subcommands collect करता है list,add,helpजैसे subcommands code में कहीं भी हो सकते हैं- अगर
mainfunction सिर्फCLI_SUBCOMMANDSsection definition देख सके, तो registered subcommand names और locations जाने बिना भी dynamic dispatch कर सकता है - अगर कोई registered subcommand न हो, तो default subcommand पर वापस जाया जाता है; उदाहरण में
helpdefault की तरह काम करता है
Immutable data से आगे
- पहले के उदाहरण मानते हैं कि linked data immutable है, लेकिन linker-based data organization mutable data पर भी लागू हो सकता है
- Global static data की mutability Rust में आम समस्या है, और इसे mutex या atomic types जैसी interior mutability tools से संभाला जा सकता है
- Mutex और atomic types contention न होने पर बहुत महंगे नहीं होते, लेकिन वे पूरी तरह free भी नहीं हैं
- Rust में data को safely mutate करने के लिए mutation thread-safe होनी चाहिए, और mutable reference मौजूद होने पर उसी data के दूसरे references नहीं होने चाहिए
- Pre-main environment single-threaded होता है, जब तक threads को explicitly start न किया जाए, इसलिए atomic operations की ज़रूरत नहीं होती
- Single-threaded environment में mutation के बाद reading आने वाला happens-before संबंध अपने-आप स्थापित हो जाता है
mainसे पहले link section data में किया गया mutation बाद में किसी भी thread से lock के बिना safely access किया जा सकता है- अगर mutable references सिर्फ
mainसे पहले बनाई और समाप्त की जाएँ, तो mutable reference के दौरान दूसरे references न होने की शर्त भी पूरी हो जाती है - Link section की slice section के अंदर static items का alias होती है, इसलिए slice और static items दोनों पर aliasing rules लागू होते हैं
- Slice के जरिए safely mutate करने के लिए static items को ज़रूर
UnsafeCellके अंदर रखना होगा UnsafeCellमें wrap न किए गए static items के लिए LLVM value को cache कर सकता है, reorder कर सकता है, या data के बारे में assumptions बना सकता हैUnsafeCellखुदSyncनहीं है, इसलिए एक अलग wrapper type की ज़रूरत होती है- उदाहरण
SyncUnsafeCellऔरMaybeUninit<SyncUnsafeCell<...>>का उपयोग करके boundary symbols और items बनाता है - Sort किए जा सकने वाले string interning pool का उदाहरण link time पर string pool define करता है, runtime की शुरुआत में slice को sort करता है, और बाद में binary search से strings ढूँढता है
- Manual implementation में boilerplate अधिक है, लेकिन
ctorऔरlink-sectionका उपयोग करकेTypedMutableSectionऔर constructors से वही structure अधिक संक्षेप में बनाया जा सकता है TypedMutableSectionके itemsconstहोने चाहिए, क्योंकि अंदरूनी रूप से manual implementation जैसे ही code का उपयोग किया जाता है
Link section pattern के फायदे
- यह pattern tagged items को guaranteed तरीके से aggregate करता है, और सारे data को pre-allocated contiguous memory में रखता है
- Registration locations को code में कहीं भी फैलाया जा सकता है
- Section के अंदर items की संख्या guaranteed value के रूप में पाई जा सकती है
- Link sections में अलग allocation की ज़रूरत नहीं होती
- Link sections के बिना वही structure बनाने पर
HashMap,Vec, या दूसरी data structures allocate करनी पड़तीं, और items जोड़ते समय कई बार resize करना पड़ता - Traditional collection तरीकों में shared type module, contribution modules, और collection module के बीच dependencies गहराई से उलझ जाती हैं
- Link sections का उपयोग करने पर collector कहीं भी रह सकता है और उसे यह सोचने की ज़रूरत नहीं कि कौन-सा module data contribute कर रहा है
scattered-collectlink-time support वाले कई data structure analogs देता हैScattered*Sliceslice देने वाले कईVec-जैसे structures हैं, जिनमें optional sorting support भी हैScatteredMapऔरScatteredSetHashMap·HashSetजैसे structures हैं, जो न्यूनतम pre-main initialization के साथ hash-based key-value lookup देते हैं
कब इस तरीके का उपयोग नहीं करना चाहिए
- Link-time computation शक्तिशाली है, लेकिन हर बार सही tool नहीं है
- Link-time तरीके के बजाय, ऐसा crate जो data contribute करने वाली हर crate को देख सकता है, उसमें data को manually collect किया जा सकता है
- Manual collection असुविधाजनक हो सकता है; contributors के लिए core crate में एक single contribution point देखने के बजाय कई crate references वाले collector crate की ज़रूरत पड़ती है
- Dead code removal मुश्किल हो जाती है
link-sectionऔरlinkmeitems पर#[used]लगाते हैं, इसलिए linker unused data हटा नहीं सकता- Interned string atoms जैसे छोटे data में यह समस्या न हो, लेकिन raw JSON·JavaScript fragments या बड़े data structures को intern करने पर पहचानना कठिन dead code बहुत जमा हो सकता है
- Pre-main constructor functions पर सीमाएँ होती हैं
- Constructor functions panic नहीं कर सकतीं, और Rust यह guarantee नहीं देता कि standard library की हर function उपलब्ध होगी
- एक ही priority के भीतर initialization function call order guaranteed नहीं है और platform dependency बहुत अधिक है
- सावधान design से इन सीमाओं को कुछ हद तक टाला जा सकता है, लेकिन pre-main तरीका subtle होने और debug करने में कठिन होने के कारण सही न भी हो सकता है
- Miri सभी pre-main constructors और link section setups को पूरी तरह support नहीं करता
- अभी Miri pre-main execution को बहुत basic रूप में देखता है और link sections को model नहीं करता
- Undefined behavior testing के लिए ASan, TSan जैसे LLVM sanitizers की सिफारिश की जाती है
- Inversion of control pattern यह auditing कठिन बना सकता है कि link sections में data किन-किन locations से contribute हो रहा है
- व्यापक रूप से वितरित और बहुत इस्तेमाल होने वाले कई Rust programs पहले से
ctor,link-section,inventory,linkmeजैसी pre-main features पर निर्भर हैं
WASM पर संक्षिप्त टिप्पणी
- WASM, पुराने design choices के प्रभाव के कारण, linker sections को native रूप से support नहीं करता
#[link_section]annotation items को वास्तविक code section में नहीं रख पाता, बल्कि उन्हें WASM custom sections में रखता है जिन तक WASM code खुद पहुँच नहीं सकताlinktimecrate WASM को support करती है, और ऐसा emulation workaround देती है जिससे यह approach WASM binaries में भी काम कर सके- उचित WASM support जोड़ने के तरीके भविष्य में प्रस्तावित किए जा सकते हैं
निष्कर्ष
mainसे पहले बहुत-से ऐसे काम किए जा सकते हैं जो कुछ खास मामलों में काफ़ी लाभ देते हैं- Pre-main environment में order पर कड़ा नियंत्रण और उच्च controlability होती है, इसलिए locks, atomic types, और अन्य synchronization primitives के बिना भी कई काम अधिक भरोसे के साथ किए जा सकते हैं
- Link sections पूरी binary में फैले संबंधित data को मनचाहे ढंग से aggregate और co-locate करने देते हैं, जिससे crates के असहज dependency order से बचा जा सकता है
- कई मामलों में allocations को पूरी तरह टाला जा सकता है, जिससे repeated allocation से होने वाली fragmentation जैसी allocator समस्याओं से दूरी बनती है
- संबंधित crates में
ctor,dtor,link-section,scattered-collectशामिल हैं
1 टिप्पणियां
Lobste.rs की राय
Apple system call के लिए ABI stability boundary के रूप में libSystem.dylib का उपयोग करता है, और NT-परिवार Windows में system call नहीं बल्कि
ntdll.dllABI stability boundary है: not syscallsOpenBSD पर लगता है कि Go ने ऐसा metadata flag सेट किया था जो NX bit enforcement को बंद कर देता था, ताकि उस नीति से बचा जा सके जिसमें loader द्वारा सेट किए गए read-only
libcmapping के बाहर system call करने पर kernel process को खत्म कर देता हैहालांकि libSystem.dylib contains the functionality which would normally be
libc.soplus other things, इसलिए इस अर्थ में यह BSD-परिवार के उस मॉडल जैसा ही है जहाँ “libc stability boundary है”और As of Go 1.16 से Go भी OpenBSD की system call policy का पालन करने के लिए libc का उपयोग करता है
Linux अपेक्षाकृत दुर्लभ मामला है जहाँ stable system call numbers हैं, क्योंकि अन्य OS की तरह वहाँ “kernel के वे हिस्से जो process address space में dynamic library के रूप में load होते हैं, kernel-mode code के साथ unstable system call
enumdefinitions साझा करते हैं” जैसी संरचना नहीं है, और Linux तथा glibc अन्य जगहों की तरह एक ही repository में साथ विकसित भी नहीं होतेWindows में C runtime यह काम भी संभालता है कि MS-DOS से आई और Windows की child process creation API में जारी रही CP/M-शैली command string को POSIX-शैली
argvarray में parse करेइसी वजह से Python
subprocessdocumentation में Converting an argument sequence to a string on Windows सेक्शन है, जो बताता है कि MS C runtime में हार्डकोड किए गए quote rules के अनुसारargvarray को string में कैसे बदला जाता है. बुलाए गए child process का अपना parser चाहे तो इन rules से अलग भी काम कर सकता हैLinux का
_startभी सख्ती से देखें तो इसका मतलब यह नहीं कि linker उस नाम का symbol अपने-आप binary में डाल देता है. यदि ELF-format binary कोई library नहीं बल्कि executable है, तो header केe_entryfield, यानी offset0x18, में वह address होता है जहाँ memory setup के बाद loader jump करेगा_startGCC की परंपरा है, जो तबe_entryके target को निर्दिष्ट करती है जब libc द्वारा दिया गया entry point इस्तेमाल नहीं किया जाता. याद पड़ता है कि NASM जैसे tools भी इसे follow करते हैंWindows का
_WinMainCRTStartupभी loader द्वारा PE header केAddressOfEntryPointसे खोजा जाता है. यह PE header की शुरुआत से Offset0x0028पर होता है, और यह PE header MZ(DOS EXE) header तथा DOS Stub के बाद आता हैPE header की बारीकियाँ सीखने के लिए Making the smallest Windows application और Tiny PE अच्छे हैं. Tiny PE कभी-कभी PE spec का उल्लंघन भी करता है, लेकिन उस तरीके से जिसे Windows स्वीकार कर लेता है; उदाहरण के लिए, ऐसे हिस्सों को overlap कर देता है जिन्हें OS पढ़ता ही नहीं, या इस्तेमाल न होने वाले header fields में code रख देता है. इस स्तर पर Windows जिस न्यूनतम file size को स्वीकार करता है, वह चल रहे Windows version के अनुसार बदलता है
Linux के बहुत छोटे ELF executables के लिए A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux भी देखने लायक है
_startके संदर्भ में, a.out systems में kernel जिस entry point से executable में जाता था, वह परंपरागत रूप से csu/crt0 में घोषितstartहोता था. उदाहरण के लिए 7th edition, VAX BSDउस समय C compiler global symbols के आगे
_जोड़ता था, इसलिए V7 में_mainघोषित दिखता है, और BSD में C केstart()के assembly name को बिना decoration वालेstartके रूप में घोषित किया गया हैउस दौर में program file के शुरुआती बिंदु से शुरू होता था, और
ccकी linker invocation इस तरह रखी जाती थी किcrt0सबसे आगे आए.csuका मतलब C startup code है, औरcrt0का अर्थ 0वाँ C runtime support object हैELF आने के बाद System V में यह ठीक-ठीक कैसे काम करता था, यह ढूँढना थोड़ा कठिन है, लेकिन
startया_startcsu/crt0 में घोषित program entry point के रूप में इस्तेमाल होते रहेELF ने
_prefix handling को कैसे बदला, यह मैंने कभी ठीक से नहीं समझा, लेकिन शायद मज़ाक-मज़ाक में एक परत और जुड़ गई और किसी कारणstartसे_startबन गयाएक स्पष्ट जोड़ी के रूप में ELF ने शायद
_endजोड़ा, जो BSS के शीर्ष के बराबर होता है और उस स्थान से मेल खाता है जिसे heap बनाने से पहलेsbrk(0)लौटाता हैmainसे पहले की life में रुचि थी, और लगा कि यह क्या है और क्यों उपयोगी है, इस पर एक लेख में समेटना अच्छा रहेगाlinker aggregation का उपयोग करके तेज़ collections बनाने जैसे follow-up article ideas भी हैं, लेकिन पहले मैं इस introductory topic पर feedback सुनना चाहता हूँ
no_stdऔर कभी-कभीallocभी न होने वाले environments मेंmainबस एक और function होता है और initialization ज़्यादातर developer की ज़िम्मेदारी होती हैइसी तरह के उद्देश्यों के लिए codebase में अपने हाथ से लिखा हुआ काफ़ी boilerplate है, इसलिए जिज्ञासा है कि ये crates embedded environments के साथ कैसे फिट बैठते हैं