BerandaComputers and TechnologyMenciptakan Tiny Executable di D

Menciptakan Tiny Executable di D

Dalam semangat “ Tutorial Whirlwind tentang Membuat Eksekusi ELF yang Benar-benar Remaja untuk Linux , ”kami akan mencoba membuat ELF 64 bit minimal yang dapat dieksekusi hanya dengan menggunakan standar D dan beberapa kode assembly inline.

Kami akan fokus pada pembuatan standar hello world yang dapat dieksekusi yang dipopulerkan oleh Brian Kernighan.

Dalam bahasa D standar, menggunakan pustaka phobos (sekarang satu-satunya pustaka standar yang sebenarnya untuk D, sebelumnya ada dua pustaka standar yang bersaing), kita akan menulis ini sebagai:

 impor std.stdio;  int main () {     tulis ("Halo  n");     kembali 0; } 

Menyusun ini:

  dmd basic_hello.d  

menghasilkan 1,9 juta yang dapat dieksekusi.

  wc -c ./basic_hello  1895528 ./basic_hello

Menggunakan gdc menurunkannya menjadi 28k. Catatan: gdc seperti gcc secara default memberi nama ‘a.out’ yang dapat dieksekusi, sedangkan dmd menamainya setelah file atau modul.

  gdc basic_hello.d wc -c ./a.out  28608 ./a.out

Jadi kami mendapatkan beberapa perbaikan. Masalah terbesarnya adalah bahwa ini secara dinamis menautkan sejumlah kode yang tidak kami perlukan, termasuk sekumpulan mesin untuk pengecualian yang tidak akan kami lempar, dan kode untuk menangani kasus yang tidak kami perlukan. Sungguh kita hanya perlu menempatkan string ke standar, yang persis seperti apa fungsi C menempatkan.

 import core.stdc.stdio;  int main () {     put ("Halo");     kembali 0; } 

Mengompilasi ini dengan gdc memberi kita sedikit penurunan ukuran.

  gdc basic_hello.d wc -c ./a.out  21296 ./a.out

Dengan menjalankan ldd di atasnya, kita dapat melihat bahwa kita masih menarik runtime D meskipun kita tidak membutuhkannya.

> ldd ./a.out
linux-vdso.so.1 (0x00007ffd02db8000)
libgphobos.so.1=> /lib64/libgphobos.so.1 (0x00007f4b1caa8000)
libgcc_s.so .1=> /lib64/libgcc_s.so.1 (0x00007f4b1ca8d000)
libm.so.6=> /lib64/libm.so.6 (0x00007f4b1c947000)
libpthread.so.0=> /lib64/libpthread.so.0 (0x00007f4b1c925000)
libdl. so.2=> /lib64/libdl.so.2 (0x00007f4b1c91e000)
libc.so.6=> /lib64/libc.so.6 (0x00007f4b1c753000)
/ lib64 / ld-linux-x86-64.so.2 (0x00007f4b1d09f000)

Inilah gunanya mode BetterC! Ayo coba. Kita hanya perlu memodifikasi fungsi utama kita menjadi ‘extern (C)’.

 import core.stdc.stdio; eksternal (C): int main () {     put ("Halo");     kembali 0; } 

Dengan gdc, mode betterC disebut dengan bendera ‘no-druntime’.

  gdc -fno-druntime basic_hello. d wc -c ./a.out  20600 ./a.out

Jadi itu cukup bagus dan kami turun menjadi hanya 20k. Dan kami hanya menautkan ke pustaka C standar.

> ldd ./a.out
linux-vdso.so.1 (0x00007ffd119c7000)
libgcc_s.so.1=> /lib64/libgcc_s.so.1 (0x00007f447f734000)
libc.so .6=> /lib64/libc.so.6 (0x00007f447f569000)
/ lib64 / ld-linux-x86-64.so.2 (0x00007f447f783000)

Kita dapat menjalankan strip di atasnya untuk menghemat ruang:

  strip -s ./a. di luar wc -c ./a.out  15016 ./a.out

