glibc ABI Uyumluluğu ve Eski RHEL Sistemleri
Yeni bir Linux dağıtımında derlenen binary'nin eski RHEL üzerinde GLIBC_x.y not found ile çalışmaması çoğu zaman paket eksikliği değil, build-time ABI taban çizgisi problemidir.
Geliştirici makinesinde sorunsuz çalışan bir binary'yi eski bir RHEL sunucuya kopyaladığınızda tek satırlık bir hata bütün deployment'ı bitirebilir:
version 'GLIBC_2.xx' not found
Bu hata bana göre Linux deployment tarafındaki en öğretici örneklerden biridir. Kaynak kodunuzda yeni bir API kullanmamış olabilirsiniz. Compiler aynı olabilir. Yine de build aldığınız ortam binary'nin çalışabileceği en eski userspace'i fiilen belirler.
RHEL tabanlı kapalı ve uzun ömürlü sistemlerde çalışırken bunu "sunucuda eksik paket var" şeklinde okumak yerine ELF ve ABI seviyesinde ele almak gerekir.
glibc neden yalnız bir shared library değildir?
Linux uygulamalarının önemli bölümü libc.so.6 üzerinden glibc'ye bağlanır. Ancak dinamik linker yalnız fonksiyon adına bakmaz. glibc symbol versioning kullanır.
Binary içinde örneğin memcpy@GLIBC_2.14 benzeri bağımlılıklar oluşabilir. Hedef sistemde memcpy fonksiyonu bulunması tek başına yeterli değildir. İstenen symbol version da bulunmalıdır.
readelf --version-info ve objdump -T bu yüzden deployment incelemesinde değerlidir. Uygulamanın "glibc 2.x kullanıyor" demesi yerine hangi sembollerin hangi minimum sürümü istediğini görmek mümkündür.
Yeni sistemde build almak taban çizgisini yükseltir
Güncel bir Ubuntu veya Fedora üzerinde compile edilen executable, toolchain ve link edilen sistem library'leri üzerinden yeni symbol version'lara bağlanabilir.
Kaynak kodu eski API'lerle sınırlamak bunu otomatik olarak engellemez. Compiler'ın ürettiği çağrılar, startup object'leri veya transitif native dependency yeni glibc sembolleri getirebilir.
Bu nedenle "C++17 kullandım ama eski RHEL'de niye çalışmıyor?" sorusunun cevabı çoğu zaman dil standardında değildir. Dil standardı ile runtime ABI farklı katmanlardır.
En güvenli yöntem: en eski hedefte build
Backward compatibility yönü önemlidir. Eski glibc üzerinde derlenen binary'nin daha yeni glibc üzerinde çalışma ihtimali yüksektir; ters yön garanti değildir.
Bu yüzden desteklenen en eski Linux dağıtımı build baseline olarak seçilebilir.
Ben production hedefi eski RHEL olduğunda build container veya CI image'ının da o uyumluluk tabanını temsil etmesini tercih ederim. Geliştirici makinesindeki güncel distro üzerinden release üretmek kolaydır fakat deployment sürprizlerini artırır.
Burada container hedef runtime'ın yerini tutmaz; build toolchain'ini tekrarlanabilir hale getirir.
Sysroot ile kontrollü cross-build
Fiziksel veya virtual eski sistemde build almak her zaman pratik değildir. Alternatif olarak eski userspace header ve library'lerini içeren bir sysroot kullanılabilir.
Compiler/linker hedef glibc header'ları, startup object'leri, dynamic loader sözleşmesi ve hedef libstdc++/libgcc sürümlerine kontrollü biçimde yönlendirilir.
Bu yöntem yalnız -I ile eski header göstermekten daha kapsamlıdır. Yanlışlıkla host /usr/lib altındaki güncel library'nin link'e girmesi ABI tabanını tekrar yükseltebilir.
Build log'unda gerçek linker command line'ını görmek bu nedenle değerlidir.
Sorun yalnız glibc değildir
C++ uygulamalarında GLIBCXX_x.y ve CXXABI_x.y bağımlılıkları da ortaya çıkar. Bunlar libstdc++ tarafındadır.
Bir native executable için uyumluluk matrisi en az kernel syscall beklentileri, glibc, dynamic loader, libstdc++, libgcc, transitif native library'ler ve CPU instruction set'i katmanlarını içerir.
Son madde özellikle -march=native kullanıldığında önemlidir. Binary glibc açısından uyumlu olsa bile build makinesindeki AVX2/AVX-512 instruction'larını içerebilir ve eski CPU'da illegal instruction ile düşebilir.
Statik glibc linklemek neden sihirli çözüm değildir?
İlk refleks -static olabilir. glibc ile tamamen statik linkleme DNS/NSS, locale ve bazı sistem entegrasyonları nedeniyle beklenmeyen davranışlar üretebilir. Ayrıca güvenlik güncellemesi shared library değiştirilerek otomatik alınamaz; binary yeniden build edilmelidir.
Musl gibi farklı libc ile statik dağıtım ayrı bir stratejidir, fakat uygulamanın ve native dependency'lerin buna uygun olması gerekir.
"Statik linkledim, compatibility bitti" genellemesi güvenli değildir.
Java uygulaması da native dünyadan kaçamaz
Saf Java bytecode glibc'ye doğrudan link edilmiyor gibi görünür. Fakat JVM'nin kendisi native executable'dır. JNI library'leri, compression codec'leri, database driver'ın native bileşenleri veya ML runtime'ları ayrıca ABI bağımlılığı getirebilir.
Özellikle AI inference tarafında ONNX Runtime, CTranslate2, CUDA runtime veya özel C/C++ library kullanıldığında deployment matrisi tekrar native seviyeye iner.
Bu nedenle JAR'ın Java sürümünü bilmek tek başına yeterli değildir.
Release doğrulamasına ABI kontrolü eklemek
Ben eski Linux hedefleri olan projelerde release artifact'ına readelf -V, objdump -T, ldd --version, ldd ve readelf -l kontrollerini eklemeyi değerli buluyorum.
Ayrıca temiz hedef image üzerinde smoke test yapılmalıdır. Build makinesinde test etmek dependency leakage'i gizleyebilir.
Artifact'ın minimum desteklenen distribution üzerinde başlatılması, teorik compatibility yorumundan daha güçlü kanıttır.
ABI taban çizgisi bir proje gereksinimidir
"Linux destekleniyor" ifadesi üretim sistemi için yetersizdir. Hangi distribution ailesi, hangi minimum glibc, hangi CPU ISA ve hangi kernel tabanı destekleniyor açık olmalıdır.
Bu bilgi dokümantasyon ayrıntısı değil, derleme kararını belirleyen gereksinimdir.
Eski RHEL gibi uzun ömürlü sistemlerde doğru yaklaşım hedefi güncellemeye zorlamak değilse, build ortamını hedefin ABI sınırlarına göre kurmaktır. Binary compatibility problemi deployment sonunda değil, toolchain seçilirken çözülmelidir.
Kaynakça