Saat kita mengetik gcc main.c -o main, GCC sebenarnya menjalankan empat tahap berurutan: preprocessing, compilation, assembly, dan linking. Setiap tahap memiliki masalah khasnya sendiri — kesalahan makro muncul di preprocessing, kesalahan sintaks di compilation, dan undefined reference di linking. Pada Chapter ini kita akan menjalankan setiap tahap secara terpisah, memeriksa output antara, dan melihat apa yang sebenarnya dihasilkan oleh GCC pada masing-masing tahap.

Empat Tahap Kompilasi #

Tahapan yang berjalan saat GCC memproses file .c adalah:

  1. Preprocessing — memproses direktif #include, #define, #if, dan lainnya; menghasilkan kode C murni tanpa direktif.
  2. Compilation — menerjemahkan kode C hasil preprocess menjadi kode assembly spesifik arsitektur.
  3. Assembly — menerjemahkan kode assembly menjadi kode mesin; menghasilkan object file .o.
  4. Linking — menggabungkan satu atau lebih object file beserta pustaka standar menjadi executable.

GCC menyediakan flag untuk menghentikan proses pada setiap tahap agar kita bisa memeriksa hasil antara.

Visualisasi Konsep #

Diagram berikut memetakan empat tahap kompilasi GCC beserta file antara yang dihasilkan pada setiap tahap:

