Topik ini membangun mental model tentang apa yang terjadi ketika sebuah program berjalan, mencakup peran kode sumber, kompilator, kode mesin, RAM, dan CPU. Pemahaman ini menjadi landasan seluruh jalur karena setiap variabel, perulangan, dan fungsi dalam C pada akhirnya bermuara pada sel memori dan instruksi mesin. Pembahasan ditutup dengan demonstrasi nyata yang membandingkan isi berkas kode sumber dengan isi berkas program hasil kompilasi.
Dari kode sumber ke kode mesin#
Kode sumber (source code) adalah teks biasa yang dapat dibaca manusia. CPU hanya mengerti kode mesin (machine code), yaitu deretan bita yang menyatakan operasi paling dasar seperti membaca memori, menjumlahkan, dan melompat ke alamat lain. Teks tidak pernah dijalankan CPU secara langsung. Kompilator (compiler) bertugas menerjemahkan kode sumber menjadi kode mesin, dan hasilnya disimpan sebagai berkas program, yaitu berkas biner yang siap dijalankan.
Peran tiap komponen tersebut dapat diamati pada program kecil berikut. Program mengonversi 135 menit menjadi jam dan menit, lalu mencetak hasilnya. Kode disimpan sebagai hitung.c, dan setelah dikompilasi serta dijalankan, keluarannya muncul di terminal.
#include <stdio.h>
int main(void) {
int total_menit = 135;
int jam = total_menit / 60;
int sisa = total_menit % 60;
printf("%d menit = %d jam %d menit\n", total_menit, jam, sisa);
return 0;
}135 menit = 2 jam 15 menitKini bandingkan isi kedua berkasnya, yaitu hitung.c dan berkas program hasil kompilasinya. Dua perintah berikut mengompilasi ulang kode sumber, lalu menghitung ukuran keduanya dalam byte.
# kompilasi berkas sumber menjadi program
gcc hitung.c -o hitung
# bandingkan ukuran keduanya dalam byte
wc -c hitung.c hitung 214 hitung.c
15952 hitung
16166 totalUkuran program jauh lebih besar daripada sumbernya karena berisi kode mesin beserta metadata format yang diperlukan sistem operasi. Perbedaan isinya makin jelas ketika bita awal kedua berkas dibongkar.
# dua belas bita pertama berkas sumber, masih teks biasa
head -c 12 hitung.c | od -c
# empat bita pertama program hasil kompilasi
head -c 4 hitung | od -c0000000 # i n c l u d e < s t
0000014
0000000 177 E L F
0000004Berkas sumber diawali teks #include <st yang masih terbaca, sedangkan program diawali bita 177 diikuti huruf E, L, dan F. Deretan itu adalah penanda format ELF (executable and linkable format), yaitu format berkas program pada Linux. Alur lengkap dari teks hingga CPU dirangkum dalam diagram berikut.
RAM: sel memori yang bernomor#
RAM adalah deretan sel bernomor, dan setiap sel menyimpan satu byte. Nomor sel disebut alamat (address), dimulai dari 0 dan naik hingga batas kapasitas memori. Ketika program dijalankan, pemuat (loader) sistem operasi menyalin isi berkas program ke RAM, sehingga instruksi mesin dan data program sama-sama berada di memori sebelum CPU mulai bekerja. CPU tidak pernah mengeksekusi berkas yang masih berada di disk.
Sifat bernomor itu dapat dibuktikan dengan program berikut. Array lima bilangan bulat menempati lima sel berurutan, dan program mencetak selisih alamat antarelemen, bukan alamat mutlaknya. Alamat mutlak sengaja tidak dicetak karena nilainya berubah pada setiap eksekusi, sedangkan selisihnya selalu tetap.
#include <stdio.h>
int main(void) {
int nilai[5] = {10, 20, 30, 40, 50};
printf("Ukuran satu elemen int : %zu byte\n", sizeof(nilai[0]));
printf("Selisih elemen ke-0 dan ke-1 : %td byte\n", (char *)&nilai[1] - (char *)&nilai[0]);
printf("Selisih elemen ke-0 dan ke-4 : %td byte\n", (char *)&nilai[4] - (char *)&nilai[0]);
return 0;
}Ukuran satu elemen int : 4 byte
Selisih elemen ke-0 dan ke-1 : 4 byte
Selisih elemen ke-0 dan ke-4 : 16 byteSatu elemen int menempati 4 byte, sehingga elemen ke-1 berjarak 4 byte dari elemen ke-0 dan elemen ke-4 berjarak 16 byte. Letak array di RAM boleh berpindah antareksekusi, tetapi pola berjaraknya tidak pernah berubah. Pola tetap inilah yang nanti menjadi dasar kerja pointer dan indeks array.
CPU: siklus ambil, uraikan, jalankan#
CPU menjalankan program melalui siklus tiga langkah yang berulang terus-menerus. Pertama, mengambil (fetch), yaitu membaca instruksi dari alamat memori yang sedang ditunjuk. Kedua, menguraikan (decode), yaitu menerjemahkan bita instruksi menjadi operasi yang dimengerti. Ketiga, menjalankan (execute) operasi tersebut, misalnya menyalin nilai ke register atau menulis ke memori. Register adalah simpanan kecil di dalam CPU yang jauh lebih cepat dibaca dan ditulis daripada RAM. Salah satu register khusus bernama program counter menyimpan alamat instruksi berikutnya, sehingga urutan eksekusi selalu terjaga.
Simulasi berikut menampilkan RAM sebuah program kecil yang menjumlahkan 10 dan 32. Empat sel pertama memuat instruksi LOAD, ADD, STORE, dan HALT, sedangkan sel lainnya memuat data. Jalankan demo dengan menekan tombol Satu langkah beberapa kali, kemudian amati program counter bertambah satu setiap kali satu instruksi selesai, dan akumulator berubah sesuai instruksi yang dijalankan.
Satu hal penting terlihat dari simulasi tersebut, yaitu instruksi dan data sama-sama berada di sel memori tanpa pembeda fisik. Konsep ini disebut arsitektur von Neumann, dan CPU hanya mengikuti program counter untuk menentukan mana yang diperlakukan sebagai instruksi. CPU desktop modern mengeksekusi miliaran siklus seperti ini per detik, sehingga empat instruksi pada simulasi selesai dalam hitungan nanodetik pada mesin sungguhan.
Mental model sebelum menulis kode#
Seluruh pembahasan di atas dirangkum menjadi tiga pernyataan yang dipakai sepanjang jalur ini. Pertama, program yang berjalan adalah kumpulan instruksi mesin dan data yang berada di RAM, bukan teks di disk. Kedua, setiap variabel adalah satu atau beberapa sel memori bernomor, sehingga menyatakan int umur = 20; berarti memesan 4 byte lalu menulis nilai 20 ke sana. Ketiga, CPU hanya berputar pada siklus ambil, uraikan, dan jalankan yang urutannya diatur program counter.
Dengan model ini, banyak topik C pada jalur ini terasa konsisten. Pointer menjadi tidak menakutkan karena alamat memori memang hanya nomor sel. Overflow bilangan bulat dapat dijelaskan karena sel memori punya batas bita. Kebiasaan berpikir dalam model ini juga melatih kehati-hatian, karena C hampir tidak menyaring hubungan antara kode yang ditulis dan perilaku mesin yang terjadi.
Cara memakai modelnya sederhana. Ketika membaca sebuah program, lacak nilai setiap variabel seperti melacak isi sel memori setelah setiap pernyataan dijalankan. Ketika sebuah program berperilaku aneh, tanyakan instruksi mesin seperti apa yang diperintahkan oleh baris yang dicurigai. Kedua kebiasaan itu akan sering dipakai sepanjang jalur ini, dan jawabannya selalu kembali ke RAM serta CPU.
Kesalahan umum#
Menyunting kode sumber tanpa mengompilasi ulang adalah kesalahan yang paling sering membingungkan pemula. Program yang dijalankan tetap merupakan salinan hasil kompilasi terakhir, sehingga perubahan kode tidak terasa sama sekali. Solusinya sederhana, yaitu kompilasi ulang setiap kali kode sumber diubah, baru perilaku barunya diuji.
Kesalahan kedua adalah mencoba menjalankan berkas .c itu sendiri, misalnya ./hitung.c. Berkas itu hanyalah teks dan bukan program, sehingga sistem menolaknya dengan pesan permission denied. Memberi izin eksekusi pada berkas teks pun tidak menolong, karena yang gagal bukan izinnya, melainkan isinya yang bukan instruksi mesin. Yang dapat dieksekusi hanyalah hasil kompilasi, dalam contoh ini berkas hitung.
Kesalahan ketiga adalah mengira satu berkas program berjalan di semua arsitektur. Kode mesin yang dibuat untuk prosesor x86-64 tidak dimengerti prosesor ARM. Yang portabel dari C adalah kode sumbernya, bukan binernya, sehingga program yang sama perlu dikompilasi ulang pada setiap arsitektur yang dituju.
Ringkasan#
- Kode sumber adalah teks, sedangkan CPU hanya mengeksekusi kode mesin, dan kompilatorlah yang menerjemahkan antara keduanya.
- Program yang berjalan berada di RAM sebagai deretan sel bernomor yang memuat instruksi dan data sekaligus.
- CPU bekerja dalam siklus ambil, uraikan, dan jalankan yang urutannya diatur oleh program counter.
- Perubahan kode sumber baru berlaku setelah kompilasi ulang, dan kode mesin hanya berlaku pada arsitektur yang dituju.
Referensi#
- Invoking GCC — GCC, GNU Project — Cara memanggil gcc dan tahapan yang dilakukannya saat menerjemahkan kode sumber.
- execve(2) — Linux man-pages project — Cara sistem operasi memuat program baru ke memori untuk dijalankan.
- elf(5) — Linux man-pages project — Format berkas ELF yang diawali bita 0x7f serta huruf E, L, dan F.