Sekarang kita hampir mencapai batasan tentang apa yang dapat kita lakukan dengan menautkan ke pustaka standar C tanpa beralih ke musl. Dan bahkan di sana kita menjauh dari pemrograman dalam D dan malah hanya mengotak-atik mengoptimalkan kode C, yang bukan inti dari artikel ini.

Jadi bagaimana perpustakaan standar C mencetak string ke keluaran standar? Pada akhirnya ia melakukan panggilan sistem. Di Windows modern, Microsoft membuatnya sangat sulit untuk melakukan ini dengan mengubah nomor syscall pada setiap pembaruan. Ini mungkin menghentikan beberapa virus yang memanggil sistem hardcode, tetapi membuatnya tidak mungkin untuk melakukan pemrograman Windows apa pun tanpa menggunakan pustaka standar C / Win32 API. Jadi dalam artikel ini kita akan fokus pada Linux khususnya karena antarmuka panggilan sistem secara eksplisit diberkati oleh pengembang kernel. FreeBSD dan Unix lainnya cenderung lebih memilih penggunaan API pustaka standar C. Di sini kita juga hanya akan mengkhawatirkan arsitektur x86_64 (karena tahun 2020, satu-satunya arsitektur lain yang banyak digunakan yang didukung secara luas oleh kompiler D adalah AARCH64).

Antarmuka panggilan sistem untuk Linux ditentukan dalam dokumen ABI Sistem V sini. Singkatnya, Anda cukup meneruskan nomor syscall di register RAX, dan argumen lainnya di rdi, rsi, rdx, dll.

3 kompiler bahasa D utama, DMD, GDC, dan LDC, semuanya memiliki cara yang sedikit berbeda untuk memasukkan kode assembly sebaris. Hanya untuk bersenang-senang saya menulis versi untuk masing-masing menggunakan fitur mereka. Satu hal yang saya perhatikan adalah ketika menggunakan versi LDC perakitan inline itu menyebutkan perlu daftar yang dipisahkan koma untuk pembatas. Saya menyertakan beberapa spasi dalam daftar dan menerima kesalahan kendala asm tidak valid yang sangat sulit untuk dilacak.

Satu hal lain yang perlu diperhatikan adalah bahwa fungsi utama yang kami gunakan sejauh ini didasarkan pada fakta bahwa pada akhirnya kami menautkan ke pustaka C standar yang juga memiliki fungsi utama yang dipanggil dari titik masuk lain, dengan tidak menggunakan pustaka C standar dan file start darinya (yang pada dasarnya membantu runtime bahasa C menyiapkan beberapa perancah yang dibutuhkan untuk ‘atexit’ dan semacamnya (catatan samping, Bahasa C memang memiliki runtime, Anda biasanya tidak menyadarinya)) kami tidak lagi memiliki titik masuk. Kebanyakan hal menganggap titik masuk disebut ‘_start’, tetapi ini dapat diganti di linker.

