Pada Chapter Separate Compilation kita mengompilasi file per file, menyimpan object file, lalu menyuruh linker menyambungnya — tanpa benar-benar melihat apa yang disambungkan. Kali ini kita membuka kotaknya: mata uang linker adalah simbol, nama internal untuk setiap fungsi dan objek yang satu object file sediakan dan object file lain butuhkan. Memahami simbol membuat pesan undefined reference to ... yang tadinya tidak terbaca berubah menjadi peta rute yang jelas — dan tool nm akan membuat kita bisa membacanya langsung dari object file.
Simbol: Nama yang Dilihat Linker #
Setiap fungsi dan variabel global yang kita tulis menjadi simbol di object file, dan setiap simbol berada dalam salah satu dari dua kondisi: defined — kode atau data sesungguhnya ada di file itu; undefined — file itu memakai nama tersebut, tetapi implementasinya harus dicari di tempat lain. Tugas linker persis mencocokkan keduanya: setiap simbol undefined di satu file harus menemukan satu definisi di file lain, pustaka, atau ditolak dengan error. Perhatikan pula bahwa nama simbol tidak persis sama dengan nama C++ kita: karena C++ mendukung overload — banyak fungsi bernama sama dengan parameter berbeda — compiler menyandikan tipe parameter ke dalam nama simbol, teknik yang disebut name mangling. Fungsi ringkas tadi tersimpan sebagai _Z7ringkasB5cxx11RKSt6vectorI8ItemNotaSaIS0_EE, bukan ringkas.
Membaca Simbol dengan nm #
Tool standar untuk mengintip simbol adalah nm dari binutils. Kita pakai pada modul nota tiga file berikut:
#include <string>
#include <vector>
struct ItemNota {
std::string nama;
int harga;
int jumlah;
};
int totalNota(const std::vector<ItemNota>& items);
std::string ringkas(const std::vector<ItemNota>& items);
void cetakNota(const std::vector<ItemNota>& items);#include <iostream>
#include "nota.h"
int totalNota(const std::vector<ItemNota>& items) {
int total = 0;
for (const ItemNota& item : items) {
total += item.harga * item.jumlah;
}
return total;
}
std::string ringkas(const std::vector<ItemNota>& items) {
return std::to_string(items.size()) + " item, total Rp"
+ std::to_string(totalNota(items));
}
void cetakNota(const std::vector<ItemNota>& items) {
for (const ItemNota& item : items) {
std::cout << "- " << item.nama << " x" << item.jumlah
<< " = " << item.harga * item.jumlah << "\n";
}
std::cout << "Total: " << totalNota(items) << "\n";
}#include <iostream>
#include "nota.h"
int main() {
std::vector<ItemNota> belanja {
{"Buku", 15000, 2},
{"Pulpen", 5000, 3}
};
std::cout << ringkas(belanja) << "\n";
cetakNota(belanja);
return 0;
}Kompilasi keduanya menjadi object file, lalu tanya utama.o — output nyata dari lingkungan praktikum, dengan nama main disaring bersama dua fungsi yang kita cari:
g++ -std=c++23 -Wall -Wextra -c utama.cpp
g++ -std=c++23 -Wall -Wextra -c nota.cpp
nm utama.o | grep -E "main|ringkas|cetakNota"Output yang Dihasilkan:
U _Z7ringkasB5cxx11RKSt6vectorI8ItemNotaSaIS0_EE
U _Z9cetakNotaRKSt6vectorI8ItemNotaSaIS0_EE
0000000000000000 T mainHuruf di tiap baris adalah status simbol: T berarti defined pada bagian kode (text section), sedangkan U berarti undefined — utama.o memanggil ringkas dan cetakNota dan menyerahkan pencariannya ke linker. Adapun nota.o yang menampung definisinya bisa kita baca dengan opsi -C untuk men-demangle nama:
nm -C nota.o | grep -E "totalNota|ringkas|cetakNota"Output yang Dihasilkan:
000000000000008c T ringkas[abi:cxx11](std::vector<ItemNota, std::allocator<ItemNota> > const&)
0000000000000175 T cetakNota(std::vector<ItemNota, std::allocator<ItemNota> > const&)
0000000000000000 T totalNota(std::vector<ItemNota, std::allocator<ItemNota> > const&)Huruf nm | Arti |
|---|---|
T | Defined — berada di bagian kode (text) |
U | Undefined — butuh definisi dari file lain |
D | Defined — berada di bagian data terinisialisasi |
R | Defined — berada di bagian data read-only |
Tiga T di atas berarti ketiga fungsi memang terdefinisi di nota.o — simbol undefined di utama.o sudah punya pasangan.
Saat Simbol Tidak Ketemu #
Apa yang terjadi bila ada simbol yang tidak menemukan pasangannya? Kita uji: nota.cpp versi yang kehilangan definisi cetakNota (deklarasi di header tetap ada, sehingga kompilasi utama.cpp tetap lolos):
#include "nota.h"
int totalNota(const std::vector<ItemNota>& items) {
int total = 0;
for (const ItemNota& item : items) {
total += item.harga * item.jumlah;
}
return total;
}
std::string ringkas(const std::vector<ItemNota>& items) {
return std::to_string(items.size()) + " item, total Rp"
+ std::to_string(totalNota(items));
}
// cetakNota sengaja TIDAK didefinisikan di file ini
g++ -std=c++23 -Wall -Wextra -c nota-tanpa-cetak.cpp -o nota-salah.o
g++ utama.o nota-salah.o -o nota-salah/usr/bin/ld: utama.o: in function `main':
utama.cpp:(.text+0x198): undefined reference to `cetakNota(std::vector<ItemNota, std::allocator<ItemNota> > const&)'
collect2: error: ld returned 1 exit statusInilah error undefined reference dalam bentuk paling murni — dan sekarang kita bisa membacanya baris demi baris: linker (ld) sedang menyelesaikan utama.o, tepatnya di fungsi main, dan menabrak simbol cetakNota yang tidak punya definisi di mana pun. Versi kurang ramah yang sering Anda lihat di internet menampilkan nama mangled panjang — itu simbol yang sama, hanya belum di-demangle.
Setelah semua simbol bertemu pasangannya, executable siap berjalan. Namun linker ternyata punya trik lebih dari sekadar menyambung potongan kita sendiri: ia juga tahu cara mengambil kode dari kumpulan simbol yang sudah dikemas — library. Chapter berikutnya membuka pintu itu: static library .a dan dynamic library .so, cara membuat keduanya dari nol.
Referensi: Link Options di dokumentasi GCC .