1 puan yazan GN⁺ 5 시간 전 | 1 yorum | WhatsApp'ta paylaş
  • Yalnızca GCC ile üretilen ./a.out ikilisinin boyutunu küçültme deneyi; çalıştırmanın başarılı olması, çıkış kodunun 0 olması ve sonradan işleme yasak olması koşullarıyla başlıyor
  • Temel int main(){ return 0; } 15.816 bayttı; -s ile debug bilgisi kaldırılarak 14.352 bayta indirildi
  • -nostartfiles ile main öncesi başlangıç kodu atlanıyor; -nostdlib -static -no-pie ve doğrudan SYS_exit sistem çağrısı kullanılarak dinamik bağlama tabanlı yapı kaldırılıyor
  • .comment, .eh_frame, .note.gnu.property bölümleri sırasıyla -fno-ident, -fno-exceptions -fno-asynchronous-unwind-tables, -Wa,-mx86-used-note=no ile kaldırılarak bölüm ek yükü azaltılıyor
  • -Wl,--nmagic ile 0x1000 hizalama dolgusunu azaltan nihai ikili 400 bayt ve objcopy gibi sonradan işleme adımları kapsam dışında

Hedef ve temel koşullar

  • Hedef, mümkün olan en küçük ./a.out ikilisini üretmek
  • Programın üç koşulu var
    • ./a.out başarıyla çalışmalı
    • $? değeri deterministik olarak 0 olmalı
    • İkili yalnızca GCC ile üretilmeli; objcopy, hex editör veya elle yama gibi sonradan işleme yasak
  • Başlangıç noktası en basit program
// compiled with gcc empty.c
int main() {
return 0;
}
  • Bu temel programın dosya boyutu stat ölçümüne göre 15.816 bayt; hiçbir şey yapmayan bir ikiliyi barındırmak için Apollo guidance computer RAM’inden dört tane gerektiği benzetmesi yapılıyor
  • file a.out çıktısı ELF 64-bit LSB pie executable, dynamically linked, yorumlayıcı yolu ve not stripped durumunu gösteriyor
  • not stripped durumunu küçültmek için GCC’nin -s bayrağı kullanılıyor; bu sayede debug bilgisi tutulmadan derleniyor ve boyut 14.352 bayta düşüyor
Reklam

Başlangıç kodunu atlamak ve dinamik bağlamayı kaldırmak

// compiled with gcc empty.c -s -nostartfiles
#include <cstdlib>
extern "C" __attribute((noreturn)) void _start() { exit(0); }
  • Bu değişiklikten sonra boyut 13.632 bayt ve azalma çok büyük değil
  • objdump -x a.out çıktısı; dinamik bölümle birlikte NEEDED libc.so.6, yorumlayıcı yolu, dinamik sembol tablosu, relocation meta verisi, PLT/GOT yapısı ve paylaşımlı kütüphane referanslarını gösteriyor
  • Programın amacı yalnızca hemen çıkmak olduğu için üç bayrakla büyük bileşenler kaldırılıyor
    • -nostdlib: standart kütüphaneleri bağlama
    • -static: dinamik bağlama yapısından kaçınma
    • -no-pie: konumdan bağımsız yürütülebilir yerine sabit adresli yürütülebilir üretme
// 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();
}
  • Doğrudan SYS_exit sistem çağrısı yapacak şekilde değiştirildikten sonra boyut 8.704 bayt oluyor
Reklam

Kalan bölümleri kaldırmak

  • objdump -D a.out çıktısında .note.gnu.property, .text, .eh_frame, .comment gibi bölümler hâlâ bulunuyor
  • .comment bölümü, ikiliyi üreten derleyici bilgisini saklıyor; bu örnekte GCC: (GNU) 15.2.0 dizgesini içeriyor
    • objdump bu veriyi assembly gibi yorumlayıp garip komutlar olarak gösterebiliyor
    • -fno-ident eklendiğinde .comment bölümü kaldırılıyor ve boyut 8.616 bayta düşüyor
  • .eh_frame bölümü stack unwinding için kullanılıyor; hiçbir şey yapmayan bir programda hata işleme açısından gerekli değil
    • -fno-exceptions -fno-asynchronous-unwind-tables kullanıldığında boyut 4 KB seviyesine iniyor
  • Son kaldırılacak hedef .note.gnu.property bölümü
    • readelf -n a.out; x86 feature used: x86, x86 ISA used: x86-64-baseline özelliklerini gösteriyor
    • GNU, diğer araçların okuyabilmesi için bu bölümde notlar bırakıyor; burada notu ekleyen bileşen assembler
    • -Wa,-mx86-used-note=no eklendiğinde boyut 4.320 bayt oluyor
  • Bu noktadaki objdump -D a.out, yalnızca .text bölümündeki komutları gösteriyor
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
Reklam

Hizalama dolgusu ve 400 baytlık yapı

  • 4.320 bayt durumundaki readelf -a a.out çıktısı; ELF header, 3 program header, 3 section header, .text, .shstrtab yapısını gösteriyor
  • Program header, işletim sistemi yükleyicisinin program başlangıcında dosyayı bellek segmentlerine nasıl eşleyeceğini söyleyen tablo
  • İlgili çıktıda LOAD 232 bayt; bu da 64 baytlık ELF header ile 56 baytlık 3 program header’a karşılık geliyor
  • LOAD girdisinin hizalama gereksinimi 0x1000 olduğu için linker .text bölümünü dolgunun arkasına yerleştiriyor
  • -Wl,--nmagic ile linkera bu varsayımı yapmaması söylenince ELF meta verisi ile .text bölümü birlikte eşlenebiliyor, tek bir LOAD kalıyor ve boyut 400 bayta düşüyor
  • 400 baytlık ikilinin yapısı şöyle
