- सिर्फ GCC से बने
./a.out बाइनरी का आकार घटाने का प्रयोग इस शर्त से शुरू होता है कि रन सफल हो, exit code 0 हो, और post-processing वर्जित हो
- बेसिक
int main(){ return 0; } का आकार 15,816 बाइट था, और -s से debug information हटाकर इसे 14,352 बाइट तक घटाया गया
-nostartfiles से main से पहले का startup code छोड़ा गया, और -nostdlib -static -no-pie के साथ सीधे SYS_exit system call इस्तेमाल करके dynamic linking आधारित संरचना हटाई गई
.comment, .eh_frame, .note.gnu.property को क्रमशः -fno-ident, -fno-exceptions -fno-asynchronous-unwind-tables, -Wa,-mx86-used-note=no से हटाकर section overhead कम किया गया
-Wl,--nmagic से 0x1000 alignment padding घटाने पर अंतिम बाइनरी 400 बाइट की रह गई, और objcopy जैसी post-processing इस दायरे से बाहर है
लक्ष्य और बुनियादी शर्तें
- लक्ष्य है संभवतः सबसे छोटे आकार की
./a.out बाइनरी बनाना
- प्रोग्राम के लिए तीन शर्तें हैं
./a.out सफलतापूर्वक चलना चाहिए
$? निश्चित रूप से 0 होना चाहिए
- बाइनरी सिर्फ GCC से बननी चाहिए;
objcopy, hex editor, manual patch जैसी post-processing वर्जित है
- शुरुआती बिंदु सबसे सरल प्रोग्राम है
// compiled with gcc empty.c
int main() {
return 0;
}
- इस बेसिक प्रोग्राम का फ़ाइल आकार
stat के अनुसार 15,816 बाइट है, और तुलना के लिए कहा गया है कि कुछ भी न करने वाले इस बाइनरी को रखने के लिए Apollo guidance computer की RAM के चार हिस्सों जितनी जगह चाहिए
file a.out का आउटपुट ELF 64-bit LSB pie executable, dynamically linked, interpreter path, और not stripped स्थिति दिखाता है
not stripped स्थिति घटाने के लिए GCC का -s flag इस्तेमाल करने पर compile करते समय debug information नहीं रखी जाती, और आकार 14,352 बाइट रह जाता है
startup code को बायपास करना और dynamic linking हटाना
// compiled with gcc empty.c -s -nostartfiles
#include <cstdlib>
extern "C" __attribute((noreturn)) void _start() { exit(0); }
- इस बदलाव के बाद आकार 13,632 बाइट है, यानी कमी बहुत बड़ी नहीं है
objdump -x a.out का आउटपुट dynamic section के साथ NEEDED libc.so.6, interpreter path, dynamic symbol table, relocation metadata, PLT/GOT संरचना, और shared library references दिखाता है
- क्योंकि प्रोग्राम का लक्ष्य सिर्फ तुरंत exit करना है, इसलिए तीन flags से बड़े components हटाए गए
-nostdlib: standard library link नहीं की जाती
-static: dynamic linking संरचना से बचता है
-no-pie: position-independent executable की जगह fixed-address executable बनाता है
// compiled with gcc -static -nostdlib -no-pie -s empty.c
extern "C" __attribute__((noreturn)) void _start() {
__asm__ volatile(
"mov $60, %%eax\n" // SYS_exit
"xor %%edi, %%edi\n" // exit status 0
"syscall\n" ::
: "rax", "rdi");
__builtin_unreachable();
}
- सीधे
SYS_exit system call करने के बाद आकार 8,704 बाइट रह जाता है
बचे हुए sections हटाना
objdump -D a.out के आउटपुट में .note.gnu.property, .text, .eh_frame, .comment जैसे sections अभी भी मौजूद हैं
.comment section बाइनरी बनाने वाले compiler की जानकारी रखता है, और इस मामले में इसमें GCC: (GNU) 15.2.0 string है
objdump इस data को assembly की तरह पढ़कर अजीब instructions जैसा दिखाता है
-fno-ident जोड़ने पर .comment section हट जाता है और आकार 8,616 बाइट हो जाता है
.eh_frame section stack unwinding के लिए होता है, और कुछ भी न करने वाले प्रोग्राम में error handling के लिए इसकी ज़रूरत नहीं है
-fno-exceptions -fno-asynchronous-unwind-tables से आकार 4KB के दायरे में आ जाता है
- अंत में हटाया जाने वाला section
.note.gnu.property है
readelf -n a.out में x86 feature used: x86, x86 ISA used: x86-64-baseline properties दिखती हैं
- GNU इस section में notes छोड़ता है ताकि दूसरे tools उन्हें पढ़ सकें, और इस मामले में assembler यह note जोड़ रहा था
-Wa,-mx86-used-note=no जोड़ने पर आकार 4,320 बाइट हो जाता है
- इस बिंदु पर
objdump -D a.out में सिर्फ .text section की instructions दिखती हैं
401000: 55 push %rbp
401001: 48 89 e5 mov %rsp,%rbp
401004: b8 3c 00 00 00 mov $0x3c,%eax
401009: 31 ff xor %edi,%edi
40100b: 0f 05 syscall
alignment padding और 400-बाइट संरचना
- 4,320-बाइट स्थिति में
readelf -a a.out का आउटपुट ELF header, 3 program headers, 3 section headers, .text, .shstrtab संरचना दिखाता है
- program header वह table है जो OS loader को बताती है कि प्रोग्राम शुरू होते समय फ़ाइल को memory segments में कैसे map करना है
- उस आउटपुट में
LOAD के 232 बाइट, 64-बाइट ELF header और 56-बाइट के 3 program headers के बराबर हैं
LOAD entry की alignment requirement 0x1000 है, इसलिए linker .text को padding के बाद रखता है
-Wl,--nmagic से linker को यह मानकर न चलने को कहा जाता है, जिससे ELF metadata और .text section को साथ map किया जा सकता है, सिर्फ एक LOAD बचता है, और आकार 400 बाइट रह जाता है
- 400-बाइट बाइनरी की संरचना इस प्रकार है
| घटक |
आकार |
| ELF header |
64 B |
Program header: PT_LOAD |
56 B |
Program header: PT_GNU_STACK |
56 B |
.text section contents |
11 B |
.shstrtab section contents, "\0.shstrtab\0.text\0" |
17 B |
| section header के लिए padding |
4 B |
Section header [0]: NULL |
64 B |
Section header [1]: .text |
64 B |
Section header [2]: .shstrtab |
64 B |
PT_LOAD instructions को load करने के लिए ज़रूरी है, और PT_GNU_STACK GCC हमेशा बनाता है
.shstrtab को सिर्फ GCC से हटाया नहीं जा सकता
- पहला section header entry System V ABI ELF specification के अनुसार value 0 वाले undefined section index
SHN_UNDEF के लिए reserved होना चाहिए
- वास्तव में यह entry
SHT_NULL type की है, इसलिए tools में NULL section के रूप में दिखती है
objcopy जैसे tools कुछ entries को और काट सकते हैं, लेकिन वह तरीका इस दायरे से बाहर है
चरण-दर-चरण आकार और अंतिम कोड
| चरण |
flag / बदलाव |
आकार |
सामान्य main |
gcc empty.c |
15,816 बाइट |
| symbols हटाना |
-s |
14,352 बाइट |
| Freestanding |
-nostartfiles |
13,632 बाइट |
| libc हटाना / static link / no PIE |
-nostdlib -static -no-pie |
8,704 बाइट |
.comment section हटाना |
-fno-ident |
8,616 बाइट |
| unwind information हटाना |
-fno-asynchronous-unwind-tables -fno-exceptions |
4,400 बाइट |
| GNU property note हटाना |
-Wa,-mx86-used-note=no |
4,320 बाइट |
| alignment कम करना |
-Wl,--nmagic / -Wl,-n |
400 बाइट |
- अंतिम compile command और code इस प्रकार हैं
// gcc -Wl,--nmagic -Wa,-mx86-used-note=no -static -nostdlib -no-pie -s -fno-ident -fno-exceptions -fno-asynchronous-unwind-tables empty.c
extern "C" __attribute__((noreturn)) void _start() {
__asm__ volatile(
"mov $60, %%eax\n" // SYS_exit
"xor %%edi, %%edi\n" // exit status 0
"syscall\n" ::
: "rax", "rdi");
__builtin_unreachable();
}
- यह
objdump और ld का पहला व्यावहारिक प्रयोग था, और -fno-asynchronous-unwind-tables -fno-exceptions GCC को बताता है कि error होने पर stack unwinding की ज़रूरत नहीं है
ld में --no-eh-frame-hdr flag भी है
- reddit पर इसे 124 बाइट तक घटाने का एक उदाहरण भी है
1 टिप्पणियां
Lobste.rs की रायें
अगर आखिरकार सिर्फ assembly ही इस्तेमाल करनी है, तो C compiler क्यों इस्तेमाल किया जा रहा है, समझ नहीं आता
बस मज़े के लिए किया गया एक प्रयोग है :)
assembly शुरुआत के लिए बहुत अच्छी है। मेरे पास यहाँ compile किया हुआ 231-byte hello world binary है:
https://github.com/Cons-Cat/libCat/blob/main/examples%2Fhello.cpp
मैंने कुछ साल पहले एक मिलते-जुलते tutorial से शुरुआत की थी, और उसके बाद code को बेहतर तरीके से अलग करते हुए, simple cases का overhead जितना हो सके उतना कम रखते हुए आसपास की techniques को धीरे-धीरे जोड़ा। 231 bytes बनाए रखना ज़रूरी है, इसलिए इसे सुनिश्चित करने के लिए CI test भी रखा है
संपादन: अभी ध्यान गया कि एक अनावश्यक include छूट गया था। उसे ठीक करना होगा
सहमत हूँ। फिर भी काफ़ी C-specific tricks हैं, और थोड़ा assembly न होता तो शायद पूरी तस्वीर पूरी नहीं होती
संबंधित लिंक: https://www.muppetlabs.com/~breadbox/software/tiny/
यहाँ वास्तव में 45-byte binary है। चरम स्थिति में, शायद सिर्फ
dbकी सूची से ही assembly में encode किया जा सकता है, औरgccसे उसे फिर 45-byte “raw” file के रूप में assemble कराया जा सकता हैसंयोग से वह ELF बन जाएगा, लेकिन
gccको यह जानने की ज़रूरत नहीं है। तब शायद यह मूल लेख के नियमों को पूरा कर देहालाँकि ज़्यादातर उचित परिभाषाओं में उसे C binary कहना मुश्किल हो जाएगा
जवाब शायद compiler पर निर्भर करेगा। लेकिन सिर्फ इसलिए कि कुछ C compilers उसे स्वीकार कर लेते हैं, क्या non-C code पर निर्भर रहने को मान्य माना जा सकता है, यह निश्चित नहीं है 😉
exit(3)को call करने वाले C++ program औरSYS_exitassembler call के बीच एक मध्यवर्ती चरण है। जैसा कि manual section number से पता चलता है,exit(3)एक library function है, इसलिए यहatexit(3)mechanism आदि के साथ बहुत साराlibcखींच लाता हैraw exit system call को बुलाने का standard तरीका
_exit(2)है, और अगर उसे_start()में डालकर static link किया जाए तो काफ़ी छोटा result आना चाहिए। C++ की जगह C में लिखने से compiler invocation और source code size भी कम हो सकती हैमैंने ठीक ऐसा ही करके देखा
#include <stdlib.h>void _start(void){_Exit(0); /* C99 function to call SYS_exit() */}gcc -Os -nostdlib -static -o x x.c -lcसे compile करने पर stripped executable का आकार 8912 bytes था, लेकिन वास्तव में generate हुआ code सिर्फ 96 bytes था। ऐसा इसलिए क्योंकि_Exit()के लिए एक genericsyscall()function शामिल हो गया था