Pada diagram di atas, file main.c mengalir melalui empat tahap berurutan: preprocessing (mengolah #include dan #define), compilation (menerjemahkan C menjadi assembly), assembly (menerjemahkan assembly menjadi object file kode mesin), dan linking (menggabungkan object file dengan pustaka standar menjadi executable). Setiap tahap menghasilkan file antara yang bisa diperiksa dengan flag -E, -S, dan -c. Pesan galat yang muncul pada tahap berbeda menunjukkan masalah berbeda: preprocessor error untuk direktif #, compiler error untuk sintaks C, dan linker error (undefined reference) untuk simbol tak terdefinisi.

Tahap Preprocessing #

Flag -E menyuruh GCC berhenti setelah preprocessing. Output ditulis ke standard output; biasanya kita piping ke tail agar terbaca. Mari kita lihat kode sumber berikut:

c
#include <stdio.h>

#define NAMA "Andi"
#define UBAH 5

int main(void) {
    int nilai = 10 + UBAH;
    printf("Halo, %s! Nilai = %d\n", NAMA, nilai);
    return 0;
}

Periksa hasil preprocessing dengan perintah:

bash
gcc -std=c11 -Wall -Wextra -E test_ch11_a.c | tail -6

Output yang Dihasilkan:

text
# 6 "/tmp/test_ch11_a.c"
int main(void) {
    int nilai = 10 + 5;
    printf("Halo, %s! Nilai = %d\n", "Andi", nilai);
    return 0;
}

Mari kita telaah baris-baris output di atas:

  1. Baris 1 (# 6 "/tmp/test_ch11_a.c"): penanda baris (line marker) yang dipakai compiler untuk melacak baris asli kode sumber. Format # N "file" menyatakan baris berikutnya berasal dari file tersebut pada baris N.
  2. Baris 3 (int nilai = 10 + 5;): makro UBAH sudah diganti dengan 5; compiler sekarang melihat ekspresi aritmetika 10 + 5.
  3. Baris 4 (printf("Halo, %s! ...", "Andi", nilai);): makro NAMA sudah diganti dengan string literal "Andi".

Tahap Compilation ke Assembly #

Flag -S menyuruh GCC berhenti setelah compilation, menghasilkan file assembly .s. Mari kita lihat kode assembly yang dihasilkan:

bash
gcc -std=c11 -Wall -Wextra -S test_ch11_a.c -o test_ch11_a.s
head -15 test_ch11_a.s

Output yang Dihasilkan:

text
        .file   "test_ch11_a.c"
        .text
        .section        .rodata
.LC0:
        .string "Andi"
.LC1:
        .string "Halo, %s! Nilai = %d\n"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16

Mari kita telaah baris-baris output assembly di atas:

  1. Baris 1 (.file "test_ch11_a.c"): direktif assembler yang menyatakan nama file sumber.
  2. Baris 4 (.LC0:): label untuk string "Andi" di section .rodata (data hanya baca).
  3. Baris 6 (.LC1:): label untuk string format printf.
  4. Baris 8 (.globl main): menandai simbol main sebagai global agar bisa dipanggil oleh startup code sistem operasi.
  5. Baris 10 (main:): titik masuk fungsi main di tingkat assembly.
  6. Baris 13 (pushq %rbp): instruksi prolog fungsi yang menyimpan base pointer di stack — pola standar pemanggilan fungsi pada arsitektur x86-64.

Tahap Assembly ke Object File #

Flag -c menyuruh GCC berhenti setelah assembly, menghasilkan object file .o berisi kode mesin namun belum siap dijalankan:

bash
gcc -std=c11 -Wall -Wextra -c test_ch11_a.c -o test_ch11_a.o
file test_ch11_a.o
ls -l test_ch11_a.o

Output yang Dihasilkan:

text
test_ch11_a.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
-rw-rw-r-- 1 z z 1448 Oct 13 03:46 test_ch11_a.o

Perintah file mengonfirmasi format object file ini: ELF relocatable 64-bit untuk arsitektur x86-64. Kata relocatable berarti alamat-alamat simbol di dalamnya belum ditentukan; tugas linker adalah memfinalisasi alamat tersebut.

Kita bisa memeriksa simbol apa saja yang ada di object file dengan nm:

bash
nm test_ch11_a.o

Output yang Dihasilkan:

text
0000000000000000 T main
                 U printf

Mari kita telaah baris-baris output di atas:

  1. Baris 1 (T main): huruf T berarti simbol berada di section text (kode) dan sudah terdefinisi di object file ini.
  2. Baris 2 (U printf): huruf U berarti simbol undefined — object file ini memakai printf tetapi tidak mendefinisikannya; linker harus mencari definisi printf di pustaka standar.

Tahap Linking #

Tahap terakhir adalah linking. Pada tahap ini, linker menggabungkan satu atau lebih object file beserta pustaka standar (seperti libc) menjadi executable. Pustaka standar berisi definisi printf dan fungsi-fungsi lain yang dipakai runtime C.

bash
gcc -std=c11 -o test_ch11_a test_ch11_a.o
./test_ch11_a

Output yang Dihasilkan:

text
Halo, Andi! Nilai = 15

Linker mengambil object file test_ch11_a.o yang berisi simbol main dan printf (belum terdefinisi), lalu mencari definisi printf di pustaka standar libc. Setelah simbol-simbol diresolusi, executable akhir test_ch11_a siap dijalankan.

Melakukan Keempat Tahap Satu Perintah #

Dalam praktik sehari-hari, kita jarang memisahkan keempat tahap secara eksplisit. Satu perintah gcc saja sudah cukup:

bash
gcc -std=c11 -Wall -Wextra -o program main.c

GCC secara internal menjalankan preprocessor cpp, compiler cc1, assembler as, dan linker ld secara berurutan. Memahami tahap-tahap ini tetap penting karena:

  • Pesan galat seperti undefined reference to 'printf' menandakan masalah di linking.
  • Pesan galat seperti expected ';' before '}' token menandakan masalah di compilation.
  • Pesan galat seperti #include expects "FILENAME" or <FILENAME> menandakan masalah di preprocessing.
  • Saat men-debug kompilasi, gunakan flag -v (verbose) untuk melihat keempat tahap yang berjalan: gcc -v -std=c11 -o program main.c. Output akan menampilkan setiap sub-program yang dipanggil beserta argumennya.
  • Pesan galat undefined reference yang melibatkan fungsi pustaka standar seperti printf atau sqrt biasanya berarti kita lupa menyertakan pustaka saat linking. Untuk printf, pustaka libc di-link otomatis oleh GCC. Untuk sqrt dari header <math.h>, kita perlu menambahkan flag -lm di akhir perintah gcc.