Saat kita mengetik g++ program.cpp -o program, compiler sebenarnya menjalankan empat tahap berurutan yang masing-masing menghasilkan keluaran berbeda. Memahami tahapan ini bukan sekadar teori: ketika kita menemui pesan undefined reference atau header file not found, mengetahui di tahap mana error terjadi mempercepat diagnosis. Pada Chapter ini kita akan menjalankan tiap tahap secara terpisah dengan flag -E, -S, dan -c, lalu menghubungkan beberapa file objek menjadi satu binary. Kita akan memakai proyek kecil dengan dua file .cpp dan satu header .hpp untuk mendemokan proses linking.

Empat Tahap Kompilasi #

Pipeline kompilasi C++ terdiri dari empat tahap:

Tabel berikut merangkum tiap tahap, perintah untuk menjalankannya secara terpisah, dan keluaran yang dihasilkan:

TahapAlatPerintah GCCKeluaran
1. PreprocessingPreprocessor (cpp)g++ -E file.cppKode sumber dengan macro & include disubstitusi
2. KompilasiCompiler (cc1plus)g++ -S file.cppFile assembly .s
3. AssemblingAssembler (as)g++ -c file.cppFile objek .o
4. LinkingLinker (ld)g++ file1.o file2.o -o exeBinary executable

Struktur Proyek Demo #

Kita akan memakai tiga file berikut untuk mendemokan pipeline:

cpp
// File: math_utils.hpp
#ifndef MATH_UTILS_HPP
#define MATH_UTILS_HPP

int kuadrat(int x);

#endif
cpp
// File: math_utils.cpp
#include "math_utils.hpp"

int kuadrat(int x) {
    return x * x;
}
cpp
// File: main_pipeline.cpp
#include <iostream>
#include "math_utils.hpp"

#define NAMA "Pipeline Demo"

int main() {
    std::cout << NAMA << "\n";
    std::cout << "Kuadrat 7 = " << kuadrat(7) << "\n";
    return 0;
}

Header guard #ifndef ... #define ... #endif pada math_utils.hpp mencegah double inclusion ketika header disertakan dari beberapa file .cpp sekaligus. Tanpa header guard, definisi kuadrat akan tercatat dua kali dan menyebabkan error saat linking.

Tahap 1: Preprocessing #

Preprocessor memproses semua direktif yang diawali #: #include disubstitusi dengan isi file yang ditunjuk, #define disubstitusi pada tempat pemakaiannya, #ifdef/#endif mengevaluasi kondisi kompilasi bersyarat.

Mekanisme #

Saat preprocessor menemui #include "math_utils.hpp", preprocessor membuka file math_utils.hpp lalu menempelkan isinya menggantikan baris #include. Saat menemui #define KUADRAT(x) ((x) * (x)), setiap pemanggilan KUADRAT(7) digantikan oleh ((7) * (7)). Preprocessor juga menyisipkan line markers berbentuk # angka "file" agar compiler dapat menelusuri lokasi asli tiap baris untuk pesan error.

Perintah dan Output #

Untuk melihat hasil preprocessing saja (tanpa kompilasi), pakai flag -E. Agar output lebih ringkas, kita pakai file demo kecil tanpa <iostream>:

cpp
// File: prepro_demo.cpp
#define NAMA "Pipeline Demo"
#define KUADRAT(x) ((x) * (x))

#include "math_utils.hpp"

int main() {
    int hasil = KUADRAT(7);
    return hasil + kuadrat(7);
}

Jalankan preprocessor dan tampilkan bagian akhir output:

bash
g++ -std=c++17 -E prepro_demo.cpp | tail -15

Output yang Dihasilkan (hanya bagian akhir ditampilkan):

text
# 1 "math_utils.hpp" 1




int kuadrat(int x);
# 6 "prepro_demo.cpp" 2

int main() {
    int hasil = ((7) * (7));
    return hasil + kuadrat(7);
}

Bedah Output #

  1. Baris int kuadrat(int x);: ini berasal dari math_utils.hpp yang disisipkan menggantikan #include "math_utils.hpp". Header guard membuat #ifndef pertama tidak mengulang definisi bila header diinclude ulang.
  2. Baris int hasil = ((7) * (7));: macro KUADRAT(7) digantikan oleh definisi ((7) * (7)). Tanda kurung ganda mencegah precedence bug ketika argumen berupa ekspresi.
  3. # 1 "math_utils.hpp" 1 dan # 6 "prepro_demo.cpp" 2: line markers yang dipakai compiler untuk melacak asal usul tiap baris. Inilah yang membuat pesan error dapat menyebut file header walaupun kompilasi dimulai dari file .cpp.

Tahap 2: Kompilasi ke Assembly #

Pada tahap kedua, compiler menerjemahkan translation unit (hasil preprocessing) menjadi kode assembly. Kode assembly adalah representasi teks yang mendekati instruksi mesin tetapi masih dapat dibaca manusia.

Perintah dan Output #

bash
g++ -std=c++17 -S main_pipeline.cpp -o main_pipeline.s

Perintah di atas menghasilkan file main_pipeline.s. Tampilkan beberapa baris pertama:

