Featured image of post अलग-अलग लिखा गया कोड एक कैसे बनता है? लिंकर और लोडर की आंतरिक कार्यप्रणाली

अलग-अलग लिखा गया कोड एक कैसे बनता है? लिंकर और लोडर की आंतरिक कार्यप्रणाली

प्रस्तावना: सॉफ्टवेयर निर्माण के अदृश्य शिल्पी

दैनिक सॉफ्टवेयर विकास में हम अक्सर यह कमांड चलाते हैं:

1
gcc -O2 main.c utils.c math.c -o myprogram

इस पूरी प्रक्रिया को बोलचाल में केवल “कंपाइलेशन” कह दिया जाता है। परंतु कंपाइलर का काम केवल आधा सफर तय करना है। शेष आधा सफर—दर्जनों अलग-अलग ऑब्जेक्ट फाइलों और लाइब्रेरी को आपस में जोड़ना, मेमोरी पते निर्धारित करना और प्रोग्राम को रैम में लोड करना—लिंकर (Linker) और लोडर (Loader) द्वारा पूरा किया जाता है।

  flowchart TD
    subgraph BuildPipeline ["सोर्स कोड से निष्पादन तक"]
        SourceA["main.c"] -->|कंपाइलर| ObjA["main.o (रिलोकेटेबल ऑब्जेक्ट)"]
        SourceB["utils.c"] -->|कंपाइलर| ObjB["utils.o (रिलोकेटेबल ऑब्जेक्ट)"]
        Lib["libc.so / libm.a\n(सिस्टम लाइब्रेरी)"]

        ObjA --> Linker["★ लिंकर (ld)\n[सिंबल रेजोल्यूशन & रिलोकेशन]"]
        ObjB --> Linker
        Lib --> Linker

        Linker --> Exec["myprogram (एक्जीक्यूटिव ELF फाइल)"]
    end

    Exec -->|execve| Loader["★ लोडर (कर्नेल & ld-linux.so)\n[मेमोरी मैपिंग & डायनामिक बाइंडिंग]"]
    Loader --> CPU["सीपीयू निष्पादन (_start -> main)"]

    style BuildPipeline fill:#f9f9f9,stroke:#333,stroke-width:2px
    style Linker fill:#ffe6e6,stroke:#d9534f,stroke-width:2px
    style Loader fill:#e6f3ff,stroke:#0066cc,stroke-width:2px

यदि लिंकर न होता तो हमें लाखों पंक्तियों का कोड एक ही भारी-भरकम फाइल में लिखना पड़ता।


1. ऑब्जेक्ट फाइल का विच्छेदन: ELF फॉर्मेट

कंपाइलर द्वारा निर्मित .o फाइल ELF (Executable and Linkable Format) प्रारूप में होती है:

  • .text: सीपीयू मशीन कोड निर्देश।
  • .rodata: केवल पढ़ने योग्य स्ट्रिंग और स्थिरांक।
  • .data: प्रारंभिक मान वाले ग्लोबल और स्टैटिक वेरिएबल्स।
  • .bss: बिना प्रारंभिक मान वाले ग्लोबल वेरिएबल्स। डिस्क पर यह शून्य बाइट घेरता है!
  • .symtab: सिंबल टेबल (फंक्शन व वेरिएबल नामों की सूची)।
  • .rela.text: रिलोकेशन तालिका जिसमें सुधारे जाने वाले पते दर्ज होते हैं।

2. सिंबल रेजोल्यूशन (Symbol Resolution)

लिंकर प्रत्येक बाहरी संदर्भ को उसकी वास्तविक परिभाषा से जोड़ता है।

मजबूत (Strong) बनाम कमजोर (Weak) सिंबल:

  • मजबूत: फंक्शन और मान वाले ग्लोबल वेरिएबल (int x = 1;)।
  • कमजोर: बिना मान वाले ग्लोबल वेरिएबल (int x;)।

नियम:

  1. एक ही नाम के दो मजबूत सिंबल होने पर लिंकर एरर देता है (multiple definition)।
  2. मजबूत सिंबल हमेशा कमजोर पर भारी पड़ता है।
  3. दो कमजोर सिंबलों में लिंकर किसी एक को मनमाने ढंग से चुन लेता है।

लाइब्रेरी क्रम: लिंकर तर्कों को बाएं से दाएं एक ही बार पढ़ता है, इसलिए -lm को हमेशा main.c के बाद लिखना चाहिए।


3. रिलोकेशन (Relocation)

कंपाइलर बाहरी फंक्शन कॉल के स्थान पर शून्य लिख देता है:

1
callq 0x00000000  # अस्थायी पता
$$\text{मान} = S + A - P$$

($S$: सिंबल पता, $A$: एडेंड, $P$: निर्देश पता)।


4. डायनामिक लिंकिंग: PIC, GOT और PLT

मेमोरी की बर्बादी रोकने के लिए शेयर्ड लाइब्रेरी (.so) का उपयोग किया जाता है। इसके लिए पोजिशन-इंडिपेंडेंट कोड (PIC) की आवश्यकता होती है, जो दो तालिकाओं द्वारा काम करता है:

  • GOT (Global Offset Table): डेटा मेमोरी में वास्तविक पते रखता है।
  • PLT (Procedure Linkage Table): कोड में जंप टेबल बनाता है।

लेजी बाइंडिंग (Lazy Binding) के कारण किसी फंक्शन का पता डायनामिक लिंकर (ld-linux.so) द्वारा तभी खोजा जाता है जब उसे पहली बार बुलाया जाए।


5. लोडर: _start से main() तक

जब ./myprogram चलाया जाता है, तो कर्नेल का execve सिस्टम कॉल मेमोरी में पेज मैप करता है और _start पर जंप करता है। सी-रनटाइम .init_array के सभी ग्लोबल कंस्ट्रक्टर्स को चलाकर अंततः हमारे main() फंक्शन को आमंत्रित करता है।

6. सिस्टम इंजीनियर्स के लिए बाइनरी विश्लेषण और डायग्नोस्टिक टूल्स

जब सिस्टम स्तर पर लिंकिंग या लोडिंग की समस्याएं आती हैं, तो पेशेवर इंजीनियर इन कमांड-लाइन टूल्स का उपयोग करते हैं:

  • readelf: ELF हेडर, सेक्शन्स और प्रोग्राम हेडर्स की विस्तृत जानकारी प्राप्त करने के लिए:
    1
    2
    
    readelf -h myprogram    # फ़ाइल हेडर की जांच
    readelf -l myprogram    # प्रोग्राम हेडर (मेमोरी सेगमेंट्स)
    
  • nm: सिंबल टेबल का विश्लेषण करने और अनसुलझे (undefined) सिंबलों की खोज के लिए:
    1
    
    nm -u myprogram        # केवल अनिर्धारित सिंबल देखना
    
  • objdump: मशीन कोड को डिसअसेंबल करने और रिलोकेशन रिकॉर्ड्स की जांच के लिए:
    1
    2
    
    objdump -d main.o      # डिसअसेंबली देखना
    objdump -r main.o      # रिलोकेशन रिकॉर्ड्स देखना
    
  • ldd: यह जांचने के लिए कि निष्पादन योग्य बाइनरी किन डायनामिक शेयर्ड लाइब्रेरीज़ पर निर्भर है:
    1
    
    ldd /bin/bash          # साझा लाइब्रेरी निर्भरता
    
comments powered by Disqus