- Linux में एक छोटा C
hello प्रोग्राम भी ELF executable फ़ाइल बनता है, और readelf, nm, objdump से उसकी आंतरिक संरचना सीधे देखी जा सकती है
- executable फ़ाइल को समझने के मुख्य आधार symbol, section और segment हैं, और ये क्रमशः function linking, code/data विभाजन और रनटाइम memory layout का काम संभालते हैं
objdump और readelf का उपयोग करके .text, .rodata, .data, .bss, .interp जैसी sections के bytes और properties देखे जा सकते हैं
- प्रोग्राम सीधे
main से शुरू नहीं होता, बल्कि पहले _start में प्रवेश करता है, फिर कई initialization steps के बाद main को call करता है
- executable फ़ाइल कोई “पढ़ी न जा सकने वाली गुत्थी” नहीं है, बल्कि एक तय format वाली फ़ाइल है, इसलिए tools की मदद से code, string और linking जानकारी को चरण-दर-चरण ट्रेस किया जा सकता है
executable फ़ाइल एक पढ़ी जा सकने वाली file format है
- compile की गई executable फ़ाइल शुरू में पढ़ी न जा सकने वाली “जादुई binary” जैसी लग सकती है, लेकिन वास्तव में यह एक समझी जा सकने वाली file format है
- उदाहरण Linux की ELF binary पर आधारित है, और क्योंकि binary platform-dependent होती है, इसलिए यह व्याख्या भी platform से बंधी हुई है
- इस्तेमाल किया गया उदाहरण यह C प्रोग्राम है
#include <stdio.h>
int main() {
printf("Penguin!\n");
}
gcc -o hello hello.c से compile करके hello executable बनाई जाती है, फिर उसके अंदर की संरचना देखी जाती है
- पूरी चर्चा तीन concepts के आसपास आगे बढ़ती है
- symbol:
printf जैसे कहीं और defined function को call करते समय उसकी location खोजने में इस्तेमाल होते हैं
- section: code और data को अलग करने की इकाइयाँ, जैसे
.text, .data, .rodata
- segment: sections को रनटाइम memory layout की इकाइयों में समूहित करते हैं
text के रूप में खोलने पर भी कुछ संकेत मिलते हैं
cat hello की तरह executable फ़ाइल को सीधे खोलने पर ज़्यादातर output टूटी-फूटी characters जैसा दिखता है
- फिर भी output में
Penguin! और ELF जैसी strings मिल सकती हैं
- ELF इस binary की file format का नाम है
- ज़्यादातर output इंसानों के लिए पढ़ना कठिन इसलिए है क्योंकि executable फ़ाइल binary data होती है
symbol table से function नाम और linking की जाँच होती है
readelf --symbols hello executable की symbol table दिखाता है
- उदाहरण output में कुछ मुख्य symbols दिखाई देते हैं
main: आपने जो main() function लिखा है उसका address
puts@@GLIBC_2.2.5: यह code में call किए गए printf से जुड़ा reference लगता है, और अनुमान है कि compiler optimization के कारण इसे puts में बदल दिया गया
_start: प्रोग्राम startup से जुड़ा एक महत्वपूर्ण symbol
- प्रोग्राम सीधे
main से शुरू नहीं होता, बल्कि वास्तव में _start में प्रवेश करता है
_start कई महत्वपूर्ण काम करता है, जिनमें main को call करना भी शामिल है
symbol linking को संभव बनाते हैं
- अगर आप प्रोग्राम में
hello नाम का function लिखते हैं, तो compiled binary में उस function के code के साथ hello नाम का symbol जुड़ता है
printf जैसी library function को call करने के लिए उसके code की location खोजने का कोई तरीका होना चाहिए
- function location खोजने की इस प्रक्रिया को linking कहा जाता है
- अगर यह compile के तुरंत बाद हो, तो static linking
- अगर यह प्रोग्राम के चलने के समय हो, तो dynamic linking
libc में C standard library functions होती हैं
- अगर
nm libc पर “no symbols” दिखाए, तब भी objdump -tT /lib/x86_64-linux-gnu/libc-2.15.so से symbols देखे जा सकते हैं
- libc की symbol table में
sprintf, strlen, fork, exec जैसी functions देखी जा सकती हैं
hello द्वारा puts को call करना और libc की symbol table में puts की location ढूँढना, इस flow के ज़रिए dynamic linking के काम करने का तरीका समझा जा सकता है
section code और data को अलग करते हैं
objdump -s hello executable की हर section में मौजूद bytes को hexadecimal और ASCII में दिखाता है
- मुख्य sections इस प्रकार हैं
.text: प्रोग्राम का वास्तविक code, यानी assembly, और इसमें _start व main शामिल होते हैं
.rodata: read-only data रखता है, और उदाहरण में इसमें "Penguin!" string है
.interp: dynamic linker फ़ाइल का नाम रखता है
- section और segment अलग-अलग चरणों में उपयोग होते हैं
- section को linking के समय
ld इस्तेमाल करता है
- segment को execution के समय इस्तेमाल किया जाता है
readelf --sections hello से section metadata और विस्तार से देखा जा सकता है
- उदाहरण के flags से हर section का स्वभाव समझ आता है
.text: executable और read-only
.rodata: read-only
.data: read/write
.bss: writable data area
disassembly से machine code को assembly में देखा जाता है
.text section में वे bytes होते हैं जिन्हें CPU code के रूप में समझकर execute करता है
- उदाहरण में
.text के शुरुआती bytes 31 ed का अर्थ इंसान तुरंत नहीं समझ सकता, इसलिए disassembler की ज़रूरत होती है
objdump -d ./hello .text section को disassemble करके assembly instructions के रूप में दिखाता है
- उदाहरण output में
31 ed को xor %ebp,%ebp के रूप में दिखाया जाता है
- इस तरीके से binary के अंदर मौजूद code bytes किस assembly instruction से मेल खाते हैं, यह देखा जा सकता है
segment रनटाइम memory layout तय करते हैं
- executable फ़ाइल segment या program headers से भी बनी होती है
readelf --segments hello प्रोग्राम के segments और section-segment mapping दिखाता है
- segment यह तय करने के लिए उपयोग होते हैं कि प्रोग्राम के अलग-अलग हिस्सों को memory में कैसे बाँटकर रखा जाए
- उदाहरण में दो मुख्य
LOAD segments हैं
- पहला
LOAD: R E के रूप में दिखता है, यानी यह readable और executable है
- दूसरा
LOAD: RW के रूप में दिखता है, यानी यह readable और writable है
.text को पढ़ा और execute किया जाना चाहिए, लेकिन लिखा नहीं जाना चाहिए, इसलिए वह पहले segment में जाता है
.data और .bss writable होने चाहिए, लेकिन उन्हें execute करने की ज़रूरत नहीं होती, इसलिए वे दूसरे segment में जाते हैं
आगे देखने लायक tools और सामग्री
- ELF executable कोई विशेष जादू नहीं, बल्कि एक सामान्य file format है, और Linux binaries की जाँच
readelf, nm, objdump से की जा सकती है
- संबंधित सामग्री
1 टिप्पणियां
Hacker News की राय
जैसा कि मैंने दूसरे थ्रेड में भी कहा था https://news.ycombinator.com/item?id=38847750#38862450, ELF को एक बार हाथ से लिखकर देखने की मैं ज़ोरदार सिफारिश करता हूँ
यह executable file के बुनियादी घटकों को समझने का अच्छा अभ्यास है, और इस लेख के उलट ऊपर से नीचे नहीं बल्कि नीचे से ऊपर की दिशा में समझना हो तब भी यह मददगार है
उस दूसरे HN लेख के कई थ्रेड्स में भी अच्छी चर्चा है
format को खुद और दूसरों को समझाने के लिए मैंने file के bytes दिखाने वाला एक interactive visualization भी बनाया
bytes पर click करने से उसका विवरण आता है, और file के भीतर जुड़े हुए bytes highlight होते हैं, जिससे समझने में मदद मिलती है: https://scratchpad.avikdas.com/elf-explanation/elf-explanati...
dynamic linking में implementation की जटिलता काफ़ी होती है, लेकिन अगर सिर्फ static ELF support करना हो तो यह काफ़ी सीधा है
शुरू में जब वह काम नहीं करता तो debugging लगभग असंभव लगती है, इसलिए काफ़ी झुंझलाहट होती है, लेकिन जब आखिरकार वह चलने लगता है तो बहुत शानदार लगता है
ELF को दिलचस्प तरीकों से patch किया जा सकता है, और auxiliary vector का उपयोग करके runtime में खुद का निरीक्षण भी किया जा सकता है
Linux program header table का address दे देता है, इसलिए वहाँ से कहीं भी पहुँचा जा सकता है, और LOAD segment को बढ़ाकर पूरे binary को कवर कराया जा सकता है
उदाहरण के लिए, मैंने अपने Lisp interpreter executable के भीतर सीधे Lisp modules और code embed करने वाला एक tool बनाया था
embed किया गया segment ELF से अपने-आप load हो जाता है, और interpreter उसे ढूँढकर चलाता है
यह छोटा-सा feature मुझे इतना पसंद आया कि मैंने इस पर एक लेख भी लिखा: https://www.matheusmoreira.com/articles/self-contained-lone-...
अच्छा होता अगर mainstream भाषाएँ भी इस तरीके को अपनातीं
documentation पढ़ना, processor datasheet से ज़रूरी bytes चुनना, उन्हें अलग-अलग sections में क्रम से रखना, और ELF fields भरना — अंत में मामला सब कुछ खुद दर्ज करने पर ही आ टिकता है
ELF से पहले के दौर में 8-bit Apple II जैसे माहौल में machine language monitor program bytes को सीधे दर्ज करने देता था, और वही bytes चल जाते थे
उसे disk पर सहेजना बस थोड़ा अधिक जटिल था, और यहाँ भी एक और अवसर मौजूद है
disk sector editor से files बनाई जा सकती हैं, और इस तरह सिलसिला आगे बढ़ता जाता है
मेरी समझ से
mainsymbol C-विशेष चीज़ है_startsymbol भाषा-स्वतंत्र binary entry point है, और इस मामले में वहीmainको call करता हैअगर कोई परंपरा ऐसी होती कि entry point को
_startकहा जाए और वहींmainकाargc/argvदिया जाए, तो format की लचीलापन काफ़ी कम हो जाती_startनाम भी कोई विशेष चीज़ नहीं हैbinary header में entry point address लिखता है, और operating system उसी address से execution शुरू करता है
उस symbol को
_startकहना बस C और दूसरी भाषाओं की परंपरा है, और linker ELF header लिखते समय entry point set करने के लिए इसका उपयोग करता हैअगर आप खुद linker script लिखें, तो entry point का नाम अपनी पसंद से रख सकते हैं
mainsymbol सही मायने में सिर्फ hosted C में दिया जाता हैfreestanding C में आप अपनी पसंद का entry point रख सकते हैं
_startभी बस linker का default है, और-Wl,--entry="${symbol}"से बेहतर symbol दिया जा सकता है, जबकि GCC बिना बदसूरत-Wlके भी इसे सीधे set करने का support करता हैइसके अलावा entry point वास्तव में symbol नहीं बल्कि pointer है
linker सिर्फ दिए गए symbol का address लेकर उसे ELF entry point के रूप में set करता है
argument count और argument vector के अलावा stack पर environment vector और auxiliary vector भी होते हैं
process startup code stack से इन मानों को निकालकर सही registers में रख सकता है और फिर मनचाहा C function call कर सकता है — यह इतना ही सरल हो सकता है
entry point खुद function नहीं होता, इसलिए उसके लौटने की कोई जगह नहीं होती
entry point code को
mainके status code लौटाने पर process को साफ़-सुथरे ढंग से बंद करने के लिएexitsystem call पर खत्म होना चाहिएकम-से-कम Linux में यह ऐसे ही काम करता है
Rust/C/C++ जैसी भाषाओं में linker flags के ज़रिए initialize किए जाने वाले variables inject भी किए जा सकते हैं
अगर program dynamic linking का उपयोग करता है, तो मेरी समझ में
_startसे पहले linker runtime चलकर links resolve करता है और फिर control_startको सौंप देता हैआखिरकार यह extensibility देने के लिए समय के साथ जुड़ते गए hack के ऊपर hack ही हैं, लेकिन सामाजिक रूप से इतना स्वीकार किए जा चुके हैं और इतने अच्छे से काम करते हैं कि आज भी इस्तेमाल हो रहे हैं
2012 में अपनी अकादमिक दिशा गणित से computer science की ओर बदलते समय मैंने ब्लॉग शुरू किया था, और यह विषय सचमुच उन पहली चीज़ों में था जिन्हें मैंने पढ़ा था: https://heinrichhartmann.com/archive/Dissecting-Hello-World....
इस गहरे rabbit hole में उतरने का मुझे कभी अफ़सोस नहीं हुआ
अगर मुझे सही याद है, तो Julia की background भी गणित में है
शायद गणित वाले लोग ऐसे प्रयोगों की ओर इसलिए खिंचते हैं क्योंकि उनमें नींव से तर्क करने की इच्छा होती है
अच्छा लगा कि उन्होंने इस विषय को बहुत से लोगों के लिए सुलभ बना दिया
Julia की लिखी चीज़ें हमेशा बेहतरीन होती हैं
compiled code कोई राज़ छिपा नहीं सकता — यह सिखाने के लिए
stringsका demo देना हमेशा असरदार रहा हैकिसी बेचारे व्यक्ति पर सिर्फ़ इसलिए जुर्माना लगा दिया गया क्योंकि उसने binary पर
stringsचलाने जैसी ही विधि से password ढूँढ लिया थाjudges ने माना कि उसने software की security measure को “bypass” किया था: https://www.theregister.com/2024/01/19/germany_fine_security...
यह न आलोचना है, न ही बाल की खाल निकालना, बस यूँ ही एक विचार आया
“binary लगभग platform-specific की परिभाषा है, इसलिए यह पूरी चीज़ भी platform-specific है” यह पंक्ति पढ़कर मुझे वह समय याद आ गया जब Actually Portable Executable ने दिखाया था कि एक ही binary कई platforms पर चल सकती है
मैं अब तक उस surreal पल से मानसिक रूप से पूरी तरह उबर नहीं पाया हूँ
दशकों तक हमने Java, cross-platform libraries और न जाने कितने fractal-जैसे तरीकों से cross-platform समस्या सुलझाने की कोशिश की, जबकि समाधान शायद शुरू से ही हमारी आँखों के सामने था
तेज़ computers के इस दौर में मुझे source distribution और local compile, binary distribution से बेहतर लगते हैं
दुख की बात है कि जिस software पर हम निर्भर हैं, उसका काफ़ी हिस्सा बहुत बड़ा है, और compilers तुलनात्मक रूप से धीमे हैं, इसलिए binary distribution एक तरह की necessary evil बन जाती है
portable binaries की बजाय मैं चाहूँगा कि ज़्यादा effort ऐसे सरल software components और तेज़ compilers पर लगे जो स्वाभाविक रूप से जल्दी compile हों
यह एक script है जो किसी भी system पर चल सकती है, और वही script binary को load कर सकती है
अगर मुझे सही याद है, तो शुरुआती version को load होने से पहले base64 से decode करना पड़ता था
यानी यह एक executable binary loader के ज़्यादा क़रीब है
1990 के शुरुआती दशक में मैं executable file formats से इतना मोहित था कि मैंने कुछ हफ़्ते लगाकर Modula 2 में DOS और Windows executable file viewer बनाया, उसका नाम VEXE रखा, और 1991 में उसे shareware के रूप में जारी किया
यह tool crackers के बीच एक niche लोकप्रियता तक पहुँचा, यहाँ तक कि +ORC tutorial में भी इसका ज़िक्र हुआ: https://gist.github.com/callowaysutton/48bdf0245e17e72d41a15...
शायद इसलिए कि यह program की reverse engineering रोकने के लिए इस्तेमाल की जाने वाली कई encryption और compression schemes का पता लगा सकता था
अगर आप जानना चाहते हैं कि ELF binary file कितनी छोटी हो सकती है, तो यह मज़ेदार लेख आपको पसंद आ सकता है: https://www.muppetlabs.com/~breadbox/software/tiny/teensy.ht...
ELF को SQL से एक्सप्लोर करने देने वाला मेरा tool भी देख सकते हैं: https://github.com/fzakaria/sqlelf
क्या Python background मज़बूत होने वाले किसी व्यक्ति के लिए व्यावहारिक low-level programming की शुरुआती सामग्री या किताबें सुझाई जा सकती हैं
मैंने हाल ही में Rust सीखना शुरू किया है, और महसूस हुआ कि बहुत कुछ है जिसकी भरपाई करनी है
मैंने कभी compiler course नहीं किया, इसलिए शायद बहुत-सी जानकारी मुझसे छूट रही है
उदाहरण के लिए, मुझे यह भी नहीं पता था कि binaries में symbols जैसी चीज़ होती है, और न ही ELF और Mach-O के फ़र्क़ के बारे में मालूम था
terminal में binary को
catकरना दुख तक पहुँचने का शॉर्टकट हैमुझे
| hdपसंद है, जो मूलतःhexdump -Cही है, हालाँकि नंगी आँखों से देखने पर वह भी उतना ही दुरूह लगता है