Alur Kompilasi

Empat tahap pembentukan program, yaitu preproses, kompilasi, perakitan, dan pengaitan, beserta keluaran tiap tahap.

Daftar isi

Perintah gcc halo.c -o halo terlihat seperti langkah tunggal, padahal di dalamnya terjadi empat tahap yang berurutan. Topik ini membahas keempat tahap tersebut, yaitu preproses, kompilasi, perakitan, dan pengaitan, lengkap dengan berkas perantara yang dihasilkan setiap tahap. Setelah membacanya, pesan error yang berasal dari tahap kompilasi maupun tahap pengaitan dapat dibedakan. Opsi seperti -E, -S, serta -c juga dapat digunakan untuk menghentikan proses pada tahap tertentu.

Empat tahap dari kode sumber ke program#

Kompiler C tidak langsung mengubah kode sumber menjadi program utuh. Prosesnya dibagi menjadi empat tahap, dan setiap tahap menerima berkas dari tahap sebelumnya lalu menghasilkan berkas baru. Alur lengkapnya digambarkan oleh diagram berikut.

Empat tahap kompilasi dari kode sumber menjadi programpreprosesgcc -Ekompilasigcc -Sperakitangcc -cpengaitangccsalam.csalam.isalam.ssalam.osalamkode sumberkode diperluaskode assemblyberkas objekprogram utuh
Empat tahap pembentukan program C beserta opsi gcc untuk berhenti di tiap tahap

Tahap pertama adalah preproses (preprocessing), yang memproses seluruh arahan berawalan #, misalnya #include. Tahap kedua adalah kompilasi (compilation), yang menerjemahkan kode C menjadi kode assembly. Tahap ketiga adalah perakitan (assembly), yang menerjemahkan kode assembly menjadi kode mesin dalam bentuk berkas objek. Tahap keempat adalah pengaitan (linking), yang menggabungkan berkas objek dengan pustaka menjadi program utuh. Opsi pada tabel berikut menghentikan gcc pada tahap yang dimaksud.

OpsiProses berhenti setelahHasil akhirnya
-Epreprosesberkas .i
-Skompilasiberkas .s
-cperakitanberkas .o
tanpa opsipengaitanprogram utuh

Keempat tahap tersebut diuji dengan program kecil berikut. Simpan kode ini sebagai salam.c.

c salam.c
#include <stdio.h>

int main(void) {
    printf("Salam dari empat tahap kompilasi!\n");
    return 0;
}
text Keluaran
Salam dari empat tahap kompilasi!

Tahap pertama: preproses#

Preproses memproses setiap baris yang berawalan # sebelum kompiler bekerja. Arahan #include <stdio.h> membuat preprosesor menyalin seluruh isi berkas stdio.h ke posisi baris itu, sehingga berkas hasil preproses jauh lebih panjang daripada asalnya. Selain menyisipkan berkas, tahap ini juga membuang seluruh komentar, mengembangkan makro #define, dan memproses kompilasi bersyarat. Opsi -E menghentikan proses tepat setelah tahap ini:

bash
# jalankan hanya tahap preproses
gcc -E salam.c -o salam.i
# bandingkan jumlah baris sebelum dan sesudah preproses
wc -l salam.c salam.i
text Keluaran
    6 salam.c
  816 salam.i
  822 total

Berkas salam.c hanya berisi enam baris, tetapi hasil preprosesnya mencapai 816 baris karena seluruh isi stdio.h tersalin ke dalamnya. Baris-baris awal salam.i diisi deklarasi fungsi pustaka standar, sedangkan kode yang ditulis sendiri tetap berada di bagian akhir berkas:

bash
# lihat lima baris terakhir salam.i
tail -n 5 salam.i
text Keluaran
# 3 "salam.c"
int main(void) {
    printf("Salam dari empat tahap kompilasi!\n");
    return 0;
}

Baris # 3 "salam.c" adalah penanda (linemarker) buatan preprosesor, bukan kode C. Penanda ini memberi tahu posisi asli kode berikutnya sehingga pesan error nantinya tetap menunjuk baris pada salam.c, bukan baris pada salam.i. Hasil preproses masih berupa teks C yang sah, sehingga salam.i sebenarnya dapat dikompilasi langsung menjadi program tanpa melalui berkas sumber aslinya. Dengan kata lain, preproses hanya menyiapkan teks, dan di tahap inilah komentar, seperti yang dipraktikkan pada topik Komentar dan Gaya Kode, benar-benar dihapus dari program.

Tahap kedua: kompilasi#

Tahap kompilasi menerjemahkan kode C hasil preproses menjadi kode assembly, yaitu teks berisi instruksi mesin yang masih berbentuk kata-kata seperti pushq dan call. Opsi -S menghentikan proses setelah tahap ini:

bash
# jalankan preproses dan kompilasi
gcc -S salam.c -o salam.s
# tampilkan isi kode assembly, tab dijadikan empat spasi agar rapi
sed 's/\t/    /g' salam.s
text Keluaran
    .file    "salam.c"
    .text
    .section    .rodata
    .align 8
.LC0:
    .string    "Salam dari empat tahap kompilasi!"
    .text
    .globl    main
    .type    main, @function
main:
.LFB0:
    .cfi_startproc
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    leaq    .LC0(%rip), %rax
    movq    %rax, %rdi
    call    puts@PLT
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size    main, .-main
    .ident    "GCC: (Debian 14.2.0-19) 14.2.0"
    .section    .note.GNU-stack,"",@progbits