Mari kita tulis rutinitas syscall kita dalam sebuah file bernama syscall.d:

 modul syscall; versi (LDC) {     impor ldc.llvmasm; }  versi (Posix) { eksternal (C): pragma (inline, true):     syscall1 panjang (bilangan panjang, a1 panjang) @system @nogc nothrow     {          versi (DigitalMars)         {             ret panjang;             asm @system @nogc nothrow             {                 mov RAX, nomor;                 mov RDI, a1 [RBP];                 syscall;                 mov ret, RAX;             }             ret kembali;         }         versi (GNU)         {             ret panjang;             asm @system @nogc nothrow             {                 "syscall": "=a" (ret): "a" (angka), "D" (a1): "rcx", "r11";             }             ret kembali;         }         versi (LDC)         {             pragma (LDC_allow_inline);             return __asm! long ("syscall", "={rax}, {rax}, {rdi}         ~ {rcx}, ~ {r11} ", nomor, a1);         }     } eksternal (C): pragma (inline, true):     syscall3 panjang (nomor panjang, a1 panjang, a2 panjang, a3 panjang) @system @nogc nothrow     {          versi (DigitalMars)         {             ret panjang;             asm @system @nogc nothrow             {                 mov RAX, nomor;                 mov RDI, a1 [RBP];                 mov RSI, a2 [RBP];                 mov RDX, a3 [RBP];                 syscall;                 mov ret, RAX;             }             ret kembali;         }         versi (GNU)         {             ret panjang;             asm @system @nogc nothrow             {                 "syscall": "=a" (ret): "a" (angka), "D" (a1), "S" (a2), "d" (a3): "rcx", "r11";             }             ret kembali;         }         versi (LDC)         {             pragma (LDC_allow_inline);             return __asm! long ("syscall",                     "={rax}, {rax}, {rdi}, {rsi}, {rdx}, ~ {rcx}, ~ {r11}", number, a1, a2, a3);         }     }    }  
  Dan titik masuk _start kami dan beberapa fungsi pembantu di hello.d:  
 impor syscall;  panjang SYS_WRITE=1 yang tidak dapat diubah; panjang tetap SYS_EXIT=60;  eksternal (C): @nogel nothrow: pragma (inline, true): size_t write (int fd, dalam char buf, size_t count) {     return syscall3 (SYS_WRITE, fd, cast (long) buf, count); }  eksternal (C): @nogel nothrow: pragma (inline, true): batal keluar (exit_code panjang) {      syscall1 (SYS_EXIT, exit_code); }  eksternal (C): @nogel nothrow: void _start () {     tulis (1, "Halo  n", 6);     keluar (0); } 

Kami juga perlu menautkan file ini sendiri sehingga kami tidak menarik dependensi libc apa pun, kami melakukan ini dengan menggunakan perintah ‘ld’ dan hanya dengan mengompilasi dan tidak menautkan (tanda ‘-c’).

  gdc -fno-druntime -nodefaultlibs hello.d syscall.d -c ld ./hello.o ldd ./a.out          bukan eksekusi dinamis  

Keren jadi kami sekarang memiliki executable yang terhubung secara statis yang tidak memiliki ketergantungan pada pustaka C standar! Berapa banyak yang kami hemat?

  wc -c ./a. di luar  13672 ./a.out

Ugh semua itu berfungsi dan hanya penghematan ekstra 2k! Mari kita lihat apa yang bisa dieksekusi. Kita bisa mendapatkan ini dengan menjalankan ‘readelf -a’ pada executable. Eksekusi kami memiliki 9 bagian.

  [0] [1] .teks [2] .rodata [3] .eh_frame [4] .data [5] .comment [6] .symtab [7] .strtab [8] .shstrtab  

Yang pertama menonjol adalah bagian ‘.eh_frame’. Itu digunakan untuk pengecualian pembongkaran, pembersihan tumpukan, dan berbagai macam hal lain yang tidak kita perlukan. Kita dapat memberi tahu gdc bahwa kita tidak menginginkannya dengan menambahkan dua tanda:

 > gdc -fno-druntime - nodefaultlibs -fno-exceptionions -fno-asynchronous-unwind-tables hello.d syscall.d -c> ld ./hello.o> wc -c ./a.out  9488 ./a.out

Luar biasa kami baru saja menghemat 4k, tetapi masih ada lemak untuk dipangkas. Satu hal yang dapat kita pelajari dari membaca halaman info untuk ‘ld’ adalah bahwa terdapat tanda untuk membantu mengemas file yang dapat dieksekusi jika Anda tidak membutuhkan pustaka bersama, ‘-n’. Menggunakan ini menghemat sekitar 3k:

 > gdc -fno-druntime - nodefaultlibs -fno-exceptionions -fno-asynchronous-unwind-tables hello.d syscall.d -c> ld -n ./hello.o> wc -c ./a.out  6112 ./a.out

