- Sebelum program dijalankan, analisis teknis ini menelusuri proses kernel membuat dan menginisialisasi proses melalui system call
execve
- Panggilan ini meneruskan path berkas eksekusi, argumen, dan variabel lingkungan, lalu kernel berdasarkan itu memuat berkas eksekusi berformat ELF
- Berkas ELF mencakup kode, data, simbol, informasi dynamic linking, dan lainnya, lalu kernel menafsirkannya untuk melakukan memory mapping dan inisialisasi stack
- Setelah itu kernel menyerahkan kontrol ke entry point
_start, dan fungsi main yang didefinisikan pengguna baru dipanggil setelah runtime tiap bahasa selesai diinisialisasi
- Proses ini menunjukkan struktur kolaborasi antara sistem operasi, compiler, dan runtime, serta penting untuk memahami bagaimana eksekusi program berlangsung di level sistem
Titik awal eksekusi program: pemanggilan execve
- Di Linux, eksekusi program dimulai melalui system call
execve
- Dalam bentuk
execve(const char *filename, char *const argv[], char *const envp[]), ia meneruskan nama berkas eksekusi, daftar argumen, dan daftar variabel lingkungan
- Melalui ini, kernel menentukan program apa yang akan dijalankan dan dalam lingkungan seperti apa
- Dalam bahasa tingkat tinggi, panggilan ini dibungkus oleh API eksekusi proses milik standard library
- Contoh:
std::process::Command di Rust secara internal memanggil execve
- Ia menjalankan proses yang mirip dengan pencarian PATH di shell, mengubah nama perintah menjadi path lengkap
- Untuk skrip yang memiliki shebang (
#!), kernel menjalankan program menggunakan interpreter yang ditentukan
- Contoh:
#!/usr/bin/python3 → dijalankan oleh interpreter Python
ELF: struktur berkas eksekusi
- Berkas eksekusi di Linux mengikuti format ELF (Executable and Linkable Format)
- ELF adalah format berkas eksekusi standar yang memuat kode, data, simbol, informasi relokasi, dan sebagainya
- OS lain memakai format terpisah seperti Mach-O (macOS) dan PE (Windows)
- Header ELF memuat informasi tentang struktur berkas dan tata letak memori
- Contoh item:
ELF Magic, Class, Entry point address, Program headers, Section headers
Entry point address adalah alamat instruksi yang pertama kali akan dijalankan program
- Dalam contoh header ELF, berkas tersebut adalah berkas eksekusi ELF32 untuk arsitektur RISC-V, dengan alamat
0x10358 ditetapkan sebagai entry point
Komponen internal ELF
- Berkas ELF tersusun dari beberapa section
.text: kode yang dieksekusi
.data: variabel global yang sudah diinisialisasi
.bss: variabel global yang belum diinisialisasi
.plt: tabel untuk pemanggilan shared library
.symtab, .strtab: tabel simbol dan string
- PLT (Procedure Linkage Table) mendukung pemanggilan fungsi dari shared library
- Contoh:
printf, malloc dari libc
- Section
PT_INTERP pada ELF menentukan dynamic linker (interpreter)
- Kernel membaca ELF untuk menempatkan section yang dapat dimuat ke memori, dan bila perlu menerapkan fitur keamanan seperti ASLR dan NX bit
Tabel simbol dan runtime link
- Symbol table (
symtab) pada ELF memuat informasi alamat fungsi dan variabel
- Contoh: ada entri seperti
_start, main, __libc_start_main
- Bahkan program “Hello, World!” sederhana pun bisa memuat lebih dari 2300 simbol
- Sebagian besar berasal dari standard library dan kode inisialisasi runtime
- Karena implementasi
libc seperti musl atau glibc ikut terhubung
- Setelah memuat setiap section ELF, kernel menyerahkan kontrol ke interpreter (dynamic linker)
- Interpreter menangani relocation, address randomization (ASLR), pengaturan hak eksekusi (NX bit), dan sebagainya
Proses inisialisasi stack
- Sebelum program dijalankan, kernel harus menyusun stack secara langsung
- Stack digunakan untuk variabel lokal, frame pemanggilan fungsi, penerusan argumen, dan lain-lain
argv, envp yang diteruskan saat pemanggilan execve disimpan di stack
- Program mengakses argumen command line dan variabel lingkungan melalui ini
- Kernel juga menyertakan auxiliary vector ELF (
auxv) di stack
- Berisi sekitar 30 item seperti ukuran halaman, metadata ELF, dan informasi sistem
- Contoh:
AT_PAGESZ menentukan ukuran halaman memori, misalnya 4KiB
- Dalam contoh emulator RISC-V, stack pointer (
sp) dimulai dari alamat tinggi lalu argumen, variabel lingkungan, dan auxiliary vector ditumpuk dalam urutan terbalik
Entry point dan fungsi _start
- Entry point ELF ditetapkan ke alamat fungsi
_start
_start adalah kode ruang pengguna pertama yang menerima kontrol dari kernel
- Sebagian besar bahasa melakukan inisialisasi runtime di
_start lalu memanggil main
- Contoh:
std::rt::lang_start di Rust, __libc_start_main di C
- Dalam contoh Rust, atribut
#![no_std], #![no_main] memungkinkan pendefinisian _start secara langsung tanpa runtime
- Di dalam
_start, argc, argv, envp dibaca dari stack lalu pointer main dipanggil
- Runtime tiap bahasa menjalankan pekerjaan inisialisasi khusus bahasa seperti konstruktor global, thread-local storage, dan exception handling
Alur lengkap hingga pemanggilan main()
- Seluruh proses dapat diringkas sebagai berikut
- Pemanggilan
execve → kernel memuat berkas ELF
- Interpretasi ELF → pemetaan section kode/data, penentuan interpreter
- Penyusunan stack → penyimpanan argumen, variabel lingkungan, auxiliary vector
- Eksekusi entry point
_start
- Setelah inisialisasi runtime,
main() dipanggil
- Rangkaian proses ini menunjukkan struktur kolaborasi antara kernel sistem operasi, format ELF, dan runtime bahasa
- Kernel Linux yang sesungguhnya juga mencakup logika internal tambahan seperti address space, process table, dan manajemen grup, tetapi tulisan ini menjelaskan alur inti pada tahap sebelumnya
Kesimpulan dan koreksi
- Proses eksekusi sebelum
main() adalah gabungan antara inisialisasi level kernel dan penyiapan runtime
- Bahkan program “Hello, World!” yang sederhana pun dijalankan setelah melewati struktur ELF yang kompleks dan inisialisasi runtime
- Pada versi awal tulisan, sebagian logika pemuatan section dikaitkan ke kernel, tetapi kemudian dikoreksi bahwa itu sebenarnya peran interpreter ELF
- Analisis ini berguna sebagai bahan dasar untuk memahami system programming, compiler, dan arsitektur OS
1 komentar
Komentar Hacker News
Menjelaskan proses dynamic linking pada berkas ELF
Kernel memetakan segmen PT_LOAD dari ELF, memuat dynamic linker (ld.so) yang ditentukan oleh PT_INTERP, lalu menyerahkan kendali kepadanya
Setelah itu dynamic linker melakukan relocation pada dirinya sendiri dan memuat shared object yang diperlukan dengan mmap/mprotect
Struktur ini dianalogikan mirip dengan mekanisme shebang(#!) pada skrip
Ia membagikan pengalaman pernah bingung saat mencoba menyisipkan berkas arbitrer ke ELF dengan objcopy tetapi kernel tidak memuatnya
Pada akhirnya ia membuat sendiri alat patch tabel program header, dan katanya fitur ini juga ditambahkan ke linker mold
Tulisan terkait: Self-contained Lone Lisp Applications
Mengatakan pernah bereksperimen mengemas seluruh kode sebelum main() atau tanpa main()
Tulisan terkait: Packing a codebase into a single function
Ia bercanda bahwa semua fungsi tinggal diubah menjadi bentuk main(100+n, ...)
Jika tertarik pada topik ini, ia menyarankan melihat cpu.land yang ia buat
Isinya membahas multitasking dan proses pemuatan kode, bukan layout memori
Ia penasaran seberapa banyak proyek C yang menghindari standard library dan hanya memanggil Linux syscall secara langsung
Menurutnya menulis kode seperti ini jauh lebih menyenangkan
Fitur seperti ALSA dan DRM punya banyak keuntungan jika diakses lewat system library, bukan syscall kernel
Dijelaskan bahwa pendekatan ini lebih baik dari pendekatan ala Windows dalam hal portabilitas dan kemudahan pemeliharaan
Sekarang proyek itu dihentikan karena header nolibc milik Linux sudah bagus,
tetapi saat ini ia sedang mengembangkan bahasa interpreter Lisp berbasis syscall
Katanya ini perjalanan yang sangat menarik sebagai eksperimen membangun Linux user space langsung dari system call
Dijelaskan bahwa interpreter ELF (ld.so) menangani seluruh proses pemuatan setelah memetakan segmen ELF awal
execve memetakan segmen PT_LOAD dan mengisi aux vector di stack,
lalu melompat ke entry point milik interpreter ELF
Kernel tidak mengetahui apa pun tentang PLT/GOT
Sebagai orang yang mengajar topik ini di universitas, ia mengatakan mahasiswa sering bingung karena diagram memori
Buku teks menggambar alamat yang lebih tinggi di bagian atas, tetapi pada proses Linux nyata,
alamat rendah ada di atas dan alamat tinggi ada di bawah
Jika melihat
/proc/<pid>/maps, semakin menggulir ke bawah maka alamat makin besarJadi, ungkapan “heap tumbuh ke atas (stack tumbuh ke bawah)” hanyalah arah secara numerik,
sedangkan secara visual justru kebalikannya
Ia mengusulkan agar digambar seperti IDE, yaitu alamat makin besar saat turun ke bawah, karena jauh lebih intuitif
Hanya saja, ia mengusulkan visualisasinya lebih alami jika dibuat secara horizontal
Ia mengatakan suka melakukan eksperimen seperti ini dengan mikrokontroler PIC16 lama
Menurutnya menyenangkan bisa menangani sendiri stack pointer, timer, pengaturan variabel, dan sebagainya
Ada yang membagikan pengalaman terkait shebang(#!)
Sebuah aplikasi Java menampilkan error bahwa skrip eksekusi tidak ditemukan,
tetapi masalah sebenarnya adalah path shebang pada skrip yang salah
Di lokal skrip itu berjalan baik, tetapi di server remote path interpreter-nya berbeda sehingga menimbulkan masalah
Ia menyarankan menjalankannya dengan strace agar bisa langsung melihat syscall mana yang memunculkan error
Saat debugging, ia mengatakan selalu bingung kapan urutan relocation pada biner utama diterapkan
Apakah sebelum atau sesudah linker menyelesaikan simbolnya sendiri terasa seperti ilmu hitam
Ditunjukkan bahwa tautan pada bagian “lang_start function (defined here)” di dalam Markdown rusak