bash
head -5 main_pipeline.s

Output yang Dihasilkan:

text
	.file	"main_pipeline.cpp"
	.text
#APP
	.globl	_ZSt21ios_base_library_initv
	.section	.rodata

Baris-baris di atas adalah instruksi assembly AT&T syntax. Simbol _ZSt21ios_base_library_initv adalah mangled name dari fungsi internal STL yang dipanggil saat inisialisasi. Name mangling mengkodekan informasi tipe ke dalam nama simbol agar fitur seperti function overloading dapat bekerja.

Tahap 3: Assembling ke Object Code #

Tahap ketiga, assembling, menerjemahkan kode assembly menjadi instruksi mesin biner. Hasilnya disebut object file dengan ekstensi .o (Linux/macOS) atau .obj (Windows). Object file belum bisa dijalankan karena referensi ke simbol di file lain belum diselesaikan.

Perintah dan Output #

bash
g++ -std=c++17 -c main_pipeline.cpp -o main_pipeline.o
g++ -std=c++17 -c math_utils.cpp -o math_utils.o

Verifikasi tipe file dengan perintah file:

bash
file main_pipeline.o

Output yang Dihasilkan (di Linux 64-bit):

text
main_pipeline.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

Penjelasan output:

  • ELF: format file executable Linux (Executable and Linkable Format).
  • 64-bit LSB: arsitektur 64-bit, least significant byte (little-endian).
  • relocatable: alamat simbol belum final, akan diselesaikan saat linking.
  • x86-64: target arsitektur CPU.
  • not stripped: masih mengandung simbol debug.

Tahap 4: Linking #

Tahap terakhir, linking, menggabungkan beberapa object file dan library menjadi satu binary executable. Linker menyelesaikan referensi simbol antar file: misalnya, main_pipeline.o memanggil kuadrat(int) yang didefinisikan di math_utils.o. Linker mencocokkan kedua simbol tersebut dan menghasilkan binary yang dapat dijalankan.

Perintah dan Output #

bash
g++ -std=c++17 main_pipeline.o math_utils.o -o pipeline_demo
./pipeline_demo

Output yang Dihasilkan:

text
Pipeline Demo
Kuadrat 7 = 49

Bedah Proses Linking #

  1. main_pipeline.o berisi fungsi main yang memanggil kuadrat(7). Pada saat compiling main_pipeline.cpp, compiler tidak tahu implementasi kuadrat — compiler hanya tahu deklarasi dari math_utils.hpp. Alamat kuadrat sementara dibiarkan kosong di object file.
  2. math_utils.o berisi definisi kuadrat. Object file ini menyediakan alamat fungsi kuadrat.
  3. Saat linking, linker membaca kedua file objek, mencocokkan referensi kuadrat di main_pipeline.o dengan definisi di math_utils.o, lalu mengisi alamat akhir.
  4. Setelah seluruh simbol terkait, linker menambahkan runtime startup code yang memanggil main dari sistem operasi dan menghasilkan binary pipeline_demo.

Pesan Error Umum Tiap Tahap #

Memahami tahap kompilasi membantu kita memetakan pesan error:

TahapPesan Error KhasPenyebab Umum
Preprocessingfatal error: foo.hpp: No such file or directoryPath include salah atau file header tidak ada
Kompilasierror: 'X' was not declared in this scopeVariabel/fungsi belum dideklarasikan
Kompilasierror: no matching function for call to ...Tipe argumen tidak cocok dengan overload
AssemblingJarang error; biasanya masalah hardware/diskFile .s korup
Linkingundefined reference to 'kuadrat(int)'Definisi fungsi tidak ditemukan saat linking
Linkingmultiple definition of 'X'Fungsi/variabel didefinisikan di header tanpa inline

Memeriksa Simbol di Object File #

Untuk melihat simbol apa saja yang ada di file objek, kita bisa memakai nm:

bash
nm math_utils.o

Output yang Dihasilkan (disederhanakan):

text
0000000000000000 T _Z7kuadrati

Penjelasan:

  • T di kolom kedua berarti simbol berada di section .text (kode, global).
  • _Z7kuadrati adalah mangled name. Dekode: _Z = awal, 7 = panjang nama berikutnya, kuadrat = nama fungsi, i = tipe parameter int. Compiler demangle: kuadrat(int).

Perintah c++filt mendekode mangled name ke bentuk asli:

bash
nm math_utils.o | c++filt

Output yang Dihasilkan:

text
0000000000000000 T kuadrat(int)

Ringkasan #

Pada Chapter ini kita telah membedah empat tahap kompilasi C++:

  • Preprocessing (-E): substitusi #include, #define, dan #ifdef.
  • Kompilasi (-S): menerjemahkan translation unit menjadi kode assembly.
  • Assembling (-c): menerjemahkan assembly menjadi object file .o.
  • Linking: menggabungkan object file dan library menjadi binary executable.

Pemahaman ini akan membantu kita men-debug error undefined reference, masalah header guard, dan kebingungan seputar translation unit. Pada Chapter berikutnya kita akan memakai CMake untuk mengotomatiskan proses build pada proyek multi-file.