Setiap kali kita mengetik satu perintah g++, sebenarnya empat program bekerja berantai di belakang layar: preprocessor, compiler, assembler, dan linker. Selama hasilnya benar, kita boleh tidak peduli — tetapi begitu muncul error yang penyebabnya tidak jelas seperti undefined reference, pemahaman atas empat tahap ini menjadi satu-satunya peta yang menunjukkan di tahap mana masalah itu lahir. Kabar baiknya, tahap-tahap tersebut tidak tersembunyi: g++ menyediakan flag untuk menjalankan setiap tahap secara manual, dan pada Chapter ini kita akan memecah satu kompilasi menjadi empat langkah yang semua hasilnya kita periksa sendiri.

Program Contoh dan Peta Empat Tahap #

Program bahan uji kita cukup kecil, tetapi memuat semua yang dibutuhkan tiap tahap: sebuah komentar, konstanta, fungsi, dan pemakaian pustaka standar. File ini sudah diuji di lingkungan praktikum:

cpp
#include <iostream>

// Komentar ini tidak akan pernah sampai ke compiler
const int SISI = 6;

int volumeKubus(int sisi) {
    return sisi * sisi * sisi;
}

int main() {
    std::cout << "Volume kubus sisi " << SISI << " = " << volumeKubus(SISI) << "\n";
    return 0;
}

Output yang Dihasilkan:

text
Volume kubus sisi 6 = 216

Berikut peta perjalanannya dari teks sampai executable:

Perhatikan pola ekstensinya: .i untuk hasil preprocessor, .s untuk assembly, dan .o untuk object file. Setiap flag di diagram bisa dipanggil manual, dan berikutnya kita lakukan tepat itu — tahap demi tahap, dengan file hasilnya diperiksa sungguhan.

Tahap 1: Preprocessor #

Flag -E memerintahkan g++ berhenti setelah preprocessor. Preprocessor memproses semua directive berawalan #: #include menyalin isi header ke tempat directive itu berada, komentar dibuang, dan makro diganti. Jalankan pada file kita, lalu periksa hasilnya:

bash
g++ -std=c++23 -E tahapan.cpp -o tahapan.i
wc -l tahapan.i
text
69366 tahapan.i

Dari sekitar 13 baris source code, hasil preprocessor membengkak menjadi 69.366 baris — hampir semuanya adalah isi header <iostream> yang disalin utuh beserta header-header yang disertakannya. Bukti kedua lebih dramatis: kata “Komentar” pada file kita ada satu, tetapi di hasil preprocessor hilang sama sekali:

bash
grep -c "Komentar" tahapan.cpp tahapan.i
text
tahapan.cpp:1
tahapan.i:0

Inilah sifat preprocessor: ia manipulator teks murni, tidak memahami tipe atau fungsi. Kode yang keluar darinya adalah satu berkas teks panjang yang sudah “siap baca compiler” — komentar hilang, header menyatu, dan semua directive # telah dieksekusi.

Tahap 2: Kompilasi ke Assembly #

Flag -S menghentikan proses setelah compiler menerjemahkan kode menjadi assembly — representasi teks yang hampir satu banding satu dengan instruksi CPU. Tahap ini juga tempat sebagian besar optimasi dan semua pemeriksaan tipe dilakukan:

bash
g++ -std=c++23 -S tahapan.i -o tahapan.s
wc -l tahapan.s
text
348 tahapan.s

Enam baris pertama file assembly-nya tampak seperti ini:

text
	.file	"tahapan.cpp"
	.text
#APP
	.globl	_ZSt21ios_base_library_initv
#NO_APP
	.section	.rodata

Baris-baris berawalan titik (.file, .text, .section) adalah directive assembler, dan simbol dengan awalan panjang seperti _ZSt21ios... adalah nama fungsi pustaka yang sudah mangled — teknik penamaan C++ untuk mengemas tipe parameter ke dalam nama simbol. Perhatikan penyusutan drastisnya: 69.366 baris source diperluas menyusut menjadi 348 baris assembly, karena compiler hanya menyusun instruksi untuk kode yang kita tulis, bukan seluruh isi header.

