1 पॉइंट द्वारा GN⁺ 4 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • 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::args interface में बदलता है
  • 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_entry field में रखा जाता है, और default रूप से linker _start नाम के symbol का address वहाँ रखता है
  • Windows में भी ऐसा ही hook होता है, और executable _WinMainCRTStartup function से शुरू होता है
  • शुरुआती 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 से पहले malloc initialization की ज़रूरत हो सकती है
  • Linux के आधुनिक glibc runtime में 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 और stop symbols को support करता है, लेकिन उनके नाम अलग हैं; Windows start और stop symbols को support नहीं करता, लेकिन व्यवहार में उसके पास equivalent section ordering rules हैं
  • ctor और link-section linktime project की crates हैं, जो platform-specific differences और linker work की जटिलता को abstract करती हैं
  • inventory और linkme भी इसी सिद्धांत पर बनी widely used crates हैं, लेकिन उदाहरणों में उनकी सीमाएँ हैं
  • ctor crate 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_section attribute के जरिए यही 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_ को .text section की शुरुआत और अंत की ओर 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 Type pointers नहीं होते; उनके पास अपना 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_SECTION symbols अपने आप define हो जाते हैं
  • macOS में हर section के लिए section$start और section$end symbols synthesize करने का समान pattern है
  • GNU linker में linker script में explicitly न बताए गए sections को orphan sections कहा जाता है
  • Linker _start·_stop prefixed 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 उसका विकल्प बनता है
  • static item के लिए &raw const pointer बनाना हमेशा valid है, इसलिए value पढ़े बिना सिर्फ address को safely लिया जा सकता है
  • link-section linker 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 हो सकता है
  • उदाहरण CliSubcommand struct, const constructor function, और #[section] से subcommands collect करता है
  • list, add, help जैसे subcommands code में कहीं भी हो सकते हैं
  • अगर main function सिर्फ CLI_SUBCOMMANDS section definition देख सके, तो registered subcommand names और locations जाने बिना भी dynamic dispatch कर सकता है
  • अगर कोई registered subcommand न हो, तो default subcommand पर वापस जाया जाता है; उदाहरण में help default की तरह काम करता है

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 के items const होने चाहिए, क्योंकि अंदरूनी रूप से 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-collect link-time support वाले कई data structure analogs देता है
    • Scattered*Slice slice देने वाले कई Vec-जैसे structures हैं, जिनमें optional sorting support भी है
    • ScatteredMap और ScatteredSet HashMap·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 और linkme items पर #[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 खुद पहुँच नहीं सकता
  • linktime crate 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 टिप्पणियां

 
GN⁺ 4 시간 전
Lobste.rs की राय
  • Go इस मामले में अधिकांश प्लेटफ़ॉर्म पर C runtime से बचने के कारण अपवाद है, लेकिन Apple system call access के लिए C runtime की मांग करता है
    Apple system call के लिए ABI stability boundary के रूप में libSystem.dylib का उपयोग करता है, और NT-परिवार Windows में system call नहीं बल्कि ntdll.dll ABI stability boundary है: not syscalls
    OpenBSD पर लगता है कि Go ने ऐसा metadata flag सेट किया था जो NX bit enforcement को बंद कर देता था, ताकि उस नीति से बचा जा सके जिसमें loader द्वारा सेट किए गए read-only libc mapping के बाहर system call करने पर kernel process को खत्म कर देता है
    हालांकि libSystem.dylib contains the functionality which would normally be libc.so plus 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 enum definitions साझा करते हैं” जैसी संरचना नहीं है, और Linux तथा glibc अन्य जगहों की तरह एक ही repository में साथ विकसित भी नहीं होते
    Windows में C runtime यह काम भी संभालता है कि MS-DOS से आई और Windows की child process creation API में जारी रही CP/M-शैली command string को POSIX-शैली argv array में parse करे
    इसी वजह से Python subprocess documentation में Converting an argument sequence to a string on Windows सेक्शन है, जो बताता है कि MS C runtime में हार्डकोड किए गए quote rules के अनुसार argv array को string में कैसे बदला जाता है. बुलाए गए child process का अपना parser चाहे तो इन rules से अलग भी काम कर सकता है
    Linux का _start भी सख्ती से देखें तो इसका मतलब यह नहीं कि linker उस नाम का symbol अपने-आप binary में डाल देता है. यदि ELF-format binary कोई library नहीं बल्कि executable है, तो header के e_entry field, यानी offset 0x18, में वह address होता है जहाँ memory setup के बाद loader jump करेगा
    _start GCC की परंपरा है, जो तब e_entry के target को निर्दिष्ट करती है जब libc द्वारा दिया गया entry point इस्तेमाल नहीं किया जाता. याद पड़ता है कि NASM जैसे tools भी इसे follow करते हैं
    Windows का _WinMainCRTStartup भी loader द्वारा PE header के AddressOfEntryPoint से खोजा जाता है. यह PE header की शुरुआत से Offset 0x0028 पर होता है, और यह 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 भी देखने लायक है
    • FreeBSD और NetBSD के system calls भी system library की तरह ABI stability रखते हैं
    • _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 या _start csu/crt0 में घोषित program entry point के रूप में इस्तेमाल होते रहे
      ELF ने _ prefix handling को कैसे बदला, यह मैंने कभी ठीक से नहीं समझा, लेकिन शायद मज़ाक-मज़ाक में एक परत और जुड़ गई और किसी कारण start से _start बन गया
      एक स्पष्ट जोड़ी के रूप में ELF ने शायद _end जोड़ा, जो BSS के शीर्ष के बराबर होता है और उस स्थान से मेल खाता है जिसे heap बनाने से पहले sbrk(0) लौटाता है
  • मुझे Rust में main से पहले की life में रुचि थी, और लगा कि यह क्या है और क्यों उपयोगी है, इस पर एक लेख में समेटना अच्छा रहेगा
    linker aggregation का उपयोग करके तेज़ collections बनाने जैसे follow-up article ideas भी हैं, लेकिन पहले मैं इस introductory topic पर feedback सुनना चाहता हूँ
    • मैंने embedded Rust काफी किया है, इसलिए no_std और कभी-कभी alloc भी न होने वाले environments में main बस एक और function होता है और initialization ज़्यादातर developer की ज़िम्मेदारी होती है
      इसी तरह के उद्देश्यों के लिए codebase में अपने हाथ से लिखा हुआ काफ़ी boilerplate है, इसलिए जिज्ञासा है कि ये crates embedded environments के साथ कैसे फिट बैठते हैं