प्रस्तावना: सॉफ्टवेयर निर्माण के अदृश्य शिल्पी
दैनिक सॉफ्टवेयर विकास में हम अक्सर यह कमांड चलाते हैं:
| |
इस पूरी प्रक्रिया को बोलचाल में केवल “कंपाइलेशन” कह दिया जाता है। परंतु कंपाइलर का काम केवल आधा सफर तय करना है। शेष आधा सफर—दर्जनों अलग-अलग ऑब्जेक्ट फाइलों और लाइब्रेरी को आपस में जोड़ना, मेमोरी पते निर्धारित करना और प्रोग्राम को रैम में लोड करना—लिंकर (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;)।
नियम:
- एक ही नाम के दो मजबूत सिंबल होने पर लिंकर एरर देता है (
multiple definition)। - मजबूत सिंबल हमेशा कमजोर पर भारी पड़ता है।
- दो कमजोर सिंबलों में लिंकर किसी एक को मनमाने ढंग से चुन लेता है।
लाइब्रेरी क्रम: लिंकर तर्कों को बाएं से दाएं एक ही बार पढ़ता है, इसलिए -lm को हमेशा main.c के बाद लिखना चाहिए।
3. रिलोकेशन (Relocation)
कंपाइलर बाहरी फंक्शन कॉल के स्थान पर शून्य लिख देता है:
| |
($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 2readelf -h myprogram # फ़ाइल हेडर की जांच readelf -l myprogram # प्रोग्राम हेडर (मेमोरी सेगमेंट्स)nm: सिंबल टेबल का विश्लेषण करने और अनसुलझे (undefined) सिंबलों की खोज के लिए:1nm -u myprogram # केवल अनिर्धारित सिंबल देखनाobjdump: मशीन कोड को डिसअसेंबल करने और रिलोकेशन रिकॉर्ड्स की जांच के लिए:1 2objdump -d main.o # डिसअसेंबली देखना objdump -r main.o # रिलोकेशन रिकॉर्ड्स देखनाldd: यह जांचने के लिए कि निष्पादन योग्य बाइनरी किन डायनामिक शेयर्ड लाइब्रेरीज़ पर निर्भर है:1ldd /bin/bash # साझा लाइब्रेरी निर्भरता