Teks program masih terbaca sebagian. Baris .string menyimpan teks yang dicetak, call puts@PLT memanggil fungsi pustaka untuk mencetaknya, dan movl $0, %eax menyiapkan nilai kembali 0 sesuai pernyataan return 0;. Tiga baris ini menunjukkan hasil penerjemahan tiga baris kode asli tersebut. Nama-nama berawalan titik seperti .LC0 adalah label yang menandai posisi di dalam kode, sedangkan %rax dan %rdi adalah nama register, yaitu penyimpan sementara di dalam prosesor.

Tahap inilah yang paling sering dihubungkan dengan kata kompilasi dalam arti sempit. Error yang muncul di sini bersifat semantik maupun sintaksik, misalnya titik koma yang hilang, tipe yang tidak cocok, atau pemanggilan fungsi yang tidak dideklarasikan. Kesalahan sejenis terdeteksi karena kode C diperiksa dan diterjemahkan pada tahap ini.

Pemanggilan yang muncul adalah puts, bukan printf. Kompiler mengenali bahwa printf yang hanya mencetak teks lalu pindah baris dapat digantikan fungsi puts yang lebih sederhana. Penggantian seperti ini adalah contoh pekerjaan kompiler di luar sekadar menerjemahkan, yaitu mengubah kode menjadi bentuk yang setara namun lebih efisien.

Tahap ketiga dan keempat: perakitan dan pengaitan#

Perakitan menerjemahkan kode assembly menjadi kode mesin biner dan menyimpannya dalam berkas objek (object file). Opsi -c menghentikan proses setelah tahap ini:

bash
# rakit salam.s menjadi berkas objek
gcc -c salam.s -o salam.o
# periksa jenis berkas salam.o
file salam.o
text Keluaran
salam.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

Berkas objek sudah berisi kode mesin, tetapi belum bisa dijalankan karena sebagian nama fungsi di dalamnya belum memiliki alamat. Formatnya adalah ELF (Executable and Linkable Format), format berkas biner standar di Linux. Status relocatable menandakan alamat-alamat di dalamnya masih boleh digeser saat digabungkan. Daftar simbol berkas objek memperlihatkan kondisi tersebut:

bash
# tampilkan tabel simbol salam.o
nm salam.o
text Keluaran
0000000000000000 T main
                 U puts

Huruf T menandakan main sudah terdefinisi di dalam berkas objek ini, sedangkan huruf U (undefined) menandakan puts masih menunggu definisi dari luar. Tahap pengaitan inilah yang mencarikan definisi tersebut. Linker menggabungkan salam.o dengan pustaka standar C tempat puts berada, lalu menghasilkan program utuh. Pada sistem Linux, pustaka standar dirangkai dalam bentuk pustaka bersama (shared library), yaitu pustaka yang berkasnya dipakai bersama oleh banyak program pada saat berjalan. Karena itu, kode puts sendiri baru benar-benar dimuat ketika program dieksekusi:

bash
# kaitkan salam.o dengan pustaka standar menjadi program utuh
gcc salam.o -o salam
# jalankan program hasil pengaitan
./salam
text Keluaran
Salam dari empat tahap kompilasi!

Kesalahan umum#

Pesan error pada topik ini paling sering muncul di tahap pengaitan, bukan di tahap kompilasi. Kompiler hanya menuntut setiap fungsi yang dipanggil memiliki deklarasi, sedangkan linker memeriksa keberadaan definisinya. Deklarasi tanpa definisi berikut lolos dari tahap kompilasi, tetapi gagal saat dikaitkan.

c Contoh: fungsi dideklarasikan tanpa definisi
// tidak-dijalankan
int jumlah(int a, int b);

int main(void) {
    printf("Hasil: %d\n", jumlah(2, 3));
    return 0;
}

Apabila kode di atas disimpan dan dikompilasi, dua baris terakhir pesannya berbunyi:

text Pesan error pengaitan (dua baris terakhir)
(.text+0xf): undefined reference to `jumlah'
collect2: error: ld returned 1 exit status

Frasa undefined reference selalu berarti linker tidak menemukan definisi sebuah nama. Penyebabnya lazimnya salah satu dari tiga hal, yaitu nama fungsi salah tulis, berkas .c yang memuat definisinya tidak disertakan pada perintah kompilasi, atau fungsi pustaka yang bersangkutan belum dihubungkan. Contoh penyebab ketiga adalah fungsi matematika seperti sqrt yang pada banyak sistem menuntut opsi -lm di akhir perintah kompilasi agar definisinya ditemukan.

Membedakan tahap asal pesan error mempercepat pencarian penyebabnya. Error sintaks dan tipe datang dari kompiler dan pesannya menyebut nomor baris kode sumber, sedangkan undefined reference datang dari linker dan pesannya menyebut nama simbol yang tidak ditemukan. Keduanya menuntut perbaikan yang berbeda, sehingga mengenali asalnya sudah setengah jalan menyelesaikannya.

Ringkasan#

  • Kompilasi C terdiri atas empat tahap berurutan, yaitu preproses, kompilasi, perakitan, dan pengaitan.
  • Opsi -E, -S, dan -c menghentikan gcc setelah preproses, kompilasi, dan perakitan, dengan hasil berkas .i, .s, dan .o.
  • Preprosesor menyalin isi berkas header, kompiler menerjemahkan C menjadi assembly, dan perakit mengubahnya menjadi kode mesin.
  • Linker mengisi nama-nama yang belum terdefinisi, seperti puts, dengan definisi dari pustaka standar, dan pesan undefined reference berasal dari tahap inilah.

Referensi#