Saat kita menulis gcc main.c -o main, sebenarnya terdapat empat tahap berbeda yang berjalan secara berurutan di balik layar. Memahami setiap tahap penting karena setiap tahap memiliki masalah tipikalnya sendiri: kesalahan makro terjadi di preprocessing, kesalahan sintaks di compilation, masalah symbol di linking, dan masalah system call baru muncul saat menjalankan executable. Pada Chapter ini kita akan menelusuri keempat tahap tersebut lewat contoh konkret, lalu melihat bagaimana linker menyatukan beberapa object file menjadi satu executable.
Empat Tahap: Preprocess, Compile, Assemble, Link #
Tahap pertama adalah preprocessing, yang dipicu oleh direktif awalan # seperti #include dan #define. Preprocessor mengganti direktif ini dengan hasil substitusi teks. Tahap kedua adalah compilation: kode sumber hasil preprocess diterjemahkan menjadi assembly spesifik arsitektur. Tahap ketiga adalah assembly: kode assembly diterjemahkan menjadi kode mesin, menghasilkan object file berekstensi .o. Tahap keempat adalah linking: linker menggabungkan beberapa object file dan pustaka standar menjadi satu executable.
Visualisasi Konsep #
Diagram berikut memperlihatkan perjalanan satu file sumber .c melalui empat tahap sampai menjadi executable yang siap dijalankan sistem operasi:
Pada diagram di atas, file main.c diubah bertahap menuju executable. Tahap preprocess mengganti direktif # dengan kode sumber murni. Tahap compile menerjemahkan C menjadi assembly. Tahap assemble mengubah assembly menjadi object file main.o berisi kode mesin yang belum punya alamat final (masih relocatable). Tahap link menggabungkan main.o dengan pustaka standar (misalnya libc tempat printf didefinisikan) dan menentukan alamat final setiap simbol, menghasilkan executable yang bisa dijalankan sistem operasi. Pesan galat berbeda muncul pada tahap berbeda—kesalahan makro di preprocess, kesalahan sintaks di compile, undefined reference di link.
Mari kita lihat kode sumber yang memakai makro prapemroses:
#include <stdio.h>
#define VERSI "1.0"
#define KUADRAT(x) ((x) * (x))
int main(void) {
printf("Versi program: %s\n", VERSI);
printf("Kuadrat 9 = %d\n", KUADRAT(9));
return 0;
}Output yang Dihasilkan:
Versi program: 1.0
Kuadrat 9 = 81Mari kita telaah baris-baris kode di atas:
- Baris 3 (
#define VERSI "1.0"): mendefinisikan makro objekVERSIyang akan diganti dengan string"1.0"pada tahap preprocessing. - Baris 4 (
#define KUADRAT(x) ((x) * (x))): mendefinisikan makro fungsiKUADRAT; tanda kurung ganda mencegah bug saat argumen berupa ekspresi kompleks. - Baris 7 (
printf("...%s\n", VERSI)): pada tahap preprocessing,VERSIdiganti menjadi"1.0"sebelum compiler melihat kode. - Baris 8 (
KUADRAT(9)): makro diekspansi menjadi((9) * (9)), lalu dikompilasi menjadi instruksi perkalian.
Kita bisa melihat hasil tahap preprocessing secara langsung dengan flag -E:
gcc -std=c11 -Wall -Wextra -E /tmp/test_ch4_macro.c | tail -8Output yang Dihasilkan:
# 6 "/tmp/test_ch4_macro.c"
int main(void) {
printf("Versi program: %s\n", "1.0");
printf("Kuadrat 9 = %d\n", ((9) * (9)));
return 0;
}Perhatikan bahwa pada output preprocess, VERSI sudah diganti menjadi "1.0" dan KUADRAT(9) menjadi ((9) * (9)). Baris #include <stdio.h> juga sudah diganti dengan deklarasi printf yang panjang (tidak terlihat karena terpotong tail -8).
Memisahkan Tahap Compile dan Assemble #
GCC menyediakan flag untuk menghentikan proses pada setiap tahap. Flag -S menghentikan setelah compilation, menghasilkan file assembly .s. Flag -c menghentikan setelah assembly, menghasilkan object file .o. Berikut alur yang memisahkan tahap-tahap tersebut menggunakan proyek multi-file sederhana.
Misalkan kita memiliki tiga file: matematika.h, matematika.c, dan main.c. File matematika.c mengimplementasikan fungsi kali, sedangkan main.c memanggil fungsi tersebut.
/* matematika.h */
#ifndef MATEMATIKA_H
#define MATEMATIKA_H
int kali(int a, int b);
#endif/* matematika.c */
#include "matematika.h"
int kali(int a, int b) {
return a * b;
}/* main.c */
#include <stdio.h>
#include "matematika.h"
int main(void) {
int hasil = kali(6, 7);
printf("6 x 7 = %d\n", hasil);
return 0;
}Kompilasi terpisah dilakukan langkah demi langkah berikut:
gcc -std=c11 -Wall -Wextra -c matematika.c
gcc -std=c11 -Wall -Wextra -c main.c
gcc -o program main.o matematika.o
./programOutput yang Dihasilkan:
6 x 7 = 42Mari kita telaah baris-baris kode di atas:
- Baris 1 (
gcc ... -c matematika.c): kompilasimatematika.cmenjadi object filematematika.otanpa melakukan linking. - Baris 2 (
gcc ... -c main.c): kompilasimain.cmenjadimain.o; pada tahap ini compiler hanya melihat deklarasiint kali(int, int);dari header, bukan implementasinya. - Baris 3 (
gcc -o program main.o matematika.o): linker menggabungkan dua object file menjadi executableprogramsekaligus menyertakan startup code dan pustaka standar. - Baris 4 (
./program): sistem operasi memuat executable ke memori dan menjalankan fungsimain.
Peran Header dan Include Guard #
Header matematika.h memakai pola include guard:
#ifndef MATEMATIKA_H
#define MATEMATIKA_H
...
#endifPola ini mencegah header dikompilasi dua kali dalam satu unit translasi. Bila sebuah file .c meng-include header yang sama lewat rantai #include berlapis, preprocessor akan melihat MATEMATIKA_H sudah didefinisikan pada include kedua dan melewatkan isi header. Tanpa include guard, deklarasi ganda dapat memicu redefinition error.
Linker dan Penyebab Error Umum #
Linker bekerja berdasarkan nama symbol. Saat main.o memanggil kali, linker mencari definisi kali di object file atau pustaka lain. Bila definisi tidak ditemukan, kita mendapat pesan undefined reference. Bila kali didefinisikan dua kali di dua object file berbeda, kita mendapat pesan multiple definition.