- 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
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
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
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
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
dblistesiyle assembly içinde kodlayıp sonragccile bunu 45 baytlık “ham” bir dosya olarak tekrar assemble etmek bile mümkün olabilirTesadüfen ELF olurdu ama
gcc'nin bunu bilmesine gerek yok. Böylece orijinal metindeki kuralları karşılıyor olabilirAncak ç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ı ileSYS_exitassembly çağrısı arasında bir ara aşama var. Kılavuzdaki bölüm numarasından da anlaşılacağı gibiexit(3)bir kütüphane fonksiyonu, dolayısıylaatexit(3)mekanizması gibi birçoklibcparçasını da beraberinde getiriyorHam 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üçülebilirTam 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 -lcile 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