6 poin oleh GN⁺ 2024-02-03 | 1 komentar | Bagikan ke WhatsApp
  • 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

1 komentar

 
GN⁺ 2024-02-03
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

    • Baru-baru ini saya menulis file ELF sendiri: https://github.com/avik-das/garlic/blob/master/recursive/elf...
      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...
    • Dengan cara serupa, saya juga menyarankan untuk mencoba menulis loader ELF sederhana
      Dynamic linking memang punya kompleksitas implementasi yang cukup besar, tetapi jika hanya mendukung ELF statis, itu cukup mudah dipahami
    • Memodifikasi ELF yang sudah ada juga sangat edukatif dan menyenangkan
      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
    • Kalau dicoba sendiri, pada dasarnya ini mirip assembly manual
      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
    • A Magnetized Needle and a Steady Hand karya Chris Wellons adalah artikel tentang membuat executable ELF dari nol: https://nullprogram.com/blog/2016/11/17/
  • Sejauh pemahaman saya, simbol main itu spesifik untuk C
    Simbol _start adalah entry point binary yang independen dari bahasa, dan dalam kasus ini memanggil main
    Jika ada konvensi yang menyebut entry point sebagai _start lalu meneruskan argc/argv milik main ke sana, formatnya akan jauh kurang fleksibel

    • Secara ketat, nama _start juga tidak istimewa
      Binary mencatat alamat entry point di header, dan sistem operasi mulai menjalankan dari alamat itu
      Menamai simbol itu _start hanyalah konvensi di C dan bahasa lain, dan digunakan linker untuk menetapkan entry point saat menulis header ELF
      Jika menulis linker script sendiri, nama entry point bisa diberi apa pun yang diinginkan
    • Simbol main memang hanya disediakan pada hosted C
      Dalam freestanding C, Anda bisa punya entry point yang diinginkan
      _start juga sekadar default linker, dan Anda bisa menentukan simbol yang lebih baik dengan -Wl,--entry="${symbol}"; GCC juga mendukung pengaturan langsung tanpa -Wl yang tidak sedap dilihat itu
      Selain 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 exit agar proses berakhir dengan rapi ketika main mengembalikan kode status
      Setidaknya di Linux, beginilah cara kerjanya
    • Tergantung runtime bahasa, salah satu pekerjaan umum adalah menginisialisasi nilai statis global yang bukan nol
      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 _start
      Pada 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 strings selalu efektif

    • Itu juga harus dijelaskan kepada para hakim Jerman
      Ada seseorang yang malang didenda karena menemukan kata sandi dengan cara yang sama seperti menjalankan strings pada sebuah binary
      Para 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

    • Secara pribadi, saya tidak yakin apakah binary portabel secara netto merupakan hal yang baik
      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
    • Mungkin saya salah paham, tetapi APE sepertinya bukan format binary itu sendiri
      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 cat pada binary di terminal adalah jalan pintas menuju kesedihan
    Saya lebih suka | hd, yang pada dasarnya adalah hexdump -C, meski sama-sama sulit dipahami jika dilihat dengan mata telanjang