Yapı Boyut
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 için padding 4 B
Section header [0]: NULL 64 B
Section header [1]: .text 64 B
Section header [2]: .shstrtab 64 B
  • PT_LOAD, komutları yüklemek için gerekli; PT_GNU_STACK ise GCC tarafından her zaman üretiliyor
  • .shstrtab, yalnızca GCC ile kaldırılamıyor
  • İlk section header girdisinin, System V ABI ELF specification gereği tüm değerleri 0 olan tanımsız bölüm indeksi SHN_UNDEF için ayrılmış olması gerekiyor
  • Gerçekte bu girdi SHT_NULL tipinde olduğu için araçlarda NULL bölümü olarak görünüyor
  • objcopy gibi araçlar bazı girdileri daha da kesebilir, ancak bu yaklaşım kapsam dışında

Adım adım boyutlar ve nihai kod

Aşama Bayrak / değişiklik Boyut
Normal main gcc empty.c 15.816 bayt
Sembolleri kaldırma -s 14.352 bayt
Freestanding -nostartfiles 13.632 bayt
libc kaldırma / statik bağlama / no PIE -nostdlib -static -no-pie 8.704 bayt
.comment bölümünü kaldırma -fno-ident 8.616 bayt
Unwind bilgisini kaldırma -fno-asynchronous-unwind-tables -fno-exceptions 4.400 bayt
GNU property note kaldırma -Wa,-mx86-used-note=no 4.320 bayt
Hizalamayı küçültme -Wl,--nmagic / -Wl,-n 400 bayt
  • Nihai derleme komutu ve kod şu şekilde
// 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();
}
  • Bu, objdump ve ld ile yapılan ilk pratik çalışma olmuş; -fno-asynchronous-unwind-tables -fno-exceptions, hata durumunda stack unwinding işlemesine ihtiyaç olmadığını GCC’ye bildiriyor
  • ld içinde --no-eh-frame-hdr bayrağı da var
  • reddit üzerinde bunu 124 bayta kadar indiren bir örnek de bulunuyor

1 yorum

 
GN⁺ 5 시간 전
Lobste.rs yorumları
  • Madem zaten assembly kullanacaksın, neden C derleyicisi kullandığını anlamıyorum

    • Sadece eğlencesine yapılan bir deney :)

    • Assembly başlangıç noktası olarak gayet iyi. Buradan derlenmiş 231 baytlık bir hello world ikili dosyam var:
      https://github.com/Cons-Cat/libCat/blob/main/examples%2Fhello.cpp

      Birkaç yıl önce benzer bir öğreticiyle başlamıştım; sonrasında kodu daha iyi ayırırken basit durumlarda ek yükü olabildiğince düşük tutup çevresindeki teknikleri kademeli olarak inşa ettim. 231 baytı korumak önemli olduğu için bunu garanti eden bir CI testi bile ekledim

      Düzenleme: Gereksiz bir include bıraktığımı şimdi fark ettim. Düzeltmem gerek

    • Katılıyorum. Yine de epeyce C'ye özgü hile var ve biraz assembly olmasaydı bütün resim tamamlanmış olmazdı gibi geliyor

  • İlgili bağlantı: https://www.muppetlabs.com/~breadbox/software/tiny/

    • Burada gerçekten 45 baytlık bir ikili var. Uç noktada, yalnızca db listesiyle assembly içinde kodlayıp sonra gcc ile bunu 45 baytlık “ham” bir dosya olarak tekrar assemble etmek bile mümkün olabilir
      Tesadüfen ELF olurdu ama gcc'nin bunu bilmesine gerek yok. Böylece orijinal metindeki kuralları karşılıyor olabilir

      Ancak çoğu makul tanıma göre buna artık C ikilisi demek zorlaşır

  • Yanıtın derleyiciye bağlı olduğunu düşünüyorum. Ama bazı C derleyicilerinin kabul etmesi, C olmayan koda dayanmayı meşru kılar mı, bundan emin değilim 😉

  • exit(3) çağıran bir C++ programı ile SYS_exit assembly çağrısı arasında bir ara aşama var. Kılavuzdaki bölüm numarasından da anlaşılacağı gibi exit(3) bir kütüphane fonksiyonu, dolayısıyla atexit(3) mekanizması gibi birçok libc parçasını da beraberinde getiriyor
    Ham exit sistem çağrısını yapmanın standart yolu _exit(2) kullanmak; bunu _start() içine koyup statik bağlarsan oldukça küçük bir sonuç çıkmalı. C++ yerine C ile yazarsan derleyici çağrısı ve kaynak kod boyutu da küçülebilir

    • Tam olarak bunu denedim

      #include <stdlib.h>
      void _start(void)
      {
      _Exit(0); /* C99 function to call SYS_exit() */
      }

      gcc -Os -nostdlib -static -o x x.c -lc ile derledim; strip edilmiş çalıştırılabilir dosya 8912 bayttı ama fiilen üretilen kod yalnızca 96 bayttı. Bunun nedeni _Exit() için genel amaçlı syscall() fonksiyonunun dahil edilmesiydi