Jika kita melihat executable dengan readelf lagi, kita dapat melihat bahwa compiler mengeluarkan simbol untuk banyak hal yang tidak kita butuhkan:

Tabel simbol ‘.symtab’ berisi 21 entri:
Jumlah: Nilai Ukuran Jenis Bind Vis Ndx Nama
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004000b0 0 BAGIAN DEFAULT LOKAL 1
2: 00000000004002cf 0 BAGIAN LOKAL DEFAULT 2
3: 00000000004012d8 0 BAGIAN LOKAL DEFAULT 3
4: 0000000000000000 0 BAGIAN LOKAL DEFAULT 4
5: 0000000000000000 0 FILE LOCAL DEFAULT tes ABS. D
6: 0000000000400187 32 FUNC GLOBAL DEFAULT 1 syscall0
7: 00000000004012e0 8 OBYEK GLOBAL DEFAULT 3 _D4test8SYS_EXITyl
8: 000000000040021d 83 FUNC GLOBAL DEFAULT 1 syscall5
9: 00000000004012d8 8 OBYEK GLOBAL DEFAULT 3 _D4test9SYS_WRITEyl
10: 00000000004001a7 51 FUNC GLOBAL DEFAULT 1 syscall2
11: 00000000004000b0 47 FUNC GLOBAL DEFAULT 1 tulis
12: 0000000000400162 37 FUNC GLOBAL DEFAULT 1 _mulai
13: 00000000004012e8 0 NOTYPE GLOBAL DEFAULT 3 __bss_start
14: 00000000004001da 67 FUNC DEFAULT GLOBAL 1 syscall4
15: 0000000000400137 43 FUNC GLOBAL DEFAULT 1 syscall1
16: 00000000004012e8 0 NOTYPE GLOBAL DEFAULT 3 _edata
17: 00000000004012e8 0 NOTYPE GLOBAL DEFAULT 3 _selesai
18: 0000000000400117 32 FUNC GLOBAL DEFAULT 1 keluar
19: 0000000000400270 95 FUNC GLOBAL DEFAULT 1 syscall6
20: 00000000004000df 56 FUNC GLOBAL DEFAULT 1 syscall3

Kita dapat menempatkan setiap fungsi ke dalam bagian terpisah dan kemudian memiliki linker “sampah mengumpulkan” bagian yang tidak digunakan. Ini membutuhkan dua bagian, pertama kita perlu membuat gdc meletakkan fungsi dan data ke dalam beberapa bagian terpisah, ‘-fdata-section’ dan ‘-ffunction-section’. Selanjutnya kita perlu untuk meneruskan linker ‘–gc-section’. Kita dapat menambahkan perintah untuk linker untuk menghapus semua yang tidak kita perlukan ‘–strip-all’ pada saat yang sama, dan menyetel id build ke none. Kami mungkin juga meneruskan ‘-Os’ ke gdc juga untuk melihat apakah itu dapat mengoptimalkannya untuk ukuran sama sekali (Tanpa ini, 800 byte, bukan 616).

 > gdc -fno-druntime - fno-asynchronous-unwind-tables -nodefaultlibs -ffunction-section -fdata-section -fno-exceptionions -Os hello.d syscall.d -c> ld -s --gc-section --strip-all --build-id=none -n ​​./hello.o> wc -c ./a.out  616 ./a.out

Wow! Sekarang kita bicara! sebuah eksekusi 616 byte. Tapi kita masih bisa lebih baik. Jika kita melihat dengan ‘objdump’ kita dapat melihat masih ada bagian ‘.comment’. Kami dapat menghapus ini secara langsung:

 > strip -R .comment. /a.out> wc -c ./a.out  496 ./a.out

Yang menurunkan kami hingga 496 byte. Dan itu masih berfungsi!

 > ./ a.out  Halo

Sebagai perbandingan, lihat Drew DeVault’s Hello World entri blog untuk melihat seberapa kecil bahasa lain bisa didapat.

Read More

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments