1 पॉइंट द्वारा GN⁺ 5 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • सिर्फ 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 हटाना

  • ./a.out के चलने से int main() तक पहुँचने से पहले बहुत कुछ होता है, और यह विषय Matt Godbolt के CppCon के 1 घंटे के talk में भी शामिल है
  • -nostartfiles और _start() का उपयोग करके int main() से पहले की प्रक्रिया छोड़ते हुए freestanding बाइनरी में बदला गया
// 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 टिप्पणियां

 
GN⁺ 5 시간 전
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_exit assembler 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() के लिए एक generic syscall() function शामिल हो गया था