Library C yang didistribusikan ke banyak pengguna harus dapat di-update tanpa memutus aplikasi lama. Bayangkan skenario: library libfoo versi 1.0 dipakai 1000 aplikasi. Saat versi 1.1 keluar dengan tambahan fungsi baru, semua aplikasi lama harus tetap jalan tanpa recompile. Saat versi 2.0 keluar dengan ABI breaking (struct layout berubah), aplikasi lama yang sudah dikompilasi dengan 1.x tetap boleh memakai versi 1.x tanpa dipaksa upgrade ke 2.0. Dua mekanisme Linux menyediakan ini: SONAME versioning (menandai library dengan major version) dan symbol versioning (menandai setiap fungsi dengan versi ketika ditambahkan). Pada chapter ini kita akan membangun libver.so dengan SONAME dan libver2.so dengan symbol versioning.
SONAME Versioning #
Setiap shared library di Linux punya tiga nama: (1) real name file (libfoo.so.1.0.0), (2) SONAME yang disematkan ke file binary (libfoo.so.1), dan (3) linker name (libfoo.so) yang dipakai saat compile. SONAME disematkan lewat flag linker -Wl,-soname,.... Saat executable di-link, linker menyalin SONAME ke tabel dependency executable. Saat executable dijalankan, dynamic loader mencari file dengan nama SONAME (libfoo.so.1), bukan real name.
/* File: libver.c - library untuk demo SONAME versioning */
#include <stdio.h>
int ver_tambah(int a, int b) {
return a + b;
}
const char *ver_versi(void) {
return "libver 1.0.0";
}/* File: ver_main.c - konsumsi libver */
#include <stdio.h>
extern int ver_tambah(int, int);
extern const char *ver_versi(void);
int main(void) {
printf("Versi: %s\n", ver_versi());
printf("3 + 4 = %d\n", ver_tambah(3, 4));
return 0;
}Build dengan SONAME versioning:
# Build real name dengan SONAME libver.so.1
gcc -std=c11 -Wall -Wextra -fPIC -shared \
-Wl,-soname,libver.so.1 \
-o libver.so.1.0.0 libver.c
# Buat symlink: libver.so.1 -> libver.so.1.0.0
ln -sf libver.so.1.0.0 libver.so.1
# Buat symlink: libver.so -> libver.so.1 (untuk link time)
ln -sf libver.so.1 libver.so
# Link executable memakai linker name libver.so
gcc -std=c11 -Wall -Wextra -o ver_main ver_main.c -L. -lver -Wl,-rpath,.
./ver_main
# Periksa dependency
ldd ver_main | grep libverOutput yang Dihasilkan:
Versi: libver 1.0.0
3 + 4 = 7ldd ver_main | grep libver:
libver.so.1 => ./libver.so.1 (0x00007f74cfbe8000)objdump -p libver.so.1.0.0 | grep SONAME:
SONAME libver.so.1Mari kita telaah baris-baris kode di atas:
- Flag
-Wl,-soname,libver.so.1: Menyematkan SONAMElibver.so.1ke dalam binary.so. SONAME disimpan di header ELF. Saat executable di-link, linker menyalin SONAME (bukan real namelibver.so.1.0.0) ke tabel dependency. - Real name
libver.so.1.0.0: File binary sebenarnya. Konvensi:lib<name>.so.<major>.<minor>.<patch>. Bila Anda merilis patch1.0.1, file menjadilibver.so.1.0.1tetapi SONAME tetaplibver.so.1—tidak ada perubahan ABI. - Symlink
libver.so.1: Symlink dari SONAME ke real name. Dynamic loader mencari nama SONAME (libver.so.1) di filesystem, dan symlink menunjuk ke file binary aktual. Update ke1.0.1cukup mengganti symlink—executable tidak perlu recompile. - Symlink
libver.so: Linker name untuk link time saja. Saat Anda compile program baru dengan-lver, linker mencarilibver.so. Setelah link, executable tidak peduli denganlibver.so—hanya butuhlibver.so.1saat runtime. Karena itu,libver.sotidak perlu ada di mesin pengguna akhir—cukuplibver.so.1dan real name. - Output
ldd(libver.so.1 => ./libver.so.1): Dynamic loader mencari SONAMElibver.so.1, menemukan symlink, dan memuatlibver.so.1.0.0(yang ditunjuk symlink). Path./berasal dari-rpath,.yang disematkan saat link.
Symbol Versioning dengan Version Script #
SONAME versioning hanya mengelola major version. Untuk granularitas lebih halus—misalnya melacak kapan sebuah fungsi ditambahkan—Linux mendukung symbol versioning. Setiap symbol diberi tag versi (misalnya LIBVER_1.0, LIBVER_1.2). Saat executable di-link, linker mencatat versi symbol yang dipakai; dynamic loader menolak memuat library yang tidak punya versi tersebut.
/* File: libver2.c - library dengan symbol versioning */
#include <stdio.h>
/* Versi 1.0 - API awal */
int ver_tambah(int a, int b) {
return a + b;
}
/* Versi 1.2 - API baru */
int ver_kali(int a, int b) {
return a * b;
}
const char *ver_versi(void) {
return "libver2 1.2.0";
}/* File: libver2.map - version script untuk symbol versioning */
LIBVER_1.0 {
global:
ver_tambah;
ver_versi;
local:
*;
};
LIBVER_1.2 {
global:
ver_kali;
} LIBVER_1.0;/* File: ver2_main.c - konsumsi libver2 */
#include <stdio.h>
extern int ver_tambah(int, int);
extern int ver_kali(int, int);
extern const char *ver_versi(void);
int main(void) {
printf("Versi: %s\n", ver_versi());
printf("3 + 4 = %d\n", ver_tambah(3, 4));
printf("3 * 4 = %d\n", ver_kali(3, 4));
return 0;
}Build dengan version script:
# Build dengan version script
gcc -std=c11 -Wall -Wextra -fPIC -shared \
-Wl,-soname,libver2.so.1 \
-Wl,--version-script,libver2.map \
-o libver2.so.1.2.0 libver2.c
ln -sf libver2.so.1.2.0 libver2.so.1
ln -sf libver2.so.1 libver2.so
# Verifikasi symbol versioning
objdump -T libver2.so.1.2.0 | grep ver_
# Link executable
gcc -std=c11 -Wall -Wextra -o ver2_main ver2_main.c -L. -lver2 -Wl,-rpath,.
./ver2_mainOutput yang Dihasilkan:
Versi: libver2 1.2.0
3 + 4 = 7
3 * 4 = 12objdump -T libver2.so.1.2.0 | grep ver_:
0000000000001120 g DF .text 000000000000000d LIBVER_1.0 ver_versi
000000000000110d g DF .text 0000000000000013 LIBVER_1.2 ver_kali
00000000000010f9 g DF .text 0000000000000014 LIBVER_1.0 ver_tambahMari kita telaah baris-baris kode di atas:
- Block
LIBVER_1.0 { global: ...; local: *; }: Versi symbol pertama.global:mendaftar symbol yang diekspor dengan tagLIBVER_1.0.local: *;menyembunyikan semua symbol lain—pola ini menggantikan kebutuhan__attribute__((visibility("hidden")))di setiap fungsi internal. - Block
LIBVER_1.2 { ... } LIBVER_1.0: Versi symbol kedua.ver_kaliditandaiLIBVER_1.2. Sintaks} LIBVER_1.0;menyatakan bahwaLIBVER_1.2bergantung padaLIBVER_1.0—library harus menyediakan kedua set symbol. - Hasil
objdump -T: Tiga symbol diekspor.ver_tambahdanver_versiber-tagLIBVER_1.0,ver_kaliber-tagLIBVER_1.2. Kolomg(global),DF(dynamic function), dan tag versi di kolom terakhir. - Bila executable di-link dengan library versi 1.2 tapi mencoba memakai
ver_kali: Linker menyalin tagLIBVER_1.2ke executable. Saat executable dijalankan dan library di sistem hanya versi 1.0 (tidak punyaLIBVER_1.2), dynamic loader menolak memuat dengan errorversion LIBVER_1.2 not found. Inilah keunggulan symbol versioning: error jelas di load time, bukan crash misterius di runtime. local: *;: Menyembunyikan semua symbol yang tidak eksplisit dinyatakan global. Tanpalocal: *, GCC mengekspor semua fungsi global termasuk helper internal. Polalocal: *adalah cara paling efektif menyembunyikan implementation detail.
Strategi Versioning: SemVer untuk ABI #
Konvensi semantic versioning (SemVer) memetakan nomor versi ke jenis perubahan:
| Perubahan | Major | Minor | Patch | SONAME harus berubah? |
|---|---|---|---|---|
| Bug fix tanpa perubahan API | - | - | 1.0.0 → 1.0.1 | Tidak |
| Tambah fungsi baru (ABI-compatible) | - | 1.0.1 → 1.1.0 | - | Tidak |
| Hapus / ubah signature fungsi | 1.4.2 → 2.0.0 | - | - | Ya, jadi libfoo.so.2 |
| Ubah layout struct | 1.4.2 → 2.0.0 | - | - | Ya, jadi libfoo.so.2 |
Aturan praktis:
- Patch release (
1.0.0→1.0.1): Bug fix, tidak ada perubahan API/ABI. Real name berubah; SONAME dan symbol version tetap. - Minor release (
1.0.1→1.1.0): Tambah fungsi baru. Tambah blockLIBVER_1.1di version script; SONAME tetaplibfoo.so.1. - Major release (
1.x→2.0.0): ABI breaking. Bump SONAME menjadilibfoo.so.2. Aplikasi lama yang di-link kelibfoo.so.1tetap dapat berjalan paralel—kedua versi library dapat hidup berdampingan di sistem.