Tahap 3: Assembler dan Object File #

Flag -c menjalankan assembler, yang menerjemahkan teks assembly menjadi machine code dalam format object file:

bash
g++ -std=c++23 -c tahapan.s -o tahapan.o
file tahapan.o
text
tahapan.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

Kata kunci di output itu adalah relocatable: machine code sudah ada, tetapi alamat dan beberapa simbol belum final. Pemanggilan std::cout di kode kita, misalnya, baru berupa “janji” bahwa implementasinya ada di tempat lain — object file tidak tahu di mana pustaka standar itu berada. Di sinilah tahap terakhir masuk.

Tahap 4: Linking #

Tanpa flag tambahan, g++ pada file .o menjalankan linker: menggabungkan object file milik kita dengan pustaka yang dibutuhkan, menyelesaikan semua “janji” simbol, dan mengemas hasilnya menjadi executable:

bash
g++ tahapan.o -o tahapan
file tahapan | cut -d, -f1-4
./tahapan
text
tahapan: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked
Volume kubus sisi 6 = 216

Output file kita potong dengan cut karena sisa barisnya berisi path interpreter dan BuildID — sidik jari biner yang berbeda antar mesin dan build. Yang penting dari output itu adalah kata executable dan dynamically linked: program siap dijalankan, dan ia masih bergantung pada pustaka sistem saat berjalan. Bila linker gagal — misalnya kita memanggil fungsi yang tidak pernah ada — pesannya adalah undefined reference to ..., dan sekarang Anda tahu pesan itu lahir dari tahap keempat, bukan dari compiler.

Dinamis vs Statis pada Linking #

Kata dynamically linked di atas layak dijelaskan. Default-nya linker tidak menyalin kode pustaka ke dalam program; ia hanya mencatat “program ini butuh libstdc++, muatkan saat dijalankan”. Kita bisa membuktikannya dengan ldd, yang mendaftar pustaka yang dibutuhkan sebuah executable:

bash
ldd tahapan | awk '{print $1}'
text
linux-vdso.so.1
libstdc++.so.6
libc.so.6
libm.so.6
/lib64/ld-linux-x86-64.so.2
libgcc_s.so.1

Baris-baris .so itu adalah pustaka dinamis: libstdc++.so.6 berisi implementasi std::cout dan teman-temannya, sedangkan baris terakhir adalah dynamic loader, bukan pustaka biasa. Kebalikannya, flag -static menyuruh linker menyalin seluruh kode pustaka ke dalam executable sehingga programnya mandiri:

bash
g++ -static tahapan.o -o tahapan-statis
ldd tahapan-statis
text
	not a dynamic executable

Harga mandirinya bisa kita lihat dari ukuran file (byte):

bash
ls -la tahapan tahapan-statis | awk '{print $5, $NF}'
text
22120 tahapan
2278536 tahapan-statis

Executable dinamis kita hanya 22 KB karena berbagi pustaka dengan seluruh sistem, sementara versi statis membengkak menjadi sekitar 2,2 MB karena membawa semuanya. Program statis lebih mudah disebarkan (tidak ada ketergantungan versi pustaka), sedangkan program dinamis lebih kecil dan menerima perbaikan pustaka tanpa kompilasi ulang — trade-off yang akan kembali muncul pada Section Deployment dan Distribusi.

Dari empat tahap tadi, executable akhirnya tersimpan di disk — tetapi menyimpan bukan berarti berjalan. Chapter berikutnya membahas sisi sebaliknya: bagaimana program dijalankan, mengapa Linux menuntut ./ di depan nama program, dan bagaimana exit code serta argumen program bekerja dari sisi shell.