- Program C
hello yang kecil sekalipun di Linux menjadi berkas eksekusi ELF, dan struktur internalnya bisa diperiksa langsung dengan readelf, nm, dan objdump
- Poros utama untuk memahami berkas eksekusi adalah simbol, section, dan segment; masing-masing bertanggung jawab atas pengaitan fungsi, pemisahan kode/data, dan penempatan memori saat eksekusi
- Dengan
objdump dan readelf, Anda bisa memeriksa byte dan atribut section seperti .text, .rodata, .data, .bss, dan .interp
- Program tidak langsung dimulai dari
main, melainkan masuk ke _start terlebih dahulu, lalu memanggil main setelah melewati berbagai pekerjaan inisialisasi
- Berkas eksekusi bukan “gumpalan yang tak terbaca”, melainkan berkas dengan format yang sudah ditentukan; dengan alat yang tepat, kode, string, dan informasi linking dapat dilacak secara bertahap
Berkas eksekusi adalah format berkas yang bisa dibaca
- Berkas eksekusi hasil kompilasi pada awalnya tampak seperti “biner ajaib” yang tidak bisa dibaca, tetapi sebenarnya merupakan format berkas yang dapat dipahami
- Contoh ini berfokus pada biner ELF di Linux, dan karena biner bergantung pada platform, penjelasannya juga terikat pada platform tersebut
- Contoh yang digunakan adalah program C berikut
#include <stdio.h>
int main() {
printf("Penguin!\n");
}
- Setelah mengompilasinya dengan
gcc -o hello hello.c untuk membuat berkas eksekusi hello, kita melihat bagian dalamnya
- Alur keseluruhannya berpusat pada tiga konsep
- Simbol (symbols): digunakan untuk menemukan lokasi saat memanggil fungsi yang didefinisikan di tempat lain, seperti
printf
- Section (sections): unit yang memisahkan kode dan data, seperti
.text, .data, dan .rodata
- Segment (segments): mengelompokkan section sebagai unit penempatan memori pada saat eksekusi
Petunjuk tetap terlihat meski dibuka sebagai teks
- Jika berkas eksekusi dibuka apa adanya, misalnya dengan
cat hello, sebagian besar keluarannya tampak seperti karakter rusak
- Namun, di dalam keluaran itu masih bisa ditemukan string seperti
Penguin! dan ELF
- ELF adalah nama format berkas biner ini
- Sebagian besar keluaran sulit dibaca manusia karena berkas eksekusi berisi data biner
Memeriksa nama fungsi dan koneksi melalui tabel simbol
readelf --symbols hello mencetak tabel simbol dari berkas eksekusi
- Dalam contoh keluaran, muncul beberapa simbol utama
main: alamat fungsi main() yang kita tulis
puts@@GLIBC_2.2.5: tampaknya merupakan referensi terkait printf yang dipanggil di kode, kemungkinan karena compiler mengoptimalkannya menjadi puts
_start: simbol penting yang terkait dengan awal program
- Program tidak langsung dimulai dari
main; sebenarnya program masuk ke _start terlebih dahulu
_start menjalankan sejumlah pekerjaan penting, termasuk memanggil main
Simbol memungkinkan pengaitan
- Jika Anda menulis fungsi bernama
hello di dalam program, kode fungsi tersebut di biner hasil kompilasi akan diberi simbol bernama hello
- Untuk memanggil fungsi library seperti
printf, diperlukan cara untuk menemukan lokasi kode fungsi tersebut
- Proses menemukan lokasi fungsi disebut linking
- Jika terjadi segera setelah kompilasi, disebut static linking
- Jika terjadi saat program dijalankan, disebut dynamic linking
libc berisi fungsi-fungsi library standar C
- Meskipun
nm mencetak “no symbols” untuk libc, simbol masih bisa dilihat dengan objdump -tT /lib/x86_64-linux-gnu/libc-2.15.so
- Di tabel simbol libc, fungsi seperti
sprintf, strlen, fork, dan exec dapat ditemukan
- Dengan membayangkan alur ketika
hello memanggil puts lalu menemukan lokasi puts di tabel simbol libc, kita dapat memahami cara kerja dynamic linking
Section memisahkan kode dan data
objdump -s hello mencetak byte yang ada di setiap section berkas eksekusi dalam format heksadesimal dan ASCII
- Section utama adalah sebagai berikut
.text: kode aktual program, yaitu assembly, yang berisi _start dan main
.rodata: berisi data hanya-baca; dalam contoh ini ada string "Penguin!"
.interp: berisi nama berkas dynamic linker
- Section dan segment digunakan pada waktu yang berbeda
- Section digunakan oleh
ld pada saat linking
- Segment digunakan pada saat eksekusi
- Dengan
readelf --sections hello, metadata section dapat dilihat lebih terperinci
- Dari flag pada contoh, terlihat sifat masing-masing section
.text: dapat dieksekusi dan hanya-baca
.rodata: hanya-baca
.data: dapat dibaca/ditulis
.bss: area data yang dapat ditulis
Melihat bahasa mesin sebagai assembly melalui disassembly
- Section
.text berisi byte yang diinterpretasikan dan dijalankan CPU sebagai kode
- Byte awal
.text pada contoh, 31 ed, sulit dipahami langsung oleh manusia, sehingga dibutuhkan disassembler
objdump -d ./hello melakukan disassembly pada section .text dan menampilkannya sebagai instruksi assembly
- Dalam keluaran contoh,
31 ed ditampilkan sebagai xor %ebp,%ebp
- Dengan cara ini, kita bisa memeriksa byte kode di dalam biner dan melihat instruksi assembly apa yang diwakilinya
Segment menentukan penempatan memori saat eksekusi
- Berkas eksekusi juga tersusun dari segment atau program headers
readelf --segments hello menampilkan segment program dan pemetaan section-ke-segment
- Segment digunakan untuk menentukan bagaimana tiap bagian program dibagi dan ditempatkan di memori
- Dalam contoh, ada dua segment
LOAD utama
LOAD pertama: ditandai R E, berarti dapat dibaca/dieksekusi
LOAD kedua: ditandai RW, berarti dapat dibaca/ditulis
.text harus bisa dibaca dan dieksekusi, tetapi tidak boleh ditulis, sehingga masuk ke segment pertama
.data dan .bss harus bisa ditulis, tetapi tidak perlu dieksekusi, sehingga masuk ke segment kedua
Alat dan materi untuk ditelusuri lebih lanjut
- Berkas eksekusi ELF bukan sihir khusus, melainkan format berkas biasa; biner Linux dapat diselidiki dengan
readelf, nm, dan objdump
- Materi terkait
1 komentar
Pendapat Hacker News
Seperti yang juga sudah disebutkan di thread lain https://news.ycombinator.com/item?id=38847750#38862450, saya sangat menyarankan untuk menulis ELF sekali dengan tangan
Ini latihan yang bagus untuk memahami komponen dasar sebuah executable, dan juga membantu kalau, berbeda dari artikel ini, Anda ingin melakukan pendekatan dari bawah ke atas, bukan dari atas ke bawah
Di beberapa thread pada posting HN lain itu juga ada banyak diskusi yang bagus
Saya juga membuat visualisasi interaktif yang menampilkan byte-byte file untuk menjelaskan formatnya kepada diri sendiri dan orang lain
Jika byte diklik, penjelasan akan muncul, dan byte terkait di dalam file akan disorot sehingga membantu pemahaman: https://scratchpad.avikdas.com/elf-explanation/elf-explanati...
Dynamic linking memang punya kompleksitas implementasi yang cukup besar, tetapi jika hanya mendukung ELF statis, itu cukup mudah dipahami
Awalnya menjengkelkan karena saat tidak berfungsi, debugging-nya praktis mustahil, tetapi begitu akhirnya mulai berjalan, rasanya benar-benar keren
ELF bisa di-patch dengan cara-cara menarik, dan dengan auxiliary vector kita juga bisa melakukan observasi diri saat runtime
Karena Linux memberi alamat program header table, dari sana kita bisa menjangkau ke mana saja, dan cukup memperluas segmen LOAD agar mencakup seluruh binary
Misalnya, saya membuat tool yang menyisipkan modul dan kode Lisp langsung ke dalam file executable interpreter Lisp saya
Segmen yang disisipkan otomatis dimuat oleh ELF, lalu interpreter menemukannya dan menjalankannya
Saya sangat menyukai fitur kecil ini sampai menulis artikel tentangnya: https://www.matheusmoreira.com/articles/self-contained-lone-...
Saya berharap bahasa-bahasa arus utama juga mengadopsi pendekatan seperti ini
Membaca dokumentasi, memilih byte yang diperlukan dari datasheet prosesor, lalu menempatkannya berurutan di berbagai section, mengisi field ELF, dan pada akhirnya semuanya bermuara pada mengetikkan semuanya
Di lingkungan era sebelum ELF seperti Apple II 8-bit, monitor machine code memungkinkan kita memasukkan byte program secara langsung, dan byte itu dieksekusi
Menyimpannya ke disk hanya sedikit lebih rumit, dan di sini juga ada peluang lain
Kita bisa membuat file dengan editor sektor disk, dan seterusnya begitu
Sejauh pemahaman saya, simbol
mainitu spesifik untuk CSimbol
_startadalah entry point binary yang independen dari bahasa, dan dalam kasus ini memanggilmainJika ada konvensi yang menyebut entry point sebagai
_startlalu meneruskanargc/argvmilikmainke sana, formatnya akan jauh kurang fleksibel_startjuga tidak istimewaBinary mencatat alamat entry point di header, dan sistem operasi mulai menjalankan dari alamat itu
Menamai simbol itu
_starthanyalah konvensi di C dan bahasa lain, dan digunakan linker untuk menetapkan entry point saat menulis header ELFJika menulis linker script sendiri, nama entry point bisa diberi apa pun yang diinginkan
mainmemang hanya disediakan pada hosted CDalam freestanding C, Anda bisa punya entry point yang diinginkan
_startjuga sekadar default linker, dan Anda bisa menentukan simbol yang lebih baik dengan-Wl,--entry="${symbol}"; GCC juga mendukung pengaturan langsung tanpa-Wlyang tidak sedap dilihat ituSelain itu, entry point sebenarnya bukan simbol, melainkan pointer
Linker hanya mengambil alamat dari simbol yang ditentukan lalu menetapkannya sebagai entry point ELF
Selain jumlah argumen dan vektor argumen, stack juga berisi environment vector dan auxiliary vector
Kode awal proses bisa sesederhana mengambil nilai-nilai ini dari stack, memasukkannya ke register yang sesuai, lalu memanggil fungsi C yang diinginkan
Entry point itu sendiri bukan fungsi, jadi tidak ada tempat untuk kembali
Kode entry point harus diakhiri dengan system call
exitagar proses berakhir dengan rapi ketikamainmengembalikan kode statusSetidaknya di Linux, beginilah cara kerjanya
Dalam bahasa seperti Rust/C/C++, variabel yang akan diinisialisasi juga bisa disuntikkan melalui flag linker
Jika program di-link secara dinamis, setahu saya runtime linker berjalan sebelum
_start, menyelesaikan linking, lalu menyerahkan kontrol ke_startPada akhirnya ini adalah hack di atas hack yang bertambah secara organik untuk menyediakan ekstensibilitas, dan karena sudah cukup diterima secara sosial serta bekerja cukup baik, kita terus menggunakannya
Pada 2012, ketika saya mengalihkan jalur akademis dari matematika ke ilmu komputer, saya mulai menulis blog, dan topik ini secara harfiah adalah hal pertama yang saya pelajari: https://heinrichhartmann.com/archive/Dissecting-Hello-World....
Saya tidak pernah menyesal masuk ke lubang kelinci yang dalam ini
Kalau ingatan saya benar, Julia juga punya latar belakang matematika
Mungkin alasan orang-orang dari matematika tertarik pada eksperimen seperti ini adalah dorongan untuk menalar dari dasar
Senang melihat dia membuat materi ini mudah diakses oleh banyak orang
Tulisan Julia selalu luar biasa
Saat mengajarkan bahwa kode yang sudah dikompilasi tidak bisa menyembunyikan rahasia, demonstrasi
stringsselalu efektifAda seseorang yang malang didenda karena menemukan kata sandi dengan cara yang sama seperti menjalankan
stringspada sebuah binaryPara hakim menganggap dia telah “melewati” langkah keamanan perangkat lunak tersebut: https://www.theregister.com/2024/01/19/germany_fine_security...
Ini bukan kritik atau mencari-cari kesalahan, hanya pikiran yang muncul
Saat membaca kalimat “karena binary kurang lebih adalah definisi dari sesuatu yang spesifik platform, semua ini juga spesifik platform”, saya teringat ketika Actually Portable Executable menunjukkan bahwa binary yang sama bisa dijalankan di beberapa platform
Secara mental saya masih belum sepenuhnya pulih dari momen surealis itu
Selama puluhan tahun kita mencoba menyelesaikan masalah lintas platform dengan segala macam cara yang fraktal, seperti Java, library lintas platform, dan sebagainya, padahal solusinya selama ini ada tepat di depan mata
Di era komputer cepat, saya rasa distribusi source dan kompilasi lokal lebih baik daripada distribusi binary
Sayangnya, banyak perangkat lunak yang kita andalkan terlalu besar, dan compiler relatif lambat, sehingga distribusi binary menjadi semacam kejahatan yang diperlukan
Saya berharap lebih banyak upaya dicurahkan pada komponen perangkat lunak yang lebih sederhana dan secara alami cepat dikompilasi, serta compiler yang lebih cepat, daripada pada binary portabel
Ia adalah skrip yang bisa dijalankan di sistem mana pun, dan skrip itu bisa memuat binary
Kalau ingatan saya benar, versi aslinya harus didekode dari base64 sebelum dimuat
Jadi lebih dekat ke binary loader yang dapat dieksekusi
Pada awal 1990-an saya terpesona oleh format executable, lalu selama beberapa minggu membuat viewer executable DOS dan Windows dengan Modula 2, menamainya VEXE, dan merilisnya sebagai shareware pada 1991
Alat ini mendapat popularitas ceruk di kalangan cracker, bahkan sampai disebut dalam tutorial +ORC: https://gist.github.com/callowaysutton/48bdf0245e17e72d41a15...
Mungkin karena bisa mendeteksi berbagai skema enkripsi dan kompresi yang digunakan untuk mencegah reverse engineering program
Jika penasaran seberapa kecil file binary ELF bisa dibuat, Anda mungkin menyukai tulisan menarik ini: https://www.muppetlabs.com/~breadbox/software/tiny/teensy.ht...
Coba juga lihat alat saya yang memungkinkan menjelajahi ELF dengan SQL: https://github.com/fzakaria/sqlelf
Adakah yang bisa merekomendasikan materi pengantar atau buku pemrograman low-level yang praktis untuk orang dengan latar belakang Python yang kuat
Belakangan ini saya mulai belajar Rust, dan menyadari ada banyak hal yang harus saya kejar
Saya belum pernah mengambil mata kuliah compiler, jadi mungkin saya melewatkan banyak informasi
Misalnya, saya bahkan tidak tahu bahwa ada yang namanya simbol dalam binary, dan tidak tahu perbedaan antara ELF dan Mach-O
Melakukan
catpada binary di terminal adalah jalan pintas menuju kesedihanSaya lebih suka
| hd, yang pada dasarnya adalahhexdump -C, meski sama-sama sulit dipahami jika dilihat dengan mata telanjang