1 पॉइंट द्वारा GN⁺ 2025-10-26 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • प्रोग्राम चलने से पहले, kernel execve system call के ज़रिए process को कैसे बनाता और initialize करता है — इसका तकनीकी विश्लेषण
  • यह call executable file path, arguments और environment variables देता है, और kernel इन्हीं के आधार पर ELF फ़ॉर्मेट वाले executable को load करता है
  • ELF फ़ाइल में code, data, symbol, dynamic linking जानकारी आदि शामिल होती है, और kernel इसे समझकर memory mapping और stack initialization करता है
  • इसके बाद kernel नियंत्रण _start entry point को सौंप देता है, और language-specific runtime initialize होने के बाद ही user-defined main फ़ंक्शन call होता है
  • यह प्रक्रिया operating system, compiler और runtime के सहयोगी ढांचे को दिखाती है, और system level पर प्रोग्राम execution कैसे होता है यह समझने के लिए महत्वपूर्ण है

प्रोग्राम execution की शुरुआत: execve call

  • Linux में प्रोग्राम execution execve system call से शुरू होता है
    • execve(const char *filename, char *const argv[], char *const envp[]) के रूप में यह executable file का नाम, argument list और environment variable list पास करता है
    • kernel इससे तय करता है कि कौन-सा प्रोग्राम किस environment में चलाया जाए
  • High-level languages में इस call को अक्सर standard library के process execution API में wrap किया जाता है
    • उदाहरण: Rust का std::process::Command अंदरूनी तौर पर execve call करता है
    • यह shell के PATH lookup की तरह command name को full path में बदलने की प्रक्रिया भी करता है
  • Shebang(#!) वाले script के मामले में kernel निर्दिष्ट interpreter का उपयोग करके प्रोग्राम चलाता है
    • उदाहरण: #!/usr/bin/python3 → Python interpreter से execution

ELF: executable file की संरचना

  • Linux की executable files ELF(Executable and Linkable Format) फ़ॉर्मेट का पालन करती हैं
    • ELF code, data, symbol, relocation जानकारी आदि रखने वाला standard executable file format है
    • दूसरे OS अपने अलग फ़ॉर्मेट इस्तेमाल करते हैं, जैसे Mach-O(macOS), PE(Windows)
  • ELF header में फ़ाइल की संरचना और memory layout की जानकारी होती है
    • उदाहरण के लिए: ELF Magic, Class, Entry point address, Program headers, Section headers
    • Entry point address वह address है जहाँ से प्रोग्राम का पहला instruction चलेगा
  • उदाहरण ELF header में यह RISC-V architecture के लिए ELF32 executable है, और 0x10358 address को entry point के रूप में सेट किया गया है

ELF के अंदरूनी घटक

  • ELF फ़ाइल कई section से बनी होती है
    • .text: executable code
    • .data: initialized global variables
    • .bss: uninitialized global variables
    • .plt: shared library calls के लिए table
    • .symtab, .strtab: symbol और string tables
  • PLT(Procedure Linkage Table) shared library function calls को support करती है
    • उदाहरण: libc की printf, malloc आदि
    • ELF का PT_INTERP section dynamic linker(interpreter) को निर्दिष्ट करता है
  • kernel ELF को पढ़कर load किए जा सकने वाले sections को memory में रखता है, और ज़रूरत होने पर ASLR, NX bit जैसी security features लागू करता है

symbol table और runtime linking

  • ELF की symbol table(symtab) में functions और variables के address की जानकारी होती है
    • उदाहरण: _start, main, __libc_start_main जैसी entries मौजूद होती हैं
    • एक साधारण “Hello, World!” प्रोग्राम में भी 2300 से अधिक symbols हो सकते हैं
  • इनमें से अधिकांश standard library और runtime initialization code से आते हैं
    • क्योंकि musl या glibc जैसे libc implementation link किए जाते हैं
  • kernel ELF के हर section को load करने के बाद नियंत्रण interpreter(dynamic linker) को दे देता है
    • interpreter relocation, address randomization(ASLR), execution permission setup(NX bit) आदि संभालता है

stack initialization प्रक्रिया

  • kernel को प्रोग्राम execution से पहले stack खुद बनानी होती है
    • stack का उपयोग local variables, function call frames और argument passing के लिए होता है
  • execve call में दिए गए argv, envp stack में store किए जाते हैं
    • प्रोग्राम इन्हीं के जरिए command-line arguments और environment variables तक पहुँचता है
  • kernel ELF auxiliary vector(auxv) भी stack में शामिल करता है
    • इसमें page size, ELF metadata, system information आदि से जुड़े लगभग 30 entries होती हैं
    • उदाहरण: AT_PAGESZ memory page size (जैसे 4KiB) बताता है
  • RISC-V emulator के उदाहरण में stack pointer(sp) को ऊँचे address से शुरू करके arguments, environment variables और auxiliary vector को उल्टे क्रम में रखा जाता है

entry point और _start फ़ंक्शन

  • ELF का entry point _start फ़ंक्शन के address पर सेट होता है
    • _start user space का पहला code है जिसे kernel control सौंपता है
  • ज़्यादातर languages _start में runtime initialization करने के बाद main को call करती हैं
    • उदाहरण: Rust का std::rt::lang_start, C का __libc_start_main
  • Rust के उदाहरण में #![no_std], #![no_main] attributes का उपयोग करके runtime के बिना सीधे _start define किया जा सकता है
    • _start के अंदर stack से argc, argv, envp पढ़कर main pointer को call किया जाता है
  • Language-specific runtime global constructors, thread-local storage, exception handling जैसी language-specific initialization tasks भी करती है

main() call होने से पहले का पूरा flow

  • पूरी प्रक्रिया का सार इस प्रकार है
    1. execve call → kernel ELF फ़ाइल load करता है
    2. ELF parsing → code/data sections map होते हैं, interpreter तय होता है
    3. stack setup → arguments, environment variables, auxiliary vector store होते हैं
    4. entry point _start execute होता है
    5. runtime initialization के बाद main() call होता है
  • यह पूरी श्रृंखला operating system kernel, ELF format और language runtime के सहयोगी ढांचे को दिखाती है
  • वास्तविक Linux kernel में address space, process table, group management जैसी अतिरिक्त अंदरूनी logic भी होती है, लेकिन यह लेख उससे पहले के मुख्य flow को समझाता है

निष्कर्ष और सुधार

  • main() से पहले की execution प्रक्रिया kernel-level initialization और runtime setup का संयुक्त परिणाम है
  • एक साधारण “Hello, World!” प्रोग्राम भी जटिल ELF संरचना और runtime initialization से होकर गुजरता है
  • लेख के शुरुआती संस्करण में कुछ section loading logic को kernel के हिस्से के रूप में बताया गया था, लेकिन बाद में इसे सुधारते हुए स्पष्ट किया गया कि यह वास्तव में ELF interpreter की भूमिका है
  • यह विश्लेषण system programming, compiler और OS architecture की समझ के लिए उपयोगी बुनियादी सामग्री है

1 टिप्पणियां

 
GN⁺ 2025-10-26
Hacker News राय
  • ELF फ़ाइल की dynamic linking प्रक्रिया के बारे में समझाया गया है
    kernel ELF के PT_LOAD segments को map करता है, PT_INTERP में निर्दिष्ट dynamic linker (ld.so) को लोड करता है और फिर control उसे सौंप देता है
    इसके बाद dynamic linker खुद को relocate करता है और ज़रूरी shared objects को mmap/mprotect से लोड करता है
    इस संरचना की तुलना script के shebang(#!) mechanism से की गई है

    • kernel को section जानकारी में बिल्कुल रुचि नहीं होती, वह सिर्फ PT_LOAD segments को ही संभालता है
      पहले objcopy से ELF में कोई मनचाही फ़ाइल embed करने की कोशिश की थी, लेकिन kernel ने उसे लोड नहीं किया, जिससे काफ़ी भ्रम हुआ
      अंत में उन्होंने खुद program header table patch tool बनाया, और कहा कि यह सुविधा mold linker में भी जोड़ दी गई
      संबंधित लेख: Self-contained Lone Lisp Applications
    • लेखक ने माना कि उन्होंने पहले सामग्री को ग़लत तरीके से संपादित करके पोस्ट किया था और कहा कि वे इसे ठीक करेंगे
    • उन्होंने कहा कि Linux में loader user space में चलता है, इसलिए उन्हें हमेशा यह जिज्ञासा रही कि और ज़्यादा तरह के loaders क्यों नहीं हैं
  • कहा गया कि उन्होंने पूरे code को main() से पहले या main() के बिना pack करने का प्रयोग किया
    संबंधित लेख: Packing a codebase into a single function

    • पढ़कर यह दिलचस्प लगा कि यह उम्मीद से ज़्यादा सरल है और उतना नाज़ुक भी नहीं
      मज़ाक में कहा गया कि बस सभी functions को main(100+n, ...) के रूप में बदल देना चाहिए
  • अगर इस विषय में रुचि है, तो उनके बनाए cpu.land को देखने की सलाह दी गई
    यह memory layout से ज़्यादा multitasking और code loading प्रक्रिया पर केंद्रित है

    • किसी ने धन्यवाद देते हुए कहा कि उन्हें cpu.land वाकई बहुत पसंद है
  • पूछा गया कि C projects में standard library से बचकर सिर्फ Linux syscall को सीधे कॉल करने वाले मामले कितने होते हैं
    उनका मानना था कि इस तरह code लिखना कहीं ज़्यादा मज़ेदार है

    • एक जवाब में कहा गया कि syscall को सीधे इस्तेमाल करना उल्टा अल्प-प्रभावी हो सकता है
      ALSA, DRM जैसी सुविधाओं तक kernel syscall की बजाय system libraries के ज़रिए पहुँचना कई तरह से फ़ायदेमंद है
      समझाया गया कि portability और maintainability के लिहाज़ से यह तरीका Windows-style approach से बेहतर है
    • यह भी जोड़ा गया कि Windows में सिर्फ Win32 API इस्तेमाल करने पर C runtime को link करना ज़रूरी नहीं होता
    • किसी ने कहा कि उन्होंने पहले liblinux project बनाया था, जिसमें सिर्फ syscalls से programs लिखे जाते थे
      अब Linux के nolibc headers अच्छे हो गए हैं, इसलिए वह काम रोक दिया,
      लेकिन इस समय वे syscall-आधारित Lisp interpreter language विकसित कर रहे हैं
      उनका कहना था कि system calls के ज़रिए सीधे Linux user space बनाना एक बहुत दिलचस्प सफ़र रहा
    • किसी ने कहा कि वे portability बनाए रखने की कोशिश करते हैं, लेकिन file descriptors इतने सुविधाजनक हैं कि उन्हें छोड़ना मुश्किल है
    • यह भी जोड़ा गया कि बहुत-सा driver code वास्तव में सिर्फ syscalls का ही इस्तेमाल करता है
  • समझाया गया कि ELF interpreter (ld.so) शुरुआती ELF segments को map किए जाने के बाद बाकी सारी loading संभालता है
    execve PT_LOAD segments को map करता है, aux vector को stack पर भरता है,
    और फिर ELF interpreter के entry point पर jump करता है
    kernel को PLT/GOT के बारे में कुछ भी पता नहीं होता

  • विश्वविद्यालय में यह विषय पढ़ाने वाले एक व्यक्ति ने कहा कि छात्र memory diagrams की वजह से उलझ जाते हैं
    पाठ्यपुस्तकों में addresses को ऊपर की ओर बढ़ते हुए दिखाया जाता है, लेकिन असली Linux process में
    low addresses ऊपर और high addresses नीचे छपते हैं
    /proc/<pid>/maps देखने पर नीचे scroll करने के साथ addresses बढ़ते जाते हैं
    यानी “heap ऊपर बढ़ता है और stack नीचे बढ़ता है” जैसी बात सिर्फ संख्यात्मक दिशा बताती है,
    जबकि दृश्य रूप में मामला उल्टा लगता है
    सुझाव दिया गया कि IDE की तरह नीचे जाने पर address बढ़े, इस तरह diagram बनाना ज़्यादा सहज होगा

    • एक जवाब में कहा गया कि stack तो फिर भी stack pointer के घटने के साथ बढ़ता है, इसलिए “नीचे बढ़ता है” कहना अब भी ठीक है
      हालाँकि visualization को horizontal direction में करना अधिक स्वाभाविक बताया गया
    • किसी और ने याद किया कि उन्हें भी पहले यही उलझन हुई थी, और little-endian address notation भी भ्रमित करती थी
    • एक प्रतिवाद में कहा गया कि अगर हम वास्तविक वस्तुओं के ढेर लगने की दिशा सोचें, तो “stack नीचे बढ़ता है” कहना सहज नहीं लगता
  • किसी ने कहा कि उन्हें पुराने PIC16 microcontrollers पर ऐसे प्रयोग करना पसंद है
    stack pointer, timers, variables आदि को सीधे संभालना उन्हें मज़ेदार लगता है

  • shebang(#!) से जुड़ा एक अनुभव साझा किया गया
    एक Java application ने error दिया कि execution script नहीं मिल रही,
    लेकिन असली समस्या script के shebang path के ग़लत होने की थी
    local पर सब ठीक चलता था, लेकिन remote server पर interpreter path अलग होने की वजह से समस्या आई

    • जोड़ा गया कि यह सिर्फ Java की समस्या नहीं है; ENOENT error आने वाले किसी भी program में ऐसा हो सकता है
      सलाह दी गई कि strace से चलाने पर तुरंत पता चल सकता है कि किस syscall पर error आया
    • shebang की संरचना का विश्लेषण करने वाला यह लेख साझा किया गया: What the #! means
    • यह भी जोड़ा गया कि kernel में shebang support के लिए CONFIG_BINFMT_SCRIPT=y setting चाहिए
  • किसी ने कहा कि debugging के दौरान main binary का relocation order कब लागू होता है, इसे लेकर वे हमेशा उलझ जाते हैं
    linker अपने symbols हल करने से पहले करता है या बाद में, यह उन्हें किसी black magic जैसा लगता है

  • इशारा किया गया कि Markdown में “lang_start function (defined here)” वाला link टूटा हुआ है